add session store
This commit is contained in:
146
src/DB.ts
146
src/DB.ts
@@ -1,9 +1,13 @@
|
||||
import { Sequelize, DataTypes, Op, } from 'sequelize';
|
||||
import { Store } from 'express-session'
|
||||
import { notStupidParseInt } from './Web.ts';
|
||||
import type { ModelStatic, Filterable } from 'sequelize';
|
||||
import type { LogType } from './BuildController.ts';
|
||||
import type { SessionData } from 'express-session'
|
||||
|
||||
type Status = 'queued' | 'running' | 'cancelled' | 'success' | 'error';
|
||||
type Dependencies = 'stable' | 'testing' | 'staging';
|
||||
type Callback = (err?: unknown, data?: any) => any
|
||||
|
||||
interface DBConfig {
|
||||
db?: string;
|
||||
@@ -41,7 +45,7 @@ interface LogChunk {
|
||||
chunk: string
|
||||
}
|
||||
|
||||
const MONTH = 1000 * 60 * 60 * 24 * 24;
|
||||
const MONTH = 1000 * 60 * 60 * 24 * 30;
|
||||
const FRESH = {
|
||||
[Op.or]: [
|
||||
{ startTime: { [Op.gt]: new Date(Date.now() - MONTH) } },
|
||||
@@ -50,13 +54,27 @@ const FRESH = {
|
||||
}
|
||||
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 logChunk: ModelStatic<any>;
|
||||
private user: ModelStatic<any>;
|
||||
private session: ModelStatic<any>;
|
||||
private sequelize: Sequelize;
|
||||
private ttl: number;
|
||||
|
||||
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 || '', {
|
||||
host: config.host || 'localhost',
|
||||
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.user.hasMany(this.build);
|
||||
|
||||
@@ -164,7 +192,8 @@ class DB {
|
||||
await this.user.sync();
|
||||
await this.build.sync();
|
||||
await this.logChunk.sync();
|
||||
|
||||
await this.session.sync();
|
||||
|
||||
if (!(await this.getUser('-1'))) {
|
||||
await this.createUser({
|
||||
id: '-1',
|
||||
@@ -329,8 +358,117 @@ class DB {
|
||||
await this.build.destroy({
|
||||
where: {
|
||||
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> {
|
||||
|
||||
47
src/Web.ts
47
src/Web.ts
@@ -15,6 +15,7 @@ import type { BuildController, BuildEvent } from "./BuildController.ts";
|
||||
interface WebConfig {
|
||||
sessionSecret?: string;
|
||||
port?: number;
|
||||
secure?: boolean;
|
||||
oidc?: {
|
||||
server: string;
|
||||
clientId: string;
|
||||
@@ -58,12 +59,14 @@ class Web {
|
||||
private buildController: BuildController;
|
||||
private app: expressWs.Application;
|
||||
private port: number;
|
||||
private 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 sqids = new Sqids({
|
||||
minLength: 6,
|
||||
@@ -141,7 +144,7 @@ class Web {
|
||||
|
||||
wsApp.ws(`/${slug}/:id/ws`, async (ws, req) => {
|
||||
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();
|
||||
}
|
||||
console.log('WS Opened');
|
||||
@@ -159,15 +162,29 @@ class Web {
|
||||
});
|
||||
}
|
||||
|
||||
app.get('/healthcheck', (_, res) => {
|
||||
res.send('Healthy');
|
||||
});
|
||||
|
||||
if (oidc) {
|
||||
if (!sessionSecret) {
|
||||
throw new Error('sessionSecret must be set.');
|
||||
}
|
||||
app.use(session({
|
||||
name: 'sessionId',
|
||||
secret: sessionSecret,
|
||||
resave: false,
|
||||
saveUninitialized: false
|
||||
resave: true,
|
||||
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) {
|
||||
done(null, user.id);
|
||||
});
|
||||
@@ -177,12 +194,9 @@ class Web {
|
||||
done(null, {
|
||||
id: user.id,
|
||||
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) => {
|
||||
if (req?.user) {
|
||||
return res.redirect('/');
|
||||
@@ -197,11 +211,7 @@ class Web {
|
||||
});
|
||||
});
|
||||
app.post('/login', passport.authenticate('openidconnect'));
|
||||
app.get('/cb', passport.authenticate('openidconnect', { failureRedirect: '/login', failureMessage: true }),
|
||||
function (_, res) {
|
||||
res.redirect('/');
|
||||
}
|
||||
);
|
||||
app.get('/cb', passport.authenticate('openidconnect', { successRedirect: '/', failureRedirect: '/login', failureMessage: true }));
|
||||
app.get('/logout', (req, res) => {
|
||||
req.logOut((err) => {
|
||||
if (err) {
|
||||
@@ -287,9 +297,7 @@ class Web {
|
||||
res.redirect(`/build/${req.params.id}/`);
|
||||
});
|
||||
|
||||
app.get('/healthcheck', (_, res) => {
|
||||
res.send('Healthy');
|
||||
});
|
||||
this._webserver = this.app.listen(this.port, () => console.log(`archery is running on port ${this.port}`));
|
||||
}
|
||||
|
||||
close = () => {
|
||||
@@ -300,9 +308,6 @@ class Web {
|
||||
|
||||
setDB = (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> => {
|
||||
@@ -340,5 +345,5 @@ class Web {
|
||||
}
|
||||
|
||||
export default Web;
|
||||
export { Web };
|
||||
export { Web, notStupidParseInt };
|
||||
export type { WebConfig };
|
||||
|
||||
@@ -19,6 +19,7 @@ await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
const db = new DB(config.db);
|
||||
web.setDB(db);
|
||||
web.setBuildController(buildController);
|
||||
web.initialize();
|
||||
buildController.setDB(db);
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
|
||||
Reference in New Issue
Block a user