Signup and login flows

Two endpoints cover almost every auth flow you will ever write. POST /api/auth/register creates a user. POST /api/auth/login verifies credentials and returns a token. Everything else in auth is a variation on these.

after/controllers/auth.controller.ts
typescript
const AuthSchema = z.object({
  username: z.string().min(3),
  password: z.string().min(6),
});

export async function registerHandler(req: Request, res: Response, next: NextFunction) {
  try {
    const validation = AuthSchema.safeParse(req.body);
    if (!validation.success) {
      res.status(400).json({ error: 'Invalid input', details: validation.error.format() });
      return;
    }
    const { username, password } = validation.data;

    const existing = await userService.getUserByUsername(username);
    if (existing) {
      res.status(409).json({ error: 'Username already exists' });
      return;
    }

    const newUser = await userService.createUser(username, password);
    res.status(201).json(newUser);
  } catch (err) { next(err); }
}

Validate, check for duplicates (409), create, return 201.

The login flow looks similar but with one key difference. If the user does not exist, OR if the password is wrong, you return the same error message: Invalid username or password. Never say 'user not found' on one branch and 'wrong password' on the other. That tells an attacker which usernames are real, and they can grind through a list.

One new thing appears in the login code: when the credentials check out, the server hands back a token. jwt.sign creates it by signing a small payload (user id, username, role) with a server-side secret called JWT_SECRET, and expiresIn: '1h' stamps an expiry on it. The client stores that token and sends it with every request as proof of who they are. How the signature works and how the server verifies it is exactly what we unpack when we build the requireAuth middleware. For now, read jwt.sign as: issue a tamper-proof pass that expires in an hour.

after/controllers/auth.controller.ts
typescript
export async function loginHandler(req: Request, res: Response, next: NextFunction) {
  try {
    const validation = AuthSchema.safeParse(req.body);
    if (!validation.success) {
      res.status(400).json({ error: 'Invalid input', details: validation.error.format() });
      return;
    }
    const { username, password } = validation.data;

    const user = await userService.getUserByUsername(username);

    // Same message on both branches. No username enumeration.
    if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
      res.status(401).json({ error: 'Invalid username or password' });
      return;
    }

    const token = jwt.sign(
      { userId: user.id, username: user.username, role: user.role },
      JWT_SECRET,
      { expiresIn: '1h' },
    );

    res.json({ token, user: { id: user.id, username: user.username, role: user.role } });
  } catch (err) { next(err); }
}

Same error message for both failure branches. No hints.

Quiz: Quiz

Loading practiceโ€ฆ