Authentication (Dashboard)
Dashboard login uses NextAuth v4 with the credentials provider and JWT sessions.
Files
src/lib/auth/options.ts— NextAuth config.src/app/api/auth/[...nextauth]/route.ts— NextAuth catch-all.src/app/api/auth/register/route.ts— signup.middleware.ts— gate for/overview/*.src/app/providers.tsx— clientSessionProvider.src/app/(auth)/login/page.tsx,register/page.tsx— UI.
Registration flow
POST /api/auth/register:
{ "name": "Jane", "email": "jane@acme.com", "password": "at-least-8" }
Creates:
usersrow withbcrypt.hash(password, 12).organizationsrow — name"{name}'s Org", slug from email prefix + last 6 chars of orgId.org_membersrow withrole: "owner".
All three inserts run sequentially — no transaction wrapper (improvement candidate).
Login flow
CredentialsProvider.authorize():
- Look up user by email.
bcrypt.compare(password, user.passwordHash).- Return
{ id, email, name }on success, elsenull.
JWT callback
On first sign-in:
- Set
token.userId = user.id. - Look up user's first
org_members.orgId(multi-org unsupported currently). - Set
token.orgId.
Session callback
session.user.id = token.userIdsession.user.orgId = token.orgId
Type augmentation in src/lib/auth/options.ts extends the NextAuth Session and JWT interfaces.
Dashboard gate
middleware.ts runs on /overview/:path*:
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
if (!token) return redirect("/login?callbackUrl=...");
Dashboard layout.tsx double-checks with getServerSession and redirects if absent — defense in depth.
OAuth providers
Google and GitHub providers are conditionally registered when their env pairs are set:
GOOGLE_CLIENT_ID+GOOGLE_CLIENT_SECRETGITHUB_CLIENT_ID+GITHUB_CLIENT_SECRET
First-time OAuth sign-in calls findOrCreateUser(email, name):
- If the email exists → return the existing user id (OAuth links to the existing account).
- Else create user + organization + org_member (owner) in a single transaction and mark email verified.
2FA
Credentials provider accepts an optional totp field. If the user has two_factor_secrets.enabled = true:
- Missing totp →
throw new Error("2FA_REQUIRED")— client should re-prompt. - TOTP code (validates via
verifyTotp()) OR one of the backup codes unlocks sign-in. - Used backup codes are filtered out of the stored array on successful login.
See two-factor-auth.md for setup flow.
Email verification & password reset
See email-verification.md and password-reset.md.
Multi-org
See team.md. Active org is tracked by cookie (sendoka_active_org) — no re-login needed to switch.
Remaining gaps
- JWT still captures first orgId at sign-in — server components using
session.user.orgIddirectly see that default, not the cookie-overridden active org. Migrate togetActiveOrgId()helper as needed. - No session invalidation on password reset.
- No WebAuthn / passkeys.