feat(auth): surface duplicate-account traps around BankID login (#1234)
* feat(auth): surface duplicate-account traps around BankID login Three escape hatches for the stale-duplicate-account trap (#1231, the Chillen support case): a user whose BankID resolves to an abandoned account got an empty app with no hint that their real bookkeeping lives in another account. - check-org-number: new exists_elsewhere signal (service role, reduced to one boolean) + a warn chip in the onboarding journey when the org number already exists in an account the user is not a member of. - Hem: one AttnLine under the greeting when the whole account has zero journal entries but a same-orgnr company elsewhere has real bookkeeping, with a sign-out action. Common case costs one indexed existence probe. - scripts/support/unlink-bankid.ts: dry-run-by-default support action that unlinks a BankID identity (delete + app_metadata clear + append-only SECURITY_EVENT audit_log row). Replaces the raw SQL used to resolve the original ticket. Closes #1231 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(auth): harden unlink script and paginate hint queries per review - other-account-hint: fetchAllRows() on both company listings (PostgREST 1000-row cap; byrå users can hold many memberships); the journal probes stay limit(1) existence checks. - unlink-bankid: audit_log row is written BEFORE the delete so a partial failure can never delete without a trace; context queries fail closed instead of rendering an unknown account as empty; stdout no longer prints the personnummer hash or ciphertext (the unsalted hash is brute-forceable over the personnummer space); record_id now carries the identity row id and the snapshot includes id + linked_at. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
ff205951b1
commit
46c0b72ab0
@@ -2,7 +2,10 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { createClient } from '@/lib/supabase/client'
|
||||
import { AttnLine } from '@/components/ui/attn-line'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { useCapability, useCompany } from '@/contexts/CompanyContext'
|
||||
@@ -30,6 +33,12 @@ interface DashboardContentProps {
|
||||
suggestedMatches: SuggestedMatch[]
|
||||
/** In-progress work for the Fortsätt pane (lib/worklist/resume). */
|
||||
resumeItems: ResumeItem[]
|
||||
/**
|
||||
* True when this account looks bookkeeping-empty while a same-orgnr
|
||||
* company with real bookkeeping exists in another account (#1231): the
|
||||
* user probably signed in with the wrong login (stale BankID account).
|
||||
*/
|
||||
otherAccountHint?: boolean
|
||||
onboardingProgress?: OnboardingProgress
|
||||
initialSetup: InitialSetupState
|
||||
/**
|
||||
@@ -55,6 +64,7 @@ export default function DashboardContent({
|
||||
worklist,
|
||||
suggestedMatches,
|
||||
resumeItems,
|
||||
otherAccountHint = false,
|
||||
onboardingProgress,
|
||||
initialSetup,
|
||||
agentBuilt = true,
|
||||
@@ -62,6 +72,15 @@ export default function DashboardContent({
|
||||
const t = useTranslations('dashboard')
|
||||
const hasAi = useCapability(CAPABILITY.ai)
|
||||
const { company } = useCompany()
|
||||
const router = useRouter()
|
||||
|
||||
// Wrong-account hint action: sign out so the user can come back in with
|
||||
// their other login (email+password). Same flow as SandboxBanner.
|
||||
async function handleSwitchAccount() {
|
||||
const supabase = createClient()
|
||||
await supabase.auth.signOut()
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
// Time-of-day greeting (concept: "God morgon, Jakob."). Client-side clock
|
||||
// on purpose (the user's local morning, not the server's), captured once
|
||||
@@ -89,6 +108,14 @@ export default function DashboardContent({
|
||||
{dateLine}
|
||||
{company?.name ? ` · ${company.name}` : ''}
|
||||
</p>
|
||||
{otherAccountHint && (
|
||||
<AttnLine
|
||||
className="mt-3"
|
||||
action={{ label: t('other_account_hint_action'), onClick: handleSwitchAccount }}
|
||||
>
|
||||
{t('other_account_hint')}
|
||||
</AttnLine>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<NewUserChecklist
|
||||
|
||||
Reference in New Issue
Block a user