Authentication
The platform uses JWT-based authentication with HTTP-only cookies for session management.
Authentication Flow
┌──────────────┐ ┌─────────────────┐ ┌───────────────────┐
│ Login Form │────▶│ Server Action │────▶│ MongoDB Lookup │
│ (email/pwd) │ │ login() │ │ + bcrypt compare │
└──────────────┘ └────────┬────────┘ └───────────────────┘
│
▼
┌─────────────────────┐
│ createSession() │
│ Signs JWT with │
│ jose.SignJWT │
└────────┬────────────┘
│
▼
┌─────────────────────┐
│ Set HTTP-only │
│ "session" cookie │
│ (7-day expiry) │
└─────────────────────┘
Login Process
The login page is at src/app/login/page.tsx. It uses a server action for credential validation.
Server action: src/app/login/actions.ts
// Validation schema
const loginSchema = z.object({
email: z.string().email("Invalid email format"),
password: z.string().min(5, "Password must be at least 5 characters"),
});
Steps:
- User submits email and password
- Zod validates the input format
validateUser()connects to MongoDB and finds the user by emailbcrypt.compare()verifies the password against the stored hashcreateSession()creates a JWT containing the full user payload and sets it as an HTTP-only cookie namedsession
Post-login routing:
- If the user has not completed onboarding → redirects to
/onboarding - If the user is an admin or has a valid token → redirects to
/dashboard/notifications
Session Cookie
The session is stored as a JWT in an HTTP-only cookie:
| Property | Value |
|---|---|
| Cookie name | session |
| Type | HTTP-only |
| Expiry | 7 days from creation |
| Content | JWT signed with jose.SignJWT containing the full user document |
Middleware — Route Protection
The middleware at src/middleware.ts intercepts every request and enforces authentication:
// Route classifications
const protectedRoutes = ["/api/users", "/api/myUser", "/onboarding"];
const adminRoutes = ["/dashboard/admin"];
const publicRoutes = ["/login", "/api/verify", "/docs"];
Middleware logic:
- Read the
sessioncookie - If the cookie exists, verify it with
verifyToken()(usesjose.jwtVerify) - If verification fails → delete the cookie, redirect to
/login - If accessing an admin route → check that
role === "admin", otherwise redirect to/dashboard/notifications - If accessing a protected route without a session → redirect to
/login - If a logged-in user visits
/login→ redirect to/dashboard/notifications - Root path
/always redirects to/login
API-Level Authentication
API routes extract user identity from the session cookie using the helper at src/helpers/auth.ts:
export async function getUserIdFromCookies() {
const cookieStore = await cookies();
const cookie = cookieStore.get("session");
if (!cookie) {
return {
error: true,
response: NextResponse.json({ error: "unauthorized" }, { status: 401 }),
};
}
// Decode JWT payload
const token = cookie.value;
const base64Payload = token.split(".")[1];
const payload = JSON.parse(
Buffer.from(base64Payload, "base64").toString("utf-8")
);
const userId = new mongoose.Types.ObjectId(payload._id);
return { error: false, userId };
}
Most API routes also directly verify the JWT using jose.jwtVerify for additional security.
Onboarding
New users must complete a three-step onboarding process before accessing the platform:
- Profile picture upload (optional)
- Signature upload (required)
- Password reset (required — changes from the admin-assigned initial password)
Component: src/features/onboarding/components/onboarding_wrapper.tsx
After onboarding, the user's onboarded field is set to true in the database.
Password Management
- Passwords are hashed with
bcryptbefore storage - Initial passwords are set by admins during user creation
- Users must reset their password during onboarding
- Password updates re-hash only if the password value has changed
Logout
The logout server action at src/app/login/actions.ts deletes the session cookie and redirects to /login.