feat: multi-city GTFS uploads, Google auth, changelog & announcement banner - #19
Open
faizm10 wants to merge 9 commits into
Open
feat: multi-city GTFS uploads, Google auth, changelog & announcement banner#19faizm10 wants to merge 9 commits into
faizm10 wants to merge 9 commits into
Conversation
…anner - Add GTFS upload pipeline: TypeScript processor (fflate unzip + CSV parse) derives 8 data files and uploads to Vercel Blob; background processing via next/server after() so upload returns immediately - New API routes: /api/gtfs/upload, /api/gtfs/process, /api/gtfs/feeds, /api/gtfs/feeds/[id], /api/gtfs/feeds/[id]/geojson-url - Gate uploads behind Google/GitHub auth (NextAuth v5); feeds scoped to signed-in user with ownership check on delete - Add gtfs_feeds table to Neon DB schema (Drizzle ORM) - feedLoader abstraction: resolves GO Transit (local) vs city (Blob) data sources; all API routes generalised with ?feed= param and per-feed caches - City route layer in Mapbox GL using GTFS route_color; FeedSelector moved into top nav bar with GO / City / Both mode switcher - /gtfs upload page with 3-state UI (idle → processing poll → feed list) - Dismissable announcement banner on home page linking to /changelog - /changelog page with full release history styled to match landing page - Add Changelog nav item to LandingHeader Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
- Replace buffered multipart upload (hit Next.js body limit) with client-side direct upload via @vercel/blob/client upload() - Add /api/gtfs/upload-token for auth-gated Blob token generation - Add /api/gtfs/register to create DB row + trigger processing after upload - Update /api/gtfs/process to accept blobUrl directly instead of listing blobs - Fix auth redirect loop: /gtfs no longer redirects server-side; shows inline sign-in gate when unauthenticated (passes authenticated prop to GtfsClient) Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- Pass user from server session to GtfsClient; show avatar + name in header - Sign-in page now redirects to callbackUrl (not '/') when already authenticated, so signing in from /gtfs brings the user back to /gtfs after OAuth Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…emory fixes - Map page now applies ?feed= and ?feedMode= params from /gtfs links (previously ignored) - Selecting a city feed auto-switches to city view; map zooms to the city's extent - Upload shows a real progress bar; instant client-side validation with friendly errors - Processing failures surface the stored error message; polling times out instead of spinning forever; stale processing feeds are flagged as stuck - Stale/deleted active feeds fall back to GO Transit with a toast - GTFS processor streams stop_times/shapes line-by-line and frees zip entries and decoded CSVs to cut peak memory on large city feeds Co-Authored-By: Claude Fable 5 <[email protected]>
…wnership - Remove auth requirement from upload-token and register; anonymous feeds store a null userId (column was already nullable — no migration) - New lib/localFeeds.ts tracks uploaded feed IDs in localStorage; /gtfs and the map's City Feeds dropdown query /api/gtfs/feeds?ids=<local ids> - Feeds API validates IDs as UUIDs (max 50) and still merges in account-owned feeds when a session exists - Delete allows anonymous feeds via their unguessable UUID; account-owned feeds still require the owning session - Drop the sign-in gate on /gtfs; note that feeds are remembered per browser Co-Authored-By: Claude Fable 5 <[email protected]>
…ead route - Delete the raw ZIP from Blob after processing (success or failure); it lives at feeds/raw/* which the feed-delete handler never covered - Replace the "dev-secret" fallback: /api/gtfs/process now refuses to run unconfigured in production (503) and register fails loudly before accepting an upload if GTFS_PROCESS_SECRET is missing - New FeedCache (TTL + LRU, GO Transit pinned) replaces unbounded module-level Maps in routes/simulation/stops/variant-stops/gtfsRawIndex - Remove unused server-side /api/gtfs/upload route; process endpoint now requires blobUrl instead of falling back to a blob listing Co-Authored-By: Claude Fable 5 <[email protected]>
- Drop onUploadCompleted from the upload-token handler: it makes Vercel Blob call back into the deployment before the browser upload resolves, which hangs at ~99% on localhost and protected preview deployments - Enable multipart uploads (parallel parts + per-part retries) for large ZIPs - Trigger /api/gtfs/process via the deployment's own VERCEL_URL so previews don't call another environment; send the protection-bypass header when configured; mark the feed failed if the trigger errors instead of leaving it spinning - Raise register maxDuration to 300s to cover the awaited processing call Co-Authored-By: Claude Fable 5 <[email protected]>
Comment on lines
36
to
37
| const { callbackUrl } = await searchParams; | ||
| const callback = callbackUrl ?? "/"; |
There was a problem hiding this comment.
Open redirect for already-authenticated users
redirect(callback) uses callbackUrl directly from the URL query string without any origin check. A crafted link like /auth/signin?callbackUrl=https://evil.com silently forwards an already-signed-in visitor to the attacker's domain, which is a classic phishing vector. (NextAuth's own signIn() with redirectTo does validate the origin, but this direct redirect() call does not.)
Suggested change
| const { callbackUrl } = await searchParams; | |
| const callback = callbackUrl ?? "/"; | |
| const { callbackUrl } = await searchParams; | |
| // Only allow same-origin redirects — reject absolute URLs and protocol-relative paths. | |
| const callback = | |
| callbackUrl && callbackUrl.startsWith("/") && !callbackUrl.startsWith("//") | |
| ? callbackUrl | |
| : "/"; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
fflate+ custom CSV parser) derives 8 data files (routes, variants, stops, schedules, departures, simulation trips) and streams them to Vercel Blob with background processing viaafter()so the upload API returns in ~5s regardless of feed sizesession.user.idwith ownership check on delete;gtfs_feedstable added to Neon DBroute_color;FeedSelectordropdown with clear upload CTA/changelog/changelogwith full release history styled to match landing page; nav link added toLandingHeaderNew files
lib/gtfsProcessor.tslib/feedLoader.tslib/feedColors.tsroute_colorfieldapp/api/gtfs/uploadapp/api/gtfs/processapp/api/gtfs/feedsapp/api/gtfs/feeds/[id]/geojson-urlapp/gtfs/page.tsxcomponents/overlays/FeedSelector.tsxhooks/useFeed.tsapp/changelog/page.tsxcomponents/marketing/AnnouncementBanner.tsxAPI changes
All existing API routes (
/api/routes,/api/stops,/api/departures,/api/simulation,/api/variant-stops) now accept an optional?feed=<uuid>param. Omitting it defaults to"gotransit"— no breaking change.Test plan
?feed=param)/changelogpage renders correctly🤖 Generated with Claude Code
Greptile Summary
This PR adds multi-city GTFS feed uploads (client-side Blob upload →
/api/gtfs/register→after()-triggered processing), auth-gated feeds scoped tosession.user.id, a feed mode switcher on the map, a changelog page, and a dismissable announcement banner. The existing GO Transit API routes have been refactored to accept an optional?feed=<uuid>param, backed by a newFeedCacheLRU/TTL class and afeedLoaderabstraction.GtfsClientuploads directly to Vercel Blob, thenPOST /api/gtfs/registerinserts a DB row and usesafter()to asynchronously call/api/gtfs/process; the upload-token and register routes have complementary auth handling./routes,/stops,/departures,/simulation,/variant-stops) now accept?feed=and route throughresolveFeedSource→FeedCache.FeedSelectornav dropdown,useFeedhook with localStorage persistence, announcement banner, and/changelogpage.Confidence Score: 4/5
Safe to merge after fixing the unvalidated callbackUrl redirect in the sign-in page.
The GTFS pipeline, feed-scoped API routes, caching layer, and auth-gating are all well-structured. The one concrete defect is the sign-in page redirecting already-authenticated users to whatever URL is in the query string — a crafted link can silently forward a signed-in visitor to an external domain.
client/app/auth/signin/page.tsx — the unvalidated callbackUrl redirect needs a same-origin guard before this merges.
Security Review
client/app/auth/signin/page.tsxlines 36–40):callbackUrlfrom the query string is used verbatim inredirect(callback)for already-authenticated users, enabling an attacker to redirect signed-in visitors to an external domain via a crafted link.Important Files Changed
Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant Browser participant UploadToken as /api/gtfs/upload-token participant BlobCDN as Vercel Blob participant Register as /api/gtfs/register participant DB as Neon DB participant Process as /api/gtfs/process Browser->>UploadToken: POST (get upload token) UploadToken-->>Browser: signed token Browser->>BlobCDN: PUT feeds/raw/timestamp-file.zip BlobCDN-->>Browser: blobUrl Browser->>Register: "POST { blobUrl, cityName }" Register->>DB: "INSERT gtfs_feeds (status=processing)" Register-->>Browser: "{ feedId, status: processing }" Note over Register: after() runs post-response Register->>Process: "POST x-gtfs-secret + { feedId, blobUrl }" Process->>BlobCDN: fetch raw ZIP BlobCDN-->>Process: ZIP bytes Process->>Process: processGtfsFeed() — parse GTFS, derive 8 JSON files Process->>BlobCDN: "PUT feeds/feedId/*.json + geojson" Process->>DB: "UPDATE status=ready, blobBaseUrl, routeCount, stopCount" Process->>BlobCDN: DELETE raw ZIP Process-->>Register: 200 OK Browser->>DB: "poll /api/gtfs/feeds?ids=..." DB-->>Browser: "{ feeds: [{ status: ready }] }"%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant Browser participant UploadToken as /api/gtfs/upload-token participant BlobCDN as Vercel Blob participant Register as /api/gtfs/register participant DB as Neon DB participant Process as /api/gtfs/process Browser->>UploadToken: POST (get upload token) UploadToken-->>Browser: signed token Browser->>BlobCDN: PUT feeds/raw/timestamp-file.zip BlobCDN-->>Browser: blobUrl Browser->>Register: "POST { blobUrl, cityName }" Register->>DB: "INSERT gtfs_feeds (status=processing)" Register-->>Browser: "{ feedId, status: processing }" Note over Register: after() runs post-response Register->>Process: "POST x-gtfs-secret + { feedId, blobUrl }" Process->>BlobCDN: fetch raw ZIP BlobCDN-->>Process: ZIP bytes Process->>Process: processGtfsFeed() — parse GTFS, derive 8 JSON files Process->>BlobCDN: "PUT feeds/feedId/*.json + geojson" Process->>DB: "UPDATE status=ready, blobBaseUrl, routeCount, stopCount" Process->>BlobCDN: DELETE raw ZIP Process-->>Register: 200 OK Browser->>DB: "poll /api/gtfs/feeds?ids=..." DB-->>Browser: "{ feeds: [{ status: ready }] }"Reviews (2): Last reviewed commit: "fix: unstick uploads hanging at 99% and ..." | Re-trigger Greptile