ea45e9dc2ff14f874f0146deb9e63be0e2d43f6e
18
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6ac9679fb5 |
feat(auth): base available login methods off GoTrue providers (#1869)
* feat(auth): base login fields on GoTrue providers Signed-off-by: Goostaf <gasplund2@gmail.com> # Conflicts: # app/(auth)/login/login-client.tsx # app/(auth)/register/page.tsx * fix: address feedback Signed-off-by: Goostaf <gasplund2@gmail.com> * chore: remove hardcoded Google enabled checks Signed-off-by: Goostaf <gasplund2@gmail.com> # Conflicts: # .env.example * feat: show label when password login is disabled Signed-off-by: Goostaf <gasplund2@gmail.com> * feat: use MicrosoftMark, correct comment Signed-off-by: Goostaf <gasplund2@gmail.com> * feat: display custom providers Signed-off-by: Goostaf <gasplund2@gmail.com> * feat: show when no methods are available Signed-off-by: Goostaf <gasplund2@gmail.com> * feat: display custom provider labels Signed-off-by: Goostaf <gasplund2@gmail.com> * feat: add SAML login path Signed-off-by: Goostaf <gasplund2@gmail.com> * feat: show when no methods are available Signed-off-by: Goostaf <gasplund2@gmail.com> * fix: display SAML button when enabled Signed-off-by: Goostaf <gasplund2@gmail.com> * fix: preserve nextPath and broken key Signed-off-by: Goostaf <gasplund2@gmail.com> * fix: redirect test to client Signed-off-by: Goostaf <gasplund2@gmail.com> * fix: expose registerEnabled Signed-off-by: Goostaf <gasplund2@gmail.com> * feat: provider allowlist and request timeout Signed-off-by: Goostaf <gasplund2@gmail.com> * fix: restore compact labels Signed-off-by: Goostaf <gasplund2@gmail.com> * refactor: move withTimeout implementation to utils Signed-off-by: Goostaf <gasplund2@gmail.com> * fix: include nextPath Signed-off-by: Goostaf <gasplund2@gmail.com> * fix: export function and test case Signed-off-by: Goostaf <gasplund2@gmail.com> * feat: only show SAML button if vars configured Signed-off-by: Goostaf <gasplund2@gmail.com> * fix(auth): map SAML sign-in error through getErrorMessage The antipattern ratchet (check:guards, raw-user-error) rejects a raw error.message reaching a user-visible sink. Route the signInWithSSO error through getErrorMessage like the other auth error paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013BAzJjXQBa9F5L1U42wUMj Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> * feat(auth): GitHub brand mark on the provider button; decision log GitHub allows its invertocat in solid black/white, so currentColor is correct; custom OIDC providers keep the generic key icon. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013BAzJjXQBa9F5L1U42wUMj Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> --------- Signed-off-by: Goostaf <gasplund2@gmail.com> Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
341d61131a |
fix(auth): email-change recovery re-send and confirmation feedback (#2034)
* fix(auth): email-change recovery re-send and confirmation feedback A half-completed secure email change was a dead end: the pending-address short-circuit in /api/account/email swallowed every retry without re-sending mails, so once the confirmation links expired the user could never recover, and confirmation clicks landed on the dashboard with no feedback at all. - /api/account/email: only short-circuit a repeat request while the pending mails are fresh (30 min); a stale pending change falls through to GoTrue, which restarts the change and re-sends both mails - /auth/callback: type=email_change now redirects to a status page (/auth/email-change) that says whether one click remains, the change is complete, or the link was dead, instead of landing silently - auth mail templates: both email-change mails explain that two mails are sent and both links must be clicked - settings: the save button re-enables for the pending address as Skicka igen, so users can trigger the re-send themselves Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018sbGMZQE5W7KfSVFjK7E4p * fix(auth): exempt email-change confirmations from the authenticated /auth bounce (skeptic findings) - middleware: let /auth/email-change and /auth/callback?type=email_change through for authenticated users; the bounce to / swallowed confirmation clicks before verifyOtp ran (pre-existing since #2017) - email-change done page resolves the WL-14 landing destination for the CTA - /api/account/email returns resent flag; settings toast says mails were already sent instead of claiming a fresh send Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018sbGMZQE5W7KfSVFjK7E4p --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
52e99295de |
fix(white-label): accept byrå-team invites before landing, so admins reach /clients (#2002)
A newly-invited byrå admin/member who signed up with email+password landed on /onboarding instead of the cockpit. Root cause: team-invite acceptance lived only in POST /api/team/accept, which the email-confirmation signup flow never reaches before the dashboard (no session for the register page's client-side accept), while the auth callback and the onboarding/select-company recovery only understood company_invitations. So the invitee's byrå membership did not exist when landing resolved, and they were funneled into creating a company. - New shared helper acceptPendingTeamInviteByToken (lib/company/pending-invites) is the single server-side implementation of team-invite acceptance. - POST /api/team/accept delegates to it; HTTP contract unchanged. - /auth/callback accepts a team invite BEFORE the silent-team check and before resolveLandingDestination runs, so an owner/admin resolves to /clients; the invite cookie is cleared on success, kept otherwise for the retry. - acceptPendingInviteByToken (onboarding/select-company recovery) tries the company path, then falls back to the team helper. - hasPendingInviteForEmail checks both invite tables, so a tokenless byrå invitee is not misread as a first-timer. No migration (team invite tables already exist). Company-invite and non-invite flows are untouched. Claude-Session: https://claude.ai/code/session_01ByL5dQXG8gGLtNBPj8g2C4 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
a717f03898 |
feat(mcp-oauth): let an account with no company connect and sign up from the OAuth popup (#1814 PR 1) (#1855)
* feat(mcp-oauth): let an account with no company connect and sign up from the OAuth popup Identity unlock for agent-first onboarding (#1814, shape B+). A person with no Accounted account can now connect from an MCP client, create the account inside the Connect popup and finish the OAuth dance. - authorize/token no longer require a company: consent renders a companyless variant and the key is minted with company_id NULL. - validateApiKey returns companyId string|null and binds an unbound key to the user's first company on the first validation after it exists. - MCP server: company-dependent tools and data resources answer with a structured NO_COMPANY_YET error; the company-independent tools still run; telemetry skips when there is no company scope. - /api/events fails closed instead of throwing for an unbound key. - authorize forces TOTP enrollment (not just verification) for password accounts with no factor, since the middleware skips enrollment for zero-company users; BankID-linked accounts stay exempt. - /login forwards next to /register; register, GoogleAuthButton and /auth/callback carry it back to the consent page (callback honours only /api/mcp-oauth/authorize, via safeReturnTo); /mfa/enroll hard-navigates to /api/* destinations like /mfa/verify. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 * refactor(company): move getActiveCompanyId out of the next/headers module lib/auth/api-keys.ts needs the resolver for unbound-key binding, but lib/company/context.ts imports next/headers for the legacy company cookie and Turbopack refuses that import on some of api-keys' import paths (the preview build failed). The resolver and CompanyContextError now live in lib/company/active-company.ts; context.ts re-exports them so every caller and test mock is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 * fix(mcp-oauth): fail closed on a failed assurance lookup; enroll Back aborts instead of looping Review findings on #1855: requireAal2 let consent through at AAL1 when getAuthenticatorAssuranceLevel() returned nothing and a verified factor existed. Only a positive AAL2 answer passes now; a failed lookup and the inconsistent verified-factor-at-AAL1 case both step up to /mfa/verify. Back on /mfa/enroll with the consent page as returnTo went straight back into the redirect loop; it now aborts to the app. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
28b58aedc4 |
feat(auth): sign in with Google behind NEXT_PUBLIC_GOOGLE_AUTH_ENABLED (#1441)
Adds a 'Continue with Google' button to login and register, gated by NEXT_PUBLIC_GOOGLE_AUTH_ENABLED so it ships dark until the Google provider is configured in Supabase. The OAuth round-trip reuses the existing /auth/callback PKCE exchange, which already owns MFA routing, invite acceptance and silent-team creation. A flow=oauth marker on the redirect lets the callback tag failures (including provider consent denials) so the login page shows Google-specific error copy instead of the email-confirmation framing. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2296c0cd59 |
fix(auth): provision invitees server-side when signups are disabled (#1404)
* fix(auth): provision invitees server-side when signups are disabled Self-hosted installations with GoTrue disable_signup broke the invite flow silently: invitees without an account were routed to /register, where supabase.auth.signUp fails with "Signups not allowed for this instance", surfaced only as a generic toast. New server-only env flag AUTH_SIGNUPS_DISABLED (documented in .env.example) mirrors the GoTrue setting. When true, POST /api/company/members/invite checks check_email_exists and, for invitees without an account, provisions one via auth.admin.inviteUserByEmail with a redirect back to /invite/<token>, before the Resend email and before the invitation row is written so a provisioning failure leaves nothing half-created and the admin can retry. The response now carries user_provisioned alongside email_sent, and a provisioning failure returns 502 with a Swedish message mapped through getErrorMessage instead of a silently-successful invite. /auth/callback now routes type=invite verifications to /reset-password (the existing set-password surface) instead of dropping the passwordless user on the dashboard, and preserves the invite token from next=/invite/<token> as the pre-auth invite cookie so the existing reset-password invite handoff accepts the membership right after the password is saved. getErrorMessage learns two GoTrue patterns: "Signups not allowed" (account creation closed on this installation, contact your inviter or administrator) so the /register dead end is explained even for flows that bypass provisioning, and "Error sending ... email" (GoTrue SMTP not configured) so the 502 above is actionable. Hosted is untouched: the flag is unset there and every new code path is gated on it. Fixes #1335 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(auth): restore check_email_exists RPC and harden self-host invite config Adversarial review of #1404 found that the check_email_exists function the invite flow depends on does not exist anywhere: it shipped in PR #229 and was lost in the #244 migration consolidation before ever reaching prod (verified missing on the hosted production database directly). Today app/api/team/accept destructures only { data } from the RPC call, so alreadyHasAccount is silently null on every deployment and the invite page routes even existing-account invitees toward /register. - New migration 20260804140000 restores the function exactly as originally shipped: SECURITY DEFINER over auth.users, EXECUTE revoked from PUBLIC, anon and authenticated, granted to service_role only (prevents email enumeration). Fixes hosted prod behavior too once applied. - New tests/pg/check-email-exists.pg.test.ts locks in existence, case-insensitive matching, false-for-unknown, and the role grants. - .env.docker.example gains the AUTH_SIGNUPS_DISABLED block self-hosters actually use; both env templates now note that the GoTrue redirect URI allow-list must include /invite/* or the invite email redirect silently falls back to SITE_URL. - Invite route test for the existsError branch: RPC failure logs a warning and provisioning proceeds anyway (GoTrue is authoritative). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(auth): mask invitee email in provisioning-failure log (#1335) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
15300aa8e2 |
fix(invites): accept invite on BankID signup, recover missed invites on onboarding surfaces (#1157)
An invited user who registered via BankID was funneled into creating a company instead of joining the one they were invited to: the register page's BankID path never processed the gnubok-invite-token cookie (unlike the login, MFA-verify, and auth-callback paths). Observed in production 2026-07-24. - register: BankID signup now accepts the pending invite before routing to /select-company, mirroring the login page's BankID path. - lib/company/pending-invites: acceptPendingInviteByToken retries a missed acceptance from the cookie (pending + unexpired + email match, same rules as POST /api/team/accept); hasPendingInviteForEmail detects a stranded invitee whose cookie is gone. - /onboarding and /select-company retry acceptance from the cookie and redirect to the dashboard on success, making the auth callback's long-promised fallback real; with no cookie but a pending invitation, both surfaces show a 'join via the link in the invitation email' hint instead of silently asking the invitee to create a company. - No new accept path without the token: the hint deliberately points back to the mailed link, so mailbox possession stays required and no company name is leaked to unverified emails. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d704714eef |
fix(auth): show confirmation-specific copy when a signup link fails (#1027)
A failed email-verification link redirected to /login?error=auth_error with no flow context, so the login page framed every callback failure as a broken password-reset link and pushed new users into a reset form for an account that was never confirmed. The callback now forwards a coarse flow hint (recovery vs signup); the login page renders confirmation copy without the reset CTA for the signup case. The new copy names the likely cause (link opened in a different browser than signup, or a one-time token consumed by a mail scanner) instead of only "expired or already used". Silent-team creation is also wrapped in try/catch so a transient insert failure cannot turn an otherwise-successful first-time confirmation into a 500. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
185b6f799c |
fix(build): pin turbopack.root, update comment for middleware to proxy rename (#897)
Pins turbopack.root to the project directory so a parent-directory lockfile cannot make Turbopack misdetect the workspace root, and updates a stale comment reference (middleware.ts is now proxy.ts). The rename itself already landed on main. |
||
|
|
ec27228a8e |
style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
Em dashes (—) and en dashes (–) had spread across comments, docs, tests, and a few UI strings, reading as AI-generated boilerplate rather than house style. Replaced each with punctuation matching its context: colon for explanatory clauses, comma for asides, plain hyphen for numeric/legal ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for paired-dash asides. messages/en.json and messages/sv.json were fixed by hand together to keep sv/en in sync. Left untouched where the dash is the functional subject rather than decorative punctuation: date-range-parser.ts's separator regex, charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the agent system-prompt files that already instruct against em dashes, and a golden iXBRL test fixture compared byte-for-byte. Also fixes two bugs surfaced along the way: an off-by-one in ApiKeysPanel's scope-label split (a leftover from an earlier partial pass), and a charset-repair test that had lost the literal en-dash it exists to verify. Regenerated the agent atom seed migration (skills:generate) since 27 SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes, with an explicit carve-out for the functional-dash cases above. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
4253afc343 |
fix(company): validate user_preferences write when switching company (#708)
Fixes #701. setActiveCompany upserted active_company_id without checking the result, then set the gnubok-company-id cookie unconditionally. A failed write — including an RLS-filtered UPDATE, which affects zero rows without raising an error — looked like a successful switch: switchCompany returned {}, the UI hard-reloaded, and middleware (which reads user_preferences, not the cookie) resolved the old company. - setActiveCompany now verifies the upsert with .select().single() and throws a typed CompanyContextError ('not_member' | 'persist_failed'); the cookie is only set after the write is confirmed, so it can no longer diverge from the database. - switchCompany logs the failure and returns distinct error codes instead of reporting every failure as a permissions problem. - CompanySwitcher now shows a destructive toast on failure (it previously failed with no feedback); BankIdCompanyPicker translates the codes. Messages added to sv/en under company_switcher and select_company. - The remaining fire-and-forget user_preferences writers (middleware fallback write-back, team invite accept, auth callback invite accept) now check and log errors; non-fatal by design since each has a working fallback path. - New tests cover every failure mode, including cookie-not-set on a failed write and the silent zero-row write caught by the read-back. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cd96e5ec26 |
feat(api): v1 invoice :mark-paid + :credit action verbs (Phase 2 PR-B-2b combined) (#455)
* feat(api): v1 invoice :mark-paid + :credit action verbs (Phase 2 PR-B-2b combined)
Bigger PR per the user's request. Lands the remaining two journal-entry-
centric action verbs together — they share the same lifecycle pattern
established in :mark-sent (idempotent, dry-runnable, scope-gated,
warnings on partial-state failures).
POST /api/v1/companies/:companyId/invoices/:id/mark-paid
- Books a payment against a sent / overdue invoice. Updates status to
paid (or partially_paid when remaining_amount > 0). Three booking paths:
- Faktureringsmetoden (accrual default): Debit 1930 / Credit 1510 via
createInvoicePaymentJournalEntry — settles AR.
- Kontantmetoden (cash): Debit 1930 / Credit revenue + Credit VAT via
createInvoiceCashEntry — revenue recognition happens HERE under cash.
- Custom lines (partial payment): caller-supplied balanced journal lines
via createJournalEntry directly. Validated for balance (sum debits ==
sum credits, both > 0) → 400 INVOICE_PAID_LINES_UNBALANCED otherwise.
- Optional body: { payment_date?, exchange_rate_difference?, lines? }
- Race-condition guard: status update matches .in(['sent','overdue',
'partially_paid']) so a concurrent payment returns 409 INVOICE_PAID_RACE.
- Emits invoice.paid (new event type, added to lib/events/types.ts with
paymentAmount + paymentDate in the payload).
POST /api/v1/companies/:companyId/invoices/:id/credit
- Issues a kreditfaktura against a sent / paid / overdue invoice
(ML 17 kap 22–23§). Creates a NEW invoice row with:
- invoice_number = "KR-<original>"
- credited_invoice_id = original id
- status = 'sent'
- All amounts negated (subtotal, vat_amount, total, items quantities/totals)
- Items mirror the original with negated values; inserted in a separate
step with company-scoped rollback DELETE on failure.
- Flips original invoice to status='credited'. Warns ORIGINAL_NOT_FLIPPED
if the flip fails (the credit note still exists; operator reconciles).
- Posts reverse journal entry via createCreditNoteJournalEntry (accrual
only; cash basis defers to refund time).
- Emits credit_note.created (existing event in the bus).
Both endpoints:
- Use the established wrapper + Idempotency-Key + dry-run + warnings
pattern from :mark-sent.
- Validate document_type (no delivery_notes), credited_invoice_id (no
recursive credits), and status before any mutation.
- Use explicit column projections (no SELECT *).
- Sanitize pg_message from client responses (kept in logs).
- Emit error-level logs on partial-state failures + surface warnings to
the caller via meta.warnings.
Event types union (lib/events/types.ts) gains invoice.paid; credit
uses the existing credit_note.created event.
URL convention: plain /verb subpaths (e.g. /invoices/:id/mark-paid),
consistent with :mark-sent. Stripe/QuickBooks pattern, not the
AIP-style :verb that Next.js routing fights.
17 new tests covering happy paths (accrual + cash for mark-paid),
custom-lines balance validation, dry-run preview, document-shape
guards, scope, idempotency, race conditions, and credit-of-credit /
delivery-note rejection.
3194/3194 vitest pass; build clean; lint clean on v1 paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(api): address PR #455 review + include password-recovery fixes
PR #455 review fixes:
- Greptile P1 (CLAUDE.md architecture rule): API routes that emit events
via eventBus must call ensureInitialized() at module level to wire
extension event handlers. Neither :mark-paid (invoice.paid) nor :credit
(credit_note.created) had it — nor did the already-merged :mark-sent,
POST /invoices, POST /customers, etc. Fixed once at the wrapper layer:
ensureInitialized() now runs at module import of lib/api/v1/with-api-v1.ts,
so EVERY v1 route gets the init at import time. Single source of truth
prevents future routes from forgetting (idempotent guard makes the
repeated call safe). Cleaner than per-route copy of the call.
- Swarm PI1.3 (low): 0.005 epsilon in mark-paid was undocumented. Added
a comment explaining: after rounding to 2 decimals, newRemaining is in
steps of 0.01; values ≤ half-an-öre only arise from float artefacts.
Pushing back (recurring triage, consistent with prior PRs):
- V8.2.1 + CC6.3 × 4 "ctx.companyId vs params.companyId mismatch" —
impossible by construction. The wrapper sets ctx.companyId FROM the URL
params after the membership check. They are guaranteed equal.
- V2.3 + A.8.15 + A.8.28 atomicity / floating-point / partial-failure
alerts — same architectural / cross-surface deferred work as prior PRs;
matches internal /api/invoices pattern precisely.
- V4.5 account_number allowlist — engine validates it.
- V2.4 idempotency TOCTOU — wrapper handles via DB unique constraint.
- Art.5(1)(f) PII in logs, A.8.11 dry-run preview scope, A.8.15 partial-
failure naming, test scope coverage — all recurring triage.
Password-recovery flow fixes (included per request — pre-existing
working-tree changes the user authored):
- app/(auth)/auth/callback/route.ts: when the callback exchanges a
recovery token (type='recovery' or next='/reset-password'), redirect
directly to /reset-password instead of running onboarding/MFA/
dashboard checks. Previously users clicking the password-reset email
got bounced through onboarding.
- lib/supabase/middleware.ts: /reset-password no longer bounces
authenticated users to / (the recovery flow lands here with a fresh
session by design — the user is *supposed* to call updateUser({
password }) on this page).
- app/(auth)/login/page.tsx: shows an error banner when ?error=auth_error
is set (expired/used recovery link), with a button to request a new
one. Wrapped the page in <Suspense> because useSearchParams() now
forces dynamic rendering (Next.js 16 static-prerender bail-out
otherwise).
- app/(auth)/auth/callback/__tests__/route.test.ts: new test file
covering the recovery callback path.
3197/3197 vitest pass (3194 prior + 3 from the new auth-callback tests).
Build clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(api): mark-paid uses remaining_amount as default payment, not total
Real correctness fix from Swedish-compliance review on PR #455. When no
customLines is supplied, mark-paid previously defaulted paymentAmount to
typed.total. Combined with the race-condition guard that allows the
status UPDATE to flip a partially_paid invoice to paid, this could
over-credit AR in a race scenario:
1. Invoice in 'sent' status, total=12500, remaining=12500.
2. Concurrent partial payment lands first → status='partially_paid',
remaining=7500.
3. The full-payment request's pre-flight saw 'sent' and passed; its
UPDATE matches partially_paid (race guard allows it). With the old
logic the journal entry was for total=12500 against an AR balance
of only 7500 — a 5000 over-credit.
Using remaining_amount as the default eliminates this. Same end state
in the common case (no prior partial); correct booking in the race.
3197/3197 vitest pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
bf5a8d9195 |
Fix/multiple company (#203)
* Refactor onboarding and dashboard logic; add silent team creation for users - Removed unnecessary useCompany context in DashboardContent and SettingsSidebar components. - Simplified onboarding setup logic to allow direct access to the dashboard for users without companies. - Introduced WelcomeOnboarding component to handle user onboarding steps. - Added migration to create silent teams for all users at signup, backfilling existing users without teams, and cleaning up incomplete companies. * fix: update greeting logic and improve email handling in TIC extension * Redirect to onboarding for users without companies and update onboarding flow * Build issue fix * Enhance onboarding experience by adding existing companies check |
||
|
|
0dd1f5ebc1 |
feat: multi-tenant company refactor (GNU-19) (#153)
* feat: multi-tenant company refactor (GNU-19) Introduce companies table, company_members, and user_preferences to support multiple companies per user. All data scoping changes from user_id to company_id across the entire codebase. Key changes: - Database migration: new tables, company_id on 40+ tables, backfill, RLS rewrite from user_id to company-member-based, updated RPCs - Types: Company, CompanyMember, CompanyRole, UserPreferences types; company_id added to all entity interfaces; companyId on all events - Engine: all 7 core functions take companyId; storno, period, year-end services updated; 16 report generators updated - Middleware: company context resolution (cookie → prefs → first company) - API routes: ~120 routes updated with requireCompanyId() - Frontend: CompanyProvider context, layout/dashboard/onboarding updated - Extensions: context factory, 9 extensions, all lib files updated - Tests: 1880 tests passing, all helpers updated with company_id defaults Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add database migrations for multi-tenant company and team system (GNU-19) Adds company_invitations, company creation RPC, team_members, account deletion RPC, and teams table refactor migrations. Updates base multi-tenant migration with cascading FKs and onboarding_step column. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add team types and update core infrastructure for multi-tenancy (GNU-19) Adds TeamRole, MemberSource, and Team types. Refactors Supabase service client to be stateless, updates middleware for team-aware routing, extends CompanyContext with team/role fields, and updates extension service types to accept companyId. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: thread company_id through business logic functions (GNU-19) Replaces user_id scoping with company_id across all lib modules: bookkeeping, documents, transactions, invoices, reconciliation, tax, deadlines, and import. Updates corresponding tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: thread company_id through API routes and extensions (GNU-19) Updates all existing API routes to extract and pass companyId. Updates enable-banking and arcim-migration extensions for company-scoped transaction ingestion and sync. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add company and team management API routes (GNU-19) Adds CRUD endpoints for company members, company invitations, team members, and team invitations. Includes invite token utilities, email templates, and company switch server action. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add team/company UI components, pages, and dashboard updates (GNU-19) Adds CompanySwitcher, ConsultantEmptyState, Step0RoleChoice, company members and team management panels. Updates dashboard layout for team-aware routing, onboarding for multi-step role choice, and auth callback for team invite acceptance. Ignores supabase/.branches/. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add null guards for company in import page (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: move appUrl declaration to outer scope in invite route (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add optional chaining for company.name in members section (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add optional chaining for second company.name in members section (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add null guards for company in extension components (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: pass companyId to executeSIEImport in arcim-migration extension (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update tests to use companyId instead of userId and improve type handling --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
928a145f9a |
feat: upgrade auth to email+password with optional TOTP MFA
- Replace magic-link-only login with email+password (primary) and magic link (toggle) - Add registration page with strong password validation - Add MFA enrollment (/mfa/enroll) with QR code and manual secret - Add MFA verification (/mfa/verify) with 6-digit TOTP input - Add password reset flow (/reset-password) - Add middleware MFA enforcement gated by NEXT_PUBLIC_REQUIRE_MFA env var - Self-hosted deployments (NEXT_PUBLIC_SELF_HOSTED=true) skip MFA entirely - Add Security tab in Settings for password change and MFA management - Add requireAuth() API route helper with MFA check - Update CLAUDE.md with Authentication section and env var docs - Update Dockerfile and docker-entrypoint.sh for new env var placeholders Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
6f6fbb19d0 | Merging | ||
|
|
a25d10a528 | Initial copy from influencer-biz |