From c5cd5c7418e70dfce9dc2901ec15066d4980992f Mon Sep 17 00:00:00 2001 From: Guido X Jansen Date: Sat, 28 Feb 2026 14:34:12 +0100 Subject: [PATCH] fix(auth): upsert user row on OAuth callback before profile sync (#101) The auth callback relied on profileSync.syncProfile() to create the user row, but that method only does an UPDATE -- silently no-oping for first-time logins. Now the callback upserts the user row (with DID and handle) before firing profile sync, so the row exists for the subsequent profile UPDATE and all downstream queries. Fixes barazo-forum/barazo-workspace#XX --- src/routes/auth.ts | 10 ++++++++++ tests/unit/routes/auth.test.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/routes/auth.ts b/src/routes/auth.ts index 7aa36b0..906deff 100644 --- a/src/routes/auth.ts +++ b/src/routes/auth.ts @@ -9,6 +9,7 @@ import { FALLBACK_SCOPE, hasCrossPostScopes, } from '../auth/scopes.js' +import { users } from '../db/schema/users.js' import { userPreferences } from '../db/schema/user-preferences.js' // --------------------------------------------------------------------------- @@ -131,6 +132,15 @@ export function authRoutes(oauthClient: NodeOAuthClient): FastifyPluginCallback // (PLC directory lookup with Valkey cache + DB fallback) const handle = await handleResolver.resolve(did) + // Ensure user row exists (first login creates, subsequent logins update handle) + await app.db + .insert(users) + .values({ did, handle }) + .onConflictDoUpdate({ + target: users.did, + set: { handle, lastActiveAt: new Date() }, + }) + const session = await sessionService.createSession(did, handle) // Fire-and-forget profile sync from PDS (never blocks auth flow) diff --git a/tests/unit/routes/auth.test.ts b/tests/unit/routes/auth.test.ts index 9b50cd1..55f87ac 100644 --- a/tests/unit/routes/auth.test.ts +++ b/tests/unit/routes/auth.test.ts @@ -5,6 +5,7 @@ import type { FastifyInstance } from 'fastify' import type { SessionService, SessionWithToken, Session } from '../../../src/auth/session.js' import type { Env } from '../../../src/config/env.js' import { authRoutes } from '../../../src/routes/auth.js' +import { users } from '../../../src/db/schema/users.js' import type { HandleResolver } from '../../../src/lib/handle-resolver.js' import { BARAZO_BASE_SCOPES, @@ -314,6 +315,32 @@ describe('auth routes', () => { expect(body.error).toBe('Invalid callback parameters') }) + it('upserts user row in database on successful callback', async () => { + const mockSession = makeMockSessionWithToken() + const mockOAuthSession = { did: TEST_DID } + + callbackFn.mockResolvedValueOnce({ + session: mockOAuthSession, + state: 'some-state', + }) + resolveFn.mockResolvedValueOnce(TEST_HANDLE) + createSessionFn.mockResolvedValueOnce(mockSession) + + await app.inject({ + method: 'GET', + url: '/api/auth/callback?iss=https://pds.example.com&code=test-code&state=test-state', + }) + + // Verify user row was upserted (insert with onConflictDoUpdate) + expect(dbInsertFn).toHaveBeenCalledWith(users) + expect(dbValuesFn).toHaveBeenCalledWith( + expect.objectContaining({ did: TEST_DID, handle: TEST_HANDLE }) + ) + expect(dbOnConflictDoUpdateFn).toHaveBeenCalledWith( + expect.objectContaining({ target: users.did }) + ) + }) + it('redirects to frontend with error when OAuth client throws', async () => { callbackFn.mockRejectedValueOnce(new Error('Token exchange failed')) -- 2.51.2