feat: add db-backed session cookie auth compatibility

This commit is contained in:
Nico 2026-02-16 01:43:27 -08:00
parent 0f9d349fa5
commit 119994b602
6 changed files with 182 additions and 10 deletions

View File

@ -2,6 +2,10 @@ const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const User = require("../models/user.model");
const { sendError } = require("../utils/http");
const Session = require("../models/session.model");
const { parseCookieHeader } = require("../utils/cookies");
const { setSessionCookie, clearSessionCookie, cookieName } = require("../utils/session-cookie");
const { logError } = require("../utils/logger");
exports.register = async (req, res) => {
let { username, password, name } = req.body;
@ -15,6 +19,7 @@ exports.register = async (req, res) => {
res.json({ message: "User registered", user });
} catch (err) {
logError(req, "auth.register", err);
sendError(res, 400, "Registration failed");
}
};
@ -41,5 +46,30 @@ exports.login = async (req, res) => {
{ expiresIn: "1 year" }
);
try {
const session = await Session.createSession(user.id, req.headers["user-agent"] || null);
setSessionCookie(res, session.id);
} catch (err) {
logError(req, "auth.login.createSession", err);
return sendError(res, 500, "Failed to create session");
}
res.json({ token, userId: user.id, username, role: user.role });
};
exports.logout = async (req, res) => {
try {
const cookies = parseCookieHeader(req.headers.cookie);
const sid = cookies[cookieName()];
if (sid) {
await Session.deleteSession(sid);
}
clearSessionCookie(res);
res.json({ message: "Logged out" });
} catch (err) {
logError(req, "auth.logout", err);
sendError(res, 500, "Failed to logout");
}
};

View File

@ -1,19 +1,48 @@
const jwt = require("jsonwebtoken");
const { sendError } = require("../utils/http");
const Session = require("../models/session.model");
const { parseCookieHeader } = require("../utils/cookies");
const { cookieName } = require("../utils/session-cookie");
const { logError } = require("../utils/logger");
function auth(req, res, next) {
const header = req.headers.authorization;
if (!header) return sendError(res, 401, "Missing token");
async function auth(req, res, next) {
const header = req.headers.authorization || "";
const token = header.startsWith("Bearer ") ? header.slice(7).trim() : null;
const token = header.split(" ")[1];
if (!token) return sendError(res, 401, "Invalid token format");
if (token) {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // id + role
return next();
} catch (err) {
return sendError(res, 401, "Invalid or expired token");
}
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // id + role
next();
const cookies = parseCookieHeader(req.headers.cookie);
const sid = cookies[cookieName()];
if (!sid) {
return sendError(res, 401, "Missing authentication");
}
const session = await Session.getActiveSessionWithUser(sid);
if (!session) {
return sendError(res, 401, "Invalid or expired session");
}
req.user = {
id: session.user_id,
role: session.role,
username: session.username,
};
req.session_id = session.id;
return next();
} catch (err) {
sendError(res, 401, "Invalid or expired token");
logError(req, "middleware.auth", err);
return sendError(res, 500, "Authentication check failed");
}
}

View File

@ -0,0 +1,56 @@
const crypto = require("crypto");
const pool = require("../db/pool");
const { SESSION_TTL_DAYS } = require("../utils/session-cookie");
function generateSessionId() {
if (typeof crypto.randomUUID === "function") {
return crypto.randomUUID().replace(/-/g, "") + crypto.randomBytes(8).toString("hex");
}
return crypto.randomBytes(32).toString("hex");
}
exports.createSession = async (userId, userAgent = null) => {
const id = generateSessionId();
const result = await pool.query(
`INSERT INTO sessions (id, user_id, expires_at, user_agent)
VALUES ($1, $2, NOW() + ($3 || ' days')::interval, $4)
RETURNING id, user_id, created_at, expires_at`,
[id, userId, String(SESSION_TTL_DAYS), userAgent]
);
return result.rows[0];
};
exports.getActiveSessionWithUser = async (sessionId) => {
const result = await pool.query(
`SELECT
s.id,
s.user_id,
s.expires_at,
u.username,
u.role
FROM sessions s
JOIN users u ON u.id = s.user_id
WHERE s.id = $1
AND s.expires_at > NOW()`,
[sessionId]
);
const session = result.rows[0] || null;
if (!session) return null;
await pool.query(
`UPDATE sessions
SET last_seen_at = NOW()
WHERE id = $1`,
[sessionId]
);
return session;
};
exports.deleteSession = async (sessionId) => {
await pool.query(
`DELETE FROM sessions WHERE id = $1`,
[sessionId]
);
};

View File

@ -4,6 +4,7 @@ const User = require("../models/user.model");
router.post("/register", controller.register);
router.post("/login", controller.login);
router.post("/logout", controller.logout);
router.post("/", async (req, res) => {
res.status(200).json({
message: "Auth API is running.",

20
backend/utils/cookies.js Normal file
View File

@ -0,0 +1,20 @@
function parseCookieHeader(cookieHeader) {
const cookies = {};
if (!cookieHeader || typeof cookieHeader !== "string") return cookies;
const segments = cookieHeader.split(";");
for (const segment of segments) {
const index = segment.indexOf("=");
if (index === -1) continue;
const key = segment.slice(0, index).trim();
const value = segment.slice(index + 1).trim();
if (!key) continue;
cookies[key] = decodeURIComponent(value);
}
return cookies;
}
module.exports = {
parseCookieHeader,
};

View File

@ -0,0 +1,36 @@
const SESSION_COOKIE_NAME = process.env.SESSION_COOKIE_NAME || "sid";
const SESSION_TTL_DAYS = Number(process.env.SESSION_TTL_DAYS || 30);
function sessionMaxAgeMs() {
return SESSION_TTL_DAYS * 24 * 60 * 60 * 1000;
}
function cookieName() {
return SESSION_COOKIE_NAME;
}
function setSessionCookie(res, sessionId) {
res.cookie(cookieName(), sessionId, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/",
maxAge: sessionMaxAgeMs(),
});
}
function clearSessionCookie(res) {
res.clearCookie(cookieName(), {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/",
});
}
module.exports = {
SESSION_TTL_DAYS,
clearSessionCookie,
cookieName,
setSessionCookie,
};