session management

This commit is contained in:
mrjvs 2023-10-28 18:34:32 +02:00
parent 94e1f9ebe1
commit 8f503b9c5a
13 changed files with 448 additions and 13 deletions

View File

@ -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)

99
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -12,4 +12,7 @@ export const devFragment: FragmentSchema = {
postgres: { postgres: {
syncSchema: true, syncSchema: true,
}, },
crypto: {
sessionSecret: 'aINCithRivERecKENdmANDRaNKenSiNi',
},
}; };

View File

@ -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),
}),
}); });

46
src/db/models/Session.ts Normal file
View File

@ -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,
};
}

View File

@ -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),
}; };
}), }),
); );

View File

@ -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;
}),
);
});

View File

@ -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);
},
};
} }

9
src/services/error.ts Normal file
View File

@ -0,0 +1,9 @@
export class StatusError extends Error {
errorStatusCode: number;
constructor(message: string, code: number) {
super(message);
this.errorStatusCode = code;
this.message = message;
}
}

View File

@ -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),
}), }),
); );
}; };

69
src/services/session.ts Normal file
View File

@ -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;
}
}

View File

@ -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==