Skip to content

feat(website): sync user with backend on login, use internal user ID#1202

Merged
fhennig merged 5 commits into
feat/user-tablefrom
feat/user-frontend
May 19, 2026
Merged

feat(website): sync user with backend on login, use internal user ID#1202
fhennig merged 5 commits into
feat/user-tablefrom
feat/user-frontend

Conversation

@fhennig
Copy link
Copy Markdown
Contributor

@fhennig fhennig commented May 6, 2026

resolves #1190

Summary

  • On GitHub login, mapProfileToUser calls POST /users/sync to upsert the user in the backend and stores the returned internal ID as gsUserId in the better-auth session (stateless JWE cookie)
  • If the sync fails, login is aborted (no silent fallback)
  • Replaces the use of the GitHub ID for ownership checks — collections and subscriptions now use the internal numeric user ID throughout
  • Owner name is resolved via GET /users/{id} and displayed on the collection detail page

Changes

Auth & middleware

  • auth.ts: mapProfileToUser syncs user with backend, stores gsUserId (number); throws on failure to abort login
  • authMiddleware.ts: reads gsUserId from session into Astro.locals
  • backendProxy.ts: forwards gsUserId as userId query param

Types & API

  • types/Collection.ts: ownedBy changed from string to number to match backend Long
  • types/PublicUser.ts: new Zod schema for GET /users/{id} response
  • backendService.ts: adds getUser() to resolve owner names server-side
  • pages/api/users/[id].ts: new proxy route for public user lookup

Collection pages

  • Detail page: fetches and displays owner name (falls back to numeric ID on error); ownerName is always a string
  • Edit page: uses gsUserId for ownership check

Tests

  • Unit/browser specs: fix ownedBy from string to number in fixtures
  • E2E tests: sync E2E user via POST /users/sync in beforeAll to obtain internal ID for collection ownership

Notes

This branch depends on the backend changes in feat/user-table (users table, POST /users/sync, GET /users/{id} endpoints).

🤖 Generated with Claude Code

@vercel
Copy link
Copy Markdown

vercel Bot commented May 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dashboards Ready Ready Preview, Comment May 19, 2026 9:22am

Request Review

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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; propagate gsUserId via middleware and backend proxy.
  • Switch collection/subscription ownership logic to use numeric internal user IDs (ownedBy: number), and resolve/display owner name via GET /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 thread website/src/auth.ts Outdated
Comment thread website/tests/helpers/auth.ts Outdated
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');
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 22 changed files in this pull request and generated 2 comments.

Comment thread website/src/pages/collections/[organism]/[id]/index.astro
Comment thread website/src/backendApi/backendService.ts
Comment thread website/src/auth.ts Outdated
fhennig and others added 2 commits May 19, 2026 10:44
…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>
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 fhennig merged commit 21dde36 into feat/user-table May 19, 2026
10 checks passed
@fhennig fhennig deleted the feat/user-frontend branch May 19, 2026 09:44
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants