Session persistence

This commit is contained in:
2019-10-08 23:24:25 +02:00
parent c996881547
commit 9b7324e20a
20 changed files with 258 additions and 50 deletions

7
src/custom.d.ts vendored
View File

@@ -14,6 +14,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { User } from "@kredens/db/models";
import { Cookie } from "@holdyourwaffle/express-session";
declare global {
namespace Express {
@@ -21,4 +22,10 @@ declare global {
user?: User;
}
}
interface SessionData {
cookie: Cookie;
userID?: number;
csrfToken: string;
}
}

View File

@@ -16,6 +16,7 @@
import {
Extensions,
MigrationRepository,
SessionRepository,
TaskRepository,
UserRepository
} from "@kredens/db/repos";
@@ -34,6 +35,7 @@ const initOptions: IInitOptions<Extensions> = {
obj.migrations = new MigrationRepository(obj, pgp);
obj.tasks = new TaskRepository(obj, pgp);
obj.users = new UserRepository(obj, pgp);
obj.sessions = new SessionRepository(obj, pgp);
}
};

View File

@@ -38,3 +38,8 @@ export interface Task {
max_frequency?: number;
created_at: DateTime;
}
export interface Session {
sid: string;
session: SessionData;
}

View File

@@ -14,13 +14,20 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { MigrationRepository } from "@kredens/db/repos/migrations";
import { SessionRepository } from "@kredens/db/repos/sessions";
import { TaskRepository } from "@kredens/db/repos/tasks";
import { UserRepository } from "@kredens/db/repos/users";
export interface Extensions {
migrations: MigrationRepository;
users: UserRepository;
sessions: SessionRepository;
tasks: TaskRepository;
users: UserRepository;
}
export { MigrationRepository, UserRepository, TaskRepository };
export {
MigrationRepository,
UserRepository,
SessionRepository,
TaskRepository
};

70
src/db/repos/sessions.ts Normal file
View File

@@ -0,0 +1,70 @@
// Copyright (C) 2019 ModZero <modzero@modzero.xyz>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Session } from "@kredens/db/models";
import { sessions as sql } from "@kredens/db/sql";
import { DateTime } from "luxon";
import { Maybe } from "monet";
import { IDatabase, IMain } from "pg-promise";
export class SessionRepository {
private db: IDatabase<any>;
constructor(db: IDatabase<any>, pgp: IMain) {
this.db = db;
}
public async all(): Promise<Session[]> {
return this.db.map(sql.all, [], row => ({
session: row.session,
sid: row.sid
}));
}
public async clear() {
return this.db.none(sql.clear);
}
public async destroy(sid: string) {
return this.db.none(sql.destroy, [sid]);
}
public async get(sid: string): Promise<Maybe<Session>> {
return this.db.oneOrNone(sql.get, [sid]).then(row =>
row
? Maybe.Some({
session: row.session,
sid: row.sid
})
: Maybe.None()
);
}
public async length(): Promise<number> {
return this.db.one(sql.length).then(row => +row.length);
}
public async set(sid: string, session: SessionData, expiresAt: DateTime) {
return this.db.none(sql.set, [
sid,
JSON.stringify(session),
expiresAt.toSQL()
]);
}
public async touch(sid: string, expiresAt: DateTime) {
return this.db.none(sql.touch, [sid, expiresAt.toSQL()]);
}
}

View File

@@ -45,7 +45,17 @@ const tasks = {
list: sql("tasks/list.sql")
};
export { migrations, users, tasks };
const sessions = {
all: sql("sessions/all.sql"),
clear: sql("sessions/clear.sql"),
destroy: sql("sessions/destroy.sql"),
get: sql("sessions/get.sql"),
length: sql("sessions/length.sql"),
set: sql("sessions/set.sql"),
touch: sql("sessions/touch.sql")
};
export { migrations, users, tasks, sessions };
/** Helper for linking to external query files */
function sql(file: string): QueryFile {

View File

@@ -13,19 +13,21 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import session, { SessionOptions } from "@holdyourwaffle/express-session";
import { server as graphqlServer } from "@kredens/api";
import { authMiddleware } from "@kredens/auth";
import { db } from "@kredens/db";
import logger from "@kredens/logger";
import indexRouter from "@kredens/routes/";
import bootstrapRouter from "@kredens/routes/bootstrap";
import { PgStore } from "@kredens/sessions";
import cookieParser from "cookie-parser";
import csrf from "csurf";
import express from "express";
import pinoExpress from "express-pino-logger";
import session, { SessionOptions } from "express-session";
import helmet from "helmet";
import createHttpError from "http-errors";
async function main() {
await db.tx(async t => {
await t.migrations.create();
@@ -59,7 +61,8 @@ async function main() {
const sessionOptions: SessionOptions = {
resave: false,
saveUninitialized: false,
secret: process.env.SECRET
secret: process.env.SECRET,
store: new PgStore()
};
if (app.get("env") === "production") {

99
src/sessions.ts Normal file
View File

@@ -0,0 +1,99 @@
// Copyright (C) 2019 ModZero <modzero@modzero.xyz>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Store } from "@holdyourwaffle/express-session";
import { db } from "@kredens/db";
import { DateTime, Duration } from "luxon";
export class PgStore extends Store {
private ttl: Duration;
constructor(options: { ttl?: Duration } = {}) {
super();
this.ttl = options.ttl || Duration.fromObject({ days: 1 }); // One day in seconds.
}
public get(
sid: string,
cb: (err: any, session?: SessionData | null) => void
) {
db.sessions
.get(sid)
.then(s => cb(null, s.map(ss => ss.session).orNull()))
.catch(r => cb(r, null));
}
public set(sid: string, session: SessionData, cb?: (err?: any) => void) {
const expiresAt = this.getExpiresAt(session);
const p = db.sessions.set(sid, session, expiresAt);
if (cb) {
p.then(s => cb(null)).catch(r => cb(r));
}
}
public destroy(sid: string, cb?: (err?: any) => void) {
const p = db.sessions.destroy(sid);
if (cb) {
p.then(s => cb()).catch(r => cb(r));
}
}
public all(
cb: (err: any, obj?: { [sid: string]: SessionData } | null) => void
) {
db.sessions
.all()
.then(ss => {
const sessions: { [sid: string]: SessionData } = {};
for (const s of ss) {
sessions[s.sid] = s.session;
}
cb(null, sessions);
})
.catch(r => cb(r, null));
}
public length(cb: (err: any, length: number) => void) {
db.sessions
.length()
.then(l => cb(null, l))
.catch(r => cb(r, 0));
}
public clear(cb?: (err?: any) => void) {
const p = db.sessions.clear();
if (cb) {
p.then(() => cb()).catch(r => cb(r));
}
}
public touch(sid: string, session: SessionData, cb?: (err?: any) => void) {
const p = db.sessions.touch(sid, this.getExpiresAt(session));
if (cb) {
p.then(() => cb()).catch(r => cb(r));
}
}
private getExpiresAt(session: SessionData) {
if (session && session.cookie && session.cookie.expires) {
return DateTime.fromJSDate(session.cookie.expires);
} else {
return DateTime.local().plus(this.ttl);
}
}
}