Skip to main content

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:

  1. User submits email and password
  2. Zod validates the input format
  3. validateUser() connects to MongoDB and finds the user by email
  4. bcrypt.compare() verifies the password against the stored hash
  5. createSession() creates a JWT containing the full user payload and sets it as an HTTP-only cookie named session

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

The session is stored as a JWT in an HTTP-only cookie:

PropertyValue
Cookie namesession
TypeHTTP-only
Expiry7 days from creation
ContentJWT 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:

  1. Read the session cookie
  2. If the cookie exists, verify it with verifyToken() (uses jose.jwtVerify)
  3. If verification fails → delete the cookie, redirect to /login
  4. If accessing an admin route → check that role === "admin", otherwise redirect to /dashboard/notifications
  5. If accessing a protected route without a session → redirect to /login
  6. If a logged-in user visits /login → redirect to /dashboard/notifications
  7. 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:

  1. Profile picture upload (optional)
  2. Signature upload (required)
  3. 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 bcrypt before 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.