add session store
This commit is contained in:
144
src/DB.ts
144
src/DB.ts
@@ -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> {
|
||||||
|
|||||||
47
src/Web.ts
47
src/Web.ts
@@ -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 };
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user