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'))