feat: add register, login, logout, and me auth endpoints
This commit is contained in:
parent
80f0b3535b
commit
d1156745ca
1 changed files with 114 additions and 0 deletions
|
|
@ -1,3 +1,117 @@
|
||||||
import { Router } from "express";
|
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 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;
|
export default router;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue