feat: add db-backed session cookie auth compatibility
This commit is contained in:
parent
0f9d349fa5
commit
119994b602
@ -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");
|
||||
}
|
||||
};
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
56
backend/models/session.model.js
Normal file
56
backend/models/session.model.js
Normal 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]
|
||||
);
|
||||
};
|
||||
@ -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
20
backend/utils/cookies.js
Normal 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,
|
||||
};
|
||||
36
backend/utils/session-cookie.js
Normal file
36
backend/utils/session-cookie.js
Normal 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,
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user