- Introduction
- Pitch
- Hogwarts
- Live Demo
- MVP
- Roadmap
- Launch Sprint
- PRD
- Get Started
- Localhost
- Architecture
- Structure
- Pattern
- Page
- Layout
- Content
- Types
- Config
- Actions
- Queries
- Authorization
- Validation
- Form
- Table
- Detail
- Card
- Util
- Hooks
- List Params
- Views
- README.md
- ISSUE.md
- Technology Stack
- Database
- File
- CDN Assets
- Entry Points
- Dashboard
- Authentication
- Credentials
- OAuth
- Flow Diagrams
- Multi-Tenancy
- Offline
- Onboarding
- Onboarding Videos
- Add Values
- Admission
- Application
- Attendance
- Compliance
- Profile
- Exams
- Exam Wizard
- Timetable
- Classrooms
- Notifications
- Conference
- LMS (Lumos)
- Finance
- Fee Management
- Invoice
- Wallet
- Salary
- Payroll
- Timesheet
- Expenses
- Budget
- Receipt
- Accounts
- Banking
- Reports
- Dashboard
- Permissions
- Messages
- Integration Flow
- Provision
- AI Document Processing
- Document Intelligence
- Internationalization
- Translation
- Translation Guide
- Icons
- Docs Factory
- Inspiration
- Listings
- Teachers
- Students
- Catalog
- Library
- Contributing
- Code of conduct
- GitHub Workflow
- Database Seeds
- Database Safety
- Test Accounts
- Playwright
- Prettier
- Block Rebound
Credentials authentication covers four flows on top of Auth.js: registration, email verification, login (with 2FA branch), and password reset. Tokens, emails, and audit logs are shared infrastructure.
For OAuth, multi-tenant redirect logic, and SSO see authentication and oauth.
Registration
Source: src/components/auth/join/{form,action}.ts.
- Client posts
{ email, password, username }toregister(). - Validate
RegisterSchema(email format, password ≥ 6, username ≥ 1). getUserByEmail(email)—findMany, prefers user with password set.- Duplicate →
"Email already in use!". bcrypt.hash(password, 10)→db.user.create()withrole: USER, noschoolId.generateVerificationToken(email)(UUID, 24h TTL).sendVerificationEmail()→ link/{locale}/new-verification?token={uuid}.- Return
"Confirmation email sent!".
| Result | Type |
|---|---|
Invalid fields! | error |
Email already in use! | error |
Confirmation email sent! | success |
Email verification
Source: src/components/auth/verification/{form,action,verificiation-token}.ts.
- User opens
/new-verification?token={uuid}. NewVerificationFormcallsnewVerification(token)on mount.getVerificationTokenByToken(token)— if missing, fall back to checking already-verified state.- Check
expires < now. getUserByEmail(token.email). If already verified →"Email verified!"(idempotent).- Update
user.emailVerified = new Date()anduser.email = token.email. - Delete the token.
- UI shows success + "Back to login" link (no auto-redirect).
| Result | Type |
|---|---|
Token does not exist! | error |
Token has expired! | error |
Email does not exist! | error |
Email verified! | success |
Email already verified! | success |
Login
10-step flow with a 2FA branch. Source: src/components/auth/login/{form,action}.ts, src/auth.config.ts.
- Validate —
LoginSchema.safeParse({ email, password, code? }). - Brute force check —
isBruteForceBlocked(email)blocks at ≥ 5 failures in last 15 min. - User lookup —
getUserByEmail(email)(multi-tenantfindMany, prefers user with password). - Exists — missing user/email/password logs
USER_NOT_FOUND→"Email does not exist!". - Email verified? — if not, regenerate verification token, send email, return
"Confirmation email sent!". - 2FA branch (if
isTwoFactorEnabled):- No code → generate 6-digit token (5 min TTL), email it, return
{ twoFactor: true }. - Code submitted → validate exists/matches/unexpired, delete token, replace
TwoFactorConfirmation.
- No code → generate 6-digit token (5 min TTL), email it, return
- Build redirect URL — context-aware (callbackUrl, role, subdomain) — see authentication.
- Sign in —
signIn("credentials", { email, password, redirectTo }). - Authorize (
auth.config.ts) — re-parse,getUserByEmail,bcrypt.compare. - Errors —
AuthError.CredentialsSignin → "Invalid credentials!";NEXT_REDIRECTre-thrown.
| Result | Type |
|---|---|
Invalid fields! | error |
Too many failed attempts. Please try again in 15 minutes. | error |
Email does not exist! | error |
Confirmation email sent! | success |
Invalid code! | error |
Code expired! | error |
Invalid credentials! | error |
Something went wrong! | error |
An unexpected error occurred. Please try again. | error |
Password reset
Two steps — request, then set new password.
Request
Source: src/components/auth/reset/{form,action}.ts.
- Submit
{ email }toreset(). ResetSchemavalidates email.getUserByEmail(email)— missing →"Email not found!".generatePasswordResetToken(email)(UUID, 1h TTL).sendPasswordResetEmail()→/{locale}/new-password?token={uuid}.- Return
"Reset email sent!".
Set new password
Source: src/components/auth/password/{form,action,token}.ts.
- Submit
{ password }+ token from URL. - No token →
"Missing token!". NewPasswordSchemavalidates password ≥ 6.getPasswordResetTokenByToken(token)— missing →"Invalid token!"; expired →"Token has expired!".getUserByEmail(token.email)— missing →"Email does not exist!".bcrypt.hash(password, 10)→db.user.update.- Delete token.
- Return
"Password updated!".
2FA
Integrated into login when user.isTwoFactorEnabled.
// src/components/auth/tokens.ts
const token = crypto.randomInt(100_000, 1_000_000).toString() // 6-digit
const expires = new Date(Date.now() + 5 * 60 * 1000) // 5 minutesLifecycle:
- Login detects
isTwoFactorEnabled+ no code → generate token, email it, return{ twoFactor: true }. - Client shows code input.
- Resubmit with code → validate (exact match + not expired).
- Delete
TwoFactorToken. - Replace
TwoFactorConfirmationfor the user. - Continue with normal
signIn("credentials").
Email format (no link, code inline):
| Locale | Subject | Body |
|---|---|---|
| en | 2FA Code | Your 2FA code: {token} |
| ar | رمز التحقق | رمز التحقق الخاص بك: {token} |
Source: src/components/auth/{tokens,verification/2f-token,verification/2f-confirmation}.ts.
Token system
All tokens use delete-before-create — only one active token per email at a time.
| Token | Format | TTL | Generator | Model |
|---|---|---|---|---|
| Verification | UUID v4 | 24h | generateVerificationToken | VerificationToken |
| Password reset | UUID v4 | 1h | generatePasswordResetToken | PasswordResetToken |
| 2FA | 6-digit numeric | 5 min | generateTwoFactorToken | TwoFactorToken |
Source: src/components/auth/tokens.ts.
Email service
| Setting | Value |
|---|---|
| Provider | Resend (RESEND_API_KEY) |
| Sender | noreply@databayt.org |
| Domain | NEXT_PUBLIC_APP_URL |
| Languages | English + Arabic (dir="rtl") |
| Errors | Silent — try/catch with console.error |
| Function | Subject (en) | Subject (ar) | Link |
|---|---|---|---|
sendVerificationEmail | Confirm your email | تأكيد بريدك الإلكتروني | /{locale}/new-verification?token={uuid} |
sendPasswordResetEmail | Reset your password | إعادة تعيين كلمة المرور | /{locale}/new-password?token={uuid} |
sendTwoFactorTokenEmail | 2FA Code | رمز التحقق | None — code inline |
Source: src/components/auth/mail.ts.
Brute force protection
Source: src/lib/audit-log.ts.
// isBruteForceBlocked(email, ip?) — email-only, ip stored but not used in query
const recentFailures = await db.loginAttempt.count({
where: {
email,
success: false,
timestamp: { gte: fifteenMinutesAgo },
},
})
return recentFailures >= 5logLoginAttempt({ email, success, failureReason, ip, userAgent, schoolId }) records every attempt for auditing.
Validation schemas
Two tiers exist: legacy (server-side, English) and i18n factories (client-side, dictionary-translated).
| Legacy schema | Fields | Constraints |
|---|---|---|
LoginSchema | email, password, code? | email format, password ≥ 1 |
RegisterSchema | email, password, username | email format, password ≥ 6, username ≥ 1 |
ResetSchema | email | email format |
NewPasswordSchema | password | ≥ 6 chars |
SettingsSchema | name?, email?, role, password?, newPassword?, isTwoFactorEnabled? | newPassword ↔ password coupling |
Factories: createLoginSchema(dictionary), createRegisterSchema(dictionary), createResetSchema(dictionary), createNewPasswordSchema(dictionary), createSettingsSchema(dictionary). Note: password/form.tsx still uses the legacy NewPasswordSchema.
Source: src/components/auth/validation.ts.
Models
User, VerificationToken, PasswordResetToken, TwoFactorToken, TwoFactorConfirmation, LoginAttempt. The User model uses @@unique([email, schoolId]) rather than @unique email — see the multi-tenant adapter section in oauth for the implications. Schemas live under prisma/models/auth.prisma. See database and accounts for full field reference.
See also
- Authentication
- OAuth — multi-tenant adapter
- Accounts
- Database