diff --git a/package-lock.json b/package-lock.json
index 02cf583..bcc7da8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -43,6 +43,33 @@
"integrity": "sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA==",
"dev": true
},
+ "@holdyourwaffle/express-session": {
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/@holdyourwaffle/express-session/-/express-session-1.16.2.tgz",
+ "integrity": "sha512-S4MutTCUvOFawZ2g+PCLkfg7WXK7bR9ZVUCp27X98YBuJLUaRZGO73jVyPKe9S5/rXc2Ppsf36/NwOB9j66n/w==",
+ "requires": {
+ "cookie": "0.4.0",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "~2.0.0",
+ "on-headers": "~1.0.2",
+ "parseurl": "~1.3.3",
+ "safe-buffer": "5.2.0",
+ "uid-safe": "~2.1.5"
+ },
+ "dependencies": {
+ "depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
+ },
+ "safe-buffer": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
+ "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg=="
+ }
+ }
+ },
"@phc/format": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@phc/format/-/format-0.5.0.tgz",
@@ -225,16 +252,6 @@
"@types/range-parser": "*"
}
},
- "@types/express-session": {
- "version": "1.15.14",
- "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.15.14.tgz",
- "integrity": "sha512-7kVzFTT0Jy0zmUYDt9ik76XbcqyS9NalV4gn4eLwhk1nGQn+lS/HjPODhG3Oi/GBR2w1LQHUdkz/5KICYMACiw==",
- "dev": true,
- "requires": {
- "@types/express": "*",
- "@types/node": "*"
- }
- },
"@types/fs-capacitor": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/fs-capacitor/-/fs-capacitor-2.0.0.tgz",
@@ -2653,33 +2670,6 @@
"pino-http": "^4.0.0"
}
},
- "express-session": {
- "version": "1.16.2",
- "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.16.2.tgz",
- "integrity": "sha512-oy0sRsdw6n93E9wpCNWKRnSsxYnSDX9Dnr9mhZgqUEEorzcq5nshGYSZ4ZReHFhKQ80WI5iVUUSPW7u3GaKauw==",
- "requires": {
- "cookie": "0.3.1",
- "cookie-signature": "1.0.6",
- "debug": "2.6.9",
- "depd": "~2.0.0",
- "on-headers": "~1.0.2",
- "parseurl": "~1.3.3",
- "safe-buffer": "5.1.2",
- "uid-safe": "~2.1.5"
- },
- "dependencies": {
- "cookie": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
- "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
- },
- "depd": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
- "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
- }
- }
- },
"extend-shallow": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
diff --git a/package.json b/package.json
index e93cb13..5d0ebb6 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
"license": "AGPL-3.0-or-later",
"private": true,
"dependencies": {
+ "@holdyourwaffle/express-session": "^1.16.2",
"@types/pug": "^2.0.4",
"apollo-server-express": "^2.9.3",
"argon2": "^0.24.1",
@@ -24,7 +25,6 @@
"dotenv": "^8.1.0",
"express": "^4.17.1",
"express-pino-logger": "^4.0.0",
- "express-session": "^1.16.2",
"graphql": "^14.5.7",
"helmet": "^3.21.1",
"http-errors": "^1.7.3",
@@ -44,7 +44,6 @@
"@types/dotenv": "^6.1.1",
"@types/express": "^4.17.1",
"@types/express-pino-logger": "^4.0.1",
- "@types/express-session": "^1.15.14",
"@types/helmet": "0.0.44",
"@types/http-errors": "^1.6.2",
"@types/luxon": "^1.15.2",
diff --git a/sql/migrations/patches/20190925000000-updated-at/up.sql b/sql/migrations/patches/20190925000000-updated-at/up.sql
index 9a6fde1..70c07ed 100644
--- a/sql/migrations/patches/20190925000000-updated-at/up.sql
+++ b/sql/migrations/patches/20190925000000-updated-at/up.sql
@@ -1,11 +1,7 @@
CREATE OR REPLACE FUNCTION set_updated_timestamp()
RETURNS TRIGGER AS $$
BEGIN
- IF row(NEW.*) IS DISTINCT FROM row(OLD.*) THEN
- NEW.updated_at = now();
- RETURN NEW;
- ELSE
- RETURN OLD;
- END IF;
+ NEW.updated_at = now();
+ RETURN NEW;
END;
$$ language 'plpgsql';
diff --git a/sql/migrations/patches/20191008190200-sessions/down.sql b/sql/migrations/patches/20191008190200-sessions/down.sql
new file mode 100644
index 0000000..d49b7ae
--- /dev/null
+++ b/sql/migrations/patches/20191008190200-sessions/down.sql
@@ -0,0 +1 @@
+DROP TABLE "sessions";
\ No newline at end of file
diff --git a/sql/migrations/patches/20191008190200-sessions/up.sql b/sql/migrations/patches/20191008190200-sessions/up.sql
new file mode 100644
index 0000000..a294b45
--- /dev/null
+++ b/sql/migrations/patches/20191008190200-sessions/up.sql
@@ -0,0 +1,10 @@
+CREATE TABLE "sessions" (
+ "sid" text NOT NULL COLLATE "default",
+ "session" json NOT NULL,
+ "expires_at" timestamptz NOT NULL,
+ "created_at" timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updated_at" timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+ALTER TABLE "sessions" ADD CONSTRAINT "session_pkey" PRIMARY KEY ("sid") NOT DEFERRABLE INITIALLY IMMEDIATE;
+CREATE TRIGGER "set_sessions_updated" BEFORE UPDATE ON "sessions" FOR EACH ROW EXECUTE PROCEDURE set_updated_timestamp();
diff --git a/sql/sessions/all.sql b/sql/sessions/all.sql
new file mode 100644
index 0000000..1a8241b
--- /dev/null
+++ b/sql/sessions/all.sql
@@ -0,0 +1 @@
+SELECT "sid", "session" FROM "sessions";
diff --git a/sql/sessions/clear.sql b/sql/sessions/clear.sql
new file mode 100644
index 0000000..e2ddaa4
--- /dev/null
+++ b/sql/sessions/clear.sql
@@ -0,0 +1 @@
+TRUNCATE "sessions";
\ No newline at end of file
diff --git a/sql/sessions/destroy.sql b/sql/sessions/destroy.sql
new file mode 100644
index 0000000..8f163e8
--- /dev/null
+++ b/sql/sessions/destroy.sql
@@ -0,0 +1 @@
+DELETE FROM "sessions" WHERE "sid" = $1;
\ No newline at end of file
diff --git a/sql/sessions/get.sql b/sql/sessions/get.sql
new file mode 100644
index 0000000..9d790a0
--- /dev/null
+++ b/sql/sessions/get.sql
@@ -0,0 +1 @@
+SELECT "session" FROM "sessions" WHERE "sid"=$1 AND CURRENT_TIMESTAMP < "expires_at";
\ No newline at end of file
diff --git a/sql/sessions/length.sql b/sql/sessions/length.sql
new file mode 100644
index 0000000..78a579d
--- /dev/null
+++ b/sql/sessions/length.sql
@@ -0,0 +1 @@
+SELECT COUNT(*) as length FROM "sessions";
\ No newline at end of file
diff --git a/sql/sessions/set.sql b/sql/sessions/set.sql
new file mode 100644
index 0000000..2e43e9f
--- /dev/null
+++ b/sql/sessions/set.sql
@@ -0,0 +1,3 @@
+INSERT INTO "sessions" AS s ("sid", "session", "expires_at") VALUES ($1, $2, $3)
+ ON CONFLICT ON CONSTRAINT "session_pkey"
+ DO UPDATE SET "session"=$2, "expires_at"=$3 WHERE "s"."sid"=$1;
diff --git a/sql/sessions/touch.sql b/sql/sessions/touch.sql
new file mode 100644
index 0000000..788e45d
--- /dev/null
+++ b/sql/sessions/touch.sql
@@ -0,0 +1 @@
+UPDATE "sessions" SET "expires_at"=$2 WHERE "sid"=$1;
\ No newline at end of file
diff --git a/src/custom.d.ts b/src/custom.d.ts
index 5f37445..15c4caa 100644
--- a/src/custom.d.ts
+++ b/src/custom.d.ts
@@ -14,6 +14,7 @@
// along with this program. If not, see .
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;
+ }
}
diff --git a/src/db/index.ts b/src/db/index.ts
index a444a5d..ad8b032 100644
--- a/src/db/index.ts
+++ b/src/db/index.ts
@@ -16,6 +16,7 @@
import {
Extensions,
MigrationRepository,
+ SessionRepository,
TaskRepository,
UserRepository
} from "@kredens/db/repos";
@@ -34,6 +35,7 @@ const initOptions: IInitOptions = {
obj.migrations = new MigrationRepository(obj, pgp);
obj.tasks = new TaskRepository(obj, pgp);
obj.users = new UserRepository(obj, pgp);
+ obj.sessions = new SessionRepository(obj, pgp);
}
};
diff --git a/src/db/models.ts b/src/db/models.ts
index 6405062..5022164 100644
--- a/src/db/models.ts
+++ b/src/db/models.ts
@@ -38,3 +38,8 @@ export interface Task {
max_frequency?: number;
created_at: DateTime;
}
+
+export interface Session {
+ sid: string;
+ session: SessionData;
+}
diff --git a/src/db/repos/index.ts b/src/db/repos/index.ts
index 365a072..d70efb2 100644
--- a/src/db/repos/index.ts
+++ b/src/db/repos/index.ts
@@ -14,13 +14,20 @@
// along with this program. If not, see .
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
+};
diff --git a/src/db/repos/sessions.ts b/src/db/repos/sessions.ts
new file mode 100644
index 0000000..51e1872
--- /dev/null
+++ b/src/db/repos/sessions.ts
@@ -0,0 +1,70 @@
+// Copyright (C) 2019 ModZero
+//
+// 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 .
+
+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;
+
+ constructor(db: IDatabase, pgp: IMain) {
+ this.db = db;
+ }
+
+ public async all(): Promise {
+ 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> {
+ return this.db.oneOrNone(sql.get, [sid]).then(row =>
+ row
+ ? Maybe.Some({
+ session: row.session,
+ sid: row.sid
+ })
+ : Maybe.None()
+ );
+ }
+
+ public async length(): Promise {
+ 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()]);
+ }
+}
diff --git a/src/db/sql/index.ts b/src/db/sql/index.ts
index a740829..e334f74 100644
--- a/src/db/sql/index.ts
+++ b/src/db/sql/index.ts
@@ -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 {
diff --git a/src/index.ts b/src/index.ts
index 78a97c5..d6e8be9 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -13,19 +13,21 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
+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") {
diff --git a/src/sessions.ts b/src/sessions.ts
new file mode 100644
index 0000000..97cfe6e
--- /dev/null
+++ b/src/sessions.ts
@@ -0,0 +1,99 @@
+// Copyright (C) 2019 ModZero
+//
+// 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 .
+
+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);
+ }
+ }
+}