diff --git a/package-lock.json b/package-lock.json index 5799b0e2..8cae7a19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4348,6 +4348,149 @@ "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, + "node_modules/@graphql-codegen/typescript-graphql-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-graphql-request/-/typescript-graphql-request-6.1.0.tgz", + "integrity": "sha512-hMhBazvdSQWuOrH6GnYew8zb9wDq9le0o3tPu07if/v97LVVKsVfPNcMG/cTLYZufaog9o2jScLbyo17HEqicg==", + "dev": true, + "dependencies": { + "@graphql-codegen/plugin-helpers": "^3.0.0", + "@graphql-codegen/visitor-plugin-common": "2.13.1", + "auto-bind": "~4.0.0", + "tslib": "~2.6.0" + }, + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", + "graphql-request": "^6.0.0", + "graphql-tag": "^2.0.0" + } + }, + "node_modules/@graphql-codegen/typescript-graphql-request/node_modules/@graphql-codegen/plugin-helpers": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-3.1.2.tgz", + "integrity": "sha512-emOQiHyIliVOIjKVKdsI5MXj312zmRDwmHpyUTZMjfpvxq/UVAHUJIVdVf+lnjjrI+LXBTgMlTWTgHQfmICxjg==", + "dev": true, + "dependencies": { + "@graphql-tools/utils": "^9.0.0", + "change-case-all": "1.0.15", + "common-tags": "1.8.2", + "import-from": "4.0.0", + "lodash": "~4.17.0", + "tslib": "~2.4.0" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/@graphql-codegen/typescript-graphql-request/node_modules/@graphql-codegen/plugin-helpers/node_modules/tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "dev": true + }, + "node_modules/@graphql-codegen/typescript-graphql-request/node_modules/@graphql-codegen/visitor-plugin-common": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-2.13.1.tgz", + "integrity": "sha512-mD9ufZhDGhyrSaWQGrU1Q1c5f01TeWtSWy/cDwXYjJcHIj1Y/DG2x0tOflEfCvh5WcnmHNIw4lzDsg1W7iFJEg==", + "dev": true, + "dependencies": { + "@graphql-codegen/plugin-helpers": "^2.7.2", + "@graphql-tools/optimize": "^1.3.0", + "@graphql-tools/relay-operation-optimizer": "^6.5.0", + "@graphql-tools/utils": "^8.8.0", + "auto-bind": "~4.0.0", + "change-case-all": "1.0.14", + "dependency-graph": "^0.11.0", + "graphql-tag": "^2.11.0", + "parse-filepath": "^1.0.2", + "tslib": "~2.4.0" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/@graphql-codegen/typescript-graphql-request/node_modules/@graphql-codegen/visitor-plugin-common/node_modules/@graphql-codegen/plugin-helpers": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-2.7.2.tgz", + "integrity": "sha512-kln2AZ12uii6U59OQXdjLk5nOlh1pHis1R98cDZGFnfaiAbX9V3fxcZ1MMJkB7qFUymTALzyjZoXXdyVmPMfRg==", + "dev": true, + "dependencies": { + "@graphql-tools/utils": "^8.8.0", + "change-case-all": "1.0.14", + "common-tags": "1.8.2", + "import-from": "4.0.0", + "lodash": "~4.17.0", + "tslib": "~2.4.0" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/@graphql-codegen/typescript-graphql-request/node_modules/@graphql-codegen/visitor-plugin-common/node_modules/@graphql-tools/utils": { + "version": "8.13.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.13.1.tgz", + "integrity": "sha512-qIh9yYpdUFmctVqovwMdheVNJqFh+DQNWIhX87FJStfXYnmweBUDATok9fWPleKeFwxnW8IapKmY8m8toJEkAw==", + "dev": true, + "dependencies": { + "tslib": "^2.4.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-codegen/typescript-graphql-request/node_modules/@graphql-codegen/visitor-plugin-common/node_modules/change-case-all": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/change-case-all/-/change-case-all-1.0.14.tgz", + "integrity": "sha512-CWVm2uT7dmSHdO/z1CXT/n47mWonyypzBbuCy5tN7uMg22BsfkhwT6oHmFCAk+gL1LOOxhdbB9SZz3J1KTY3gA==", + "dev": true, + "dependencies": { + "change-case": "^4.1.2", + "is-lower-case": "^2.0.2", + "is-upper-case": "^2.0.2", + "lower-case": "^2.0.2", + "lower-case-first": "^2.0.2", + "sponge-case": "^1.0.1", + "swap-case": "^2.0.2", + "title-case": "^3.0.3", + "upper-case": "^2.0.2", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/@graphql-codegen/typescript-graphql-request/node_modules/@graphql-codegen/visitor-plugin-common/node_modules/tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "dev": true + }, + "node_modules/@graphql-codegen/typescript-graphql-request/node_modules/@graphql-tools/optimize": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/optimize/-/optimize-1.4.0.tgz", + "integrity": "sha512-dJs/2XvZp+wgHH8T5J2TqptT9/6uVzIYvA6uFACha+ufvdMBedkfR4b4GbT8jAKLRARiqRTxy3dctnwkTM2tdw==", + "dev": true, + "dependencies": { + "tslib": "^2.4.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-codegen/typescript-graphql-request/node_modules/@graphql-tools/relay-operation-optimizer": { + "version": "6.5.18", + "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-6.5.18.tgz", + "integrity": "sha512-mc5VPyTeV+LwiM+DNvoDQfPqwQYhPV/cl5jOBjTgSniyaq8/86aODfMkrE2OduhQ5E00hqrkuL2Fdrgk0w1QJg==", + "dev": true, + "dependencies": { + "@ardatan/relay-compiler": "12.0.0", + "@graphql-tools/utils": "^9.2.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/@graphql-codegen/typescript-operations": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-4.0.1.tgz", @@ -14398,7 +14541,6 @@ "version": "3.1.8", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", - "dev": true, "dependencies": { "node-fetch": "^2.6.12" } @@ -16827,7 +16969,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-6.1.0.tgz", "integrity": "sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==", - "dev": true, "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", "cross-fetch": "^3.1.5" @@ -26379,6 +26520,7 @@ "casbin": "^5.28.0", "casbin-mongoose-adapter": "^5.3.1", "csv-parser": "^3.0.0", + "graphql-request": "^6.1.0", "graphql-type-json": "^0.3.2", "jsonschema": "^1.4.1", "mongoose": "^7.4.3", @@ -26388,6 +26530,7 @@ "rxjs": "^7.2.0" }, "devDependencies": { + "@graphql-codegen/typescript-graphql-request": "^6.1.0", "@nestjs/cli": "^9.0.0", "@nestjs/schematics": "^9.0.0", "@nestjs/testing": "^9.0.0", @@ -29486,6 +29629,129 @@ } } }, + "@graphql-codegen/typescript-graphql-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-graphql-request/-/typescript-graphql-request-6.1.0.tgz", + "integrity": "sha512-hMhBazvdSQWuOrH6GnYew8zb9wDq9le0o3tPu07if/v97LVVKsVfPNcMG/cTLYZufaog9o2jScLbyo17HEqicg==", + "dev": true, + "requires": { + "@graphql-codegen/plugin-helpers": "^3.0.0", + "@graphql-codegen/visitor-plugin-common": "2.13.1", + "auto-bind": "~4.0.0", + "tslib": "~2.6.0" + }, + "dependencies": { + "@graphql-codegen/plugin-helpers": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-3.1.2.tgz", + "integrity": "sha512-emOQiHyIliVOIjKVKdsI5MXj312zmRDwmHpyUTZMjfpvxq/UVAHUJIVdVf+lnjjrI+LXBTgMlTWTgHQfmICxjg==", + "dev": true, + "requires": { + "@graphql-tools/utils": "^9.0.0", + "change-case-all": "1.0.15", + "common-tags": "1.8.2", + "import-from": "4.0.0", + "lodash": "~4.17.0", + "tslib": "~2.4.0" + }, + "dependencies": { + "tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "dev": true + } + } + }, + "@graphql-codegen/visitor-plugin-common": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-2.13.1.tgz", + "integrity": "sha512-mD9ufZhDGhyrSaWQGrU1Q1c5f01TeWtSWy/cDwXYjJcHIj1Y/DG2x0tOflEfCvh5WcnmHNIw4lzDsg1W7iFJEg==", + "dev": true, + "requires": { + "@graphql-codegen/plugin-helpers": "^2.7.2", + "@graphql-tools/optimize": "^1.3.0", + "@graphql-tools/relay-operation-optimizer": "^6.5.0", + "@graphql-tools/utils": "^8.8.0", + "auto-bind": "~4.0.0", + "change-case-all": "1.0.14", + "dependency-graph": "^0.11.0", + "graphql-tag": "^2.11.0", + "parse-filepath": "^1.0.2", + "tslib": "~2.4.0" + }, + "dependencies": { + "@graphql-codegen/plugin-helpers": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-2.7.2.tgz", + "integrity": "sha512-kln2AZ12uii6U59OQXdjLk5nOlh1pHis1R98cDZGFnfaiAbX9V3fxcZ1MMJkB7qFUymTALzyjZoXXdyVmPMfRg==", + "dev": true, + "requires": { + "@graphql-tools/utils": "^8.8.0", + "change-case-all": "1.0.14", + "common-tags": "1.8.2", + "import-from": "4.0.0", + "lodash": "~4.17.0", + "tslib": "~2.4.0" + } + }, + "@graphql-tools/utils": { + "version": "8.13.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.13.1.tgz", + "integrity": "sha512-qIh9yYpdUFmctVqovwMdheVNJqFh+DQNWIhX87FJStfXYnmweBUDATok9fWPleKeFwxnW8IapKmY8m8toJEkAw==", + "dev": true, + "requires": { + "tslib": "^2.4.0" + } + }, + "change-case-all": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/change-case-all/-/change-case-all-1.0.14.tgz", + "integrity": "sha512-CWVm2uT7dmSHdO/z1CXT/n47mWonyypzBbuCy5tN7uMg22BsfkhwT6oHmFCAk+gL1LOOxhdbB9SZz3J1KTY3gA==", + "dev": true, + "requires": { + "change-case": "^4.1.2", + "is-lower-case": "^2.0.2", + "is-upper-case": "^2.0.2", + "lower-case": "^2.0.2", + "lower-case-first": "^2.0.2", + "sponge-case": "^1.0.1", + "swap-case": "^2.0.2", + "title-case": "^3.0.3", + "upper-case": "^2.0.2", + "upper-case-first": "^2.0.2" + } + }, + "tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "dev": true + } + } + }, + "@graphql-tools/optimize": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/optimize/-/optimize-1.4.0.tgz", + "integrity": "sha512-dJs/2XvZp+wgHH8T5J2TqptT9/6uVzIYvA6uFACha+ufvdMBedkfR4b4GbT8jAKLRARiqRTxy3dctnwkTM2tdw==", + "dev": true, + "requires": { + "tslib": "^2.4.0" + } + }, + "@graphql-tools/relay-operation-optimizer": { + "version": "6.5.18", + "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-6.5.18.tgz", + "integrity": "sha512-mc5VPyTeV+LwiM+DNvoDQfPqwQYhPV/cl5jOBjTgSniyaq8/86aODfMkrE2OduhQ5E00hqrkuL2Fdrgk0w1QJg==", + "dev": true, + "requires": { + "@ardatan/relay-compiler": "12.0.0", + "@graphql-tools/utils": "^9.2.1", + "tslib": "^2.4.0" + } + } + } + }, "@graphql-codegen/typescript-operations": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-4.0.1.tgz", @@ -36715,7 +36981,6 @@ "version": "3.1.8", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", - "dev": true, "requires": { "node-fetch": "^2.6.12" } @@ -38576,7 +38841,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-6.1.0.tgz", "integrity": "sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==", - "dev": true, "requires": { "@graphql-typed-document-node/core": "^3.2.0", "cross-fetch": "^3.1.5" @@ -43152,6 +43416,7 @@ "requires": { "@apollo/subgraph": "^2.4.12", "@google-cloud/storage": "^7.7.0", + "@graphql-codegen/typescript-graphql-request": "^6.1.0", "@nestjs/apollo": "^12.0.7", "@nestjs/axios": "^3.0.1", "@nestjs/cli": "^9.0.0", @@ -43178,6 +43443,7 @@ "eslint": "^8.0.1", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", + "graphql-request": "*", "graphql-type-json": "^0.3.2", "jest": "28.1.3", "jsonschema": "^1.4.1", diff --git a/packages/server/package.json b/packages/server/package.json index e43070e2..011457c0 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -19,7 +19,8 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" + "test:e2e": "jest --config ./test/jest-e2e.json", + "introspection": "graphql-codegen --config src/auth/graphql-codegen.yml" }, "dependencies": { "@apollo/subgraph": "^2.4.12", @@ -38,6 +39,7 @@ "casbin": "^5.28.0", "casbin-mongoose-adapter": "^5.3.1", "csv-parser": "^3.0.0", + "graphql-request": "^6.1.0", "graphql-type-json": "^0.3.2", "jsonschema": "^1.4.1", "mongoose": "^7.4.3", @@ -47,6 +49,7 @@ "rxjs": "^7.2.0" }, "devDependencies": { + "@graphql-codegen/typescript-graphql-request": "^6.1.0", "@nestjs/cli": "^9.0.0", "@nestjs/schematics": "^9.0.0", "@nestjs/testing": "^9.0.0", diff --git a/packages/server/schema.gql b/packages/server/schema.gql index 0f06f13e..1bbaeea9 100644 --- a/packages/server/schema.gql +++ b/packages/server/schema.gql @@ -28,6 +28,24 @@ A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date """ scalar DateTime +type UserModel { + id: ID! +} + +type Permission { + user: UserModel! + role: Roles! + hasRole: Boolean! + editable: Boolean! +} + +enum Roles { + OWNER + PROJECT_ADMIN + STUDY_ADMIN + CONTRIBUTOR +} + type TagSchema { dataSchema: JSON! uiSchema: JSON! @@ -108,6 +126,7 @@ type Query { getOrganizations: [Organization!]! exists(name: String!): Boolean! getDatasets: [Dataset!]! + getProjectPermissions(project: ID!): [Permission!]! projectExists(name: String!): Boolean! getProjects: [Project!]! studyExists(name: String!, project: ID!): Boolean! diff --git a/packages/server/src/app.module.ts b/packages/server/src/app.module.ts index 23bf25eb..d35a1669 100644 --- a/packages/server/src/app.module.ts +++ b/packages/server/src/app.module.ts @@ -13,6 +13,7 @@ import { TagModule } from './tag/tag.module'; import { SharedModule } from './shared/shared.module'; import { JwtModule } from './jwt/jwt.module'; import { PermissionModule } from './permission/permission.module'; +import { AuthModule } from './auth/auth.module'; @Module({ imports: [ @@ -42,7 +43,8 @@ import { PermissionModule } from './permission/permission.module'; TagModule, SharedModule, JwtModule, - PermissionModule + PermissionModule, + AuthModule ] }) export class AppModule {} diff --git a/packages/server/src/auth/auth.module.ts b/packages/server/src/auth/auth.module.ts new file mode 100644 index 00000000..1a85795b --- /dev/null +++ b/packages/server/src/auth/auth.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { authSDKProvider } from './providers/sdk.provider'; +import { UserService } from './services/user.service'; + +@Module({ + providers: [authSDKProvider, UserService], + exports: [UserService] +}) +export class AuthModule {} diff --git a/packages/server/src/auth/graphql-codegen.yml b/packages/server/src/auth/graphql-codegen.yml new file mode 100644 index 00000000..68e54850 --- /dev/null +++ b/packages/server/src/auth/graphql-codegen.yml @@ -0,0 +1,24 @@ +overwrite: true +schema: https://test-auth-service.sail.codes/graphql +documents: src/auth/graphql/**/*.graphql +generates: + src/auth/graphql/graphql.ts: + plugins: + - add: + content: '/* Generated File DO NOT EDIT. */' + - add: + content: '/* tslint:disable */' + - typescript + src/auth/graphql/sdk.ts: + documents: src/auth/graphql/**/*.graphql + presetConfig: + baseTypesPath: graphql.ts + extension: .ts + plugins: + - add: + content: '/* Generated File DO NOT EDIT. */' + - add: + content: '/* tslint:disable */' + - typescript + - typescript-operations + - typescript-graphql-request diff --git a/packages/server/src/auth/graphql/graphql.ts b/packages/server/src/auth/graphql/graphql.ts new file mode 100644 index 00000000..e184b165 --- /dev/null +++ b/packages/server/src/auth/graphql/graphql.ts @@ -0,0 +1,325 @@ +/* Generated File DO NOT EDIT. */ +/* tslint:disable */ +export type Maybe = T | null; +export type InputMaybe = Maybe; +export type Exact = { [K in keyof T]: T[K] }; +export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; +export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: { input: string; output: string }; + String: { input: string; output: string }; + Boolean: { input: boolean; output: boolean }; + Int: { input: number; output: number }; + Float: { input: number; output: number }; + DateTime: { input: any; output: any }; + JSON: { input: any; output: any }; + _Any: { input: any; output: any }; + federation__FieldSet: { input: any; output: any }; + link__Import: { input: any; output: any }; +}; + +/** Input type for accepting an invite */ +export type AcceptInviteModel = { + /** The email address of the user accepting the invite */ + email: Scalars['String']['input']; + /** The full name of the user accepting the invite */ + fullname: Scalars['String']['input']; + /** The invite code that was included in the invite email */ + inviteCode: Scalars['String']['input']; + /** The password for the new user account */ + password: Scalars['String']['input']; + /** The ID of the project the invite is associated with */ + projectId: Scalars['String']['input']; +}; + +export type AccessToken = { + __typename?: 'AccessToken'; + accessToken: Scalars['String']['output']; + refreshToken: Scalars['String']['output']; +}; + +export type ConfigurableProjectSettings = { + description?: InputMaybe; + homePage?: InputMaybe; + logo?: InputMaybe; + muiTheme?: InputMaybe; + name?: InputMaybe; + redirectUrl?: InputMaybe; +}; + +export type EmailLoginDto = { + email: Scalars['String']['input']; + password: Scalars['String']['input']; + projectId: Scalars['String']['input']; +}; + +export type ForgotDto = { + email: Scalars['String']['input']; + projectId: Scalars['String']['input']; +}; + +export type GoogleLoginDto = { + credential: Scalars['String']['input']; + projectId: Scalars['String']['input']; +}; + +export type InviteModel = { + __typename?: 'InviteModel'; + /** The date and time at which the invitation was created. */ + createdAt: Scalars['DateTime']['output']; + /** The date and time at which the invitation was deleted, if applicable. */ + deletedAt?: Maybe; + /** The email address of the user being invited. */ + email: Scalars['String']['output']; + /** The date and time at which the invitation expires. */ + expiresAt: Scalars['DateTime']['output']; + /** The ID of the invitation. */ + id: Scalars['ID']['output']; + /** The ID of the project to which the invitation belongs. */ + projectId: Scalars['String']['output']; + /** The role that the user being invited will have. */ + role: Scalars['Int']['output']; + /** The status of the invitation. */ + status: InviteStatus; + /** The date and time at which the invitation was last updated. */ + updatedAt: Scalars['DateTime']['output']; +}; + +/** The status of an invite */ +export enum InviteStatus { + Accepted = 'ACCEPTED', + Cancelled = 'CANCELLED', + Expired = 'EXPIRED', + Pending = 'PENDING' +} + +export type Mutation = { + __typename?: 'Mutation'; + acceptInvite: InviteModel; + cancelInvite: InviteModel; + createInvite: InviteModel; + createProject: ProjectModel; + forgotPassword: Scalars['Boolean']['output']; + loginEmail: AccessToken; + loginGoogle: AccessToken; + loginUsername: AccessToken; + refresh: AccessToken; + resendInvite: InviteModel; + resetPassword: Scalars['Boolean']['output']; + signup: AccessToken; + updateProject: ProjectModel; + updateProjectAuthMethods: ProjectModel; + updateProjectSettings: ProjectModel; + updateUser: UserModel; +}; + +export type MutationAcceptInviteArgs = { + input: AcceptInviteModel; +}; + +export type MutationCancelInviteArgs = { + id: Scalars['ID']['input']; +}; + +export type MutationCreateInviteArgs = { + email: Scalars['String']['input']; + role?: InputMaybe; +}; + +export type MutationCreateProjectArgs = { + project: ProjectCreateInput; +}; + +export type MutationForgotPasswordArgs = { + user: ForgotDto; +}; + +export type MutationLoginEmailArgs = { + user: EmailLoginDto; +}; + +export type MutationLoginGoogleArgs = { + user: GoogleLoginDto; +}; + +export type MutationLoginUsernameArgs = { + user: UsernameLoginDto; +}; + +export type MutationRefreshArgs = { + refreshToken: Scalars['String']['input']; +}; + +export type MutationResendInviteArgs = { + id: Scalars['ID']['input']; +}; + +export type MutationResetPasswordArgs = { + user: ResetDto; +}; + +export type MutationSignupArgs = { + user: UserSignupDto; +}; + +export type MutationUpdateProjectArgs = { + id: Scalars['String']['input']; + settings: ConfigurableProjectSettings; +}; + +export type MutationUpdateProjectAuthMethodsArgs = { + id: Scalars['String']['input']; + projectAuthMethods: ProjectAuthMethodsInput; +}; + +export type MutationUpdateProjectSettingsArgs = { + id: Scalars['String']['input']; + projectSettings: ProjectSettingsInput; +}; + +export type MutationUpdateUserArgs = { + email: Scalars['String']['input']; + fullname: Scalars['String']['input']; +}; + +export type ProjectAuthMethodsInput = { + emailAuth?: InputMaybe; + googleAuth?: InputMaybe; +}; + +export type ProjectAuthMethodsModel = { + __typename?: 'ProjectAuthMethodsModel'; + emailAuth: Scalars['Boolean']['output']; + googleAuth: Scalars['Boolean']['output']; +}; + +export type ProjectCreateInput = { + allowSignup?: InputMaybe; + description?: InputMaybe; + displayProjectName?: InputMaybe; + emailAuth?: InputMaybe; + googleAuth?: InputMaybe; + homePage?: InputMaybe; + logo?: InputMaybe; + muiTheme?: InputMaybe; + name: Scalars['String']['input']; + redirectUrl?: InputMaybe; +}; + +export type ProjectModel = { + __typename?: 'ProjectModel'; + authMethods: ProjectAuthMethodsModel; + createdAt: Scalars['DateTime']['output']; + deletedAt?: Maybe; + description?: Maybe; + homePage?: Maybe; + id: Scalars['ID']['output']; + logo?: Maybe; + muiTheme: Scalars['JSON']['output']; + name: Scalars['String']['output']; + redirectUrl?: Maybe; + settings: ProjectSettingsModel; + updatedAt: Scalars['DateTime']['output']; + users: Array; +}; + +export type ProjectSettingsInput = { + allowSignup?: InputMaybe; + displayProjectName?: InputMaybe; +}; + +export type ProjectSettingsModel = { + __typename?: 'ProjectSettingsModel'; + allowSignup: Scalars['Boolean']['output']; + displayProjectName: Scalars['Boolean']['output']; +}; + +export type Query = { + __typename?: 'Query'; + _entities: Array>; + _service: _Service; + getProject: ProjectModel; + getUser: UserModel; + invite: InviteModel; + invites: Array; + listProjects: Array; + me: UserModel; + projectUsers: Array; + publicKey: Array; + users: Array; +}; + +export type Query_EntitiesArgs = { + representations: Array; +}; + +export type QueryGetProjectArgs = { + id: Scalars['String']['input']; +}; + +export type QueryGetUserArgs = { + id: Scalars['ID']['input']; +}; + +export type QueryInviteArgs = { + id: Scalars['ID']['input']; +}; + +export type QueryInvitesArgs = { + status?: InputMaybe; +}; + +export type QueryProjectUsersArgs = { + projectId: Scalars['String']['input']; +}; + +export type ResetDto = { + code: Scalars['String']['input']; + email: Scalars['String']['input']; + password: Scalars['String']['input']; + projectId: Scalars['String']['input']; +}; + +export type UserModel = { + __typename?: 'UserModel'; + createdAt: Scalars['DateTime']['output']; + deletedAt?: Maybe; + email?: Maybe; + fullname?: Maybe; + id: Scalars['ID']['output']; + projectId: Scalars['String']['output']; + role: Scalars['Int']['output']; + updatedAt: Scalars['DateTime']['output']; + username?: Maybe; +}; + +export type UserSignupDto = { + email: Scalars['String']['input']; + fullname: Scalars['String']['input']; + password: Scalars['String']['input']; + projectId: Scalars['String']['input']; + username?: InputMaybe; +}; + +export type UsernameLoginDto = { + password: Scalars['String']['input']; + projectId: Scalars['String']['input']; + username: Scalars['String']['input']; +}; + +export type _Entity = InviteModel | ProjectModel | UserModel; + +export type _Service = { + __typename?: '_Service'; + sdl?: Maybe; +}; + +export enum Link__Purpose { + /** `EXECUTION` features provide metadata necessary for operation execution. */ + Execution = 'EXECUTION', + /** `SECURITY` features provide metadata necessary to securely resolve fields. */ + Security = 'SECURITY' +} diff --git a/packages/server/src/auth/graphql/sdk.ts b/packages/server/src/auth/graphql/sdk.ts new file mode 100644 index 00000000..2d9e8246 --- /dev/null +++ b/packages/server/src/auth/graphql/sdk.ts @@ -0,0 +1,394 @@ +/* Generated File DO NOT EDIT. */ +/* tslint:disable */ +import { GraphQLClient } from 'graphql-request'; +import { GraphQLClientRequestHeaders } from 'graphql-request/build/cjs/types'; +import gql from 'graphql-tag'; +export type Maybe = T | null; +export type InputMaybe = Maybe; +export type Exact = { [K in keyof T]: T[K] }; +export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; +export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: { input: string; output: string }; + String: { input: string; output: string }; + Boolean: { input: boolean; output: boolean }; + Int: { input: number; output: number }; + Float: { input: number; output: number }; + DateTime: { input: any; output: any }; + JSON: { input: any; output: any }; + _Any: { input: any; output: any }; + federation__FieldSet: { input: any; output: any }; + link__Import: { input: any; output: any }; +}; + +/** Input type for accepting an invite */ +export type AcceptInviteModel = { + /** The email address of the user accepting the invite */ + email: Scalars['String']['input']; + /** The full name of the user accepting the invite */ + fullname: Scalars['String']['input']; + /** The invite code that was included in the invite email */ + inviteCode: Scalars['String']['input']; + /** The password for the new user account */ + password: Scalars['String']['input']; + /** The ID of the project the invite is associated with */ + projectId: Scalars['String']['input']; +}; + +export type AccessToken = { + __typename?: 'AccessToken'; + accessToken: Scalars['String']['output']; + refreshToken: Scalars['String']['output']; +}; + +export type ConfigurableProjectSettings = { + description?: InputMaybe; + homePage?: InputMaybe; + logo?: InputMaybe; + muiTheme?: InputMaybe; + name?: InputMaybe; + redirectUrl?: InputMaybe; +}; + +export type EmailLoginDto = { + email: Scalars['String']['input']; + password: Scalars['String']['input']; + projectId: Scalars['String']['input']; +}; + +export type ForgotDto = { + email: Scalars['String']['input']; + projectId: Scalars['String']['input']; +}; + +export type GoogleLoginDto = { + credential: Scalars['String']['input']; + projectId: Scalars['String']['input']; +}; + +export type InviteModel = { + __typename?: 'InviteModel'; + /** The date and time at which the invitation was created. */ + createdAt: Scalars['DateTime']['output']; + /** The date and time at which the invitation was deleted, if applicable. */ + deletedAt?: Maybe; + /** The email address of the user being invited. */ + email: Scalars['String']['output']; + /** The date and time at which the invitation expires. */ + expiresAt: Scalars['DateTime']['output']; + /** The ID of the invitation. */ + id: Scalars['ID']['output']; + /** The ID of the project to which the invitation belongs. */ + projectId: Scalars['String']['output']; + /** The role that the user being invited will have. */ + role: Scalars['Int']['output']; + /** The status of the invitation. */ + status: InviteStatus; + /** The date and time at which the invitation was last updated. */ + updatedAt: Scalars['DateTime']['output']; +}; + +/** The status of an invite */ +export enum InviteStatus { + Accepted = 'ACCEPTED', + Cancelled = 'CANCELLED', + Expired = 'EXPIRED', + Pending = 'PENDING' +} + +export type Mutation = { + __typename?: 'Mutation'; + acceptInvite: InviteModel; + cancelInvite: InviteModel; + createInvite: InviteModel; + createProject: ProjectModel; + forgotPassword: Scalars['Boolean']['output']; + loginEmail: AccessToken; + loginGoogle: AccessToken; + loginUsername: AccessToken; + refresh: AccessToken; + resendInvite: InviteModel; + resetPassword: Scalars['Boolean']['output']; + signup: AccessToken; + updateProject: ProjectModel; + updateProjectAuthMethods: ProjectModel; + updateProjectSettings: ProjectModel; + updateUser: UserModel; +}; + +export type MutationAcceptInviteArgs = { + input: AcceptInviteModel; +}; + +export type MutationCancelInviteArgs = { + id: Scalars['ID']['input']; +}; + +export type MutationCreateInviteArgs = { + email: Scalars['String']['input']; + role?: InputMaybe; +}; + +export type MutationCreateProjectArgs = { + project: ProjectCreateInput; +}; + +export type MutationForgotPasswordArgs = { + user: ForgotDto; +}; + +export type MutationLoginEmailArgs = { + user: EmailLoginDto; +}; + +export type MutationLoginGoogleArgs = { + user: GoogleLoginDto; +}; + +export type MutationLoginUsernameArgs = { + user: UsernameLoginDto; +}; + +export type MutationRefreshArgs = { + refreshToken: Scalars['String']['input']; +}; + +export type MutationResendInviteArgs = { + id: Scalars['ID']['input']; +}; + +export type MutationResetPasswordArgs = { + user: ResetDto; +}; + +export type MutationSignupArgs = { + user: UserSignupDto; +}; + +export type MutationUpdateProjectArgs = { + id: Scalars['String']['input']; + settings: ConfigurableProjectSettings; +}; + +export type MutationUpdateProjectAuthMethodsArgs = { + id: Scalars['String']['input']; + projectAuthMethods: ProjectAuthMethodsInput; +}; + +export type MutationUpdateProjectSettingsArgs = { + id: Scalars['String']['input']; + projectSettings: ProjectSettingsInput; +}; + +export type MutationUpdateUserArgs = { + email: Scalars['String']['input']; + fullname: Scalars['String']['input']; +}; + +export type ProjectAuthMethodsInput = { + emailAuth?: InputMaybe; + googleAuth?: InputMaybe; +}; + +export type ProjectAuthMethodsModel = { + __typename?: 'ProjectAuthMethodsModel'; + emailAuth: Scalars['Boolean']['output']; + googleAuth: Scalars['Boolean']['output']; +}; + +export type ProjectCreateInput = { + allowSignup?: InputMaybe; + description?: InputMaybe; + displayProjectName?: InputMaybe; + emailAuth?: InputMaybe; + googleAuth?: InputMaybe; + homePage?: InputMaybe; + logo?: InputMaybe; + muiTheme?: InputMaybe; + name: Scalars['String']['input']; + redirectUrl?: InputMaybe; +}; + +export type ProjectModel = { + __typename?: 'ProjectModel'; + authMethods: ProjectAuthMethodsModel; + createdAt: Scalars['DateTime']['output']; + deletedAt?: Maybe; + description?: Maybe; + homePage?: Maybe; + id: Scalars['ID']['output']; + logo?: Maybe; + muiTheme: Scalars['JSON']['output']; + name: Scalars['String']['output']; + redirectUrl?: Maybe; + settings: ProjectSettingsModel; + updatedAt: Scalars['DateTime']['output']; + users: Array; +}; + +export type ProjectSettingsInput = { + allowSignup?: InputMaybe; + displayProjectName?: InputMaybe; +}; + +export type ProjectSettingsModel = { + __typename?: 'ProjectSettingsModel'; + allowSignup: Scalars['Boolean']['output']; + displayProjectName: Scalars['Boolean']['output']; +}; + +export type Query = { + __typename?: 'Query'; + _entities: Array>; + _service: _Service; + getProject: ProjectModel; + getUser: UserModel; + invite: InviteModel; + invites: Array; + listProjects: Array; + me: UserModel; + projectUsers: Array; + publicKey: Array; + users: Array; +}; + +export type Query_EntitiesArgs = { + representations: Array; +}; + +export type QueryGetProjectArgs = { + id: Scalars['String']['input']; +}; + +export type QueryGetUserArgs = { + id: Scalars['ID']['input']; +}; + +export type QueryInviteArgs = { + id: Scalars['ID']['input']; +}; + +export type QueryInvitesArgs = { + status?: InputMaybe; +}; + +export type QueryProjectUsersArgs = { + projectId: Scalars['String']['input']; +}; + +export type ResetDto = { + code: Scalars['String']['input']; + email: Scalars['String']['input']; + password: Scalars['String']['input']; + projectId: Scalars['String']['input']; +}; + +export type UserModel = { + __typename?: 'UserModel'; + createdAt: Scalars['DateTime']['output']; + deletedAt?: Maybe; + email?: Maybe; + fullname?: Maybe; + id: Scalars['ID']['output']; + projectId: Scalars['String']['output']; + role: Scalars['Int']['output']; + updatedAt: Scalars['DateTime']['output']; + username?: Maybe; +}; + +export type UserSignupDto = { + email: Scalars['String']['input']; + fullname: Scalars['String']['input']; + password: Scalars['String']['input']; + projectId: Scalars['String']['input']; + username?: InputMaybe; +}; + +export type UsernameLoginDto = { + password: Scalars['String']['input']; + projectId: Scalars['String']['input']; + username: Scalars['String']['input']; +}; + +export type _Entity = InviteModel | ProjectModel | UserModel; + +export type _Service = { + __typename?: '_Service'; + sdl?: Maybe; +}; + +export enum Link__Purpose { + /** `EXECUTION` features provide metadata necessary for operation execution. */ + Execution = 'EXECUTION', + /** `SECURITY` features provide metadata necessary to securely resolve fields. */ + Security = 'SECURITY' +} + +export type ProjectUsersQueryVariables = Exact<{ + projectId: Scalars['String']['input']; +}>; + +export type ProjectUsersQuery = { + __typename?: 'Query'; + projectUsers: Array<{ + __typename?: 'UserModel'; + id: string; + projectId: string; + username?: string | null; + fullname?: string | null; + email?: string | null; + role: number; + createdAt: any; + updatedAt: any; + deletedAt?: any | null; + }>; +}; + +export const ProjectUsersDocument = gql` + query projectUsers($projectId: String!) { + projectUsers(projectId: $projectId) { + id + projectId + username + fullname + email + role + createdAt + updatedAt + deletedAt + } + } +`; + +export type SdkFunctionWrapper = ( + action: (requestHeaders?: Record) => Promise, + operationName: string, + operationType?: string, + variables?: any +) => Promise; + +const defaultWrapper: SdkFunctionWrapper = (action, _operationName, _operationType, variables) => action(); + +export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = defaultWrapper) { + return { + projectUsers( + variables: ProjectUsersQueryVariables, + requestHeaders?: GraphQLClientRequestHeaders + ): Promise { + return withWrapper( + (wrappedRequestHeaders) => + client.request(ProjectUsersDocument, variables, { + ...requestHeaders, + ...wrappedRequestHeaders + }), + 'projectUsers', + 'query', + variables + ); + } + }; +} +export type Sdk = ReturnType; diff --git a/packages/server/src/auth/graphql/users/users.graphql b/packages/server/src/auth/graphql/users/users.graphql new file mode 100644 index 00000000..5aa9dc9d --- /dev/null +++ b/packages/server/src/auth/graphql/users/users.graphql @@ -0,0 +1,13 @@ +query projectUsers($projectId: String!) { + projectUsers(projectId: $projectId) { + id + projectId + username + fullname + email + role + createdAt + updatedAt + deletedAt + } +} diff --git a/packages/server/src/auth/graphql/users/users.ts b/packages/server/src/auth/graphql/users/users.ts new file mode 100644 index 00000000..4dc1575c --- /dev/null +++ b/packages/server/src/auth/graphql/users/users.ts @@ -0,0 +1,52 @@ +/* Generated File DO NOT EDIT. */ +/* tslint:disable */ +import * as Types from '../graphql'; + +import { DocumentNode } from 'graphql'; +import gql from 'graphql-tag'; +export type UsersQueryVariables = Types.Exact<{ [key: string]: never }>; + +export type UsersQuery = { + __typename?: 'Query'; + users: Array<{ + __typename?: 'UserModel'; + id: string; + projectId: string; + username?: string | null; + fullname?: string | null; + email?: string | null; + role: number; + createdAt: any; + updatedAt: any; + deletedAt?: any | null; + }>; +}; + +export const UsersDocument = gql` + query users { + users { + id + projectId + username + fullname + email + role + createdAt + updatedAt + deletedAt + } + } +`; +export type Requester = ( + doc: DocumentNode, + vars?: V, + options?: C +) => Promise | AsyncIterable; +export function getSdk(requester: Requester) { + return { + users(variables?: UsersQueryVariables, options?: C): Promise { + return requester(UsersDocument, variables, options) as Promise; + } + }; +} +export type Sdk = ReturnType; diff --git a/packages/server/src/auth/providers/sdk.provider.ts b/packages/server/src/auth/providers/sdk.provider.ts new file mode 100644 index 00000000..012e7267 --- /dev/null +++ b/packages/server/src/auth/providers/sdk.provider.ts @@ -0,0 +1,19 @@ +import { Provider } from '@nestjs/common'; +import { Sdk, getSdk } from '../graphql/sdk'; +import { GraphQLClient } from 'graphql-request'; +import { ConfigService } from '@nestjs/config'; + +export const AUTH_SDK_PROVIDER = 'AUTH_SDK_PROVIDER'; + +export const authSDKProvider: Provider = { + provide: AUTH_SDK_PROVIDER, + inject: [ConfigService], + useFactory: async (configService: ConfigService) => { + // TODO: In the future, authentication will need to be handled on + // the endpoint + const endpoint = configService.getOrThrow('auth.graphqlEndpoint'); + const client = new GraphQLClient(endpoint); + + return getSdk(client); + } +}; diff --git a/packages/server/src/auth/services/user.service.ts b/packages/server/src/auth/services/user.service.ts new file mode 100644 index 00000000..6823ed85 --- /dev/null +++ b/packages/server/src/auth/services/user.service.ts @@ -0,0 +1,13 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { AUTH_SDK_PROVIDER } from '../providers/sdk.provider'; +import { Sdk, UserModel } from '../graphql/sdk'; + +@Injectable() +export class UserService { + constructor(@Inject(AUTH_SDK_PROVIDER) private readonly authSdk: Sdk) {} + + async getUsersForProject(projectId: string): Promise { + const { projectUsers } = await this.authSdk.projectUsers({ projectId }); + return projectUsers; + } +} diff --git a/packages/server/src/config/configuration.ts b/packages/server/src/config/configuration.ts index 9cf2592d..59598b66 100644 --- a/packages/server/src/config/configuration.ts +++ b/packages/server/src/config/configuration.ts @@ -20,7 +20,8 @@ export default () => ({ signedURLExpiration: process.env.GCP_STORAGE_ENTRY_SIGNED_URL_EXPIRATION || 15 * 60 * 1000 // 15 minutes }, auth: { - publicKeyUrl: process.env.AUTH_PUBLIC_KEY_URL || 'https://test-auth-service.sail.codes/public-key' + publicKeyUrl: process.env.AUTH_PUBLIC_KEY_URL || 'https://test-auth-service.sail.codes/public-key', + graphqlEndpoint: process.env.AUTH_GRAPHQL_ENDPOINT || 'https://test-auth-service.sail.codes/graphql' }, casbin: { model: process.env.CASBIN_MODEL || 'src/config/casbin-model.conf', diff --git a/packages/server/src/permission/permission.model.ts b/packages/server/src/permission/permission.model.ts new file mode 100644 index 00000000..1b3eff1c --- /dev/null +++ b/packages/server/src/permission/permission.model.ts @@ -0,0 +1,31 @@ +import { ObjectType, registerEnumType, Field, Directive, ID } from '@nestjs/graphql'; +import { Roles } from './permissions/roles'; + +registerEnumType(Roles, { + name: 'Roles' +}); + +/** Definition for external user */ +@ObjectType() +@Directive('@key(fields: "id")') +@Directive('@extends') +export class UserModel { + @Field(() => ID) + @Directive('@external') + id: string; +} + +@ObjectType() +export class Permission { + @Field(() => UserModel) + user: string; + + @Field(() => Roles) + role: Roles; + + @Field() + hasRole: boolean; + + @Field() + editable: boolean; +} diff --git a/packages/server/src/permission/permission.module.ts b/packages/server/src/permission/permission.module.ts index 534e61f0..950bee2b 100644 --- a/packages/server/src/permission/permission.module.ts +++ b/packages/server/src/permission/permission.module.ts @@ -1,9 +1,12 @@ -import { Module } from '@nestjs/common'; +import { Module, forwardRef } from '@nestjs/common'; import { casbinProvider } from './casbin.provider'; import { PermissionResolver } from './permission.resolver'; import { PermissionService } from './permission.service'; +import { ProjectModule } from '../project/project.module'; +import { AuthModule } from '../auth/auth.module'; @Module({ + imports: [forwardRef(() => ProjectModule), AuthModule], providers: [casbinProvider, PermissionResolver, PermissionService], exports: [casbinProvider] }) diff --git a/packages/server/src/permission/permission.resolver.ts b/packages/server/src/permission/permission.resolver.ts index a3b7692c..7204f3ae 100644 --- a/packages/server/src/permission/permission.resolver.ts +++ b/packages/server/src/permission/permission.resolver.ts @@ -1,4 +1,4 @@ -import { Resolver, Mutation, Args, ID } from '@nestjs/graphql'; +import { Resolver, Mutation, Args, ID, Query, ResolveField, Parent } from '@nestjs/graphql'; import { JwtAuthGuard } from '../jwt/jwt.guard'; import { UseGuards } from '@nestjs/common'; import { TokenContext } from '../jwt/token.context'; @@ -6,9 +6,12 @@ import { TokenPayload } from '../jwt/token.dto'; import { PermissionService } from './permission.service'; import { OrganizationContext } from 'src/organization/organization.context'; import { Organization } from 'src/organization/organization.model'; +import { ProjectPipe } from '../project/pipes/project.pipe'; +import { Project } from '../project/project.model'; +import { Permission, UserModel } from './permission.model'; @UseGuards(JwtAuthGuard) -@Resolver() +@Resolver(() => Permission) export class PermissionResolver { constructor(private readonly permissionService: PermissionService) {} @@ -21,4 +24,17 @@ export class PermissionResolver { await this.permissionService.grantOwner(targetUser, requestingUser.id, organization._id); return true; } + + @Query(() => [Permission]) + async getProjectPermissions( + @Args('project', { type: () => ID }, ProjectPipe) project: Project, + @TokenContext() requestingUser: TokenPayload + ): Promise { + return this.permissionService.getProjectPermissions(project, requestingUser); + } + + @ResolveField('user', () => UserModel) + resolveUser(@Parent() permission: Permission): any { + return { __typename: 'UserModel', id: permission.user }; + } } diff --git a/packages/server/src/permission/permission.service.ts b/packages/server/src/permission/permission.service.ts index d07f8cbd..3e49d3d8 100644 --- a/packages/server/src/permission/permission.service.ts +++ b/packages/server/src/permission/permission.service.ts @@ -2,10 +2,17 @@ import { Injectable, Inject, UnauthorizedException } from '@nestjs/common'; import { CASBIN_PROVIDER } from './casbin.provider'; import * as casbin from 'casbin'; import { Roles } from './permissions/roles'; +import { UserService } from '../auth/services/user.service'; +import { Project } from '../project/project.model'; +import { TokenPayload } from '../jwt/token.dto'; +import { Permission } from './permission.model'; @Injectable() export class PermissionService { - constructor(@Inject(CASBIN_PROVIDER) private readonly enforcer: casbin.Enforcer) {} + constructor( + @Inject(CASBIN_PROVIDER) private readonly enforcer: casbin.Enforcer, + private readonly userService: UserService + ) {} /** requestingUser must be an owner themselves */ async grantOwner(targetUser: string, requestingUser: string, organization: string): Promise { @@ -17,4 +24,26 @@ export class PermissionService { await this.enforcer.addPolicy(targetUser, Roles.OWNER, organization); } + + async getProjectPermissions(project: Project, requestingUser: TokenPayload): Promise { + // Get all the users associated with the organization + const users = await this.userService.getUsersForProject(requestingUser.projectId); + + // Create the cooresponding permission representation + const permissions = await Promise.all( + users.map(async (user) => { + const hasRole = await this.enforcer.enforce(user.id, Roles.PROJECT_ADMIN, project._id); + const editable = !(await this.enforcer.enforce(user.id, Roles.OWNER, project._id)); + + return { + user: user.id, + role: Roles.PROJECT_ADMIN, + hasRole, + editable + }; + }) + ); + + return permissions; + } } diff --git a/packages/server/src/permission/permissions/project.ts b/packages/server/src/permission/permissions/project.ts index 0327f6e8..0d055daf 100644 --- a/packages/server/src/permission/permissions/project.ts +++ b/packages/server/src/permission/permissions/project.ts @@ -5,7 +5,8 @@ export enum ProjectPermissions { CREATE = 'project:create', READ = 'project:read', UPDATE = 'project:update', - DELETE = 'project:delete' + DELETE = 'project:delete', + GRANT_ADMIN = 'project:grant_admin' } /** All role to project permissions */ @@ -13,6 +14,7 @@ export const roleToProjectPermissions: string[][] = [ // OWNER permissions [Roles.OWNER, ProjectPermissions.CREATE], [Roles.OWNER, ProjectPermissions.DELETE], + [Roles.OWNER, ProjectPermissions.GRANT_ADMIN], // PROJECT_ADMIN permissions [Roles.PROJECT_ADMIN, ProjectPermissions.UPDATE], diff --git a/packages/server/src/project/project.module.ts b/packages/server/src/project/project.module.ts index 61a6c339..fa584f5d 100644 --- a/packages/server/src/project/project.module.ts +++ b/packages/server/src/project/project.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { Module, forwardRef } from '@nestjs/common'; import { ProjectResolver } from './project.resolver'; import { ProjectService } from './project.service'; import { MongooseModule } from '@nestjs/mongoose'; @@ -29,7 +29,7 @@ import { PermissionModule } from '../permission/permission.module'; } ]), JwtModule, - PermissionModule + forwardRef(() => PermissionModule) ], providers: [ProjectResolver, ProjectService, ProjectPipe], exports: [ProjectPipe, ProjectService]