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:
Jakob Wennberg
2026-07-27 17:00:15 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent ff205951b1
commit 46c0b72ab0
10 changed files with 557 additions and 11 deletions
+3
View File
@@ -1330,6 +1330,7 @@
"journey_ans_monthly": "Monthly",
"journey_ans_yearly": "Yearly",
"journey_dup_note": "You already have {name} in Accounted.",
"journey_dup_elsewhere_note": "This company already exists in Accounted. Do you have another account from before? Sign in there instead, or ask for an invitation.",
"journey_pending_invite_note": "You have a pending invitation to a company. Open the link in the invitation email to join instead.",
"journey_fact_vat": "VAT registered",
"journey_fact_ceased": "Deregistered",
@@ -5311,6 +5312,8 @@
"greeting_morning": "Good morning",
"greeting_day": "Good day",
"greeting_evening": "Good evening",
"other_account_hint": "This account is empty, but bookkeeping for the same organisation number exists in another Accounted account. Did you sign in with the wrong login?",
"other_account_hint_action": "Switch account",
"resume_title": "Continue",
"resume_invoice_draft": "Draft: invoice to {customer}",
"resume_invoice_unsent": "Send invoice {number}",
+3
View File
@@ -1330,6 +1330,7 @@
"journey_ans_monthly": "Månadsvis",
"journey_ans_yearly": "Årsvis",
"journey_dup_note": "Du har redan {name} i Accounted.",
"journey_dup_elsewhere_note": "Det här bolaget finns redan i Accounted. Har du ett annat konto sedan tidigare? Logga in där istället, eller be om en inbjudan.",
"journey_pending_invite_note": "Du har en väntande inbjudan till ett företag. Öppna länken i inbjudningsmejlet för att gå med istället.",
"journey_fact_vat": "Momsregistrerat",
"journey_fact_ceased": "Avregistrerat",
@@ -5311,6 +5312,8 @@
"greeting_morning": "God morgon",
"greeting_day": "God dag",
"greeting_evening": "God kväll",
"other_account_hint": "Det här kontot är tomt, men bokföring för samma organisationsnummer finns i ett annat Accounted-konto. Loggade du in med fel inloggning?",
"other_account_hint_action": "Byt konto",
"resume_title": "Fortsätt",
"resume_invoice_draft": "Utkast: faktura till {customer}",
"resume_invoice_unsent": "Skicka faktura {number}",