feat(website): sync user with backend on login, use internal user ID#1202
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
42de45d to
d0613f3
Compare
d0613f3 to
02444de
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
This PR updates the website’s authentication + ownership model to use a backend-issued internal numeric user ID (synced on GitHub login) instead of the GitHub user ID, and uses that to display a human-readable owner name on collection detail pages.
Changes:
- Sync GitHub users to the backend on login (
POST /users/sync) and store the returned internal ID (gsUserId) in the session; propagategsUserIdvia middleware and backend proxy. - Switch collection/subscription ownership logic to use numeric internal user IDs (
ownedBy: number), and resolve/display owner name viaGET /users/{id}. - Update unit/browser/E2E tests and fixtures for the new numeric ownership ID model.
Reviewed changes
Copilot reviewed 20 out of 21 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| website/src/auth.ts | Sync user on GitHub login and store internal gsUserId in the session |
| website/src/middleware/authMiddleware.ts | Copy gsUserId into Astro.locals and switch locals “logged out” to undefined |
| website/src/env.d.ts | Update locals typing (undefined + add gsUserId) |
| website/src/backendApi/backendProxy.ts | Forward internal numeric user id as userId query param |
| website/src/backendApi/backendService.ts | Add getUser() for owner name resolution |
| website/src/types/PublicUser.ts | New Zod schema/type for public user response |
| website/src/types/Collection.ts | Change ownedBy from string to number |
| website/src/pages/api/users/[id].ts | Add unauthenticated proxy route for public user lookup |
| website/src/pages/collections/[organism]/[id]/index.astro | Redirect handling + owner name lookup + internal ID ownership check |
| website/src/pages/collections/[organism]/[id]/edit.astro | Use internal gsUserId for ownership checks |
| website/src/components/collections/detail/CollectionDetail.tsx | Display resolved owner name (string) |
| website/src/pages/collections/[organism]/index.astro | Switch “logged in” checks to user !== undefined |
| website/src/pages/collections/[organism]/create.astro | Switch “logged in” checks to user !== undefined |
| website/src/pages/subscriptions/index.astro | Switch “logged in” checks to user !== undefined |
| website/src/pages/subscriptions/create.astro | Switch “logged in” checks to user !== undefined |
| website/src/layouts/base/header/HamburgerMenu.astro | Switch “logged in” checks to user !== undefined |
| website/src/backendApi/backendService.spec.ts | Update fixture to numeric ownedBy |
| website/src/components/collections/overview/CollectionsOverview.browser.spec.tsx | Update fixtures to numeric ownedBy |
| website/tests/helpers/auth.ts | Update E2E session cookie payload to use gsUserId |
| website/tests/collections/collectionForm.spec.ts | Sync E2E user via backend and use internal id for collection operations |
| website/tests/collections/collectionDetail.spec.ts | Sync E2E user via backend and use internal id for collection operations |
Comments suppressed due to low confidence (1)
website/src/pages/collections/[organism]/[id]/index.astro:83
- Because the server code above now returns early when
collection === undefined, this render path can never hit the fallback branch. Simplify the template by removing the now-unreachable conditional/fallback (or revert the early redirect if you still want to show a “failed to load” message).
lapisConfig={lapisConfig}
isOwner={userIsOwner}
ownerName={ownerName}
client:load
/>
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
37
to
+41
| logger.error(`Failed to fetch collection ${id}: ${getErrorLogMessage(error)}`); | ||
| } | ||
|
|
||
| const collectionTitle = collection !== undefined ? `#${id} ${collection.name}` : `Collection #${id}`; | ||
| if (collection === undefined) { | ||
| return Astro.redirect('/404'); |
808d0b4 to
ebdb834
Compare
02444de to
0fb948b
Compare
0fb948b to
5e761b2
Compare
fengelniederhammer
approved these changes
May 19, 2026
…for ownership
On GitHub login, mapProfileToUser calls POST /users/sync to upsert the user
in the backend and stores the returned internal Long ID as internalUserId in
the better-auth session (stateless JWE cookie). This replaces the previous
use of the GitHub ID for ownership checks on collections and subscriptions.
- auth.ts: async mapProfileToUser syncs user, adds internalUserId additional field
- authMiddleware.ts: reads internalUserId from session user
- backendProxy.ts: forwards internalUserId as userId query param instead of githubId
- Collection.ts: ownedBy changed from string to number to match backend Long
- PublicUser.ts: new Zod schema for GET /users/{id} response
- backendService.ts: adds getUser() to resolve owner names
- pages/api/users/[id].ts: new proxy route for public user lookup
- collection detail/edit pages: use internalUserId for ownership, display owner name
- E2E tests: sync user via POST /users/sync in beforeAll to get internal ID
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(website): fix lint errors in auth.ts and authMiddleware.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
refactor(website): rename internalUserId to gsUserId
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(website): make login fail if backend user sync fails, simplify gsUserId handling
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(website): make ownerName non-optional, simplify collection detail page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
refactor(website): simplify gsUserId assignment in authMiddleware
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
foo
better-auth serialises all session fields as strings in the JWE cookie regardless of the declared field type, so comparing gsUserId (string) against collection.ownedBy (number) with === always returned false. Fix by coercing gsUserId to a number in the middleware, and align the e2e auth cookie helper to use the real backend user ID instead of a hardcoded '1'. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5038d51 to
33dad68
Compare
Co-authored-by: Fabian Engelniederhammer <92720311+fengelniederhammer@users.noreply.github.com>
better-auth types profile.name as string, but GitHub can return null at runtime. Cast to string | null | undefined so the ?? fallback to profile.login is not flagged as unnecessary. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ability The linter sees through `as` casts, so the rule fires even with the cast to string | null | undefined. Use eslint-disable to document that the ?? chain is intentional — GitHub can return null for name at runtime despite the better-auth types declaring it as string. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fhennig
added a commit
that referenced
this pull request
May 19, 2026
… IDs, use in website code (#1201) * feat(backend): add users table with GitHub sync and internal IDs Adds a users_table (BIGSERIAL PK, nullable github_id) with upsert via POST /users/sync and lookup via GET /users/{id}. Migrates owned_by and user_id columns in collections and subscriptions from GitHub ID strings to BIGINT foreign keys referencing the new table. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * format * refactor(backend): extract now() utility, add PublicUser DTO for GET /users/{id} Extracts the shared now() instant helper into util/InstantProvider.kt to avoid duplication across models. Adds PublicUser DTO (id + name only) so the public GET /users/{id} endpoint does not expose email. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(backend): apply linter formatting to CollectionModel and UsersControllerTest Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(backend): restore nullValue import in UsersControllerTest Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(e2e): sync user before creating collections in tests Collections now require a Long internal user ID. Tests previously passed string GitHub IDs directly, causing 400/500 errors. Now each test suite syncs the user via POST /users/sync first and uses the returned internal ID. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * feat(backend): validate user existence before creating or listing subscriptions/collections Raises a 404 NotFoundException when an unknown userId is passed to postSubscriptions, getSubscriptions, createCollection, or getCollections, instead of silently creating orphaned records or returning empty results. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(backend): recreate indexes on owned_by and user_id after bigint migration V1.3 drops the old varchar columns (and their indexes) and renames the new bigint FK columns into place. PostgreSQL does not auto-index FK referencing columns, so the indexes must be recreated explicitly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(e2e): extract shared backend setup helpers for collection tests Moves repeated user sync and collection create/delete API calls into a backendClient.ts helper, reducing boilerplate in beforeAll/afterAll across collectionForm and collectionDetail specs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(website): sync user with backend on login, use internal user ID (#1202) * feat(website): sync user with backend on login, use internal user ID for ownership On GitHub login, mapProfileToUser calls POST /users/sync to upsert the user in the backend and stores the returned internal Long ID as internalUserId in the better-auth session (stateless JWE cookie). This replaces the previous use of the GitHub ID for ownership checks on collections and subscriptions. - auth.ts: async mapProfileToUser syncs user, adds internalUserId additional field - authMiddleware.ts: reads internalUserId from session user - backendProxy.ts: forwards internalUserId as userId query param instead of githubId - Collection.ts: ownedBy changed from string to number to match backend Long - PublicUser.ts: new Zod schema for GET /users/{id} response - backendService.ts: adds getUser() to resolve owner names - pages/api/users/[id].ts: new proxy route for public user lookup - collection detail/edit pages: use internalUserId for ownership, display owner name - E2E tests: sync user via POST /users/sync in beforeAll to get internal ID Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> fix(website): fix lint errors in auth.ts and authMiddleware.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> refactor(website): rename internalUserId to gsUserId Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> fix(website): make login fail if backend user sync fails, simplify gsUserId handling Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> fix(website): make ownerName non-optional, simplify collection detail page Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> refactor(website): simplify gsUserId assignment in authMiddleware Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> foo * fix(website): fix collection edit button never showing for owners better-auth serialises all session fields as strings in the JWE cookie regardless of the declared field type, so comparing gsUserId (string) against collection.ownedBy (number) with === always returned false. Fix by coercing gsUserId to a number in the middleware, and align the e2e auth cookie helper to use the real backend user ID instead of a hardcoded '1'. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Update website/src/auth.ts Co-authored-by: Fabian Engelniederhammer <92720311+fengelniederhammer@users.noreply.github.com> * fix(website): cast profile.name to nullable to satisfy linter better-auth types profile.name as string, but GitHub can return null at runtime. Cast to string | null | undefined so the ?? fallback to profile.login is not flagged as unnecessary. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(website): suppress no-unnecessary-condition for profile.name nullability The linter sees through `as` casts, so the rule fires even with the cast to string | null | undefined. Use eslint-disable to document that the ?? chain is intentional — GitHub can return null for name at runtime despite the better-auth types declaring it as string. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Fabian Engelniederhammer <92720311+fengelniederhammer@users.noreply.github.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Fabian Engelniederhammer <92720311+fengelniederhammer@users.noreply.github.com>
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.
resolves #1190
Summary
mapProfileToUsercallsPOST /users/syncto upsert the user in the backend and stores the returned internal ID asgsUserIdin the better-auth session (stateless JWE cookie)GET /users/{id}and displayed on the collection detail pageChanges
Auth & middleware
auth.ts:mapProfileToUsersyncs user with backend, storesgsUserId(number); throws on failure to abort loginauthMiddleware.ts: readsgsUserIdfrom session intoAstro.localsbackendProxy.ts: forwardsgsUserIdasuserIdquery paramTypes & API
types/Collection.ts:ownedBychanged fromstringtonumberto match backendLongtypes/PublicUser.ts: new Zod schema forGET /users/{id}responsebackendService.ts: addsgetUser()to resolve owner names server-sidepages/api/users/[id].ts: new proxy route for public user lookupCollection pages
ownerNameis always astringgsUserIdfor ownership checkTests
ownedByfrom string to number in fixturesPOST /users/syncinbeforeAllto obtain internal ID for collection ownershipNotes
This branch depends on the backend changes in
feat/user-table(users table,POST /users/sync,GET /users/{id}endpoints).🤖 Generated with Claude Code