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 jwt = require("jsonwebtoken");
|
||||||
const User = require("../models/user.model");
|
const User = require("../models/user.model");
|
||||||
const { sendError } = require("../utils/http");
|
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) => {
|
exports.register = async (req, res) => {
|
||||||
let { username, password, name } = req.body;
|
let { username, password, name } = req.body;
|
||||||
@ -15,6 +19,7 @@ exports.register = async (req, res) => {
|
|||||||
|
|
||||||
res.json({ message: "User registered", user });
|
res.json({ message: "User registered", user });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
logError(req, "auth.register", err);
|
||||||
sendError(res, 400, "Registration failed");
|
sendError(res, 400, "Registration failed");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -41,5 +46,30 @@ exports.login = async (req, res) => {
|
|||||||
{ expiresIn: "1 year" }
|
{ 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 });
|
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 jwt = require("jsonwebtoken");
|
||||||
const { sendError } = require("../utils/http");
|
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) {
|
async function auth(req, res, next) {
|
||||||
const header = req.headers.authorization;
|
const header = req.headers.authorization || "";
|
||||||
if (!header) return sendError(res, 401, "Missing token");
|
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 {
|
try {
|
||||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||||
req.user = decoded; // id + role
|
req.user = decoded; // id + role
|
||||||
next();
|
return next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
sendError(res, 401, "Invalid or expired token");
|
return sendError(res, 401, "Invalid or expired token");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
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) {
|
||||||
|
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("/register", controller.register);
|
||||||
router.post("/login", controller.login);
|
router.post("/login", controller.login);
|
||||||
|
router.post("/logout", controller.logout);
|
||||||
router.post("/", async (req, res) => {
|
router.post("/", async (req, res) => {
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
message: "Auth API is running.",
|
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