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
@@ -93,6 +93,7 @@ export default function OnboardingJourney({
|
||||
const [narration, setNarration] = useState<string | null>(null)
|
||||
const [monogram, setMonogram] = useState<string | null>(null)
|
||||
const [dupName, setDupName] = useState<string | null>(null)
|
||||
const [dupElsewhere, setDupElsewhere] = useState(false)
|
||||
|
||||
const station = stationOfStep(state.step)
|
||||
const entity = state.settings.entity_type
|
||||
@@ -128,6 +129,7 @@ export default function OnboardingJourney({
|
||||
return
|
||||
}
|
||||
setDupName(null)
|
||||
setDupElsewhere(false)
|
||||
dispatch({ type: 'ORG_SUBMITTED', orgNumber: raw })
|
||||
fetchCompanyLookup(raw, { ticEnabled }).then((outcome) => {
|
||||
dispatch({ type: 'LOOKUP_RESULT', outcome })
|
||||
@@ -137,6 +139,7 @@ export default function OnboardingJourney({
|
||||
if (!res.ok) return
|
||||
const { data } = await res.json()
|
||||
setDupName(data?.companies?.[0]?.name ?? null)
|
||||
setDupElsewhere(Boolean(data?.exists_elsewhere))
|
||||
})
|
||||
.catch(() => {})
|
||||
},
|
||||
@@ -695,6 +698,16 @@ export default function OnboardingJourney({
|
||||
{t('journey_dup_note', { name: dupName })}
|
||||
</span>
|
||||
) : null}
|
||||
{!dupName && dupElsewhere && station === 0 ? (
|
||||
// Cross-account duplicate (#1231): the same org number
|
||||
// already exists under another Accounted account. Shown
|
||||
// only when there is no own-account match, which is the
|
||||
// more specific hint.
|
||||
<span className="jny-f is-on is-warn" style={{ transitionDelay: `${lookupFacts.length * 150}ms` }}>
|
||||
{lookupFacts.length > 0 ? ' · ' : ''}
|
||||
{t('journey_dup_elsewhere_note')}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user