diff --git a/app/(auth)/auth/callback/__tests__/route.test.ts b/app/(auth)/auth/callback/__tests__/route.test.ts index 97b1588a..a226da60 100644 --- a/app/(auth)/auth/callback/__tests__/route.test.ts +++ b/app/(auth)/auth/callback/__tests__/route.test.ts @@ -57,7 +57,7 @@ describe('GET /auth/callback: recovery flow', () => { expect(exchangeCodeForSession).toHaveBeenCalledWith('xyz') }) - it('redirects to /login?error=auth_error when the recovery OTP is expired or already consumed', async () => { + it('tags a failed recovery link with flow=recovery so the login page shows reset copy', async () => { verifyOtp.mockResolvedValue({ error: { message: 'Token has expired or is invalid' } }) const request = new NextRequest( @@ -66,6 +66,22 @@ describe('GET /auth/callback: recovery flow', () => { const response = await GET(request) expect(response.status).toBe(307) - expect(response.headers.get('location')).toBe('http://localhost:3000/login?error=auth_error') + expect(response.headers.get('location')).toBe( + 'http://localhost:3000/login?error=auth_error&flow=recovery' + ) + }) + + it('tags a failed signup confirmation (PKCE code, no type/next) with flow=signup', async () => { + exchangeCodeForSession.mockResolvedValue({ + error: { message: 'code verifier missing' }, + }) + + const request = new NextRequest('http://localhost:3000/auth/callback?code=xyz') + const response = await GET(request) + + expect(response.status).toBe(307) + expect(response.headers.get('location')).toBe( + 'http://localhost:3000/login?error=auth_error&flow=signup' + ) }) }) diff --git a/app/(auth)/auth/callback/route.ts b/app/(auth)/auth/callback/route.ts index d06571b3..1808b409 100644 --- a/app/(auth)/auth/callback/route.ts +++ b/app/(auth)/auth/callback/route.ts @@ -164,24 +164,31 @@ export async function GET(request: NextRequest) { .maybeSingle() if (!teamMembership) { - // Create team via service client (RPC requires auth.uid() which isn't available here) - const serviceClient = createServerClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.SUPABASE_SERVICE_ROLE_KEY!, - { cookies: { getAll: () => [], setAll: () => {} } } - ) + // Create team via service client (RPC requires auth.uid() which isn't available here). + // Non-fatal: a failure here must not turn a successfully confirmed session into a + // 500 that reads as "signup verification failed". The dashboard / onboarding path + // recreates the silent team when it is missing, so log and continue. + try { + const serviceClient = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY!, + { cookies: { getAll: () => [], setAll: () => {} } } + ) - const teamId = crypto.randomUUID() - await serviceClient.from('teams').insert({ - id: teamId, - name: 'Personal', - created_by: user.id, - }) - await serviceClient.from('team_members').insert({ - team_id: teamId, - user_id: user.id, - role: 'owner', - }) + const teamId = crypto.randomUUID() + await serviceClient.from('teams').insert({ + id: teamId, + name: 'Personal', + created_by: user.id, + }) + await serviceClient.from('team_members').insert({ + team_id: teamId, + user_id: user.id, + role: 'owner', + }) + } catch (err) { + console.error('[auth/callback] silent team creation failed:', err) + } } // Always redirect to dashboard: it handles zero-company and incomplete states @@ -198,6 +205,16 @@ export async function GET(request: NextRequest) { return response } - // Authentication failed: redirect to login with error - return NextResponse.redirect(new URL('/login?error=auth_error', origin)) + // Authentication failed: redirect to login with error. Forward a coarse + // flow hint so the login page can show the right copy: a failed signup + // confirmation must not be framed as a failed password reset. On the PKCE + // (?code=) path there is no `type`, so recovery is identified by the + // next=/reset-password marker that resetPasswordForEmail sets; everything + // else defaults to the signup/confirmation framing. + const failedFlow = + type === 'recovery' || next === '/reset-password' ? 'recovery' : 'signup' + const loginUrl = new URL('/login', origin) + loginUrl.searchParams.set('error', 'auth_error') + loginUrl.searchParams.set('flow', failedFlow) + return NextResponse.redirect(loginUrl) } diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index c67bbf0c..e1146fd9 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -43,6 +43,7 @@ function LoginPageContent() { const router = useRouter() const searchParams = useSearchParams() const callbackError = searchParams.get('error') + const callbackFlow = searchParams.get('flow') const supabase = createClient() const bankIdEnabled = isBankIdEnabled() const tAuth = useTranslations('auth') @@ -385,20 +386,33 @@ function LoginPageContent() {
{callbackError === 'auth_error' && (
-

- {tAuth('callback_error_title')} -

-

- {tAuth('callback_error_body')}{' '} - - . -

+ {callbackFlow === 'recovery' ? ( + <> +

+ {tAuth('callback_error_title')} +

+

+ {tAuth('callback_error_body')}{' '} + + . +

+ + ) : ( + <> +

+ {tAuth('callback_error_title_signup')} +

+

+ {tAuth('callback_error_body_signup')} +

+ + )}
)} {bankIdEnabled && ( diff --git a/messages/en.json b/messages/en.json index 86981f19..1b8b2f29 100644 --- a/messages/en.json +++ b/messages/en.json @@ -276,6 +276,8 @@ "callback_error_title": "The reset link did not work", "callback_error_body": "The link has expired or has already been used.", "request_new_reset_link": "Request a new reset link", + "callback_error_title_signup": "The confirmation link did not work", + "callback_error_body_signup": "The link may have expired, already been used, or been opened in a different browser than the one you signed up in. If you have already confirmed your email, sign in below; otherwise you can register again.", "bankid_no_account_greeting": "Hi {name}!", "bankid_no_account_body": "We did not find an account linked to your BankID. Sign in with email below and then link BankID in settings.", "bankid_no_account_create": "Or create a new account", diff --git a/messages/sv.json b/messages/sv.json index faa1f70e..7a56ada0 100644 --- a/messages/sv.json +++ b/messages/sv.json @@ -276,6 +276,8 @@ "callback_error_title": "Återställningslänken fungerade inte", "callback_error_body": "Länken har gått ut eller använts redan.", "request_new_reset_link": "Begär en ny återställningslänk", + "callback_error_title_signup": "Bekräftelselänken fungerade inte", + "callback_error_body_signup": "Länken kan ha upphört, redan använts, eller öppnats i en annan webbläsare än den du registrerade dig i. Har du redan bekräftat din e-post loggar du in nedan, annars kan du registrera dig igen.", "bankid_no_account_greeting": "Hej {name}!", "bankid_no_account_body": "Vi hittade inget konto kopplat till ditt BankID. Logga in med e-post nedan och koppla sedan BankID i inställningar.", "bankid_no_account_create": "Eller skapa ett nytt konto",