cartwright
Features

Authentication & accounts

Every sign-in method (magic link, password, GitHub, Google), password rules, the forgot/reset flow, and the iron rule that admin is never granted via OAuth.

Cartwright uses NextAuth v5 (Auth.js), configured in lib/auth.ts + lib/auth.config.ts. This page is the authoritative reference for all sign-in methods and account management. For a magic-link deep-dive see Auth (magic link).

Setting up a brand-new shop? See Sign in for the first time — on a fresh install (no Resend key) the login page shows only the password tab, and that password is generated by the seed into .admin-credentials.

Sign-in methods

MethodForGate
Magic linkcustomers + adminsdefault; Resend (RESEND_API_KEY), falls back to .mail-previews/ in dev
Password (credentials)customers + adminsalways wired; uses User.passwordHash
Continue with GitHubcustomersgithubAuth flag + GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET
Continue with GooglecustomersgoogleAuth flag + GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET

The credentials provider compares with a constant-time dummy-hash fallback so a wrong email and a wrong password take the same time (no timing oracle).

Admin is never granted via OAuth. Admin is a database role. GitHub/Google sign-in always create or link a customer. The only path to admin is the DB role (set during the setup wizard or via Prisma Studio) — you cannot accidentally create an admin-via-OAuth bypass.

Passwords

  • Minimum 12 characters, validated by validatePasswordStrength() (lib/auth/password.ts).
  • The seed generates a strong random admin password (no hardcoded default); the new owner is forced to change it on first login at /admin/konto (User.mustChangePassword).
  • Customers set or change a password at /account/settings; magic-link-only accounts can set one via the reset flow.

Forgot / reset password

The flow lives in lib/auth/password-reset.ts:

/account/forgot-passwordrequestPasswordReset(email) emails a link (Resend).
The link opens /account/reset-passwordconsumePasswordResetToken() validates and sets the new password.

Security properties:

  • No account-existence leak — the request returns the same response whether or not the email exists.
  • HMAC-SHA256 token peppered with AUTH_SECRET; only the hash is stored.
  • 1-hour TTL, single-use (usedAt stamped on consume).
  • A magic-link-only account can use this flow to set its first password.

Environment

AUTH_SECRET=            # required — signs sessions + peppers reset tokens & API keys
GITHUB_CLIENT_ID=       # optional — Continue with GitHub
GITHUB_CLIENT_SECRET=
GOOGLE_CLIENT_ID=       # optional — Continue with Google (customer sign-in, T1)
GOOGLE_CLIENT_SECRET=
RESEND_API_KEY=         # optional in dev (.mail-previews fallback); required in prod

GOOGLE_CLIENT_ID/SECRET (customer sign-in) are separate from GOOGLE_OAUTH_CLIENT_ID/SECRET (the server-side Google Workspace connector for Sheets/Drive/Docs). Different OAuth client, different purpose.

Rotating AUTH_SECRET invalidates every active session and API key.

On this page