From 3f758ff6538d1003e4a7946c3508323c79dfcb20 Mon Sep 17 00:00:00 2001 From: Will Howell <55269884+wrfhowell@users.noreply.github.com> Date: Sat, 9 Mar 2024 11:41:42 -0800 Subject: [PATCH] Merge userbase to main (#67) * removed specific files and changed small things * added scopes, roles, and scope_roles * added new sql tables and infrastructure * added id to user table * editUser endpoint * editUser testing * added createUser * updated createUser.ts * fixed small typo * update createUser.ts * createUser got id to return correctly * implement getUser and getUsers endpoints * delete user working * delete user update * add scope checking * added gh actions for userbase branch * middleware scope handling * #38: role and scope handling * scope and authorizer implementation * scope verification * #38 - comments --------- Co-authored-by: Aryan Gandhi Co-authored-by: Will Howell Co-authored-by: Xavier Lam Co-authored-by: Xavier Lam <58374369+xavier-c-lam@users.noreply.github.com> --- .github/workflows/pr.yml | 2 +- .github/workflows/push.yml | 2 +- lib/posting.stack.ts | 124 ------ lib/project.stack.ts | 129 ------ lib/user.stack.ts | 12 - package-lock.json | 388 +++++++++--------- package.json | 13 +- .../database/migrations/1687038383236.sql | 96 ----- .../database/migrations/1690825596849.sql | 34 -- .../database/migrations/1700543219488.sql | 104 ----- resources/database/migrations/1703270939.sql | 89 ++++ resources/database/migrations/1703270982.sql | 175 ++++++++ resources/database/setup.ts | 11 +- scripts/serve | 2 +- src/roles/addUserRole.ts | 35 +- src/roles/createRole.ts | 35 +- src/roles/deleteRole.ts | 30 +- src/roles/deleteUserRole.ts | 43 +- src/roles/getUserRoles.ts | 43 +- src/users/createUser.ts | 73 +++- src/users/deleteUser.ts | 66 ++- src/users/editUser.ts | 78 +++- src/users/getUser.ts | 88 +++- src/users/getUserId.ts | 38 -- src/users/getUsers.ts | 50 ++- src/util/db.ts | 115 +++--- src/util/middleware/authorizer.ts | 63 ++- src/util/middleware/middleware.ts | 35 +- src/util/middleware/scopeHandler.ts | 65 +-- 29 files changed, 1044 insertions(+), 994 deletions(-) delete mode 100644 lib/posting.stack.ts delete mode 100644 lib/project.stack.ts delete mode 100644 resources/database/migrations/1687038383236.sql delete mode 100644 resources/database/migrations/1690825596849.sql delete mode 100644 resources/database/migrations/1700543219488.sql create mode 100644 resources/database/migrations/1703270939.sql create mode 100644 resources/database/migrations/1703270982.sql delete mode 100644 src/users/getUserId.ts diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index a71ecd1..2726460 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -2,7 +2,7 @@ name: PR on: pull_request: - branches: [ "main" ] + branches: [ "main", "userbase" ] jobs: build: diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 036f9ae..3bf3591 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -2,7 +2,7 @@ name: PR on: push: - branches: [ "main" ] + branches: [ "main", "userbase" ] jobs: deploy-lamda: diff --git a/lib/posting.stack.ts b/lib/posting.stack.ts deleted file mode 100644 index fa201bc..0000000 --- a/lib/posting.stack.ts +++ /dev/null @@ -1,124 +0,0 @@ -// import * as cdk from 'aws-cdk-lib'; -// import { Construct } from 'constructs'; -// import * as lambda from 'aws-cdk-lib/aws-lambda'; -// import * as apigateway from 'aws-cdk-lib/aws-apigateway'; -// import { Cors } from 'aws-cdk-lib/aws-apigateway'; -// import { config } from 'dotenv'; -// import { IDatabaseConfig } from '../config/database.config'; -// import { StackInfo } from './util/LPStack'; -// config({ path: `.env.local`, override: true }); - -// export const POSTING_STACK_INFO: StackInfo = { NAME: 'posting-stack' }; - -// export interface PostingStackProps extends cdk.StackProps { -// databaseConfig: IDatabaseConfig; -// } - -// export class PostingStack extends cdk.Stack { -// constructor(scope: Construct, id: string, props: PostingStackProps) { -// super(scope, id, props); - -// const { databaseConfig } = props; - -// const dataBaseInfo = { -// DB_HOST: databaseConfig.host, -// DB_USERNAME: databaseConfig.user, -// DB_PASSWORD: databaseConfig.password, -// DB_NAME: databaseConfig.database, -// }; - -// const api: apigateway.RestApi = new apigateway.RestApi( -// this, -// 'users-api', -// { -// restApiName: 'postings-api', -// defaultCorsPreflightOptions: { -// allowOrigins: Cors.ALL_ORIGINS, -// }, -// } -// ); - -// const components = api.root.addResource('postings'); - -// const getPostings = new lambda.Function(this, 'getPostings', { -// runtime: lambda.Runtime.NODEJS_16_X, // execution environment -// code: lambda.Code.fromAsset('dist/postings/getPostings'), // code loaded from "lambda" directory -// handler: 'index.handler', -// environment: { -// ...dataBaseInfo, -// }, -// }); -// components.addMethod( -// 'GET', -// new apigateway.LambdaIntegration(getPostings) -// ); - -// const getApplications = new lambda.Function(this, 'getApplications', { -// runtime: lambda.Runtime.NODEJS_16_X, // execution environment -// code: lambda.Code.fromAsset('dist/postings/getApplications'), // code loaded from "lambda" directory -// handler: 'index.handler', -// environment: { -// ...dataBaseInfo, -// }, -// }); -// const getAppStatusTypes = new lambda.Function( -// this, -// 'getAppStatusTypes', -// { -// runtime: lambda.Runtime.NODEJS_16_X, // execution environment -// code: lambda.Code.fromAsset('dist/postings/getAppStatusTypes'), // code loaded from "lambda" directory -// handler: 'index.handler', -// environment: { -// ...dataBaseInfo, -// }, -// } -// ); - -// const getPosting = new lambda.Function(this, 'getPosting', { -// runtime: lambda.Runtime.NODEJS_16_X, // execution environment -// code: lambda.Code.fromAsset('dist/postings/getPosting'), // code loaded from "lambda" directory -// handler: 'index.handler', -// environment: { -// ...dataBaseInfo, -// }, -// }); - -// const getUserApplications = new lambda.Function( -// this, -// 'getUserApplications', -// { -// runtime: lambda.Runtime.NODEJS_16_X, // execution environment -// code: lambda.Code.fromAsset( -// 'dist/postings/getUserApplications' -// ), // code loaded from "lambda" directory -// handler: 'index.handler', -// environment: { -// ...dataBaseInfo, -// }, -// } -// ); - -// const components2 = components.addResource('{id}'); -// const components3 = components2.addResource('applications'); -// components3.addMethod( -// 'GET', -// new apigateway.LambdaIntegration(getApplications) -// ); - -// components2.addMethod( -// 'GET', -// new apigateway.LambdaIntegration(getPosting) -// ); - -// const components7 = components.addResource('applications'); -// components7.addMethod( -// 'GET', -// new apigateway.LambdaIntegration(getUserApplications) -// ); -// const components5 = components7.addResource('status'); -// components5.addMethod( -// 'GET', -// new apigateway.LambdaIntegration(getAppStatusTypes) -// ); -// } -// } diff --git a/lib/project.stack.ts b/lib/project.stack.ts deleted file mode 100644 index d9ccde9..0000000 --- a/lib/project.stack.ts +++ /dev/null @@ -1,129 +0,0 @@ -// import * as cdk from 'aws-cdk-lib'; -// import { Construct } from 'constructs'; -// import * as lambda from 'aws-cdk-lib/aws-lambda'; -// import * as apigateway from 'aws-cdk-lib/aws-apigateway'; -// import { Cors } from 'aws-cdk-lib/aws-apigateway'; -// import { config } from 'dotenv'; -// import { IDatabaseConfig } from '../config/database.config'; -// import { StackInfo } from './util/LPStack'; -// config({ path: `.env.local`, override: true }); - -// export const PROJECT_STACK_INFO: StackInfo = { NAME: 'project-stack' }; - -// export interface ProjectStackProps extends cdk.StackProps { -// databaseConfig: IDatabaseConfig; -// } - -// export class ProjectStack extends cdk.Stack { -// constructor(scope: Construct, id: string, props: ProjectStackProps) { -// super(scope, id, props); - -// const { databaseConfig } = props; - -// const dataBaseInfo = { -// DB_HOST: databaseConfig.host, -// DB_USERNAME: databaseConfig.user, -// DB_PASSWORD: databaseConfig.password, -// DB_NAME: databaseConfig.database, -// }; - -// const api: apigateway.RestApi = new apigateway.RestApi( -// this, -// 'users-api', -// { -// restApiName: 'projects-api', -// defaultCorsPreflightOptions: { -// allowOrigins: Cors.ALL_ORIGINS, -// }, -// } -// ); - -// const components = api.root.addResource('projects'); - -// const getProjects = new lambda.Function(this, 'getProjects', { -// runtime: lambda.Runtime.NODEJS_16_X, // execution environment -// code: lambda.Code.fromAsset('dist/projects/getProjects'), // code loaded from "lambda" directory -// handler: 'index.handler', -// environment: { -// ...dataBaseInfo, -// }, -// }); - -// const getProject = new lambda.Function(this, 'getProject', { -// runtime: lambda.Runtime.NODEJS_16_X, // execution environment -// code: lambda.Code.fromAsset('dist/projects/getProject'), // code loaded from "lambda" directory -// handler: 'index.handler', -// environment: { -// ...dataBaseInfo, -// }, -// }); - -// const createProject = new lambda.Function(this, 'createProject', { -// runtime: lambda.Runtime.NODEJS_16_X, // execution environment -// code: lambda.Code.fromAsset('dist/projects/createProject'), // code loaded from "lambda" directory -// handler: 'index.handler', -// environment: { -// ...dataBaseInfo, -// }, -// }); - -// const getResourceTypes = new lambda.Function(this, 'getResourceTypes', { -// runtime: lambda.Runtime.NODEJS_16_X, // execution environment -// code: lambda.Code.fromAsset('dist/projects/getResourceTypes'), // code loaded from "lambda" directory -// handler: 'index.handler', -// environment: { -// ...dataBaseInfo, -// }, -// }); - -// const getProjectRoles = new lambda.Function(this, 'getProjectRoles', { -// runtime: lambda.Runtime.NODEJS_16_X, // execution environment -// code: lambda.Code.fromAsset('dist/projects/getProjectRoles'), // code loaded from "lambda" directory -// handler: 'index.handler', -// environment: { -// ...dataBaseInfo, -// }, -// }); - -// const getProjectStatus = new lambda.Function(this, 'getProjectStatus', { -// runtime: lambda.Runtime.NODEJS_16_X, // execution environment -// code: lambda.Code.fromAsset('dist/projects/getProjectStatus'), // code loaded from "lambda" directory -// handler: 'index.handler', -// environment: { -// ...dataBaseInfo, -// }, -// }); - -// components.addMethod( -// 'GET', -// new apigateway.LambdaIntegration(getProjects) -// ); -// components.addMethod( -// 'POST', -// new apigateway.LambdaIntegration(createProject) -// ); -// const components2 = components.addResource('{id}'); -// components2.addMethod( -// 'GET', -// new apigateway.LambdaIntegration(getProject) -// ); - -// const components3 = api.root.addResource('project-roles'); -// components3.addMethod( -// 'GET', -// new apigateway.LambdaIntegration(getProjectRoles) -// ); - -// const components4 = api.root.addResource('project-status'); -// components4.addMethod( -// 'GET', -// new apigateway.LambdaIntegration(getProjectStatus) -// ); - -// const components5 = api.root.addResource('resource-types'); -// components5.addMethod( -// 'GET', -// new apigateway.LambdaIntegration(getResourceTypes) -// ); -// } -// } diff --git a/lib/user.stack.ts b/lib/user.stack.ts index a6aede4..168f2d4 100644 --- a/lib/user.stack.ts +++ b/lib/user.stack.ts @@ -68,7 +68,6 @@ export class UserStack extends LPStack { subresources: { roles: { endpoints: { - GET: { id: 'getUserRoles', path: `${baseLambdaDir}/roles/getUserRoles`, @@ -91,15 +90,6 @@ export class UserStack extends LPStack { }, }, }, - - me: { - endpoints: { - GET: { - id: 'getUserId', - path: `${usersLambdaDir}/getUserId`, - }, - }, - }, }, }, faculties: { @@ -112,8 +102,6 @@ export class UserStack extends LPStack { id: 'createFaculty', path: `${facultiesLambdaDir}/createFaculty`, }, - - }, subresources: { "{id}": { diff --git a/package-lock.json b/package-lock.json index 3406dac..bd8cecf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,12 +32,12 @@ "@types/aws-sdk": "^2.7.0", "@types/jest": "^29.5.2", "@types/node": "^18.14.6", - "@typescript-eslint/eslint-plugin": "^5.59.9", - "@typescript-eslint/parser": "^5.59.9", + "@typescript-eslint/eslint-plugin": "^6.20.0", + "@typescript-eslint/parser": "^6.20.0", "aws-cdk": "2.70.0", "babel-jest": "^29.5.0", "esbuild": "^0.17.19", - "eslint": "^8.42.0", + "eslint": "^8.56.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-prettier": "^4.2.1", "jest": "^29.5.0", @@ -50,6 +50,15 @@ "node": ">=18.0.0" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -3268,23 +3277,23 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", - "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/eslintrc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", - "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.2", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -3300,9 +3309,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.42.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.42.0.tgz", - "integrity": "sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -3317,13 +3326,13 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", - "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", "minimatch": "^3.0.5" }, "engines": { @@ -3344,9 +3353,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, "node_modules/@istanbuljs/load-nyc-config": { @@ -4888,9 +4897,9 @@ } }, "node_modules/@types/json-schema": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", - "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "node_modules/@types/node": { @@ -4906,9 +4915,9 @@ "dev": true }, "node_modules/@types/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, "node_modules/@types/stack-utils": { @@ -4933,32 +4942,33 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.59.9", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.9.tgz", - "integrity": "sha512-4uQIBq1ffXd2YvF7MAvehWKW3zVv/w+mSfRAu+8cKbfj3nwzyqJLNcZJpQ/WZ1HLbJDiowwmQ6NO+63nCA+fqA==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.20.0.tgz", + "integrity": "sha512-fTwGQUnjhoYHeSF6m5pWNkzmDDdsKELYrOBxhjMrofPqCkoC2k3B2wvGHFxa1CTIqkEn88nlW1HVMztjo2K8Hg==", "dev": true, "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.9", - "@typescript-eslint/type-utils": "5.59.9", - "@typescript-eslint/utils": "5.59.9", + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.20.0", + "@typescript-eslint/type-utils": "6.20.0", + "@typescript-eslint/utils": "6.20.0", + "@typescript-eslint/visitor-keys": "6.20.0", "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -4967,25 +4977,26 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.59.9", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.9.tgz", - "integrity": "sha512-FsPkRvBtcLQ/eVK1ivDiNYBjn3TGJdXy2fhXX+rc7czWl4ARwnpArwbihSOHI2Peg9WbtGHrbThfBUkZZGTtvQ==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.20.0.tgz", + "integrity": "sha512-bYerPDF/H5v6V76MdMYhjwmwgMA+jlPVqjSDq2cRqMi8bP5sR3Z+RLOiOMad3nsnmDVmn2gAFCyNgh/dIrfP/w==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.59.9", - "@typescript-eslint/types": "5.59.9", - "@typescript-eslint/typescript-estree": "5.59.9", + "@typescript-eslint/scope-manager": "6.20.0", + "@typescript-eslint/types": "6.20.0", + "@typescript-eslint/typescript-estree": "6.20.0", + "@typescript-eslint/visitor-keys": "6.20.0", "debug": "^4.3.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -4994,16 +5005,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.9", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.9.tgz", - "integrity": "sha512-8RA+E+w78z1+2dzvK/tGZ2cpGigBZ58VMEHDZtpE1v+LLjzrYGc8mMaTONSxKyEkz3IuXFM0IqYiGHlCsmlZxQ==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.20.0.tgz", + "integrity": "sha512-p4rvHQRDTI1tGGMDFQm+GtxP1ZHyAh64WANVoyEcNMpaTFn3ox/3CcgtIlELnRfKzSs/DwYlDccJEtr3O6qBvA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.9", - "@typescript-eslint/visitor-keys": "5.59.9" + "@typescript-eslint/types": "6.20.0", + "@typescript-eslint/visitor-keys": "6.20.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -5011,25 +5022,25 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.59.9", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.9.tgz", - "integrity": "sha512-ksEsT0/mEHg9e3qZu98AlSrONAQtrSTljL3ow9CGej8eRo7pe+yaC/mvTjptp23Xo/xIf2mLZKC6KPv4Sji26Q==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.20.0.tgz", + "integrity": "sha512-qnSobiJQb1F5JjN0YDRPHruQTrX7ICsmltXhkV536mp4idGAYrIyr47zF/JmkJtEcAVnIz4gUYJ7gOZa6SmN4g==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.59.9", - "@typescript-eslint/utils": "5.59.9", + "@typescript-eslint/typescript-estree": "6.20.0", + "@typescript-eslint/utils": "6.20.0", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -5038,12 +5049,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.59.9", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.9.tgz", - "integrity": "sha512-uW8H5NRgTVneSVTfiCVffBb8AbwWSKg7qcA4Ot3JI3MPCJGsB4Db4BhvAODIIYE5mNj7Q+VJkK7JxmRhk2Lyjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.20.0.tgz", + "integrity": "sha512-MM9mfZMAhiN4cOEcUOEx+0HmuaW3WBfukBZPCfwSqFnQy0grXYtngKCqpQN339X3RrwtzspWJrpbrupKYUSBXQ==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -5051,21 +5062,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.9", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.9.tgz", - "integrity": "sha512-pmM0/VQ7kUhd1QyIxgS+aRvMgw+ZljB3eDb+jYyp6d2bC0mQWLzUDF+DLwCTkQ3tlNyVsvZRXjFyV0LkU/aXjA==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.20.0.tgz", + "integrity": "sha512-RnRya9q5m6YYSpBN7IzKu9FmLcYtErkDkc8/dKv81I9QiLLtVBHrjz+Ev/crAqgMNW2FCsoZF4g2QUylMnJz+g==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.9", - "@typescript-eslint/visitor-keys": "5.59.9", + "@typescript-eslint/types": "6.20.0", + "@typescript-eslint/visitor-keys": "6.20.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -5077,53 +5089,82 @@ } } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/utils": { - "version": "5.59.9", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.9.tgz", - "integrity": "sha512-1PuMYsju/38I5Ggblaeb98TOoUvjhRvLpLa1DoTOFaLWqaXl/1iQ1eGurTXgBY58NUdtfTXKP5xBq7q9NDaLKg==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.20.0.tgz", + "integrity": "sha512-/EKuw+kRu2vAqCoDwDCBtDRU6CTKbUmwwI7SH7AashZ+W+7o8eiyy6V2cdOqN49KsTcASWsC5QeghYuRDTyOOg==", "dev": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.9", - "@typescript-eslint/types": "5.59.9", - "@typescript-eslint/typescript-estree": "5.59.9", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.20.0", + "@typescript-eslint/types": "6.20.0", + "@typescript-eslint/typescript-estree": "6.20.0", + "semver": "^7.5.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.9", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.9.tgz", - "integrity": "sha512-bT7s0td97KMaLwpEBckbzj/YohnvXtqbe2XgqNvTl6RJVakY5mvENOTPvw5u66nljfZxthESpDozs86U+oLY8Q==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.20.0.tgz", + "integrity": "sha512-E8Cp98kRe4gKHjJD4NExXKz/zOJ1A2hhZc+IMVD6i7w4yjIvh6VyuRI0gRtxAsXtoC35uGMaQ9rjI2zJaXDEAw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.9", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "6.20.0", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -6330,27 +6371,28 @@ } }, "node_modules/eslint": { - "version": "8.42.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.42.0.tgz", - "integrity": "sha512-ulg9Ms6E1WPf67PHaEY4/6E2tEn5/f7FXGzr3t9cBMugOmf1INYvuUwwh1aXQN4MfJ6a5K2iNwP3w4AColvI9A==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.42.0", - "@humanwhocodes/config-array": "^0.11.10", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -6360,7 +6402,6 @@ "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", @@ -6370,9 +6411,8 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "bin": { @@ -6418,23 +6458,10 @@ } } }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/eslint-visitor-keys": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", - "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -6460,9 +6487,9 @@ } }, "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", - "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -6485,12 +6512,12 @@ } }, "node_modules/espree": { - "version": "9.5.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", - "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "dependencies": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" }, @@ -6556,15 +6583,6 @@ "node": ">=4.0" } }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -6644,9 +6662,9 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -6911,9 +6929,9 @@ } }, "node_modules/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -6963,12 +6981,6 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, - "node_modules/grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", - "dev": true - }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -8485,12 +8497,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -8549,17 +8555,17 @@ } }, "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" }, "engines": { "node": ">= 0.8.0" @@ -8843,9 +8849,9 @@ } }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "engines": { "node": ">=6" @@ -9098,9 +9104,9 @@ "dev": true }, "node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -9358,6 +9364,18 @@ "node": ">=8.0" } }, + "node_modules/ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-jest": { "version": "29.1.0", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.0.tgz", @@ -9449,21 +9467,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -9733,15 +9736,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index 2adbd15..ed8a2d1 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ }, "scripts": { "clean": "rm -rf dist", - "build": " sh ./scripts/build", - "test:api": " sh ./scripts/serve", + "build": " sh ./scripts/build", + "test:api": " sh ./scripts/serve", "watch": "tsc -w", "cdk:synth": "cdk synth", "lint": "eslint .", @@ -23,7 +23,6 @@ "test:src": "jest --verbose ./src", "start:local:api": "sam local start-api -t ./cdk.out/$npm_config_stack.template.json --port 8000 --container-host-interface host.docker.internal", "invoke:lambda": "sam local invoke -t ./cdk.out/$npm_config_stack.template.json -e $npm_config_event ${npm_config_warm-containers=LAZY}", - "start:local:lambdas": "sam local start-lambda -t ./cdk.out/$npm_config_stack.template.json ${npm_config_port:3000} ${npm_config_warm-containers=LAZY}", "db:up": "esbuild ./resources/**/*.ts --entry-names=[dir]/[name]/index --bundle --platform=node --target=node16.14 --outdir=./dist/migrations && node ./dist/migrations/setup/index.js" }, @@ -49,12 +48,12 @@ "@types/aws-sdk": "^2.7.0", "@types/jest": "^29.5.2", "@types/node": "^18.14.6", - "@typescript-eslint/eslint-plugin": "^5.59.9", - "@typescript-eslint/parser": "^5.59.9", + "@typescript-eslint/eslint-plugin": "^6.20.0", + "@typescript-eslint/parser": "^6.20.0", "aws-cdk": "2.70.0", "babel-jest": "^29.5.0", "esbuild": "^0.17.19", - "eslint": "^8.42.0", + "eslint": "^8.56.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-prettier": "^4.2.1", "jest": "^29.5.0", @@ -63,4 +62,4 @@ "ts-node": "^10.9.1", "typescript": "~4.9.5" } -} \ No newline at end of file +} diff --git a/resources/database/migrations/1687038383236.sql b/resources/database/migrations/1687038383236.sql deleted file mode 100644 index 8958e7b..0000000 --- a/resources/database/migrations/1687038383236.sql +++ /dev/null @@ -1,96 +0,0 @@ --- Create the faculty table -CREATE TABLE IF NOT EXISTS faculty ( -id INT AUTO_INCREMENT PRIMARY KEY, -label VARCHAR(100) UNIQUE NOT NULL -); - --- Create the standing table -CREATE TABLE IF NOT EXISTS standing ( -id INT AUTO_INCREMENT PRIMARY KEY, -label VARCHAR(100) UNIQUE NOT NULL -); - --- Create the role table -CREATE TABLE IF NOT EXISTS role ( -id INT AUTO_INCREMENT PRIMARY KEY, -label VARCHAR(100) UNIQUE NOT NULL -); - --- Create the specialization table -CREATE TABLE IF NOT EXISTS specialization ( -id INT AUTO_INCREMENT PRIMARY KEY, -label VARCHAR(100) UNIQUE NOT NULL -); - --- Create the gender table -CREATE TABLE IF NOT EXISTS gender ( -id INT AUTO_INCREMENT PRIMARY KEY, -label VARCHAR(100) UNIQUE NOT NULL -); - --- Create the pronoun table -CREATE TABLE IF NOT EXISTS pronoun ( -id INT AUTO_INCREMENT PRIMARY KEY, -label VARCHAR(100) UNIQUE NOT NULL -); - --- Create the ethnicity table -CREATE TABLE IF NOT EXISTS ethnicity ( -id INT AUTO_INCREMENT PRIMARY KEY, -label VARCHAR(100) UNIQUE NOT NULL -); - --- CREATE the profile table -CREATE TABLE IF NOT EXISTS profile ( -id INT PRIMARY KEY, -first_name VARCHAR(255) NOT NULL, -pref_name VARCHAR(255) NOT NULL, -last_name VARCHAR(255) NOT NULL -); - --- Create the background table -CREATE TABLE IF NOT EXISTS background ( -id INT PRIMARY KEY, -resume_link VARCHAR(255), -faculty_id INT NOT NULL, -standing_id INT NOT NULL, -specialization_id INT NOT NULL -); - --- Create the account table -CREATE TABLE IF NOT EXISTS person ( -id INT AUTO_INCREMENT PRIMARY KEY, -username VARCHAR(255) UNIQUE NOT NULL, -email VARCHAR(255) UNIQUE NOT NULL, -created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -member_since TIMESTAMP NULL DEFAULT NULL -); - --- Create the person_role table -CREATE TABLE IF NOT EXISTS person_role ( -user_id INT, -role_id INT, -PRIMARY KEY (user_id, role_id) -); - --- Create the person_pronoun table -CREATE TABLE IF NOT EXISTS person_pronoun ( -user_id INT, -pronoun_id INT, -PRIMARY KEY (user_id, pronoun_id) -); - --- Create the person_gender table -CREATE TABLE IF NOT EXISTS person_gender ( -user_id INT, -gender_id INT, -PRIMARY KEY (user_id, gender_id) -); - --- Create the person_ethnicity table -CREATE TABLE IF NOT EXISTS person_ethnicity ( -user_id INT, -ethnicity_id INT, -PRIMARY KEY (user_id, ethnicity_id) -); \ No newline at end of file diff --git a/resources/database/migrations/1690825596849.sql b/resources/database/migrations/1690825596849.sql deleted file mode 100644 index 962a8b1..0000000 --- a/resources/database/migrations/1690825596849.sql +++ /dev/null @@ -1,34 +0,0 @@ --- -- Create the scope table --- CREATE TABLE IF NOT EXISTS scope ( --- label VARCHAR(100) PRIMARY KEY, --- description VARCHAR(255) --- ); - --- -- Create the scope_role table --- CREATE TABLE IF NOT EXISTS scope_role ( --- scope_label VARCHAR(100), --- role_label VARCHAR(100), --- PRIMARY KEY (scope_label, role_label), --- FOREIGN KEY (scope_label) REFERENCES scope (label) ON DELETE CASCADE, --- FOREIGN KEY (role_label) REFERENCES role (label) ON DELETE CASCADE ON UPDATE CASCADE --- ); - --- -- populate scope table --- INSERT INTO scope (label, description) VALUES ('admin:read', 'Admin full read access'); --- INSERT INTO scope (label, description) VALUES ('admin:write', 'Admin full write access'); --- INSERT INTO scope (label, description) VALUES ('admin:write:limited', 'can write most things, but not all'); --- INSERT INTO scope (label, description) VALUES ('profile:read:others', 'can read other profiles. When/if user is blocked/restricted the access can be revoked.'); --- INSERT INTO scope (label, description) VALUES ('profile:write:others', 'can update and delete other profiles'); --- INSERT INTO scope (label, description) VALUES ('profile:write:others:limited', 'can only update other profiles, but not delete'); - --- -- populate scope_role table --- INSERT INTO scope_role (scope_label, role_label) --- VALUES --- ('profile:read:others', 'Tech Lead'), --- ('profile:read:others', 'Design Lead'), --- ('profile:read:others', 'Co-pres'), --- ('admin:read', 'Co-pres'), --- ('admin:write', 'Co-pres'), --- ('profile:write:others', 'Tech Lead'), --- ('profile:write:others', 'Design Lead'), --- ('profile:write:others', 'Co-pres'); \ No newline at end of file diff --git a/resources/database/migrations/1700543219488.sql b/resources/database/migrations/1700543219488.sql deleted file mode 100644 index c09dd1d..0000000 --- a/resources/database/migrations/1700543219488.sql +++ /dev/null @@ -1,104 +0,0 @@ -INSERT INTO faculty (label) VALUES -('Forestry'), -('Land and Food Systems'), -('Education'), -('Business'), -('Arts'), -('Applied Science'), -('Science'), -('Medicine'), -('Other'); - - -INSERT INTO standing (label) VALUES -('First Year'), -('Second Year'), -('Third Year'), -('Fourth Year and up'), -('Graduate Student'), -('Alumni'), -('Other'); - -INSERT INTO role (id, label) VALUES -(1, 'Explorer');; - - - --- Populate degree_program table -INSERT INTO specialization (label) -VALUES - ('Accounting'), - ('African Studies'), - ('American Studies'), - ('Anthropology'), - ('Archaeology'), - ('Art History'), - ('Asian Studies'), - ('Astronomy'), - ('Biochemistry'), - ('Biology'), - ('Biomedical Engineering'), - ('Business Administration'), - ('Canadian Studies'), - ('Chemistry'), - ('Chinese Studies'), - ('Civil Engineering'), - ('Classics'), - ('Cognitive Systems: Computational Intelligence and Design'), - ('Communication'), - ('Computer Science'), - ('Conservation and Restoration of Cultural Property'), - ('Criminology'), - ('Development Studies'), - ('Economics'), - ('Education'), - ('Electrical and Computer Engineering'), - ('English'), - ('Environmental Engineering'), - ('Environmental Sciences'), - ('European Studies'), - ('Film and Media Studies'), - ('Finance'), - ('French Studies'), - ('Geography'), - ('Germanic and Slavic Studies'), - ('Global Studies'), - ('History'), - ('Human Geography'), - ('Information Technology'), - ('International Business'), - ('International Relations'), - ('Italian Studies'), - ('Japanese Studies'), - ('Jewish Studies'), - ('Latin American Studies'), - ('Law'), - ('Linguistics'), - ('Management'), - ('Marketing'), - ('Mathematics'), - ('Mechanical Engineering'), - ('Media Studies'), - ('Microbiology'), - ('Middle Eastern Studies'), - ('Molecular Biology and Biochemistry'), - ('Music'), - ('Natural Resources Economics'), - ('Neuroscience'), - ('Nursing'), - ('Philosophy'), - ('Physics'), - ('Political Science'), - ('Psychology'), - ('Public Policy'), - ('Religious Studies'), - ('Romance Languages and Literatures'), - ('Russian and East European Studies'), - ('Science'), - ('Sociology'), - ('Spanish and Portuguese Studies'), - ('Statistics'), - ('Sustainability'), - ('Theatre'), - ('Urban Planning'), - ('Other'); \ No newline at end of file diff --git a/resources/database/migrations/1703270939.sql b/resources/database/migrations/1703270939.sql new file mode 100644 index 0000000..a196e94 --- /dev/null +++ b/resources/database/migrations/1703270939.sql @@ -0,0 +1,89 @@ +-- Create the pronoun table +CREATE TABLE IF NOT EXISTS pronouns ( + id INT AUTO_INCREMENT PRIMARY KEY, + label VARCHAR(100) UNIQUE NOT NULL +); + +-- Create the gender table +CREATE TABLE IF NOT EXISTS gender ( + id INT AUTO_INCREMENT PRIMARY KEY, + label VARCHAR(100) UNIQUE NOT NULL +); + +-- Create the ethnicity table +CREATE TABLE IF NOT EXISTS ethnicity ( + id INT AUTO_INCREMENT PRIMARY KEY, + label VARCHAR(100) UNIQUE NOT NULL +); + +-- Create the faculty table +CREATE TABLE IF NOT EXISTS faculty ( + id INT AUTO_INCREMENT PRIMARY KEY, + label VARCHAR(100) UNIQUE NOT NULL +); + +-- Create the standing table +CREATE TABLE IF NOT EXISTS standing ( + id INT AUTO_INCREMENT PRIMARY KEY, + label VARCHAR(100) UNIQUE NOT NULL +); + +-- Create the specialization table +CREATE TABLE IF NOT EXISTS specialization ( + id INT AUTO_INCREMENT PRIMARY KEY, + label VARCHAR(100) UNIQUE NOT NULL +); + +CREATE TABLE IF NOT EXISTS person_role ( + person_id INT, + role_id INT, + PRIMARY KEY (person_id, role_id) +); + +-- Create the role table +CREATE TABLE IF NOT EXISTS role ( + id INT AUTO_INCREMENT PRIMARY KEY, + label VARCHAR(100) UNIQUE NOT NULL +); + +-- Create the scope table +CREATE TABLE IF NOT EXISTS scope ( + label VARCHAR(100) PRIMARY KEY, + description VARCHAR(255) +); + +-- Create the scope_role table +CREATE TABLE IF NOT EXISTS scope_role ( + scope_label VARCHAR(100), + role_label VARCHAR(100), + PRIMARY KEY (scope_label, role_label) +); + +-- Create the person table +CREATE TABLE IF NOT EXISTS person ( + id INT AUTO_INCREMENT PRIMARY KEY, + email VARCHAR(255), + member_since TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + account_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + first_name VARCHAR(255) NOT NULL, + pref_name VARCHAR(255), + last_name VARCHAR(255) NOT NULL, + pronouns_id INT, + gender_id INT, + ethnicity_id INT, + faculty_id INT, + standing_id INT, + specialization_id INT, + student_number VARCHAR(255), + phone_number VARCHAR(255), + linkedin_link VARCHAR(255), + github_link VARCHAR(255), + website_link VARCHAR(255), + resume_link VARCHAR(255), + KEY pronouns_id_idx (pronouns_id), + KEY gender_id_idx (gender_id), + KEY ethicity_id_idx (ethnicity_id), + KEY faculty_id_ids (faculty_id), + KEY standing_id_idx (standing_id), + KEY specialization_id_idx (specialization_id) +); diff --git a/resources/database/migrations/1703270982.sql b/resources/database/migrations/1703270982.sql new file mode 100644 index 0000000..b911466 --- /dev/null +++ b/resources/database/migrations/1703270982.sql @@ -0,0 +1,175 @@ +-- populate pronouns table +INSERT INTO pronouns (label) +VALUES + ('He/Him/His'), + ('She/Her/Hers'), + ('They/Them/Theirs'), + ('Ze/Hir/Hirs'), + ('I do not use a pronoun'), + ('I use all gender pronouns'), + ('Other, please ask'); + +-- populate gender table +INSERT INTO gender (label) +VALUES + ('Male'), + ('Female'), + ('Non-Binary'), + ("Other"); + +-- populate ethnicity table +INSERT INTO ethnicity (label) +VALUES + ('Indigenous'), + ('Caucasian / White'), + ('African / Black'), + ('East Asian'), + ('South Asian'), + ('Southeast Asian'), + ('Middle Eastern'), + ('Latin American / Hispanic'), + ('Caribbean'), + ('Other'); + + +-- populate faculty table +INSERT INTO faculty (label) +VALUES + ('Forestry'), + ('Land and Food Systems'), + ('Education'), + ('Business'), + ('Arts'), + ('Applied Science'), + ('Science'), + ('Medicine'), + ('Other'); + +-- populate standing table +INSERT INTO standing (label) +VALUES + ('First Year'), + ('Second Year'), + ('Third Year'), + ('Fourth Year and up'), + ('Graduate Student'), + ('Alumni'), + ('Other'); + +-- populate specialization table +INSERT INTO specialization (label) +VALUES + ('Accounting'), + ('African Studies'), + ('American Studies'), + ('Anthropology'), + ('Archaeology'), + ('Art History'), + ('Asian Studies'), + ('Astronomy'), + ('Biochemistry'), + ('Biology'), + ('Biomedical Engineering'), + ('Business Administration'), + ('Canadian Studies'), + ('Chemistry'), + ('Chinese Studies'), + ('Civil Engineering'), + ('Classics'), + ('Cognitive Systems: Computational Intelligence and Design'), + ('Communication'), + ('Computer Science'), + ('Conservation and Restoration of Cultural Property'), + ('Criminology'), + ('Development Studies'), + ('Economics'), + ('Education'), + ('Electrical and Computer Engineering'), + ('English'), + ('Environmental Engineering'), + ('Environmental Sciences'), + ('European Studies'), + ('Film and Media Studies'), + ('Finance'), + ('French Studies'), + ('Geography'), + ('Germanic and Slavic Studies'), + ('Global Studies'), + ('History'), + ('Human Geography'), + ('Information Technology'), + ('International Business'), + ('International Relations'), + ('Italian Studies'), + ('Japanese Studies'), + ('Jewish Studies'), + ('Latin American Studies'), + ('Law'), + ('Linguistics'), + ('Management'), + ('Marketing'), + ('Mathematics'), + ('Mechanical Engineering'), + ('Media Studies'), + ('Microbiology'), + ('Middle Eastern Studies'), + ('Molecular Biology and Biochemistry'), + ('Music'), + ('Natural Resources Economics'), + ('Neuroscience'), + ('Nursing'), + ('Philosophy'), + ('Physics'), + ('Political Science'), + ('Psychology'), + ('Public Policy'), + ('Religious Studies'), + ('Romance Languages and Literatures'), + ('Russian and East European Studies'), + ('Science'), + ('Sociology'), + ('Spanish and Portuguese Studies'), + ('Statistics'), + ('Sustainability'), + ('Theatre'), + ('Urban Planning'), + ('Other'); + +-- populate role table +INSERT INTO role (label) +VALUES + ('Admin'), + ('Lead'), + ('Member'); + +-- populate scope table +INSERT INTO scope (label, description) +VALUES + ('read:admin', 'Admin full read access'), + ('read:profile:all', 'Read all data of profiles'), + ('read:profile:restricted', 'Read certain data of profiles'), + ('read:profile:personal', 'Read all data to your own profile'), + ('write:admin', 'Admin full write access'), + ('write:profile:all', 'Write all data of profiles'), + ('update:admin', 'Admin full update access'), + ('update:profile:all', 'Update all data of profiles'), + ('update:profile:personal', 'Update all data to your own profile'), + ('delete:admin', 'Admin full delete access'), + ('delete:profile:all', 'Delete all data of profiles'), + ('delete:profile:personal', 'Delete all data to your own profile'); + +-- populate scope_role table +INSERT INTO scope_role (scope_label, role_label) +VALUES + ('read:admin', 'Admin'), + ('write:admin', 'Admin'), + ('update:admin', 'Admin'), + ('delete:admin', 'Admin'), + ('read:profile:all', 'Lead'), + ('write:profile:all', 'Lead'), + ('update:profile:all', 'Lead'), + ('delete:profile:all', 'Lead'), + ('read:profile:personal', 'Member'), + ('read:profile:restricted', 'Member'), + ('update:profile:personal', 'Member'), + ('delete:profile:personal', 'Member'); diff --git a/resources/database/setup.ts b/resources/database/setup.ts index f67980e..f811c6e 100644 --- a/resources/database/setup.ts +++ b/resources/database/setup.ts @@ -6,8 +6,7 @@ import mysql, { Connection } from 'mysql2'; console.log('Connected to PlanetScale!') dotenv.config(); -const DATABASE_META_NAME = 'meta'; -const DATABASE_NAME = 'cosmic-dev'; +const DATABASE_NAME = 'userbase'; const MIGRATION_PATH = './resources/database/migrations'; const setUpDatabase = async ( @@ -29,7 +28,7 @@ const setUpDatabase = async ( 'Migrations table already exists. Skipping initialization' ); } - await runMigrations(connection, DATABASE_NAME); + await runMigrations(connection); }; const initializeDatabase = async (connection: Connection): Promise => { @@ -72,7 +71,6 @@ const resetDatabase = async (connection: Connection): Promise => { async function runMigrations( connection: Connection, - primaryDatabaseName = 'main' ): Promise { let files = fs.readdirSync(MIGRATION_PATH); @@ -94,11 +92,6 @@ async function runMigrations( }); } - // await query(connection, `CREATE DATABASE IF NOT EXISTS ??`, [ - // primaryDatabaseName, - // ]); - // await query(connection, `USE ??`, [primaryDatabaseName]); - const currentDB = await query(connection, 'SELECT database() AS db'); console.log( chalk.blue('INFO: ') + diff --git a/scripts/serve b/scripts/serve index 469746c..971ac20 100644 --- a/scripts/serve +++ b/scripts/serve @@ -34,7 +34,7 @@ esbuild ./src/**/*.ts \ cdk synth --no-staging --quiet # Default values -template="wiki-stack.template" +template="users-stack.template" port="8000" lazy="LAZY" diff --git a/src/roles/addUserRole.ts b/src/roles/addUserRole.ts index ac16dd6..c6f71f1 100644 --- a/src/roles/addUserRole.ts +++ b/src/roles/addUserRole.ts @@ -1,23 +1,40 @@ import { getDatabase } from '../util/db'; -import { LambdaBuilder } from '../util/middleware/middleware'; -import { BadRequestError, SuccessResponse } from '../util/middleware/response'; -import { InputValidator } from '../util/middleware/inputValidator'; -import { APIGatewayEvent } from 'aws-lambda'; import { Authorizer } from '../util/middleware/authorizer'; +import { InputValidator } from '../util/middleware/inputValidator'; +import { LambdaBuilder, LambdaInput } from '../util/middleware/middleware'; +import { BadRequestError, SuccessResponse } from '../util/middleware/response'; +import { + ACCESS_SCOPES, + ScopeController, +} from '../util/middleware/scopeHandler'; const db = getDatabase(); + +// Only valid for user with Admin role +const validScopes = [ACCESS_SCOPES.ADMIN_WRITE]; + export const handler = new LambdaBuilder(addUserRoleRequest) .use(new InputValidator()) - .use(new Authorizer()) + .use(new Authorizer(db)) + .use(new ScopeController(db)) .build(); -async function addUserRoleRequest(event: APIGatewayEvent) { +async function addUserRoleRequest(event: LambdaInput) { + if (!event.pathParameters) { + throw new BadRequestError('Event path parameters missing'); + } + if (!event.pathParameters.id || !event.pathParameters.roleId) { + throw new BadRequestError('User id or role id missing'); + } const { id, roleId } = event.pathParameters; - await addRole(id, roleId); + + ScopeController.verifyScopes(event.userScopes, validScopes); + + await addRole(parseInt(id), parseInt(roleId)); return new SuccessResponse({ message: 'Role added successfully' }); } -export const addRole = async (userId: string, roleId: string) => { +export const addRole = async (userId: number, roleId: number) => { const verifyRole = await db .selectFrom('role') .select('id') @@ -30,6 +47,6 @@ export const addRole = async (userId: string, roleId: string) => { await db .insertInto('person_role') - .values({ user_id: userId, role_id: roleId }) + .values({ person_id: userId, role_id: roleId }) .execute(); }; diff --git a/src/roles/createRole.ts b/src/roles/createRole.ts index 14673c5..6a55cd3 100644 --- a/src/roles/createRole.ts +++ b/src/roles/createRole.ts @@ -1,20 +1,34 @@ import { getDatabase, NewRole } from '../util/db'; -import {LambdaBuilder, LambdaInput} from '../util/middleware/middleware'; -import { SuccessResponse } from '../util/middleware/response'; +import { Authorizer } from '../util/middleware/authorizer'; import { InputValidator } from '../util/middleware/inputValidator'; +import { LambdaBuilder, LambdaInput } from '../util/middleware/middleware'; +import { BadRequestError, SuccessResponse } from '../util/middleware/response'; +import { + ACCESS_SCOPES, + ScopeController, +} from '../util/middleware/scopeHandler'; import { getRoles, refreshCache } from './roles'; -import { Authorizer } from '../util/middleware/authorizer'; -import {ScopeController} from "../util/middleware/scopeHandler"; const db = getDatabase(); + +// Only valid for Admin role +const validScopes = [ACCESS_SCOPES.ADMIN_WRITE]; export const handler = new LambdaBuilder(createRoleRequest) .use(new InputValidator()) - .use(new Authorizer()) + .use(new Authorizer(db)) .use(new ScopeController(db)) .build(); async function createRoleRequest(event: LambdaInput) { - ScopeController.verifyScopes(event.userScopes, ['admin:write']) + if (!event.userScopes) { + throw new BadRequestError('Event userScopes missing'); + } + if (!event.body) { + throw new BadRequestError('Event body missing'); + } + + ScopeController.verifyScopes(event.userScopes, validScopes); + const { label } = JSON.parse(event.body); await createRole({ label }); await refreshCache(db); @@ -22,10 +36,15 @@ async function createRoleRequest(event: LambdaInput) { } export const createRole = async (newRole: NewRole) => { - const { id } = await db + const result = await db .insertInto('role') .values(newRole) .returning('id') .executeTakeFirst(); - return id; + + if (result === undefined) { + throw new BadRequestError('Role not created'); + } + + return result.id; }; diff --git a/src/roles/deleteRole.ts b/src/roles/deleteRole.ts index 5fba42d..c7c1f78 100644 --- a/src/roles/deleteRole.ts +++ b/src/roles/deleteRole.ts @@ -1,26 +1,40 @@ import { getDatabase } from '../util/db'; -import {LambdaBuilder, LambdaInput} from '../util/middleware/middleware'; -import { SuccessResponse } from '../util/middleware/response'; +import { Authorizer } from '../util/middleware/authorizer'; import { InputValidator } from '../util/middleware/inputValidator'; +import { LambdaBuilder, LambdaInput } from '../util/middleware/middleware'; +import { BadRequestError, SuccessResponse } from '../util/middleware/response'; +import { + ACCESS_SCOPES, + ScopeController, +} from '../util/middleware/scopeHandler'; import { getRoles, refreshCache } from './roles'; -import { Authorizer } from '../util/middleware/authorizer'; -import {ScopeController} from "../util/middleware/scopeHandler"; const db = getDatabase(); + +// Only valid for Admin role +const validScopes = [ACCESS_SCOPES.ADMIN_WRITE]; export const handler = new LambdaBuilder(deleteRoleRequest) .use(new InputValidator()) - .use(new Authorizer()) + .use(new Authorizer(db)) .use(new ScopeController(db)) .build(); async function deleteRoleRequest(event: LambdaInput) { - ScopeController.verifyScopes(event.userScopes, ['admin:write']) - const { id } = event.pathParameters; + if (!event.pathParameters) { + throw new BadRequestError('Event path parameters missing'); + } + if (!event.pathParameters.id) { + throw new BadRequestError('ID is undefined'); + } + const id = parseInt(event.pathParameters.id); + + ScopeController.verifyScopes(event.userScopes, validScopes); + await deleteRole(id); await refreshCache(db); return new SuccessResponse(await getRoles(db)); } -export const deleteRole = async (id: string) => { +export const deleteRole = async (id: number) => { await db.deleteFrom('role').where('id', '=', id).execute(); }; diff --git a/src/roles/deleteUserRole.ts b/src/roles/deleteUserRole.ts index 3b7162a..7c53fd5 100644 --- a/src/roles/deleteUserRole.ts +++ b/src/roles/deleteUserRole.ts @@ -1,36 +1,53 @@ import { getDatabase } from '../util/db'; -import { LambdaBuilder } from '../util/middleware/middleware'; -import { BadRequestError, SuccessResponse } from '../util/middleware/response'; -import { InputValidator } from '../util/middleware/inputValidator'; -import { APIGatewayEvent } from 'aws-lambda'; import { Authorizer } from '../util/middleware/authorizer'; +import { InputValidator } from '../util/middleware/inputValidator'; +import { LambdaBuilder, LambdaInput } from '../util/middleware/middleware'; +import { BadRequestError, SuccessResponse } from '../util/middleware/response'; +import { + ACCESS_SCOPES, + ScopeController, +} from '../util/middleware/scopeHandler'; const db = getDatabase(); + +// Only valid for user with Admin role +const validScopes = [ACCESS_SCOPES.ADMIN_WRITE]; export const handler = new LambdaBuilder(deleteUserRoleRequest) .use(new InputValidator()) - .use(new Authorizer()) + .use(new Authorizer(db)) + .use(new ScopeController(db)) .build(); -async function deleteUserRoleRequest(event: APIGatewayEvent) { - const { id, roleId } = JSON.parse(event.body); - await deleteUserRole(id, roleId); +async function deleteUserRoleRequest(event: LambdaInput) { + if (!event.pathParameters) { + throw new BadRequestError('Event pathParameters missing'); + } + if (!event.pathParameters.id || !event.pathParameters.roleId) { + throw new BadRequestError('User id or role id missing'); + } + + const { id, roleId } = event.pathParameters; + ScopeController.verifyScopes(event.userScopes, validScopes); + await deleteUserRole(parseInt(id), parseInt(roleId)); return new SuccessResponse({ message: 'Role deleted successfully' }); } -export const deleteUserRole = async (userId: string, roleId: string) => { +export const deleteUserRole = async (userId: number, roleId: number) => { const verifyRole = await db .selectFrom('person_role') - .select(['user_id', 'role_id']) - .where('user_id', '=', userId) + .select(['person_id', 'role_id']) + .where('person_id', '=', userId) .where('role_id', '=', roleId) .executeTakeFirst(); if (!verifyRole) { - throw new BadRequestError(`role id: ${roleId} does not exist for user id: ${userId}`); + throw new BadRequestError( + `role id: ${roleId} does not exist for user id: ${userId}` + ); } await db .deleteFrom('person_role') - .where('user_id', '=', userId) + .where('person_id', '=', userId) .where('role_id', '=', roleId) .execute(); }; diff --git a/src/roles/getUserRoles.ts b/src/roles/getUserRoles.ts index 5886c1e..aaef327 100644 --- a/src/roles/getUserRoles.ts +++ b/src/roles/getUserRoles.ts @@ -1,38 +1,47 @@ -import {Database, getDatabase} from '../util/db'; -import { LambdaBuilder } from '../util/middleware/middleware'; -import { SuccessResponse } from '../util/middleware/response'; -import { InputValidator } from '../util/middleware/inputValidator'; import { APIGatewayEvent } from 'aws-lambda'; +import { Kysely } from 'kysely'; +import { Database, getDatabase } from '../util/db'; +import { InputValidator } from '../util/middleware/inputValidator'; +import { LambdaBuilder } from '../util/middleware/middleware'; +import { BadRequestError, SuccessResponse } from '../util/middleware/response'; import { Authorizer } from '../util/middleware/authorizer'; -import {Kysely} from "kysely"; const db = getDatabase(); export const handler = new LambdaBuilder(getUserRoleRequest) .use(new InputValidator()) - .use(new Authorizer()) + .use(new Authorizer(db)) .build(); async function getUserRoleRequest(event: APIGatewayEvent) { - const {id} = event.pathParameters; - const roles = await getUserRoles(id); - const userRoles: {id: string, label: string, scopes: string[]}[] = []; - roles.map(role => userRoles.push({id: role.id, label: role.label, scopes: []})); + if (!event.pathParameters) { + throw new BadRequestError('Event path parameters missing'); + } + const { id } = event.pathParameters; + const roles = await getUserRoles(Number(id)); + const userRoles: { id: number; label: string; scopes: string[] }[] = []; + roles.map((role) => + userRoles.push({ id: role.id, label: role.label, scopes: [] }) + ); for (const role of userRoles) { role.scopes = await attachScopes(db, role.label); } return new SuccessResponse(userRoles); } -export const getUserRoles = async (userId: string) => { +export const getUserRoles = async (userId: number) => { return await db - .selectFrom('person_role') - .innerJoin('role', 'role.id', 'person_role.role_id') + .selectFrom('role') + .innerJoin('person_role', 'role.id', 'person_role.role_id') .select(['role.id', 'role.label']) - .where('user_id', '=', userId) + .where('person_role.person_id', '=', userId) .execute(); }; export const attachScopes = async (db: Kysely, role: string) => { - const scopes = await db.selectFrom('scope_role').select(['scope_label']).where('role_label', '=', role).execute(); - return scopes.map(scope => scope.scope_label); -} + const scopes = await db + .selectFrom('scope_role') + .select(['scope_label']) + .where('role_label', '=', role) + .execute(); + return scopes.map((scope) => scope.scope_label); +}; diff --git a/src/users/createUser.ts b/src/users/createUser.ts index e82c58a..a358485 100644 --- a/src/users/createUser.ts +++ b/src/users/createUser.ts @@ -1,34 +1,48 @@ -import { APIGatewayProxyEvent } from 'aws-lambda'; import { getDatabase, NewPerson, Person } from '../util/db'; -import { LambdaBuilder } from '../util/middleware/middleware'; -import { InputValidator } from '../util/middleware/inputValidator'; import { Authorizer } from '../util/middleware/authorizer'; +import { InputValidator } from '../util/middleware/inputValidator'; +import { LambdaBuilder, LambdaInput } from '../util/middleware/middleware'; import { APIResponse, BadRequestError, SuccessResponse, } from '../util/middleware/response'; +import { + ACCESS_SCOPES, + ScopeController, +} from '../util/middleware/scopeHandler'; const db = getDatabase(); +const validScopes = [ACCESS_SCOPES.ADMIN_WRITE, ACCESS_SCOPES.WRITE_PROFILE]; export const handler = new LambdaBuilder(router) .use(new InputValidator()) - .use(new Authorizer()) + .use(new Authorizer(db)) + .use(new ScopeController(db)) .build(); -export async function router( - event: APIGatewayProxyEvent -): Promise { +export async function router(event: LambdaInput): Promise { + if (!event.body) { + throw new BadRequestError('Event body missing'); + } + if (!event.userScopes) { + throw new BadRequestError('Event userScopes missing'); + } + + ScopeController.verifyScopes(event.userScopes, validScopes); + const body = JSON.parse(event.body) as Person; + const createdUserId = await CreateUser(body); + return new SuccessResponse({ - message: `user with id : ${createdUserId} created`, + message: `user with id: ${createdUserId} created`, }); } export const CreateUser = async (person: Person): Promise => { await validateUserInformation(person); - return await AddUserToDatabase(person); + return await addUserToDatabase(person); }; export const validateUserInformation = async ( @@ -81,7 +95,7 @@ export const validateEmail = async (email: string): Promise => { } }; -export const validateFacultyId = async (facultyId: string): Promise => { +export const validateFacultyId = async (facultyId: number): Promise => { const faculty = await db .selectFrom('faculty') .select(['id']) @@ -92,7 +106,7 @@ export const validateFacultyId = async (facultyId: string): Promise => { } }; -export const validateStandingId = async (standingId: string): Promise => { +export const validateStandingId = async (standingId: number): Promise => { const standing = await db .selectFrom('standing') .select(['id']) @@ -104,7 +118,7 @@ export const validateStandingId = async (standingId: string): Promise => { }; export const validateSpecializationId = async ( - specializationId: string + specializationId: number ): Promise => { const specialization = await db .selectFrom('specialization') @@ -116,21 +130,44 @@ export const validateSpecializationId = async ( } }; -export const AddUserToDatabase = async (user: NewPerson): Promise => { +export const addUserToDatabase = async (user: NewPerson): Promise => { const UserInfo = user; for (const key in UserInfo) { if (UserInfo[key] === undefined) { UserInfo[key] = null; } } - if (!user.username) { - user.username = user.email; - } const person = await db .insertInto('person') .values(user) - .returning('id') .executeTakeFirst(); - return person.id; + + if (!person.insertId) { + throw new BadRequestError('Could not find created user id'); + } + + addUserDefaultRole(Number(person.insertId)); + + return person.insertId.toString(); +}; + + +export const addUserDefaultRole = async (userId: number): Promise => { + const roles = await db + .selectFrom('role') + .select(['id', 'label']) + .where('label', 'like', 'Member') + .execute(); + + if (roles.length === 0) { + throw new BadRequestError('Role not found'); + } + + for (const role of roles) { + await db + .insertInto('person_role') + .values({ person_id: userId, role_id: role.id }) + .execute(); + } }; diff --git a/src/users/deleteUser.ts b/src/users/deleteUser.ts index 7a5fdeb..201a069 100644 --- a/src/users/deleteUser.ts +++ b/src/users/deleteUser.ts @@ -1,26 +1,74 @@ -import { APIGatewayProxyEvent } from 'aws-lambda'; import { getDatabase } from '../util/db'; -import { LambdaBuilder } from '../util/middleware/middleware'; -import { APIResponse, SuccessResponse } from '../util/middleware/response'; -import { InputValidator } from '../util/middleware/inputValidator'; import { Authorizer } from '../util/middleware/authorizer'; +import { InputValidator } from '../util/middleware/inputValidator'; +import { LambdaBuilder, LambdaInput } from '../util/middleware/middleware'; +import { + APIResponse, + BadRequestError, + SuccessResponse, +} from '../util/middleware/response'; +import { ACCESS_SCOPES } from '../util/middleware/scopeHandler'; const db = getDatabase(); +const validScopes = [ + ACCESS_SCOPES.ADMIN_DELETE, + ACCESS_SCOPES.DELETE_ALL_PROFILE, +]; export const handler = new LambdaBuilder(router) .use(new InputValidator()) - .use(new Authorizer()) + .use(new Authorizer(db)) .build(); -export async function router( - event: APIGatewayProxyEvent -): Promise { +export async function router(event: LambdaInput): Promise { + if (!event.pathParameters) { + throw new BadRequestError('Event path parameters missing'); + } + validateScope(event); + await deleteUser(event.pathParameters.id as string); + await deleteUserRole(event.pathParameters.id as string); + return new SuccessResponse({ message: `user with id : ${event.pathParameters.id} deleted`, }); } export const deleteUser = async (userId: string): Promise => { - await db.deleteFrom('person').where('id', '=', userId).execute(); + const verifyRole = await db + .selectFrom('person') + .select(['id']) + .where('id', '=', parseInt(userId)) + .executeTakeFirst(); + if (!verifyRole) { + throw new BadRequestError(`user id: ${userId} does not exist`); + } + await db.deleteFrom('person').where('id', '=', Number(userId)).execute(); +}; + +export const deleteUserRole = async (userId: string): Promise => { + await db + .deleteFrom('person_role') + .where('person_id', '=', Number(userId)) + .execute(); +}; + +export const validateScope = (event: LambdaInput) => { + if (!event.pathParameters) { + throw new BadRequestError('Event path parameters missing'); + } + if (!event.pathParameters.id) { + throw new BadRequestError('Id missing'); + } + + const id = parseInt(event.pathParameters.id); + const userScopes = event.userScopes; + Authorizer.authorizeOrVerifyScopes( + db, + id, + userScopes, + ACCESS_SCOPES.DELETE_OWN_PROFILE, + validScopes, + event.googleUser + ); }; diff --git a/src/users/editUser.ts b/src/users/editUser.ts index bb34746..3e64a4a 100644 --- a/src/users/editUser.ts +++ b/src/users/editUser.ts @@ -1,42 +1,80 @@ -import { APIGatewayProxyEvent } from 'aws-lambda'; -import { LambdaBuilder } from '../util/middleware/middleware'; -import { InputValidator } from '../util/middleware/inputValidator'; +import { UpdatePerson, getDatabase } from '../util/db'; import { Authorizer } from '../util/middleware/authorizer'; -import { APIResponse, SuccessResponse } from '../util/middleware/response'; -import { getDatabase, UpdatePerson } from '../util/db'; +import { InputValidator } from '../util/middleware/inputValidator'; +import { LambdaBuilder, LambdaInput } from '../util/middleware/middleware'; +import { + APIResponse, + BadRequestError, + SuccessResponse, +} from '../util/middleware/response'; +import { + ACCESS_SCOPES, + ScopeController, +} from '../util/middleware/scopeHandler'; const db = getDatabase(); +const validScopes = [ACCESS_SCOPES.UPDATE_ALL_PROFILE]; export const handler = new LambdaBuilder(updateRequest) .use(new InputValidator()) - .use(new Authorizer()) + .use(new Authorizer(db)) + .use(new ScopeController(db)) .build(); -export async function updateRequest( - event: APIGatewayProxyEvent -): Promise { - await updateUser( - event.pathParameters.id as string, - JSON.parse(event.body) as UpdatePerson +export async function updateRequest(event: LambdaInput): Promise { + if (!event.pathParameters) { + throw new Error('Event parameters is missing'); + } + + if (!event.body) { + throw new Error('Event body missing'); + } + if (!event.pathParameters.id) { + throw new BadRequestError('ID is undefined'); + } + + const id = parseInt(event.pathParameters.id); + const userScopes = event.userScopes; + await Authorizer.authorizeOrVerifyScopes( + db, + id, + userScopes, + ACCESS_SCOPES.UPDATE_OWN_PROFILE, + validScopes, + event.googleAccount ); + + const updatePersonData = JSON.parse(event.body) as UpdatePerson; + + await updateUser(event.pathParameters.id as string, updatePersonData); return new SuccessResponse({ - message: `user with id : ${event.pathParameters.id} updated`, + message: `User with id : ${event.pathParameters.id} updated successfully`, + updateUser: updatePersonData, }); } export const updateUser = async ( userId: string, - UpdatePerson: UpdatePerson + updatePersonData: UpdatePerson ): Promise => { + const existingUser = await db + .selectFrom('person') + .select('id') + .where('id', '=', Number(userId)) + .executeTakeFirst(); + + if (!existingUser) { + throw new Error(`User with id ${userId} not found`); + } + const updatedUser = { - ...UpdatePerson, - updated_at: new Date().toISOString(), + ...updatePersonData, + account_updated: new Date(), }; + await db .updateTable('person') - .set({ - ...updatedUser, - }) - .where('id', '=', userId) + .set(updatedUser) + .where('id', '=', Number(userId)) .execute(); }; diff --git a/src/users/getUser.ts b/src/users/getUser.ts index aa2b2d4..c0c05e8 100644 --- a/src/users/getUser.ts +++ b/src/users/getUser.ts @@ -1,25 +1,54 @@ import { getDatabase } from '../util/db'; -import { LambdaBuilder } from '../util/middleware/middleware'; -import { APIGatewayProxyEvent } from 'aws-lambda'; -import { APIResponse, SuccessResponse } from '../util/middleware/response'; import { Authorizer } from '../util/middleware/authorizer'; import { InputValidator } from '../util/middleware/inputValidator'; +import { LambdaBuilder, LambdaInput } from '../util/middleware/middleware'; +import { + APIResponse, + BadRequestError, + SuccessResponse, +} from '../util/middleware/response'; +import { + ACCESS_SCOPES, + ScopeController, +} from '../util/middleware/scopeHandler'; const db = getDatabase(); +const validScopes = [ + ACCESS_SCOPES.READ_ALL_PROFILE_DATA, + ACCESS_SCOPES.ADMIN_READ, +]; + export const handler = new LambdaBuilder(router) .use(new InputValidator()) - .use(new Authorizer()) + .use(new Authorizer(db)) + .use(new ScopeController(db)) .build(); -export async function router( - event: APIGatewayProxyEvent -): Promise { - const id = event.pathParameters.id; +export async function router(event: LambdaInput): Promise { + if (!event.pathParameters) { + throw new BadRequestError('Event path parameters missing'); + } + + if (event.pathParameters.id === undefined) { + throw new BadRequestError('ID is undefined'); + } + + const id = parseInt(event.pathParameters.id); + + await Authorizer.authorizeOrVerifyScopes( + db, + id, + event.userScopes, + ACCESS_SCOPES.READ_OWN_PROFILE, + validScopes, + event.googleAccount + ); + return new SuccessResponse(await getUser(id)); } -export async function getUser(userId: string) { +export async function getUser(userId: number) { const res = await db .selectFrom('person') .innerJoin('faculty', 'person.faculty_id', 'faculty.id') @@ -31,18 +60,24 @@ export async function getUser(userId: string) { ) .select([ 'person.id', - 'person.last_name', - 'person.pref_name', - 'person.first_name', - 'person.created_at', - 'person.updated_at', - 'person.resume_link', - 'person.member_since', - 'person.username', 'person.email', + 'person.member_since', + 'person.account_updated', + 'person.first_name', + 'person.pref_name', + 'person.last_name', + 'person.pronouns_id', + 'person.gender_id', + 'person.ethnicity_id', 'person.faculty_id', 'person.standing_id', 'person.specialization_id', + 'person.student_number', + 'person.phone_number', + 'person.linkedin_link', + 'person.github_link', + 'person.website_link', + 'person.resume_link', 'faculty.label as faculty_label', 'standing.label as standing_label', 'specialization.label as specialization_label', @@ -56,15 +91,24 @@ export async function getUser(userId: string) { return { id: res.id, + email: res.email, + member_since: res.member_since, + account_updated: res.account_updated, first_name: res.first_name, last_name: res.last_name, pref_name: res.pref_name, - email: res.email, - created_at: res.created_at, - updated_at: res.updated_at, + pronouns_id: res.pronouns_id, + gender_id: res.gender_id, + ethnicity_id: res.ethnicity_id, + faculty_id: res.faculty_id, + standing_id: res.standing_id, + specialiaztion_id: res.specialization_id, + student_number: res.student_number, + phone_number: res.phone_number, + linkedin_link: res.linkedin_link, + github_link: res.github_link, + website_link: res.website_link, resume_link: res.resume_link, - member_since: res.member_since, - username: res.username, faculty: { id: res.faculty_id, label: res.faculty_label, diff --git a/src/users/getUserId.ts b/src/users/getUserId.ts deleted file mode 100644 index 6ea322c..0000000 --- a/src/users/getUserId.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { APIGatewayProxyEvent } from 'aws-lambda'; -import { getDatabase } from '../util/db'; -import { LambdaBuilder } from '../util/middleware/middleware'; -import { Authorizer } from '../util/middleware/authorizer'; -import { InputValidator } from '../util/middleware/inputValidator'; -import { - APIResponse, - NotFoundError, - SuccessResponse, -} from '../util/middleware/response'; - -const db = getDatabase(); -export const handler = new LambdaBuilder(router) - .use(new InputValidator()) - .use(new Authorizer()) - .build(); - -export async function router( - event: APIGatewayProxyEvent -): Promise { - const userEmail = (event as unknown as { googleAccount: { email: string } }) - .googleAccount.email; - const user = await getUser(userEmail); - return new SuccessResponse(user); -} - -export async function getUser(userEmail: string) { - const user = await db - .selectFrom('person') - .select(['id', 'email']) - .where('email', '=', userEmail) - .executeTakeFirst(); - - if (!user) { - throw new NotFoundError(`User with email ${userEmail} not found`); - } - return user; -} diff --git a/src/users/getUsers.ts b/src/users/getUsers.ts index a7b9222..e457064 100644 --- a/src/users/getUsers.ts +++ b/src/users/getUsers.ts @@ -1,25 +1,37 @@ +import { getDatabase } from '../util/db'; import { Authorizer } from '../util/middleware/authorizer'; import { InputValidator } from '../util/middleware/inputValidator'; -import { getDatabase } from '../util/db'; -import { IPersonQuery } from '../util/types/general'; -import {LambdaBuilder, LambdaInput} from '../util/middleware/middleware'; +import { LambdaBuilder, LambdaInput } from '../util/middleware/middleware'; +import { + PaginationHelper, + ResponseMetaTagger, +} from '../util/middleware/paginationHelper'; import { APIResponse, SuccessResponse } from '../util/middleware/response'; -import { PaginationHelper, ResponseMetaTagger } from '../util/middleware/paginationHelper'; +import { + ACCESS_SCOPES, + ScopeController, +} from '../util/middleware/scopeHandler'; +import { IPersonQuery } from '../util/types/general'; const db = getDatabase(); +const validScopes = [ + ACCESS_SCOPES.ADMIN_READ, + ACCESS_SCOPES.READ_ALL_PROFILE_DATA, +]; -const LIMIT = 50; +const DEFAULT_LIMIT = 50; const OFFSET = 0; export const handler = new LambdaBuilder(getRequest) .use(new InputValidator()) - // .use(new Authorizer()) - .use(new PaginationHelper({ limit: LIMIT, offset: OFFSET})) + .use(new Authorizer(db)) + .use(new ScopeController(db)) + .use(new PaginationHelper({ limit: DEFAULT_LIMIT, offset: OFFSET })) .useAfter(new ResponseMetaTagger()) .build(); -export async function getRequest( - event: LambdaInput -): Promise { +export async function getRequest(event: LambdaInput): Promise { + ScopeController.verifyScopes(event.userScopes, validScopes); + const personQuery = ((event && event.queryStringParameters) || {}) as unknown as IPersonQuery; return new SuccessResponse(await getAll(personQuery)); @@ -28,13 +40,15 @@ export async function getRequest( export async function getAll(personQuery: IPersonQuery) { const res = await db .selectFrom('person') - .select( [ + .select([ 'person.id', + 'person.first_name', + 'person.last_name', + 'person.pref_name', 'person.email', - 'person.created_at', - 'person.updated_at', - 'person.member_since' - ]) + 'person.account_updated', + 'person.member_since', + ]) .limit(personQuery.limit || 10) .offset(personQuery.offset || 0) .execute(); @@ -42,9 +56,11 @@ export async function getAll(personQuery: IPersonQuery) { return res.map((user) => { return { id: user.id, + first_name: user.first_name, + last_name: user.last_name, + pref_name: user.pref_name, email: user.email || '', - created_at: user.created_at, - updated_at: user.updated_at, + account_updated: user.account_updated, member_since: user.member_since, }; }); diff --git a/src/util/db.ts b/src/util/db.ts index 4675765..de2da69 100644 --- a/src/util/db.ts +++ b/src/util/db.ts @@ -11,14 +11,17 @@ import { fetch } from 'undici' config(); export interface Database { + ethnicity: EthnicityTable; faculty: FacultyTable; - standing: StandingTable; - role: RoleTable; - specialization: SpecializationTable; + gender: GenderTable; person: PersonTable; person_role: PersonRoleTable; + pronouns: PronounsTable; + role: RoleTable; scope: ScopeTable; scope_role: ScopeRole; + specialization: SpecializationTable; + standing: StandingTable; } export function getDatabase() { @@ -42,77 +45,44 @@ export interface DictTable { label: T; } +export type EthnicityTable = DictTable; +export type Ethnicity = Selectable; +export type NewEthnicity = Insertable; +export type UpdateEthnicity = Updateable; + export type FacultyTable = DictTable; export type Faculty = Selectable; export type NewFaculty = Insertable; export type UpdateFaculty = Updateable; +export type GenderTable = DictTable; +export type Gender = Selectable; +export type NewGender = Insertable; +export type UpdateGender = Updateable; + +export type PronounsTable = DictTable; +export type Pronouns = Selectable; +export type NewPronouns = Insertable; +export type UpdatePronouns = Updateable; + export type StandingTable = DictTable; export type Standing = Selectable; export type NewStanding = Insertable; export type UpdateStanding = Updateable; -export type RoleTable = DictTable; -export type Role = Selectable; -export type NewRole = Insertable; -export type UpdateRole = Updateable; - export type SpecializationTable = DictTable; export type Specialization = Selectable; export type NewSpecialization = Insertable; export type UpdateSpecialization = Updateable; - - -export interface PersonTable { - id: Generated; - username: string; - email: string; - created_at: string; - updated_at: string; - member_since: Date | null; -} - -export interface ProfileTable { - id: string; - first_name: string; - pref_name: string; - last_name: string; -} - -export interface BackgroundTable { - id: string; - resume_link: string | null; - faculty_id: string; - standing_id: string; - specialization_id: string; -} - -export type Person = Selectable; -export type NewPerson = Insertable; -export type UpdatePerson = Updateable; - -export type Profile = Selectable; -export type NewProfile = Insertable; -export type UpdateProfile = Updateable; - -export type Background = Selectable; -export type NewBackground = Insertable; -export type UpdateBackground = Updateable; - - -export interface PersonRoleTable { - user_id: string; - role_id: string; -} - -export type PersonRole = Selectable; -export type NewPersonRole = Insertable; -export type UpdatePersonRole = Updateable; +export type RoleTable = DictTable; +export type Role = Selectable; +export type NewRole = Insertable; +export type UpdateRole = Updateable; export interface ScopeTable { - label: string; - description: string; + label: string; + description: string; } export type Scope = Selectable; @@ -127,3 +97,34 @@ export interface ScopeRoleTable { export type ScopeRole = Selectable; export type NewScopeRole = Insertable; export type UpdateScopeRole = Updateable; + +export interface PersonTable { + id: Generated; + email: string; + member_since: Date; + account_updated: Date; + first_name: string; + pref_name: string; + last_name: string; + pronouns_id: Generated; + gender_id: Generated; + ethnicity_id: Generated; + faculty_id: Generated; + standing_id: Generated; + specialization_id: Generated; + student_number: string; + phone_number: string; + linkedin_link: string; + github_link: string; + website_link: string; + resume_link: string; +} + +export interface PersonRoleTable { + person_id: number; + role_id: number; +} + +export type Person = Selectable; +export type NewPerson = Insertable; +export type UpdatePerson = Updateable; diff --git a/src/util/middleware/authorizer.ts b/src/util/middleware/authorizer.ts index 2b6317a..73d4696 100644 --- a/src/util/middleware/authorizer.ts +++ b/src/util/middleware/authorizer.ts @@ -1,10 +1,14 @@ import { APIGatewayProxyEvent } from 'aws-lambda'; -import { NotFoundError, UnauthorizedError } from './response'; import jwt_decode from 'jwt-decode'; +import { Kysely } from 'kysely'; import { GoogleAuthUser } from '../authorization'; +import { Database } from '../db'; +import { NotFoundError, UnauthorizedError } from './response'; +import { ScopeController } from './scopeHandler'; import { IHandlerEvent, IMiddleware } from './types'; export class Authorizer implements IMiddleware { + private connection: Kysely; public handler = async (event: APIGatewayProxyEvent) => { const auth = event.headers.Authorization; @@ -12,6 +16,7 @@ export class Authorizer implements IMiddleware { throw new UnauthorizedError('Authorization header is missing'); } const googleAuth = await this.verifyUserIsLoggedIn(auth); + return { googleAccount: googleAuth }; }; @@ -21,6 +26,62 @@ export class Authorizer implements IMiddleware { if (!googleAuthUser.email) { throw new NotFoundError('User not found'); } + + const user = await this.connection + .selectFrom('person') + .select(['email']) + .where('person.email', '=', googleAuthUser.email) + .executeTakeFirst(); + + if (!user) { + throw new NotFoundError('Authorized user not found'); + } + return googleAuthUser; }; + + public static verifyCurrentUser = async ( + connection: Kysely, + id: number, + googleAuthUser: GoogleAuthUser + ) => { + const user = await connection + .selectFrom('person') + .select(['email']) + .where('person.id', '=', id) + .executeTakeFirst(); + + if (!user) { + return false; + } + console.log(user); + console.log(googleAuthUser); + return googleAuthUser.email == user.email; + }; + + public static authorizeOrVerifyScopes = async ( + db: Kysely, + userId: number, + userScopes: string[], + personalScope: string, + validScopes: string[], + googleUser: GoogleAuthUser + ) => { + const canAccessOwnProfile = userScopes.includes(personalScope); + const isCurrentUser = await Authorizer.verifyCurrentUser( + db, + userId, + googleUser + ); + console.log(`currentuser: ${isCurrentUser}`); + if (canAccessOwnProfile && isCurrentUser) { + console.log('User is authorized'); + return; + } + ScopeController.verifyScopes(userScopes, validScopes); + }; + + constructor(connection: Kysely) { + this.connection = connection; + } } diff --git a/src/util/middleware/middleware.ts b/src/util/middleware/middleware.ts index 1d06979..fd2c00b 100644 --- a/src/util/middleware/middleware.ts +++ b/src/util/middleware/middleware.ts @@ -6,7 +6,7 @@ import { APIReturnResponse, UnsupportedEndpointError, } from './response'; -import { IMiddleware, isRouter, LambdaHandler, Router } from './types'; +import { IMiddleware, LambdaHandler, Router, isRouter } from './types'; export interface LambdaInput extends APIGatewayProxyEvent { [key: string]: any; @@ -23,25 +23,24 @@ export class LambdaBuilder { async router(event: LambdaInput): Promise { if (isRouter(this.entry)) { const router = this.entry as Router; - if (router[event.httpMethod.toLowerCase()] === undefined) { + const method = event.httpMethod.toLowerCase(); + + if (router[method] === undefined) { + throw new UnsupportedEndpointError( + `Unsupported endpoint ${event.httpMethod} ${event.path}` + ); + } + + if (typeof router[method] !== 'function') { throw new UnsupportedEndpointError( `Unsupported endpoint ${event.httpMethod} ${event.path}` ); } - switch (event.httpMethod) { - case 'GET': - return await router.get(event); - case 'POST': - return await router.post(event); - case 'PATCH': - return await router.patch(event); - case 'DELETE': - return await router.delete(event); - default: - throw new UnsupportedEndpointError( - `Unsupported endpoint ${event.httpMethod} ${event.path}` - ); + try { + return await router[method](event); + } catch (err) { + return new APIErrorResponse(err); } } else { const lambda = this.entry as LambdaHandler; @@ -67,9 +66,9 @@ export class LambdaBuilder { const output = await middleware.handler(input); input = { ...input, ...output }; } - const res = await this.router(input); - - input = { ...input, ...{data: res.data} }; + const res = await this.router(input as LambdaInput); + + input = { ...input, ...{ data: res.data } }; let finalOutput = res.data; for (const middleware of this.cleanupFunctions) { const output = await middleware.handler(input); diff --git a/src/util/middleware/scopeHandler.ts b/src/util/middleware/scopeHandler.ts index 2c381f0..a6ea10a 100644 --- a/src/util/middleware/scopeHandler.ts +++ b/src/util/middleware/scopeHandler.ts @@ -1,43 +1,60 @@ import { APIGatewayProxyEvent } from 'aws-lambda'; +import { Kysely } from 'kysely'; +import { Database } from '../db'; import { NotFoundError, UnauthorizedError } from './response'; import { IHandlerEvent, IMiddleware } from './types'; -import {Kysely} from "kysely"; -import {Database} from "../db"; export const ACCESS_SCOPES = { - PROFILE_READ_OTHERS: 'profile:read:others', - PROFILE_WRITE_OTHERS: 'profile:write:others', - ADMIN_READ: 'admin:read', - ADMIN_WRITE: 'admin:write', -} + ADMIN_READ: 'read:admin', + READ_ALL_PROFILE_DATA: 'read:profile:all', + READ_OWN_PROFILE: 'read:profile:personal', + READ_RESTRICTED_PROFILE_DATA: 'read:profile:restricted', + ADMIN_WRITE: 'write:admin', + WRITE_PROFILE: 'write:profile:all', + ADMIN_UPDATE: 'update:admin', + UPDATE_ALL_PROFILE: 'update:profile:all', + UPDATE_OWN_PROFILE: 'update:profile:personal', + ADMIN_DELETE: 'delete:admin', + DELETE_ALL_PROFILE: 'delete:profile:all', + DELETE_OWN_PROFILE: 'delete:profile:personal', +}; -export type AccessScope = typeof ACCESS_SCOPES[keyof typeof ACCESS_SCOPES]; +export type AccessScope = (typeof ACCESS_SCOPES)[keyof typeof ACCESS_SCOPES]; export class ScopeController implements IMiddleware { private connection: Kysely; - public static verifyScopes(scopes: AccessScope[], requiredScopes: AccessScope[]) { - const missingScopes = requiredScopes.filter(scope => !scopes.includes(scope)); - console.log(missingScopes); + public static verifyScopes( + userScopes: AccessScope[], + requiredScopes: AccessScope[] + ) { + const hasRequiredScope = requiredScopes.some((scope) => + userScopes.includes(scope) + ); - if (missingScopes.length > 0) { - throw new UnauthorizedError(`Missing scopes: ${missingScopes.join(', ')}`); + if (!hasRequiredScope) { + throw new UnauthorizedError(`Missing required permission.`); } } - public handler = async (event: APIGatewayProxyEvent) => { - const userEmail = (event as unknown as { googleAccount: { email: string } }) - .googleAccount.email; - const user = await this.connection.selectFrom('person').select(['email']).where('email','=', userEmail).executeTakeFirst(); + const userEmail = ( + event as unknown as { googleAccount: { email: string } } + ).googleAccount.email; + const user = await this.connection + .selectFrom('person') + .select(['email']) + .where('email', '=', userEmail) + .executeTakeFirst(); if (!user) { throw new NotFoundError('User not found'); } - const scopes = await this.connection.selectFrom('scope_role') - .innerJoin('role', 'scope_role.role_label', 'role.label') - .innerJoin('person_role', 'role.id', 'person_role.role_id') - .innerJoin('person', 'person_role.user_id', 'person.id') + const scopes = await this.connection + .selectFrom('scope_role') + .innerJoin('role', 'role.label', 'scope_role.role_label') + .innerJoin('person_role', 'person_role.role_id', 'role.id') + .innerJoin('person', 'person.id', 'person_role.person_id') .select('scope_role.scope_label') .where('person.email', '=', userEmail) .execute(); @@ -45,11 +62,11 @@ export class ScopeController implements IMiddleware { console.log(scopes); return { - userScopes: scopes.map(scope => scope.scope_label) - } + userScopes: scopes.map((scope) => scope.scope_label), + }; }; - constructor( connection: Kysely) { + constructor(connection: Kysely) { this.connection = connection; } }