session management
This commit is contained in:
parent
94e1f9ebe1
commit
8f503b9c5a
28
README.md
28
README.md
|
@ -2,10 +2,24 @@
|
||||||
Backend for movie-web
|
Backend for movie-web
|
||||||
|
|
||||||
## Todo list
|
## Todo list
|
||||||
- [ ] endpoint to consume provider metrics
|
- [ ] standard endpoints:
|
||||||
- [ ] register endpoint
|
- [ ] make account (PFP, account name)
|
||||||
- [ ] remove user endpoint
|
- [ ] login
|
||||||
- [ ] ratelimits for all endpoints
|
- [X] logout a session
|
||||||
- [ ] metrics for all http requests
|
- [ ] read all sessions from logged in user
|
||||||
- [ ] session CRUD endpoints
|
- [ ] edit current session device name
|
||||||
- [ ] data save endpoints
|
- [ ] edit account name and PFP
|
||||||
|
- [ ] delete logged in user
|
||||||
|
- [ ] backend meta (name and description)
|
||||||
|
- [ ] upsert settings
|
||||||
|
- [ ] upsert watched items
|
||||||
|
- [ ] upsert bookmarks
|
||||||
|
- [ ] consume provider metrics
|
||||||
|
- [ ] prometheus metrics
|
||||||
|
- [ ] requests
|
||||||
|
- [ ] user count
|
||||||
|
- [ ] provider metrics
|
||||||
|
- [ ] ratelimits (stored in redis)
|
||||||
|
- [ ] switch to pnpm
|
||||||
|
- [ ] think of privacy centric method of auth
|
||||||
|
- [ ] global namespacing (accounts are stored on a namespace)
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
"@mikro-orm/postgresql": "^5.9.0",
|
"@mikro-orm/postgresql": "^5.9.0",
|
||||||
"fastify": "^4.21.0",
|
"fastify": "^4.21.0",
|
||||||
"fastify-type-provider-zod": "^1.1.9",
|
"fastify-type-provider-zod": "^1.1.9",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"neat-config": "^2.0.0",
|
"neat-config": "^2.0.0",
|
||||||
"type-fest": "^4.2.0",
|
"type-fest": "^4.2.0",
|
||||||
"winston": "^3.10.0",
|
"winston": "^3.10.0",
|
||||||
|
@ -20,6 +21,7 @@
|
||||||
"zod": "^3.22.2"
|
"zod": "^3.22.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/jsonwebtoken": "^9.0.4",
|
||||||
"@types/node": "^20.5.3",
|
"@types/node": "^20.5.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
||||||
"@typescript-eslint/parser": "^6.4.1",
|
"@typescript-eslint/parser": "^6.4.1",
|
||||||
|
@ -599,6 +601,15 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/jsonwebtoken": {
|
||||||
|
"version": "9.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.4.tgz",
|
||||||
|
"integrity": "sha512-8UYapdmR0QlxgvJmyE8lP7guxD0UGVMfknsdtCFZh4ovShdBl3iOI4zdvqBHrB/IS+xUj3PSx73Qkey1fhWz+g==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.5.3",
|
"version": "20.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.3.tgz",
|
||||||
|
@ -1118,6 +1129,11 @@
|
||||||
"ieee754": "^1.2.1"
|
"ieee754": "^1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-equal-constant-time": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
|
||||||
|
},
|
||||||
"node_modules/buffer-writer": {
|
"node_modules/buffer-writer": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
|
||||||
|
@ -1478,6 +1494,14 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/ecdsa-sig-formatter": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
@ -2663,6 +2687,46 @@
|
||||||
"graceful-fs": "^4.1.6"
|
"graceful-fs": "^4.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jsonwebtoken": {
|
||||||
|
"version": "9.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
||||||
|
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"jws": "^3.2.2",
|
||||||
|
"lodash.includes": "^4.3.0",
|
||||||
|
"lodash.isboolean": "^3.0.3",
|
||||||
|
"lodash.isinteger": "^4.0.4",
|
||||||
|
"lodash.isnumber": "^3.0.3",
|
||||||
|
"lodash.isplainobject": "^4.0.6",
|
||||||
|
"lodash.isstring": "^4.0.1",
|
||||||
|
"lodash.once": "^4.0.0",
|
||||||
|
"ms": "^2.1.1",
|
||||||
|
"semver": "^7.5.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12",
|
||||||
|
"npm": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jwa": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-equal-constant-time": "1.0.1",
|
||||||
|
"ecdsa-sig-formatter": "1.0.11",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jws": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
||||||
|
"dependencies": {
|
||||||
|
"jwa": "^1.4.1",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/knex": {
|
"node_modules/knex": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/knex/-/knex-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/knex/-/knex-2.5.1.tgz",
|
||||||
|
@ -2781,6 +2845,36 @@
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.includes": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isboolean": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isinteger": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isnumber": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isplainobject": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isstring": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
|
||||||
|
},
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
|
@ -2788,6 +2882,11 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.once": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
|
||||||
|
},
|
||||||
"node_modules/logform": {
|
"node_modules/logform": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/logform/-/logform-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/logform/-/logform-2.5.1.tgz",
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
"build:compile": "tsc && tsc-alias"
|
"build:compile": "tsc && tsc-alias"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/jsonwebtoken": "^9.0.4",
|
||||||
"@types/node": "^20.5.3",
|
"@types/node": "^20.5.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
||||||
"@typescript-eslint/parser": "^6.4.1",
|
"@typescript-eslint/parser": "^6.4.1",
|
||||||
|
@ -36,6 +37,7 @@
|
||||||
"@mikro-orm/postgresql": "^5.9.0",
|
"@mikro-orm/postgresql": "^5.9.0",
|
||||||
"fastify": "^4.21.0",
|
"fastify": "^4.21.0",
|
||||||
"fastify-type-provider-zod": "^1.1.9",
|
"fastify-type-provider-zod": "^1.1.9",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"neat-config": "^2.0.0",
|
"neat-config": "^2.0.0",
|
||||||
"type-fest": "^4.2.0",
|
"type-fest": "^4.2.0",
|
||||||
"winston": "^3.10.0",
|
"winston": "^3.10.0",
|
||||||
|
|
|
@ -12,4 +12,7 @@ export const devFragment: FragmentSchema = {
|
||||||
postgres: {
|
postgres: {
|
||||||
syncSchema: true,
|
syncSchema: true,
|
||||||
},
|
},
|
||||||
|
crypto: {
|
||||||
|
sessionSecret: 'aINCithRivERecKENdmANDRaNKenSiNi',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -38,4 +38,8 @@ export const configSchema = z.object({
|
||||||
// it is extremely destructive, do not use it EVER in production
|
// it is extremely destructive, do not use it EVER in production
|
||||||
syncSchema: z.coerce.boolean().default(false),
|
syncSchema: z.coerce.boolean().default(false),
|
||||||
}),
|
}),
|
||||||
|
crypto: z.object({
|
||||||
|
// session secret. used for signing session tokens
|
||||||
|
sessionSecret: z.string().min(32),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { Entity, PrimaryKey, Property } from '@mikro-orm/core';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
|
||||||
|
@Entity({ tableName: 'sessions' })
|
||||||
|
export class Session {
|
||||||
|
@PrimaryKey({ name: 'id', type: 'uuid' })
|
||||||
|
id: string = randomUUID();
|
||||||
|
|
||||||
|
@Property({ name: 'user', type: 'uuid' })
|
||||||
|
user!: string;
|
||||||
|
|
||||||
|
@Property({ type: 'date' })
|
||||||
|
createdAt: Date = new Date();
|
||||||
|
|
||||||
|
@Property({ type: 'date' })
|
||||||
|
accessedAt!: Date;
|
||||||
|
|
||||||
|
@Property({ type: 'date' })
|
||||||
|
expiresAt!: Date;
|
||||||
|
|
||||||
|
@Property({ type: 'text' })
|
||||||
|
device!: string;
|
||||||
|
|
||||||
|
@Property({ type: 'text' })
|
||||||
|
userAgent!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionDTO {
|
||||||
|
id: string;
|
||||||
|
user: string;
|
||||||
|
createdAt: string;
|
||||||
|
accessedAt: string;
|
||||||
|
device: string;
|
||||||
|
userAgent: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSession(session: Session): SessionDTO {
|
||||||
|
return {
|
||||||
|
id: session.id,
|
||||||
|
user: session.id,
|
||||||
|
createdAt: session.createdAt.toISOString(),
|
||||||
|
accessedAt: session.accessedAt.toISOString(),
|
||||||
|
device: session.device,
|
||||||
|
userAgent: session.userAgent,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
|
import { formatSession } from '@/db/models/Session';
|
||||||
import { User, formatUser } from '@/db/models/User';
|
import { User, formatUser } from '@/db/models/User';
|
||||||
import { handle } from '@/services/handler';
|
import { handle } from '@/services/handler';
|
||||||
import { makeRouter } from '@/services/router';
|
import { makeRouter } from '@/services/router';
|
||||||
|
import { makeSession, makeSessionToken } from '@/services/session';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const registerSchema = z.object({
|
const registerSchema = z.object({
|
||||||
|
@ -12,13 +14,22 @@ export const manageAuthRouter = makeRouter((app) => {
|
||||||
app.post(
|
app.post(
|
||||||
'/auth/register',
|
'/auth/register',
|
||||||
{ schema: { body: registerSchema } },
|
{ schema: { body: registerSchema } },
|
||||||
handle(({ em, body }) => {
|
handle(async ({ em, body, req }) => {
|
||||||
const user = new User();
|
const user = new User();
|
||||||
user.name = body.name;
|
user.name = body.name;
|
||||||
em.persistAndFlush(user);
|
const session = makeSession(
|
||||||
|
user.id,
|
||||||
|
body.device,
|
||||||
|
req.headers['user-agent'],
|
||||||
|
);
|
||||||
|
|
||||||
|
em.persist([user, session]);
|
||||||
|
await em.flush();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user: formatUser(user),
|
user: formatUser(user),
|
||||||
|
session: formatSession(session),
|
||||||
|
token: makeSessionToken(session),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { Session } from '@/db/models/Session';
|
||||||
|
import { StatusError } from '@/services/error';
|
||||||
|
import { handle } from '@/services/handler';
|
||||||
|
import { makeRouter } from '@/services/router';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const sessionRouter = makeRouter((app) => {
|
||||||
|
app.delete(
|
||||||
|
'/auth/session/:sid',
|
||||||
|
{
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
sid: z.string(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handle(async ({ auth, params, em }) => {
|
||||||
|
auth.assert();
|
||||||
|
|
||||||
|
const targetedSession = await em.findOne(Session, { id: params.sid });
|
||||||
|
if (!targetedSession) return true; // already deleted
|
||||||
|
|
||||||
|
if (targetedSession.user !== auth.user.id)
|
||||||
|
throw new StatusError('Cant delete sessions you dont own', 401);
|
||||||
|
|
||||||
|
await em.removeAndFlush(targetedSession);
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
|
@ -1,5 +1,62 @@
|
||||||
|
import { Session } from '@/db/models/Session';
|
||||||
|
import { User } from '@/db/models/User';
|
||||||
import { Roles } from '@/services/access';
|
import { Roles } from '@/services/access';
|
||||||
|
import { StatusError } from '@/services/error';
|
||||||
|
import { getSessionAndBump, verifySessionToken } from '@/services/session';
|
||||||
|
import { EntityManager } from '@mikro-orm/postgresql';
|
||||||
|
import { FastifyRequest } from 'fastify';
|
||||||
|
|
||||||
export function assertHasRole(_role: Roles) {
|
export function makeAuthContext(manager: EntityManager, req: FastifyRequest) {
|
||||||
throw new Error('requires role');
|
let userCache: User | null = null;
|
||||||
|
let sessionCache: Session | null = null;
|
||||||
|
const em = manager.fork();
|
||||||
|
|
||||||
|
return {
|
||||||
|
getSessionId(): string | null {
|
||||||
|
const header = req.headers.authorization;
|
||||||
|
if (!header) return null;
|
||||||
|
const [type, token] = header.split(' ', 2);
|
||||||
|
if (type.toLowerCase() !== 'Bearer')
|
||||||
|
throw new StatusError('Invalid auth', 400);
|
||||||
|
const payload = verifySessionToken(token);
|
||||||
|
if (!payload) throw new StatusError('Invalid auth', 400);
|
||||||
|
return payload.sid;
|
||||||
|
},
|
||||||
|
async getSession() {
|
||||||
|
if (sessionCache) return sessionCache;
|
||||||
|
const sid = this.getSessionId();
|
||||||
|
if (!sid) return null;
|
||||||
|
const session = await getSessionAndBump(em, sid);
|
||||||
|
if (session) return null;
|
||||||
|
sessionCache = session;
|
||||||
|
return session;
|
||||||
|
},
|
||||||
|
async getUser() {
|
||||||
|
if (userCache) return userCache;
|
||||||
|
const session = await this.getSession();
|
||||||
|
if (!session) return null;
|
||||||
|
const user = await em.findOne(User, { id: session.user });
|
||||||
|
if (!user) return null;
|
||||||
|
userCache = user;
|
||||||
|
return user;
|
||||||
|
},
|
||||||
|
async assert() {
|
||||||
|
const user = await this.getUser();
|
||||||
|
if (!user) throw new StatusError('Not logged in', 403);
|
||||||
|
return user;
|
||||||
|
},
|
||||||
|
get user() {
|
||||||
|
if (!userCache) throw new Error('call assert before getting user');
|
||||||
|
return userCache;
|
||||||
|
},
|
||||||
|
get session() {
|
||||||
|
if (!sessionCache) throw new Error('call assert before getting session');
|
||||||
|
return sessionCache;
|
||||||
|
},
|
||||||
|
async assertHasRole(role: Roles) {
|
||||||
|
const user = await this.assert();
|
||||||
|
const hasRole = user.roles.includes(role);
|
||||||
|
if (!hasRole) throw new StatusError('No permissions', 401);
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
export class StatusError extends Error {
|
||||||
|
errorStatusCode: number;
|
||||||
|
|
||||||
|
constructor(message: string, code: number) {
|
||||||
|
super(message);
|
||||||
|
this.errorStatusCode = code;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import { getORM } from '@/modules/mikro';
|
import { getORM } from '@/modules/mikro';
|
||||||
|
import { makeAuthContext } from '@/services/auth';
|
||||||
import { EntityManager } from '@mikro-orm/postgresql';
|
import { EntityManager } from '@mikro-orm/postgresql';
|
||||||
import {
|
import {
|
||||||
ContextConfigDefault,
|
ContextConfigDefault,
|
||||||
|
@ -73,6 +74,7 @@ export type RequestContext<
|
||||||
Logger
|
Logger
|
||||||
>['query'];
|
>['query'];
|
||||||
em: EntityManager;
|
em: EntityManager;
|
||||||
|
auth: ReturnType<typeof makeAuthContext>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function handle<
|
export function handle<
|
||||||
|
@ -112,6 +114,7 @@ export function handle<
|
||||||
Logger
|
Logger
|
||||||
> {
|
> {
|
||||||
const reqHandler: any = async (req: any, res: any) => {
|
const reqHandler: any = async (req: any, res: any) => {
|
||||||
|
const em = getORM().em.fork();
|
||||||
res.send(
|
res.send(
|
||||||
await handler({
|
await handler({
|
||||||
req,
|
req,
|
||||||
|
@ -119,7 +122,8 @@ export function handle<
|
||||||
body: req.body,
|
body: req.body,
|
||||||
params: req.params,
|
params: req.params,
|
||||||
query: req.query,
|
query: req.query,
|
||||||
em: getORM().em.fork(),
|
em,
|
||||||
|
auth: makeAuthContext(em, req),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { conf } from '@/config';
|
||||||
|
import { Session } from '@/db/models/Session';
|
||||||
|
import { EntityManager } from '@mikro-orm/postgresql';
|
||||||
|
import { sign, verify } from 'jsonwebtoken';
|
||||||
|
|
||||||
|
// 21 days in ms
|
||||||
|
const SESSION_EXPIRY_MS = 21 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
export async function getSession(
|
||||||
|
em: EntityManager,
|
||||||
|
id: string,
|
||||||
|
): Promise<Session | null> {
|
||||||
|
const session = await em.findOne(Session, { id });
|
||||||
|
if (!session) return null;
|
||||||
|
|
||||||
|
if (session.expiresAt < new Date()) return null;
|
||||||
|
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSessionAndBump(
|
||||||
|
em: EntityManager,
|
||||||
|
id: string,
|
||||||
|
): Promise<Session | null> {
|
||||||
|
const session = await getSession(em, id);
|
||||||
|
if (!session) return null;
|
||||||
|
em.assign(session, {
|
||||||
|
accessedAt: new Date(),
|
||||||
|
expiresAt: new Date(Date.now() + SESSION_EXPIRY_MS),
|
||||||
|
});
|
||||||
|
await em.flush();
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeSession(
|
||||||
|
user: string,
|
||||||
|
device: string,
|
||||||
|
userAgent?: string,
|
||||||
|
): Session {
|
||||||
|
if (!userAgent) throw new Error('No useragent provided');
|
||||||
|
|
||||||
|
const session = new Session();
|
||||||
|
session.accessedAt = new Date();
|
||||||
|
session.createdAt = new Date();
|
||||||
|
session.expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS);
|
||||||
|
session.userAgent = userAgent;
|
||||||
|
session.device = device;
|
||||||
|
session.user = user;
|
||||||
|
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeSessionToken(session: Session): string {
|
||||||
|
return sign({ sid: session.id }, conf.crypto.sessionSecret, {
|
||||||
|
algorithm: 'ES512',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verifySessionToken(token: string): { sid: string } | null {
|
||||||
|
try {
|
||||||
|
const payload = verify(token, conf.crypto.sessionSecret, {
|
||||||
|
algorithms: ['ES512'],
|
||||||
|
});
|
||||||
|
if (typeof payload === 'string') return null;
|
||||||
|
return payload as { sid: string };
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
89
yarn.lock
89
yarn.lock
|
@ -265,6 +265,13 @@
|
||||||
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz"
|
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz"
|
||||||
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
|
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
|
||||||
|
|
||||||
|
"@types/jsonwebtoken@^9.0.4":
|
||||||
|
version "9.0.4"
|
||||||
|
resolved "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.4.tgz"
|
||||||
|
integrity sha512-8UYapdmR0QlxgvJmyE8lP7guxD0UGVMfknsdtCFZh4ovShdBl3iOI4zdvqBHrB/IS+xUj3PSx73Qkey1fhWz+g==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/node@*", "@types/node@^20.5.3":
|
"@types/node@*", "@types/node@^20.5.3":
|
||||||
version "20.5.3"
|
version "20.5.3"
|
||||||
resolved "https://registry.npmjs.org/@types/node/-/node-20.5.3.tgz"
|
resolved "https://registry.npmjs.org/@types/node/-/node-20.5.3.tgz"
|
||||||
|
@ -549,6 +556,11 @@ braces@^3.0.2, braces@~3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
fill-range "^7.0.1"
|
fill-range "^7.0.1"
|
||||||
|
|
||||||
|
buffer-equal-constant-time@1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz"
|
||||||
|
integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==
|
||||||
|
|
||||||
buffer-writer@2.0.0:
|
buffer-writer@2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz"
|
resolved "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz"
|
||||||
|
@ -760,6 +772,13 @@ eastasianwidth@^0.2.0:
|
||||||
resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz"
|
resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz"
|
||||||
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
|
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
|
||||||
|
|
||||||
|
ecdsa-sig-formatter@1.0.11:
|
||||||
|
version "1.0.11"
|
||||||
|
resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz"
|
||||||
|
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
|
||||||
|
dependencies:
|
||||||
|
safe-buffer "^5.0.1"
|
||||||
|
|
||||||
emoji-regex@^8.0.0:
|
emoji-regex@^8.0.0:
|
||||||
version "8.0.0"
|
version "8.0.0"
|
||||||
resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz"
|
resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz"
|
||||||
|
@ -1429,6 +1448,39 @@ jsonfile@^6.0.1:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
graceful-fs "^4.1.6"
|
graceful-fs "^4.1.6"
|
||||||
|
|
||||||
|
jsonwebtoken@^9.0.2:
|
||||||
|
version "9.0.2"
|
||||||
|
resolved "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz"
|
||||||
|
integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==
|
||||||
|
dependencies:
|
||||||
|
jws "^3.2.2"
|
||||||
|
lodash.includes "^4.3.0"
|
||||||
|
lodash.isboolean "^3.0.3"
|
||||||
|
lodash.isinteger "^4.0.4"
|
||||||
|
lodash.isnumber "^3.0.3"
|
||||||
|
lodash.isplainobject "^4.0.6"
|
||||||
|
lodash.isstring "^4.0.1"
|
||||||
|
lodash.once "^4.0.0"
|
||||||
|
ms "^2.1.1"
|
||||||
|
semver "^7.5.4"
|
||||||
|
|
||||||
|
jwa@^1.4.1:
|
||||||
|
version "1.4.1"
|
||||||
|
resolved "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz"
|
||||||
|
integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==
|
||||||
|
dependencies:
|
||||||
|
buffer-equal-constant-time "1.0.1"
|
||||||
|
ecdsa-sig-formatter "1.0.11"
|
||||||
|
safe-buffer "^5.0.1"
|
||||||
|
|
||||||
|
jws@^3.2.2:
|
||||||
|
version "3.2.2"
|
||||||
|
resolved "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz"
|
||||||
|
integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
|
||||||
|
dependencies:
|
||||||
|
jwa "^1.4.1"
|
||||||
|
safe-buffer "^5.0.1"
|
||||||
|
|
||||||
knex@2.5.1:
|
knex@2.5.1:
|
||||||
version "2.5.1"
|
version "2.5.1"
|
||||||
resolved "https://registry.npmjs.org/knex/-/knex-2.5.1.tgz"
|
resolved "https://registry.npmjs.org/knex/-/knex-2.5.1.tgz"
|
||||||
|
@ -1478,11 +1530,46 @@ locate-path@^6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-locate "^5.0.0"
|
p-locate "^5.0.0"
|
||||||
|
|
||||||
|
lodash.includes@^4.3.0:
|
||||||
|
version "4.3.0"
|
||||||
|
resolved "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz"
|
||||||
|
integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==
|
||||||
|
|
||||||
|
lodash.isboolean@^3.0.3:
|
||||||
|
version "3.0.3"
|
||||||
|
resolved "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz"
|
||||||
|
integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==
|
||||||
|
|
||||||
|
lodash.isinteger@^4.0.4:
|
||||||
|
version "4.0.4"
|
||||||
|
resolved "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz"
|
||||||
|
integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==
|
||||||
|
|
||||||
|
lodash.isnumber@^3.0.3:
|
||||||
|
version "3.0.3"
|
||||||
|
resolved "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz"
|
||||||
|
integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==
|
||||||
|
|
||||||
|
lodash.isplainobject@^4.0.6:
|
||||||
|
version "4.0.6"
|
||||||
|
resolved "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz"
|
||||||
|
integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==
|
||||||
|
|
||||||
|
lodash.isstring@^4.0.1:
|
||||||
|
version "4.0.1"
|
||||||
|
resolved "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz"
|
||||||
|
integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==
|
||||||
|
|
||||||
lodash.merge@^4.6.2:
|
lodash.merge@^4.6.2:
|
||||||
version "4.6.2"
|
version "4.6.2"
|
||||||
resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"
|
resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"
|
||||||
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
||||||
|
|
||||||
|
lodash.once@^4.0.0:
|
||||||
|
version "4.1.1"
|
||||||
|
resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz"
|
||||||
|
integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==
|
||||||
|
|
||||||
lodash@^4.17.21:
|
lodash@^4.17.21:
|
||||||
version "4.17.21"
|
version "4.17.21"
|
||||||
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
|
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
|
||||||
|
@ -2078,7 +2165,7 @@ run-parallel@^1.1.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
queue-microtask "^1.2.2"
|
queue-microtask "^1.2.2"
|
||||||
|
|
||||||
safe-buffer@~5.2.0:
|
safe-buffer@^5.0.1, safe-buffer@~5.2.0:
|
||||||
version "5.2.1"
|
version "5.2.1"
|
||||||
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz"
|
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz"
|
||||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||||
|
|
Loading…
Reference in New Issue