Files
accounted/app/api/clients/landing/route.ts
T
MattssonandClaude Fable 5 a860c690ed feat(white-label): WL-14 cockpit landing for BankID and OAuth/magic-link logins (#1972)
* feat(white-label): WL-14 cockpit landing for BankID and OAuth/magic-link logins

Byra staff logging in via BankID or the Google/magic-link callback on
their brand domain landed on /select-company resp. / instead of the
cockpit, because those two paths bypassed the WL-14 landing rule.

- Extract the rule into resolveLandingDestination
  (lib/company/landing-server.ts) so server code can call it without an
  HTTP round-trip; /api/clients/landing becomes a thin wrapper.
- Auth callback: with no explicit destination, AAL1 sessions resolve the
  landing from the request host, degrading to / on any failure
  (MFA-enrolled users already get the rule via /mfa/verify).
- BankID login: byra staff on their brand host get /clients; everyone
  else keeps the deliberate /select-company picker byte-identically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(white-label): address PR 1972 review findings

- /api/clients/landing: requireAuth() directly instead of
  withRouteContext, which 4xxed byra staff without a company of their
  own (COMPANY_CONTEXT_MISSING) and silently sent the cockpit's primary
  persona to /select-company. MFA enforcement unchanged.
- landing-server: log the byra membership query error before degrading
  to '/' so a persistent failure is distinguishable from no membership.
- Deduplicate the clientWithTeamMembership test mock to file scope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(white-label): paginate the byra membership query

fetchAllRows per repo convention: PostgREST silently caps unpaginated
selects at 1000 rows, which could hide a qualifying owner/admin
membership. Errors still degrade to '/' with a log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 13:37:20 +02:00

31 lines
1.3 KiB
TypeScript

import { NextResponse } from 'next/server'
import { requireAuth } from '@/lib/auth/require-auth'
import { resolveLandingDestination } from '@/lib/company/landing-server'
/**
* GET /api/clients/landing
*
* Post-login landing decision (WL-14): thin HTTP wrapper around
* resolveLandingDestination (lib/company/landing-server.ts), for the client
* auth surfaces (login and MFA-verify pages) that cannot call it in-process.
* The helper carries the whole rule, including the owner/admin role gate
* (2026-08-27). Called when no explicit destination was requested; any
* failure degrades to '/' at the caller.
*
* Uses requireAuth() directly (the sanctioned withRouteContext opt-out, MFA
* still enforced) because the decision needs no active company. Byrå staff
* without a company of their own are the cockpit's primary persona and must
* still land on /clients; withRouteContext would 4xx them with
* COMPANY_CONTEXT_MISSING.
*/
export async function GET(request: Request) {
const auth = await requireAuth()
if (auth.error) return auth.error
const { user, supabase } = auth
const host =
request.headers.get('x-forwarded-host') ?? request.headers.get('host') ?? ''
const destination = await resolveLandingDestination(supabase, user.id, host)
return NextResponse.json({ data: { destination } })
}