Skip to main content
CampusBite uses a comprehensive authentication system that handles user registration, login, email verification, and password recovery. The system supports three user roles: students, faculty, and store employees.

User roles and validation

The platform enforces strict role-based validation during registration:
  • Students: Must use a christuniversity.in email (including subdomains) and provide a register number
  • Faculty: Must use a christuniversity.in email and provide an employee ID
  • Store employees: Must provide an employee ID, phone number, store name, and valid UPI ID
The system validates that Christ University emails use the correct domain (christuniversity.in) or approved subdomains to ensure only authorized campus users can register.

Registration flow

When you register a new user, the system performs multiple validation steps and creates necessary records.
1

Validate input data

The registration schema validates all required fields using Zod. Password requirements include:
  • Minimum 8 characters
  • At least one uppercase letter
  • At least one lowercase letter
  • At least one digit
2

Check for existing users

The system checks if an account with the provided email already exists to prevent duplicates.
3

Hash password and generate token

Passwords are hashed using bcrypt with a cost factor of 12. A unique email verification token is generated using crypto.
4

Create user and store records

For store employees, both a User and Store record are created. If store creation fails, the user record is rolled back.
5

Send verification email

A verification email is sent to the user with a unique token link.

Registration code example

backend/src/controllers/authController.js
export const register = async (req, res, next) => {
  try {
    const {
      name,
      email,
      password,
      role,
      registerNumber,
      employeeId,
      phoneNumber,
      storeName,
      storeUpiId,
    } = req.body;

    const normalizedEmail = email.trim().toLowerCase();

    // Check for existing user
    const existingUser = await User.findOne({ email: normalizedEmail }).lean();
    if (existingUser) {
      return res.status(409).json({
        success: false,
        message: "An account with this email already exists.",
      });
    }

    // Hash password and create verification token
    const hashedPassword = await bcrypt.hash(password, 12);
    const emailVerificationToken = crypto.randomBytes(32).toString("hex");

    // Create user
    const user = await User.create({
      name: name.trim(),
      email: normalizedEmail,
      password: hashedPassword,
      role,
      register_number: registerNumber || null,
      employee_id: employeeId || null,
      phone_number: phoneNumber || null,
      email_verification_token: emailVerificationToken,
    });

    // Create store for store employees
    if (role === "store_employee") {
      try {
        await Store.create({
          name: storeName.trim(),
          upi_id: storeUpiId.trim().toLowerCase(),
          owner_id: user._id,
        });
      } catch (storeError) {
        await User.deleteOne({ _id: user._id });
        throw storeError;
      }
    }

    // Send verification email
    await sendVerificationEmail(user.email, user.name, emailVerificationToken);

    res.status(201).json({
      success: true,
      message: "Registration successful. Please check your email to verify your account.",
      data: sanitizeUser(user),
    });
  } catch (error) {
    next(error);
  }
};

Email verification

Users must verify their email before they can log in. The verification process uses a unique token stored in the database.
export const verifyEmail = async (req, res, next) => {
  try {
    const { token } = req.params;

    const user = await User.findOne({ email_verification_token: token });

    if (!user) {
      return res.status(400).json({
        success: false,
        message: "Invalid or expired verification token.",
      });
    }

    // Idempotent - if already verified, return success
    if (user.is_email_verified) {
      return res.json({
        success: true,
        message: "Email already verified. You can log in.",
      });
    }

    user.is_email_verified = true;
    await user.save();

    res.json({
      success: true,
      message: "Email verified successfully. You can now log in.",
    });
  } catch (error) {
    next(error);
  }
};

Login and JWT tokens

The login system uses a dual-token approach with access tokens and refresh tokens for enhanced security.

Token generation

When you log in successfully, the system generates two JWT tokens:
backend/src/controllers/authController.js
const generateTokens = (userId) => {
  const accessToken = jwt.sign({ userId }, process.env.JWT_SECRET, {
    expiresIn: process.env.JWT_EXPIRES_IN || "1h",
  });

  const refreshToken = jwt.sign({ userId }, process.env.JWT_REFRESH_SECRET, {
    expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || "7d",
  });

  return { accessToken, refreshToken };
};
  • Access token: 1 hour (default)
  • Refresh token: 7 days (default)
The refresh token is stored in the database and can be used to obtain a new access token without requiring the user to log in again.

Login flow

backend/src/controllers/authController.js
export const login = async (req, res, next) => {
  try {
    const { email, password } = req.body;
    const normalizedEmail = email.trim().toLowerCase();

    // Find user
    const user = await User.findOne({ email: normalizedEmail });
    if (!user) {
      return res.status(401).json({
        success: false,
        message: "Invalid email or password.",
      });
    }

    // Verify password
    const isPasswordValid = await bcrypt.compare(password, user.password);
    if (!isPasswordValid) {
      return res.status(401).json({
        success: false,
        message: "Invalid email or password.",
      });
    }

    // Check email verification
    if (!user.is_email_verified) {
      return res.status(403).json({
        success: false,
        message: "Please verify your email before logging in.",
      });
    }

    // Generate tokens
    const userId = user._id.toString();
    const { accessToken, refreshToken } = generateTokens(userId);

    // Store refresh token
    await RefreshToken.create({
      user_id: user._id,
      token: refreshToken,
      expires_at: toDateFromDays(7),
    });

    res.json({
      success: true,
      message: "Login successful.",
      data: {
        user: sanitizeUser(user),
        accessToken,
        refreshToken,
      },
    });
  } catch (error) {
    next(error);
  }
};
The login endpoint returns generic error messages for invalid credentials to prevent user enumeration attacks. Both invalid email and invalid password return the same “Invalid email or password” message.

Token refresh mechanism

When an access token expires, you can use the refresh token to obtain a new pair of tokens without requiring the user to log in again.
backend/src/controllers/authController.js
export const refreshToken = async (req, res, next) => {
  try {
    const { refreshToken: token } = req.body;

    if (!token) {
      return res.status(400).json({
        success: false,
        message: "Refresh token is required.",
      });
    }

    // Verify token
    let decoded;
    try {
      decoded = jwt.verify(token, process.env.JWT_REFRESH_SECRET);
    } catch {
      return res.status(401).json({
        success: false,
        message: "Invalid refresh token.",
      });
    }

    // Check if token exists in database
    const tokenDoc = await RefreshToken.findOne({
      token,
      user_id: decoded.userId,
      expires_at: { $gt: new Date() },
    });

    if (!tokenDoc) {
      return res.status(401).json({
        success: false,
        message: "Refresh token not found or expired.",
      });
    }

    // Delete old token
    await RefreshToken.deleteOne({ _id: tokenDoc._id });

    // Generate new tokens
    const { accessToken: newAccessToken, refreshToken: newRefreshToken } =
      generateTokens(decoded.userId);

    // Store new refresh token
    await RefreshToken.create({
      user_id: decoded.userId,
      token: newRefreshToken,
      expires_at: toDateFromDays(7),
    });

    res.json({
      success: true,
      message: "Tokens refreshed successfully.",
      data: {
        accessToken: newAccessToken,
        refreshToken: newRefreshToken,
      },
    });
  } catch (error) {
    next(error);
  }
};

Password recovery

Users can reset their password using a secure token-based flow.
1

Request password reset

User submits their email address. The system generates a unique reset token that expires in 1 hour.
2

Receive reset email

An email with a reset link containing the token is sent to the user.
3

Submit new password

User clicks the link, enters a new password, and submits the form.
4

Invalidate all sessions

After password reset, all existing refresh tokens are deleted, forcing the user to log in again on all devices.
export const forgotPassword = async (req, res, next) => {
  try {
    const { email } = req.body;
    const normalizedEmail = email.trim().toLowerCase();
    const user = await User.findOne({ email: normalizedEmail });

    // Always return success to prevent user enumeration
    if (!user) {
      return res.json({
        success: true,
        message: "If an account with that email exists, a password reset link has been sent.",
      });
    }

    // Generate reset token (expires in 1 hour)
    const resetToken = crypto.randomBytes(32).toString("hex");
    const resetExpires = new Date();
    resetExpires.setHours(resetExpires.getHours() + 1);

    user.password_reset_token = resetToken;
    user.password_reset_expires = resetExpires;
    await user.save();

    await sendPasswordResetEmail(user.email, user.name, resetToken);

    res.json({
      success: true,
      message: "If an account with that email exists, a password reset link has been sent.",
    });
  } catch (error) {
    next(error);
  }
};

Data sanitization

Before returning user data to the client, sensitive fields are removed:
backend/src/controllers/authController.js
const sanitizeUser = (userDoc) => {
  const user = formatUser(userDoc);
  if (!user) return null;

  delete user.password;
  delete user.email_verification_token;
  delete user.password_reset_token;
  delete user.password_reset_expires;
  delete user.__v;

  return user;
};
The sanitization function ensures that password hashes and security tokens are never exposed in API responses, even if the full user document is accidentally passed to the response.

API endpoints

Here’s a summary of the authentication endpoints:
EndpointMethodDescription
/api/auth/registerPOSTRegister a new user account
/api/auth/loginPOSTLog in with email and password
/api/auth/verify-email/:tokenGETVerify email address
/api/auth/forgot-passwordPOSTRequest password reset
/api/auth/reset-password/:tokenPOSTReset password with token
/api/auth/refreshPOSTRefresh access token
/api/auth/logoutPOSTLog out and invalidate refresh token

Build docs developers (and LLMs) love