add session store
All checks were successful
App Image CI / Build app image (pull_request) Successful in -1m28s
NPM Audit Check / Check NPM audit (pull_request) Successful in -2m13s

This commit is contained in:
2025-09-24 23:31:24 -05:00
parent 77441fe7ed
commit fe49302eca
3 changed files with 169 additions and 25 deletions

144
src/DB.ts
View File

@@ -1,9 +1,13 @@
import { Sequelize, DataTypes, Op, } from 'sequelize'; import { Sequelize, DataTypes, Op, } from 'sequelize';
import { Store } from 'express-session'
import { notStupidParseInt } from './Web.ts';
import type { ModelStatic, Filterable } from 'sequelize'; import type { ModelStatic, Filterable } from 'sequelize';
import type { LogType } from './BuildController.ts'; import type { LogType } from './BuildController.ts';
import type { SessionData } from 'express-session'
type Status = 'queued' | 'running' | 'cancelled' | 'success' | 'error'; type Status = 'queued' | 'running' | 'cancelled' | 'success' | 'error';
type Dependencies = 'stable' | 'testing' | 'staging'; type Dependencies = 'stable' | 'testing' | 'staging';
type Callback = (err?: unknown, data?: any) => any
interface DBConfig { interface DBConfig {
db?: string; db?: string;
@@ -41,7 +45,7 @@ interface LogChunk {
chunk: string chunk: string
} }
const MONTH = 1000 * 60 * 60 * 24 * 24; const MONTH = 1000 * 60 * 60 * 24 * 30;
const FRESH = { const FRESH = {
[Op.or]: [ [Op.or]: [
{ startTime: { [Op.gt]: new Date(Date.now() - MONTH) } }, { startTime: { [Op.gt]: new Date(Date.now() - MONTH) } },
@@ -50,13 +54,27 @@ const FRESH = {
} }
const SELECT = ['id', 'repo', 'commit', 'distro', 'dependencies', 'startTime', 'endTime', 'status']; const SELECT = ['id', 'repo', 'commit', 'distro', 'dependencies', 'startTime', 'endTime', 'status'];
class DB { function handleCallback<T>(err: unknown, data: T, cb?: Callback): T {
if (cb) {
cb(err, data);
}
if (err) {
throw err;
}
return data;
}
class DB extends Store {
private build: ModelStatic<any>; private build: ModelStatic<any>;
private logChunk: ModelStatic<any>; private logChunk: ModelStatic<any>;
private user: ModelStatic<any>; private user: ModelStatic<any>;
private session: ModelStatic<any>;
private sequelize: Sequelize; private sequelize: Sequelize;
private ttl: number;
constructor(config: DBConfig = {}) { constructor(config: DBConfig = {}) {
super();
this.ttl = notStupidParseInt(process.env['COOKIETTL']) || 1000 * 60 * 60 * 24 * 30;
this.sequelize = new Sequelize(config.db || 'archery', config.user || 'archery', process.env.PASSWORD || config.password || '', { this.sequelize = new Sequelize(config.db || 'archery', config.user || 'archery', process.env.PASSWORD || config.password || '', {
host: config.host || 'localhost', host: config.host || 'localhost',
port: config.port || 5432, port: config.port || 5432,
@@ -154,6 +172,16 @@ class DB {
} }
}); });
this.session = this.sequelize.define('session', {
sid: {
type: DataTypes.STRING,
primaryKey: true,
},
sessionData: {
type: DataTypes.JSONB,
}
});
this.build.belongsTo(this.user); this.build.belongsTo(this.user);
this.user.hasMany(this.build); this.user.hasMany(this.build);
@@ -164,6 +192,7 @@ class DB {
await this.user.sync(); await this.user.sync();
await this.build.sync(); await this.build.sync();
await this.logChunk.sync(); await this.logChunk.sync();
await this.session.sync();
if (!(await this.getUser('-1'))) { if (!(await this.getUser('-1'))) {
await this.createUser({ await this.createUser({
@@ -329,8 +358,117 @@ class DB {
await this.build.destroy({ await this.build.destroy({
where: { where: {
startTime: { [Op.lt]: new Date(Date.now() - MONTH * 6) } startTime: { [Op.lt]: new Date(Date.now() - MONTH * 6) }
} },
force: true
}); });
await this.session.destroy({
where: {
updatedAt: { [Op.lt]: new Date(Date.now() - this.ttl) }
},
force: true
});
}
public getTTL(sessionData: SessionData) {
if (sessionData?.cookie?.expires) {
const ms = Number(new Date(sessionData.cookie.expires)) - Date.now();
return ms;
}
else {
return this.ttl;
}
}
public async set(sid: string, sessionData: SessionData, cb?: Callback): Promise<void> {
const ttl = this.getTTL(sessionData);
try {
if (ttl > 0) {
await this.session.upsert({
sid,
sessionData
});
handleCallback(null, null, cb);
return;
}
await this.destroy(sid, cb);
}
catch (err) {
return handleCallback(err, null, cb);
}
}
public async get(sid: string, cb?: Callback): Promise<SessionData> {
try {
return handleCallback(null, ((await this.session.findByPk(sid))?.sessionData) as SessionData || null, cb);
}
catch (err) {
return handleCallback(err, null, cb);
}
}
public async destroy(sid: string, cb?: Callback): Promise<void> {
try {
await this.session.destroy({
where: {
sid
},
force: true
});
handleCallback(null, null, cb);
}
catch (err) {
handleCallback(err, null, cb);
}
}
public async clear(cb?: Callback): Promise<void> {
try {
await this.session.destroy({
truncate: true,
force: true
});
handleCallback(null, null, cb);
}
catch (err) {
handleCallback(err, null, cb);
}
}
public async length(cb?: Callback): Promise<number> {
try {
return handleCallback(null, await this.session.count(), cb);
}
catch (err) {
handleCallback(err, null, cb);
}
}
public async touch(sid: string, sessionData: SessionData, cb?: Callback): Promise<void> {
try {
await this.session.update({},
{
where: {
sid
}
}
);
handleCallback(null, null, cb);
}
catch (err) {
handleCallback(err, null, cb);
}
}
public async all(cb?: Callback): Promise<SessionData[]> {
try {
const all = await this.session.findAll({
attributes: ['sessionData']
});
return handleCallback(null, all.map(row => row.sessionData as SessionData), cb);
}
catch (err) {
handleCallback(err, null, cb);
}
} }
public async close(): Promise<void> { public async close(): Promise<void> {

View File

@@ -15,6 +15,7 @@ import type { BuildController, BuildEvent } from "./BuildController.ts";
interface WebConfig { interface WebConfig {
sessionSecret?: string; sessionSecret?: string;
port?: number; port?: number;
secure?: boolean;
oidc?: { oidc?: {
server: string; server: string;
clientId: string; clientId: string;
@@ -58,12 +59,14 @@ class Web {
private buildController: BuildController; private buildController: BuildController;
private app: expressWs.Application; private app: expressWs.Application;
private port: number; private port: number;
private options:WebConfig;
constructor(options: WebConfig = {}) { constructor(options: WebConfig = {}) {
this.initialize(options) this.options = options;
} }
initialize = async (options: WebConfig) => { initialize = async () => {
const options = this.options;
const sessionSecret = process.env['SESSIONSECRET'] || options.sessionSecret; const sessionSecret = process.env['SESSIONSECRET'] || options.sessionSecret;
const sqids = new Sqids({ const sqids = new Sqids({
minLength: 6, minLength: 6,
@@ -141,7 +144,7 @@ class Web {
wsApp.ws(`/${slug}/:id/ws`, async (ws, req) => { wsApp.ws(`/${slug}/:id/ws`, async (ws, req) => {
const build = await getBuildFn(req.params.id); const build = await getBuildFn(req.params.id);
if (! build || (build.status !== 'queued' && build.status !== 'running')) { if (!build || (build.status !== 'queued' && build.status !== 'running')) {
return ws.close(); return ws.close();
} }
console.log('WS Opened'); console.log('WS Opened');
@@ -159,15 +162,29 @@ class Web {
}); });
} }
app.get('/healthcheck', (_, res) => {
res.send('Healthy');
});
if (oidc) { if (oidc) {
if (!sessionSecret) { if (!sessionSecret) {
throw new Error('sessionSecret must be set.'); throw new Error('sessionSecret must be set.');
} }
app.use(session({ app.use(session({
name: 'sessionId',
secret: sessionSecret, secret: sessionSecret,
resave: false, resave: true,
saveUninitialized: false saveUninitialized: false,
store: this.db,
cookie: {
maxAge: notStupidParseInt(process.env['COOKIETTL']) || 1000 * 60 * 60 * 24 * 30, // 30 days
httpOnly: true,
secure: !!options.secure
}
})); }));
passport.use(oidc);
app.use(passport.initialize());
app.use(passport.session());
passport.serializeUser(function (user: User, done) { passport.serializeUser(function (user: User, done) {
done(null, user.id); done(null, user.id);
}); });
@@ -177,12 +194,9 @@ class Web {
done(null, { done(null, {
id: user.id, id: user.id,
username: user.username, username: user.username,
name: user.displayName displayName: user.displayName
}); });
}); });
passport.use(oidc);
app.use(passport.initialize());
app.use(passport.session());
app.get('/login', (req, res) => { app.get('/login', (req, res) => {
if (req?.user) { if (req?.user) {
return res.redirect('/'); return res.redirect('/');
@@ -197,11 +211,7 @@ class Web {
}); });
}); });
app.post('/login', passport.authenticate('openidconnect')); app.post('/login', passport.authenticate('openidconnect'));
app.get('/cb', passport.authenticate('openidconnect', { failureRedirect: '/login', failureMessage: true }), app.get('/cb', passport.authenticate('openidconnect', { successRedirect: '/', failureRedirect: '/login', failureMessage: true }));
function (_, res) {
res.redirect('/');
}
);
app.get('/logout', (req, res) => { app.get('/logout', (req, res) => {
req.logOut((err) => { req.logOut((err) => {
if (err) { if (err) {
@@ -287,9 +297,7 @@ class Web {
res.redirect(`/build/${req.params.id}/`); res.redirect(`/build/${req.params.id}/`);
}); });
app.get('/healthcheck', (_, res) => { this._webserver = this.app.listen(this.port, () => console.log(`archery is running on port ${this.port}`));
res.send('Healthy');
});
} }
close = () => { close = () => {
@@ -300,9 +308,6 @@ class Web {
setDB = (db: DB) => { setDB = (db: DB) => {
this.db = db; this.db = db;
if (!this._webserver) {
this._webserver = this.app.listen(this.port, () => console.log(`archery is running on port ${this.port}`));
}
} }
initializeOIDC = async (options: WebConfig): Promise<OpenIDConnectStrategy | false> => { initializeOIDC = async (options: WebConfig): Promise<OpenIDConnectStrategy | false> => {
@@ -340,5 +345,5 @@ class Web {
} }
export default Web; export default Web;
export { Web }; export { Web, notStupidParseInt };
export type { WebConfig }; export type { WebConfig };

View File

@@ -19,6 +19,7 @@ await new Promise((resolve) => setTimeout(resolve, 1500));
const db = new DB(config.db); const db = new DB(config.db);
web.setDB(db); web.setDB(db);
web.setBuildController(buildController); web.setBuildController(buildController);
web.initialize();
buildController.setDB(db); buildController.setDB(db);
process.on('SIGTERM', () => { process.on('SIGTERM', () => {