feat: add register, login, logout, and me auth endpoints

This commit is contained in:
Aaron Wood 2026-04-11 00:26:49 -04:00
parent 80f0b3535b
commit d1156745ca

View file

@ -1,3 +1,117 @@
import { Router } from "express";
import type { RowDataPacket, ResultSetHeader } from "mysql2";
import bcrypt from "bcrypt";
import db from "../db.js";
import { signToken, COOKIE_NAME } from "../auth/jwt.js";
import { requireAuth } from "../auth/middleware.js";
const router = Router();
const BCRYPT_ROUNDS = 12;
const COOKIE_OPTS = {
httpOnly: true,
sameSite: "lax" as const,
secure: process.env.NODE_ENV === "production",
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days in ms
};
// POST /api/auth/register
router.post("/register", async (req, res) => {
try {
const { email, username, password } = req.body;
if (!email?.trim() || !username?.trim() || !password) {
res.status(400).json({ error: "email, username, and password are required" });
return;
}
if (password.length < 8) {
res.status(400).json({ error: "Password must be at least 8 characters" });
return;
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
res.status(400).json({ error: "Invalid email format" });
return;
}
const [existing] = await db.execute<RowDataPacket[]>(
"SELECT id FROM users WHERE email = ?",
[email.trim().toLowerCase()]
);
if (existing.length > 0) {
res.status(409).json({ error: "Email already registered" });
return;
}
const passwordHash = await bcrypt.hash(password, BCRYPT_ROUNDS);
const [result] = await db.execute<ResultSetHeader>(
"INSERT INTO users (email, username, password_hash) VALUES (?, ?, ?)",
[email.trim().toLowerCase(), username.trim(), passwordHash]
);
const token = signToken({
userId: result.insertId,
email: email.trim().toLowerCase(),
username: username.trim(),
});
res.cookie(COOKIE_NAME, token, COOKIE_OPTS);
res.status(201).json({
userId: result.insertId,
email: email.trim().toLowerCase(),
username: username.trim(),
});
} catch (err) {
console.error(err);
res.status(500).json({ error: "Internal server error" });
}
});
// POST /api/auth/login
router.post("/login", async (req, res) => {
try {
const { email, password } = req.body;
if (!email?.trim() || !password) {
res.status(400).json({ error: "email and password are required" });
return;
}
const [rows] = await db.execute<RowDataPacket[]>(
"SELECT id, email, username, password_hash FROM users WHERE email = ?",
[email.trim().toLowerCase()]
);
if (rows.length === 0) {
res.status(401).json({ error: "Invalid email or password" });
return;
}
const user = rows[0];
const valid = await bcrypt.compare(password, user.password_hash as string);
if (!valid) {
res.status(401).json({ error: "Invalid email or password" });
return;
}
const token = signToken({
userId: user.id as number,
email: user.email as string,
username: user.username as string,
});
res.cookie(COOKIE_NAME, token, COOKIE_OPTS);
res.json({ userId: user.id, email: user.email, username: user.username });
} catch (err) {
console.error(err);
res.status(500).json({ error: "Internal server error" });
}
});
// POST /api/auth/logout
router.post("/logout", (_req, res) => {
res.clearCookie(COOKIE_NAME);
res.json({ ok: true });
});
// GET /api/auth/me
router.get("/me", requireAuth, (req, res) => {
res.json(req.user);
});
export default router;