Session persistence
This commit is contained in:
parent
c996881547
commit
9b7324e20a
64
package-lock.json
generated
64
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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';
|
||||
|
1
sql/migrations/patches/20191008190200-sessions/down.sql
Normal file
1
sql/migrations/patches/20191008190200-sessions/down.sql
Normal file
@ -0,0 +1 @@
|
||||
DROP TABLE "sessions";
|
10
sql/migrations/patches/20191008190200-sessions/up.sql
Normal file
10
sql/migrations/patches/20191008190200-sessions/up.sql
Normal file
@ -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();
|
1
sql/sessions/all.sql
Normal file
1
sql/sessions/all.sql
Normal file
@ -0,0 +1 @@
|
||||
SELECT "sid", "session" FROM "sessions";
|
1
sql/sessions/clear.sql
Normal file
1
sql/sessions/clear.sql
Normal file
@ -0,0 +1 @@
|
||||
TRUNCATE "sessions";
|
1
sql/sessions/destroy.sql
Normal file
1
sql/sessions/destroy.sql
Normal file
@ -0,0 +1 @@
|
||||
DELETE FROM "sessions" WHERE "sid" = $1;
|
1
sql/sessions/get.sql
Normal file
1
sql/sessions/get.sql
Normal file
@ -0,0 +1 @@
|
||||
SELECT "session" FROM "sessions" WHERE "sid"=$1 AND CURRENT_TIMESTAMP < "expires_at";
|
1
sql/sessions/length.sql
Normal file
1
sql/sessions/length.sql
Normal file
@ -0,0 +1 @@
|
||||
SELECT COUNT(*) as length FROM "sessions";
|
3
sql/sessions/set.sql
Normal file
3
sql/sessions/set.sql
Normal file
@ -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;
|
1
sql/sessions/touch.sql
Normal file
1
sql/sessions/touch.sql
Normal file
@ -0,0 +1 @@
|
||||
UPDATE "sessions" SET "expires_at"=$2 WHERE "sid"=$1;
|
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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user