webpods
Version:
Append-only log service with OAuth authentication
117 lines • 3.98 kB
JavaScript
/**
* PostgreSQL session store for SSO
*/
import session from "express-session";
import connectPgSimple from "connect-pg-simple";
import { getDb } from "../db.js";
import { createLogger } from "../logger.js";
import { getConfig } from "../config-loader.js";
const logger = createLogger("webpods:auth:session");
const PgSession = connectPgSimple(session);
let sessionStore = null;
/**
* Get or create PostgreSQL session store
*/
export function getSessionStore() {
if (!sessionStore) {
// Build connection string from config
const config = getConfig();
const { host, port, database, user, password } = config.database;
const conString = `postgresql://${user}:${password}@${host}:${port}/${database}`;
sessionStore = new PgSession({
conString,
tableName: "session",
createTableIfMissing: false, // We create it via migrations
pruneSessionInterval: 60 * 60, // Prune expired sessions every hour (seconds)
errorLog: (error) => {
logger.error("Session store error", { error });
},
});
logger.info("PostgreSQL session store initialized");
}
return sessionStore;
}
/**
* Get session middleware configuration
*/
export function getSessionConfig() {
const config = getConfig();
return {
store: getSessionStore(),
secret: config.auth.sessionSecret,
resave: false,
saveUninitialized: false,
rolling: true, // Reset expiry on activity
cookie: {
secure: config.server.public?.isSecure || false, // Use HTTPS from public URL
httpOnly: true,
sameSite: "lax",
maxAge: 10 * 365 * 24 * 60 * 60 * 1000, // 10 years (effectively unlimited)
// Set domain to share across subdomains
// Cookie domain cannot have port
domain: `.${config.server.public?.hostname || "localhost"}`,
},
name: "webpods.sid", // Custom session cookie name
};
}
/**
* List all active sessions for a user
*/
export async function getUserSessions(userId) {
const db = getDb();
const sessions = await db.manyOrNone(`SELECT sid, sess, expire
FROM session
WHERE expire > NOW()`);
// Filter sessions that belong to the user
const userSessions = [];
for (const session of sessions) {
const sessionData = typeof session.sess === "string"
? JSON.parse(session.sess)
: session.sess;
if (sessionData.user?.id === userId) {
userSessions.push({
id: session.sid,
user: sessionData.user,
createdAt: sessionData.cookie?.originalMaxAge
? new Date(session.expire.getTime() - sessionData.cookie.originalMaxAge)
: null,
expiresAt: session.expire,
});
}
}
return userSessions;
}
/**
* Revoke a specific session
*/
export async function revokeSession(sessionId) {
const db = getDb();
const result = await db.result(`DELETE FROM session WHERE sid = $(sessionId)`, { sessionId }, (r) => r.rowCount);
return result > 0;
}
/**
* Revoke all sessions for a user
*/
export async function revokeUserSessions(userId) {
const sessions = await getUserSessions(userId);
let revokedCount = 0;
for (const session of sessions) {
if (await revokeSession(session.id)) {
revokedCount++;
}
}
logger.info("Revoked user sessions", { userId, count: revokedCount });
return revokedCount;
}
/**
* Clean up expired sessions
*/
export async function cleanupExpiredSessions() {
const db = getDb();
const result = await db.result(`DELETE FROM session WHERE expire < NOW()`, [], (r) => r.rowCount);
if (result > 0) {
logger.info("Cleaned up expired sessions", { count: result });
}
return result;
}
//# sourceMappingURL=session-store.js.map