From 0a8d840b3f11b3463bb2210b27c032bebbfc6f6c Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Tue, 3 Sep 2024 15:48:16 -0500 Subject: [PATCH 1/4] write functionality --- package.json | 3 +- src/config.ts | 6 +- src/errors/index.ts | 15 +- src/functions/database.ts | 56 +++++++ src/index.ts | 11 +- src/lambda.ts | 22 ++- src/models/linkry.model.ts | 45 ++++++ src/plugins/auth.ts | 12 +- src/roles.ts | 2 + src/routes/linkry.ts | 302 +++++++++++++++++++++++++++++++++++++ src/types.d.ts | 7 + tsconfig.json | 1 + yarn.lock | 282 +++++++++++++++++++++++++++++++++- 13 files changed, 735 insertions(+), 29 deletions(-) create mode 100644 src/functions/database.ts create mode 100644 src/models/linkry.model.ts create mode 100644 src/routes/linkry.ts diff --git a/package.json b/package.json index 88ed6dd..c6377c3 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "type": "module", "scripts": { "build": "rm -rf dist/ && tsc", - "dev": "touch .env && tsx watch src/index.ts", + "dev": "touch .env && tsx watch src/lambda.ts", "build:lambda": "yarn build && cp package.json dist/src/ && yarn lockfile-manage", "lockfile-manage": "synp --source-file yarn.lock && cp package-lock.json dist/src/ && rm package-lock.json", "typecheck": "tsc --noEmit", @@ -57,6 +57,7 @@ "@fastify/auth": "^4.6.1", "@fastify/aws-lambda": "^4.1.0", "@fastify/cors": "^9.0.1", + "@sequelize/postgres": "^7.0.0-alpha.41", "@touch4it/ical-timezones": "^1.9.0", "discord.js": "^14.15.3", "dotenv": "^16.4.5", diff --git a/src/config.ts b/src/config.ts index 2ca4edf..2f1ea55 100644 --- a/src/config.ts +++ b/src/config.ts @@ -58,7 +58,10 @@ const environmentConfig: EnvironmentConfigType = { GroupRoleMapping: { "48591dbc-cdcb-4544-9f63-e6b92b067e33": allAppRoles, // Infra Chairs "ff49e948-4587-416b-8224-65147540d5fc": allAppRoles, // Officers - "ad81254b-4eeb-4c96-8191-3acdce9194b1": [AppRoles.EVENTS_MANAGER], // Exec + "ad81254b-4eeb-4c96-8191-3acdce9194b1": [ + AppRoles.EVENTS_MANAGER, + AppRoles.LINKS_MANAGER, + ], // Exec }, AzureRoleMapping: { AutonomousWriters: [AppRoles.EVENTS_MANAGER] }, ValidCorsOrigins: [ @@ -75,6 +78,7 @@ export type SecretConfig = { jwt_key?: string; discord_guild_id: string; discord_bot_token: string; + postgres_url: string; }; export { genericConfig, environmentConfig }; diff --git a/src/errors/index.ts b/src/errors/index.ts index 438e04f..b92ccf6 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -73,7 +73,7 @@ export class NotFoundError extends BaseError<"NotFoundError"> { super({ name: "NotFoundError", id: 103, - message: `${endpointName} is not a valid URL.`, + message: `${endpointName} is not a valid resource.`, httpStatusCode: 404, }); } @@ -112,11 +112,22 @@ export class DatabaseFetchError extends BaseError<"DatabaseFetchError"> { } } +export class DatabaseDeleteError extends BaseError<"DatabaseDeleteError"> { + constructor({ message }: { message: string }) { + super({ + name: "DatabaseDeleteError", + id: 107, + message, + httpStatusCode: 500, + }); + } +} + export class DiscordEventError extends BaseError<"DiscordEventError"> { constructor({ message }: { message?: string }) { super({ name: "DiscordEventError", - id: 107, + id: 108, message: message || "Could not create Discord event.", httpStatusCode: 500, }); diff --git a/src/functions/database.ts b/src/functions/database.ts new file mode 100644 index 0000000..5832417 --- /dev/null +++ b/src/functions/database.ts @@ -0,0 +1,56 @@ +import { Sequelize } from "@sequelize/core"; +import { PostgresDialect } from "@sequelize/postgres"; +import { getSecretValue } from "../plugins/auth.js"; +import { genericConfig } from "../config.js"; +import { InternalServerError } from "../errors/index.js"; +import { ShortLinkModel } from "../models/linkry.model.js"; + +let logDebug: CallableFunction = console.log; +let logFatal: CallableFunction = console.log; + +// Function to set the current logger for each invocation +export function setSequelizeLogger( + debugLogger: CallableFunction, + fatalLogger: CallableFunction, +) { + logDebug = (msg: string) => debugLogger(msg); + logFatal = (msg: string) => fatalLogger(msg); +} + +export async function getSequelizeInstance(): Promise { + let secret = null; + if (!process.env.DATABASE_URL) { + secret = await getSecretValue(genericConfig.ConfigSecretName); + if (!secret) { + throw new InternalServerError({ + message: "Invalid secret configuration", + }); + } + } + + const sequelize = new Sequelize({ + dialect: PostgresDialect, + url: process.env.DATABASE_URL || secret?.postgres_url, + ssl: { + rejectUnauthorized: false, + }, + models: [ShortLinkModel], + logging: logDebug as (sql: string, timing?: number) => void, + pool: { + max: 2, + min: 0, + idle: 0, + acquire: 3000, + evict: 30, // lambda function timeout in seconds + }, + }); + try { + await sequelize.sync(); + } catch (e: unknown) { + logFatal(`Could not authenticate to DB! ${e}`); + throw new InternalServerError({ + message: "Could not establish database connection.", + }); + } + return sequelize; +} diff --git a/src/index.ts b/src/index.ts index 3df269f..f6b04a2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ import organizationsPlugin from "./routes/organizations.js"; import icalPlugin from "./routes/ics.js"; import vendingPlugin from "./routes/vending.js"; import * as dotenv from "dotenv"; +import linkryRoutes from "./routes/linkry.js"; dotenv.config(); const now = () => Date.now(); @@ -70,6 +71,7 @@ async function init() { api.register(protectedRoute, { prefix: "/protected" }); api.register(eventsPlugin, { prefix: "/events" }); api.register(organizationsPlugin, { prefix: "/organizations" }); + api.register(linkryRoutes, { prefix: "/linkry" }); api.register(icalPlugin, { prefix: "/ical" }); if (app.runEnvironment === "dev") { api.register(vendingPlugin, { prefix: "/vending" }); @@ -84,13 +86,4 @@ async function init() { return app; } -if (import.meta.url === `file://${process.argv[1]}`) { - // local development - const app = await init(); - app.listen({ port: 8080 }, (err) => { - /* eslint no-console: ["error", {"allow": ["log", "error"]}] */ - if (err) console.error(err); - console.log("Server listening on 8080"); - }); -} export default init; diff --git a/src/lambda.ts b/src/lambda.ts index 0cd9a76..ff97daf 100644 --- a/src/lambda.ts +++ b/src/lambda.ts @@ -4,9 +4,19 @@ import awsLambdaFastify from "@fastify/aws-lambda"; import init from "./index.js"; const app = await init(); -const handler = awsLambdaFastify(app, { - decorateRequest: false, - serializeLambdaArguments: true, -}); -await app.ready(); // needs to be placed after awsLambdaFastify call because of the decoration: https://github.com/fastify/aws-lambda-fastify/blob/master/index.js#L9 -export { handler }; +let handler = () => {}; +if (import.meta.url === `file://${process.argv[1]}`) { + // local development + app.listen({ port: 8080 }, (err) => { + /* eslint no-console: ["error", {"allow": ["log", "error"]}] */ + if (err) console.error(err); + console.log("Server listening on 8080"); + }); +} else { + const handler = awsLambdaFastify(app, { + decorateRequest: false, + serializeLambdaArguments: true, + }); + await app.ready(); // needs to be placed after awsLambdaFastify call because of the decoration: https://github.com/fastify/aws-lambda-fastify/blob/master/index.js#L9 +} +export { handler, app }; diff --git a/src/models/linkry.model.ts b/src/models/linkry.model.ts new file mode 100644 index 0000000..0f1ab6c --- /dev/null +++ b/src/models/linkry.model.ts @@ -0,0 +1,45 @@ +import { + InferCreationAttributes, + InferAttributes, + Model, + CreationOptional, + DataTypes, +} from "@sequelize/core"; +import { + AllowNull, + Attribute, + CreatedAt, + NotNull, + PrimaryKey, + Table, + UpdatedAt, +} from "@sequelize/core/decorators-legacy"; + +@Table({ timestamps: true, tableName: "short_links" }) +export class ShortLinkModel extends Model< + InferAttributes, + InferCreationAttributes +> { + @Attribute(DataTypes.STRING) + @PrimaryKey + declare slug: string; + + @Attribute(DataTypes.STRING) + @NotNull + declare full: string; + + @Attribute(DataTypes.ARRAY(DataTypes.STRING)) + @AllowNull + declare groups?: CreationOptional; + + @Attribute(DataTypes.STRING) + declare author: string; + + @Attribute(DataTypes.DATE) + @CreatedAt + declare createdAt: CreationOptional; + + @Attribute(DataTypes.DATE) + @UpdatedAt + declare updatedAt: CreationOptional; +} diff --git a/src/plugins/auth.ts b/src/plugins/auth.ts index 988c4f6..704e279 100644 --- a/src/plugins/auth.ts +++ b/src/plugins/auth.ts @@ -13,9 +13,9 @@ import { UnauthenticatedError, UnauthorizedError, } from "../errors/index.js"; -import { genericConfig } from "../config.js"; +import { genericConfig, SecretConfig } from "../config.js"; -function intersection(setA: Set, setB: Set): Set { +export function intersection(setA: Set, setB: Set): Set { const _intersection = new Set(); for (const elem of setB) { if (setA.has(elem)) { @@ -57,7 +57,7 @@ const smClient = new SecretsManagerClient({ export const getSecretValue = async ( secretId: string, -): Promise | null> => { +): Promise => { const data = await smClient.send( new GetSecretValueCommand({ SecretId: secretId }), ); @@ -65,10 +65,7 @@ export const getSecretValue = async ( return null; } try { - return JSON.parse(data.SecretString) as Record< - string, - string | number | boolean - >; + return JSON.parse(data.SecretString) as SecretConfig; } catch { return null; } @@ -216,6 +213,7 @@ const authPlugin: FastifyPluginAsync = async (fastify, _options) => { message: "Invalid token.", }); } + request.userRoles = userRoles; return userRoles; }, ); diff --git a/src/roles.ts b/src/roles.ts index e571592..ce6102a 100644 --- a/src/roles.ts +++ b/src/roles.ts @@ -3,6 +3,8 @@ export const runEnvironments = ["dev", "prod"] as const; export type RunEnvironment = (typeof runEnvironments)[number]; export enum AppRoles { EVENTS_MANAGER = "manage:events", + LINKS_MANAGER = "manage:links", + LINKS_ADMIN = "admin:links", } export const allAppRoles = Object.values(AppRoles).filter( (value) => typeof value === "string", diff --git a/src/routes/linkry.ts b/src/routes/linkry.ts new file mode 100644 index 0000000..d92d238 --- /dev/null +++ b/src/routes/linkry.ts @@ -0,0 +1,302 @@ +import { FastifyPluginAsync, FastifyRequest } from "fastify"; +import { + getSequelizeInstance, + setSequelizeLogger, +} from "../functions/database.js"; +import { ShortLinkModel } from "../models/linkry.model.js"; +import { z } from "zod"; +import { AppRoles } from "../roles.js"; +import { + BaseError, + DatabaseDeleteError, + DatabaseFetchError, + DatabaseInsertError, + InternalServerError, + NotFoundError, + UnauthenticatedError, + UnauthorizedError, + ValidationError, +} from "../errors/index.js"; +import { UniqueConstraintError } from "@sequelize/core"; +import { intersection } from "../plugins/auth.js"; +import { NoDataRequest } from "../types.js"; + +type LinkrySlugOnlyRequest = { + Params: { id: string }; + Querystring: undefined; + Body: undefined; +}; + +const rawRequest = { + slug: z.string().min(1), + full: z.string().url().min(1), + groups: z.optional(z.array(z.string()).min(1)), +}; + +const createRequest = z.object(rawRequest); +const patchRequest = z.object({ ...rawRequest, slug: z.undefined() }); +// todo: patchRequest is all optional + +type LinkyCreateRequest = { + Params: undefined; + Querystring: undefined; + Body: z.infer; +}; + +type LinkryPatchRequest = { + Params: { id: string }; + Querystring: undefined; + Body: z.infer; +}; + +await getSequelizeInstance(); + +function userCanManageLink( + request: FastifyRequest, + link: ShortLinkModel, +): boolean { + if (request.userRoles?.has(AppRoles.LINKS_ADMIN)) { + return true; + } + if (request.username === link.author) { + return true; + } + if (!link.groups) { + return false; + } + if ( + request.userRoles && + intersection(request.userRoles, new Set(link.groups)) + ) { + return true; + } + return false; +} + +const linkryRoutes: FastifyPluginAsync = async (fastify, _options) => { + fastify.get("/redir/:id", async (request, reply) => { + // update logger instance + setSequelizeLogger(request.log.debug, request.log.fatal); + const slug = request.params.id; + try { + const result = await ShortLinkModel.findByPk(slug); + if (!result) { + const isProd = fastify.runEnvironment === "prod"; + // hide the real URL from the user in prod + throw new NotFoundError({ + endpointName: isProd ? `/${slug}` : `/api/v1/linkry/redir/${slug}`, + }); + } else { + reply.redirect(result.full); + } + } catch (e) { + if (e instanceof BaseError) { + throw e; + } + request.log.error(`Failed to retrieve short link: ${e}`); + throw new DatabaseFetchError({ + message: "Could not fetch short link entry.", + }); + } + }); + fastify.post( + "/redir", + { + preValidation: async (request, reply) => { + await fastify.zodValidateBody(request, reply, createRequest); + }, + onRequest: async (request, reply) => { + await fastify.authorize(request, reply, [AppRoles.LINKS_MANAGER]); + }, + }, + async (request, reply) => { + // update logger instance + setSequelizeLogger(request.log.debug, request.log.fatal); + const slug = request.body.slug; + if (!request.username) { + throw new UnauthenticatedError({ + message: "Could not determine username.", + }); + } + try { + await ShortLinkModel.create({ + ...request.body, + author: request.username, + }); + } catch (e) { + if (e instanceof BaseError) { + throw e; + } + if (e instanceof UniqueConstraintError) { + throw new ValidationError({ + message: "This slug already exists, you must PATCH it directly.", + }); + } + request.log.error(`Failed to insert short link: ${e}`); + throw new DatabaseInsertError({ + message: "Could not create short link entry.", + }); + } + return reply.send({ + message: "Short link created.", + slug, + resource: `/api/v1/linkry/redir/${slug}`, + }); + }, + ); + fastify.patch( + "/redir/:id", + { + preValidation: async (request, reply) => { + await fastify.zodValidateBody(request, reply, patchRequest); + }, + onRequest: async (request, reply) => { + await fastify.authorize(request, reply, [AppRoles.LINKS_MANAGER]); + }, + }, + async (request, reply) => { + // update logger instance + setSequelizeLogger(request.log.debug, request.log.fatal); + const slug = request.params.id; + let result; + try { + result = await ShortLinkModel.findByPk(slug); + if (!result) { + const isProd = fastify.runEnvironment === "prod"; + // hide the real URL from the user in prod + throw new NotFoundError({ + endpointName: isProd ? `/${slug}` : `/api/v1/linkry/redir/${slug}`, + }); + } + } catch (e) { + if (e instanceof BaseError) { + throw e; + } + request.log.error(`Failed to retrieve short link when modifying: ${e}`); + throw new DatabaseFetchError({ + message: "Could not fetch short link entry.", + }); + } + if (!userCanManageLink(request, result)) { + throw new UnauthenticatedError({ + message: "User cannot manage this link.", + }); + } + try { + result.set({ ...request.body, slug: undefined }); + await result.save(); + } catch (e) { + request.log.error(`Failed to modify short link ${slug}: ${e}`); + throw new DatabaseInsertError({ + message: "Failed to modify short link.", + }); + } + reply.send({ + message: "Short link modified.", + slug, + resource: `/api/v1/linkry/redir/${slug}`, + }); + }, + ); + fastify.delete( + "/redir/:id", + { + preValidation: async (request, reply) => { + await fastify.zodValidateBody(request, reply, createRequest); + }, + onRequest: async (request, reply) => { + await fastify.authorize(request, reply, [AppRoles.LINKS_MANAGER]); + }, + }, + async (request, reply) => { + // update logger instance + if (!request.username) { + throw new UnauthenticatedError({ + message: "Could not determine username.", + }); + } + let result; + try { + result = await ShortLinkModel.findByPk(request.params.id); + if (!result) { + throw new NotFoundError({ + endpointName: `/api/v1/linkry/redir/${request.params.id}`, + }); + } + if (!userCanManageLink(request, result)) { + throw new UnauthenticatedError({ + message: "User cannot manage this link.", + }); + } + } catch (e) { + if (e instanceof BaseError) { + throw e; + } + request.log.error( + `Could not fetch original link entry to delete it: ${e}`, + ); + throw new DatabaseFetchError({ + message: `Could not fetch original link entry to delete it.`, + }); + } + + try { + await result.destroy(); + } catch (e) { + if (e instanceof BaseError) { + throw e; + } + request.log.error(`Could not delete short link entry: ${e}`); + throw new DatabaseDeleteError({ + message: `Could not delete short link entry.`, + }); + } + reply.send({ message: "Short link deleted." }); + }, + ); + fastify.get( + "/redirs", + { + onRequest: async (request, reply) => { + await fastify.authorize(request, reply, [AppRoles.LINKS_MANAGER]); + }, + }, + async (request, reply) => { + if (!request.userRoles) { + throw new UnauthorizedError({ message: "Could not get user roles." }); + } + try { + const isAdmin = request.userRoles.has(AppRoles.LINKS_ADMIN); + let filteredLinks = await ShortLinkModel.findAll(); + if (!isAdmin) { + filteredLinks = filteredLinks.filter((slm: ShortLinkModel) => { + return ( + slm.author == request.username || + (slm.groups && + request.userRoles && + intersection(new Set(slm.groups), request.userRoles).size > 0) + ); + }); + } + const myLinks: ShortLinkModel[] = []; + const delegatedLinks: ShortLinkModel[] = []; + for (const link of filteredLinks) { + if (link.author === request.username) { + myLinks.push(link); + } else { + delegatedLinks.push(link); + } + } + return reply.send({ admin: isAdmin, myLinks, delegatedLinks }); + } catch (e) { + if (e instanceof BaseError) { + throw e; + } + request.log.error(`Could not get user's links: ${e}`); + throw new InternalServerError({ message: "Could not get user links." }); + } + }, + ); +}; + +export default linkryRoutes; diff --git a/src/types.d.ts b/src/types.d.ts index e265a07..226ccc0 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -24,6 +24,13 @@ declare module "fastify" { interface FastifyRequest { startTime: number; username?: string; + userRoles?: Set; tokenPayload?: AadToken; } } + +export type NoDataRequest = { + Params: undefined; + Querystring: undefined; + Body: undefined; +}; diff --git a/tsconfig.json b/tsconfig.json index cb903f2..cdd1c7d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "module": "Node16", "outDir": "dist", + "experimentalDecorators": true }, "ts-node": { "esm": true diff --git a/yarn.lock b/yarn.lock index f2fbe03..7a4e781 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1162,6 +1162,54 @@ resolved "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz" integrity sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ== +"@sequelize/core@7.0.0-alpha.41": + version "7.0.0-alpha.41" + resolved "https://registry.yarnpkg.com/@sequelize/core/-/core-7.0.0-alpha.41.tgz#7d3e36b9ef33affabc908bf19e2448217363f8a4" + integrity sha512-4SKtalcOv4NYQTwJdeL0YOQc3ezsEY2c4csUDLH0eoYgFX20CIOthPeX/EMafBmWr/c81+p48x/EyvrntoD6yQ== + dependencies: + "@sequelize/utils" "7.0.0-alpha.41" + "@types/debug" "^4.1.12" + "@types/validator" "^13.11.9" + bnf-parser "^3.1.6" + chalk "^4.1.2" + dayjs "^1.11.10" + debug "^4.3.4" + dottie "^2.0.6" + fast-glob "^3.3.2" + inflection "^3.0.0" + lodash "^4.17.21" + retry-as-promised "^7.0.4" + semver "^7.3" + sequelize-pool "^8.0.0" + toposort-class "^1.0.1" + type-fest "^4.14.0" + uuid "^9.0.1" + validator "^13.11.0" + +"@sequelize/postgres@^7.0.0-alpha.41": + version "7.0.0-alpha.41" + resolved "https://registry.yarnpkg.com/@sequelize/postgres/-/postgres-7.0.0-alpha.41.tgz#9c52e6ed8612229d4fba7ee1efd8c3b5fc19b8d8" + integrity sha512-RbKEJjeE4CrQgZXEin8lV2WJqWaQujjwONkvkd6xM6SEglxn7ESFbxVN7mYEJcB5B0F+QdEnz6F6yxGV6hDSxA== + dependencies: + "@sequelize/core" "7.0.0-alpha.41" + "@sequelize/utils" "7.0.0-alpha.41" + "@types/pg" "^8.11.4" + lodash "^4.17.21" + pg "^8.11.3" + pg-hstore "^2.3.4" + pg-types "^4.0.2" + postgres-array "^3.0.2" + semver "^7.6.0" + wkx "^0.5.0" + +"@sequelize/utils@7.0.0-alpha.41": + version "7.0.0-alpha.41" + resolved "https://registry.yarnpkg.com/@sequelize/utils/-/utils-7.0.0-alpha.41.tgz#dd0a9bc380454be70f6ba694a139080e2d61f0ca" + integrity sha512-5D2TaI9zPVPS2FmsvElQ8PbXptoCEcS1vBBvbH8yUb7Mn5h+C7vRVEOJPj9cKNWkjj9FXczxTQMLxEy09bjZLw== + dependencies: + "@types/lodash" "^4.17.0" + lodash "^4.17.21" + "@sinonjs/commons@^2.0.0": version "2.0.0" resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz" @@ -1634,6 +1682,13 @@ resolved "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz" integrity sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q== +"@types/debug@^4.1.12": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== + dependencies: + "@types/ms" "*" + "@types/estree@1.0.5", "@types/estree@^1.0.0": version "1.0.5" resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz" @@ -1676,6 +1731,11 @@ dependencies: "@types/node" "*" +"@types/lodash@^4.17.0": + version "4.17.7" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.7.tgz#2f776bcb53adc9e13b2c0dfd493dfcbd7de43612" + integrity sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA== + "@types/methods@^1.1.4": version "1.1.4" resolved "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz" @@ -1686,6 +1746,11 @@ resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz" integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== +"@types/ms@*": + version "0.7.34" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" + integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== + "@types/node@*", "@types/node@^22.1.0": version "22.1.0" resolved "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz" @@ -1693,6 +1758,15 @@ dependencies: undici-types "~6.13.0" +"@types/pg@^8.11.4": + version "8.11.8" + resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.11.8.tgz#bc712f1ad8ca664acb1d321b42691d1a166a88d6" + integrity sha512-IqpCf8/569txXN/HoP5i1LjXfKZWL76Yr2R77xgeIICUbAYHeoaEZFhYHo2uDftecLWrTJUq63JvQu8q3lnDyA== + dependencies: + "@types/node" "*" + pg-protocol "*" + pg-types "^4.0.1" + "@types/qs@*": version "6.9.15" resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz" @@ -1750,6 +1824,11 @@ "@types/methods" "^1.1.4" "@types/superagent" "^8.1.0" +"@types/validator@^13.11.9": + version "13.12.1" + resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.12.1.tgz#8835d22f7e25b261e624d02a42fe4ade2c689a3c" + integrity sha512-w0URwf7BQb0rD/EuiG12KP0bailHKHP5YVviJG9zw3ykAokL0TuxU2TUqMB7EwZ59bDHYdeTIvjI5m0S7qHfOA== + "@types/ws@^8.5.10": version "8.5.12" resolved "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz" @@ -2244,6 +2323,11 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +bnf-parser@^3.1.6: + version "3.1.6" + resolved "https://registry.yarnpkg.com/bnf-parser/-/bnf-parser-3.1.6.tgz#a36df2ba38ad63576a5864747d799cb716e760aa" + integrity sha512-3x0ECh6CghmcAYnY6uiVAOfl263XkWffDq5fQS20ac3k0U7TE5rTWNXTnOTckgmfZc94iharwqCyoV8OAYxYoA== + bowser@^2.11.0: version "2.11.0" resolved "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz" @@ -2330,7 +2414,7 @@ chalk@^2.1.0, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0, chalk@^4.1.0: +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -2531,6 +2615,11 @@ data-view-byte-offset@^1.0.0: es-errors "^1.3.0" is-data-view "^1.0.1" +dayjs@^1.11.10: + version "1.11.13" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c" + integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== + debug@^3.2.7: version "3.2.7" resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" @@ -2640,6 +2729,11 @@ dotenv@^16.4.5: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== +dottie@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/dottie/-/dottie-2.0.6.tgz#34564ebfc6ec5e5772272d466424ad5b696484d4" + integrity sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA== + each-parallel-async@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/each-parallel-async/-/each-parallel-async-1.0.0.tgz" @@ -3725,6 +3819,11 @@ imurmurhash@^0.1.4: resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== +inflection@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/inflection/-/inflection-3.0.0.tgz#6a956fa90d72a27d22e6b32ec1064877593ee23b" + integrity sha512-1zEJU1l19SgJlmwqsEyFTbScw/tkMHFenUo//Y0i+XEP83gDFdMvPizAD/WGcE+l1ku12PcTVHQhO6g5E0UCMw== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" @@ -4523,6 +4622,11 @@ obliterator@^2.0.1: resolved "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz" integrity sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ== +obuf@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + on-exit-leak-free@^2.1.0: version "2.1.2" resolved "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz" @@ -4661,6 +4765,87 @@ performance-now@^2.1.0: resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== +pg-cloudflare@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98" + integrity sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q== + +pg-connection-string@^2.6.4: + version "2.6.4" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.4.tgz#f543862adfa49fa4e14bc8a8892d2a84d754246d" + integrity sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA== + +pg-hstore@^2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/pg-hstore/-/pg-hstore-2.3.4.tgz#4425e3e2a3e15d2a334c35581186c27cf2e9b8dd" + integrity sha512-N3SGs/Rf+xA1M2/n0JBiXFDVMzdekwLZLAO0g7mpDY9ouX+fDI7jS6kTq3JujmYbtNSJ53TJ0q4G98KVZSM4EA== + dependencies: + underscore "^1.13.1" + +pg-int8@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" + integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== + +pg-numeric@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pg-numeric/-/pg-numeric-1.0.2.tgz#816d9a44026086ae8ae74839acd6a09b0636aa3a" + integrity sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw== + +pg-pool@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.2.tgz#3a592370b8ae3f02a7c8130d245bc02fa2c5f3f2" + integrity sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg== + +pg-protocol@*, pg-protocol@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.6.1.tgz#21333e6d83b01faaebfe7a33a7ad6bfd9ed38cb3" + integrity sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg== + +pg-types@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" + integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== + dependencies: + pg-int8 "1.0.1" + postgres-array "~2.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.4" + postgres-interval "^1.1.0" + +pg-types@^4.0.1, pg-types@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-4.0.2.tgz#399209a57c326f162461faa870145bb0f918b76d" + integrity sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng== + dependencies: + pg-int8 "1.0.1" + pg-numeric "1.0.2" + postgres-array "~3.0.1" + postgres-bytea "~3.0.0" + postgres-date "~2.1.0" + postgres-interval "^3.0.0" + postgres-range "^1.1.1" + +pg@^8.11.3: + version "8.12.0" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.12.0.tgz#9341724db571022490b657908f65aee8db91df79" + integrity sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ== + dependencies: + pg-connection-string "^2.6.4" + pg-pool "^3.6.2" + pg-protocol "^1.6.1" + pg-types "^2.1.0" + pgpass "1.x" + optionalDependencies: + pg-cloudflare "^1.1.1" + +pgpass@1.x: + version "1.0.5" + resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.5.tgz#9b873e4a564bb10fa7a7dbd55312728d422a223d" + integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug== + dependencies: + split2 "^4.1.0" + picocolors@^1.0.0, picocolors@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz" @@ -4720,6 +4905,55 @@ postcss@^8.4.40: picocolors "^1.0.1" source-map-js "^1.2.0" +postgres-array@^3.0.2, postgres-array@~3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-3.0.2.tgz#68d6182cb0f7f152a7e60dc6a6889ed74b0a5f98" + integrity sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog== + +postgres-array@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" + integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== + +postgres-bytea@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" + integrity sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w== + +postgres-bytea@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-3.0.0.tgz#9048dc461ac7ba70a6a42d109221619ecd1cb089" + integrity sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw== + dependencies: + obuf "~1.1.2" + +postgres-date@~1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" + integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== + +postgres-date@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-2.1.0.tgz#b85d3c1fb6fb3c6c8db1e9942a13a3bf625189d0" + integrity sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA== + +postgres-interval@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" + integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== + dependencies: + xtend "^4.0.0" + +postgres-interval@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-3.0.0.tgz#baf7a8b3ebab19b7f38f07566c7aab0962f0c86a" + integrity sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw== + +postgres-range@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/postgres-range/-/postgres-range-1.1.4.tgz#a59c5f9520909bcec5e63e8cf913a92e4c952863" + integrity sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w== + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" @@ -4914,6 +5148,11 @@ ret@~0.4.0: resolved "https://registry.npmjs.org/ret/-/ret-0.4.3.tgz" integrity sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ== +retry-as-promised@^7.0.4: + version "7.0.4" + resolved "https://registry.yarnpkg.com/retry-as-promised/-/retry-as-promised-7.0.4.tgz#9df73adaeea08cb2948b9d34990549dc13d800a2" + integrity sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" @@ -5045,11 +5284,16 @@ semver@^6.1.2, semver@^6.3.1: resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.5.4, semver@^7.6.0: +semver@^7.3, semver@^7.5.4, semver@^7.6.0: version "7.6.3" resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== +sequelize-pool@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/sequelize-pool/-/sequelize-pool-8.0.0.tgz#4bd3a62b9ab8ec336190e2cbce81aa769d7365ee" + integrity sha512-xY04c5ctp6lKE4ZoSVo7YJHN5FHVhoYMoH2Z8jWIJG26c9kJSkIjql5HgD9XG5cwJbYMF0Ko2Fhgws20HC90Ug== + set-cookie-parser@^2.4.1: version "2.7.0" resolved "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.0.tgz" @@ -5194,7 +5438,7 @@ source-map-js@^1.2.0: resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz" integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== -split2@^4.0.0: +split2@^4.0.0, split2@^4.1.0: version "4.2.0" resolved "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz" integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== @@ -5478,6 +5722,11 @@ toad-cache@^3.3.0: resolved "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz" integrity sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw== +toposort-class@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toposort-class/-/toposort-class-1.0.1.tgz#7ffd1f78c8be28c3ba45cd4e1a3f5ee193bd9988" + integrity sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg== + totalist@^3.0.0: version "3.0.1" resolved "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz" @@ -5587,6 +5836,11 @@ type-fest@^0.8.1: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^4.14.0: + version "4.26.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.26.0.tgz#703f263af10c093cd6277d079e26b9e17d517c4b" + integrity sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw== + typed-array-buffer@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz" @@ -5646,6 +5900,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +underscore@^1.13.1: + version "1.13.7" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.7.tgz#970e33963af9a7dda228f17ebe8399e5fbe63a10" + integrity sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g== + undici-types@~6.13.0: version "6.13.0" resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz" @@ -5683,6 +5942,11 @@ v8-compile-cache@^2.0.3: resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz" integrity sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw== +validator@^13.11.0: + version "13.12.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.12.0.tgz#7d78e76ba85504da3fee4fd1922b385914d4b35f" + integrity sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg== + verror@1.10.0: version "1.10.0" resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz" @@ -5783,6 +6047,13 @@ why-is-node-running@^2.3.0: siginfo "^2.0.0" stackback "0.0.2" +wkx@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/wkx/-/wkx-0.5.0.tgz#c6c37019acf40e517cc6b94657a25a3d4aa33e8c" + integrity sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg== + dependencies: + "@types/node" "*" + word-wrap@^1.2.5, word-wrap@~1.2.3: version "1.2.5" resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" @@ -5814,6 +6085,11 @@ ws@^8.16.0: resolved "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz" integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + yallist@^2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz" From 3d1e90214e0e17c7cd06c0491d7f91b9ae55718f Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Tue, 3 Sep 2024 23:57:39 -0500 Subject: [PATCH 2/4] stuff --- cloudformation/main.yml | 29 +++++++++++++++++++++++++++++ src/config.ts | 16 +++++++++++++++- src/plugins/auth.ts | 20 +++++++++++--------- src/routes/protected.ts | 15 ++++++++++++++- 4 files changed, 69 insertions(+), 11 deletions(-) diff --git a/cloudformation/main.yml b/cloudformation/main.yml index bbe01c0..054f35e 100644 --- a/cloudformation/main.yml +++ b/cloudformation/main.yml @@ -105,6 +105,35 @@ Resources: Path: /{proxy+} Method: ANY + AppApiLambdaFunctionVpc: + Type: AWS::Serverless::Function + DependsOn: + - AppLogGroups + Properties: + CodeUri: ../dist/src/ + AutoPublishAlias: live + Runtime: nodejs20.x + Description: !Sub "${ApplicationFriendlyName} API Lambda - VPC attached" + FunctionName: !Sub ${ApplicationPrefix}-lambda-vpc + Handler: lambda.handler + MemorySize: 512 + Role: !GetAtt AppSecurityRoles.Outputs.MainFunctionRoleArn + Timeout: 60 + Environment: + Variables: + RunEnvironment: !Ref RunEnvironment + VpcConfig: + Ipv6AllowedForDualStack: True + SecurityGroupIds: !FindInMap [EnvironmentToCidr, !Ref RunEnvironment, SecurityGroupIds] + SubnetIds: !FindInMap [EnvironmentToCidr, !Ref RunEnvironment, SubnetIds] + Events: + LinkryEvent: + Type: Api + Properties: + RestApiId: !Ref AppApiGateway + Path: /api/v1/linkry/{proxy+} + Method: ANY + EventRecordsTable: Type: 'AWS::DynamoDB::Table' DeletionPolicy: "Retain" diff --git a/src/config.ts b/src/config.ts index 2f1ea55..6061f88 100644 --- a/src/config.ts +++ b/src/config.ts @@ -6,7 +6,9 @@ type ArrayOfValueOrArray = Array>; type OriginType = string | boolean | RegExp; type ValueOrArray = T | ArrayOfValueOrArray; -type GroupRoleMapping = Record; +type GroupRoleMapping = { + [K in KnownAzureGroupId]?: readonly AppRoles[]; +}; type AzureRoleMapping = Record; export type ConfigType = { @@ -27,6 +29,18 @@ type EnvironmentConfigType = { [env in RunEnvironment]: ConfigType; }; +export const GroupNameMapping = { + "48591dbc-cdcb-4544-9f63-e6b92b067e33": "Infra Chairs", + "940e4f9e-6891-4e28-9e29-148798495cdb": "Infra Team", + "f8dfc4cf-456b-4da3-9053-f7fdeda5d5d6": "Infra Leads", + "ff49e948-4587-416b-8224-65147540d5fc": "Officers", + "ad81254b-4eeb-4c96-8191-3acdce9194b1": "ACM Exec", + "0": "Testing Admin", + "1": "Testing Public", +}; + +export type KnownAzureGroupId = keyof typeof GroupNameMapping; + const genericConfig: GenericConfigType = { DynamoTableName: "infra-core-api-events", ConfigSecretName: "infra-core-api-config", diff --git a/src/plugins/auth.ts b/src/plugins/auth.ts index 704e279..28abd3b 100644 --- a/src/plugins/auth.ts +++ b/src/plugins/auth.ts @@ -13,7 +13,7 @@ import { UnauthenticatedError, UnauthorizedError, } from "../errors/index.js"; -import { genericConfig, SecretConfig } from "../config.js"; +import { genericConfig, KnownAzureGroupId, SecretConfig } from "../config.js"; export function intersection(setA: Set, setB: Set): Set { const _intersection = new Set(); @@ -161,10 +161,12 @@ const authPlugin: FastifyPluginAsync = async (fastify, _options) => { fastify.environmentConfig.GroupRoleMapping ) { for (const group of verifiedTokenData.groups) { - if (fastify.environmentConfig["GroupRoleMapping"][group]) { - for (const role of fastify.environmentConfig["GroupRoleMapping"][ - group - ]) { + const roles = + fastify.environmentConfig["GroupRoleMapping"][ + group as KnownAzureGroupId + ]; + if (roles) { + for (const role of roles) { userRoles.add(role); } } @@ -175,10 +177,10 @@ const authPlugin: FastifyPluginAsync = async (fastify, _options) => { fastify.environmentConfig.AzureRoleMapping ) { for (const group of verifiedTokenData.roles) { - if (fastify.environmentConfig["AzureRoleMapping"][group]) { - for (const role of fastify.environmentConfig[ - "AzureRoleMapping" - ][group]) { + const roles = + fastify.environmentConfig["AzureRoleMapping"][group]; + if (roles) { + for (const role of roles) { userRoles.add(role); } } diff --git a/src/routes/protected.ts b/src/routes/protected.ts index 44c1cbe..b602031 100644 --- a/src/routes/protected.ts +++ b/src/routes/protected.ts @@ -1,9 +1,22 @@ import { FastifyPluginAsync } from "fastify"; +import { GroupNameMapping, KnownAzureGroupId } from "../config.js"; const protectedRoute: FastifyPluginAsync = async (fastify, _options) => { fastify.get("/", async (request, reply) => { const roles = await fastify.authorize(request, reply, []); - reply.send({ username: request.username, roles: Array.from(roles) }); + const groups = []; + for (const group in request.tokenPayload?.groups) { + groups.push({ + id: group, + name: GroupNameMapping[group as KnownAzureGroupId], + }); + } + + reply.send({ + username: request.username, + roles: Array.from(roles), + groups, + }); }); }; From b45391cb54a796123d225cec729ac7f604874bf1 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Wed, 4 Sep 2024 00:01:09 -0500 Subject: [PATCH 3/4] fix linter --- src/functions/discord.ts | 3 +-- src/plugins/auth.ts | 8 +++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/functions/discord.ts b/src/functions/discord.ts index 53d53ef..9888669 100644 --- a/src/functions/discord.ts +++ b/src/functions/discord.ts @@ -28,8 +28,7 @@ export const updateDiscord = async ( isDelete: boolean = false, logger: FastifyBaseLogger, ): Promise => { - const secretApiConfig = - (await getSecretValue(genericConfig.ConfigSecretName)) || {}; + const secretApiConfig = await getSecretValue(genericConfig.ConfigSecretName); const client = new Client({ intents: [GatewayIntentBits.Guilds] }); let payload: GuildScheduledEventCreateOptions | null = null; diff --git a/src/plugins/auth.ts b/src/plugins/auth.ts index 28abd3b..d6dbe89 100644 --- a/src/plugins/auth.ts +++ b/src/plugins/auth.ts @@ -57,17 +57,19 @@ const smClient = new SecretsManagerClient({ export const getSecretValue = async ( secretId: string, -): Promise => { +): Promise => { const data = await smClient.send( new GetSecretValueCommand({ SecretId: secretId }), ); if (!data.SecretString) { - return null; + throw new InternalServerError({ message: "config secret is invalid." }); } try { return JSON.parse(data.SecretString) as SecretConfig; } catch { - return null; + throw new InternalServerError({ + message: "config secret cannot be parsed as JSON.", + }); } }; From 432989ad12a73848cea18bd0d8733c283689f4cf Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Wed, 4 Sep 2024 00:12:27 -0500 Subject: [PATCH 4/4] potentially revert this --- package.json | 2 +- src/index.ts | 11 ++++++++++- src/lambda.ts | 22 ++++++---------------- tests/unit/auth.test.ts | 4 ++-- tests/unit/discordEvent.test.ts | 6 +++--- 5 files changed, 22 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index c6377c3..8f592a5 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "type": "module", "scripts": { "build": "rm -rf dist/ && tsc", - "dev": "touch .env && tsx watch src/lambda.ts", + "dev": "touch .env && tsx watch src/index.ts", "build:lambda": "yarn build && cp package.json dist/src/ && yarn lockfile-manage", "lockfile-manage": "synp --source-file yarn.lock && cp package-lock.json dist/src/ && rm package-lock.json", "typecheck": "tsc --noEmit", diff --git a/src/index.ts b/src/index.ts index f6b04a2..a49a6d8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -71,8 +71,8 @@ async function init() { api.register(protectedRoute, { prefix: "/protected" }); api.register(eventsPlugin, { prefix: "/events" }); api.register(organizationsPlugin, { prefix: "/organizations" }); - api.register(linkryRoutes, { prefix: "/linkry" }); api.register(icalPlugin, { prefix: "/ical" }); + api.register(linkryRoutes, { prefix: "/linkry" }); if (app.runEnvironment === "dev") { api.register(vendingPlugin, { prefix: "/vending" }); } @@ -86,4 +86,13 @@ async function init() { return app; } +if (import.meta.url === `file://${process.argv[1]}`) { + // local development + const app = await init(); + app.listen({ port: 8080 }, (err) => { + /* eslint no-console: ["error", {"allow": ["log", "error"]}] */ + if (err) console.error(err); + console.log("Server listening on 8080"); + }); +} export default init; diff --git a/src/lambda.ts b/src/lambda.ts index ff97daf..0cd9a76 100644 --- a/src/lambda.ts +++ b/src/lambda.ts @@ -4,19 +4,9 @@ import awsLambdaFastify from "@fastify/aws-lambda"; import init from "./index.js"; const app = await init(); -let handler = () => {}; -if (import.meta.url === `file://${process.argv[1]}`) { - // local development - app.listen({ port: 8080 }, (err) => { - /* eslint no-console: ["error", {"allow": ["log", "error"]}] */ - if (err) console.error(err); - console.log("Server listening on 8080"); - }); -} else { - const handler = awsLambdaFastify(app, { - decorateRequest: false, - serializeLambdaArguments: true, - }); - await app.ready(); // needs to be placed after awsLambdaFastify call because of the decoration: https://github.com/fastify/aws-lambda-fastify/blob/master/index.js#L9 -} -export { handler, app }; +const handler = awsLambdaFastify(app, { + decorateRequest: false, + serializeLambdaArguments: true, +}); +await app.ready(); // needs to be placed after awsLambdaFastify call because of the decoration: https://github.com/fastify/aws-lambda-fastify/blob/master/index.js#L9 +export { handler }; diff --git a/tests/unit/auth.test.ts b/tests/unit/auth.test.ts index aaadffd..1169c7a 100644 --- a/tests/unit/auth.test.ts +++ b/tests/unit/auth.test.ts @@ -8,7 +8,7 @@ import init from "../../src/index.js"; import { secretJson, secretObject, jwtPayload } from "./secret.testdata.js"; import jwt from "jsonwebtoken"; -const ddbMock = mockClient(SecretsManagerClient); +const smMock = mockClient(SecretsManagerClient); const app = await init(); const jwt_secret = secretObject["jwt_key"]; @@ -34,7 +34,7 @@ vi.stubEnv("JwtSigningKey", jwt_secret); const testJwt = createJwt(); test("Test happy path", async () => { - ddbMock.on(GetSecretValueCommand).resolves({ + smMock.on(GetSecretValueCommand).resolves({ SecretString: secretJson, }); const response = await app.inject({ diff --git a/tests/unit/discordEvent.test.ts b/tests/unit/discordEvent.test.ts index 8d3f75a..ffcbfca 100644 --- a/tests/unit/discordEvent.test.ts +++ b/tests/unit/discordEvent.test.ts @@ -1,4 +1,4 @@ -import { afterAll, expect, test, beforeEach, vi, Mock } from "vitest"; +import { afterAll, expect, test, beforeEach, vi, Mock, it } from "vitest"; import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb"; import { mockClient } from "aws-sdk-client-mock"; import init from "../../src/index.js"; @@ -31,7 +31,7 @@ const app = await init(); // TODO: add discord reject test describe("Test Events <-> Discord integration", () => { - test("Happy path: valid publish submission.", async () => { + it("Happy path: valid publish submission.", async () => { ddbMock.on(PutItemCommand).resolves({}); smMock.on(GetSecretValueCommand).resolves({ SecretString: secretJson, @@ -55,7 +55,7 @@ describe("Test Events <-> Discord integration", () => { expect((updateDiscord as Mock).mock.calls.length).toBe(1); }); - test("Happy path: do not publish repeating events.", async () => { + it("Happy path: do not publish repeating events.", async () => { ddbMock.on(PutItemCommand).resolves({}); smMock.on(GetSecretValueCommand).resolves({ SecretString: secretJson,