Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/dev0302/nextjs-project-1/llms.txt

Use this file to discover all available pages before exploring further.

AnonMessage uses a two-phase authentication system: new users verify their email address with a one-time password (OTP) before their account is created, and returning users authenticate through a NextAuth CredentialsProvider backed by bcrypt password comparison. All sessions are stored as JWTs — no database session table is required.

Sign-Up Flow

1

Request an OTP

The client sends the user’s email to POST /api/send-otp. The server checks that the email is not already registered, generates a unique 6-digit numeric OTP via otp-generator, creates an OTP document in MongoDB, and triggers an email via Brevo (through the mailSender helper). The OTP document has a MongoDB TTL index set to 5 * 60 seconds, so it expires automatically after five minutes.
// POST /api/send-otp — core logic
const otp = otpGenerator.generate(6, {
  upperCaseAlphabets: false,
  lowerCaseAlphabets: false,
  specialChars: false,
});

otpDoc = await OTP.create({ email, otp });
If the email is already taken the route returns 409 Conflict with { success: false, message: "User already registered" }.
2

Submit credentials + OTP

The client sends { username, email, password, otp } to POST /api/sign-up. The server:
  1. Validates that all four fields are present.
  2. Rejects with 409 if the email already exists.
  3. Fetches the most-recently-created OTP for that email and compares it with the submitted value — a mismatch returns 401 Invalid OTP.
  4. Hashes the password with bcrypt.hash(password, 10).
  5. Creates the user document with isVerified: true and isAcceptingMessages: true.
// POST /api/sign-up — OTP check + user creation
const recentOTP = await OTP.find({ email })
  .sort({ createdAt: -1 })
  .limit(1);

if (!recentOTP.length || recentOTP[0].otp !== otp) {
  return NextResponse.json(
    { success: false, message: "Invalid OTP" },
    { status: 401 }
  );
}

const hashedPassword = await bcrypt.hash(password, 10);

const user = await User.create({
  username,
  email,
  password: hashedPassword,
  isVerified: true,
  isAcceptingMessages: true,
  messages: [],
});
A successful response is 201 Created with the safe user object (no hashed password).
OTP documents carry a MongoDB TTL index of 5 minutes (expires: 5 * 60 on the createdAt field). After five minutes the document is automatically deleted, making the OTP permanently invalid even if the user never submits it. The while (true) retry loop in send-otp ensures uniqueness — it retries generation only on a duplicate-key error (code 11000).

Sign-In Flow

Sign-in is handled by NextAuth’s CredentialsProvider. The authorize callback runs on every login attempt:
// src/app/lib/auth.ts — authorize callback
async authorize(credentials: any): Promise<any> {
  await dbConnect();

  const { email, password } = credentials || {};

  if (!email || !password) {
    throw new Error("Email and Password required");
  }

  const user = await User.findOne({ email });

  if (!user) {
    throw new Error("User not found");
  }

  if (await bcrypt.compare(password, user.password)) {
    return user;
  } else {
    throw new Error("Invalid password");
  }
}
If bcrypt.compare returns true, NextAuth receives the full Mongoose user document and proceeds to the jwt callback. Any thrown error message is surfaced back to the client through NextAuth’s error handling.

JWT Strategy and Custom Token Fields

AnonMessage uses session.strategy: "jwt" — no database is queried on every request to verify the session. The jwt callback enriches the token with four custom fields the first time it is created (when user is present):
// src/app/lib/auth.ts — jwt callback
jwt: async ({ user, token }) => {
  if (user) {
    token._id = user.id?.toString();
    token.isVerified = user.isVerified;
    token.isAcceptingMessages = user.isAcceptingMessages;
    token.username = user.username;
  }
  return token;
},
The session callback then copies those token fields onto session.user so they are accessible in both Server Components and client hooks:
// src/app/lib/auth.ts — session callback
session: ({ session, token }) => {
  if (session.user) {
    session.user._id = token._id;
    session.user.isVerified = token.isVerified;
    session.user.isAcceptingMessages = token.isAcceptingMessages;
    session.user.username = token.username;
  }
  return session;
},

Session Type Declarations

Because NextAuth’s built-in types do not include the custom fields, src/app/types/next-auth.d.ts uses TypeScript module augmentation to extend the User, Session, and JWT interfaces:
// src/app/types/next-auth.d.ts
import { DefaultSession } from "next-auth";

declare module "next-auth" {
  interface User {
    _id?: string;
    isVerified?: boolean;
    isAcceptingMessages?: boolean;
    username?: string;
  }

  interface Session {
    user: {
      _id?: string;
      isVerified?: boolean;
      isAcceptingMessages?: boolean;
      username?: string;
    } & DefaultSession["user"];
  }
}

declare module "next-auth/jwt" {
  interface JWT {
    _id?: string;
    isVerified?: boolean;
    isAcceptingMessages?: boolean;
    username?: string;
  }
}
The & DefaultSession['user'] intersection preserves the original name, email, and image fields so nothing is accidentally dropped.

Protected Routes (Middleware)

Route protection is implemented in src/middleware.ts using getToken from next-auth/jwt:
// src/middleware.ts
import { getToken } from "next-auth/jwt";
import { NextRequest, NextResponse } from "next/server";

export async function middleware(request: NextRequest) {
  const token = await getToken({ req: request });
  const url = request.nextUrl.clone();

  // Logged-in users are redirected away from auth pages
  if (token && (url.pathname === "/sign-in" || url.pathname === "/sign-up")) {
    return NextResponse.redirect(new URL("/dashboard", request.url));
  }

  // Unauthenticated users cannot access the dashboard
  if (!token && url.pathname.startsWith("/dashboard")) {
    return NextResponse.redirect(new URL("/sign-in", request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/sign-in", "/signup", "/dashboard/:path*"],
};
This means:
  • Visiting /dashboard without a valid JWT cookie redirects to /sign-in.
  • Visiting /sign-in or /sign-up while already authenticated redirects to /dashboard.

Accessing the Session in Server Components

Any Server Component or API route can read the full session (including custom fields) via getServerSession:
import { getServerSession } from "next-auth";
import { NEXT_AUTH_CONFIG } from "@/app/lib/auth";

const session = await getServerSession(NEXT_AUTH_CONFIG);

// session.user._id        — MongoDB ObjectId as string
// session.user.username   — unique handle
// session.user.isVerified — email-verified flag
// session.user.isAcceptingMessages — messaging preference
Pass NEXT_AUTH_CONFIG (the exported AuthOptions object from lib/auth.ts) as the argument — not the handler — so getServerSession can decrypt the token using the same secret and callbacks.

Build docs developers (and LLMs) love