Session persistence
This commit is contained in:
7
src/custom.d.ts
vendored
7
src/custom.d.ts
vendored
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -38,3 +38,8 @@ export interface Task {
|
||||
max_frequency?: number;
|
||||
created_at: DateTime;
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
sid: string;
|
||||
session: SessionData;
|
||||
}
|
||||
|
||||
@@ -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
70
src/db/repos/sessions.ts
Normal 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()]);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
99
src/sessions.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user