From d96a7624258f66202e865c5d9980e3b32e96e261 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Fri, 5 Jul 2024 12:11:24 +0200 Subject: [PATCH 01/31] progress --- Dockerfile | 17 + .../OauthHttpException.ts | 0 .../auth-server.module.ts} | 12 +- .../auth-token.controller.ts} | 14 +- .../dto/auth-function.dto.ts | 0 .../oauth-autorize.middleware.ts | 0 .../oauth-endpoints.controller.ts | 4 +- .../oauth-redirect.middleware.ts | 0 .../oauth-token.middleware.ts | 0 .../post-credentials.middleware.ts | 0 .../token-authorization-code.middleware.ts | 0 backend/src/app.module.ts | 8 +- backend/src/openapi-tag.ts | 2 +- frontend/.browserslistrc | 4 + frontend/.dockerignore | 1 + frontend/.editorconfig | 5 + frontend/.env | 1 + frontend/.gitignore | 24 + frontend/README.md | 57 + frontend/index.html | 16 + frontend/package-lock.json | 3525 +++++++++++++++++ frontend/package.json | 76 + frontend/src/App.vue | 123 + frontend/src/assets/logo.svg | 28 + frontend/src/components/BaseLayout.vue | 76 + frontend/src/components/GropiusCard.vue | 8 + frontend/src/main.ts | 21 + frontend/src/plugins/index.ts | 16 + frontend/src/plugins/theme.ts | 104 + frontend/src/plugins/vuetify.ts | 116 + frontend/src/router/index.ts | 27 + frontend/src/styles/settings.scss | 45 + frontend/src/util/types.ts | 3 + frontend/src/util/vuetifyFormConfig.ts | 9 + frontend/src/util/withErrorMessage.ts | 34 + frontend/src/views/Login.vue | 288 ++ frontend/src/views/Register.vue | 105 + frontend/src/views/RouterOnly.vue | 1 + frontend/src/views/model.ts | 82 + frontend/src/vite-env.d.ts | 7 + frontend/tsconfig.json | 28 + frontend/tsconfig.node.json | 9 + frontend/vite.config.mts | 68 + 43 files changed, 4944 insertions(+), 20 deletions(-) create mode 100644 Dockerfile rename backend/src/{oauth-server => api-internal}/OauthHttpException.ts (100%) rename backend/src/{oauth-server/oauth-server.module.ts => api-internal/auth-server.module.ts} (89%) rename backend/src/{oauth-server/oauth-token.controller.ts => api-internal/auth-token.controller.ts} (95%) rename backend/src/{oauth-server => api-internal}/dto/auth-function.dto.ts (100%) rename backend/src/{oauth-server => api-internal}/oauth-autorize.middleware.ts (100%) rename backend/src/{oauth-server => api-internal}/oauth-endpoints.controller.ts (96%) rename backend/src/{oauth-server => api-internal}/oauth-redirect.middleware.ts (100%) rename backend/src/{oauth-server => api-internal}/oauth-token.middleware.ts (100%) rename backend/src/{oauth-server => api-internal}/post-credentials.middleware.ts (100%) rename backend/src/{oauth-server => api-internal}/token-authorization-code.middleware.ts (100%) create mode 100644 frontend/.browserslistrc create mode 100644 frontend/.dockerignore create mode 100644 frontend/.editorconfig create mode 100644 frontend/.env create mode 100644 frontend/.gitignore create mode 100644 frontend/README.md create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/assets/logo.svg create mode 100644 frontend/src/components/BaseLayout.vue create mode 100644 frontend/src/components/GropiusCard.vue create mode 100644 frontend/src/main.ts create mode 100644 frontend/src/plugins/index.ts create mode 100644 frontend/src/plugins/theme.ts create mode 100644 frontend/src/plugins/vuetify.ts create mode 100644 frontend/src/router/index.ts create mode 100644 frontend/src/styles/settings.scss create mode 100644 frontend/src/util/types.ts create mode 100644 frontend/src/util/vuetifyFormConfig.ts create mode 100644 frontend/src/util/withErrorMessage.ts create mode 100644 frontend/src/views/Login.vue create mode 100644 frontend/src/views/Register.vue create mode 100644 frontend/src/views/RouterOnly.vue create mode 100644 frontend/src/views/model.ts create mode 100644 frontend/src/vite-env.d.ts create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.mts diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..6f969653 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM node:18 +ENV NODE_ENV build +USER node +WORKDIR /home/node +ADD . . +RUN npm ci +RUN npm run build + +FROM node:18 +ENV NODE_ENV production +USER node +WORKDIR /home/node +COPY --from=0 /home/node/package*.json ./ +COPY --from=0 /home/node/node_modules ./node_modules/ +COPY --from=0 /home/node/dist ./dist/ +COPY --from=0 /home/node/static ./static/ +CMD ["node", "dist/main.js"] \ No newline at end of file diff --git a/backend/src/oauth-server/OauthHttpException.ts b/backend/src/api-internal/OauthHttpException.ts similarity index 100% rename from backend/src/oauth-server/OauthHttpException.ts rename to backend/src/api-internal/OauthHttpException.ts diff --git a/backend/src/oauth-server/oauth-server.module.ts b/backend/src/api-internal/auth-server.module.ts similarity index 89% rename from backend/src/oauth-server/oauth-server.module.ts rename to backend/src/api-internal/auth-server.module.ts index 8d85ac97..a56e76bd 100644 --- a/backend/src/oauth-server/oauth-server.module.ts +++ b/backend/src/api-internal/auth-server.module.ts @@ -8,7 +8,7 @@ import { StrategiesModule } from "../strategies/strategies.module"; import { OauthAutorizeMiddleware } from "./oauth-autorize.middleware"; import { OauthEndpointsController } from "./oauth-endpoints.controller"; import { OauthRedirectMiddleware } from "./oauth-redirect.middleware"; -import { OauthTokenController } from "./oauth-token.controller"; +import { AuthTokenController } from "./auth-token.controller"; import { OauthTokenMiddleware } from "./oauth-token.middleware"; import { PostCredentialsMiddleware } from "./post-credentials.middleware"; import { TokenAuthorizationCodeMiddleware } from "./token-authorization-code.middleware"; @@ -22,9 +22,9 @@ import { TokenAuthorizationCodeMiddleware } from "./token-authorization-code.mid TokenAuthorizationCodeMiddleware, PostCredentialsMiddleware, ], - controllers: [OauthTokenController, OauthEndpointsController], + controllers: [AuthTokenController, OauthEndpointsController], }) -export class OauthServerModule { +export class AuthServerModule { private middlewares: { middlewares: NestMiddleware[]; path: string }[] = []; constructor( @@ -46,17 +46,17 @@ export class OauthServerModule { // its just to make absolutely sure, no unauthorized request gets through this.errorHandler, ], - path: "authenticate/oauth/:id/authorize/:mode?", + path: "internal/auth/redirect/:id/:mode", }); this.middlewares.push({ middlewares: [this.strategies, this.oauthRedirect, this.errorHandler], - path: "authenticate/oauth/:id/callback", + path: "internal/auth/callback/:id", }); this.middlewares.push({ middlewares: [this.modeExtractor, this.oauthToken, this.errorHandler], - path: "authenticate/oauth/:id?/token/:mode?", + path: "internal/auth/submit/:id/:mode", }); } diff --git a/backend/src/oauth-server/oauth-token.controller.ts b/backend/src/api-internal/auth-token.controller.ts similarity index 95% rename from backend/src/oauth-server/oauth-token.controller.ts rename to backend/src/api-internal/auth-token.controller.ts index 8aac3dcf..20fab4ca 100644 --- a/backend/src/oauth-server/oauth-token.controller.ts +++ b/backend/src/api-internal/auth-token.controller.ts @@ -13,7 +13,7 @@ import { ensureState } from "src/strategies/utils"; import { OauthServerStateData } from "./oauth-autorize.middleware"; import { OauthHttpException } from "./OauthHttpException"; -export interface OauthTokenEdnpointResponseDto { +export interface OauthTokenEndpointResponseDto { access_token: string; token_type: "bearer"; expires_in: number; @@ -21,10 +21,10 @@ export interface OauthTokenEdnpointResponseDto { scope: string; } -@Controller("oauth") -@ApiTags(OpenApiTag.CREDENTIALS) -export class OauthTokenController { - private readonly logger = new Logger(OauthTokenController.name); +@Controller("auth") +@ApiTags(OpenApiTag.INTERNAL_API) +export class AuthTokenController { + private readonly logger = new Logger(AuthTokenController.name); constructor( private readonly authClientService: AuthClientService, private readonly activeLoginService: ActiveLoginService, @@ -99,7 +99,7 @@ export class OauthTokenController { loginData: UserLoginData, activeLogin: ActiveLogin, currentClient: AuthClient, - ): Promise { + ): Promise { const tokenExpiresInMs: number = parseInt(process.env.GROPIUS_ACCESS_TOKEN_EXPIRATION_TIME_MS, 10); let accessToken: string; @@ -134,7 +134,7 @@ export class OauthTokenController { } @Post(":id?/token/:mode?") - async token(@Res({ passthrough: true }) res: Response): Promise { + async token(@Res({ passthrough: true }) res: Response): Promise { ensureState(res); const currentClient = (res.locals.state as OauthServerStateData).client; if (!currentClient) { diff --git a/backend/src/oauth-server/dto/auth-function.dto.ts b/backend/src/api-internal/dto/auth-function.dto.ts similarity index 100% rename from backend/src/oauth-server/dto/auth-function.dto.ts rename to backend/src/api-internal/dto/auth-function.dto.ts diff --git a/backend/src/oauth-server/oauth-autorize.middleware.ts b/backend/src/api-internal/oauth-autorize.middleware.ts similarity index 100% rename from backend/src/oauth-server/oauth-autorize.middleware.ts rename to backend/src/api-internal/oauth-autorize.middleware.ts diff --git a/backend/src/oauth-server/oauth-endpoints.controller.ts b/backend/src/api-internal/oauth-endpoints.controller.ts similarity index 96% rename from backend/src/oauth-server/oauth-endpoints.controller.ts rename to backend/src/api-internal/oauth-endpoints.controller.ts index c5f9f7f3..ebf9c81e 100644 --- a/backend/src/oauth-server/oauth-endpoints.controller.ts +++ b/backend/src/api-internal/oauth-endpoints.controller.ts @@ -28,7 +28,7 @@ export class OauthEndpointsController { required: false, description: "The function/mode how to authenticate. Defaults to 'login'", }) - @ApiTags(OpenApiTag.CREDENTIALS) + @ApiTags(OpenApiTag.INTERNAL_API) authorizeEndpoint(@Param("id") id: string, @Param("mode") mode?: AuthFunctionInput) { throw new HttpException( "This controller shouldn't be reached as all functionality is handeled in middleware", @@ -48,7 +48,7 @@ export class OauthEndpointsController { name: "id", description: "The id of the strategy instance which initiated the funcation calling the callback.", }) - @ApiTags(OpenApiTag.CREDENTIALS) + @ApiTags(OpenApiTag.INTERNAL_API) redirectEndpoint() { throw new HttpException( "This controller shouldn't be reached as all functionality is handeled in middleware", diff --git a/backend/src/oauth-server/oauth-redirect.middleware.ts b/backend/src/api-internal/oauth-redirect.middleware.ts similarity index 100% rename from backend/src/oauth-server/oauth-redirect.middleware.ts rename to backend/src/api-internal/oauth-redirect.middleware.ts diff --git a/backend/src/oauth-server/oauth-token.middleware.ts b/backend/src/api-internal/oauth-token.middleware.ts similarity index 100% rename from backend/src/oauth-server/oauth-token.middleware.ts rename to backend/src/api-internal/oauth-token.middleware.ts diff --git a/backend/src/oauth-server/post-credentials.middleware.ts b/backend/src/api-internal/post-credentials.middleware.ts similarity index 100% rename from backend/src/oauth-server/post-credentials.middleware.ts rename to backend/src/api-internal/post-credentials.middleware.ts diff --git a/backend/src/oauth-server/token-authorization-code.middleware.ts b/backend/src/api-internal/token-authorization-code.middleware.ts similarity index 100% rename from backend/src/oauth-server/token-authorization-code.middleware.ts rename to backend/src/api-internal/token-authorization-code.middleware.ts diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 727af7e9..26ebc2a2 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -8,7 +8,7 @@ import { ModelModule } from "./model/model.module"; import { StrategiesModule } from "./strategies/strategies.module"; import { BackendServicesModule } from "./backend-services/backend-services.module"; import { validationSchema } from "./configuration-validator"; -import { OauthServerModule } from "./oauth-server/oauth-server.module"; +import { AuthServerModule } from "./api-internal/auth-server.module"; import { DefaultReturn } from "./default-return.dto"; import { InitializationModule } from "./initialization/initialization.module"; import * as path from "path"; @@ -57,12 +57,12 @@ import { ServeStaticModule } from "@nestjs/serve-static"; ApiLoginModule, ApiSyncModule, StrategiesModule, - OauthServerModule, + AuthServerModule, RouterModule.register([ { path: "login", module: ApiLoginModule }, { path: "login", module: StrategiesModule }, - { path: "syncApi", module: ApiSyncModule }, - { path: "authenticate", module: OauthServerModule }, + { path: "sync", module: ApiSyncModule }, + { path: "internal", module: AuthServerModule }, ]), BackendServicesModule, InitializationModule, diff --git a/backend/src/openapi-tag.ts b/backend/src/openapi-tag.ts index c9ac8186..3bd5dcbd 100644 --- a/backend/src/openapi-tag.ts +++ b/backend/src/openapi-tag.ts @@ -1,5 +1,5 @@ export enum OpenApiTag { LOGIN_API = "login-api", SYNC_API = "sync-api", - CREDENTIALS = "credentials", + INTERNAL_API = "internal-api", } diff --git a/frontend/.browserslistrc b/frontend/.browserslistrc new file mode 100644 index 00000000..dc3bc09a --- /dev/null +++ b/frontend/.browserslistrc @@ -0,0 +1,4 @@ +> 1% +last 2 versions +not dead +not ie 11 diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 00000000..40b878db --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/frontend/.editorconfig b/frontend/.editorconfig new file mode 100644 index 00000000..a1879fcd --- /dev/null +++ b/frontend/.editorconfig @@ -0,0 +1,5 @@ +[*.{js,jsx,ts,tsx,vue}] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 00000000..6bc7cddd --- /dev/null +++ b/frontend/.env @@ -0,0 +1 @@ +VITE_LOGIN_OAUTH_CLIENT_ID=ToDo_InsertClientIdHere \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 00000000..559348ad --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +.DS_Store +node_modules +/dist + +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +tsconfig.tsbuildinfo \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 00000000..f080963f --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,57 @@ +# essentials + +## Project setup + +``` +# yarn +yarn + +# npm +npm install + +# pnpm +pnpm install +``` + +### Compiles and hot-reloads for development + +``` +# yarn +yarn dev + +# npm +npm run dev + +# pnpm +pnpm dev +``` + +### Compiles and minifies for production + +``` +# yarn +yarn build + +# npm +npm run build + +# pnpm +pnpm build +``` + +### Lints and fixes files + +``` +# yarn +yarn lint + +# npm +npm run lint + +# pnpm +pnpm lint +``` + +### Customize configuration + +See [Configuration Reference](https://vitejs.dev/config/). diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 00000000..fc16145a --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,16 @@ + + + + + + + + Gropius + + + +
+ + + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 00000000..e499ded9 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3525 @@ +{ + "name": "gropius-login-service-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gropius-login-service-frontend", + "version": "0.0.0", + "dependencies": { + "@fontsource/roboto": "^5.0.13", + "@material/material-color-utilities": "^0.2.7", + "@mdi/font": "7.4.47", + "@vee-validate/yup": "^4.13.1", + "@vueuse/core": "^10.11.0", + "axios": "^1.7.2", + "core-js": "^3.37.1", + "jwt-decode": "^4.0.0", + "vee-validate": "^4.13.1", + "vue": "3.4.29", + "vue-router": "^4.3.3", + "vuetify": "^3.6.10", + "webfontloader": "^1.6.28", + "yup": "^1.4.0" + }, + "devDependencies": { + "@babel/types": "^7.24.7", + "@types/node": "^20.14.6", + "@types/webfontloader": "^1.6.38", + "@vitejs/plugin-vue": "^5.0.5", + "@vue/eslint-config-typescript": "^13.0.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-vue": "^9.26.0", + "prettier": "^3.3.2", + "sass": "^1.77.6", + "typescript": "^5.4.5", + "unplugin-fonts": "^1.1.1", + "vite": "^5.3.1", + "vite-plugin-vuetify": "^2.0.3", + "vue-tsc": "^2.0.21" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "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.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@fontsource/roboto": { + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.13.tgz", + "integrity": "sha512-j61DHjsdUCKMXSdNLTOxcG701FWnF0jcqNNQi2iPCDxU8seN/sMxeh62dC++UiagCWq9ghTypX+Pcy7kX+QOeQ==" + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@material/material-color-utilities": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@material/material-color-utilities/-/material-color-utilities-0.2.7.tgz", + "integrity": "sha512-0FCeqG6WvK4/Cc06F/xXMd/pv4FeisI0c1tUpBbfhA2n9Y8eZEv4Karjbmf2ZqQCPUWMrGp8A571tCjizxoTiQ==" + }, + "node_modules/@mdi/font": { + "version": "7.4.47", + "resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz", + "integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", + "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", + "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", + "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", + "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", + "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", + "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", + "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", + "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", + "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", + "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", + "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", + "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", + "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", + "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", + "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", + "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "devOptional": true + }, + "node_modules/@types/node": { + "version": "20.14.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz", + "integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==", + "devOptional": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==" + }, + "node_modules/@types/webfontloader": { + "version": "1.6.38", + "resolved": "https://registry.npmjs.org/@types/webfontloader/-/webfontloader-1.6.38.tgz", + "integrity": "sha512-kUaF72Fv202suFx6yBrwXqeVRMx7hGtJTesyESZgn9sEPCUeDXm2p0SiyS1MTqW74nQP4p7JyrOCwZ7pNFns4w==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.15.0.tgz", + "integrity": "sha512-uiNHpyjZtFrLwLDpHnzaDlP3Tt6sGMqTCiqmxaN4n4RP0EfYZDODJyddiFDF44Hjwxr5xAcaYxVKm9QKQFJFLA==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.15.0", + "@typescript-eslint/type-utils": "7.15.0", + "@typescript-eslint/utils": "7.15.0", + "@typescript-eslint/visitor-keys": "7.15.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.15.0.tgz", + "integrity": "sha512-k9fYuQNnypLFcqORNClRykkGOMOj+pV6V91R4GO/l1FDGwpqmSwoOQrOHo3cGaH63e+D3ZiCAOsuS/D2c99j/A==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.15.0", + "@typescript-eslint/types": "7.15.0", + "@typescript-eslint/typescript-estree": "7.15.0", + "@typescript-eslint/visitor-keys": "7.15.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.15.0.tgz", + "integrity": "sha512-Q/1yrF/XbxOTvttNVPihxh1b9fxamjEoz2Os/Pe38OHwxC24CyCqXxGTOdpb4lt6HYtqw9HetA/Rf6gDGaMPlw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.15.0", + "@typescript-eslint/visitor-keys": "7.15.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.15.0.tgz", + "integrity": "sha512-SkgriaeV6PDvpA6253PDVep0qCqgbO1IOBiycjnXsszNTVQe5flN5wR5jiczoEoDEnAqYFSFFc9al9BSGVltkg==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.15.0", + "@typescript-eslint/utils": "7.15.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.15.0.tgz", + "integrity": "sha512-aV1+B1+ySXbQH0pLK0rx66I3IkiZNidYobyfn0WFsdGhSXw+P3YOqeTq5GED458SfB24tg+ux3S+9g118hjlTw==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.15.0.tgz", + "integrity": "sha512-gjyB/rHAopL/XxfmYThQbXbzRMGhZzGw6KpcMbfe8Q3nNQKStpxnUKeXb0KiN/fFDR42Z43szs6rY7eHk0zdGQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.15.0", + "@typescript-eslint/visitor-keys": "7.15.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.15.0.tgz", + "integrity": "sha512-hfDMDqaqOqsUVGiEPSMLR/AjTSCsmJwjpKkYQRo1FNbmW4tBwBspYDwO9eh7sKSTwMQgBw9/T4DHudPaqshRWA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.15.0", + "@typescript-eslint/types": "7.15.0", + "@typescript-eslint/typescript-estree": "7.15.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.15.0.tgz", + "integrity": "sha512-Hqgy/ETgpt2L5xueA/zHHIl4fJI2O4XUE9l4+OIfbJIRSnTJb/QscncdqqZzofQegIJugRIF57OJea1khw2SDw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.15.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.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/@vee-validate/yup": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@vee-validate/yup/-/yup-4.13.1.tgz", + "integrity": "sha512-XS057kHcf4uFew708haHVxCjqJ9OYXV57KmOj/R9fnuh76i54F79DCRzm4FDAevOJzkuo7rNBYOSTplY4iXPrQ==", + "dependencies": { + "type-fest": "^4.8.3", + "vee-validate": "4.13.1", + "yup": "^1.3.2" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.5.tgz", + "integrity": "sha512-LOjm7XeIimLBZyzinBQ6OSm3UBCNVCpLkxGC0oWmm2YPzVZoxMsdvNVimLTBzpAnR9hl/yn1SHGuRfe6/Td9rQ==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.0-alpha.14", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.0-alpha.14.tgz", + "integrity": "sha512-R6eJcUKo/KftaWHwJrWjBgj/+vW9g4xTByVQEK3IHTciMKmomoSbxaNqolu1/sJKbH9Tdg0EAqTFqIzKU9iQHw==", + "dev": true, + "dependencies": { + "@volar/source-map": "2.4.0-alpha.14" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.0-alpha.14", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.0-alpha.14.tgz", + "integrity": "sha512-ACOsoDKvW29BIfdfnvQkm8S1m/RLARuHL9x7qS/9c6liMl1K0Y3RqXuC42HhWrWBm4hk0UyRKgdnv2R0teXPvg==", + "dev": true + }, + "node_modules/@volar/typescript": { + "version": "2.4.0-alpha.14", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.0-alpha.14.tgz", + "integrity": "sha512-FQtQruOc7qQwcq5Q666pxF6ekRqZG5ILL3sS40Oac1V69QdAZ7q+IOQ2+z6SHJDENY49ygBv0hN9HrxRLtk15Q==", + "dev": true, + "dependencies": { + "@volar/language-core": "2.4.0-alpha.14", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.29.tgz", + "integrity": "sha512-TFKiRkKKsRCKvg/jTSSKK7mYLJEQdUiUfykbG49rubC9SfDyvT2JrzTReopWlz2MxqeLyxh9UZhvxEIBgAhtrg==", + "dependencies": { + "@babel/parser": "^7.24.7", + "@vue/shared": "3.4.29", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.29.tgz", + "integrity": "sha512-A6+iZ2fKIEGnfPJejdB7b1FlJzgiD+Y/sxxKwJWg1EbJu6ZPgzaPQQ51ESGNv0CP6jm6Z7/pO6Ia8Ze6IKrX7w==", + "dependencies": { + "@vue/compiler-core": "3.4.29", + "@vue/shared": "3.4.29" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.29.tgz", + "integrity": "sha512-zygDcEtn8ZimDlrEQyLUovoWgKQic6aEQqRXce2WXBvSeHbEbcAsXyCk9oG33ZkyWH4sl9D3tkYc1idoOkdqZQ==", + "dependencies": { + "@babel/parser": "^7.24.7", + "@vue/compiler-core": "3.4.29", + "@vue/compiler-dom": "3.4.29", + "@vue/compiler-ssr": "3.4.29", + "@vue/shared": "3.4.29", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.10", + "postcss": "^8.4.38", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.29.tgz", + "integrity": "sha512-rFbwCmxJ16tDp3N8XCx5xSQzjhidYjXllvEcqX/lopkoznlNPz3jyy0WGJCyhAaVQK677WWFt3YO/WUEkMMUFQ==", + "dependencies": { + "@vue/compiler-dom": "3.4.29", + "@vue/shared": "3.4.29" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.3.tgz", + "integrity": "sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw==" + }, + "node_modules/@vue/eslint-config-typescript": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-13.0.0.tgz", + "integrity": "sha512-MHh9SncG/sfqjVqjcuFLOLD6Ed4dRAis4HNt0dXASeAuLqIAx4YMB1/m2o4pUKK1vCt8fUvYG8KKX2Ot3BVZTg==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "^7.1.1", + "@typescript-eslint/parser": "^7.1.1", + "vue-eslint-parser": "^9.3.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "peerDependencies": { + "eslint": "^8.56.0", + "eslint-plugin-vue": "^9.0.0", + "typescript": ">=4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core": { + "version": "2.0.24", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.24.tgz", + "integrity": "sha512-997YD6Lq/66LXr3ZOLNxDCmyn13z9NP8LU1UZn9hGCDWhzlbXAIP0hOgL3w3x4RKEaWTaaRtsHP9DzHvmduruQ==", + "dev": true, + "dependencies": { + "@volar/language-core": "~2.4.0-alpha.2", + "@vue/compiler-dom": "^3.4.0", + "@vue/shared": "^3.4.0", + "computeds": "^0.0.1", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "vue-template-compiler": "^2.7.14" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.29.tgz", + "integrity": "sha512-w8+KV+mb1a8ornnGQitnMdLfE0kXmteaxLdccm2XwdFxXst4q/Z7SEboCV5SqJNpZbKFeaRBBJBhW24aJyGINg==", + "dependencies": { + "@vue/shared": "3.4.29" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.29.tgz", + "integrity": "sha512-s8fmX3YVR/Rk5ig0ic0NuzTNjK2M7iLuVSZyMmCzN/+Mjuqqif1JasCtEtmtoJWF32pAtUjyuT2ljNKNLeOmnQ==", + "dependencies": { + "@vue/reactivity": "3.4.29", + "@vue/shared": "3.4.29" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.29.tgz", + "integrity": "sha512-gI10atCrtOLf/2MPPMM+dpz3NGulo9ZZR9d1dWo4fYvm+xkfvRrw1ZmJ7mkWtiJVXSsdmPbcK1p5dZzOCKDN0g==", + "dependencies": { + "@vue/reactivity": "3.4.29", + "@vue/runtime-core": "3.4.29", + "@vue/shared": "3.4.29", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.29.tgz", + "integrity": "sha512-HMLCmPI2j/k8PVkSBysrA2RxcxC5DgBiCdj7n7H2QtR8bQQPqKAe8qoaxLcInzouBmzwJ+J0x20ygN/B5mYBng==", + "dependencies": { + "@vue/compiler-ssr": "3.4.29", + "@vue/shared": "3.4.29" + }, + "peerDependencies": { + "vue": "3.4.29" + } + }, + "node_modules/@vue/shared": { + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==" + }, + "node_modules/@vuetify/loader-shared": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@vuetify/loader-shared/-/loader-shared-2.0.3.tgz", + "integrity": "sha512-Ss3GC7eJYkp2SF6xVzsT7FAruEmdihmn4OCk2+UocREerlXKWgOKKzTN5PN3ZVN5q05jHHrsNhTuWbhN61Bpdg==", + "devOptional": true, + "dependencies": { + "upath": "^2.0.1" + }, + "peerDependencies": { + "vue": "^3.0.0", + "vuetify": "^3.0.0" + } + }, + "node_modules/@vueuse/core": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.0.tgz", + "integrity": "sha512-x3sD4Mkm7PJ+pcq3HX8PLPBadXCAlSDR/waK87dz0gQE+qJnaaFhc/dZVfJz+IUYzTMVGum2QlR7ImiJQN4s6g==", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.0", + "@vueuse/shared": "10.11.0", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.8.tgz", + "integrity": "sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.0.tgz", + "integrity": "sha512-kQX7l6l8dVWNqlqyN3ePW3KmjCQO3ZMgXuBMddIu83CmucrsBfXlH+JoviYyRBws/yLTQO8g3Pbw+bdIoVm4oQ==", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.0.tgz", + "integrity": "sha512-fyNoIXEq3PfX1L3NkNhtVQUSRtqYwJtJg+Bp9rIzculIZWHTkKSysujrOk2J+NrRulLTQH9+3gGSfYLWSEWU1A==", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.8.tgz", + "integrity": "sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "devOptional": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "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/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "devOptional": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "devOptional": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "devOptional": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/computeds": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz", + "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/core-js": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz", + "integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "devOptional": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@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.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", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-vue": { + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.27.0.tgz", + "integrity": "sha512-5Dw3yxEyuBSXTzT5/Ge1X5kIkRTQ3nvBn/VwPwInNiZBSJOO/timWMUaflONnFBzU6NhB68lxnCda7ULV5N7LA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "globals": "^13.24.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^6.0.15", + "semver": "^7.6.0", + "vue-eslint-parser": "^9.4.3", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-scope": { + "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", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "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" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "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.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-glob": { + "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", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "devOptional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "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" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", + "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", + "devOptional": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "devOptional": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "devOptional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "devOptional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "engines": { + "node": ">=18" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/magic-string": { + "version": "0.30.10", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", + "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "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.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "devOptional": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", + "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", + "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/punycode": { + "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" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "devOptional": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", + "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", + "devOptional": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.18.0", + "@rollup/rollup-android-arm64": "4.18.0", + "@rollup/rollup-darwin-arm64": "4.18.0", + "@rollup/rollup-darwin-x64": "4.18.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", + "@rollup/rollup-linux-arm-musleabihf": "4.18.0", + "@rollup/rollup-linux-arm64-gnu": "4.18.0", + "@rollup/rollup-linux-arm64-musl": "4.18.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", + "@rollup/rollup-linux-riscv64-gnu": "4.18.0", + "@rollup/rollup-linux-s390x-gnu": "4.18.0", + "@rollup/rollup-linux-x64-gnu": "4.18.0", + "@rollup/rollup-linux-x64-musl": "4.18.0", + "@rollup/rollup-win32-arm64-msvc": "4.18.0", + "@rollup/rollup-win32-ia32-msvc": "4.18.0", + "@rollup/rollup-win32-x64-msvc": "4.18.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sass": { + "version": "1.77.6", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.6.tgz", + "integrity": "sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==", + "devOptional": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "devOptional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.21.0.tgz", + "integrity": "sha512-ADn2w7hVPcK6w1I0uWnM//y1rLXZhzB9mr0a3OirzclKF1Wp6VzevUmzz/NRAWunOT6E8HrnpGY7xOfc6K57fA==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "devOptional": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "devOptional": true + }, + "node_modules/unplugin": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.11.0.tgz", + "integrity": "sha512-3r7VWZ/webh0SGgJScpWl2/MRCZK5d3ZYFcNaeci/GQ7Teop7zf0Nl2pUuz7G21BwPd9pcUPOC5KmJ2L3WgC5g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.3", + "chokidar": "^3.6.0", + "webpack-sources": "^3.2.3", + "webpack-virtual-modules": "^0.6.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/unplugin-fonts": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unplugin-fonts/-/unplugin-fonts-1.1.1.tgz", + "integrity": "sha512-/Aw/rL9D2aslGGM0vi+2R2aG508RSwawLnnBuo+JDSqYc4cHJO1R1phllhN6GysEhBp/6a4B6+vSFPVapWyAAw==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.12", + "unplugin": "^1.3.1" + }, + "peerDependencies": { + "@nuxt/kit": "^3.0.0", + "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/upath": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz", + "integrity": "sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==", + "devOptional": true, + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vee-validate": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/vee-validate/-/vee-validate-4.13.1.tgz", + "integrity": "sha512-JAlUWTBHg0z66n+v66mrtE9IC1xmVCggzpyc7UXCNkizVok8Zgt1VAVjobSxA/0N19Zn6v6hRfjoYciYH/Z11Q==", + "dependencies": { + "@vue/devtools-api": "^6.6.1", + "type-fest": "^4.8.3" + }, + "peerDependencies": { + "vue": "^3.4.26" + } + }, + "node_modules/vite": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz", + "integrity": "sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==", + "devOptional": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.39", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-vuetify": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/vite-plugin-vuetify/-/vite-plugin-vuetify-2.0.3.tgz", + "integrity": "sha512-HbYajgGgb/noaVKNRhnnXIiQZrNXfNIeanUGAwXgOxL6h/KULS40Uf51Kyz8hNmdegF+DwjgXXI/8J1PNS83xw==", + "devOptional": true, + "dependencies": { + "@vuetify/loader-shared": "^2.0.3", + "debug": "^4.3.3", + "upath": "^2.0.1" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": ">=5", + "vue": "^3.0.0", + "vuetify": "^3.0.0" + } + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "dev": true + }, + "node_modules/vue": { + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.29.tgz", + "integrity": "sha512-8QUYfRcYzNlYuzKPfge1UWC6nF9ym0lx7mpGVPJYNhddxEf3DD0+kU07NTL0sXuiT2HuJuKr/iEO8WvXvT0RSQ==", + "dependencies": { + "@vue/compiler-dom": "3.4.29", + "@vue/compiler-sfc": "3.4.29", + "@vue/runtime-dom": "3.4.29", + "@vue/server-renderer": "3.4.29", + "@vue/shared": "3.4.29" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-eslint-parser": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/vue-router": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.0.tgz", + "integrity": "sha512-HB+t2p611aIZraV2aPSRNXf0Z/oLZFrlygJm+sZbdJaW6lcFqEDQwnzUBXn+DApw+/QzDU/I9TeWx9izEjTmsA==", + "dependencies": { + "@vue/devtools-api": "^6.5.1" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-template-compiler": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", + "dev": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "2.0.24", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.24.tgz", + "integrity": "sha512-1qi4P8L7yS78A7OJ7CDDxUIZPD6nVxoQEgX3DkRZNi1HI1qOfzOJwQlNpmwkogSVD6S/XcanbW9sktzpSxz6rA==", + "dev": true, + "dependencies": { + "@volar/typescript": "~2.4.0-alpha.2", + "@vue/language-core": "2.0.24", + "semver": "^7.5.4" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/vuetify": { + "version": "3.6.11", + "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.6.11.tgz", + "integrity": "sha512-DMreVZ6+WCVnvRoFVPGtC+Kc4afx+etcTLgX2AqUj6lQ4RrEx0TlQoNAW1oKPH4eViv2wfdvQ3xb/Yw1c86cTw==", + "engines": { + "node": "^12.20 || >=14.13" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/johnleider" + }, + "peerDependencies": { + "typescript": ">=4.7", + "vite-plugin-vuetify": ">=1.0.0", + "vue": "^3.3.0", + "vue-i18n": "^9.0.0", + "webpack-plugin-vuetify": ">=2.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vite-plugin-vuetify": { + "optional": true + }, + "vue-i18n": { + "optional": true + }, + "webpack-plugin-vuetify": { + "optional": true + } + } + }, + "node_modules/webfontloader": { + "version": "1.6.28", + "resolved": "https://registry.npmjs.org/webfontloader/-/webfontloader-1.6.28.tgz", + "integrity": "sha512-Egb0oFEga6f+nSgasH3E0M405Pzn6y3/9tOVanv/DLfa1YBIgcv90L18YyWnvXkRbIM17v5Kv6IT2N6g1x5tvQ==" + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yup": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.4.0.tgz", + "integrity": "sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/yup/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..e6081330 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,76 @@ +{ + "name": "gropius-login-service-frontend", + "version": "0.0.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "vue-tsc --noEmit && vite build", + "preview": "vite preview", + "lint": "eslint . --fix --ignore-path .gitignore", + "codegen": "graphql-codegen --config codegen.yaml", + "format": "prettier --write \"**/*.{ts,json,js,tsx,jsx,vue}\"" + }, + "dependencies": { + "@fontsource/roboto": "^5.0.13", + "@material/material-color-utilities": "^0.2.7", + "@mdi/font": "7.4.47", + "@vee-validate/yup": "^4.13.1", + "@vueuse/core": "^10.11.0", + "axios": "^1.7.2", + "core-js": "^3.37.1", + "jwt-decode": "^4.0.0", + "vee-validate": "^4.13.1", + "vue": "3.4.29", + "vue-router": "^4.3.3", + "vuetify": "^3.6.10", + "webfontloader": "^1.6.28", + "yup": "^1.4.0" + }, + "devDependencies": { + "@babel/types": "^7.24.7", + "@types/node": "^20.14.6", + "@types/webfontloader": "^1.6.38", + "@vitejs/plugin-vue": "^5.0.5", + "@vue/eslint-config-typescript": "^13.0.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-vue": "^9.26.0", + "prettier": "^3.3.2", + "sass": "^1.77.6", + "typescript": "^5.4.5", + "unplugin-fonts": "^1.1.1", + "vite": "^5.3.1", + "vite-plugin-vuetify": "^2.0.3", + "vue-tsc": "^2.0.21" + }, + "prettier": { + "tabWidth": 4, + "trailingComma": "none", + "printWidth": 120 + }, + "eslintConfig": { + "env": { + "node": true + }, + "root": true, + "parser": "vue-eslint-parser", + "parserOptions": { + "parser": "@typescript-eslint/parser" + }, + "extends": [ + "plugin:vue/vue3-recommended", + "prettier", + "eslint:recommended", + "@vue/typescript/recommended" + ], + "plugins": [ + "@typescript-eslint", + "prettier" + ], + "rules": { + "prettier/prettier": "error", + "vue/no-mutating-props": "warn" + } + } +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 00000000..49aa7d63 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,123 @@ + + + + diff --git a/frontend/src/assets/logo.svg b/frontend/src/assets/logo.svg new file mode 100644 index 00000000..da0df230 --- /dev/null +++ b/frontend/src/assets/logo.svg @@ -0,0 +1,28 @@ + + + logo + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/BaseLayout.vue b/frontend/src/components/BaseLayout.vue new file mode 100644 index 00000000..03a274a4 --- /dev/null +++ b/frontend/src/components/BaseLayout.vue @@ -0,0 +1,76 @@ + + + diff --git a/frontend/src/components/GropiusCard.vue b/frontend/src/components/GropiusCard.vue new file mode 100644 index 00000000..29a3d82d --- /dev/null +++ b/frontend/src/components/GropiusCard.vue @@ -0,0 +1,8 @@ + diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 00000000..43ce5094 --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,21 @@ +/** + * main.ts + * + * Bootstraps Vuetify and other plugins then mounts the App` + */ + +// Components +import "unfonts.css"; +import App from "./App.vue"; + +// Composables +import { createApp } from "vue"; + +// Plugins +import { registerPlugins } from "@/plugins"; + +const app = createApp(App); + +registerPlugins(app); + +app.mount("#app"); diff --git a/frontend/src/plugins/index.ts b/frontend/src/plugins/index.ts new file mode 100644 index 00000000..e51ba824 --- /dev/null +++ b/frontend/src/plugins/index.ts @@ -0,0 +1,16 @@ +/** + * plugins/index.ts + * + * Automatically included in `./src/main.ts` + */ + +// Plugins +import vuetify from "./vuetify"; +import router from "../router"; + +// Types +import type { App } from "vue"; + +export function registerPlugins(app: App) { + app.use(vuetify).use(router); +} diff --git a/frontend/src/plugins/theme.ts b/frontend/src/plugins/theme.ts new file mode 100644 index 00000000..4bf53918 --- /dev/null +++ b/frontend/src/plugins/theme.ts @@ -0,0 +1,104 @@ +import { + Scheme, + argbFromHex, + themeFromSourceColor, + hexFromArgb, + alphaFromArgb, + redFromArgb, + greenFromArgb, + blueFromArgb, + argbFromRgb +} from "@material/material-color-utilities"; + +interface ThemeColors { + [key: string]: string; +} + +const commonColors = { + "issue-open": "#00BA39", + "issue-closed": "#FF0036", + "issue-incoming": "#00C6EB", + "issue-outgoing": "#FF8900" +}; + +const mainColorMappings: [string, keyof Scheme][] = [ + ["primary", "primary"], + ["on-primary", "onPrimary"], + ["secondary", "secondary"], + ["on-secondary", "onSecondary"], + ["tertiary", "tertiary"], + ["on-tertiary", "onTertiary"], + ["error", "error"], + ["on-error", "onError"], + ["primary-container", "primaryContainer"], + ["on-primary-container", "onPrimaryContainer"], + ["secondary-container", "secondaryContainer"], + ["on-secondary-container", "onSecondaryContainer"], + ["tertiary-container", "tertiaryContainer"], + ["on-tertiary-container", "onTertiaryContainer"], + ["error-container", "errorContainer"], + ["on-error-container", "onErrorContainer"], + ["outline", "outline"], + ["outline-variant", "outlineVariant"] +]; + +const surfaceColorMappings: [string, number, number][] = [ + ["surface-container-lowest", 100, 4], + ["surface-container-low", 96, 10], + ["surface-container", 94, 12], + ["surface-container-high", 92, 17], + ["surface-container-highest", 90, 22], + ["surface", 98, 6], + ["on-surface", 10, 90], + ["surface-variant", 90, 30], + ["on-surface-variant", 30, 80], + ["background", 98, 6], + ["on-background", 10, 90], + ["inverse-surface", 20, 90], + ["on-inverse-surface", 95, 20] +]; + +const elevationMappings = [1, 3, 6, 8, 12]; + +function splitColor(color: number): [number, number, number, number] { + return [alphaFromArgb(color), redFromArgb(color), greenFromArgb(color), blueFromArgb(color)]; +} + +function calculateElevatedSurfaceColor(surface: number, tint: number, elevation: number): number { + const [_alpha, red, green, blue] = splitColor(surface); + const [_tintAlpha, tintRed, tintGreen, tintBlue] = splitColor(tint); + const elevationDp = elevationMappings[elevation - 1]; + const tintAlpha = (4.5 * Math.log(elevationDp + 1) + 2) / 100; + const newRed = Math.round((1 - tintAlpha) * red + tintAlpha * tintRed); + const newGreen = Math.round((1 - tintAlpha) * green + tintAlpha * tintGreen); + const newBlue = Math.round((1 - tintAlpha) * blue + tintAlpha * tintBlue); + return argbFromRgb(newRed, newGreen, newBlue); +} + +export function generateThemeColors(color: string, dark: boolean): ThemeColors { + const theme = themeFromSourceColor(argbFromHex(color)); + const scheme = dark ? theme.schemes.dark : theme.schemes.light; + const colors: ThemeColors = {}; + mainColorMappings.forEach(([key, value]) => { + colors[key] = hexFromArgb(scheme[value] as number); + }); + surfaceColorMappings.forEach(([key, lightValue, darkValue]) => { + const value = dark ? darkValue : lightValue; + colors[key] = hexFromArgb(theme.palettes.neutral.tone(value)); + }); + + const surfaceValue = dark ? 6 : 98; + const surface = theme.palettes.neutral.tone(surfaceValue); + const tintValue = dark ? 80 : 40; + const tint = theme.palettes.primary.tone(tintValue); + + for (let i = 1; i <= 5; i++) { + colors[`surface-elevated-${i}`] = hexFromArgb(calculateElevatedSurfaceColor(surface, tint, i)); + colors[`on-surface-elevated-${i}`] = colors[`on-surface`]; + } + + return { + ...colors, + ...commonColors + }; +} diff --git a/frontend/src/plugins/vuetify.ts b/frontend/src/plugins/vuetify.ts new file mode 100644 index 00000000..0ff015a5 --- /dev/null +++ b/frontend/src/plugins/vuetify.ts @@ -0,0 +1,116 @@ +import "@mdi/font/css/materialdesignicons.css"; +import "vuetify/styles"; +import { createVuetify } from "vuetify"; +import { md3 } from "vuetify/blueprints"; +import { generateThemeColors } from "./theme"; +import { VBtn, VContainer, VListItemTitle } from "vuetify/lib/components/index.mjs"; +import { aliases, mdi } from "vuetify/iconsets/mdi"; + +const themeIndependentVariables = { + "hover-opacity": 0.08 +}; + +export default createVuetify({ + theme: { + themes: { + light: { + colors: generateThemeColors("#2196f3", false), + variables: { + ...themeIndependentVariables + } + }, + dark: { + colors: generateThemeColors("#2196f3", true), + variables: { + ...themeIndependentVariables + } + } + }, + variations: false + }, + blueprint: md3, + aliases: { + FAB: VBtn, + SmallFAB: VBtn, + IconButton: VBtn, + DefaultButton: VBtn + }, + defaults: { + VTextField: { + variant: "outlined", + density: "comfortable", + color: "primary" + }, + VTextarea: { + variant: "outlined", + density: "comfortable", + color: "primary" + }, + VSelect: { + variant: "outlined", + density: "comfortable" + }, + VAutocomplete: { + variant: "outlined", + density: "comfortable" + }, + VCheckbox: { + density: "comfortable", + color: "primary" + }, + VTooltip: { + openDelay: 250 + }, + DefaultButton: { + height: "40px", + color: "primary", + rounded: "xl" + }, + FAB: { + width: "56px", + height: "56px", + minWidth: 0, + rounded: "lger" + }, + SmallFAB: { + width: "40px", + height: "40px", + minWidth: 0, + rounded: "lg" + }, + IconButton: { + icon: true, + variant: "text", + color: "tertiary", + density: "comfortable" + }, + VChip: { + rounded: "pill" + }, + VBtnToggle: { + variant: "outlined", + color: "primary", + divided: true, + border: 5 + }, + VSwitch: { + color: "primary", + inset: true, + density: "comfortable", + hideDetails: true + } + }, + components: { + VContainer, + VListItemTitle + }, + icons: { + defaultSet: "mdi", + aliases: { + ...aliases + }, + sets: { + mdi + } + } +}); diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts new file mode 100644 index 00000000..3c3e53c9 --- /dev/null +++ b/frontend/src/router/index.ts @@ -0,0 +1,27 @@ +// Composables +import { RouteRecordRaw, createRouter, createWebHistory } from "vue-router"; + +const routes: RouteRecordRaw[] = [ + { + path: "/login", + name: "login", + component: () => import("../views/Login.vue"), + }, + { + path: "/logout", + name: "logout", + component: () => import("../views/Logout.vue") + }, + { + path: "/register", + name: "register", + component: () => import("../views/Register.vue"), + } +]; + +const router = createRouter({ + history: createWebHistory(process.env.BASE_URL), + routes +}); + +export default router; diff --git a/frontend/src/styles/settings.scss b/frontend/src/styles/settings.scss new file mode 100644 index 00000000..77fc4ca0 --- /dev/null +++ b/frontend/src/styles/settings.scss @@ -0,0 +1,45 @@ +/** + * src/styles/settings.scss + * + * Configures SASS variables and Vuetify overwrites + */ + +// https://vuetifyjs.com/features/sass-variables/` +@use "vuetify/settings" with ( + $color-pack: false, + $rounded: ( + "lger": 16px + ), + $typography: ( + 'button': ( + 'text-transform': none + ), + 'subtitle-1': ( + 'weight': 500 + ), + ), + $icon-sizes: ( + "x-large": 2.5em + ), + + $border-color-root: rgb(var(--v-theme-outline)), + + $tooltip-background-color: rgb(var(--v-theme-inverse-surface)), + $tooltip-text-color: rgb(var(--v-theme-on-inverse-surface)), + + $divider-border-color: rgb(var(--v-theme-outline-variant)), + $divider-opacity: 1, + + $field-outline-opacity: 1, + + $switch-track-opacity: 1, + $switch-track-background: rgb(var(--v-theme-surface-container-highest)), + + $stepper-header-elevation: 0, + + $sheet-border-color: rgb(var(--v-theme-outline-variant)), +); + +$icon-with-number-width: 70px; + +$side-bar-width: 80px; diff --git a/frontend/src/util/types.ts b/frontend/src/util/types.ts new file mode 100644 index 00000000..709d3b1f --- /dev/null +++ b/frontend/src/util/types.ts @@ -0,0 +1,3 @@ +export type IdObject = { id: string }; + +export type ValueOf = T[keyof T]; diff --git a/frontend/src/util/vuetifyFormConfig.ts b/frontend/src/util/vuetifyFormConfig.ts new file mode 100644 index 00000000..b1de733b --- /dev/null +++ b/frontend/src/util/vuetifyFormConfig.ts @@ -0,0 +1,9 @@ +import { InputBindsConfig } from "vee-validate"; + +export const fieldConfig: any = { + props: (state: any) => { + return { + "error-messages": state.errors + }; + } +}; diff --git a/frontend/src/util/withErrorMessage.ts b/frontend/src/util/withErrorMessage.ts new file mode 100644 index 00000000..c515d6fc --- /dev/null +++ b/frontend/src/util/withErrorMessage.ts @@ -0,0 +1,34 @@ +import { Ref, ref } from "vue"; + +export async function withErrorMessage( + action: () => Promise, + message: string | ((error: unknown) => string) +): Promise { + try { + return await action(); + } catch (error) { + if (typeof message === "function") { + //pushErrorMessage(message(error)); + } else { + //pushErrorMessage(message); + } + console.error(error); + throw error; + } +} + +export function useBlockingWithErrorMessage(): [typeof withErrorMessage, Ref] { + const blocking = ref(false); + const blockingWithErrorMessage = async ( + action: () => Promise, + message: string | ((error: unknown) => string) + ) => { + blocking.value = true; + try { + return await withErrorMessage(action, message); + } finally { + blocking.value = false; + } + }; + return [blockingWithErrorMessage, blocking]; +} diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue new file mode 100644 index 00000000..91c31bac --- /dev/null +++ b/frontend/src/views/Login.vue @@ -0,0 +1,288 @@ + + + diff --git a/frontend/src/views/Register.vue b/frontend/src/views/Register.vue new file mode 100644 index 00000000..592d517d --- /dev/null +++ b/frontend/src/views/Register.vue @@ -0,0 +1,105 @@ + + + diff --git a/frontend/src/views/RouterOnly.vue b/frontend/src/views/RouterOnly.vue new file mode 100644 index 00000000..4a8f5efc --- /dev/null +++ b/frontend/src/views/RouterOnly.vue @@ -0,0 +1 @@ + diff --git a/frontend/src/views/model.ts b/frontend/src/views/model.ts new file mode 100644 index 00000000..44b4622b --- /dev/null +++ b/frontend/src/views/model.ts @@ -0,0 +1,82 @@ +export interface StrategyInstanceBase { + type: "credential" | "redirect"; + id: string; + name: string; + isLoginActive: boolean; + isSelfRegisterActive: boolean; + isSyncActive: boolean; + doesImplicitRegister: boolean; +} + +export interface CredentialStrategyInstance extends StrategyInstanceBase { + type: "credential"; + loginFields: LoginStrategyVariable[]; + registerFields: LoginStrategyVariable[]; +} + +export interface RedirectStrategyInstance extends StrategyInstanceBase { + type: "redirect"; +} + +export type StrategyInstance = CredentialStrategyInstance | RedirectStrategyInstance; + +export interface GroupedStrategyInstances { + credential: CredentialStrategyInstance[]; + redirect: RedirectStrategyInstance[]; +} + +export interface LoginStrategyVariable { + name: string; + displayName?: string; + type: "boolean" | "number" | "object" | "string" | "password"; + nullable?: boolean; +} + +export interface LoginStrategy { + typeName: string; + canLoginRegister: boolean; + canSync: boolean; + needsRedirectFlow: boolean; + allowsImplicitSignup: boolean; + acceptsVariables: { [name: string]: LoginStrategyVariable }; +} + +export interface LoginStrategyInstance { + id: string; + name: string; + type: string; + isLoginActive: boolean; + isSelfRegisterActive: boolean; + isSyncActive: boolean; + doesImplicitRegister: boolean; +} + +export interface OAuthRespose { + access_token: string; + token_type: string; + expires_in: number; + refresh_token: string; + scope: string; +} + +export enum TokenScope { + LOGIN_SERVICE = "login", + LOGIN_SERVICE_REGISTER = "login-register", + BACKEND = "backend", + REFRESH_TOKEN = "token", + NONE = "none" +} + +export enum UserDataSuggestionStatus { + ALREADY_REGISTERED = "already-registered", + USERNAME_TAKEN = "username-taken", + NO_DATA = "no-data", + OK = "ok" +} + +export interface UserDataSuggestionResponse { + status: UserDataSuggestionStatus; + username?: string; + displayName?: string; + email?: string; +} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 00000000..899b0bc9 --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1,7 @@ +/// + +declare module "*.vue" { + import type { DefineComponent } from "vue"; + const component: DefineComponent<{}, {}, any>; + export default component; +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 00000000..4eb7a6ef --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "target": "esnext", + "useDefineForClassFields": true, + "allowSyntheticDefaultImports": true, + "composite": true, + "module": "esnext", + "moduleResolution": "node", + "strict": true, + "jsx": "preserve", + "sourceMap": false, + "resolveJsonModule": true, + "esModuleInterop": true, + "lib": ["esnext", "dom"], + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "types": ["node", "vuetify"], + "paths": { + "@/*": ["src/*"] + }, + "skipLibCheck": true + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "vite.config.ts"], + "vueCompilerOptions": { + "strictTemplates": false + } +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 00000000..13b35d0b --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.mts b/frontend/vite.config.mts new file mode 100644 index 00000000..3e11d2e7 --- /dev/null +++ b/frontend/vite.config.mts @@ -0,0 +1,68 @@ +// Plugins +import vue from "@vitejs/plugin-vue"; +import vuetify, { transformAssetUrls } from "vite-plugin-vuetify"; +import Fonts from "unplugin-fonts/vite"; + +// Utilities +import { defineConfig } from "vite"; +import { fileURLToPath, URL } from "node:url"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + vue({ + template: { + transformAssetUrls, + compilerOptions: { + isCustomElement: (tag) => ["task-lists", "relative-time"].includes(tag) + } + } + }), + // https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vite-plugin + vuetify({ + autoImport: true, + styles: { + configFile: "src/styles/settings.scss" + } + }), + Fonts({ + fontsource: { + families: [ + { + name: "Roboto", + weights: [100, 300, 400, 500, 700, 900] + } + ] + } + }) + ], + define: { "process.env": {} }, + resolve: { + alias: { + "@": fileURLToPath(new URL("./src", import.meta.url)) + }, + extensions: [".js", ".json", ".jsx", ".mjs", ".ts", ".tsx", ".vue"] + }, + server: { + port: 4200, + proxy: { + "/api/graphql": { + target: "http://localhost:8080/graphql", + changeOrigin: true, + secure: false, + ws: true, + ignorePath: true + }, + "/api/login": { + target: "http://localhost:3000", + changeOrigin: true, + secure: false, + ws: true, + rewrite: (path) => path.replace(/^\/api\/login/, "") + } + } + }, + optimizeDeps: { + exclude: ["vuetify", "@github/task-lists-element"] + } +}); From 8d5b236dcfbec77c4d283e23226279f3b20bcdee Mon Sep 17 00:00:00 2001 From: nk-coding Date: Fri, 5 Jul 2024 13:03:24 +0200 Subject: [PATCH 02/31] refactoring progress --- ...dleware.ts => auth-autorize.middleware.ts} | 2 +- ...roller.ts => auth-endpoints.controller.ts} | 0 ...dleware.ts => auth-redirect.middleware.ts} | 2 +- .../src/api-internal/auth-server.module.ts | 15 +-- .../src/api-internal/auth-token.controller.ts | 4 +- ...middleware.ts => auth-token.middleware.ts} | 6 +- .../post-credentials.middleware.ts | 2 +- .../OAuthHttpException.ts} | 0 backend/src/api-oauth/oauth.middleware.ts | 113 ++++++++++++++++++ .../token-authorization-code.middleware.ts | 2 +- backend/src/strategies/AuthResult.ts | 2 + 11 files changed, 130 insertions(+), 18 deletions(-) rename backend/src/api-internal/{oauth-autorize.middleware.ts => auth-autorize.middleware.ts} (96%) rename backend/src/api-internal/{oauth-endpoints.controller.ts => auth-endpoints.controller.ts} (100%) rename backend/src/api-internal/{oauth-redirect.middleware.ts => auth-redirect.middleware.ts} (98%) rename backend/src/api-internal/{oauth-token.middleware.ts => auth-token.middleware.ts} (96%) rename backend/src/{api-internal/OauthHttpException.ts => api-oauth/OAuthHttpException.ts} (100%) create mode 100644 backend/src/api-oauth/oauth.middleware.ts rename backend/src/{api-internal => api-oauth}/token-authorization-code.middleware.ts (97%) diff --git a/backend/src/api-internal/oauth-autorize.middleware.ts b/backend/src/api-internal/auth-autorize.middleware.ts similarity index 96% rename from backend/src/api-internal/oauth-autorize.middleware.ts rename to backend/src/api-internal/auth-autorize.middleware.ts index 77f2f079..32f12b3f 100644 --- a/backend/src/api-internal/oauth-autorize.middleware.ts +++ b/backend/src/api-internal/auth-autorize.middleware.ts @@ -13,7 +13,7 @@ export interface OauthServerStateData { } @Injectable() -export class OauthAutorizeMiddleware implements NestMiddleware { +export class AuthAutorizeMiddleware implements NestMiddleware { constructor(private readonly authClientService: AuthClientService) {} async use(req: Request, res: Response, next: () => void) { diff --git a/backend/src/api-internal/oauth-endpoints.controller.ts b/backend/src/api-internal/auth-endpoints.controller.ts similarity index 100% rename from backend/src/api-internal/oauth-endpoints.controller.ts rename to backend/src/api-internal/auth-endpoints.controller.ts diff --git a/backend/src/api-internal/oauth-redirect.middleware.ts b/backend/src/api-internal/auth-redirect.middleware.ts similarity index 98% rename from backend/src/api-internal/oauth-redirect.middleware.ts rename to backend/src/api-internal/auth-redirect.middleware.ts index 1769301d..07a6bc55 100644 --- a/backend/src/api-internal/oauth-redirect.middleware.ts +++ b/backend/src/api-internal/auth-redirect.middleware.ts @@ -6,7 +6,7 @@ import { AuthClient } from "src/model/postgres/AuthClient.entity"; import { ActiveLoginService } from "src/model/services/active-login.service"; import { AuthClientService } from "src/model/services/auth-client.service"; import { AuthStateData } from "../strategies/AuthResult"; -import { OauthServerStateData } from "./oauth-autorize.middleware"; +import { OauthServerStateData } from "./auth-autorize.middleware"; @Injectable() export class OauthRedirectMiddleware implements NestMiddleware { diff --git a/backend/src/api-internal/auth-server.module.ts b/backend/src/api-internal/auth-server.module.ts index a56e76bd..a76033f1 100644 --- a/backend/src/api-internal/auth-server.module.ts +++ b/backend/src/api-internal/auth-server.module.ts @@ -5,21 +5,19 @@ import { ErrorHandlerMiddleware } from "../strategies/error-handler.middleware"; import { ModeExtractorMiddleware } from "../strategies/mode-extractor.middleware"; import { StrategiesMiddleware } from "../strategies/strategies.middleware"; import { StrategiesModule } from "../strategies/strategies.module"; -import { OauthAutorizeMiddleware } from "./oauth-autorize.middleware"; -import { OauthEndpointsController } from "./oauth-endpoints.controller"; -import { OauthRedirectMiddleware } from "./oauth-redirect.middleware"; +import { AuthAutorizeMiddleware } from "./auth-autorize.middleware"; +import { OauthEndpointsController } from "./auth-endpoints.controller"; +import { OauthRedirectMiddleware } from "./auth-redirect.middleware"; import { AuthTokenController } from "./auth-token.controller"; -import { OauthTokenMiddleware } from "./oauth-token.middleware"; +import { OauthTokenMiddleware } from "./auth-token.middleware"; import { PostCredentialsMiddleware } from "./post-credentials.middleware"; -import { TokenAuthorizationCodeMiddleware } from "./token-authorization-code.middleware"; @Module({ imports: [ModelModule, BackendServicesModule, StrategiesModule], providers: [ - OauthAutorizeMiddleware, + AuthAutorizeMiddleware, OauthRedirectMiddleware, OauthTokenMiddleware, - TokenAuthorizationCodeMiddleware, PostCredentialsMiddleware, ], controllers: [AuthTokenController, OauthEndpointsController], @@ -28,10 +26,9 @@ export class AuthServerModule { private middlewares: { middlewares: NestMiddleware[]; path: string }[] = []; constructor( - private readonly oauthAutorize: OauthAutorizeMiddleware, + private readonly oauthAutorize: AuthAutorizeMiddleware, private readonly oauthRedirect: OauthRedirectMiddleware, private readonly oauthToken: OauthTokenMiddleware, - private readonly tokenAuthorizationCode: TokenAuthorizationCodeMiddleware, private readonly modeExtractor: ModeExtractorMiddleware, private readonly strategies: StrategiesMiddleware, private readonly errorHandler: ErrorHandlerMiddleware, diff --git a/backend/src/api-internal/auth-token.controller.ts b/backend/src/api-internal/auth-token.controller.ts index 20fab4ca..2dcbb77f 100644 --- a/backend/src/api-internal/auth-token.controller.ts +++ b/backend/src/api-internal/auth-token.controller.ts @@ -10,8 +10,8 @@ import { AuthClientService } from "src/model/services/auth-client.service"; import { OpenApiTag } from "src/openapi-tag"; import { AuthStateData } from "src/strategies/AuthResult"; import { ensureState } from "src/strategies/utils"; -import { OauthServerStateData } from "./oauth-autorize.middleware"; -import { OauthHttpException } from "./OauthHttpException"; +import { OauthServerStateData } from "./auth-autorize.middleware"; +import { OauthHttpException } from "../api-oauth/OAuthHttpException"; export interface OauthTokenEndpointResponseDto { access_token: string; diff --git a/backend/src/api-internal/oauth-token.middleware.ts b/backend/src/api-internal/auth-token.middleware.ts similarity index 96% rename from backend/src/api-internal/oauth-token.middleware.ts rename to backend/src/api-internal/auth-token.middleware.ts index 0df8120a..b3f4dcb5 100644 --- a/backend/src/api-internal/oauth-token.middleware.ts +++ b/backend/src/api-internal/auth-token.middleware.ts @@ -5,12 +5,12 @@ import { AuthClient } from "src/model/postgres/AuthClient.entity"; import { AuthClientService } from "src/model/services/auth-client.service"; import { StrategiesMiddleware } from "src/strategies/strategies.middleware"; import { StrategiesService } from "src/model/services/strategies.service"; -import { TokenAuthorizationCodeMiddleware } from "./token-authorization-code.middleware"; +import { TokenAuthorizationCodeMiddleware } from "../api-oauth/token-authorization-code.middleware"; import * as bcrypt from "bcrypt"; import { ensureState } from "src/strategies/utils"; -import { OauthServerStateData } from "./oauth-autorize.middleware"; +import { OauthServerStateData } from "./auth-autorize.middleware"; import { PostCredentialsMiddleware } from "./post-credentials.middleware"; -import { OauthHttpException } from "./OauthHttpException"; +import { OauthHttpException } from "../api-oauth/OAuthHttpException"; @Injectable() export class OauthTokenMiddleware implements NestMiddleware { diff --git a/backend/src/api-internal/post-credentials.middleware.ts b/backend/src/api-internal/post-credentials.middleware.ts index 8bffc476..1219d9a3 100644 --- a/backend/src/api-internal/post-credentials.middleware.ts +++ b/backend/src/api-internal/post-credentials.middleware.ts @@ -3,7 +3,7 @@ import { Request, Response } from "express"; import { AuthStateData } from "src/strategies/AuthResult"; import { StrategiesMiddleware } from "src/strategies/strategies.middleware"; import { ensureState } from "src/strategies/utils"; -import { OauthHttpException } from "./OauthHttpException"; +import { OauthHttpException } from "../api-oauth/OAuthHttpException"; @Injectable() export class PostCredentialsMiddleware implements NestMiddleware { diff --git a/backend/src/api-internal/OauthHttpException.ts b/backend/src/api-oauth/OAuthHttpException.ts similarity index 100% rename from backend/src/api-internal/OauthHttpException.ts rename to backend/src/api-oauth/OAuthHttpException.ts diff --git a/backend/src/api-oauth/oauth.middleware.ts b/backend/src/api-oauth/oauth.middleware.ts new file mode 100644 index 00000000..04254b2d --- /dev/null +++ b/backend/src/api-oauth/oauth.middleware.ts @@ -0,0 +1,113 @@ +import { HttpException, HttpStatus, Injectable, Logger, NestMiddleware } from "@nestjs/common"; +import { Request, Response } from "express"; +import { AuthClient } from "src/model/postgres/AuthClient.entity"; +import { AuthClientService } from "src/model/services/auth-client.service"; +import { TokenAuthorizationCodeMiddleware } from "./token-authorization-code.middleware"; +import * as bcrypt from "bcrypt"; +import { ensureState } from "src/strategies/utils"; +import { OauthServerStateData } from "./auth-autorize.middleware"; +import { OauthHttpException } from "./OAuthHttpException"; + +@Injectable() +export class OauthTokenMiddleware implements NestMiddleware { + private readonly logger = new Logger(OauthTokenMiddleware.name); + + constructor( + private readonly authClientService: AuthClientService, + private readonly tokenResponseCodeMiddleware: TokenAuthorizationCodeMiddleware, + ) {} + + private async checkGivenClientSecretValidOrNotRequired(client: AuthClient, givenSecret?: string): Promise { + if (!client.requiresSecret && (!givenSecret || givenSecret.length == 0)) { + return true; + } + const hasCorrectClientSecret = ( + await Promise.all( + client.clientSecrets.map((hashedSecret) => + bcrypt.compare(givenSecret, hashedSecret.substring(hashedSecret.indexOf(";") + 1)), + ), + ) + ).includes(true); + if (hasCorrectClientSecret) { + return true; + } + return false; + } + + /** + * Performs the OAuth client authentication by checking the given client_id and client_secret + * in the Authorization header and in the body (both allowed according to OAuth spec). + * + * Flag can be set to return any client without secrets if desired to allow logins without client + * @param req The request object + * @returns The auth client that requested (or any without secret if flag ist set) + * or `null` if credentials invalid or none given + */ + private async getCallingClient(req: Request,): Promise { + const auth_head = req.headers["authorization"]; + if (auth_head && auth_head.startsWith("Basic ")) { + const clientIdSecret = Buffer.from(auth_head.substring(6), "base64") + ?.toString("utf-8") + ?.split(":") + ?.map((text) => decodeURIComponent(text)); + + if (clientIdSecret && clientIdSecret.length == 2) { + const client = await this.authClientService.findOneBy({ + id: clientIdSecret[0], + }); + if (client && client.isValid) { + if (this.checkGivenClientSecretValidOrNotRequired(client, clientIdSecret[1])) { + return client; + } + } + return null; + } + } + + if (req.body.client_id) { + const client = await this.authClientService.findOneBy({ + id: req.body.client_id, + }); + if (client && client.isValid) { + if (this.checkGivenClientSecretValidOrNotRequired(client, req.body.client_secret)) { + return client; + } + } + return null; + } + + return null; + } + + async use(req: Request, res: Response, next: () => void) { + ensureState(res); + + const grant_type = req.body.grant_type; + + const client = await this.getCallingClient(req); + if (!client) { + throw new OauthHttpException("unauthorized_client", "Unknown client or invalid client credentials"); + } + (res.locals.state as OauthServerStateData).client = client; + + switch (grant_type) { + case "refresh_token": //Request for new token using refresh token + //Fallthrough as resfrehsh token works the same as the initial code (both used to obtain new access token) + case "authorization_code": //Request for token based on obtained code + await this.tokenResponseCodeMiddleware.use(req, res, () => { + next(); + }); + break; + case "password": // Deprecated => not supported + case "client_credentials": //Request for token for stuff on client => not supported + default: + throw new HttpException( + { + error: "unsupported_grant_type", + error_description: "No grant_type given or unsupported type", + }, + HttpStatus.BAD_REQUEST, + ); + } + } +} diff --git a/backend/src/api-internal/token-authorization-code.middleware.ts b/backend/src/api-oauth/token-authorization-code.middleware.ts similarity index 97% rename from backend/src/api-internal/token-authorization-code.middleware.ts rename to backend/src/api-oauth/token-authorization-code.middleware.ts index c246463b..2201a09f 100644 --- a/backend/src/api-internal/token-authorization-code.middleware.ts +++ b/backend/src/api-oauth/token-authorization-code.middleware.ts @@ -5,7 +5,7 @@ import { ActiveLogin } from "src/model/postgres/ActiveLogin.entity"; import { ActiveLoginService } from "src/model/services/active-login.service"; import { AuthStateData } from "src/strategies/AuthResult"; import { ensureState } from "src/strategies/utils"; -import { OauthServerStateData } from "./oauth-autorize.middleware"; +import { OauthServerStateData } from "../api-internal/auth-autorize.middleware"; @Injectable() export class TokenAuthorizationCodeMiddleware implements NestMiddleware { diff --git a/backend/src/strategies/AuthResult.ts b/backend/src/strategies/AuthResult.ts index e790484b..be072994 100644 --- a/backend/src/strategies/AuthResult.ts +++ b/backend/src/strategies/AuthResult.ts @@ -6,6 +6,8 @@ export enum AuthFunction { LOGIN = "LOGIN", REGISTER = "REG", REGISTER_WITH_SYNC = "REG_SYNC", + REGISTER_ADDITIONAL = "REG_ADD", + REGISTER_ADDITIONAL_WITH_SYNC = "REG_ADD_SYNC", } export interface AuthStateData { From bb4a40d631cc0d5a04446ff38d80a54789cbc8cc Mon Sep 17 00:00:00 2001 From: nk-coding Date: Mon, 8 Jul 2024 03:18:48 +0200 Subject: [PATCH 03/31] progress --- backend/build.gradle.kts | 3 - .../api-internal/auth-autorize.middleware.ts | 7 --- .../api-internal/auth-endpoints.controller.ts | 27 ++++++-- .../src/api-internal/auth-token.middleware.ts | 4 +- .../{auth-server.module.ts => auth.module.ts} | 9 ++- .../mode-extractor.middleware.ts | 4 +- backend/src/api-login/api-login.module.ts | 2 +- .../OAuthAuthorizeServerStateData.ts | 16 +++++ backend/src/api-oauth/api-oauth.module.ts | 48 ++++++++++++++ .../oauth-authorize-extract.middleware.ts | 32 ++++++++++ .../oauth-authorize-redirect.middleware.ts | 27 ++++++++ .../oauth-authorize-validate.middleware.ts | 42 +++++++++++++ .../api-oauth/oauth-authorize.controller.ts | 62 +++++++++++++++++++ ...th-token-authorization-code.middleware.ts} | 37 +++++------ .../oauth-token.controller.ts} | 13 ++-- ...iddleware.ts => oauth-token.middleware.ts} | 9 ++- backend/src/api-oauth/state.middleware.ts | 39 ++++++++++++ backend/src/app.module.ts | 23 ++++--- backend/src/backend-services/token.service.ts | 28 ++++++++- backend/src/openapi-tag.ts | 1 + backend/src/strategies/strategies.module.ts | 2 +- 21 files changed, 362 insertions(+), 73 deletions(-) delete mode 100644 backend/build.gradle.kts rename backend/src/api-internal/{auth-server.module.ts => auth.module.ts} (88%) rename backend/src/{strategies => api-internal}/mode-extractor.middleware.ts (89%) create mode 100644 backend/src/api-oauth/OAuthAuthorizeServerStateData.ts create mode 100644 backend/src/api-oauth/api-oauth.module.ts create mode 100644 backend/src/api-oauth/oauth-authorize-extract.middleware.ts create mode 100644 backend/src/api-oauth/oauth-authorize-redirect.middleware.ts create mode 100644 backend/src/api-oauth/oauth-authorize-validate.middleware.ts create mode 100644 backend/src/api-oauth/oauth-authorize.controller.ts rename backend/src/api-oauth/{token-authorization-code.middleware.ts => oauth-token-authorization-code.middleware.ts} (62%) rename backend/src/{api-internal/auth-token.controller.ts => api-oauth/oauth-token.controller.ts} (95%) rename backend/src/api-oauth/{oauth.middleware.ts => oauth-token.middleware.ts} (90%) create mode 100644 backend/src/api-oauth/state.middleware.ts diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts deleted file mode 100644 index 426527d1..00000000 --- a/backend/build.gradle.kts +++ /dev/null @@ -1,3 +0,0 @@ -plugins { - id("com.github.node-gradle.node") -} \ No newline at end of file diff --git a/backend/src/api-internal/auth-autorize.middleware.ts b/backend/src/api-internal/auth-autorize.middleware.ts index 32f12b3f..9aaaac63 100644 --- a/backend/src/api-internal/auth-autorize.middleware.ts +++ b/backend/src/api-internal/auth-autorize.middleware.ts @@ -5,13 +5,6 @@ import { AuthClientService } from "src/model/services/auth-client.service"; import { AuthStateData } from "../strategies/AuthResult"; import { ensureState } from "../strategies/utils"; -export interface OauthServerStateData { - state?: string; - redirect?: string; - clientId?: string; - client?: AuthClient; -} - @Injectable() export class AuthAutorizeMiddleware implements NestMiddleware { constructor(private readonly authClientService: AuthClientService) {} diff --git a/backend/src/api-internal/auth-endpoints.controller.ts b/backend/src/api-internal/auth-endpoints.controller.ts index ebf9c81e..b10b96df 100644 --- a/backend/src/api-internal/auth-endpoints.controller.ts +++ b/backend/src/api-internal/auth-endpoints.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, HttpException, HttpStatus, Param } from "@nestjs/common"; +import { Controller, Get, HttpException, HttpStatus, Param, Post } from "@nestjs/common"; import { ApiOperation, ApiParam, ApiTags } from "@nestjs/swagger"; import { OpenApiTag } from "src/openapi-tag"; import { AuthFunctionInput } from "./dto/auth-function.dto"; @@ -10,8 +10,8 @@ import { AuthFunctionInput } from "./dto/auth-function.dto"; * - Authorize endpoint * - Redirect/Callback endpoint */ -@Controller("oauth") -export class OauthEndpointsController { +@Controller("auth") +export class AuthEndpointsController { /** * Authorize endpoint for strategy instance of the given id. * Functionality performed is determined by mode parameter. @@ -19,7 +19,7 @@ export class OauthEndpointsController { * For defined behaviour of the authorize endpoint see {@link https://www.rfc-editor.org/rfc/rfc6749} * */ - @Get(":id/authorize/:mode?") + @Get("redirect/:id/:mode") @ApiOperation({ summary: "Authorize endpoint for a strategy instance" }) @ApiParam({ name: "id", type: String, description: "The id of the strategy instance to initiate" }) @ApiParam({ @@ -42,7 +42,7 @@ export class OauthEndpointsController { * Not meant to be called by a client. * Meant as callback for oauth flows started by the login-service */ - @Get(":id/callback") + @Get("callback/:id") @ApiOperation({ summary: "Redirect/Callback endpoint for a strategy instance" }) @ApiParam({ name: "id", @@ -55,4 +55,21 @@ export class OauthEndpointsController { HttpStatus.INTERNAL_SERVER_ERROR, ); } + + @Post("submit/:id/:mode") + @ApiOperation({ summary: "Submit endpoint for a strategy instance" }) + @ApiParam({ name: "id", type: String, description: "The id of the strategy instance to submit" }) + @ApiParam({ + name: "mode", + enum: AuthFunctionInput, + required: false, + description: "The function/mode how to authenticate. Defaults to 'login'", + }) + @ApiTags(OpenApiTag.INTERNAL_API) + submitEndpoint() { + throw new HttpException( + "This controller shouldn't be reached as all functionality is handeled in middleware", + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } } diff --git a/backend/src/api-internal/auth-token.middleware.ts b/backend/src/api-internal/auth-token.middleware.ts index b3f4dcb5..3a88d792 100644 --- a/backend/src/api-internal/auth-token.middleware.ts +++ b/backend/src/api-internal/auth-token.middleware.ts @@ -5,7 +5,7 @@ import { AuthClient } from "src/model/postgres/AuthClient.entity"; import { AuthClientService } from "src/model/services/auth-client.service"; import { StrategiesMiddleware } from "src/strategies/strategies.middleware"; import { StrategiesService } from "src/model/services/strategies.service"; -import { TokenAuthorizationCodeMiddleware } from "../api-oauth/token-authorization-code.middleware"; +import { OAuthTokenAuthorizationCodeMiddleware } from "../api-oauth/oauth-token-authorization-code.middleware"; import * as bcrypt from "bcrypt"; import { ensureState } from "src/strategies/utils"; import { OauthServerStateData } from "./auth-autorize.middleware"; @@ -19,7 +19,7 @@ export class OauthTokenMiddleware implements NestMiddleware { constructor( private readonly tokenService: TokenService, private readonly authClientService: AuthClientService, - private readonly tokenResponseCodeMiddleware: TokenAuthorizationCodeMiddleware, + private readonly tokenResponseCodeMiddleware: OAuthTokenAuthorizationCodeMiddleware, private readonly strategiesMiddleware: StrategiesMiddleware, private readonly postCredentialsMiddleware: PostCredentialsMiddleware, ) {} diff --git a/backend/src/api-internal/auth-server.module.ts b/backend/src/api-internal/auth.module.ts similarity index 88% rename from backend/src/api-internal/auth-server.module.ts rename to backend/src/api-internal/auth.module.ts index a76033f1..cc38d716 100644 --- a/backend/src/api-internal/auth-server.module.ts +++ b/backend/src/api-internal/auth.module.ts @@ -2,13 +2,12 @@ import { MiddlewareConsumer, Module, NestMiddleware } from "@nestjs/common"; import { BackendServicesModule } from "src/backend-services/backend-services.module"; import { ModelModule } from "src/model/model.module"; import { ErrorHandlerMiddleware } from "../strategies/error-handler.middleware"; -import { ModeExtractorMiddleware } from "../strategies/mode-extractor.middleware"; +import { ModeExtractorMiddleware } from "./mode-extractor.middleware"; import { StrategiesMiddleware } from "../strategies/strategies.middleware"; import { StrategiesModule } from "../strategies/strategies.module"; import { AuthAutorizeMiddleware } from "./auth-autorize.middleware"; -import { OauthEndpointsController } from "./auth-endpoints.controller"; +import { AuthEndpointsController } from "./auth-endpoints.controller"; import { OauthRedirectMiddleware } from "./auth-redirect.middleware"; -import { AuthTokenController } from "./auth-token.controller"; import { OauthTokenMiddleware } from "./auth-token.middleware"; import { PostCredentialsMiddleware } from "./post-credentials.middleware"; @@ -20,9 +19,9 @@ import { PostCredentialsMiddleware } from "./post-credentials.middleware"; OauthTokenMiddleware, PostCredentialsMiddleware, ], - controllers: [AuthTokenController, OauthEndpointsController], + controllers: [AuthEndpointsController], }) -export class AuthServerModule { +export class ApiInternalModule { private middlewares: { middlewares: NestMiddleware[]; path: string }[] = []; constructor( diff --git a/backend/src/strategies/mode-extractor.middleware.ts b/backend/src/api-internal/mode-extractor.middleware.ts similarity index 89% rename from backend/src/strategies/mode-extractor.middleware.ts rename to backend/src/api-internal/mode-extractor.middleware.ts index 03e6855f..d11e9899 100644 --- a/backend/src/strategies/mode-extractor.middleware.ts +++ b/backend/src/api-internal/mode-extractor.middleware.ts @@ -2,9 +2,9 @@ import { Injectable, NestMiddleware } from "@nestjs/common"; import { Request, Response } from "express"; import { ActiveLoginService } from "src/model/services/active-login.service"; import { StrategyInstanceService } from "src/model/services/strategy-instance.service"; -import { AuthFunction, AuthStateData } from "./AuthResult"; +import { AuthFunction, AuthStateData } from "../strategies/AuthResult"; import { StrategiesService } from "../model/services/strategies.service"; -import { ensureState } from "./utils"; +import { ensureState } from "../strategies/utils"; @Injectable() export class ModeExtractorMiddleware implements NestMiddleware { diff --git a/backend/src/api-login/api-login.module.ts b/backend/src/api-login/api-login.module.ts index e692a8cd..67ef0543 100644 --- a/backend/src/api-login/api-login.module.ts +++ b/backend/src/api-login/api-login.module.ts @@ -25,4 +25,4 @@ import { UsersController } from "./users.controller"; ], providers: [CheckRegistrationTokenService], }) -export class ApiLoginModule {} +export class AuthModule {} diff --git a/backend/src/api-oauth/OAuthAuthorizeServerStateData.ts b/backend/src/api-oauth/OAuthAuthorizeServerStateData.ts new file mode 100644 index 00000000..216cb51c --- /dev/null +++ b/backend/src/api-oauth/OAuthAuthorizeServerStateData.ts @@ -0,0 +1,16 @@ +import { AuthClient } from "src/model/postgres/AuthClient.entity"; + +export interface OAuthAuthorizeRequestData { + state?: string; + redirect: string; + clientId: string; + scope: string[]; + codeChallenge?: string; + codeChallengeMethod?: string; + responseType: "code"; +} + +export interface OAuthAuthorizeServerStateData { + request: OAuthAuthorizeRequestData; + client: AuthClient; +} \ No newline at end of file diff --git a/backend/src/api-oauth/api-oauth.module.ts b/backend/src/api-oauth/api-oauth.module.ts new file mode 100644 index 00000000..458c0607 --- /dev/null +++ b/backend/src/api-oauth/api-oauth.module.ts @@ -0,0 +1,48 @@ +import { MiddlewareConsumer, Module, NestMiddleware } from "@nestjs/common"; +import { ModelModule } from "src/model/model.module"; +import { OAuthAuthorizeExtractMiddleware } from "./oauth-authorize-extract.middleware"; +import { OauthTokenMiddleware as OAuthTokenMiddleware } from "./oauth-token.middleware"; +import { OAuthTokenAuthorizationCodeMiddleware } from "./oauth-token-authorization-code.middleware"; +import { OauthAuthorizeController as OAuthAuthorizeController } from "./oauth-authorize.controller"; +import { OAuthTokenController } from "./oauth-token.controller"; +import { OAuthAuthorizeValidateMiddleware } from "./oauth-authorize-validate.middleware"; +import { OAuthAuthorizeRedirectMiddleware } from "./oauth-authorize-redirect.middleware"; + +@Module({ + imports: [ModelModule], + providers: [ + OAuthAuthorizeExtractMiddleware, + OAuthAuthorizeValidateMiddleware, + OAuthAuthorizeRedirectMiddleware, + OAuthTokenMiddleware, + OAuthTokenAuthorizationCodeMiddleware, + ], + controllers: [OAuthAuthorizeController, OAuthTokenController], +}) +export class ApiOauthModule { + private middlewares: { middlewares: NestMiddleware[]; path: string }[] = []; + + constructor( + private readonly oauthAuthorizeExtract: OAuthAuthorizeExtractMiddleware, + private readonly oauthAuthorizeValidate: OAuthAuthorizeValidateMiddleware, + private readonly oauthAuthorizeRedirect: OAuthAuthorizeRedirectMiddleware, + private readonly oauthToken: OAuthTokenMiddleware, + private readonly oauthTokenAuthorizationCode: OAuthTokenAuthorizationCodeMiddleware, + ) { + this.middlewares.push({ + middlewares: [this.oauthAuthorizeExtract, this.oauthAuthorizeValidate, this.oauthAuthorizeRedirect], + path: "oauth/authorize", + }); + + this.middlewares.push({ + middlewares: [this.oauthToken, this.oauthTokenAuthorizationCode], + path: "oauth/token", + }); + } + + configure(consumer: MiddlewareConsumer) { + for (const chain of this.middlewares) { + consumer.apply(...chain.middlewares.map((m) => m.use.bind(m))).forRoutes(chain.path); + } + } +} diff --git a/backend/src/api-oauth/oauth-authorize-extract.middleware.ts b/backend/src/api-oauth/oauth-authorize-extract.middleware.ts new file mode 100644 index 00000000..eeac4584 --- /dev/null +++ b/backend/src/api-oauth/oauth-authorize-extract.middleware.ts @@ -0,0 +1,32 @@ +import { Injectable } from "@nestjs/common"; +import { Request, Response } from "express"; +import { StateMiddleware } from "./state.middleware"; +import { OAuthAuthorizeRequestData, OAuthAuthorizeServerStateData } from "./OAuthAuthorizeServerStateData"; +import { AuthClientService } from "src/model/services/auth-client.service"; + +@Injectable() +export class OAuthAuthorizeExtractMiddleware extends StateMiddleware<{}, OAuthAuthorizeServerStateData> { + constructor(private readonly authClientService: AuthClientService) { + super(); + } + + protected async useWithState( + req: Request, + res: Response, + state: { error?: any }, + next: (error?: Error | any) => void, + ) { + const requestParams: OAuthAuthorizeRequestData = { + state: req.query.state as string, + redirect: req.query.redirect as string, + clientId: req.query.client_id as string, + scope: (req.query.scope as string).split(" ").filter((s) => s.length > 0), + codeChallenge: req.query.code_challenge as string, + codeChallengeMethod: req.query.code_challenge_method as string, + responseType: req.query.response_type as "code", + }; + const client = await this.authClientService.findOneBy({ id: requestParams.clientId }); + this.appendState(res, { request: requestParams, client }); + next(); + } +} diff --git a/backend/src/api-oauth/oauth-authorize-redirect.middleware.ts b/backend/src/api-oauth/oauth-authorize-redirect.middleware.ts new file mode 100644 index 00000000..7cbfe1bf --- /dev/null +++ b/backend/src/api-oauth/oauth-authorize-redirect.middleware.ts @@ -0,0 +1,27 @@ +import { Injectable } from "@nestjs/common"; +import { Request, Response } from "express"; +import { StateMiddleware } from "./state.middleware"; +import { OAuthAuthorizeServerStateData } from "./OAuthAuthorizeServerStateData"; +import { TokenScope } from "src/backend-services/token.service"; + +@Injectable() +export class OAuthAuthorizeRedirectMiddleware extends StateMiddleware< + OAuthAuthorizeServerStateData, + OAuthAuthorizeServerStateData +> { + protected async useWithState( + req: Request, + res: Response, + state: OAuthAuthorizeServerStateData & { error?: any }, + next: (error?: Error | any) => void, + ): Promise { + const target = state.request.scope.includes(TokenScope.LOGIN_SERVICE_REGISTER) + ? "register-additional" + : "login"; + const encodedState = encodeURIComponent(JSON.stringify(state.request)); + res.status(302) + .setHeader("Location", `/auth/flow/${target}?state=${encodedState}`) + .setHeader("Content-Length", 0) + .end(); + } +} diff --git a/backend/src/api-oauth/oauth-authorize-validate.middleware.ts b/backend/src/api-oauth/oauth-authorize-validate.middleware.ts new file mode 100644 index 00000000..fc632323 --- /dev/null +++ b/backend/src/api-oauth/oauth-authorize-validate.middleware.ts @@ -0,0 +1,42 @@ +import { Injectable } from "@nestjs/common"; +import { Request, Response } from "express"; +import { StateMiddleware } from "./state.middleware"; +import { OAuthAuthorizeServerStateData } from "./OAuthAuthorizeServerStateData"; +import { OauthHttpException } from "./OAuthHttpException"; +import { TokenService } from "src/backend-services/token.service"; + +@Injectable() +export class OAuthAuthorizeValidateMiddleware extends StateMiddleware< + OAuthAuthorizeServerStateData, + OAuthAuthorizeServerStateData +> { + + constructor(private readonly tokenService: TokenService) { + super(); + } + + protected async useWithState( + req: Request, + res: Response, + state: OAuthAuthorizeServerStateData & { error?: any }, + next: (error?: Error | any) => void, + ): Promise { + if (!state.client || !state.client.isValid) { + throw new OauthHttpException("invalid_client", "Client unknown or unauthorized"); + } + if (state.request.responseType !== "code") { + throw new OauthHttpException("unsupported_response_type", "response_type must be set to 'code'"); + } + if (!state.request.redirect || !state.client.redirectUrls.includes(state.request.redirect)) { + throw new OauthHttpException("invalid_request", "Redirect URL not allowed"); + } + try { + this.tokenService.verifyScope(state.request.scope); + } catch (error) { + throw new OauthHttpException("invalid_scope", error.message); + } + //TODO validate PKCE + //TODO check if PKCE is required + next(); + } +} diff --git a/backend/src/api-oauth/oauth-authorize.controller.ts b/backend/src/api-oauth/oauth-authorize.controller.ts new file mode 100644 index 00000000..a745b867 --- /dev/null +++ b/backend/src/api-oauth/oauth-authorize.controller.ts @@ -0,0 +1,62 @@ +import { Controller, Get, HttpException, HttpStatus, Param, Query } from "@nestjs/common"; +import { ApiOperation, ApiParam, ApiQuery, ApiTags } from "@nestjs/swagger"; +import { OpenApiTag } from "src/openapi-tag"; + +/** + * Controller for the openapi generator to find the oauth server routes that are handeled exclusively in middleware. + * + * This includes: + * - Authorize endpoint + * - Redirect/Callback endpoint + */ +@Controller() +export class OauthAuthorizeController { + /** + * Authorize endpoint for strategy instance of the given id. + * Functionality performed is determined by mode parameter. + * + * For defined behaviour of the authorize endpoint see {@link https://www.rfc-editor.org/rfc/rfc6749} + * + */ + @Get("authorize") + @ApiOperation({ summary: "Authorize endpoint" }) + @ApiQuery({ name: "client_id", type: String, description: "The id of the client to initiate" }) + @ApiQuery({ + name: "response_type", + required: true, + description: "The response type to expect.", + }) + @ApiQuery({ + name: "redirect_uri", + required: true, + description: "The redirect uri to use. Must be one of the registered redirect uris of the client", + }) + @ApiQuery({ + name: "state", + required: false, + description: "The state to pass through the oauth flow", + }) + @ApiQuery({ + name: "scope", + required: true, + description: "The scope to request", + }) + @ApiQuery({ + name: "code_challenge", + required: false, + description: "The code challenge to use for PKCE", + }) + @ApiQuery({ + name: "code_challenge_method", + required: false, + description: "The code challenge method to use for PKCE", + }) + @ApiTags(OpenApiTag.OAUTH_API) + authorizeEndpoint() { + throw new HttpException( + "This controller shouldn't be reached as all functionality is handeled in middleware", + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + +} diff --git a/backend/src/api-oauth/token-authorization-code.middleware.ts b/backend/src/api-oauth/oauth-token-authorization-code.middleware.ts similarity index 62% rename from backend/src/api-oauth/token-authorization-code.middleware.ts rename to backend/src/api-oauth/oauth-token-authorization-code.middleware.ts index 2201a09f..62a91642 100644 --- a/backend/src/api-oauth/token-authorization-code.middleware.ts +++ b/backend/src/api-oauth/oauth-token-authorization-code.middleware.ts @@ -1,32 +1,28 @@ -import { HttpException, HttpStatus, Injectable, Logger, NestMiddleware } from "@nestjs/common"; +import { Injectable, Logger, NestMiddleware } from "@nestjs/common"; import { Request, Response } from "express"; import { ActiveLoginTokenResult, TokenService } from "src/backend-services/token.service"; -import { ActiveLogin } from "src/model/postgres/ActiveLogin.entity"; +import { AuthClient } from "src/model/postgres/AuthClient.entity"; import { ActiveLoginService } from "src/model/services/active-login.service"; import { AuthStateData } from "src/strategies/AuthResult"; import { ensureState } from "src/strategies/utils"; -import { OauthServerStateData } from "../api-internal/auth-autorize.middleware"; +import { OauthHttpException } from "./OAuthHttpException"; @Injectable() -export class TokenAuthorizationCodeMiddleware implements NestMiddleware { - private readonly logger = new Logger(TokenAuthorizationCodeMiddleware.name); +export class OAuthTokenAuthorizationCodeMiddleware implements NestMiddleware { + private readonly logger = new Logger(OAuthTokenAuthorizationCodeMiddleware.name); constructor(private readonly activeLoginService: ActiveLoginService, private readonly tokenService: TokenService) {} - private throwGenericCodeError(res: Response, next: () => void) { - (res.locals.state as AuthStateData).authErrorMessage = "Given code was invalid or expired"; - (res.locals.state as AuthStateData).authErrorType = "invalid_grant"; - return next(); + private throwGenericCodeError() { + throw new OauthHttpException("invalid_grant", "Given code was invalid or expired"); } async use(req: Request, res: Response, next: () => void) { ensureState(res); let tokenData: ActiveLoginTokenResult; - const currentClient = (res.locals.state as OauthServerStateData).client; + const currentClient = res.locals.state.client as AuthClient; if (!currentClient) { this.logger.warn("No client logged in"); - (res.locals.state as AuthStateData).authErrorMessage = "Client unknown or unauthorized"; - (res.locals.state as AuthStateData).authErrorType = "invalid_client"; - return; + throw new OauthHttpException("invalid_client", "Client unknown or unauthorized"); } try { tokenData = await this.tokenService.verifyActiveLoginToken( @@ -35,7 +31,7 @@ export class TokenAuthorizationCodeMiddleware implements NestMiddleware { ); } catch (err) { this.logger.warn(err); - return this.throwGenericCodeError(res, next); + return this.throwGenericCodeError(); } const activeLogin = await this.activeLoginService.findOneBy({ @@ -43,20 +39,20 @@ export class TokenAuthorizationCodeMiddleware implements NestMiddleware { }); if (!activeLogin) { this.logger.warn("No active login with id", tokenData.activeLoginId); - return this.throwGenericCodeError(res, next); + return this.throwGenericCodeError(); } const activeLoginClient = await activeLogin.createdByClient; if (activeLoginClient.id !== currentClient.id) { this.logger.warn("Active login was not created by current client", tokenData.activeLoginId); - return this.throwGenericCodeError(res, next); + return this.throwGenericCodeError(); } if (!activeLogin.isValid) { this.logger.warn("Active login set invalid", tokenData.activeLoginId); - return this.throwGenericCodeError(res, next); + return this.throwGenericCodeError(); } if (activeLogin.expires != null && activeLogin.expires <= new Date()) { this.logger.warn("Active login is expired", tokenData.activeLoginId); - return this.throwGenericCodeError(res, next); + return this.throwGenericCodeError(); } const codeUniqueId = parseInt(tokenData.tokenUniqueId, 10); if (!isFinite(codeUniqueId) || codeUniqueId !== activeLogin.nextExpectedRefreshTokenNumber) { @@ -68,10 +64,7 @@ export class TokenAuthorizationCodeMiddleware implements NestMiddleware { "Active login has been made invalid", tokenData.activeLoginId, ); - (res.locals.state as AuthStateData).authErrorMessage = - "Given code was liekely reused. Login and codes invalidated"; - (res.locals.state as AuthStateData).authErrorType = "invalid_grant"; - return next(); + throw new OauthHttpException("invalid_grant", "Given code was liekely reused. Login and codes invalidated"); } (res.locals.state as AuthStateData).activeLogin = activeLogin; next(); diff --git a/backend/src/api-internal/auth-token.controller.ts b/backend/src/api-oauth/oauth-token.controller.ts similarity index 95% rename from backend/src/api-internal/auth-token.controller.ts rename to backend/src/api-oauth/oauth-token.controller.ts index 2dcbb77f..0037a6a9 100644 --- a/backend/src/api-internal/auth-token.controller.ts +++ b/backend/src/api-oauth/oauth-token.controller.ts @@ -10,7 +10,6 @@ import { AuthClientService } from "src/model/services/auth-client.service"; import { OpenApiTag } from "src/openapi-tag"; import { AuthStateData } from "src/strategies/AuthResult"; import { ensureState } from "src/strategies/utils"; -import { OauthServerStateData } from "./auth-autorize.middleware"; import { OauthHttpException } from "../api-oauth/OAuthHttpException"; export interface OauthTokenEndpointResponseDto { @@ -21,10 +20,10 @@ export interface OauthTokenEndpointResponseDto { scope: string; } -@Controller("auth") -@ApiTags(OpenApiTag.INTERNAL_API) -export class AuthTokenController { - private readonly logger = new Logger(AuthTokenController.name); +@Controller() +@ApiTags(OpenApiTag.OAUTH_API) +export class OAuthTokenController { + private readonly logger = new Logger(OAuthTokenController.name); constructor( private readonly authClientService: AuthClientService, private readonly activeLoginService: ActiveLoginService, @@ -133,10 +132,10 @@ export class AuthTokenController { }; } - @Post(":id?/token/:mode?") + @Post("token") async token(@Res({ passthrough: true }) res: Response): Promise { ensureState(res); - const currentClient = (res.locals.state as OauthServerStateData).client; + const currentClient = res.locals.state.client as AuthClient; if (!currentClient) { throw new OauthHttpException( "invalid_client", diff --git a/backend/src/api-oauth/oauth.middleware.ts b/backend/src/api-oauth/oauth-token.middleware.ts similarity index 90% rename from backend/src/api-oauth/oauth.middleware.ts rename to backend/src/api-oauth/oauth-token.middleware.ts index 04254b2d..45f27893 100644 --- a/backend/src/api-oauth/oauth.middleware.ts +++ b/backend/src/api-oauth/oauth-token.middleware.ts @@ -2,10 +2,9 @@ import { HttpException, HttpStatus, Injectable, Logger, NestMiddleware } from "@ import { Request, Response } from "express"; import { AuthClient } from "src/model/postgres/AuthClient.entity"; import { AuthClientService } from "src/model/services/auth-client.service"; -import { TokenAuthorizationCodeMiddleware } from "./token-authorization-code.middleware"; +import { OAuthTokenAuthorizationCodeMiddleware } from "./oauth-token-authorization-code.middleware"; import * as bcrypt from "bcrypt"; import { ensureState } from "src/strategies/utils"; -import { OauthServerStateData } from "./auth-autorize.middleware"; import { OauthHttpException } from "./OAuthHttpException"; @Injectable() @@ -14,7 +13,7 @@ export class OauthTokenMiddleware implements NestMiddleware { constructor( private readonly authClientService: AuthClientService, - private readonly tokenResponseCodeMiddleware: TokenAuthorizationCodeMiddleware, + private readonly tokenResponseCodeMiddleware: OAuthTokenAuthorizationCodeMiddleware, ) {} private async checkGivenClientSecretValidOrNotRequired(client: AuthClient, givenSecret?: string): Promise { @@ -88,11 +87,11 @@ export class OauthTokenMiddleware implements NestMiddleware { if (!client) { throw new OauthHttpException("unauthorized_client", "Unknown client or invalid client credentials"); } - (res.locals.state as OauthServerStateData).client = client; + res.locals.state.client = client; switch (grant_type) { case "refresh_token": //Request for new token using refresh token - //Fallthrough as resfrehsh token works the same as the initial code (both used to obtain new access token) + //Fallthrough as resfresh token works the same as the initial code (both used to obtain new access token) case "authorization_code": //Request for token based on obtained code await this.tokenResponseCodeMiddleware.use(req, res, () => { next(); diff --git a/backend/src/api-oauth/state.middleware.ts b/backend/src/api-oauth/state.middleware.ts new file mode 100644 index 00000000..2a018927 --- /dev/null +++ b/backend/src/api-oauth/state.middleware.ts @@ -0,0 +1,39 @@ +import { NestMiddleware } from "@nestjs/common"; +import { ensureState } from "src/strategies/utils"; +import { Request, Response } from "express"; + +export abstract class StateMiddleware = {}, T extends Record = {}> + implements NestMiddleware +{ + async use(req: Request, res: Response, next: (error?: Error | any) => any) { + ensureState(res); + if (res.locals.state.error) { + this.useWithError(req, res, res.locals.state.error, next); + } else { + try { + await this.useWithState(req, res, res.locals.state, next); + } catch (error) { + this.appendState(res, { error } as any); + next(); + } + } + } + + protected abstract useWithState( + req: Request, + res: Response, + state: S & { error?: any }, + next: (error?: Error | any) => void, + ): Promise; + + /** + * Overwrite this to handle errors + */ + protected useWithError(req: Request, res: Response, error: any, next: (error?: Error | any) => void) { + next(); + } + + protected appendState(res: Response, appendedState: Partial & { error?: any }) { + res.locals.state = { ...res.locals.state, ...appendedState }; + } +} diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 26ebc2a2..07bfaef9 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,18 +1,19 @@ -import { DynamicModule, Module } from "@nestjs/common"; +import { Module } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; -import { LazyModuleLoader, RouterModule } from "@nestjs/core"; +import { RouterModule } from "@nestjs/core"; import { TypeOrmModule } from "@nestjs/typeorm"; -import { ApiLoginModule } from "./api-login/api-login.module"; +import { AuthModule } from "./api-login/api-login.module"; import { ApiSyncModule } from "./api-sync/api-sync.module"; import { ModelModule } from "./model/model.module"; import { StrategiesModule } from "./strategies/strategies.module"; import { BackendServicesModule } from "./backend-services/backend-services.module"; import { validationSchema } from "./configuration-validator"; -import { AuthServerModule } from "./api-internal/auth-server.module"; +import { ApiInternalModule } from "./api-internal/auth.module"; import { DefaultReturn } from "./default-return.dto"; import { InitializationModule } from "./initialization/initialization.module"; import * as path from "path"; import { ServeStaticModule } from "@nestjs/serve-static"; +import { ApiOauthModule } from "./api-oauth/api-oauth.module"; @Module({ imports: [ @@ -54,15 +55,17 @@ import { ServeStaticModule } from "@nestjs/serve-static"; exclude: ["/login", "/syncApi", "/authenticate"], }), ModelModule, - ApiLoginModule, + AuthModule, ApiSyncModule, StrategiesModule, - AuthServerModule, + ApiInternalModule, + ApiOauthModule, RouterModule.register([ - { path: "login", module: ApiLoginModule }, - { path: "login", module: StrategiesModule }, - { path: "sync", module: ApiSyncModule }, - { path: "internal", module: AuthServerModule }, + { path: "auth/login", module: AuthModule }, + { path: "auth/login", module: StrategiesModule }, + { path: "auth/sync", module: ApiSyncModule }, + { path: "auth/internal", module: ApiInternalModule }, + { path: "auth/oauth", module: ApiOauthModule} ]), BackendServicesModule, InitializationModule, diff --git a/backend/src/backend-services/token.service.ts b/backend/src/backend-services/token.service.ts index 2000e2c3..afecb83d 100644 --- a/backend/src/backend-services/token.service.ts +++ b/backend/src/backend-services/token.service.ts @@ -16,8 +16,10 @@ export enum TokenScope { LOGIN_SERVICE = "login", LOGIN_SERVICE_REGISTER = "login-register", BACKEND = "backend", +} + +enum RefreshTokenScope { REFRESH_TOKEN = "token", - NONE = "none", } @Injectable() @@ -118,7 +120,7 @@ export class TokenService { ...expiresInObject, jwtid: uniqueId.toString(), secret: process.env.GROPIUS_LOGIN_SPECIFIC_JWT_SECRET, - audience: [TokenScope.REFRESH_TOKEN], + audience: [RefreshTokenScope.REFRESH_TOKEN], }, ); } @@ -126,7 +128,7 @@ export class TokenService { async verifyActiveLoginToken(token: string, requiredClientId: string): Promise { const payload = await this.backendJwtService.verifyAsync(token, { secret: process.env.GROPIUS_LOGIN_SPECIFIC_JWT_SECRET, - audience: [TokenScope.REFRESH_TOKEN], + audience: [RefreshTokenScope.REFRESH_TOKEN], }); if (payload.client_id !== requiredClientId) { throw new JsonWebTokenError("Token is not for current client"); @@ -140,4 +142,24 @@ export class TokenService { tokenUniqueId: payload.jti, }; } + + /** + * Verifies that the given combination of scopes is valid. + * + * @param scopes the scopes to verify + */ + verifyScope(scopes: string[]) { + const validScopes: string[] = Object.values(TokenScope); + for (const scope of scopes) { + if (!validScopes.includes(scope)) { + throw new JsonWebTokenError("Invalid scope: " + scope); + } + } + if (scopes.length === 0) { + throw new JsonWebTokenError("No scope given"); + } + if (scopes.includes(TokenScope.LOGIN_SERVICE_REGISTER) && scopes.length > 1) { + throw new JsonWebTokenError("Register scope must be the only scope"); + } + } } diff --git a/backend/src/openapi-tag.ts b/backend/src/openapi-tag.ts index 3bd5dcbd..5b6c522b 100644 --- a/backend/src/openapi-tag.ts +++ b/backend/src/openapi-tag.ts @@ -2,4 +2,5 @@ export enum OpenApiTag { LOGIN_API = "login-api", SYNC_API = "sync-api", INTERNAL_API = "internal-api", + OAUTH_API = "oauth-api", } diff --git a/backend/src/strategies/strategies.module.ts b/backend/src/strategies/strategies.module.ts index 676d4f14..5b545052 100644 --- a/backend/src/strategies/strategies.module.ts +++ b/backend/src/strategies/strategies.module.ts @@ -2,7 +2,7 @@ import { Module } from "@nestjs/common"; import { JwtModule, JwtService } from "@nestjs/jwt"; import { ModelModule } from "src/model/model.module"; import { ErrorHandlerMiddleware } from "./error-handler.middleware"; -import { ModeExtractorMiddleware } from "./mode-extractor.middleware"; +import { ModeExtractorMiddleware } from "../api-internal/mode-extractor.middleware"; import { PerformAuthFunctionService } from "./perform-auth-function.service"; import { StrategiesMiddleware } from "./strategies.middleware"; import { UserpassStrategyService } from "./userpass/userpass.service"; From 70055d57387a2de51412d57fe3ed4fd97d24dbc4 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Mon, 8 Jul 2024 19:49:05 +0200 Subject: [PATCH 04/31] progress --- .../api-internal/auth-redirect.middleware.ts | 4 +- .../src/api-internal/auth-token.middleware.ts | 8 ++-- backend/src/api-internal/auth.module.ts | 34 +++++++-------- .../post-credentials.middleware.ts | 6 +-- ...teData.ts => OAuthAuthorizeServerState.ts} | 6 +-- backend/src/api-oauth/OAuthHttpException.ts | 4 +- .../src/api-oauth/OAuthTokenServerState.ts | 7 ++++ ...state.middleware.ts => StateMiddleware.ts} | 8 ++-- backend/src/api-oauth/api-oauth.module.ts | 17 +++++++- .../src/api-oauth/error-handler.middleware.ts | 25 +++++++++++ .../oauth-authorize-extract.middleware.ts | 8 ++-- .../oauth-authorize-redirect.middleware.ts | 10 ++--- .../oauth-authorize-validate.middleware.ts | 20 ++++----- .../oauth-error-redirect.middleware.ts | 41 +++++++++++++++++++ ...uth-token-authorization-code.middleware.ts | 35 +++++++++------- .../src/api-oauth/oauth-token.controller.ts | 20 ++++----- .../src/api-oauth/oauth-token.middleware.ts | 24 +++++++---- .../strategies/error-handler.middleware.ts | 39 ------------------ backend/src/strategies/strategies.module.ts | 6 +-- 19 files changed, 186 insertions(+), 136 deletions(-) rename backend/src/api-oauth/{OAuthAuthorizeServerStateData.ts => OAuthAuthorizeServerState.ts} (66%) create mode 100644 backend/src/api-oauth/OAuthTokenServerState.ts rename backend/src/api-oauth/{state.middleware.ts => StateMiddleware.ts} (76%) create mode 100644 backend/src/api-oauth/error-handler.middleware.ts create mode 100644 backend/src/api-oauth/oauth-error-redirect.middleware.ts delete mode 100644 backend/src/strategies/error-handler.middleware.ts diff --git a/backend/src/api-internal/auth-redirect.middleware.ts b/backend/src/api-internal/auth-redirect.middleware.ts index 07a6bc55..5d14b6f6 100644 --- a/backend/src/api-internal/auth-redirect.middleware.ts +++ b/backend/src/api-internal/auth-redirect.middleware.ts @@ -9,8 +9,8 @@ import { AuthStateData } from "../strategies/AuthResult"; import { OauthServerStateData } from "./auth-autorize.middleware"; @Injectable() -export class OauthRedirectMiddleware implements NestMiddleware { - private readonly logger = new Logger(OauthRedirectMiddleware.name); +export class AuthRedirectMiddleware implements NestMiddleware { + private readonly logger = new Logger(AuthRedirectMiddleware.name); constructor( private readonly tokenService: TokenService, private readonly activeLoginService: ActiveLoginService, diff --git a/backend/src/api-internal/auth-token.middleware.ts b/backend/src/api-internal/auth-token.middleware.ts index 3a88d792..170b3fbf 100644 --- a/backend/src/api-internal/auth-token.middleware.ts +++ b/backend/src/api-internal/auth-token.middleware.ts @@ -10,11 +10,11 @@ import * as bcrypt from "bcrypt"; import { ensureState } from "src/strategies/utils"; import { OauthServerStateData } from "./auth-autorize.middleware"; import { PostCredentialsMiddleware } from "./post-credentials.middleware"; -import { OauthHttpException } from "../api-oauth/OAuthHttpException"; +import { OAuthHttpException } from "../api-oauth/OAuthHttpException"; @Injectable() -export class OauthTokenMiddleware implements NestMiddleware { - private readonly logger = new Logger(OauthTokenMiddleware.name); +export class AuthTokenMiddleware implements NestMiddleware { + private readonly logger = new Logger(AuthTokenMiddleware.name); constructor( private readonly tokenService: TokenService, @@ -114,7 +114,7 @@ export class OauthTokenMiddleware implements NestMiddleware { const client = await this.getCallingClient(req, mayOmitClientId); if (!client) { - throw new OauthHttpException("unauthorized_client", "Unknown client or invalid client credentials"); + throw new OAuthHttpException("unauthorized_client", "Unknown client or invalid client credentials"); } (res.locals.state as OauthServerStateData).client = client; diff --git a/backend/src/api-internal/auth.module.ts b/backend/src/api-internal/auth.module.ts index cc38d716..d5ab7ac1 100644 --- a/backend/src/api-internal/auth.module.ts +++ b/backend/src/api-internal/auth.module.ts @@ -1,57 +1,53 @@ import { MiddlewareConsumer, Module, NestMiddleware } from "@nestjs/common"; import { BackendServicesModule } from "src/backend-services/backend-services.module"; import { ModelModule } from "src/model/model.module"; -import { ErrorHandlerMiddleware } from "../strategies/error-handler.middleware"; +import { ErrorHandlerMiddleware } from "../api-oauth/error-handler.middleware"; import { ModeExtractorMiddleware } from "./mode-extractor.middleware"; import { StrategiesMiddleware } from "../strategies/strategies.middleware"; import { StrategiesModule } from "../strategies/strategies.module"; import { AuthAutorizeMiddleware } from "./auth-autorize.middleware"; import { AuthEndpointsController } from "./auth-endpoints.controller"; -import { OauthRedirectMiddleware } from "./auth-redirect.middleware"; -import { OauthTokenMiddleware } from "./auth-token.middleware"; +import { AuthRedirectMiddleware } from "./auth-redirect.middleware"; +import { AuthTokenMiddleware } from "./auth-token.middleware"; import { PostCredentialsMiddleware } from "./post-credentials.middleware"; +import { ApiOauthModule } from "src/api-oauth/api-oauth.module"; +import { OAuthErrorRedirectMiddleware } from "src/api-oauth/oauth-error-redirect.middleware"; @Module({ - imports: [ModelModule, BackendServicesModule, StrategiesModule], - providers: [ - AuthAutorizeMiddleware, - OauthRedirectMiddleware, - OauthTokenMiddleware, - PostCredentialsMiddleware, - ], + imports: [ModelModule, BackendServicesModule, StrategiesModule, ApiOauthModule], + providers: [AuthAutorizeMiddleware, AuthRedirectMiddleware, AuthTokenMiddleware, PostCredentialsMiddleware], controllers: [AuthEndpointsController], }) export class ApiInternalModule { private middlewares: { middlewares: NestMiddleware[]; path: string }[] = []; constructor( - private readonly oauthAutorize: AuthAutorizeMiddleware, - private readonly oauthRedirect: OauthRedirectMiddleware, - private readonly oauthToken: OauthTokenMiddleware, + private readonly authAutorize: AuthAutorizeMiddleware, + private readonly authRedirect: AuthRedirectMiddleware, + private readonly authToken: AuthTokenMiddleware, private readonly modeExtractor: ModeExtractorMiddleware, private readonly strategies: StrategiesMiddleware, private readonly errorHandler: ErrorHandlerMiddleware, + private readonly oauthErrorRedirect: OAuthErrorRedirectMiddleware, ) { this.middlewares.push({ middlewares: [ this.modeExtractor, - this.oauthAutorize, + this.authAutorize, this.strategies, - this.oauthRedirect, - // This middleware should never be reached as the oauth middleware should already care about it, - // its just to make absolutely sure, no unauthorized request gets through + this.oauthErrorRedirect, this.errorHandler, ], path: "internal/auth/redirect/:id/:mode", }); this.middlewares.push({ - middlewares: [this.strategies, this.oauthRedirect, this.errorHandler], + middlewares: [this.strategies, this.authRedirect, this.oauthErrorRedirect, this.errorHandler], path: "internal/auth/callback/:id", }); this.middlewares.push({ - middlewares: [this.modeExtractor, this.oauthToken, this.errorHandler], + middlewares: [this.modeExtractor, this.authToken, this.errorHandler], path: "internal/auth/submit/:id/:mode", }); } diff --git a/backend/src/api-internal/post-credentials.middleware.ts b/backend/src/api-internal/post-credentials.middleware.ts index 1219d9a3..9af69563 100644 --- a/backend/src/api-internal/post-credentials.middleware.ts +++ b/backend/src/api-internal/post-credentials.middleware.ts @@ -3,7 +3,7 @@ import { Request, Response } from "express"; import { AuthStateData } from "src/strategies/AuthResult"; import { StrategiesMiddleware } from "src/strategies/strategies.middleware"; import { ensureState } from "src/strategies/utils"; -import { OauthHttpException } from "../api-oauth/OAuthHttpException"; +import { OAuthHttpException } from "../api-oauth/OAuthHttpException"; @Injectable() export class PostCredentialsMiddleware implements NestMiddleware { @@ -37,9 +37,9 @@ export class PostCredentialsMiddleware implements NestMiddleware { if (!state.activeLogin) { if (state.authErrorMessage) { - throw new OauthHttpException(state.authErrorType || "invalid_request", state.authErrorMessage); + throw new OAuthHttpException(state.authErrorType || "invalid_request", state.authErrorMessage); } - throw new OauthHttpException("invalid_request", "Unauthorized"); + throw new OAuthHttpException("invalid_request", "Unauthorized"); } next(); diff --git a/backend/src/api-oauth/OAuthAuthorizeServerStateData.ts b/backend/src/api-oauth/OAuthAuthorizeServerState.ts similarity index 66% rename from backend/src/api-oauth/OAuthAuthorizeServerStateData.ts rename to backend/src/api-oauth/OAuthAuthorizeServerState.ts index 216cb51c..80cbc727 100644 --- a/backend/src/api-oauth/OAuthAuthorizeServerStateData.ts +++ b/backend/src/api-oauth/OAuthAuthorizeServerState.ts @@ -1,6 +1,6 @@ import { AuthClient } from "src/model/postgres/AuthClient.entity"; -export interface OAuthAuthorizeRequestData { +export interface OAuthAuthorizeRequest { state?: string; redirect: string; clientId: string; @@ -10,7 +10,7 @@ export interface OAuthAuthorizeRequestData { responseType: "code"; } -export interface OAuthAuthorizeServerStateData { - request: OAuthAuthorizeRequestData; +export interface OAuthAuthorizeServerState { + request: OAuthAuthorizeRequest; client: AuthClient; } \ No newline at end of file diff --git a/backend/src/api-oauth/OAuthHttpException.ts b/backend/src/api-oauth/OAuthHttpException.ts index 4de96d62..6aa7862f 100644 --- a/backend/src/api-oauth/OAuthHttpException.ts +++ b/backend/src/api-oauth/OAuthHttpException.ts @@ -1,7 +1,7 @@ import { HttpException, HttpStatus } from "@nestjs/common"; -export class OauthHttpException extends HttpException { - constructor(private readonly error_type: string, private readonly error_message: string) { +export class OAuthHttpException extends HttpException { + constructor(readonly error_type: string, readonly error_message: string) { super( { statusCode: HttpStatus.BAD_REQUEST, diff --git a/backend/src/api-oauth/OAuthTokenServerState.ts b/backend/src/api-oauth/OAuthTokenServerState.ts new file mode 100644 index 00000000..d6ebbf4f --- /dev/null +++ b/backend/src/api-oauth/OAuthTokenServerState.ts @@ -0,0 +1,7 @@ +import { ActiveLogin } from "src/model/postgres/ActiveLogin.entity"; +import { AuthClient } from "src/model/postgres/AuthClient.entity"; + +export interface OAuthTokenServerState { + client: AuthClient; + activeLogin: ActiveLogin; +} \ No newline at end of file diff --git a/backend/src/api-oauth/state.middleware.ts b/backend/src/api-oauth/StateMiddleware.ts similarity index 76% rename from backend/src/api-oauth/state.middleware.ts rename to backend/src/api-oauth/StateMiddleware.ts index 2a018927..6f013215 100644 --- a/backend/src/api-oauth/state.middleware.ts +++ b/backend/src/api-oauth/StateMiddleware.ts @@ -8,12 +8,12 @@ export abstract class StateMiddleware = {}, T exte async use(req: Request, res: Response, next: (error?: Error | any) => any) { ensureState(res); if (res.locals.state.error) { - this.useWithError(req, res, res.locals.state.error, next); + this.useWithError(req, res, res.locals.state, res.locals.state.error, next); } else { try { await this.useWithState(req, res, res.locals.state, next); } catch (error) { - this.appendState(res, { error } as any); + this.appendState(res, { error }); next(); } } @@ -29,11 +29,11 @@ export abstract class StateMiddleware = {}, T exte /** * Overwrite this to handle errors */ - protected useWithError(req: Request, res: Response, error: any, next: (error?: Error | any) => void) { + protected useWithError(req: Request, res: Response, state: S & { error?: any }, error: any, next: (error?: Error | any) => void) { next(); } - protected appendState(res: Response, appendedState: Partial & { error?: any }) { + protected appendState(res: Response, appendedState: Partial & { error?: any } | { error?: any }) { res.locals.state = { ...res.locals.state, ...appendedState }; } } diff --git a/backend/src/api-oauth/api-oauth.module.ts b/backend/src/api-oauth/api-oauth.module.ts index 458c0607..fde0ce74 100644 --- a/backend/src/api-oauth/api-oauth.module.ts +++ b/backend/src/api-oauth/api-oauth.module.ts @@ -7,6 +7,8 @@ import { OauthAuthorizeController as OAuthAuthorizeController } from "./oauth-au import { OAuthTokenController } from "./oauth-token.controller"; import { OAuthAuthorizeValidateMiddleware } from "./oauth-authorize-validate.middleware"; import { OAuthAuthorizeRedirectMiddleware } from "./oauth-authorize-redirect.middleware"; +import { ErrorHandlerMiddleware } from "./error-handler.middleware"; +import { OAuthErrorRedirectMiddleware } from "./oauth-error-redirect.middleware"; @Module({ imports: [ModelModule], @@ -16,8 +18,11 @@ import { OAuthAuthorizeRedirectMiddleware } from "./oauth-authorize-redirect.mid OAuthAuthorizeRedirectMiddleware, OAuthTokenMiddleware, OAuthTokenAuthorizationCodeMiddleware, + ErrorHandlerMiddleware, + OAuthErrorRedirectMiddleware, ], controllers: [OAuthAuthorizeController, OAuthTokenController], + exports: [OAuthAuthorizeValidateMiddleware, ErrorHandlerMiddleware, OAuthErrorRedirectMiddleware], }) export class ApiOauthModule { private middlewares: { middlewares: NestMiddleware[]; path: string }[] = []; @@ -28,14 +33,22 @@ export class ApiOauthModule { private readonly oauthAuthorizeRedirect: OAuthAuthorizeRedirectMiddleware, private readonly oauthToken: OAuthTokenMiddleware, private readonly oauthTokenAuthorizationCode: OAuthTokenAuthorizationCodeMiddleware, + private readonly errorHandler: ErrorHandlerMiddleware, + private readonly oauthErrorRedirect: OAuthErrorRedirectMiddleware, ) { this.middlewares.push({ - middlewares: [this.oauthAuthorizeExtract, this.oauthAuthorizeValidate, this.oauthAuthorizeRedirect], + middlewares: [ + this.oauthAuthorizeExtract, + this.oauthAuthorizeValidate, + this.oauthAuthorizeRedirect, + this.oauthErrorRedirect, + this.errorHandler, + ], path: "oauth/authorize", }); this.middlewares.push({ - middlewares: [this.oauthToken, this.oauthTokenAuthorizationCode], + middlewares: [this.oauthToken, this.oauthTokenAuthorizationCode, this.errorHandler], path: "oauth/token", }); } diff --git a/backend/src/api-oauth/error-handler.middleware.ts b/backend/src/api-oauth/error-handler.middleware.ts new file mode 100644 index 00000000..3a32b4d9 --- /dev/null +++ b/backend/src/api-oauth/error-handler.middleware.ts @@ -0,0 +1,25 @@ +import { Injectable } from "@nestjs/common"; +import { Request, Response } from "express"; +import { StateMiddleware } from "./StateMiddleware"; + +@Injectable() +export class ErrorHandlerMiddleware extends StateMiddleware<{}, {}> { + protected async useWithState( + req: Request, + res: Response, + state: { error?: any }, + next: (error?: Error | any) => void, + ): Promise { + next(); + } + + protected useWithError( + req: Request, + res: Response, + state: { error?: any }, + error: any, + next: (error?: Error | any) => void, + ): void { + next(error); + } +} diff --git a/backend/src/api-oauth/oauth-authorize-extract.middleware.ts b/backend/src/api-oauth/oauth-authorize-extract.middleware.ts index eeac4584..025254ae 100644 --- a/backend/src/api-oauth/oauth-authorize-extract.middleware.ts +++ b/backend/src/api-oauth/oauth-authorize-extract.middleware.ts @@ -1,11 +1,11 @@ import { Injectable } from "@nestjs/common"; import { Request, Response } from "express"; -import { StateMiddleware } from "./state.middleware"; -import { OAuthAuthorizeRequestData, OAuthAuthorizeServerStateData } from "./OAuthAuthorizeServerStateData"; +import { StateMiddleware } from "./StateMiddleware"; +import { OAuthAuthorizeRequest, OAuthAuthorizeServerState } from "./OAuthAuthorizeServerState"; import { AuthClientService } from "src/model/services/auth-client.service"; @Injectable() -export class OAuthAuthorizeExtractMiddleware extends StateMiddleware<{}, OAuthAuthorizeServerStateData> { +export class OAuthAuthorizeExtractMiddleware extends StateMiddleware<{}, OAuthAuthorizeServerState> { constructor(private readonly authClientService: AuthClientService) { super(); } @@ -16,7 +16,7 @@ export class OAuthAuthorizeExtractMiddleware extends StateMiddleware<{}, OAuthAu state: { error?: any }, next: (error?: Error | any) => void, ) { - const requestParams: OAuthAuthorizeRequestData = { + const requestParams: OAuthAuthorizeRequest = { state: req.query.state as string, redirect: req.query.redirect as string, clientId: req.query.client_id as string, diff --git a/backend/src/api-oauth/oauth-authorize-redirect.middleware.ts b/backend/src/api-oauth/oauth-authorize-redirect.middleware.ts index 7cbfe1bf..0fcd60a8 100644 --- a/backend/src/api-oauth/oauth-authorize-redirect.middleware.ts +++ b/backend/src/api-oauth/oauth-authorize-redirect.middleware.ts @@ -1,18 +1,18 @@ import { Injectable } from "@nestjs/common"; import { Request, Response } from "express"; -import { StateMiddleware } from "./state.middleware"; -import { OAuthAuthorizeServerStateData } from "./OAuthAuthorizeServerStateData"; +import { StateMiddleware } from "./StateMiddleware"; +import { OAuthAuthorizeServerState } from "./OAuthAuthorizeServerState"; import { TokenScope } from "src/backend-services/token.service"; @Injectable() export class OAuthAuthorizeRedirectMiddleware extends StateMiddleware< - OAuthAuthorizeServerStateData, - OAuthAuthorizeServerStateData + OAuthAuthorizeServerState, + OAuthAuthorizeServerState > { protected async useWithState( req: Request, res: Response, - state: OAuthAuthorizeServerStateData & { error?: any }, + state: OAuthAuthorizeServerState & { error?: any }, next: (error?: Error | any) => void, ): Promise { const target = state.request.scope.includes(TokenScope.LOGIN_SERVICE_REGISTER) diff --git a/backend/src/api-oauth/oauth-authorize-validate.middleware.ts b/backend/src/api-oauth/oauth-authorize-validate.middleware.ts index fc632323..431d7933 100644 --- a/backend/src/api-oauth/oauth-authorize-validate.middleware.ts +++ b/backend/src/api-oauth/oauth-authorize-validate.middleware.ts @@ -1,14 +1,14 @@ import { Injectable } from "@nestjs/common"; import { Request, Response } from "express"; -import { StateMiddleware } from "./state.middleware"; -import { OAuthAuthorizeServerStateData } from "./OAuthAuthorizeServerStateData"; -import { OauthHttpException } from "./OAuthHttpException"; +import { StateMiddleware } from "./StateMiddleware"; +import { OAuthAuthorizeServerState } from "./OAuthAuthorizeServerState"; +import { OAuthHttpException } from "./OAuthHttpException"; import { TokenService } from "src/backend-services/token.service"; @Injectable() export class OAuthAuthorizeValidateMiddleware extends StateMiddleware< - OAuthAuthorizeServerStateData, - OAuthAuthorizeServerStateData + OAuthAuthorizeServerState, + OAuthAuthorizeServerState > { constructor(private readonly tokenService: TokenService) { @@ -18,22 +18,22 @@ export class OAuthAuthorizeValidateMiddleware extends StateMiddleware< protected async useWithState( req: Request, res: Response, - state: OAuthAuthorizeServerStateData & { error?: any }, + state: OAuthAuthorizeServerState & { error?: any }, next: (error?: Error | any) => void, ): Promise { if (!state.client || !state.client.isValid) { - throw new OauthHttpException("invalid_client", "Client unknown or unauthorized"); + throw new OAuthHttpException("invalid_client", "Client unknown or unauthorized"); } if (state.request.responseType !== "code") { - throw new OauthHttpException("unsupported_response_type", "response_type must be set to 'code'"); + throw new OAuthHttpException("unsupported_response_type", "response_type must be set to 'code'"); } if (!state.request.redirect || !state.client.redirectUrls.includes(state.request.redirect)) { - throw new OauthHttpException("invalid_request", "Redirect URL not allowed"); + throw new OAuthHttpException("invalid_request", "Redirect URL not allowed"); } try { this.tokenService.verifyScope(state.request.scope); } catch (error) { - throw new OauthHttpException("invalid_scope", error.message); + throw new OAuthHttpException("invalid_scope", error.message); } //TODO validate PKCE //TODO check if PKCE is required diff --git a/backend/src/api-oauth/oauth-error-redirect.middleware.ts b/backend/src/api-oauth/oauth-error-redirect.middleware.ts new file mode 100644 index 00000000..cbe69922 --- /dev/null +++ b/backend/src/api-oauth/oauth-error-redirect.middleware.ts @@ -0,0 +1,41 @@ +import { Injectable } from "@nestjs/common"; +import { Request, Response } from "express"; +import { StateMiddleware } from "./StateMiddleware"; +import { OAuthAuthorizeServerState } from "./OAuthAuthorizeServerState"; + +@Injectable() +export class OAuthErrorRedirectMiddleware extends StateMiddleware { + protected async useWithState( + req: Request, + res: Response, + state: OAuthAuthorizeServerState & { error?: any }, + next: (error?: Error | any) => void, + ): Promise { + next(); + } + + protected useWithError( + req: Request, + res: Response, + state: OAuthAuthorizeServerState & { error?: any }, + error: any, + next: (error?: Error | any) => void, + ): void { + if (state.request?.redirect) { + const url = new URL(state.request.redirect); + if (error.error_type && error.error_message) { + url.searchParams.append("error", error.error_type); + url.searchParams.append( + "error_description", + error.error_message.replace(/[^\x20-\x21\x23-\x5B\x5D-\x7E]/g, ""), + ); + } else { + url.searchParams.append("error", "server_error"); + url.searchParams.append("error_description", encodeURIComponent("An unknown error occurred")); + } + res.redirect(url.toString()); + } else { + next(); + } + } +} diff --git a/backend/src/api-oauth/oauth-token-authorization-code.middleware.ts b/backend/src/api-oauth/oauth-token-authorization-code.middleware.ts index 62a91642..19b44f50 100644 --- a/backend/src/api-oauth/oauth-token-authorization-code.middleware.ts +++ b/backend/src/api-oauth/oauth-token-authorization-code.middleware.ts @@ -1,29 +1,34 @@ -import { Injectable, Logger, NestMiddleware } from "@nestjs/common"; +import { Injectable, Logger } from "@nestjs/common"; import { Request, Response } from "express"; import { ActiveLoginTokenResult, TokenService } from "src/backend-services/token.service"; import { AuthClient } from "src/model/postgres/AuthClient.entity"; import { ActiveLoginService } from "src/model/services/active-login.service"; import { AuthStateData } from "src/strategies/AuthResult"; -import { ensureState } from "src/strategies/utils"; -import { OauthHttpException } from "./OAuthHttpException"; +import { OAuthHttpException } from "./OAuthHttpException"; +import { StateMiddleware } from "./StateMiddleware"; @Injectable() -export class OAuthTokenAuthorizationCodeMiddleware implements NestMiddleware { +export class OAuthTokenAuthorizationCodeMiddleware extends StateMiddleware<{ client: AuthClient }, AuthStateData> { private readonly logger = new Logger(OAuthTokenAuthorizationCodeMiddleware.name); - constructor(private readonly activeLoginService: ActiveLoginService, private readonly tokenService: TokenService) {} + constructor( + private readonly activeLoginService: ActiveLoginService, + private readonly tokenService: TokenService, + ) { + super(); + } private throwGenericCodeError() { - throw new OauthHttpException("invalid_grant", "Given code was invalid or expired"); + throw new OAuthHttpException("invalid_grant", "Given code was invalid or expired"); } - async use(req: Request, res: Response, next: () => void) { - ensureState(res); + protected async useWithState( + req: Request, + res: Response, + state: { client: AuthClient } & { error?: any }, + next: (error?: Error | any) => void, + ): Promise { let tokenData: ActiveLoginTokenResult; - const currentClient = res.locals.state.client as AuthClient; - if (!currentClient) { - this.logger.warn("No client logged in"); - throw new OauthHttpException("invalid_client", "Client unknown or unauthorized"); - } + const currentClient = state.client; try { tokenData = await this.tokenService.verifyActiveLoginToken( req.body.code ?? req.body.refresh_token, @@ -64,9 +69,9 @@ export class OAuthTokenAuthorizationCodeMiddleware implements NestMiddleware { "Active login has been made invalid", tokenData.activeLoginId, ); - throw new OauthHttpException("invalid_grant", "Given code was liekely reused. Login and codes invalidated"); + throw new OAuthHttpException("invalid_grant", "Given code was liekely reused. Login and codes invalidated"); } - (res.locals.state as AuthStateData).activeLogin = activeLogin; + this.appendState(res, { activeLogin }); next(); } } diff --git a/backend/src/api-oauth/oauth-token.controller.ts b/backend/src/api-oauth/oauth-token.controller.ts index 0037a6a9..568ba13c 100644 --- a/backend/src/api-oauth/oauth-token.controller.ts +++ b/backend/src/api-oauth/oauth-token.controller.ts @@ -10,7 +10,7 @@ import { AuthClientService } from "src/model/services/auth-client.service"; import { OpenApiTag } from "src/openapi-tag"; import { AuthStateData } from "src/strategies/AuthResult"; import { ensureState } from "src/strategies/utils"; -import { OauthHttpException } from "../api-oauth/OAuthHttpException"; +import { OAuthHttpException } from "../api-oauth/OAuthHttpException"; export interface OauthTokenEndpointResponseDto { access_token: string; @@ -33,11 +33,11 @@ export class OAuthTokenController { private async checkLoginDataIsVaild(loginData?: UserLoginData, activeLogin?: ActiveLogin) { if (!loginData) { this.logger.warn("Login data not found"); - throw new OauthHttpException("invalid_grant", "No login found for given grant (refresh token/code)"); + throw new OAuthHttpException("invalid_grant", "No login found for given grant (refresh token/code)"); } if (loginData.expires != null && loginData.expires <= new Date()) { this.logger.warn("Login data has expired", loginData); - throw new OauthHttpException( + throw new OAuthHttpException( "invalid_grant", "Login has expired. Try restarting login/register/link process.", ); @@ -45,37 +45,37 @@ export class OAuthTokenController { switch (loginData.state) { case LoginState.VALID: if (!(await loginData.user)) { - throw new OauthHttpException("invalid_state", "No user for valid login"); + throw new OAuthHttpException("invalid_state", "No user for valid login"); } break; case LoginState.WAITING_FOR_REGISTER: if (await loginData.user) { - throw new OauthHttpException( + throw new OAuthHttpException( "invalid_state", "Login still in register state but user already existing", ); } break; default: - throw new OauthHttpException( + throw new OAuthHttpException( "invalid_grant", "Login for given grant is not valid any more; Please re-login", ); } if (!activeLogin) { this.logger.warn("Active login not found"); - throw new OauthHttpException("invalid_grant", "No login found for given grant (refresh token/code)"); + throw new OAuthHttpException("invalid_grant", "No login found for given grant (refresh token/code)"); } if (activeLogin.expires != null && activeLogin.expires <= new Date()) { this.logger.warn("Active login has expired", activeLogin.id); - throw new OauthHttpException( + throw new OAuthHttpException( "invalid_grant", "Login has expired. Try restarting login/register/link process.", ); } if (!activeLogin.isValid) { this.logger.warn("Active login is set invalid", activeLogin.id); - throw new OauthHttpException("invalid_grant", "Login is set invalid/disabled"); + throw new OAuthHttpException("invalid_grant", "Login is set invalid/disabled"); } } @@ -137,7 +137,7 @@ export class OAuthTokenController { ensureState(res); const currentClient = res.locals.state.client as AuthClient; if (!currentClient) { - throw new OauthHttpException( + throw new OAuthHttpException( "invalid_client", "No client id/authentication given or authentication invalid", ); diff --git a/backend/src/api-oauth/oauth-token.middleware.ts b/backend/src/api-oauth/oauth-token.middleware.ts index 45f27893..5b1bcb91 100644 --- a/backend/src/api-oauth/oauth-token.middleware.ts +++ b/backend/src/api-oauth/oauth-token.middleware.ts @@ -5,16 +5,19 @@ import { AuthClientService } from "src/model/services/auth-client.service"; import { OAuthTokenAuthorizationCodeMiddleware } from "./oauth-token-authorization-code.middleware"; import * as bcrypt from "bcrypt"; import { ensureState } from "src/strategies/utils"; -import { OauthHttpException } from "./OAuthHttpException"; +import { OAuthHttpException } from "./OAuthHttpException"; +import { StateMiddleware } from "./StateMiddleware"; @Injectable() -export class OauthTokenMiddleware implements NestMiddleware { +export class OauthTokenMiddleware extends StateMiddleware<{}, { client: AuthClient }> { private readonly logger = new Logger(OauthTokenMiddleware.name); constructor( private readonly authClientService: AuthClientService, private readonly tokenResponseCodeMiddleware: OAuthTokenAuthorizationCodeMiddleware, - ) {} + ) { + super(); + } private async checkGivenClientSecretValidOrNotRequired(client: AuthClient, givenSecret?: string): Promise { if (!client.requiresSecret && (!givenSecret || givenSecret.length == 0)) { @@ -42,7 +45,7 @@ export class OauthTokenMiddleware implements NestMiddleware { * @returns The auth client that requested (or any without secret if flag ist set) * or `null` if credentials invalid or none given */ - private async getCallingClient(req: Request,): Promise { + private async getCallingClient(req: Request): Promise { const auth_head = req.headers["authorization"]; if (auth_head && auth_head.startsWith("Basic ")) { const clientIdSecret = Buffer.from(auth_head.substring(6), "base64") @@ -78,16 +81,19 @@ export class OauthTokenMiddleware implements NestMiddleware { return null; } - async use(req: Request, res: Response, next: () => void) { - ensureState(res); - + protected async useWithState( + req: Request, + res: Response, + state: { error?: any }, + next: (error?: Error | any) => void, + ): Promise { const grant_type = req.body.grant_type; const client = await this.getCallingClient(req); if (!client) { - throw new OauthHttpException("unauthorized_client", "Unknown client or invalid client credentials"); + throw new OAuthHttpException("unauthorized_client", "Unknown client or invalid client credentials"); } - res.locals.state.client = client; + this.appendState(res, { client }); switch (grant_type) { case "refresh_token": //Request for new token using refresh token diff --git a/backend/src/strategies/error-handler.middleware.ts b/backend/src/strategies/error-handler.middleware.ts deleted file mode 100644 index c939972f..00000000 --- a/backend/src/strategies/error-handler.middleware.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { HttpException, HttpStatus, Injectable, NestMiddleware } from "@nestjs/common"; -import { Request, Response } from "express"; -import { ActiveLoginService } from "src/model/services/active-login.service"; -import { StrategyInstanceService } from "src/model/services/strategy-instance.service"; -import { AuthStateData } from "./AuthResult"; -import { StrategiesService } from "../model/services/strategies.service"; - -@Injectable() -export class ErrorHandlerMiddleware implements NestMiddleware { - constructor( - private readonly strategiesService: StrategiesService, - private readonly strategyInstanceService: StrategyInstanceService, - private readonly activeLoginService: ActiveLoginService, - ) {} - - async use(req: Request, res: Response, next: () => void) { - const errorMessage = (res.locals?.state as AuthStateData)?.authErrorMessage; - const errorType = (res.locals?.state as AuthStateData)?.authErrorType; - if (errorMessage || errorType) { - if (errorType) { - throw new HttpException( - { - statusCode: HttpStatus.BAD_REQUEST, - error: errorType, - error_description: errorMessage, - message: errorMessage, - }, - HttpStatus.BAD_REQUEST, - ); - } else { - throw new HttpException(errorMessage, HttpStatus.UNAUTHORIZED); - } - } else if (res.locals?.state == undefined || res.locals?.state == null) { - throw new HttpException("State of request was lost", HttpStatus.INTERNAL_SERVER_ERROR); - } else { - next(); - } - } -} diff --git a/backend/src/strategies/strategies.module.ts b/backend/src/strategies/strategies.module.ts index 5b545052..9e58500f 100644 --- a/backend/src/strategies/strategies.module.ts +++ b/backend/src/strategies/strategies.module.ts @@ -1,8 +1,6 @@ import { Module } from "@nestjs/common"; import { JwtModule, JwtService } from "@nestjs/jwt"; import { ModelModule } from "src/model/model.module"; -import { ErrorHandlerMiddleware } from "./error-handler.middleware"; -import { ModeExtractorMiddleware } from "../api-internal/mode-extractor.middleware"; import { PerformAuthFunctionService } from "./perform-auth-function.service"; import { StrategiesMiddleware } from "./strategies.middleware"; import { UserpassStrategyService } from "./userpass/userpass.service"; @@ -35,10 +33,8 @@ import { JiraStrategyService } from "./jira/jira.service"; GithubStrategyService, JiraStrategyService, { provide: "PassportStateJwt", useExisting: JwtService }, - ModeExtractorMiddleware, StrategiesMiddleware, - ErrorHandlerMiddleware, ], - exports: [ModeExtractorMiddleware, StrategiesMiddleware, ErrorHandlerMiddleware], + exports: [StrategiesMiddleware], }) export class StrategiesModule {} From 8935d26c0e09c87469601d0ec16c49f1256ea9f8 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Tue, 9 Jul 2024 02:48:28 +0200 Subject: [PATCH 05/31] progress --- .../auth-autorize-extract.middleware.ts | 28 +++ .../api-internal/auth-autorize.middleware.ts | 40 ---- .../api-internal/auth-endpoints.controller.ts | 10 + .../api-internal/auth-redirect.middleware.ts | 174 +++++++++++------- .../src/api-internal/auth-token.middleware.ts | 147 --------------- backend/src/api-internal/auth.module.ts | 47 +++-- .../api-internal/mode-extractor.middleware.ts | 30 +-- .../post-credentials.middleware.ts | 4 +- backend/src/api-login/dto/user-inputs.dto.ts | 58 ------ backend/src/api-login/register.controller.ts | 59 +----- backend/src/api-oauth/StateMiddleware.ts | 2 +- backend/src/api-oauth/api-oauth.module.ts | 4 +- .../src/api-oauth/error-handler.middleware.ts | 4 +- .../oauth-authorize-extract.middleware.ts | 2 +- .../oauth-authorize-redirect.middleware.ts | 15 +- .../oauth-authorize-validate.middleware.ts | 2 +- .../oauth-error-redirect.middleware.ts | 4 +- ...uth-token-authorization-code.middleware.ts | 6 +- .../src/api-oauth/oauth-token.controller.ts | 4 +- .../src/api-oauth/oauth-token.middleware.ts | 2 +- .../create-default-user.service.ts | 2 +- backend/src/strategies/AuthResult.ts | 14 +- backend/src/strategies/Strategy.ts | 20 +- .../src/strategies/StrategyUsingPassport.ts | 21 +-- .../src/strategies/github/github.service.ts | 7 +- backend/src/strategies/jira/jira.service.ts | 2 +- .../perform-auth-function.service.ts | 97 ++++------ .../src/strategies/strategies.middleware.ts | 62 ++++--- 28 files changed, 330 insertions(+), 537 deletions(-) create mode 100644 backend/src/api-internal/auth-autorize-extract.middleware.ts delete mode 100644 backend/src/api-internal/auth-autorize.middleware.ts delete mode 100644 backend/src/api-internal/auth-token.middleware.ts diff --git a/backend/src/api-internal/auth-autorize-extract.middleware.ts b/backend/src/api-internal/auth-autorize-extract.middleware.ts new file mode 100644 index 00000000..c99ef377 --- /dev/null +++ b/backend/src/api-internal/auth-autorize-extract.middleware.ts @@ -0,0 +1,28 @@ +import { Injectable } from "@nestjs/common"; +import { Request, Response } from "express"; +import { AuthClientService } from "src/model/services/auth-client.service"; +import { StateMiddleware } from "src/api-oauth/StateMiddleware"; +import { OAuthAuthorizeRequest, OAuthAuthorizeServerState } from "src/api-oauth/OAuthAuthorizeServerState"; +import { JwtService } from "@nestjs/jwt"; + +@Injectable() +export class AuthAutorizeExtractMiddleware extends StateMiddleware<{}, OAuthAuthorizeServerState> { + constructor( + private readonly authClientService: AuthClientService, + private readonly jwtService: JwtService, + ) { + super(); + } + + protected override async useWithState( + req: Request, + res: Response, + state: { error?: any }, + next: (error?: Error | any) => void, + ): Promise { + const newState = this.jwtService.verify>(req.query.state as string); + const client = await this.authClientService.findOneBy({ id: newState.request.clientId }); + this.appendState(res, { client, ...newState }); + next(); + } +} diff --git a/backend/src/api-internal/auth-autorize.middleware.ts b/backend/src/api-internal/auth-autorize.middleware.ts deleted file mode 100644 index 9aaaac63..00000000 --- a/backend/src/api-internal/auth-autorize.middleware.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { HttpException, HttpStatus, Injectable, Logger, NestMiddleware } from "@nestjs/common"; -import { Request, Response } from "express"; -import { AuthClient } from "src/model/postgres/AuthClient.entity"; -import { AuthClientService } from "src/model/services/auth-client.service"; -import { AuthStateData } from "../strategies/AuthResult"; -import { ensureState } from "../strategies/utils"; - -@Injectable() -export class AuthAutorizeMiddleware implements NestMiddleware { - constructor(private readonly authClientService: AuthClientService) {} - - async use(req: Request, res: Response, next: () => void) { - ensureState(res); - const params = { ...req.query } as { [name: string]: string }; - const clientId = params.client_id; - const client = await this.authClientService.findOneBy({ id: clientId }); - if (!clientId || !client || !client.isValid) { - throw new HttpException( - "client_id not given, not a valid client id or client invalid", - HttpStatus.BAD_REQUEST, - ); - } - if (params.response_type != undefined && params.response_type !== "code") { - (res.locals.state as AuthStateData).authErrorMessage = - "response_type must be set to 'code'. Other flow types not supported"; - (res.locals.state as AuthStateData).authErrorType = "unsupported_response_type"; - } - if (!!params.redirect_uri && !client.redirectUrls.includes(params.redirect_uri)) { - throw new HttpException("Given redirect URI not valid for this client", HttpStatus.BAD_REQUEST); - } - const redirect = params.redirect_uri || client.redirectUrls[0]; - const state = params.state; - - ensureState(res); - res.locals.state.state = state; - res.locals.state.redirect = redirect; - res.locals.state.clientId = clientId; - next(); - } -} diff --git a/backend/src/api-internal/auth-endpoints.controller.ts b/backend/src/api-internal/auth-endpoints.controller.ts index b10b96df..1b4e509b 100644 --- a/backend/src/api-internal/auth-endpoints.controller.ts +++ b/backend/src/api-internal/auth-endpoints.controller.ts @@ -72,4 +72,14 @@ export class AuthEndpointsController { HttpStatus.INTERNAL_SERVER_ERROR, ); } + + @Post("register") + @ApiOperation({ summary: "Copmplete a registration" }) + @ApiTags(OpenApiTag.INTERNAL_API) + registerEndpoint() { + throw new HttpException( + "This controller shouldn't be reached as all functionality is handeled in middleware", + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } } diff --git a/backend/src/api-internal/auth-redirect.middleware.ts b/backend/src/api-internal/auth-redirect.middleware.ts index 5d14b6f6..e4647036 100644 --- a/backend/src/api-internal/auth-redirect.middleware.ts +++ b/backend/src/api-internal/auth-redirect.middleware.ts @@ -1,68 +1,65 @@ import { HttpException, HttpStatus, Injectable, Logger, NestMiddleware } from "@nestjs/common"; import { Request, Response } from "express"; -import { TokenService } from "src/backend-services/token.service"; +import { TokenScope, TokenService } from "src/backend-services/token.service"; import { ActiveLogin } from "src/model/postgres/ActiveLogin.entity"; import { AuthClient } from "src/model/postgres/AuthClient.entity"; import { ActiveLoginService } from "src/model/services/active-login.service"; import { AuthClientService } from "src/model/services/auth-client.service"; -import { AuthStateData } from "../strategies/AuthResult"; -import { OauthServerStateData } from "./auth-autorize.middleware"; +import { AuthFunction, AuthStateServerData } from "../strategies/AuthResult"; +import { OAuthHttpException } from "src/api-oauth/OAuthHttpException"; +import { StateMiddleware } from "src/api-oauth/StateMiddleware"; +import { OAuthAuthorizeServerState } from "src/api-oauth/OAuthAuthorizeServerState"; +import { LoginState, UserLoginData } from "src/model/postgres/UserLoginData.entity"; +import { JwtService } from "@nestjs/jwt"; +import { Strategy } from "src/strategies/Strategy"; +import { LoginUserService } from "src/model/services/login-user.service"; + +/** + * Return data of the user data sugestion endpoint + */ +interface UserDataSuggestion { + /** + * A potential username to use for the registration. + * If one is given, it was free the moment the suggestion is retrieved + * + * @example "testUser" + */ + username?: string; + + /** + * A potential name to display in the UI for the new user. + * + * @example "Test User" + */ + displayName?: string; + + /** + * A potential email of the new user. + * + * @example "test-user@example.com" + */ + email?: string; +} @Injectable() -export class AuthRedirectMiddleware implements NestMiddleware { +export class AuthRedirectMiddleware extends StateMiddleware< + AuthStateServerData & OAuthAuthorizeServerState & { strategy: Strategy } +> { private readonly logger = new Logger(AuthRedirectMiddleware.name); constructor( private readonly tokenService: TokenService, private readonly activeLoginService: ActiveLoginService, private readonly authClientService: AuthClientService, - ) { } - - private handleErrorCases(state: (AuthStateData & OauthServerStateData) | undefined | null, url: URL): boolean { - const errorMessage = state?.authErrorMessage; - if (errorMessage) { - url.searchParams.append("error", encodeURIComponent(state.authErrorType || "invalid_request")); - url.searchParams.append( - "error_description", - encodeURIComponent(state.authErrorMessage?.replace(/[^\x20-\x21\x23-\x5B\x5D-\x7E]/g, "")), - ); - return true; - } else if (state == undefined || state == null) { - url.searchParams.append("error", "server_error"); - url.searchParams.append( - "error_description", - encodeURIComponent("State of request was lost. Internal server error"), - ); - return true; - } else if (!state.activeLogin) { - url.searchParams.append("error", "server_error"); - url.searchParams.append( - "error_description", - encodeURIComponent("Login information was lost. Internal server error"), - ); - } else if (!state.client?.id && !state.clientId) { - url.searchParams.append("error", "server_error"); - url.searchParams.append( - "error_description", - encodeURIComponent("Client id information was lost. Internal server error"), - ); - } - return false; + private readonly jwtService: JwtService, + private readonly userService: LoginUserService, + ) { + super(); } private async assignActiveLoginToClient( - state: AuthStateData & OauthServerStateData, + state: AuthStateServerData & OAuthAuthorizeServerState, expiresIn: number, ): Promise { - if (typeof state.activeLogin == "string") { - state.activeLogin = await this.activeLoginService.findOneByOrFail({ - id: state.activeLogin, - }); - } - if (!state.client && state.clientId) { - state.client = await this.authClientService.findOneByOrFail({ - id: state.clientId, - }); - } if (!state.activeLogin.isValid) { throw new Error("Active login invalid"); } @@ -79,47 +76,86 @@ export class AuthRedirectMiddleware implements NestMiddleware { const codeJwtId = ++state.activeLogin.nextExpectedRefreshTokenNumber; state.activeLogin = await this.activeLoginService.save(state.activeLogin); - state.client = await this.authClientService.findOneBy({ - id: state.client.id, - }); return codeJwtId; } - private async generateCode(state: AuthStateData & OauthServerStateData, url: URL) { - const activeLogin = state?.activeLogin; + private async generateCode(state: AuthStateServerData & OAuthAuthorizeServerState): Promise { + const activeLogin = state.activeLogin; try { const expiresIn = parseInt(process.env.GROPIUS_OAUTH_CODE_EXPIRATION_TIME_MS, 10); const codeJwtId = await this.assignActiveLoginToClient(state, expiresIn); const token = await this.tokenService.signActiveLoginCode( typeof activeLogin == "string" ? activeLogin : activeLogin.id, - state.clientId || state.client.id, + state.client.id, codeJwtId, expiresIn, ); - url.searchParams.append("code", token); - this.logger.debug("Created token", url.searchParams); - if (state.state) { - url.searchParams.append("state", state.state); - } + this.logger.debug("Created token"); + return token; } catch (err) { this.logger.warn(err); - url.searchParams.append("error", "server_error"); - url.searchParams.append("error_description", encodeURIComponent("Could not generate code for response")); + throw new OAuthHttpException("server_error", "Could not generate code for response"); } } - async use(req: Request, res: Response, next: () => void) { - const state: OauthServerStateData = res.locals.state || {}; - if (!state.redirect) { - throw new HttpException("No redirect address in state for request", HttpStatus.BAD_REQUEST); + /** + * Return username, display name and email suggestions for registering a user + * @param input The input data containing the registration token to retrieve suggestions for + */ + async getDataSuggestions(loginData: UserLoginData, strategy: Strategy): Promise { + const suggestions = strategy.getUserDataSuggestion(loginData); + + if (suggestions.username) { + const numUsers = await this.userService.countBy({ username: suggestions.username.trim() }); + if (numUsers > 0) { + return { + email: suggestions.email, + }; + } } - const url = new URL(state.redirect); + if (!suggestions.username && !suggestions.displayName && !suggestions.email) { + return {}; + } + return { + username: suggestions.username, + displayName: suggestions.displayName, + email: suggestions.email, + }; + } - const hadErrors = this.handleErrorCases(res.locals?.state, url); - if (!hadErrors) { - await this.generateCode(res.locals?.state, url); + protected override async useWithState( + req: Request, + res: Response, + state: AuthStateServerData & OAuthAuthorizeServerState & { strategy: Strategy } & { error?: any }, + next: (error?: Error | any) => void, + ): Promise { + if (!state.activeLogin) { + throw new OAuthHttpException("server_error", "No active login"); + } + const userLoginData = await state.activeLogin.loginInstanceFor; + if (state.request.scope.includes(TokenScope.LOGIN_SERVICE_REGISTER)) { + if (userLoginData.state === LoginState.WAITING_FOR_REGISTER) { + const url = new URL(state.request.redirect); + const token = await this.generateCode(state); + url.searchParams.append("code", token); + res.redirect(url.toString()); + } else { + throw new OAuthHttpException("invalid_request", "Login is not in register state"); + } + } else { + const encodedState = encodeURIComponent( + this.jwtService.sign({ request: state.request, authState: state.authState }), + ); + const token = await this.generateCode(state); + const suggestions = await this.getDataSuggestions(userLoginData, state.strategy); + const suggestionQuery = `&email=${encodeURIComponent( + suggestions.email ?? "", + )}&username=${encodeURIComponent( + suggestions.username ?? "", + )}&displayName=${encodeURIComponent(suggestions.displayName ?? "")}`; + const url = `/auth/flow/register?code=${token}&state=${encodedState}` + suggestionQuery; + res.redirect(url); } - res.status(302).setHeader("Location", url.toString()).setHeader("Content-Length", 0).end(); } } diff --git a/backend/src/api-internal/auth-token.middleware.ts b/backend/src/api-internal/auth-token.middleware.ts deleted file mode 100644 index 170b3fbf..00000000 --- a/backend/src/api-internal/auth-token.middleware.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { HttpException, HttpStatus, Injectable, Logger, NestMiddleware } from "@nestjs/common"; -import { Request, Response } from "express"; -import { TokenService } from "src/backend-services/token.service"; -import { AuthClient } from "src/model/postgres/AuthClient.entity"; -import { AuthClientService } from "src/model/services/auth-client.service"; -import { StrategiesMiddleware } from "src/strategies/strategies.middleware"; -import { StrategiesService } from "src/model/services/strategies.service"; -import { OAuthTokenAuthorizationCodeMiddleware } from "../api-oauth/oauth-token-authorization-code.middleware"; -import * as bcrypt from "bcrypt"; -import { ensureState } from "src/strategies/utils"; -import { OauthServerStateData } from "./auth-autorize.middleware"; -import { PostCredentialsMiddleware } from "./post-credentials.middleware"; -import { OAuthHttpException } from "../api-oauth/OAuthHttpException"; - -@Injectable() -export class AuthTokenMiddleware implements NestMiddleware { - private readonly logger = new Logger(AuthTokenMiddleware.name); - - constructor( - private readonly tokenService: TokenService, - private readonly authClientService: AuthClientService, - private readonly tokenResponseCodeMiddleware: OAuthTokenAuthorizationCodeMiddleware, - private readonly strategiesMiddleware: StrategiesMiddleware, - private readonly postCredentialsMiddleware: PostCredentialsMiddleware, - ) {} - - private async checkGivenClientSecretValidOrNotRequired(client: AuthClient, givenSecret?: string): Promise { - if (!client.requiresSecret && (!givenSecret || givenSecret.length == 0)) { - return true; - } - const hasCorrectClientSecret = ( - await Promise.all( - client.clientSecrets.map((hashedSecret) => - bcrypt.compare(givenSecret, hashedSecret.substring(hashedSecret.indexOf(";") + 1)), - ), - ) - ).includes(true); - if (hasCorrectClientSecret) { - return true; - } - return false; - } - - /** - * Performs the OAuth client authentication by checking the given client_id and client_secret - * in the Authorization header and in the body (both allowed according to OAuth spec). - * - * Flag can be set to return any client without secrets if desired to allow logins without client - * @param req The request object - * @param findAnyWithoutSecret Set to `true` to find any client that has no secret - * => allowing for login without a known client - * @returns The auth client that requested (or any without secret if flag ist set) - * or `null` if credentials invalid or none given - */ - private async getCallingClient(req: Request, findAnyWithoutSecret = false): Promise { - const auth_head = req.headers["authorization"]; - if (auth_head && auth_head.startsWith("Basic ")) { - const clientIdSecret = Buffer.from(auth_head.substring(6), "base64") - ?.toString("utf-8") - ?.split(":") - ?.map((text) => decodeURIComponent(text)); - - if (clientIdSecret && clientIdSecret.length == 2) { - const client = await this.authClientService.findOneBy({ - id: clientIdSecret[0], - }); - if (client && client.isValid) { - if (this.checkGivenClientSecretValidOrNotRequired(client, clientIdSecret[1])) { - return client; - } - } - return null; - } - } - - if (req.body.client_id) { - const client = await this.authClientService.findOneBy({ - id: req.body.client_id, - }); - if (client && client.isValid) { - if (this.checkGivenClientSecretValidOrNotRequired(client, req.body.client_secret)) { - return client; - } - } - return null; - } - - if (findAnyWithoutSecret) { - this.logger.log( - "Any client password authentication is enabled. Returning any client without client secret", - ); - const client = await this.authClientService.findOneBy({ - requiresSecret: false, - isValid: true, - }); - if (client && client.isValid) { - if (this.checkGivenClientSecretValidOrNotRequired(client, "")) { - return client; - } - } - } - return null; - } - - async use(req: Request, res: Response, next: () => void) { - ensureState(res); - - const grant_type = req.body.grant_type; - - const allowNoClient: unknown = process.env.GROPIUS_ALLOW_PASSWORD_TOKEN_MODE_WITHOUT_OAUTH_CLIENT; - const mayOmitClientId = - (allowNoClient === true || allowNoClient === "true") && - (grant_type == "password" || grant_type == "post_credentials"); - - const client = await this.getCallingClient(req, mayOmitClientId); - if (!client) { - throw new OAuthHttpException("unauthorized_client", "Unknown client or invalid client credentials"); - } - (res.locals.state as OauthServerStateData).client = client; - - switch (grant_type) { - case "refresh_token": //Request for new token using refresh token - //Fallthrough as resfrehsh token works the same as the initial code (both used to obtain new access token) - case "authorization_code": //Request for token based on obtained code - await this.tokenResponseCodeMiddleware.use(req, res, () => { - next(); - }); - break; - case "password": //Request for token immediately containing username + password - //Fallthrough to custom grant where all credentials are acceptd - case "post_credentials": //Extension/Non standard: Request for token immediately containing credentials - await this.postCredentialsMiddleware.use(req, res, () => { - next(); - }); - break; - case "client_credentials": //Request for token for stuff on client => not supported - default: - throw new HttpException( - { - error: "unsupported_grant_type", - error_description: "No grant_type given or unsupported type", - }, - HttpStatus.BAD_REQUEST, - ); - } - } -} diff --git a/backend/src/api-internal/auth.module.ts b/backend/src/api-internal/auth.module.ts index d5ab7ac1..343a55ac 100644 --- a/backend/src/api-internal/auth.module.ts +++ b/backend/src/api-internal/auth.module.ts @@ -5,50 +5,75 @@ import { ErrorHandlerMiddleware } from "../api-oauth/error-handler.middleware"; import { ModeExtractorMiddleware } from "./mode-extractor.middleware"; import { StrategiesMiddleware } from "../strategies/strategies.middleware"; import { StrategiesModule } from "../strategies/strategies.module"; -import { AuthAutorizeMiddleware } from "./auth-autorize.middleware"; import { AuthEndpointsController } from "./auth-endpoints.controller"; import { AuthRedirectMiddleware } from "./auth-redirect.middleware"; -import { AuthTokenMiddleware } from "./auth-token.middleware"; import { PostCredentialsMiddleware } from "./post-credentials.middleware"; import { ApiOauthModule } from "src/api-oauth/api-oauth.module"; import { OAuthErrorRedirectMiddleware } from "src/api-oauth/oauth-error-redirect.middleware"; +import { AuthAutorizeExtractMiddleware } from "./auth-autorize-extract.middleware"; +import { OAuthAuthorizeValidateMiddleware } from "src/api-oauth/oauth-authorize-validate.middleware"; @Module({ imports: [ModelModule, BackendServicesModule, StrategiesModule, ApiOauthModule], - providers: [AuthAutorizeMiddleware, AuthRedirectMiddleware, AuthTokenMiddleware, PostCredentialsMiddleware], + providers: [AuthAutorizeExtractMiddleware, AuthRedirectMiddleware, PostCredentialsMiddleware], controllers: [AuthEndpointsController], }) export class ApiInternalModule { private middlewares: { middlewares: NestMiddleware[]; path: string }[] = []; constructor( - private readonly authAutorize: AuthAutorizeMiddleware, + private readonly authAutorizeExtract: AuthAutorizeExtractMiddleware, private readonly authRedirect: AuthRedirectMiddleware, - private readonly authToken: AuthTokenMiddleware, private readonly modeExtractor: ModeExtractorMiddleware, private readonly strategies: StrategiesMiddleware, private readonly errorHandler: ErrorHandlerMiddleware, private readonly oauthErrorRedirect: OAuthErrorRedirectMiddleware, + private readonly oauthAuthorizeValidate: OAuthAuthorizeValidateMiddleware, ) { this.middlewares.push({ middlewares: [ + this.authAutorizeExtract, + this.oauthAuthorizeValidate, this.modeExtractor, - this.authAutorize, this.strategies, this.oauthErrorRedirect, this.errorHandler, ], - path: "internal/auth/redirect/:id/:mode", + path: "auth/internal/auth/redirect/:id/:mode", }); this.middlewares.push({ - middlewares: [this.strategies, this.authRedirect, this.oauthErrorRedirect, this.errorHandler], - path: "internal/auth/callback/:id", + middlewares: [ + this.strategies, + this.oauthAuthorizeValidate, + this.authRedirect, + this.oauthErrorRedirect, + this.errorHandler, + ], + path: "auth/internal/auth/callback/:id", }); this.middlewares.push({ - middlewares: [this.modeExtractor, this.authToken, this.errorHandler], - path: "internal/auth/submit/:id/:mode", + middlewares: [ + this.authAutorizeExtract, + this.oauthAuthorizeValidate, + this.modeExtractor, + this.authRedirect, + this.oauthErrorRedirect, + this.errorHandler, + ], + path: "auth/internal/auth/submit/:id/:mode", + }); + + this.middlewares.push({ + middlewares: [ + this.authAutorizeExtract, + this.oauthAuthorizeValidate, + this.authRedirect, + this.oauthErrorRedirect, + this.errorHandler, + ], + path: "auth/internal/auth/register", }); } diff --git a/backend/src/api-internal/mode-extractor.middleware.ts b/backend/src/api-internal/mode-extractor.middleware.ts index d11e9899..a0befa7f 100644 --- a/backend/src/api-internal/mode-extractor.middleware.ts +++ b/backend/src/api-internal/mode-extractor.middleware.ts @@ -1,27 +1,31 @@ -import { Injectable, NestMiddleware } from "@nestjs/common"; +import { Injectable } from "@nestjs/common"; import { Request, Response } from "express"; -import { ActiveLoginService } from "src/model/services/active-login.service"; -import { StrategyInstanceService } from "src/model/services/strategy-instance.service"; -import { AuthFunction, AuthStateData } from "../strategies/AuthResult"; -import { StrategiesService } from "../model/services/strategies.service"; -import { ensureState } from "../strategies/utils"; +import { AuthFunction, AuthStateServerData } from "../strategies/AuthResult"; +import { StateMiddleware } from "src/api-oauth/StateMiddleware"; @Injectable() -export class ModeExtractorMiddleware implements NestMiddleware { - async use(req: Request, res: Response, next: () => void) { - ensureState(res); +export class ModeExtractorMiddleware extends StateMiddleware<{}, AuthStateServerData> { + protected override async useWithState( + req: Request, + res: Response, + state: { error?: any }, + next: (error?: Error | any) => void, + ): Promise { + let authFunction: AuthFunction; switch (req.params.mode) { case "register": - (res.locals.state as AuthStateData).function = AuthFunction.REGISTER; + authFunction = AuthFunction.REGISTER; break; case "register-sync": - (res.locals.state as AuthStateData).function = AuthFunction.REGISTER_WITH_SYNC; + authFunction = AuthFunction.REGISTER_WITH_SYNC; break; case "login": - default: - (res.locals.state as AuthStateData).function = AuthFunction.LOGIN; + authFunction = AuthFunction.LOGIN; break; + default: + throw new Error("Invalid mode"); } + this.appendState(res, { authState: { function: authFunction } }); next(); } } diff --git a/backend/src/api-internal/post-credentials.middleware.ts b/backend/src/api-internal/post-credentials.middleware.ts index 9af69563..8a6e3ae9 100644 --- a/backend/src/api-internal/post-credentials.middleware.ts +++ b/backend/src/api-internal/post-credentials.middleware.ts @@ -1,6 +1,6 @@ import { Injectable, NestMiddleware } from "@nestjs/common"; import { Request, Response } from "express"; -import { AuthStateData } from "src/strategies/AuthResult"; +import { AuthStateServerData } from "src/strategies/AuthResult"; import { StrategiesMiddleware } from "src/strategies/strategies.middleware"; import { ensureState } from "src/strategies/utils"; import { OAuthHttpException } from "../api-oauth/OAuthHttpException"; @@ -11,7 +11,7 @@ export class PostCredentialsMiddleware implements NestMiddleware { async use(req: Request, res: Response, next: (error?: any) => void) { ensureState(res); - let state: AuthStateData = res.locals.state; + let state: AuthStateServerData = res.locals.state; const mockRes = {}; for (const key in res) { if (Object.prototype.hasOwnProperty.call(res, key)) { diff --git a/backend/src/api-login/dto/user-inputs.dto.ts b/backend/src/api-login/dto/user-inputs.dto.ts index 136e5d17..6782faf0 100644 --- a/backend/src/api-login/dto/user-inputs.dto.ts +++ b/backend/src/api-login/dto/user-inputs.dto.ts @@ -125,61 +125,3 @@ export class CreateUserAsAdminInput extends BaseUserInput { return input; } } - -/** - * Status of the returned user data suggestion and information on usability - */ -export enum UserDataSuggestionStatus { - /** - * No suggestions are returned, as the user the registration token was for is already registered - */ - ALREADY_REGISTERED = "already-registered", - - /** - * No suggestion for the username is given, as the username suggested is already taken - * This might also mean that the user only wants to link an accound and not register new - */ - USERNAME_TAKEN = "username-taken", - - /** - * No suggestions can be made as no data is available - */ - NO_DATA = "no-data", - - /** - * The sugestions contains usable data - */ - OK = "ok", -} - -/** - * Return data of the user data sugestion endpoint - */ -export class UserDataSuggestionResponse { - /** - * Status of the returned user data suggestion and information on usability - */ - status: UserDataSuggestionStatus; - - /** - * A potential username to use for the registration. - * If one is given, it was free the moment the suggestion is retrieved - * - * @example "testUser" - */ - username?: string; - - /** - * A potential name to display in the UI for the new user. - * - * @example "Test User" - */ - displayName?: string; - - /** - * A potential email of the new user. - * - * @example "test-user@example.com" - */ - email?: string; -} diff --git a/backend/src/api-login/register.controller.ts b/backend/src/api-login/register.controller.ts index 6a29026b..11ef0176 100644 --- a/backend/src/api-login/register.controller.ts +++ b/backend/src/api-login/register.controller.ts @@ -23,7 +23,7 @@ import { ApiStateData } from "./ApiStateData"; import { CheckAccessTokenGuard, NeedsAdmin } from "./check-access-token.guard"; import { CheckRegistrationTokenService } from "./check-registration-token.service"; import { AdminLinkUserInput, RegistrationTokenInput } from "./dto/link-user.dto"; -import { SelfRegisterUserInput, UserDataSuggestionResponse, UserDataSuggestionStatus } from "./dto/user-inputs.dto"; +import { SelfRegisterUserInput, } from "./dto/user-inputs.dto"; /** * Controller for handling self registration of new users as well as linking of existing users to new loginData @@ -37,65 +37,8 @@ export class RegisterController { private readonly userService: LoginUserService, private readonly activeLoginService: ActiveLoginService, private readonly backendUserSerivce: BackendUserService, - private readonly strategiesSerivce: StrategiesService, ) {} - /** - * Return username, display name and email suggestions for registering a user - * @param input The input data containing the registration token to retrieve suggestions for - */ - @Post("data-suggestion") - @ApiOperation({ summary: "Return username, display name and email suggestions for registering a user" }) - @ApiOkResponse({ - type: UserDataSuggestionResponse, - description: "If valid token and successfull (partial) suggested data for the new user", - }) - @ApiUnauthorizedResponse({ - description: - "If the given registration token is not/no longer valid or the registration time frame has expired", - }) - @ApiBadRequestResponse({ - description: "If the input data is invalid", - }) - async getDataSuggestions(@Body() input: RegistrationTokenInput): Promise { - RegistrationTokenInput.check(input); - const { loginData, activeLogin } = await this.checkRegistrationTokenService.getActiveLoginAndLoginDataForToken( - input.register_token, - ); - const user = await loginData.user; - if (!!user) { - return { - status: UserDataSuggestionStatus.ALREADY_REGISTERED, - }; - } - - const strategy = this.strategiesSerivce.getStrategyByName((await loginData.strategyInstance).type); - const suggestions = strategy.getUserDataSuggestion(loginData); - - if (suggestions.username) { - const numUsers = await this.userService.countBy({ username: suggestions.username.trim() }); - if (numUsers > 0) { - return { - status: UserDataSuggestionStatus.USERNAME_TAKEN, - email: suggestions.email, - }; - } - } - - if (!suggestions.username && !suggestions.displayName && !suggestions.email) { - return { - status: UserDataSuggestionStatus.NO_DATA, - }; - } - - return { - status: UserDataSuggestionStatus.OK, - username: suggestions.username, - displayName: suggestions.displayName, - email: suggestions.email, - }; - } - /** * Given user data and a registration token, this will create a new user for the registration. * The user will also be created in the backend. diff --git a/backend/src/api-oauth/StateMiddleware.ts b/backend/src/api-oauth/StateMiddleware.ts index 6f013215..84920e71 100644 --- a/backend/src/api-oauth/StateMiddleware.ts +++ b/backend/src/api-oauth/StateMiddleware.ts @@ -34,6 +34,6 @@ export abstract class StateMiddleware = {}, T exte } protected appendState(res: Response, appendedState: Partial & { error?: any } | { error?: any }) { - res.locals.state = { ...res.locals.state, ...appendedState }; + Object.assign(res.locals.state, appendedState); } } diff --git a/backend/src/api-oauth/api-oauth.module.ts b/backend/src/api-oauth/api-oauth.module.ts index fde0ce74..71a02e31 100644 --- a/backend/src/api-oauth/api-oauth.module.ts +++ b/backend/src/api-oauth/api-oauth.module.ts @@ -44,12 +44,12 @@ export class ApiOauthModule { this.oauthErrorRedirect, this.errorHandler, ], - path: "oauth/authorize", + path: "auth/oauth/authorize", }); this.middlewares.push({ middlewares: [this.oauthToken, this.oauthTokenAuthorizationCode, this.errorHandler], - path: "oauth/token", + path: "auth/oauth/token", }); } diff --git a/backend/src/api-oauth/error-handler.middleware.ts b/backend/src/api-oauth/error-handler.middleware.ts index 3a32b4d9..49008d66 100644 --- a/backend/src/api-oauth/error-handler.middleware.ts +++ b/backend/src/api-oauth/error-handler.middleware.ts @@ -4,7 +4,7 @@ import { StateMiddleware } from "./StateMiddleware"; @Injectable() export class ErrorHandlerMiddleware extends StateMiddleware<{}, {}> { - protected async useWithState( + protected override async useWithState( req: Request, res: Response, state: { error?: any }, @@ -13,7 +13,7 @@ export class ErrorHandlerMiddleware extends StateMiddleware<{}, {}> { next(); } - protected useWithError( + protected override useWithError( req: Request, res: Response, state: { error?: any }, diff --git a/backend/src/api-oauth/oauth-authorize-extract.middleware.ts b/backend/src/api-oauth/oauth-authorize-extract.middleware.ts index 025254ae..a80edcce 100644 --- a/backend/src/api-oauth/oauth-authorize-extract.middleware.ts +++ b/backend/src/api-oauth/oauth-authorize-extract.middleware.ts @@ -10,7 +10,7 @@ export class OAuthAuthorizeExtractMiddleware extends StateMiddleware<{}, OAuthAu super(); } - protected async useWithState( + protected override async useWithState( req: Request, res: Response, state: { error?: any }, diff --git a/backend/src/api-oauth/oauth-authorize-redirect.middleware.ts b/backend/src/api-oauth/oauth-authorize-redirect.middleware.ts index 0fcd60a8..92ca7e6a 100644 --- a/backend/src/api-oauth/oauth-authorize-redirect.middleware.ts +++ b/backend/src/api-oauth/oauth-authorize-redirect.middleware.ts @@ -3,13 +3,19 @@ import { Request, Response } from "express"; import { StateMiddleware } from "./StateMiddleware"; import { OAuthAuthorizeServerState } from "./OAuthAuthorizeServerState"; import { TokenScope } from "src/backend-services/token.service"; +import { JwtService } from "@nestjs/jwt"; @Injectable() export class OAuthAuthorizeRedirectMiddleware extends StateMiddleware< OAuthAuthorizeServerState, OAuthAuthorizeServerState > { - protected async useWithState( + + constructor(private readonly jwtService: JwtService) { + super(); + } + + protected override async useWithState( req: Request, res: Response, state: OAuthAuthorizeServerState & { error?: any }, @@ -18,10 +24,7 @@ export class OAuthAuthorizeRedirectMiddleware extends StateMiddleware< const target = state.request.scope.includes(TokenScope.LOGIN_SERVICE_REGISTER) ? "register-additional" : "login"; - const encodedState = encodeURIComponent(JSON.stringify(state.request)); - res.status(302) - .setHeader("Location", `/auth/flow/${target}?state=${encodedState}`) - .setHeader("Content-Length", 0) - .end(); + const encodedState = encodeURIComponent(this.jwtService.sign({ request: state.request })); + res.redirect(`/auth/flow/${target}?state=${encodedState}`); } } diff --git a/backend/src/api-oauth/oauth-authorize-validate.middleware.ts b/backend/src/api-oauth/oauth-authorize-validate.middleware.ts index 431d7933..7ad65a5d 100644 --- a/backend/src/api-oauth/oauth-authorize-validate.middleware.ts +++ b/backend/src/api-oauth/oauth-authorize-validate.middleware.ts @@ -15,7 +15,7 @@ export class OAuthAuthorizeValidateMiddleware extends StateMiddleware< super(); } - protected async useWithState( + protected override async useWithState( req: Request, res: Response, state: OAuthAuthorizeServerState & { error?: any }, diff --git a/backend/src/api-oauth/oauth-error-redirect.middleware.ts b/backend/src/api-oauth/oauth-error-redirect.middleware.ts index cbe69922..f6f95371 100644 --- a/backend/src/api-oauth/oauth-error-redirect.middleware.ts +++ b/backend/src/api-oauth/oauth-error-redirect.middleware.ts @@ -5,7 +5,7 @@ import { OAuthAuthorizeServerState } from "./OAuthAuthorizeServerState"; @Injectable() export class OAuthErrorRedirectMiddleware extends StateMiddleware { - protected async useWithState( + protected override async useWithState( req: Request, res: Response, state: OAuthAuthorizeServerState & { error?: any }, @@ -14,7 +14,7 @@ export class OAuthErrorRedirectMiddleware extends StateMiddleware { +export class OAuthTokenAuthorizationCodeMiddleware extends StateMiddleware<{ client: AuthClient }, AuthStateServerData> { private readonly logger = new Logger(OAuthTokenAuthorizationCodeMiddleware.name); constructor( private readonly activeLoginService: ActiveLoginService, @@ -21,7 +21,7 @@ export class OAuthTokenAuthorizationCodeMiddleware extends StateMiddleware<{ cli throw new OAuthHttpException("invalid_grant", "Given code was invalid or expired"); } - protected async useWithState( + protected override async useWithState( req: Request, res: Response, state: { client: AuthClient } & { error?: any }, diff --git a/backend/src/api-oauth/oauth-token.controller.ts b/backend/src/api-oauth/oauth-token.controller.ts index 568ba13c..b3aed57b 100644 --- a/backend/src/api-oauth/oauth-token.controller.ts +++ b/backend/src/api-oauth/oauth-token.controller.ts @@ -8,7 +8,7 @@ import { LoginState, UserLoginData } from "src/model/postgres/UserLoginData.enti import { ActiveLoginService } from "src/model/services/active-login.service"; import { AuthClientService } from "src/model/services/auth-client.service"; import { OpenApiTag } from "src/openapi-tag"; -import { AuthStateData } from "src/strategies/AuthResult"; +import { AuthStateServerData } from "src/strategies/AuthResult"; import { ensureState } from "src/strategies/utils"; import { OAuthHttpException } from "../api-oauth/OAuthHttpException"; @@ -142,7 +142,7 @@ export class OAuthTokenController { "No client id/authentication given or authentication invalid", ); } - let activeLogin = (res.locals.state as AuthStateData)?.activeLogin; + let activeLogin = (res.locals.state as AuthStateServerData)?.activeLogin; if (typeof activeLogin == "string") { activeLogin = await this.activeLoginService.findOneByOrFail({ id: activeLogin, diff --git a/backend/src/api-oauth/oauth-token.middleware.ts b/backend/src/api-oauth/oauth-token.middleware.ts index 5b1bcb91..83bd144d 100644 --- a/backend/src/api-oauth/oauth-token.middleware.ts +++ b/backend/src/api-oauth/oauth-token.middleware.ts @@ -81,7 +81,7 @@ export class OauthTokenMiddleware extends StateMiddleware<{}, { client: AuthClie return null; } - protected async useWithState( + protected override async useWithState( req: Request, res: Response, state: { error?: any }, diff --git a/backend/src/initialization/create-default-user.service.ts b/backend/src/initialization/create-default-user.service.ts index 295faa7b..39a3fe18 100644 --- a/backend/src/initialization/create-default-user.service.ts +++ b/backend/src/initialization/create-default-user.service.ts @@ -69,7 +69,7 @@ export class CreateDefaultUserService { const result = await strategy.performAuth( strategyInstance, - {}, + undefined, { body: postBody, }, diff --git a/backend/src/strategies/AuthResult.ts b/backend/src/strategies/AuthResult.ts index be072994..d058d45f 100644 --- a/backend/src/strategies/AuthResult.ts +++ b/backend/src/strategies/AuthResult.ts @@ -1,20 +1,20 @@ import { ActiveLogin } from "src/model/postgres/ActiveLogin.entity"; -import { LoginUser } from "src/model/postgres/LoginUser.entity"; import { UserLoginData } from "src/model/postgres/UserLoginData.entity"; export enum AuthFunction { LOGIN = "LOGIN", REGISTER = "REG", REGISTER_WITH_SYNC = "REG_SYNC", - REGISTER_ADDITIONAL = "REG_ADD", - REGISTER_ADDITIONAL_WITH_SYNC = "REG_ADD_SYNC", } export interface AuthStateData { - function?: AuthFunction; - activeLogin?: ActiveLogin | string; - authErrorMessage?: string; - authErrorType?: string; + function: AuthFunction; + activeLogin?: string; +} + +export interface AuthStateServerData { + authState: AuthStateData; + activeLogin?: ActiveLogin; } export interface AuthResult { diff --git a/backend/src/strategies/Strategy.ts b/backend/src/strategies/Strategy.ts index 4a6913a2..1c8066bc 100644 --- a/backend/src/strategies/Strategy.ts +++ b/backend/src/strategies/Strategy.ts @@ -1,13 +1,11 @@ -import * as passport from "passport"; import { CreateStrategyInstanceInput } from "src/api-login/strategy/dto/create-strategy-instance.dto"; import { UpdateStrategyInstanceInput } from "src/api-login/strategy/dto/update-strategy-instance.dto"; -import { ActiveLogin } from "src/model/postgres/ActiveLogin.entity"; -import { LoginUser } from "src/model/postgres/LoginUser.entity"; import { StrategyInstance } from "src/model/postgres/StrategyInstance.entity"; import { UserLoginData } from "src/model/postgres/UserLoginData.entity"; import { StrategiesService } from "src/model/services/strategies.service"; import { StrategyInstanceService } from "src/model/services/strategy-instance.service"; -import { AuthResult, AuthStateData } from "./AuthResult"; +import { AuthResult, AuthStateServerData } from "./AuthResult"; +import { OAuthAuthorizeServerState } from "src/api-oauth/OAuthAuthorizeServerState"; export interface StrategyVariable { name: string; @@ -16,6 +14,12 @@ export interface StrategyVariable { nullable?: boolean; } +export interface PerformAuthResult { + result: AuthResult | null; + returnedState: Partial & Pick>; + info: any; +} + export abstract class Strategy { constructor( public readonly typeName: string, @@ -237,14 +241,10 @@ export abstract class Strategy { abstract performAuth( strategyInstance: StrategyInstance, - authStateData: AuthStateData | object, + state: (AuthStateServerData & OAuthAuthorizeServerState) | undefined, req: any, res: any, - ): Promise<{ - result: AuthResult | null; - returnedState: AuthStateData; - info: any; - }>; + ): Promise; toJSON() { return { diff --git a/backend/src/strategies/StrategyUsingPassport.ts b/backend/src/strategies/StrategyUsingPassport.ts index 52677e0e..709ff263 100644 --- a/backend/src/strategies/StrategyUsingPassport.ts +++ b/backend/src/strategies/StrategyUsingPassport.ts @@ -1,11 +1,12 @@ import * as passport from "passport"; -import { Strategy } from "./Strategy"; +import { PerformAuthResult, Strategy } from "./Strategy"; import { StrategyInstance } from "src/model/postgres/StrategyInstance.entity"; -import { AuthStateData, AuthResult } from "./AuthResult"; +import { AuthStateServerData, AuthResult } from "./AuthResult"; import { JwtService } from "@nestjs/jwt"; import { StrategyInstanceService } from "src/model/services/strategy-instance.service"; import { StrategiesService } from "src/model/services/strategies.service"; import { Logger } from "@nestjs/common"; +import { OAuthAuthorizeServerState } from "src/api-oauth/OAuthAuthorizeServerState"; export abstract class StrategyUsingPassport extends Strategy { private readonly logger = new Logger(StrategyUsingPassport.name); @@ -36,7 +37,7 @@ export abstract class StrategyUsingPassport extends Strategy { protected getAdditionalPassportOptions( strategyInstance: StrategyInstance, - authStateData: AuthStateData | object, + authStateData: (AuthStateServerData & OAuthAuthorizeServerState) | undefined, ): passport.AuthenticateOptions { return {}; } @@ -56,14 +57,10 @@ export abstract class StrategyUsingPassport extends Strategy { public override async performAuth( strategyInstance: StrategyInstance, - authStateData: AuthStateData | object, + state: (AuthStateServerData & OAuthAuthorizeServerState) | undefined, req: any, res: any, - ): Promise<{ - result: AuthResult | null; - returnedState: AuthStateData; - info: any; - }> { + ): Promise { return new Promise((resolve, reject) => { const passportStrategy = this.getPassportStrategyInstanceFor(strategyInstance); const jwtService = this.passportJwtService; @@ -71,8 +68,8 @@ export abstract class StrategyUsingPassport extends Strategy { passportStrategy, { session: false, - state: jwtService.sign(authStateData), // TODO: check if an expiration and/or an additional random value are needed - ...this.getAdditionalPassportOptions(strategyInstance, authStateData), + state: jwtService.sign({ request: state.request, authState: state.authState }), // TODO: check if an expiration and/or an additional random value are needed + ...this.getAdditionalPassportOptions(strategyInstance, state), }, (err, user: AuthResult | false, info) => { if (err) { @@ -83,8 +80,6 @@ export abstract class StrategyUsingPassport extends Strategy { returnedState = jwtService.verify(info.state); } else if (info.state) { reject("State not returned as JWT"); - } else if (authStateData) { - returnedState = authStateData; } resolve({ result: user || null, returnedState, info }); } diff --git a/backend/src/strategies/github/github.service.ts b/backend/src/strategies/github/github.service.ts index 8fb6e011..36a7864b 100644 --- a/backend/src/strategies/github/github.service.ts +++ b/backend/src/strategies/github/github.service.ts @@ -5,12 +5,13 @@ import * as passportGithub from "passport-github2"; import { StrategyInstance } from "src/model/postgres/StrategyInstance.entity"; import * as passport from "passport"; import { UserLoginDataService } from "src/model/services/user-login-data.service"; -import { AuthFunction, AuthResult, AuthStateData } from "../AuthResult"; +import { AuthFunction, AuthResult, AuthStateServerData } from "../AuthResult"; import { StrategyUsingPassport } from "../StrategyUsingPassport"; import { JwtService } from "@nestjs/jwt"; import { UserLoginData } from "src/model/postgres/UserLoginData.entity"; import { ActiveLoginService } from "src/model/services/active-login.service"; import { checkType } from "../utils"; +import { OAuthAuthorizeServerState } from "src/api-oauth/OAuthAuthorizeServerState"; @Injectable() export class GithubStrategyService extends StrategyUsingPassport { @@ -136,9 +137,9 @@ export class GithubStrategyService extends StrategyUsingPassport { protected override getAdditionalPassportOptions( strategyInstance: StrategyInstance, - authStateData: object | AuthStateData, + authStateData: (AuthStateServerData & OAuthAuthorizeServerState) | undefined, ): passport.AuthenticateOptions { - const mode = (authStateData as AuthStateData).function; + const mode = authStateData?.authState.function ?? AuthFunction.LOGIN; if (mode == AuthFunction.REGISTER_WITH_SYNC) { return { scope: ["scope", "user:email", "repo"], diff --git a/backend/src/strategies/jira/jira.service.ts b/backend/src/strategies/jira/jira.service.ts index 87e29e64..c9593ce4 100644 --- a/backend/src/strategies/jira/jira.service.ts +++ b/backend/src/strategies/jira/jira.service.ts @@ -5,7 +5,7 @@ import * as passportJira from "passport-atlassian-oauth2"; import { StrategyInstance } from "src/model/postgres/StrategyInstance.entity"; import * as passport from "passport"; import { UserLoginDataService } from "src/model/services/user-login-data.service"; -import { AuthFunction, AuthResult, AuthStateData } from "../AuthResult"; +import { AuthResult } from "../AuthResult"; import { StrategyUsingPassport } from "../StrategyUsingPassport"; import { JwtService } from "@nestjs/jwt"; import { UserLoginData } from "src/model/postgres/UserLoginData.entity"; diff --git a/backend/src/strategies/perform-auth-function.service.ts b/backend/src/strategies/perform-auth-function.service.ts index d592c14b..62ea925c 100644 --- a/backend/src/strategies/perform-auth-function.service.ts +++ b/backend/src/strategies/perform-auth-function.service.ts @@ -6,9 +6,10 @@ import { LoginState, UserLoginData } from "src/model/postgres/UserLoginData.enti import { ActiveLoginService } from "src/model/services/active-login.service"; import { LoginUserService } from "src/model/services/login-user.service"; import { UserLoginDataService } from "src/model/services/user-login-data.service"; -import { AuthStateData, AuthFunction, AuthResult } from "./AuthResult"; +import { AuthStateServerData, AuthFunction, AuthResult } from "./AuthResult"; import { StrategiesService } from "../model/services/strategies.service"; import { Strategy } from "./Strategy"; +import { OAuthHttpException } from "src/api-oauth/OAuthHttpException"; /** * Contains the logic how the system is supposed to create and link @@ -24,13 +25,17 @@ export class PerformAuthFunctionService { private readonly activeLoginService: ActiveLoginService, private readonly userLoginDataService: UserLoginDataService, private readonly strategiesService: StrategiesService, - ) { } + ) {} - public checkFunctionIsAllowed(state: AuthStateData, instance: StrategyInstance, strategy: Strategy): string | null { - switch (state?.function) { + public checkFunctionIsAllowed( + state: AuthStateServerData, + instance: StrategyInstance, + strategy: Strategy, + ): string | null { + switch (state?.authState?.function) { case AuthFunction.REGISTER_WITH_SYNC: if (!strategy.canSync) { - state.function = AuthFunction.REGISTER; + state.authState.function = AuthFunction.REGISTER; } //Fallthrough to check if registration is possible at all case AuthFunction.REGISTER: @@ -64,23 +69,16 @@ export class PerformAuthFunctionService { return this.activeLoginService.save(activeLogin); } - private async loginExistingUser(authResult: AuthResult, instance: StrategyInstance): Promise { + private async loginExistingUser(authResult: AuthResult, instance: StrategyInstance): Promise { this.logger.debug("Logging in user"); - return { - activeLogin: await this.createActiveLogin( - instance, - authResult.dataActiveLogin, - authResult.loginData, - false, - ), - }; + return this.createActiveLogin(instance, authResult.dataActiveLogin, authResult.loginData, false); } private async continueExistingRegistration( authResult: AuthResult, instance: StrategyInstance, supportsSync: boolean, - ): Promise { + ): Promise { let loginData = authResult.loginData; loginData.data = authResult.dataUserLoginData; const newExpiryDate = new Date(Date.now() + parseInt(process.env.GROPIUS_REGISTRATION_EXPIRATION_TIME_MS, 10)); @@ -88,21 +86,14 @@ export class PerformAuthFunctionService { loginData.expires = newExpiryDate; } loginData = await this.userLoginDataService.save(loginData); - return { - activeLogin: await this.createActiveLogin( - instance, - authResult.dataActiveLogin, - authResult.loginData, - supportsSync, - ), - }; + return this.createActiveLogin(instance, authResult.dataActiveLogin, authResult.loginData, supportsSync); } private async registerNewUser( authResult: AuthResult, instance: StrategyInstance, supportsSync: boolean, - ): Promise { + ): Promise { this.logger.debug("Registering new user with login data", authResult.dataUserLoginData); let loginData = new UserLoginData(); loginData.data = authResult.dataUserLoginData; @@ -110,75 +101,67 @@ export class PerformAuthFunctionService { loginData.state = LoginState.WAITING_FOR_REGISTER; loginData.strategyInstance = Promise.resolve(instance); loginData = await this.userLoginDataService.save(loginData); - return { - activeLogin: await this.createActiveLogin(instance, authResult.dataActiveLogin, loginData, supportsSync), - }; + return this.createActiveLogin(instance, authResult.dataActiveLogin, loginData, supportsSync); } public async performRequestedAction( authResult: AuthResult, - state: AuthStateData, + state: AuthStateServerData, instance: StrategyInstance, strategy: Strategy, - ): Promise { + ): Promise { + const authFunction = state.authState.function; const wantsToDoImplicitRegister = - strategy.allowsImplicitSignup && instance.doesImplicitRegister && state.function == AuthFunction.LOGIN; + strategy.allowsImplicitSignup && instance.doesImplicitRegister && authFunction == AuthFunction.LOGIN; if (authResult.loginData) { // sucessfully found login data matching the authentication if (authResult.loginData.expires != null && authResult.loginData.expires <= new Date()) { // Found login data is expired => // shouldn't happen as expired login data are filtered when searhcing for them - return { - authErrorMessage: - "The login using this strategy instance has expired. " + - "If you were just registering, try starting the registration again. " + - "If this error happens again, something internally went wrong.", - }; + throw new OAuthHttpException("server_error", "Login data expired, please try registering again"); } switch (authResult.loginData.state) { case LoginState.WAITING_FOR_REGISTER: if ( - state.function == AuthFunction.REGISTER || - state.function == AuthFunction.REGISTER_WITH_SYNC || + authFunction == AuthFunction.REGISTER || + authFunction == AuthFunction.REGISTER_WITH_SYNC || wantsToDoImplicitRegister ) { return this.continueExistingRegistration( authResult, instance, - state.function == AuthFunction.REGISTER_WITH_SYNC, + authFunction == AuthFunction.REGISTER_WITH_SYNC, + ); + } else if (authFunction == AuthFunction.LOGIN) { + throw new OAuthHttpException( + "server_error", + "For these credentials a registration process is still running. Complete (or restart) the registration before logging in", ); - } else if (state.function == AuthFunction.LOGIN) { - return { - authErrorMessage: - "For these credentials a registration process is still running. " + - "Complete (or restart) the registration before logging in", - }; } case LoginState.BLOCKED: - return { - authErrorMessage: - "The login to this account using this specific strategy instance " + - "was blocked by the administrator.", - }; + throw new OAuthHttpException( + "server_error", + "The login to this account using this specific strategy instance was blocked by the administrator.", + ); case LoginState.VALID: return this.loginExistingUser(authResult, instance); } } else { if ( - state.function == AuthFunction.REGISTER || - state.function == AuthFunction.REGISTER_WITH_SYNC || + authFunction == AuthFunction.REGISTER || + authFunction == AuthFunction.REGISTER_WITH_SYNC || wantsToDoImplicitRegister ) { if (!authResult.mayRegister) { this.logger.warn("Strategy did not provide existing loginData but it did not allow registering"); - return { authErrorMessage: "Invalid user credentials." }; + throw new OAuthHttpException("server_error", "Invalid user credentials."); } - return this.registerNewUser(authResult, instance, state.function == AuthFunction.REGISTER_WITH_SYNC); - } else if (state.function == AuthFunction.LOGIN && !wantsToDoImplicitRegister) { - return { authErrorMessage: "Invalid user credentials." }; + return this.registerNewUser(authResult, instance, authFunction == AuthFunction.REGISTER_WITH_SYNC); + } else if (authFunction == AuthFunction.LOGIN && !wantsToDoImplicitRegister) { + throw new OAuthHttpException("server_error", "Invalid user credentials."); } } - return { authErrorMessage: "Unknown error during authentication" }; + throw new OAuthHttpException("server_error", "Unknown error during authentication"); } } diff --git a/backend/src/strategies/strategies.middleware.ts b/backend/src/strategies/strategies.middleware.ts index 759a5db8..0a51924c 100644 --- a/backend/src/strategies/strategies.middleware.ts +++ b/backend/src/strategies/strategies.middleware.ts @@ -3,21 +3,30 @@ import { Request, Response } from "express"; import { ImsUserFindingService } from "src/backend-services/ims-user-finding.service"; import { StrategyInstance } from "src/model/postgres/StrategyInstance.entity"; import { StrategyInstanceService } from "src/model/services/strategy-instance.service"; -import { AuthFunction, AuthStateData } from "./AuthResult"; +import { AuthStateServerData } from "./AuthResult"; import { PerformAuthFunctionService } from "./perform-auth-function.service"; import { StrategiesService } from "../model/services/strategies.service"; import { Strategy } from "./Strategy"; -import { ensureState } from "./utils"; +import { StateMiddleware } from "src/api-oauth/StateMiddleware"; +import { OAuthHttpException } from "src/api-oauth/OAuthHttpException"; +import { OAuthAuthorizeServerState } from "src/api-oauth/OAuthAuthorizeServerState"; +import { AuthClientService } from "src/model/services/auth-client.service"; @Injectable() -export class StrategiesMiddleware implements NestMiddleware { +export class StrategiesMiddleware extends StateMiddleware< + AuthStateServerData & OAuthAuthorizeServerState, + AuthStateServerData & OAuthAuthorizeServerState +> { private readonly logger = new Logger(StrategiesMiddleware.name); constructor( private readonly strategiesService: StrategiesService, private readonly strategyInstanceService: StrategyInstanceService, private readonly performAuthFunctionService: PerformAuthFunctionService, private readonly imsUserFindingService: ImsUserFindingService, - ) {} + private readonly authClientService: AuthClientService, + ) { + super(); + } private async idToStrategyInstance(id: string): Promise { if (!id) { @@ -30,13 +39,13 @@ export class StrategiesMiddleware implements NestMiddleware { return instance; } - async performImsUserSearchIfNeeded(state: AuthStateData, instance: StrategyInstance, strategy: Strategy) { + async performImsUserSearchIfNeeded(state: AuthStateServerData, instance: StrategyInstance, strategy: Strategy) { if (strategy.canSync && instance.isSyncActive) { if (typeof state.activeLogin == "object" && state.activeLogin.id) { const imsUserSearchOnModes = process.env.GROPIUS_PERFORM_IMS_USER_SEARCH_ON.split(",").filter( (s) => !!s, ); - if (imsUserSearchOnModes.includes(state.function)) { + if (imsUserSearchOnModes.includes(state.authState.function)) { const loginData = await state.activeLogin.loginInstanceFor; try { await this.imsUserFindingService.createAndLinkImsUsersForLoginData(loginData); @@ -51,41 +60,42 @@ export class StrategiesMiddleware implements NestMiddleware { } } - async use(req: Request, res: Response, next: () => void) { - ensureState(res); - if ((res.locals.state as AuthStateData)?.authErrorMessage) { - next(); - } + protected override async useWithState( + req: Request, + res: Response, + state: AuthStateServerData & OAuthAuthorizeServerState & { error?: any }, + next: (error?: Error | any) => void, + ): Promise { const id = req.params.id; const instance = await this.idToStrategyInstance(id); const strategy = await this.strategiesService.getStrategyByName(instance.type); - const functionError = this.performAuthFunctionService.checkFunctionIsAllowed( - res.locals.state, - instance, - strategy, - ); + const functionError = this.performAuthFunctionService.checkFunctionIsAllowed(state, instance, strategy); if (functionError != null) { - (res.locals.state as AuthStateData).authErrorMessage = functionError; - return next(); + throw new OAuthHttpException("server_error", functionError); } - const result = await strategy.performAuth(instance, res.locals.state || {}, req, res); - res.locals.state = { ...res.locals.state, ...result.returnedState }; + const result = await strategy.performAuth(instance, state, req, res); + this.appendState(res, result.returnedState); + if (!state.client && state.request.clientId) { + state.client = await this.authClientService.findOneBy({ id: state.request.clientId }); + } const authResult = result.result; if (authResult) { - const executionResult = await this.performAuthFunctionService.performRequestedAction( + const activeLogin = await this.performAuthFunctionService.performRequestedAction( authResult, - res.locals.state, + state, instance, strategy, ); - res.locals.state = { ...res.locals.state, ...executionResult }; - await this.performImsUserSearchIfNeeded(res.locals.state, instance, strategy); + this.appendState(res, { activeLogin }); + await this.performImsUserSearchIfNeeded(state, instance, strategy); } else { - (res.locals.state as AuthStateData).authErrorMessage = - result.info?.message?.toString() || JSON.stringify(result.info) || "Login unsuccessfull"; + throw new OAuthHttpException( + "server_error", + result.info?.message?.toString() || JSON.stringify(result.info) || "Login unsuccessfull", + ); } this.logger.debug("Strategy Middleware completed. Calling next"); next(); From 1d67c888ad7bb85bc2b5d065b8c9fbafca7ccafe Mon Sep 17 00:00:00 2001 From: nk-coding Date: Tue, 9 Jul 2024 15:48:14 +0200 Subject: [PATCH 06/31] new register endpoint --- .../auth-autorize-extract.middleware.ts | 2 +- .../api-internal/auth-endpoints.controller.ts | 5 +- .../api-internal/auth-redirect.middleware.ts | 8 +- .../api-internal/auth-register.middleware.ts | 53 +++++++++ backend/src/api-internal/auth.module.ts | 6 +- .../post-credentials.middleware.ts | 47 -------- backend/src/api-login/register.controller.ts | 104 +----------------- .../api-oauth/OAuthAuthorizeServerState.ts | 3 +- ...uth-token-authorization-code.middleware.ts | 9 +- .../src/api-oauth/oauth-token.controller.ts | 30 ++--- backend/src/backend-services/token.service.ts | 81 +++++++------- .../model/services/user-login-data.service.ts | 69 +++++++++++- backend/src/strategies/strategies.module.ts | 2 +- 13 files changed, 197 insertions(+), 222 deletions(-) create mode 100644 backend/src/api-internal/auth-register.middleware.ts delete mode 100644 backend/src/api-internal/post-credentials.middleware.ts diff --git a/backend/src/api-internal/auth-autorize-extract.middleware.ts b/backend/src/api-internal/auth-autorize-extract.middleware.ts index c99ef377..ccda12d8 100644 --- a/backend/src/api-internal/auth-autorize-extract.middleware.ts +++ b/backend/src/api-internal/auth-autorize-extract.middleware.ts @@ -2,7 +2,7 @@ import { Injectable } from "@nestjs/common"; import { Request, Response } from "express"; import { AuthClientService } from "src/model/services/auth-client.service"; import { StateMiddleware } from "src/api-oauth/StateMiddleware"; -import { OAuthAuthorizeRequest, OAuthAuthorizeServerState } from "src/api-oauth/OAuthAuthorizeServerState"; +import { OAuthAuthorizeServerState } from "src/api-oauth/OAuthAuthorizeServerState"; import { JwtService } from "@nestjs/jwt"; @Injectable() diff --git a/backend/src/api-internal/auth-endpoints.controller.ts b/backend/src/api-internal/auth-endpoints.controller.ts index 1b4e509b..0e518f42 100644 --- a/backend/src/api-internal/auth-endpoints.controller.ts +++ b/backend/src/api-internal/auth-endpoints.controller.ts @@ -1,7 +1,8 @@ -import { Controller, Get, HttpException, HttpStatus, Param, Post } from "@nestjs/common"; +import { Body, Controller, Get, HttpException, HttpStatus, Param, Post } from "@nestjs/common"; import { ApiOperation, ApiParam, ApiTags } from "@nestjs/swagger"; import { OpenApiTag } from "src/openapi-tag"; import { AuthFunctionInput } from "./dto/auth-function.dto"; +import { SelfRegisterUserInput } from "src/api-login/dto/user-inputs.dto"; /** * Controller for the openapi generator to find the oauth server routes that are handeled exclusively in middleware. @@ -76,7 +77,7 @@ export class AuthEndpointsController { @Post("register") @ApiOperation({ summary: "Copmplete a registration" }) @ApiTags(OpenApiTag.INTERNAL_API) - registerEndpoint() { + registerEndpoint(@Body() input: SelfRegisterUserInput) { throw new HttpException( "This controller shouldn't be reached as all functionality is handeled in middleware", HttpStatus.INTERNAL_SERVER_ERROR, diff --git a/backend/src/api-internal/auth-redirect.middleware.ts b/backend/src/api-internal/auth-redirect.middleware.ts index e4647036..4fde77a3 100644 --- a/backend/src/api-internal/auth-redirect.middleware.ts +++ b/backend/src/api-internal/auth-redirect.middleware.ts @@ -1,11 +1,10 @@ -import { HttpException, HttpStatus, Injectable, Logger, NestMiddleware } from "@nestjs/common"; +import { Injectable, Logger } from "@nestjs/common"; import { Request, Response } from "express"; import { TokenScope, TokenService } from "src/backend-services/token.service"; import { ActiveLogin } from "src/model/postgres/ActiveLogin.entity"; -import { AuthClient } from "src/model/postgres/AuthClient.entity"; import { ActiveLoginService } from "src/model/services/active-login.service"; import { AuthClientService } from "src/model/services/auth-client.service"; -import { AuthFunction, AuthStateServerData } from "../strategies/AuthResult"; +import { AuthStateServerData } from "../strategies/AuthResult"; import { OAuthHttpException } from "src/api-oauth/OAuthHttpException"; import { StateMiddleware } from "src/api-oauth/StateMiddleware"; import { OAuthAuthorizeServerState } from "src/api-oauth/OAuthAuthorizeServerState"; @@ -86,9 +85,10 @@ export class AuthRedirectMiddleware extends StateMiddleware< const expiresIn = parseInt(process.env.GROPIUS_OAUTH_CODE_EXPIRATION_TIME_MS, 10); const codeJwtId = await this.assignActiveLoginToClient(state, expiresIn); const token = await this.tokenService.signActiveLoginCode( - typeof activeLogin == "string" ? activeLogin : activeLogin.id, + activeLogin.id, state.client.id, codeJwtId, + state.request.scope, expiresIn, ); this.logger.debug("Created token"); diff --git a/backend/src/api-internal/auth-register.middleware.ts b/backend/src/api-internal/auth-register.middleware.ts new file mode 100644 index 00000000..2ec06df5 --- /dev/null +++ b/backend/src/api-internal/auth-register.middleware.ts @@ -0,0 +1,53 @@ +import { HttpException, HttpStatus, Injectable } from "@nestjs/common"; +import { Request, Response } from "express"; +import { AuthClientService } from "src/model/services/auth-client.service"; +import { StateMiddleware } from "src/api-oauth/StateMiddleware"; +import { OAuthAuthorizeServerState } from "src/api-oauth/OAuthAuthorizeServerState"; +import { JwtService } from "@nestjs/jwt"; +import { AuthStateServerData } from "src/strategies/AuthResult"; +import { SelfRegisterUserInput } from "src/api-login/dto/user-inputs.dto"; +import { CheckRegistrationTokenService } from "src/api-login/check-registration-token.service"; +import { OAuthHttpException } from "src/api-oauth/OAuthHttpException"; +import { LoginUserService } from "src/model/services/login-user.service"; +import { BackendUserService } from "src/backend-services/backend-user.service"; +import { UserLoginDataService } from "src/model/services/user-login-data.service"; + +@Injectable() +export class AuthRegisterMiddleware extends StateMiddleware< + OAuthAuthorizeServerState & AuthStateServerData, + OAuthAuthorizeServerState & AuthStateServerData +> { + constructor( + private readonly authClientService: AuthClientService, + private readonly jwtService: JwtService, + private readonly checkRegistrationTokenService: CheckRegistrationTokenService, + private readonly userService: LoginUserService, + private readonly backendUserSerivce: BackendUserService, + private readonly userLoginDataService: UserLoginDataService, + ) { + super(); + } + + protected override async useWithState( + req: Request, + res: Response, + state: OAuthAuthorizeServerState & AuthStateServerData & { error?: any }, + next: (error?: Error | any) => void, + ): Promise { + const input = req.body; + SelfRegisterUserInput.check(input); + const { loginData, activeLogin } = await this.checkRegistrationTokenService.getActiveLoginAndLoginDataForToken( + input.register_token, + ); + if (state.authState.activeLogin !== activeLogin.id) { + throw new OAuthHttpException("server_error", "Invalid registration token"); + } + if ((await this.userService.countBy({ username: input.username })) > 0) { + throw new HttpException("Username is not available anymore", HttpStatus.BAD_REQUEST); + } + const newUser = await this.backendUserSerivce.createNewUser(input, false); + await this.userLoginDataService.linkAccountToUser(newUser, loginData, activeLogin); + this.appendState(res, { activeLogin }) + next(); + } +} diff --git a/backend/src/api-internal/auth.module.ts b/backend/src/api-internal/auth.module.ts index 343a55ac..62fba7c2 100644 --- a/backend/src/api-internal/auth.module.ts +++ b/backend/src/api-internal/auth.module.ts @@ -7,15 +7,15 @@ import { StrategiesMiddleware } from "../strategies/strategies.middleware"; import { StrategiesModule } from "../strategies/strategies.module"; import { AuthEndpointsController } from "./auth-endpoints.controller"; import { AuthRedirectMiddleware } from "./auth-redirect.middleware"; -import { PostCredentialsMiddleware } from "./post-credentials.middleware"; import { ApiOauthModule } from "src/api-oauth/api-oauth.module"; import { OAuthErrorRedirectMiddleware } from "src/api-oauth/oauth-error-redirect.middleware"; import { AuthAutorizeExtractMiddleware } from "./auth-autorize-extract.middleware"; import { OAuthAuthorizeValidateMiddleware } from "src/api-oauth/oauth-authorize-validate.middleware"; +import { AuthRegisterMiddleware } from "./auth-register.middleware"; @Module({ imports: [ModelModule, BackendServicesModule, StrategiesModule, ApiOauthModule], - providers: [AuthAutorizeExtractMiddleware, AuthRedirectMiddleware, PostCredentialsMiddleware], + providers: [AuthAutorizeExtractMiddleware, AuthRedirectMiddleware], controllers: [AuthEndpointsController], }) export class ApiInternalModule { @@ -29,6 +29,7 @@ export class ApiInternalModule { private readonly errorHandler: ErrorHandlerMiddleware, private readonly oauthErrorRedirect: OAuthErrorRedirectMiddleware, private readonly oauthAuthorizeValidate: OAuthAuthorizeValidateMiddleware, + private readonly authRegister: AuthRegisterMiddleware, ) { this.middlewares.push({ middlewares: [ @@ -69,6 +70,7 @@ export class ApiInternalModule { middlewares: [ this.authAutorizeExtract, this.oauthAuthorizeValidate, + this.authRegister, this.authRedirect, this.oauthErrorRedirect, this.errorHandler, diff --git a/backend/src/api-internal/post-credentials.middleware.ts b/backend/src/api-internal/post-credentials.middleware.ts deleted file mode 100644 index 8a6e3ae9..00000000 --- a/backend/src/api-internal/post-credentials.middleware.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Injectable, NestMiddleware } from "@nestjs/common"; -import { Request, Response } from "express"; -import { AuthStateServerData } from "src/strategies/AuthResult"; -import { StrategiesMiddleware } from "src/strategies/strategies.middleware"; -import { ensureState } from "src/strategies/utils"; -import { OAuthHttpException } from "../api-oauth/OAuthHttpException"; - -@Injectable() -export class PostCredentialsMiddleware implements NestMiddleware { - constructor(private readonly strategyMiddleware: StrategiesMiddleware) {} - - async use(req: Request, res: Response, next: (error?: any) => void) { - ensureState(res); - let state: AuthStateServerData = res.locals.state; - const mockRes = {}; - for (const key in res) { - if (Object.prototype.hasOwnProperty.call(res, key)) { - const value = res[key]; - if (typeof value == "function") { - mockRes[key] = () => mockRes; - } else { - mockRes[key] = mockRes; - } - } - } - (mockRes as Response).locals.state = { ...state }; - await new Promise(async (resolve, reject) => { - try { - await this.strategyMiddleware.use(req, mockRes as Response, () => resolve(undefined)); - } catch (err) { - reject(err); - } - }); - - state = { ...state, ...(mockRes as Response).locals.state }; - res.locals.state = state; - - if (!state.activeLogin) { - if (state.authErrorMessage) { - throw new OAuthHttpException(state.authErrorType || "invalid_request", state.authErrorMessage); - } - throw new OAuthHttpException("invalid_request", "Unauthorized"); - } - - next(); - } -} diff --git a/backend/src/api-login/register.controller.ts b/backend/src/api-login/register.controller.ts index 11ef0176..0cfb3098 100644 --- a/backend/src/api-login/register.controller.ts +++ b/backend/src/api-login/register.controller.ts @@ -1,6 +1,5 @@ import { Body, Controller, HttpException, HttpStatus, Post, Res, UseGuards } from "@nestjs/common"; import { - ApiBadRequestResponse, ApiBearerAuth, ApiNotFoundResponse, ApiOkResponse, @@ -9,21 +8,14 @@ import { ApiUnauthorizedResponse, } from "@nestjs/swagger"; import { Response } from "express"; -import { BackendUserService } from "src/backend-services/backend-user.service"; import { DefaultReturn } from "src/default-return.dto"; -import { ActiveLogin } from "src/model/postgres/ActiveLogin.entity"; -import { LoginUser } from "src/model/postgres/LoginUser.entity"; -import { LoginState, UserLoginData } from "src/model/postgres/UserLoginData.entity"; -import { ActiveLoginService } from "src/model/services/active-login.service"; import { LoginUserService } from "src/model/services/login-user.service"; -import { StrategiesService } from "src/model/services/strategies.service"; import { UserLoginDataService } from "src/model/services/user-login-data.service"; import { OpenApiTag } from "src/openapi-tag"; import { ApiStateData } from "./ApiStateData"; import { CheckAccessTokenGuard, NeedsAdmin } from "./check-access-token.guard"; import { CheckRegistrationTokenService } from "./check-registration-token.service"; import { AdminLinkUserInput, RegistrationTokenInput } from "./dto/link-user.dto"; -import { SelfRegisterUserInput, } from "./dto/user-inputs.dto"; /** * Controller for handling self registration of new users as well as linking of existing users to new loginData @@ -35,45 +27,8 @@ export class RegisterController { private readonly checkRegistrationTokenService: CheckRegistrationTokenService, private readonly loginDataService: UserLoginDataService, private readonly userService: LoginUserService, - private readonly activeLoginService: ActiveLoginService, - private readonly backendUserSerivce: BackendUserService, ) {} - /** - * Given user data and a registration token, this will create a new user for the registration. - * The user will also be created in the backend. - * - * For the creation to succeed, the registration token and the registration may not be expired yet. - * - * @param input The input data for creating a new user - * @returns The default return with operation "self-register" - */ - @Post("self-register") - @ApiOperation({ summary: "Self register (create user) using registration token" }) - @ApiOkResponse({ - type: DefaultReturn, - description: "If successful, the Default Return with operation 'self-register'", - }) - @ApiUnauthorizedResponse({ - description: - "If the given registration token is not/no longer valid or the registration time frame has expired", - }) - @ApiBadRequestResponse({ - description: "If any of the input data for the user creation are invalid or the username is already taken", - }) - async register(@Body() input: SelfRegisterUserInput): Promise { - SelfRegisterUserInput.check(input); - const { loginData, activeLogin } = await this.checkRegistrationTokenService.getActiveLoginAndLoginDataForToken( - input.register_token, - ); - if ((await this.userService.countBy({ username: input.username })) > 0) { - throw new HttpException("Username is not available anymore", HttpStatus.BAD_REQUEST); - } - const newUser = await this.backendUserSerivce.createNewUser(input, false); - const { loggedInUser } = await this.linkAccountToUser(newUser, loginData, activeLogin); - return new DefaultReturn("self-register"); - } - /** * Links a new authentication with a strategy instance with the currently logged in user. * For future logins using that authentication the user will be directly found @@ -110,7 +65,7 @@ export class RegisterController { input.register_token, (res.locals.state as ApiStateData).loggedInUser, ); - const { loggedInUser } = await this.linkAccountToUser( + const { loggedInUser } = await this.loginDataService.linkAccountToUser( (res.locals.state as ApiStateData).loggedInUser, loginData, activeLogin, @@ -162,62 +117,7 @@ export class RegisterController { input.register_token, linkToUser, ); - await this.linkAccountToUser(linkToUser, loginData, activeLogin); + await this.loginDataService.linkAccountToUser(linkToUser, loginData, activeLogin); return new DefaultReturn("admin-link"); } - - /** - * Helper function performing tha actual linking of login data with user. - * - * If the given login data already has a user set, the user must match the given one, - * else a INTERNAL_SERVER_ERROR is raised. - * - * The expiration of the loginData will be removed. - * The expiration of the activeLogin will be set to the default login expiration time, - * except if the strategy supports sync, then the active login will never expire. - * The state of the loginData will be updated to VALID if it was WAITING_FOR_REGISER before - * - * @param userToLinkTo The user account to link the new authentication to - * @param loginData The new authentication to link to the user - * @param activeLogin The active login that was created during the authentication flow - * @returns The saved and updated user and login data after linking - */ - private async linkAccountToUser( - userToLinkTo: LoginUser, - loginData: UserLoginData, - activeLogin: ActiveLogin, - ): Promise<{ loggedInUser: LoginUser; loginData: UserLoginData }> { - if (!userToLinkTo) { - throw new HttpException( - "Not logged in to any account. Linking not possible. Try logging in or registering", - HttpStatus.BAD_REQUEST, - ); - } - if (loginData.state == LoginState.WAITING_FOR_REGISTER) { - loginData.state = LoginState.VALID; - } - const currentLoginDataUser = await loginData.user; - if (currentLoginDataUser == null) { - loginData.user = Promise.resolve(userToLinkTo); - } else { - if (currentLoginDataUser.id !== userToLinkTo.id) { - // Shoud not be reachable as this is already checked in token check - throw new HttpException( - "Login data user did not match logged in user", - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - loginData.expires = null; - loginData = await this.loginDataService.save(loginData); - - await this.activeLoginService.setActiveLoginExpiration(activeLogin); - - userToLinkTo = await this.userService.findOneBy({ - id: userToLinkTo.id, - }); - - await this.backendUserSerivce.linkAllImsUsersToGropiusUser(userToLinkTo, loginData); - return { loggedInUser: userToLinkTo, loginData }; - } } diff --git a/backend/src/api-oauth/OAuthAuthorizeServerState.ts b/backend/src/api-oauth/OAuthAuthorizeServerState.ts index 80cbc727..5f9c357e 100644 --- a/backend/src/api-oauth/OAuthAuthorizeServerState.ts +++ b/backend/src/api-oauth/OAuthAuthorizeServerState.ts @@ -1,10 +1,11 @@ +import { TokenScope } from "src/backend-services/token.service"; import { AuthClient } from "src/model/postgres/AuthClient.entity"; export interface OAuthAuthorizeRequest { state?: string; redirect: string; clientId: string; - scope: string[]; + scope: TokenScope[]; codeChallenge?: string; codeChallengeMethod?: string; responseType: "code"; diff --git a/backend/src/api-oauth/oauth-token-authorization-code.middleware.ts b/backend/src/api-oauth/oauth-token-authorization-code.middleware.ts index a3f455a8..f05fa8ce 100644 --- a/backend/src/api-oauth/oauth-token-authorization-code.middleware.ts +++ b/backend/src/api-oauth/oauth-token-authorization-code.middleware.ts @@ -1,6 +1,6 @@ import { Injectable, Logger } from "@nestjs/common"; import { Request, Response } from "express"; -import { ActiveLoginTokenResult, TokenService } from "src/backend-services/token.service"; +import { ActiveLoginTokenResult, TokenScope, TokenService } from "src/backend-services/token.service"; import { AuthClient } from "src/model/postgres/AuthClient.entity"; import { ActiveLoginService } from "src/model/services/active-login.service"; import { AuthStateServerData } from "src/strategies/AuthResult"; @@ -8,7 +8,10 @@ import { OAuthHttpException } from "./OAuthHttpException"; import { StateMiddleware } from "./StateMiddleware"; @Injectable() -export class OAuthTokenAuthorizationCodeMiddleware extends StateMiddleware<{ client: AuthClient }, AuthStateServerData> { +export class OAuthTokenAuthorizationCodeMiddleware extends StateMiddleware< + { client: AuthClient }, + AuthStateServerData & { scope: TokenScope[] } +> { private readonly logger = new Logger(OAuthTokenAuthorizationCodeMiddleware.name); constructor( private readonly activeLoginService: ActiveLoginService, @@ -71,7 +74,7 @@ export class OAuthTokenAuthorizationCodeMiddleware extends StateMiddleware<{ cli ); throw new OAuthHttpException("invalid_grant", "Given code was liekely reused. Login and codes invalidated"); } - this.appendState(res, { activeLogin }); + this.appendState(res, { activeLogin, scope: tokenData.scope }); next(); } } diff --git a/backend/src/api-oauth/oauth-token.controller.ts b/backend/src/api-oauth/oauth-token.controller.ts index b3aed57b..46f110e0 100644 --- a/backend/src/api-oauth/oauth-token.controller.ts +++ b/backend/src/api-oauth/oauth-token.controller.ts @@ -28,7 +28,7 @@ export class OAuthTokenController { private readonly authClientService: AuthClientService, private readonly activeLoginService: ActiveLoginService, private readonly tokenService: TokenService, - ) { } + ) {} private async checkLoginDataIsVaild(loginData?: UserLoginData, activeLogin?: ActiveLogin) { if (!loginData) { @@ -98,17 +98,15 @@ export class OAuthTokenController { loginData: UserLoginData, activeLogin: ActiveLogin, currentClient: AuthClient, + scope: TokenScope[], ): Promise { const tokenExpiresInMs: number = parseInt(process.env.GROPIUS_ACCESS_TOKEN_EXPIRATION_TIME_MS, 10); let accessToken: string; - let tokenScope: TokenScope[]; if (loginData.state == LoginState.WAITING_FOR_REGISTER) { - tokenScope = [TokenScope.LOGIN_SERVICE_REGISTER]; accessToken = await this.tokenService.signRegistrationToken(activeLogin.id, tokenExpiresInMs); } else { - tokenScope = [TokenScope.BACKEND, TokenScope.LOGIN_SERVICE]; - accessToken = await this.tokenService.signBackendAccessToken(await loginData.user, tokenExpiresInMs); + accessToken = await this.tokenService.signAccessToken(await loginData.user, scope, tokenExpiresInMs); } activeLogin = await this.updateRefreshTokenIdAndExpirationDate( @@ -117,18 +115,22 @@ export class OAuthTokenController { currentClient, ); - const refreshToken = await this.tokenService.signActiveLoginCode( - activeLogin.id, - currentClient.id, - activeLogin.nextExpectedRefreshTokenNumber, - activeLogin.expires ?? undefined - ); + const refreshToken = + loginData.state != LoginState.WAITING_FOR_REGISTER + ? await this.tokenService.signActiveLoginCode( + activeLogin.id, + currentClient.id, + activeLogin.nextExpectedRefreshTokenNumber, + scope, + activeLogin.expires ?? undefined, + ) + : undefined; return { access_token: accessToken, token_type: "bearer", expires_in: Math.floor(tokenExpiresInMs / 1000), refresh_token: refreshToken, - scope: tokenScope.join(" "), + scope: scope.join(" "), }; } @@ -136,12 +138,14 @@ export class OAuthTokenController { async token(@Res({ passthrough: true }) res: Response): Promise { ensureState(res); const currentClient = res.locals.state.client as AuthClient; + const scope = res.locals.state.scope as TokenScope[]; if (!currentClient) { throw new OAuthHttpException( "invalid_client", "No client id/authentication given or authentication invalid", ); } + this.tokenService.verifyScope(scope); let activeLogin = (res.locals.state as AuthStateServerData)?.activeLogin; if (typeof activeLogin == "string") { activeLogin = await this.activeLoginService.findOneByOrFail({ @@ -150,6 +154,6 @@ export class OAuthTokenController { } const loginData = await activeLogin.loginInstanceFor; await this.checkLoginDataIsVaild(loginData, activeLogin); - return await this.createAccessToken(loginData, activeLogin, currentClient); + return await this.createAccessToken(loginData, activeLogin, currentClient, scope); } } diff --git a/backend/src/backend-services/token.service.ts b/backend/src/backend-services/token.service.ts index afecb83d..a4b33310 100644 --- a/backend/src/backend-services/token.service.ts +++ b/backend/src/backend-services/token.service.ts @@ -10,6 +10,7 @@ export interface ActiveLoginTokenResult { activeLoginId: string; clientId: string; tokenUniqueId: string; + scope: TokenScope[]; } export enum TokenScope { @@ -31,8 +32,12 @@ export class TokenService { private readonly loginUserService: LoginUserService, ) { } - async signBackendAccessToken(user: LoginUser, expiresIn?: number): Promise { + async signAccessToken(user: LoginUser, scope: string[], expiresIn?: number): Promise { const expiryObject = !!expiresIn ? { expiresIn: expiresIn / 1000 } : {}; + this.verifyScope(scope); + if (scope.includes(TokenScope.LOGIN_SERVICE_REGISTER)) { + throw new Error("Cannot sign access token with register scope"); + } if (!user.neo4jId) { throw new Error("Login user without neo4jId: " + user.id); } @@ -41,19 +46,44 @@ export class TokenService { { subject: user.neo4jId, ...expiryObject, - audience: [TokenScope.LOGIN_SERVICE, TokenScope.BACKEND], + audience: scope, }, ); } - async signLoginOnlyAccessToken(user: LoginUser, expiresIn?: number): Promise { + async signRegistrationToken(activeLoginId: string, expiresIn?: number): Promise { const expiryObject = !!expiresIn ? { expiresIn: expiresIn / 1000 } : {}; - return this.backendJwtService.sign( + return this.backendJwtService.signAsync( {}, { - subject: user.id, + subject: activeLoginId, ...expiryObject, - audience: [TokenScope.LOGIN_SERVICE], + audience: [TokenScope.LOGIN_SERVICE_REGISTER], + }, + ); + } + + async signActiveLoginCode( + activeLoginId: string, + clientId: string, + uniqueId: string | number, + scope: TokenScope[], + expiresInAt?: number | Date, + ): Promise { + this.verifyScope(scope); + const expiresInObject = (typeof expiresInAt == "number") ? { expiresIn: expiresInAt / 1000 } : {}; + const expiresAtObject = (typeof expiresInAt == "object" && expiresInAt instanceof Date) ? { exp: Math.floor(expiresInAt.getTime() / 1000) } : {}; + return await this.backendJwtService.signAsync( + { + ...expiresAtObject, + client_id: clientId, + scope, + }, + { + subject: activeLoginId, + ...expiresInObject, + jwtid: uniqueId.toString(), + audience: [RefreshTokenScope.REFRESH_TOKEN], }, ); } @@ -81,53 +111,15 @@ export class TokenService { return { user }; } - async signRegistrationToken(activeLoginId: string, expiresIn?: number): Promise { - const expiryObject = !!expiresIn ? { expiresIn: expiresIn / 1000 } : {}; - return this.backendJwtService.signAsync( - {}, - { - subject: activeLoginId, - ...expiryObject, - audience: [TokenScope.LOGIN_SERVICE_REGISTER], - secret: process.env.GROPIUS_LOGIN_SPECIFIC_JWT_SECRET, - }, - ); - } - async verifyRegistrationToken(token: string): Promise { const payload = await this.backendJwtService.verifyAsync(token, { audience: [TokenScope.LOGIN_SERVICE_REGISTER], - secret: process.env.GROPIUS_LOGIN_SPECIFIC_JWT_SECRET, }); return payload.sub; } - async signActiveLoginCode( - activeLoginId: string, - clientId: string, - uniqueId: string | number, - expiresInAt?: number | Date, - ): Promise { - const expiresInObject = (typeof expiresInAt == "number") ? { expiresIn: expiresInAt / 1000 } : {}; - const expiresAtObject = (typeof expiresInAt == "object" && expiresInAt instanceof Date) ? { exp: Math.floor(expiresInAt.getTime() / 1000) } : {}; - return await this.backendJwtService.signAsync( - { - ...expiresAtObject, - client_id: clientId, - }, - { - subject: activeLoginId, - ...expiresInObject, - jwtid: uniqueId.toString(), - secret: process.env.GROPIUS_LOGIN_SPECIFIC_JWT_SECRET, - audience: [RefreshTokenScope.REFRESH_TOKEN], - }, - ); - } - async verifyActiveLoginToken(token: string, requiredClientId: string): Promise { const payload = await this.backendJwtService.verifyAsync(token, { - secret: process.env.GROPIUS_LOGIN_SPECIFIC_JWT_SECRET, audience: [RefreshTokenScope.REFRESH_TOKEN], }); if (payload.client_id !== requiredClientId) { @@ -140,6 +132,7 @@ export class TokenService { activeLoginId: payload.sub, clientId: payload.client_id, tokenUniqueId: payload.jti, + scope: payload.scope, }; } diff --git a/backend/src/model/services/user-login-data.service.ts b/backend/src/model/services/user-login-data.service.ts index 0fdc2fc8..bf34033a 100644 --- a/backend/src/model/services/user-login-data.service.ts +++ b/backend/src/model/services/user-login-data.service.ts @@ -1,11 +1,21 @@ -import { Injectable } from "@nestjs/common"; +import { HttpException, HttpStatus, Injectable } from "@nestjs/common"; import { DataSource, Repository } from "typeorm"; import { StrategyInstance } from "../postgres/StrategyInstance.entity"; import { LoginState, UserLoginData } from "../postgres/UserLoginData.entity"; +import { LoginUserService } from "./login-user.service"; +import { ActiveLoginService } from "./active-login.service"; +import { BackendUserService } from "src/backend-services/backend-user.service"; +import { LoginUser } from "../postgres/LoginUser.entity"; +import { ActiveLogin } from "../postgres/ActiveLogin.entity"; @Injectable() export class UserLoginDataService extends Repository { - constructor(private dataSource: DataSource) { + constructor( + private dataSource: DataSource, + private readonly userService: LoginUserService, + private readonly activeLoginService: ActiveLoginService, + private readonly backendUserSerivce: BackendUserService, + ) { super(UserLoginData, dataSource.createEntityManager()); } @@ -39,4 +49,59 @@ export class UserLoginDataService extends Repository { .andWhereInIds(loginDataIds) .getMany(); } + + /** + * Helper function performing tha actual linking of login data with user. + * + * If the given login data already has a user set, the user must match the given one, + * else a INTERNAL_SERVER_ERROR is raised. + * + * The expiration of the loginData will be removed. + * The expiration of the activeLogin will be set to the default login expiration time, + * except if the strategy supports sync, then the active login will never expire. + * The state of the loginData will be updated to VALID if it was WAITING_FOR_REGISER before + * + * @param userToLinkTo The user account to link the new authentication to + * @param loginData The new authentication to link to the user + * @param activeLogin The active login that was created during the authentication flow + * @returns The saved and updated user and login data after linking + */ + public async linkAccountToUser( + userToLinkTo: LoginUser, + loginData: UserLoginData, + activeLogin: ActiveLogin, + ): Promise<{ loggedInUser: LoginUser; loginData: UserLoginData }> { + if (!userToLinkTo) { + throw new HttpException( + "Not logged in to any account. Linking not possible. Try logging in or registering", + HttpStatus.BAD_REQUEST, + ); + } + if (loginData.state == LoginState.WAITING_FOR_REGISTER) { + loginData.state = LoginState.VALID; + } + const currentLoginDataUser = await loginData.user; + if (currentLoginDataUser == null) { + loginData.user = Promise.resolve(userToLinkTo); + } else { + if (currentLoginDataUser.id !== userToLinkTo.id) { + // Shoud not be reachable as this is already checked in token check + throw new HttpException( + "Login data user did not match logged in user", + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + loginData.expires = null; + loginData = await this.save(loginData); + + await this.activeLoginService.setActiveLoginExpiration(activeLogin); + + userToLinkTo = await this.userService.findOneBy({ + id: userToLinkTo.id, + }); + + await this.backendUserSerivce.linkAllImsUsersToGropiusUser(userToLinkTo, loginData); + return { loggedInUser: userToLinkTo, loginData }; + } } diff --git a/backend/src/strategies/strategies.module.ts b/backend/src/strategies/strategies.module.ts index 9e58500f..4d32513a 100644 --- a/backend/src/strategies/strategies.module.ts +++ b/backend/src/strategies/strategies.module.ts @@ -14,7 +14,7 @@ import { JiraStrategyService } from "./jira/jira.service"; JwtModule.registerAsync({ useFactory(...args) { return { - secret: process.env.GROPIUS_LOGIN_SPECIFIC_JWT_SECRET || "blabla", + secret: process.env.GROPIUS_LOGIN_SPECIFIC_JWT_SECRET || "keyboard cat", signOptions: { issuer: process.env.GROPIUS_PASSPORT_STATE_JWT_ISSUER, }, From 2a21d4efaefff4159bfb4948eb25de9cae827e38 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Fri, 12 Jul 2024 18:39:19 +0200 Subject: [PATCH 07/31] progress --- .dockerignore | 4 ++ Dockerfile | 2 +- backend/.env.dev | 8 +-- ...{auth.module.ts => api-internal.module.ts} | 5 +- .../auth-autorize-extract.middleware.ts | 7 +- .../api-internal/auth-redirect.middleware.ts | 9 ++- .../api-internal/auth-register.middleware.ts | 8 +-- backend/src/api-login/api-login.module.ts | 1 + backend/src/api-login/register.controller.ts | 8 +-- backend/src/api-oauth/api-oauth.module.ts | 4 +- .../oauth-authorize-extract.middleware.ts | 3 +- .../oauth-authorize-redirect.middleware.ts | 10 +-- backend/src/app.module.ts | 2 +- .../backend-services.module.ts | 2 +- .../backend-services/backend-user.service.ts | 70 ++++++++++++++++++- backend/src/configuration-validator.ts | 5 +- .../create-default-auth-client.service.ts | 2 +- backend/src/main.ts | 3 +- backend/src/migrationDataSource.config.ts | 2 +- .../model/services/user-login-data.service.ts | 66 +---------------- .../src/strategies/StrategyUsingPassport.ts | 1 + .../src/strategies/github/github.service.ts | 6 +- backend/src/strategies/jira/jira.service.ts | 6 +- backend/src/strategies/strategies.module.ts | 4 +- .../strategies/userpass/userpass.service.ts | 6 +- 25 files changed, 123 insertions(+), 121 deletions(-) create mode 100644 .dockerignore rename backend/src/api-internal/{auth.module.ts => api-internal.module.ts} (95%) diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..0316732f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +backend/node_modules/ +backend/dist/ +frontend/node_modules/ +frontend/dist/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 6f969653..f6cfaf3b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM node:18 ENV NODE_ENV build USER node WORKDIR /home/node -ADD . . +ADD backend . RUN npm ci RUN npm run build diff --git a/backend/.env.dev b/backend/.env.dev index ff11e895..2de0a0ba 100644 --- a/backend/.env.dev +++ b/backend/.env.dev @@ -8,7 +8,7 @@ # ----------General settings------------------ ### JWT secret used to sign tokens that are not access tokens to the backend. -### It is recommended to choos a different secret from GROPIUS_INTERNAL_BACKEND_JWT_SECRET to avoid token misuse +### It is recommended to choos a different secret from GROPIUS_OAUTH_JWT_SECRET to avoid token misuse ### Make sure this is a LONG and RANDOM value and NOBONDY knows it. Else they can create valid tokens #GROPIUS_LOGIN_SPECIFIC_JWT_SECRET=login_secret # required, no default @@ -62,7 +62,7 @@ GROPIUS_INTERNAL_BACKEND_TOKEN=super_secret # default: no token ### It is recommended to choos a different secret from GROPIUS_LOGIN_SPECIFIC_JWT_SECRET to avoid token misuse ### Make sure this is a LONG and RANDOM value and NOBONDY knows it. Else they can create valid tokens ### This secret Text will be interpreted as base64 encoded -#GROPIUS_INTERNAL_BACKEND_JWT_SECRET=backend_secret # required, no default +#GROPIUS_OAUTH_JWT_SECRET=backend_secret # required, no default # --------------Database (postgres) configuration @@ -98,10 +98,6 @@ GROPIUS_PASSPORT_STATE_JWT_ISSUER=gropius-login-state # default: gropius-login- ### Choosing this wisely is importand. Too low makes the hashes unsecure and too high makes hasing take too long GROPIUS_BCRYPT_HASH_ROUNDS=10 # default: 10 -### Set to true to allow login strategies that don't use the full OAuth flow but just post credentials (e.g. userpass) -### to get a token without needing to provide a client id (a client without client secret still needs to exist) -#GROPIUS_ALLOW_PASSWORD_TOKEN_MODE_WITHOUT_OAUTH_CLIENT=false # default: false in production, true in testing and dev - # ------------------Sync-service API settings--------------------------- ### The secret that is expected from a sync service client connecting to the sync API prefixed with "Bearer " diff --git a/backend/src/api-internal/auth.module.ts b/backend/src/api-internal/api-internal.module.ts similarity index 95% rename from backend/src/api-internal/auth.module.ts rename to backend/src/api-internal/api-internal.module.ts index 62fba7c2..c8e8f349 100644 --- a/backend/src/api-internal/auth.module.ts +++ b/backend/src/api-internal/api-internal.module.ts @@ -12,10 +12,11 @@ import { OAuthErrorRedirectMiddleware } from "src/api-oauth/oauth-error-redirect import { AuthAutorizeExtractMiddleware } from "./auth-autorize-extract.middleware"; import { OAuthAuthorizeValidateMiddleware } from "src/api-oauth/oauth-authorize-validate.middleware"; import { AuthRegisterMiddleware } from "./auth-register.middleware"; +import { AuthModule } from "src/api-login/api-login.module"; @Module({ - imports: [ModelModule, BackendServicesModule, StrategiesModule, ApiOauthModule], - providers: [AuthAutorizeExtractMiddleware, AuthRedirectMiddleware], + imports: [ModelModule, BackendServicesModule, StrategiesModule, ApiOauthModule, AuthModule], + providers: [AuthAutorizeExtractMiddleware, AuthRedirectMiddleware, AuthRegisterMiddleware, ModeExtractorMiddleware], controllers: [AuthEndpointsController], }) export class ApiInternalModule { diff --git a/backend/src/api-internal/auth-autorize-extract.middleware.ts b/backend/src/api-internal/auth-autorize-extract.middleware.ts index ccda12d8..5c0b54b1 100644 --- a/backend/src/api-internal/auth-autorize-extract.middleware.ts +++ b/backend/src/api-internal/auth-autorize-extract.middleware.ts @@ -1,4 +1,4 @@ -import { Injectable } from "@nestjs/common"; +import { Inject, Injectable } from "@nestjs/common"; import { Request, Response } from "express"; import { AuthClientService } from "src/model/services/auth-client.service"; import { StateMiddleware } from "src/api-oauth/StateMiddleware"; @@ -9,7 +9,8 @@ import { JwtService } from "@nestjs/jwt"; export class AuthAutorizeExtractMiddleware extends StateMiddleware<{}, OAuthAuthorizeServerState> { constructor( private readonly authClientService: AuthClientService, - private readonly jwtService: JwtService, + @Inject("StateJwtService") + private readonly stateJwtService: JwtService, ) { super(); } @@ -20,7 +21,7 @@ export class AuthAutorizeExtractMiddleware extends StateMiddleware<{}, OAuthAuth state: { error?: any }, next: (error?: Error | any) => void, ): Promise { - const newState = this.jwtService.verify>(req.query.state as string); + const newState = this.stateJwtService.verify>(req.query.state as string); const client = await this.authClientService.findOneBy({ id: newState.request.clientId }); this.appendState(res, { client, ...newState }); next(); diff --git a/backend/src/api-internal/auth-redirect.middleware.ts b/backend/src/api-internal/auth-redirect.middleware.ts index 4fde77a3..ec986e8a 100644 --- a/backend/src/api-internal/auth-redirect.middleware.ts +++ b/backend/src/api-internal/auth-redirect.middleware.ts @@ -1,9 +1,8 @@ -import { Injectable, Logger } from "@nestjs/common"; +import { Inject, Injectable, Logger } from "@nestjs/common"; import { Request, Response } from "express"; import { TokenScope, TokenService } from "src/backend-services/token.service"; import { ActiveLogin } from "src/model/postgres/ActiveLogin.entity"; import { ActiveLoginService } from "src/model/services/active-login.service"; -import { AuthClientService } from "src/model/services/auth-client.service"; import { AuthStateServerData } from "../strategies/AuthResult"; import { OAuthHttpException } from "src/api-oauth/OAuthHttpException"; import { StateMiddleware } from "src/api-oauth/StateMiddleware"; @@ -48,8 +47,8 @@ export class AuthRedirectMiddleware extends StateMiddleware< constructor( private readonly tokenService: TokenService, private readonly activeLoginService: ActiveLoginService, - private readonly authClientService: AuthClientService, - private readonly jwtService: JwtService, + @Inject("StateJwtService") + private readonly stateJwtService: JwtService, private readonly userService: LoginUserService, ) { super(); @@ -145,7 +144,7 @@ export class AuthRedirectMiddleware extends StateMiddleware< } } else { const encodedState = encodeURIComponent( - this.jwtService.sign({ request: state.request, authState: state.authState }), + this.stateJwtService.sign({ request: state.request, authState: state.authState }), ); const token = await this.generateCode(state); const suggestions = await this.getDataSuggestions(userLoginData, state.strategy); diff --git a/backend/src/api-internal/auth-register.middleware.ts b/backend/src/api-internal/auth-register.middleware.ts index 2ec06df5..09bf5380 100644 --- a/backend/src/api-internal/auth-register.middleware.ts +++ b/backend/src/api-internal/auth-register.middleware.ts @@ -1,16 +1,13 @@ import { HttpException, HttpStatus, Injectable } from "@nestjs/common"; import { Request, Response } from "express"; -import { AuthClientService } from "src/model/services/auth-client.service"; import { StateMiddleware } from "src/api-oauth/StateMiddleware"; import { OAuthAuthorizeServerState } from "src/api-oauth/OAuthAuthorizeServerState"; -import { JwtService } from "@nestjs/jwt"; import { AuthStateServerData } from "src/strategies/AuthResult"; import { SelfRegisterUserInput } from "src/api-login/dto/user-inputs.dto"; import { CheckRegistrationTokenService } from "src/api-login/check-registration-token.service"; import { OAuthHttpException } from "src/api-oauth/OAuthHttpException"; import { LoginUserService } from "src/model/services/login-user.service"; import { BackendUserService } from "src/backend-services/backend-user.service"; -import { UserLoginDataService } from "src/model/services/user-login-data.service"; @Injectable() export class AuthRegisterMiddleware extends StateMiddleware< @@ -18,12 +15,9 @@ export class AuthRegisterMiddleware extends StateMiddleware< OAuthAuthorizeServerState & AuthStateServerData > { constructor( - private readonly authClientService: AuthClientService, - private readonly jwtService: JwtService, private readonly checkRegistrationTokenService: CheckRegistrationTokenService, private readonly userService: LoginUserService, private readonly backendUserSerivce: BackendUserService, - private readonly userLoginDataService: UserLoginDataService, ) { super(); } @@ -46,7 +40,7 @@ export class AuthRegisterMiddleware extends StateMiddleware< throw new HttpException("Username is not available anymore", HttpStatus.BAD_REQUEST); } const newUser = await this.backendUserSerivce.createNewUser(input, false); - await this.userLoginDataService.linkAccountToUser(newUser, loginData, activeLogin); + await this.backendUserSerivce.linkAccountToUser(newUser, loginData, activeLogin); this.appendState(res, { activeLogin }) next(); } diff --git a/backend/src/api-login/api-login.module.ts b/backend/src/api-login/api-login.module.ts index 67ef0543..da93e42a 100644 --- a/backend/src/api-login/api-login.module.ts +++ b/backend/src/api-login/api-login.module.ts @@ -24,5 +24,6 @@ import { UsersController } from "./users.controller"; AuthClientController, ], providers: [CheckRegistrationTokenService], + exports: [CheckRegistrationTokenService], }) export class AuthModule {} diff --git a/backend/src/api-login/register.controller.ts b/backend/src/api-login/register.controller.ts index 0cfb3098..0b4484d6 100644 --- a/backend/src/api-login/register.controller.ts +++ b/backend/src/api-login/register.controller.ts @@ -10,12 +10,12 @@ import { import { Response } from "express"; import { DefaultReturn } from "src/default-return.dto"; import { LoginUserService } from "src/model/services/login-user.service"; -import { UserLoginDataService } from "src/model/services/user-login-data.service"; import { OpenApiTag } from "src/openapi-tag"; import { ApiStateData } from "./ApiStateData"; import { CheckAccessTokenGuard, NeedsAdmin } from "./check-access-token.guard"; import { CheckRegistrationTokenService } from "./check-registration-token.service"; import { AdminLinkUserInput, RegistrationTokenInput } from "./dto/link-user.dto"; +import { BackendUserService } from "src/backend-services/backend-user.service"; /** * Controller for handling self registration of new users as well as linking of existing users to new loginData @@ -25,8 +25,8 @@ import { AdminLinkUserInput, RegistrationTokenInput } from "./dto/link-user.dto" export class RegisterController { constructor( private readonly checkRegistrationTokenService: CheckRegistrationTokenService, - private readonly loginDataService: UserLoginDataService, private readonly userService: LoginUserService, + private readonly backendUserSerivce: BackendUserService, ) {} /** @@ -65,7 +65,7 @@ export class RegisterController { input.register_token, (res.locals.state as ApiStateData).loggedInUser, ); - const { loggedInUser } = await this.loginDataService.linkAccountToUser( + const { loggedInUser } = await this.backendUserSerivce.linkAccountToUser( (res.locals.state as ApiStateData).loggedInUser, loginData, activeLogin, @@ -117,7 +117,7 @@ export class RegisterController { input.register_token, linkToUser, ); - await this.loginDataService.linkAccountToUser(linkToUser, loginData, activeLogin); + await this.backendUserSerivce.linkAccountToUser(linkToUser, loginData, activeLogin); return new DefaultReturn("admin-link"); } } diff --git a/backend/src/api-oauth/api-oauth.module.ts b/backend/src/api-oauth/api-oauth.module.ts index 71a02e31..e5fb08e4 100644 --- a/backend/src/api-oauth/api-oauth.module.ts +++ b/backend/src/api-oauth/api-oauth.module.ts @@ -9,9 +9,11 @@ import { OAuthAuthorizeValidateMiddleware } from "./oauth-authorize-validate.mid import { OAuthAuthorizeRedirectMiddleware } from "./oauth-authorize-redirect.middleware"; import { ErrorHandlerMiddleware } from "./error-handler.middleware"; import { OAuthErrorRedirectMiddleware } from "./oauth-error-redirect.middleware"; +import { BackendServicesModule } from "src/backend-services/backend-services.module"; +import { StrategiesModule } from "src/strategies/strategies.module"; @Module({ - imports: [ModelModule], + imports: [ModelModule, BackendServicesModule, StrategiesModule], providers: [ OAuthAuthorizeExtractMiddleware, OAuthAuthorizeValidateMiddleware, diff --git a/backend/src/api-oauth/oauth-authorize-extract.middleware.ts b/backend/src/api-oauth/oauth-authorize-extract.middleware.ts index a80edcce..a8cae1e4 100644 --- a/backend/src/api-oauth/oauth-authorize-extract.middleware.ts +++ b/backend/src/api-oauth/oauth-authorize-extract.middleware.ts @@ -3,6 +3,7 @@ import { Request, Response } from "express"; import { StateMiddleware } from "./StateMiddleware"; import { OAuthAuthorizeRequest, OAuthAuthorizeServerState } from "./OAuthAuthorizeServerState"; import { AuthClientService } from "src/model/services/auth-client.service"; +import { TokenScope } from "src/backend-services/token.service"; @Injectable() export class OAuthAuthorizeExtractMiddleware extends StateMiddleware<{}, OAuthAuthorizeServerState> { @@ -20,7 +21,7 @@ export class OAuthAuthorizeExtractMiddleware extends StateMiddleware<{}, OAuthAu state: req.query.state as string, redirect: req.query.redirect as string, clientId: req.query.client_id as string, - scope: (req.query.scope as string).split(" ").filter((s) => s.length > 0), + scope: (req.query.scope as string).split(" ").filter((s) => s.length > 0) as TokenScope[], codeChallenge: req.query.code_challenge as string, codeChallengeMethod: req.query.code_challenge_method as string, responseType: req.query.response_type as "code", diff --git a/backend/src/api-oauth/oauth-authorize-redirect.middleware.ts b/backend/src/api-oauth/oauth-authorize-redirect.middleware.ts index 92ca7e6a..7986f26e 100644 --- a/backend/src/api-oauth/oauth-authorize-redirect.middleware.ts +++ b/backend/src/api-oauth/oauth-authorize-redirect.middleware.ts @@ -1,4 +1,4 @@ -import { Injectable } from "@nestjs/common"; +import { Inject, Injectable } from "@nestjs/common"; import { Request, Response } from "express"; import { StateMiddleware } from "./StateMiddleware"; import { OAuthAuthorizeServerState } from "./OAuthAuthorizeServerState"; @@ -10,8 +10,10 @@ export class OAuthAuthorizeRedirectMiddleware extends StateMiddleware< OAuthAuthorizeServerState, OAuthAuthorizeServerState > { - - constructor(private readonly jwtService: JwtService) { + constructor( + @Inject("StateJwtService") + private readonly stateJwtService: JwtService, + ) { super(); } @@ -24,7 +26,7 @@ export class OAuthAuthorizeRedirectMiddleware extends StateMiddleware< const target = state.request.scope.includes(TokenScope.LOGIN_SERVICE_REGISTER) ? "register-additional" : "login"; - const encodedState = encodeURIComponent(this.jwtService.sign({ request: state.request })); + const encodedState = encodeURIComponent(this.stateJwtService.sign({ request: state.request })); res.redirect(`/auth/flow/${target}?state=${encodedState}`); } } diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 07bfaef9..078495f3 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -8,7 +8,7 @@ import { ModelModule } from "./model/model.module"; import { StrategiesModule } from "./strategies/strategies.module"; import { BackendServicesModule } from "./backend-services/backend-services.module"; import { validationSchema } from "./configuration-validator"; -import { ApiInternalModule } from "./api-internal/auth.module"; +import { ApiInternalModule } from "./api-internal/api-internal.module"; import { DefaultReturn } from "./default-return.dto"; import { InitializationModule } from "./initialization/initialization.module"; import * as path from "path"; diff --git a/backend/src/backend-services/backend-services.module.ts b/backend/src/backend-services/backend-services.module.ts index 81557984..74d5636d 100644 --- a/backend/src/backend-services/backend-services.module.ts +++ b/backend/src/backend-services/backend-services.module.ts @@ -11,7 +11,7 @@ import { StrategiesModule } from "src/strategies/strategies.module"; JwtModule.registerAsync({ useFactory(...args) { return { - secret: Buffer.from(process.env.GROPIUS_INTERNAL_BACKEND_JWT_SECRET, "base64"), + secret: Buffer.from(process.env.GROPIUS_OAUTH_JWT_SECRET, "base64"), signOptions: { issuer: process.env.GROPIUS_JWT_ISSUER, audience: ["backend", "login"], diff --git a/backend/src/backend-services/backend-user.service.ts b/backend/src/backend-services/backend-user.service.ts index a4b26e14..8aa92f8f 100644 --- a/backend/src/backend-services/backend-user.service.ts +++ b/backend/src/backend-services/backend-user.service.ts @@ -1,9 +1,12 @@ -import { Injectable, Logger } from "@nestjs/common"; +import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common"; import { GraphqlService } from "src/model/graphql/graphql.service"; +import { ActiveLogin } from "src/model/postgres/ActiveLogin.entity"; import { LoginUser } from "src/model/postgres/LoginUser.entity"; -import { UserLoginData } from "src/model/postgres/UserLoginData.entity"; +import { LoginState, UserLoginData } from "src/model/postgres/UserLoginData.entity"; import { UserLoginDataImsUser } from "src/model/postgres/UserLoginDataImsUser.entity"; +import { ActiveLoginService } from "src/model/services/active-login.service"; import { LoginUserService } from "src/model/services/login-user.service"; +import { UserLoginDataService } from "src/model/services/user-login-data.service"; export interface CreateUserInput { username: string; @@ -14,7 +17,13 @@ export interface CreateUserInput { @Injectable() export class BackendUserService { private readonly logger = new Logger(BackendUserService.name); - constructor(private readonly graphqlService: GraphqlService, private readonly loginUserService: LoginUserService) {} + constructor( + private readonly graphqlService: GraphqlService, + private readonly loginUserService: LoginUserService, + private readonly userService: LoginUserService, + private readonly activeLoginService: ActiveLoginService, + private readonly userLoginDataService: UserLoginDataService, + ) {} /** * Checks if the user may access admin actions. @@ -163,4 +172,59 @@ export class BackendUserService { ); //.map((result) => (result.status == "fulfilled" ? result.value : result.reason)); this.logger.warn("Failures during linking ims user and Gropius user:", failedLinks); } + + /** + * Helper function performing tha actual linking of login data with user. + * + * If the given login data already has a user set, the user must match the given one, + * else a INTERNAL_SERVER_ERROR is raised. + * + * The expiration of the loginData will be removed. + * The expiration of the activeLogin will be set to the default login expiration time, + * except if the strategy supports sync, then the active login will never expire. + * The state of the loginData will be updated to VALID if it was WAITING_FOR_REGISER before + * + * @param userToLinkTo The user account to link the new authentication to + * @param loginData The new authentication to link to the user + * @param activeLogin The active login that was created during the authentication flow + * @returns The saved and updated user and login data after linking + */ + public async linkAccountToUser( + userToLinkTo: LoginUser, + loginData: UserLoginData, + activeLogin: ActiveLogin, + ): Promise<{ loggedInUser: LoginUser; loginData: UserLoginData }> { + if (!userToLinkTo) { + throw new HttpException( + "Not logged in to any account. Linking not possible. Try logging in or registering", + HttpStatus.BAD_REQUEST, + ); + } + if (loginData.state == LoginState.WAITING_FOR_REGISTER) { + loginData.state = LoginState.VALID; + } + const currentLoginDataUser = await loginData.user; + if (currentLoginDataUser == null) { + loginData.user = Promise.resolve(userToLinkTo); + } else { + if (currentLoginDataUser.id !== userToLinkTo.id) { + // Shoud not be reachable as this is already checked in token check + throw new HttpException( + "Login data user did not match logged in user", + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + loginData.expires = null; + loginData = await this.userLoginDataService.save(loginData); + + await this.activeLoginService.setActiveLoginExpiration(activeLogin); + + userToLinkTo = await this.userService.findOneBy({ + id: userToLinkTo.id, + }); + + await this.linkAllImsUsersToGropiusUser(userToLinkTo, loginData); + return { loggedInUser: userToLinkTo, loginData }; + } } diff --git a/backend/src/configuration-validator.ts b/backend/src/configuration-validator.ts index d0122956..c5489b30 100644 --- a/backend/src/configuration-validator.ts +++ b/backend/src/configuration-validator.ts @@ -3,7 +3,7 @@ import * as Joi from "joi"; export const validationSchema = Joi.object({ GROPIUS_INTERNAL_BACKEND_ENDPOINT: Joi.string().uri().default("http://localhost:8081/graphql"), GROPIUS_INTERNAL_BACKEND_TOKEN: Joi.string(), - GROPIUS_INTERNAL_BACKEND_JWT_SECRET: Joi.string().min(1).required(), + GROPIUS_OAUTH_JWT_SECRET: Joi.string().min(1).required(), GROPIUS_LOGIN_DATABASE_DRIVER: Joi.string().default("postgres"), GROPIUS_LOGIN_DATABASE_HOST: Joi.string().default("localhost"), @@ -30,9 +30,6 @@ export const validationSchema = Joi.object({ GROPIUS_PASSPORT_STATE_JWT_ISSUER: Joi.string().default("gropius-login-state"), GROPIUS_BCRYPT_HASH_ROUNDS: Joi.number().min(8).default(10), GROPIUS_OAUTH_CODE_EXPIRATION_TIME_MS: Joi.number().min(0).default(600000), - GROPIUS_ALLOW_PASSWORD_TOKEN_MODE_WITHOUT_OAUTH_CLIENT: Joi.boolean().default( - process.env.NODE_ENV === "development" || process.env.NODE_ENV === "testing" ? true : false, - ), GROPIUS_LOGIN_SYNC_API_SECRET: Joi.string(), diff --git a/backend/src/initialization/create-default-auth-client.service.ts b/backend/src/initialization/create-default-auth-client.service.ts index 8184c49d..90a5e5b3 100644 --- a/backend/src/initialization/create-default-auth-client.service.ts +++ b/backend/src/initialization/create-default-auth-client.service.ts @@ -25,7 +25,7 @@ export class CreateDefaultAuthClientService { const clientId = process.env.GROPIUS_DEFAULT_AUTH_CLIENT_ID; const redirectUri = process.env.GROPIUS_DEFAULT_AUTH_CLIENT_REDIRECT; - if (!clientName && !clientId) { + if ((!clientName && !clientId) || !redirectUri) { return; } const nameObject = clientName ? { name: clientName } : {} diff --git a/backend/src/main.ts b/backend/src/main.ts index 3db719e5..ca2c7a08 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -26,7 +26,8 @@ async function bootstrap() { .setDescription("API for login, registration and linking Gropius accounts to accounts on IMSs") .addTag(OpenApiTag.LOGIN_API, "Endpoints to interact with the model, register and link authentications") .addTag(OpenApiTag.SYNC_API, "API to be used by sync services for exchanging IMSUser info") - .addTag(OpenApiTag.CREDENTIALS, "Endpoints for actual authentication. Token retrieval, oauth flow, ...") + .addTag(OpenApiTag.OAUTH_API, "OAuth endpoints for authorization and token management") + .addTag(OpenApiTag.INTERNAL_API, "Internal API, not meant to be used by clients") .addOAuth2({ type: "oauth2", description: "Access token provided by running the oauth flow (and if needed) registering/linking", diff --git a/backend/src/migrationDataSource.config.ts b/backend/src/migrationDataSource.config.ts index 6770eb8a..c23675ab 100644 --- a/backend/src/migrationDataSource.config.ts +++ b/backend/src/migrationDataSource.config.ts @@ -2,7 +2,7 @@ import { DataSource } from "typeorm"; import { randomBytes } from "crypto"; function setEnvVariables() { - process.env.GROPIUS_INTERNAL_BACKEND_JWT_SECRET = randomBytes(100).toString("base64"); + process.env.GROPIUS_OAUTH_JWT_SECRET = randomBytes(100).toString("base64"); process.env.GROPIUS_LOGIN_SPECIFIC_JWT_SECRET = randomBytes(100).toString("base64"); process.env.GROPIUS_ACCESS_TOKEN_EXPIRATION_TIME_MS = "10"; } diff --git a/backend/src/model/services/user-login-data.service.ts b/backend/src/model/services/user-login-data.service.ts index bf34033a..376ad284 100644 --- a/backend/src/model/services/user-login-data.service.ts +++ b/backend/src/model/services/user-login-data.service.ts @@ -1,20 +1,12 @@ -import { HttpException, HttpStatus, Injectable } from "@nestjs/common"; +import { Injectable } from "@nestjs/common"; import { DataSource, Repository } from "typeorm"; import { StrategyInstance } from "../postgres/StrategyInstance.entity"; -import { LoginState, UserLoginData } from "../postgres/UserLoginData.entity"; -import { LoginUserService } from "./login-user.service"; -import { ActiveLoginService } from "./active-login.service"; -import { BackendUserService } from "src/backend-services/backend-user.service"; -import { LoginUser } from "../postgres/LoginUser.entity"; -import { ActiveLogin } from "../postgres/ActiveLogin.entity"; +import { UserLoginData } from "../postgres/UserLoginData.entity"; @Injectable() export class UserLoginDataService extends Repository { constructor( private dataSource: DataSource, - private readonly userService: LoginUserService, - private readonly activeLoginService: ActiveLoginService, - private readonly backendUserSerivce: BackendUserService, ) { super(UserLoginData, dataSource.createEntityManager()); } @@ -50,58 +42,4 @@ export class UserLoginDataService extends Repository { .getMany(); } - /** - * Helper function performing tha actual linking of login data with user. - * - * If the given login data already has a user set, the user must match the given one, - * else a INTERNAL_SERVER_ERROR is raised. - * - * The expiration of the loginData will be removed. - * The expiration of the activeLogin will be set to the default login expiration time, - * except if the strategy supports sync, then the active login will never expire. - * The state of the loginData will be updated to VALID if it was WAITING_FOR_REGISER before - * - * @param userToLinkTo The user account to link the new authentication to - * @param loginData The new authentication to link to the user - * @param activeLogin The active login that was created during the authentication flow - * @returns The saved and updated user and login data after linking - */ - public async linkAccountToUser( - userToLinkTo: LoginUser, - loginData: UserLoginData, - activeLogin: ActiveLogin, - ): Promise<{ loggedInUser: LoginUser; loginData: UserLoginData }> { - if (!userToLinkTo) { - throw new HttpException( - "Not logged in to any account. Linking not possible. Try logging in or registering", - HttpStatus.BAD_REQUEST, - ); - } - if (loginData.state == LoginState.WAITING_FOR_REGISTER) { - loginData.state = LoginState.VALID; - } - const currentLoginDataUser = await loginData.user; - if (currentLoginDataUser == null) { - loginData.user = Promise.resolve(userToLinkTo); - } else { - if (currentLoginDataUser.id !== userToLinkTo.id) { - // Shoud not be reachable as this is already checked in token check - throw new HttpException( - "Login data user did not match logged in user", - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - loginData.expires = null; - loginData = await this.save(loginData); - - await this.activeLoginService.setActiveLoginExpiration(activeLogin); - - userToLinkTo = await this.userService.findOneBy({ - id: userToLinkTo.id, - }); - - await this.backendUserSerivce.linkAllImsUsersToGropiusUser(userToLinkTo, loginData); - return { loggedInUser: userToLinkTo, loginData }; - } } diff --git a/backend/src/strategies/StrategyUsingPassport.ts b/backend/src/strategies/StrategyUsingPassport.ts index 709ff263..82172fd2 100644 --- a/backend/src/strategies/StrategyUsingPassport.ts +++ b/backend/src/strategies/StrategyUsingPassport.ts @@ -73,6 +73,7 @@ export abstract class StrategyUsingPassport extends Strategy { }, (err, user: AuthResult | false, info) => { if (err) { + this.logger.error("Error while authenticating with passport", err); reject(err); } else { let returnedState = {}; diff --git a/backend/src/strategies/github/github.service.ts b/backend/src/strategies/github/github.service.ts index 36a7864b..10ba6419 100644 --- a/backend/src/strategies/github/github.service.ts +++ b/backend/src/strategies/github/github.service.ts @@ -20,11 +20,11 @@ export class GithubStrategyService extends StrategyUsingPassport { strategiesService: StrategiesService, strategyInstanceService: StrategyInstanceService, private readonly loginDataService: UserLoginDataService, - @Inject("PassportStateJwt") - passportJwtService: JwtService, + @Inject("StateJwtService") + stateJwtService: JwtService, private readonly activeLoginService: ActiveLoginService, ) { - super("github", strategyInstanceService, strategiesService, passportJwtService, true, true, true, true); + super("github", strategyInstanceService, strategiesService, stateJwtService, true, true, true, true); } /** diff --git a/backend/src/strategies/jira/jira.service.ts b/backend/src/strategies/jira/jira.service.ts index c9593ce4..0afb02ee 100644 --- a/backend/src/strategies/jira/jira.service.ts +++ b/backend/src/strategies/jira/jira.service.ts @@ -19,11 +19,11 @@ export class JiraStrategyService extends StrategyUsingPassport { strategiesService: StrategiesService, strategyInstanceService: StrategyInstanceService, private readonly loginDataService: UserLoginDataService, - @Inject("PassportStateJwt") - passportJwtService: JwtService, + @Inject("StateJwtService") + stateJwtService: JwtService, private readonly activeLoginService: ActiveLoginService, ) { - super("jira", strategyInstanceService, strategiesService, passportJwtService, true, true, true, true); + super("jira", strategyInstanceService, strategiesService, stateJwtService, true, true, true, true); } /** diff --git a/backend/src/strategies/strategies.module.ts b/backend/src/strategies/strategies.module.ts index 4d32513a..c67b5c34 100644 --- a/backend/src/strategies/strategies.module.ts +++ b/backend/src/strategies/strategies.module.ts @@ -32,9 +32,9 @@ import { JiraStrategyService } from "./jira/jira.service"; UserpassStrategyService, GithubStrategyService, JiraStrategyService, - { provide: "PassportStateJwt", useExisting: JwtService }, + { provide: "StateJwtService", useExisting: JwtService }, StrategiesMiddleware, ], - exports: [StrategiesMiddleware], + exports: [StrategiesMiddleware, { provide: "StateJwtService", useExisting: JwtService }], }) export class StrategiesModule {} diff --git a/backend/src/strategies/userpass/userpass.service.ts b/backend/src/strategies/userpass/userpass.service.ts index 469cdcd3..bb2dac54 100644 --- a/backend/src/strategies/userpass/userpass.service.ts +++ b/backend/src/strategies/userpass/userpass.service.ts @@ -22,10 +22,10 @@ export class UserpassStrategyService extends StrategyUsingPassport { strategyInstanceService: StrategyInstanceService, private readonly loginDataService: UserLoginDataService, private readonly loginUserService: LoginUserService, - @Inject("PassportStateJwt") - passportJwtService: JwtService, + @Inject("StateJwtService") + stateJwtService: JwtService, ) { - super("userpass", strategyInstanceService, strategiesService, passportJwtService, true, false, false, false); + super("userpass", strategyInstanceService, strategiesService, stateJwtService, true, false, false, false); } override get acceptsVariables(): { From 169d790822eac66391d15d2a7f2bd1a19ff652b5 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Fri, 12 Jul 2024 18:55:27 +0200 Subject: [PATCH 08/31] bugfixes --- backend/src/main.ts | 1 - backend/src/strategies/StrategyUsingPassport.ts | 6 +++--- backend/src/strategies/userpass/userpass.service.ts | 6 +----- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/backend/src/main.ts b/backend/src/main.ts index ca2c7a08..4219584c 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -59,5 +59,4 @@ async function bootstrap() { await app.listen(portNumber); } bootstrap() - .then(() => console.log("Application exited")) .catch((err) => console.error("NestJS Application exited with error", err)); diff --git a/backend/src/strategies/StrategyUsingPassport.ts b/backend/src/strategies/StrategyUsingPassport.ts index 82172fd2..9325bf45 100644 --- a/backend/src/strategies/StrategyUsingPassport.ts +++ b/backend/src/strategies/StrategyUsingPassport.ts @@ -14,7 +14,7 @@ export abstract class StrategyUsingPassport extends Strategy { typeName: string, strategyInstanceService: StrategyInstanceService, strategiesService: StrategiesService, - protected readonly passportJwtService: JwtService, + protected readonly stateJwtService: JwtService, canLoginRegister = true, canSync = false, needsRedirectFlow = false, @@ -63,12 +63,12 @@ export abstract class StrategyUsingPassport extends Strategy { ): Promise { return new Promise((resolve, reject) => { const passportStrategy = this.getPassportStrategyInstanceFor(strategyInstance); - const jwtService = this.passportJwtService; + const jwtService = this.stateJwtService; passport.authenticate( passportStrategy, { session: false, - state: jwtService.sign({ request: state.request, authState: state.authState }), // TODO: check if an expiration and/or an additional random value are needed + state: jwtService.sign({ request: state?.request, authState: state?.authState }), // TODO: check if an expiration and/or an additional random value are needed ...this.getAdditionalPassportOptions(strategyInstance, state), }, (err, user: AuthResult | false, info) => { diff --git a/backend/src/strategies/userpass/userpass.service.ts b/backend/src/strategies/userpass/userpass.service.ts index bb2dac54..ba064d3f 100644 --- a/backend/src/strategies/userpass/userpass.service.ts +++ b/backend/src/strategies/userpass/userpass.service.ts @@ -1,14 +1,11 @@ import { Inject, Injectable } from "@nestjs/common"; import { StrategyInstanceService } from "src/model/services/strategy-instance.service"; import { StrategiesService } from "../../model/services/strategies.service"; -import { Strategy, StrategyVariable } from "../Strategy"; +import { StrategyVariable } from "../Strategy"; import * as passportLocal from "passport-local"; import { StrategyInstance } from "src/model/postgres/StrategyInstance.entity"; import * as passport from "passport"; -import { LoginUserService } from "src/model/services/login-user.service"; import { UserLoginDataService } from "src/model/services/user-login-data.service"; -import { ActiveLogin } from "src/model/postgres/ActiveLogin.entity"; -import { LoginUser } from "src/model/postgres/LoginUser.entity"; import { AuthResult } from "../AuthResult"; import { StrategyUsingPassport } from "../StrategyUsingPassport"; import { JwtService } from "@nestjs/jwt"; @@ -21,7 +18,6 @@ export class UserpassStrategyService extends StrategyUsingPassport { strategiesService: StrategiesService, strategyInstanceService: StrategyInstanceService, private readonly loginDataService: UserLoginDataService, - private readonly loginUserService: LoginUserService, @Inject("StateJwtService") stateJwtService: JwtService, ) { From 14350a53efb1f2a02a3bbf1e36275018152b6c92 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Sat, 13 Jul 2024 06:31:39 +0200 Subject: [PATCH 09/31] working login and signup via oauth --- backend/.env.dev | 17 ++- backend/src/api-internal/AuthException.ts | 5 + .../src/api-internal/api-internal.module.ts | 26 +++-- ...s => auth-authorize-extract.middleware.ts} | 6 +- .../api-internal/auth-endpoints.controller.ts | 2 +- .../auth-error-redirect.middleware.ts | 48 ++++++++ .../api-internal/auth-redirect.middleware.ts | 64 ++++++---- .../api-internal/auth-register.middleware.ts | 4 +- .../src/api-login/auth-clients.controller.ts | 43 ++++--- .../dto/create-update-auth-client.dto.ts | 22 ++++ backend/src/api-login/dto/link-user.dto.ts | 2 +- backend/src/api-login/dto/user-inputs.dto.ts | 5 +- backend/src/api-login/register.controller.ts | 2 +- .../oauth-authorize-extract.middleware.ts | 10 +- .../oauth-error-redirect.middleware.ts | 5 +- ...uth-token-authorization-code.middleware.ts | 5 - .../src/api-oauth/oauth-token.controller.ts | 8 +- .../src/api-oauth/oauth-token.middleware.ts | 15 +-- backend/src/app.module.ts | 8 +- .../backend-services/backend-user.service.ts | 6 +- backend/src/backend-services/token.service.ts | 1 + backend/src/configuration-validator.ts | 6 +- .../1720832685920-migration.ts | 14 +++ .../create-default-auth-client.service.ts | 69 ----------- .../initialization/init-listener.service.ts | 3 - .../initialization/initialization.module.ts | 3 - .../src/model/postgres/ActiveLogin.entity.ts | 11 -- .../src/model/postgres/AuthClient.entity.ts | 8 +- .../src/model/services/auth-client.service.ts | 30 +++++ backend/src/strategies/AuthResult.ts | 1 + .../perform-auth-function.service.ts | 3 +- .../src/strategies/strategies.middleware.ts | 14 +-- .../strategies/userpass/userpass.service.ts | 6 +- frontend/src/router/index.ts | 7 +- frontend/src/views/Login.vue | 109 ++++++++---------- frontend/src/views/Register.vue | 91 ++++++--------- frontend/src/views/RouterOnly.vue | 1 - frontend/vite.config.mts | 22 ++-- 38 files changed, 359 insertions(+), 343 deletions(-) create mode 100644 backend/src/api-internal/AuthException.ts rename backend/src/api-internal/{auth-autorize-extract.middleware.ts => auth-authorize-extract.middleware.ts} (77%) create mode 100644 backend/src/api-internal/auth-error-redirect.middleware.ts create mode 100644 backend/src/database-migrations/1720832685920-migration.ts delete mode 100644 backend/src/initialization/create-default-auth-client.service.ts delete mode 100644 frontend/src/views/RouterOnly.vue diff --git a/backend/.env.dev b/backend/.env.dev index 2de0a0ba..4619c4a2 100644 --- a/backend/.env.dev +++ b/backend/.env.dev @@ -126,16 +126,13 @@ GROPIUS_BCRYPT_HASH_ROUNDS=10 # default: 10 #GROPIUS_DEFAULT_USER_POST_DATA={"password": "admin"} #GROPIUS_DEFAULT_USER_STRATEGY_INSTANCE_NAME=userpass-local -### Parameter for creating an auth client -### If this is set, an auth client with that name and with no requirement for secrets will be created. -### If you set the (optional) id, it will take precedence and a client will be created, if none with the given id AND name exist -### To use it for oauth, set a redirect url -### The clientId of the created/found auth client will be printed to the console on startup -#GROPIUS_DEFAULT_AUTH_CLIENT_NAME=initial-client -#GROPIUS_DEFAULT_AUTH_CLIENT_ID=01234567-89ab-cdef-fedc-ba9876543210 -#GROPIUS_DEFAULT_AUTH_CLIENT_REDIRECT=http://localhost:1234/redirect - ### Checking the consistency of the database entitities on startup ### Possible values: "none" (do not check consistency), ### "check" (check and exit if inconsistent), "fix" (check and fix inconsistencies if possible) -GROPIUS_DEFAULT_CHECK_DATABASE_CONSISTENT=check \ No newline at end of file +GROPIUS_DEFAULT_CHECK_DATABASE_CONSISTENT=check + +# -----------------Gropius endpoints------------------------- +### The URL where the Gropius frontend is hosted +GROPIUS_ENDPOINT=http://localhost:4200 # required, no default +### The URL where the Gropius login service is hosted +GROPIUS_LOGIN_SERVICE_ENDPOINT=http://localhost:4200/auth # required, no default \ No newline at end of file diff --git a/backend/src/api-internal/AuthException.ts b/backend/src/api-internal/AuthException.ts new file mode 100644 index 00000000..e388a8a5 --- /dev/null +++ b/backend/src/api-internal/AuthException.ts @@ -0,0 +1,5 @@ +export class AuthException extends Error { + constructor(readonly authErrorMessage: string, readonly strategyInstanceId: string) { + super(authErrorMessage); + } +} diff --git a/backend/src/api-internal/api-internal.module.ts b/backend/src/api-internal/api-internal.module.ts index c8e8f349..a0be0d3c 100644 --- a/backend/src/api-internal/api-internal.module.ts +++ b/backend/src/api-internal/api-internal.module.ts @@ -9,21 +9,28 @@ import { AuthEndpointsController } from "./auth-endpoints.controller"; import { AuthRedirectMiddleware } from "./auth-redirect.middleware"; import { ApiOauthModule } from "src/api-oauth/api-oauth.module"; import { OAuthErrorRedirectMiddleware } from "src/api-oauth/oauth-error-redirect.middleware"; -import { AuthAutorizeExtractMiddleware } from "./auth-autorize-extract.middleware"; +import { AuthAuthorizeExtractMiddleware } from "./auth-authorize-extract.middleware"; import { OAuthAuthorizeValidateMiddleware } from "src/api-oauth/oauth-authorize-validate.middleware"; import { AuthRegisterMiddleware } from "./auth-register.middleware"; import { AuthModule } from "src/api-login/api-login.module"; +import { AuthErrorRedirectMiddleware } from "./auth-error-redirect.middleware"; @Module({ imports: [ModelModule, BackendServicesModule, StrategiesModule, ApiOauthModule, AuthModule], - providers: [AuthAutorizeExtractMiddleware, AuthRedirectMiddleware, AuthRegisterMiddleware, ModeExtractorMiddleware], + providers: [ + AuthAuthorizeExtractMiddleware, + AuthRedirectMiddleware, + AuthRegisterMiddleware, + ModeExtractorMiddleware, + AuthErrorRedirectMiddleware, + ], controllers: [AuthEndpointsController], }) export class ApiInternalModule { private middlewares: { middlewares: NestMiddleware[]; path: string }[] = []; constructor( - private readonly authAutorizeExtract: AuthAutorizeExtractMiddleware, + private readonly authAutorizeExtract: AuthAuthorizeExtractMiddleware, private readonly authRedirect: AuthRedirectMiddleware, private readonly modeExtractor: ModeExtractorMiddleware, private readonly strategies: StrategiesMiddleware, @@ -31,6 +38,7 @@ export class ApiInternalModule { private readonly oauthErrorRedirect: OAuthErrorRedirectMiddleware, private readonly oauthAuthorizeValidate: OAuthAuthorizeValidateMiddleware, private readonly authRegister: AuthRegisterMiddleware, + private readonly authErrorRedirect: AuthErrorRedirectMiddleware, ) { this.middlewares.push({ middlewares: [ @@ -38,10 +46,11 @@ export class ApiInternalModule { this.oauthAuthorizeValidate, this.modeExtractor, this.strategies, + this.authErrorRedirect, this.oauthErrorRedirect, this.errorHandler, ], - path: "auth/internal/auth/redirect/:id/:mode", + path: "auth/api/internal/auth/redirect/:id/:mode", }); this.middlewares.push({ @@ -49,10 +58,11 @@ export class ApiInternalModule { this.strategies, this.oauthAuthorizeValidate, this.authRedirect, + this.authErrorRedirect, this.oauthErrorRedirect, this.errorHandler, ], - path: "auth/internal/auth/callback/:id", + path: "auth/api/internal/auth/callback/:id", }); this.middlewares.push({ @@ -60,11 +70,13 @@ export class ApiInternalModule { this.authAutorizeExtract, this.oauthAuthorizeValidate, this.modeExtractor, + this.strategies, this.authRedirect, + this.authErrorRedirect, this.oauthErrorRedirect, this.errorHandler, ], - path: "auth/internal/auth/submit/:id/:mode", + path: "auth/api/internal/auth/submit/:id/:mode", }); this.middlewares.push({ @@ -76,7 +88,7 @@ export class ApiInternalModule { this.oauthErrorRedirect, this.errorHandler, ], - path: "auth/internal/auth/register", + path: "auth/api/internal/auth/register", }); } diff --git a/backend/src/api-internal/auth-autorize-extract.middleware.ts b/backend/src/api-internal/auth-authorize-extract.middleware.ts similarity index 77% rename from backend/src/api-internal/auth-autorize-extract.middleware.ts rename to backend/src/api-internal/auth-authorize-extract.middleware.ts index 5c0b54b1..3ef89ae5 100644 --- a/backend/src/api-internal/auth-autorize-extract.middleware.ts +++ b/backend/src/api-internal/auth-authorize-extract.middleware.ts @@ -6,7 +6,7 @@ import { OAuthAuthorizeServerState } from "src/api-oauth/OAuthAuthorizeServerSta import { JwtService } from "@nestjs/jwt"; @Injectable() -export class AuthAutorizeExtractMiddleware extends StateMiddleware<{}, OAuthAuthorizeServerState> { +export class AuthAuthorizeExtractMiddleware extends StateMiddleware<{}, OAuthAuthorizeServerState> { constructor( private readonly authClientService: AuthClientService, @Inject("StateJwtService") @@ -21,8 +21,8 @@ export class AuthAutorizeExtractMiddleware extends StateMiddleware<{}, OAuthAuth state: { error?: any }, next: (error?: Error | any) => void, ): Promise { - const newState = this.stateJwtService.verify>(req.query.state as string); - const client = await this.authClientService.findOneBy({ id: newState.request.clientId }); + const newState = this.stateJwtService.verify>(req.query.state ?? req.body.state); + const client = await this.authClientService.findAuthClient(newState.request.clientId); this.appendState(res, { client, ...newState }); next(); } diff --git a/backend/src/api-internal/auth-endpoints.controller.ts b/backend/src/api-internal/auth-endpoints.controller.ts index 0e518f42..156e97f3 100644 --- a/backend/src/api-internal/auth-endpoints.controller.ts +++ b/backend/src/api-internal/auth-endpoints.controller.ts @@ -57,7 +57,7 @@ export class AuthEndpointsController { ); } - @Post("submit/:id/:mode") + @Get("submit/:id/:mode") @ApiOperation({ summary: "Submit endpoint for a strategy instance" }) @ApiParam({ name: "id", type: String, description: "The id of the strategy instance to submit" }) @ApiParam({ diff --git a/backend/src/api-internal/auth-error-redirect.middleware.ts b/backend/src/api-internal/auth-error-redirect.middleware.ts new file mode 100644 index 00000000..42898d46 --- /dev/null +++ b/backend/src/api-internal/auth-error-redirect.middleware.ts @@ -0,0 +1,48 @@ +import { Inject, Injectable, Logger } from "@nestjs/common"; +import { Request, Response } from "express"; +import { OAuthAuthorizeServerState } from "src/api-oauth/OAuthAuthorizeServerState"; +import { StateMiddleware } from "src/api-oauth/StateMiddleware"; +import { AuthException } from "./AuthException"; +import { TokenScope } from "src/backend-services/token.service"; +import { JwtService } from "@nestjs/jwt"; + +@Injectable() +export class AuthErrorRedirectMiddleware extends StateMiddleware { + private readonly logger = new Logger(AuthErrorRedirectMiddleware.name); + + constructor( + @Inject("StateJwtService") + private readonly stateJwtService: JwtService, + ) { + super(); + } + + protected override async useWithState( + req: Request, + res: Response, + state: OAuthAuthorizeServerState & { error?: any }, + next: (error?: Error | any) => void, + ): Promise { + next(); + } + + protected override useWithError( + req: Request, + res: Response, + state: OAuthAuthorizeServerState & { error?: any }, + error: any, + next: (error?: Error | any) => void, + ): void { + if (state.request?.scope && error instanceof AuthException) { + const target = state.request.scope.includes(TokenScope.LOGIN_SERVICE_REGISTER) + ? "register-additional" + : "login"; + const encodedState = encodeURIComponent(this.stateJwtService.sign({ request: state.request })); + res.redirect( + `/auth/flow/${target}?error=${encodeURIComponent(error.authErrorMessage)}&strategy_instance=${encodeURIComponent(error.strategyInstanceId)}&state=${encodedState}`, + ); + } else { + next(); + } + } +} diff --git a/backend/src/api-internal/auth-redirect.middleware.ts b/backend/src/api-internal/auth-redirect.middleware.ts index ec986e8a..2988d681 100644 --- a/backend/src/api-internal/auth-redirect.middleware.ts +++ b/backend/src/api-internal/auth-redirect.middleware.ts @@ -41,7 +41,7 @@ interface UserDataSuggestion { @Injectable() export class AuthRedirectMiddleware extends StateMiddleware< - AuthStateServerData & OAuthAuthorizeServerState & { strategy: Strategy } + AuthStateServerData & OAuthAuthorizeServerState & { strategy: Strategy; secondToken?: boolean } > { private readonly logger = new Logger(AuthRedirectMiddleware.name); constructor( @@ -55,19 +55,21 @@ export class AuthRedirectMiddleware extends StateMiddleware< } private async assignActiveLoginToClient( - state: AuthStateServerData & OAuthAuthorizeServerState, + state: AuthStateServerData & OAuthAuthorizeServerState & { secondToken?: boolean }, expiresIn: number, ): Promise { if (!state.activeLogin.isValid) { throw new Error("Active login invalid"); } - if (state.activeLogin.nextExpectedRefreshTokenNumber != ActiveLogin.LOGGED_IN_BUT_TOKEN_NOT_YET_RETRIVED) { + if ( + state.activeLogin.nextExpectedRefreshTokenNumber != + ActiveLogin.LOGGED_IN_BUT_TOKEN_NOT_YET_RETRIVED + (state.secondToken ? 2 : 0) + ) { throw new Error("Refresh token id is not initial anymore even though no token was retrieved"); } if (state.activeLogin.expires != null && state.activeLogin.expires <= new Date()) { throw new Error("Active login expired"); } - state.activeLogin.createdByClient = Promise.resolve(state.client); if (state.activeLogin.expires == null) { state.activeLogin.expires = new Date(Date.now() + expiresIn); } @@ -78,16 +80,20 @@ export class AuthRedirectMiddleware extends StateMiddleware< return codeJwtId; } - private async generateCode(state: AuthStateServerData & OAuthAuthorizeServerState): Promise { + private async generateCode( + state: AuthStateServerData & OAuthAuthorizeServerState & { secondToken?: boolean }, + clientId: string, + scope: TokenScope[], + ): Promise { const activeLogin = state.activeLogin; try { const expiresIn = parseInt(process.env.GROPIUS_OAUTH_CODE_EXPIRATION_TIME_MS, 10); const codeJwtId = await this.assignActiveLoginToClient(state, expiresIn); const token = await this.tokenService.signActiveLoginCode( activeLogin.id, - state.client.id, + clientId, codeJwtId, - state.request.scope, + scope, expiresIn, ); this.logger.debug("Created token"); @@ -135,26 +141,38 @@ export class AuthRedirectMiddleware extends StateMiddleware< const userLoginData = await state.activeLogin.loginInstanceFor; if (state.request.scope.includes(TokenScope.LOGIN_SERVICE_REGISTER)) { if (userLoginData.state === LoginState.WAITING_FOR_REGISTER) { - const url = new URL(state.request.redirect); - const token = await this.generateCode(state); - url.searchParams.append("code", token); - res.redirect(url.toString()); + await this.redirectWithCode(state, res); } else { throw new OAuthHttpException("invalid_request", "Login is not in register state"); } } else { - const encodedState = encodeURIComponent( - this.stateJwtService.sign({ request: state.request, authState: state.authState }), - ); - const token = await this.generateCode(state); - const suggestions = await this.getDataSuggestions(userLoginData, state.strategy); - const suggestionQuery = `&email=${encodeURIComponent( - suggestions.email ?? "", - )}&username=${encodeURIComponent( - suggestions.username ?? "", - )}&displayName=${encodeURIComponent(suggestions.displayName ?? "")}`; - const url = `/auth/flow/register?code=${token}&state=${encodedState}` + suggestionQuery; - res.redirect(url); + if (userLoginData.state === LoginState.WAITING_FOR_REGISTER) { + const encodedState = encodeURIComponent( + this.stateJwtService.sign({ request: state.request, authState: state.authState }), + ); + const token = await this.generateCode(state, "login-auth-client", [TokenScope.LOGIN_SERVICE_REGISTER]); + const suggestions = await this.getDataSuggestions(userLoginData, state.strategy); + const suggestionQuery = `&email=${encodeURIComponent( + suggestions.email ?? "", + )}&username=${encodeURIComponent( + suggestions.username ?? "", + )}&displayName=${encodeURIComponent(suggestions.displayName ?? "")}`; + const url = `/auth/flow/register?code=${token}&state=${encodedState}` + suggestionQuery; + res.redirect(url); + } else { + await this.redirectWithCode(state, res); + } } } + + private async redirectWithCode( + state: AuthStateServerData & OAuthAuthorizeServerState & { strategy: Strategy } & { error?: any }, + res: Response>, + ) { + const url = new URL(state.request.redirect); + const token = await this.generateCode(state, state.client.id, state.request.scope); + url.searchParams.append("code", token); + url.searchParams.append("state", state.request.state ?? ""); + res.redirect(url.toString()); + } } diff --git a/backend/src/api-internal/auth-register.middleware.ts b/backend/src/api-internal/auth-register.middleware.ts index 09bf5380..b22eaaab 100644 --- a/backend/src/api-internal/auth-register.middleware.ts +++ b/backend/src/api-internal/auth-register.middleware.ts @@ -12,7 +12,7 @@ import { BackendUserService } from "src/backend-services/backend-user.service"; @Injectable() export class AuthRegisterMiddleware extends StateMiddleware< OAuthAuthorizeServerState & AuthStateServerData, - OAuthAuthorizeServerState & AuthStateServerData + OAuthAuthorizeServerState & AuthStateServerData & { secondToken: boolean } > { constructor( private readonly checkRegistrationTokenService: CheckRegistrationTokenService, @@ -41,7 +41,7 @@ export class AuthRegisterMiddleware extends StateMiddleware< } const newUser = await this.backendUserSerivce.createNewUser(input, false); await this.backendUserSerivce.linkAccountToUser(newUser, loginData, activeLogin); - this.appendState(res, { activeLogin }) + this.appendState(res, { activeLogin, secondToken: true }) next(); } } diff --git a/backend/src/api-login/auth-clients.controller.ts b/backend/src/api-login/auth-clients.controller.ts index f74e8398..7c60ecaf 100644 --- a/backend/src/api-login/auth-clients.controller.ts +++ b/backend/src/api-login/auth-clients.controller.ts @@ -8,33 +8,22 @@ import { Param, Post, Put, - Res, UseGuards, } from "@nestjs/common"; import { ApiBadRequestResponse, ApiBearerAuth, - ApiConsumes, ApiCreatedResponse, ApiNotFoundResponse, - ApiOAuth2, ApiOkResponse, ApiOperation, ApiParam, ApiTags, } from "@nestjs/swagger"; -import { Response } from "express"; -import { BackendUserService } from "src/backend-services/backend-user.service"; -import { TokenScope } from "src/backend-services/token.service"; import { DefaultReturn } from "src/default-return.dto"; import { AuthClient } from "src/model/postgres/AuthClient.entity"; -import { LoginUser } from "src/model/postgres/LoginUser.entity"; -import { UserLoginData } from "src/model/postgres/UserLoginData.entity"; import { AuthClientService } from "src/model/services/auth-client.service"; -import { LoginUserService } from "src/model/services/login-user.service"; -import { UserLoginDataService } from "src/model/services/user-login-data.service"; import { OpenApiTag } from "src/openapi-tag"; -import { ApiStateData } from "./ApiStateData"; import { CheckAccessTokenGuard, NeedsAdmin } from "./check-access-token.guard"; import { CreateAuthClientSecretResponse } from "./dto/create-auth-client-secret.dto"; import { CreateOrUpdateAuthClientInput } from "./dto/create-update-auth-client.dto"; @@ -53,9 +42,6 @@ import { CensoredClientSecret, GetAuthClientResponse } from "./dto/get-auth-clie @ApiTags(OpenApiTag.LOGIN_API) export class AuthClientController { constructor( - private readonly userService: LoginUserService, - private readonly backendUserSerice: BackendUserService, - private readonly loginDataSerive: UserLoginDataService, private readonly authClientService: AuthClientService, ) { } @@ -75,7 +61,7 @@ export class AuthClientController { }) @ApiOperation({ summary: "List all existing auth clients." }) async listAllAuthClients(): Promise { - return this.authClientService.find(); + return [...this.authClientService.defaultAuthClients, ...await this.authClientService.find()]; } /** @@ -93,8 +79,7 @@ export class AuthClientController { @ApiParam({ name: "id", type: String, - format: "uuid", - description: "The uuid string of an existing auth client to return", + description: "The id string of an existing auth client to return", }) @ApiOkResponse({ type: GetAuthClientResponse, @@ -104,7 +89,7 @@ export class AuthClientController { description: "If no id was given or no auth client with the given id was found", }) async getOneAuthClient(@Param("id") id: string): Promise { - const authClient = await this.authClientService.findOneBy({ id }); + const authClient = await this.authClientService.findAuthClient(id); if (!authClient) { throw new HttpException("Auth client with given id not found", HttpStatus.NOT_FOUND); } @@ -162,6 +147,12 @@ export class AuthClientController { } else { newClient.requiresSecret = true; } + newClient.validScopes = []; + if (input.validScopes) { + for (const scope of input.validScopes) { + newClient.validScopes.push(scope); + } + } return this.authClientService.save(newClient); } @@ -196,7 +187,7 @@ export class AuthClientController { const authClient = await this.authClientService.findOneBy({ id }); if (!authClient) { - throw new HttpException("Auth client with given id not found", HttpStatus.NOT_FOUND); + throw new HttpException("Auth client with given id not found or is default auth client", HttpStatus.NOT_FOUND); } if (input.name) { @@ -214,6 +205,12 @@ export class AuthClientController { if (input.requiresSecret != undefined) { authClient.requiresSecret = input.requiresSecret; } + if (input.validScopes) { + authClient.validScopes = []; + for (const scope of input.validScopes) { + authClient.validScopes.push(scope); + } + } return this.authClientService.save(authClient); } @@ -243,7 +240,7 @@ export class AuthClientController { async deleteAuthClient(@Param("id") id: string): Promise { const authClient = await this.authClientService.findOneBy({ id }); if (!authClient) { - throw new HttpException("Auth client with given id not found", HttpStatus.NOT_FOUND); + throw new HttpException("Auth client with given id not found or is default auth client", HttpStatus.NOT_FOUND); } await this.authClientService.remove(authClient); @@ -274,7 +271,7 @@ export class AuthClientController { description: "If no id was given or no auth client with the given id was found", }) async getClientSecrets(@Param("id") id: string): Promise { - const authClient = await this.authClientService.findOneBy({ id }); + const authClient = await this.authClientService.findAuthClient(id); if (!authClient) { throw new HttpException("Auth client with given id not found", HttpStatus.NOT_FOUND); } @@ -312,7 +309,7 @@ export class AuthClientController { async createClientSecret(@Param("id") id: string): Promise { const authClient = await this.authClientService.findOneBy({ id }); if (!authClient) { - throw new HttpException("Auth client with given id not found", HttpStatus.NOT_FOUND); + throw new HttpException("Auth client with given id not found or is default auth client", HttpStatus.NOT_FOUND); } const result = await authClient.addSecret(); @@ -362,7 +359,7 @@ export class AuthClientController { const authClient = await this.authClientService.findOneBy({ id }); if (!authClient) { - throw new HttpException("Auth client with given id not found", HttpStatus.NOT_FOUND); + throw new HttpException("Auth client with given id not found or is default auth client", HttpStatus.NOT_FOUND); } const allSecrets = authClient.getFullHashesPlusCensoredAndFingerprint(); diff --git a/backend/src/api-login/dto/create-update-auth-client.dto.ts b/backend/src/api-login/dto/create-update-auth-client.dto.ts index 027ccc33..ac9fbbb8 100644 --- a/backend/src/api-login/dto/create-update-auth-client.dto.ts +++ b/backend/src/api-login/dto/create-update-auth-client.dto.ts @@ -1,5 +1,6 @@ import { HttpException, HttpStatus } from "@nestjs/common"; import { ApiProperty } from "@nestjs/swagger"; +import { TokenScope } from "src/backend-services/token.service"; /** * Input to `POST /login/client` and PUT /login/client/:id` @@ -38,6 +39,14 @@ export class CreateOrUpdateAuthClientInput { */ requiresSecret?: boolean; + /** + * The list of scopes that this client is allowed to request. + * + * Defaults to `[]` on create + * @example ["backend"] + */ + validScopes?: TokenScope[]; + /** * Checks a given `CreateOrUpdateAuthClientInput` for validity. * @@ -83,6 +92,19 @@ export class CreateOrUpdateAuthClientInput { if (input.requiresSecret != undefined && typeof input.requiresSecret !== "boolean") { throw new HttpException("If requiresSecret is given, it must be a valid boolean", HttpStatus.BAD_REQUEST); } + if (input.validScopes != undefined) { + if (!(input.validScopes instanceof Array)) { + throw new HttpException("validScopes must be an array of strings", HttpStatus.BAD_REQUEST); + } + } + for (const scope of input.validScopes) { + if (scope !== TokenScope.BACKEND && scope !== TokenScope.LOGIN_SERVICE) { + throw new HttpException( + `Only ${TokenScope.BACKEND} and ${TokenScope.LOGIN_SERVICE} are valid scopes`, + HttpStatus.BAD_REQUEST, + ); + } + } return input; } } diff --git a/backend/src/api-login/dto/link-user.dto.ts b/backend/src/api-login/dto/link-user.dto.ts index e8d93f0a..22935108 100644 --- a/backend/src/api-login/dto/link-user.dto.ts +++ b/backend/src/api-login/dto/link-user.dto.ts @@ -2,7 +2,7 @@ import { HttpException, HttpStatus } from "@nestjs/common"; /** * Input type for self-linking - * Only expects the registration_token + * Only expects the register_token */ export class RegistrationTokenInput { /** diff --git a/backend/src/api-login/dto/user-inputs.dto.ts b/backend/src/api-login/dto/user-inputs.dto.ts index 6782faf0..eb79fccf 100644 --- a/backend/src/api-login/dto/user-inputs.dto.ts +++ b/backend/src/api-login/dto/user-inputs.dto.ts @@ -48,9 +48,12 @@ export class BaseUserInput { if (!input.displayName || input.displayName.trim().length == 0) { throw new HttpException("Display name must be given and can't be empty", HttpStatus.BAD_REQUEST); } + if (input.email?.length === 0) { + input.email = undefined; + } if (input.email != undefined) { if (input.email.trim().length == 0) { - throw new HttpException("If email is given it can't be empty", HttpStatus.BAD_REQUEST); + throw new HttpException("If email is given it can't be blank", HttpStatus.BAD_REQUEST); } } return input; diff --git a/backend/src/api-login/register.controller.ts b/backend/src/api-login/register.controller.ts index 0b4484d6..97e94b8a 100644 --- a/backend/src/api-login/register.controller.ts +++ b/backend/src/api-login/register.controller.ts @@ -81,7 +81,7 @@ export class RegisterController { * * A (still) valid registration token is needed. * After a successful linking, the expiration of the activeLogin and loginData will be updated accoringly - * @param input The input with the registration_token and the user id of the user to link + * @param input The input with the register_token and the user id of the user to link * @param res The response object of the server containing the state with the logged in user * @returns The default response with operation 'admin-link' */ diff --git a/backend/src/api-oauth/oauth-authorize-extract.middleware.ts b/backend/src/api-oauth/oauth-authorize-extract.middleware.ts index a8cae1e4..d9b85f0b 100644 --- a/backend/src/api-oauth/oauth-authorize-extract.middleware.ts +++ b/backend/src/api-oauth/oauth-authorize-extract.middleware.ts @@ -4,6 +4,7 @@ import { StateMiddleware } from "./StateMiddleware"; import { OAuthAuthorizeRequest, OAuthAuthorizeServerState } from "./OAuthAuthorizeServerState"; import { AuthClientService } from "src/model/services/auth-client.service"; import { TokenScope } from "src/backend-services/token.service"; +import { AuthClient } from "src/model/postgres/AuthClient.entity"; @Injectable() export class OAuthAuthorizeExtractMiddleware extends StateMiddleware<{}, OAuthAuthorizeServerState> { @@ -19,14 +20,19 @@ export class OAuthAuthorizeExtractMiddleware extends StateMiddleware<{}, OAuthAu ) { const requestParams: OAuthAuthorizeRequest = { state: req.query.state as string, - redirect: req.query.redirect as string, + redirect: req.query.redirect_uri as string, clientId: req.query.client_id as string, scope: (req.query.scope as string).split(" ").filter((s) => s.length > 0) as TokenScope[], codeChallenge: req.query.code_challenge as string, codeChallengeMethod: req.query.code_challenge_method as string, responseType: req.query.response_type as "code", }; - const client = await this.authClientService.findOneBy({ id: requestParams.clientId }); + let client: AuthClient | undefined + try { + client = await this.authClientService.findAuthClient(requestParams.clientId); + } catch { + client = undefined; + } this.appendState(res, { request: requestParams, client }); next(); } diff --git a/backend/src/api-oauth/oauth-error-redirect.middleware.ts b/backend/src/api-oauth/oauth-error-redirect.middleware.ts index f6f95371..b6967a4c 100644 --- a/backend/src/api-oauth/oauth-error-redirect.middleware.ts +++ b/backend/src/api-oauth/oauth-error-redirect.middleware.ts @@ -1,10 +1,12 @@ -import { Injectable } from "@nestjs/common"; +import { Injectable, Logger } from "@nestjs/common"; import { Request, Response } from "express"; import { StateMiddleware } from "./StateMiddleware"; import { OAuthAuthorizeServerState } from "./OAuthAuthorizeServerState"; @Injectable() export class OAuthErrorRedirectMiddleware extends StateMiddleware { + private readonly logger = new Logger(OAuthErrorRedirectMiddleware.name); + protected override async useWithState( req: Request, res: Response, @@ -30,6 +32,7 @@ export class OAuthErrorRedirectMiddleware extends StateMiddleware decodeURIComponent(text)); if (clientIdSecret && clientIdSecret.length == 2) { - const client = await this.authClientService.findOneBy({ - id: clientIdSecret[0], - }); + const client = await this.authClientService.findAuthClient(clientIdSecret[0]); if (client && client.isValid) { if (this.checkGivenClientSecretValidOrNotRequired(client, clientIdSecret[1])) { return client; @@ -67,9 +62,7 @@ export class OauthTokenMiddleware extends StateMiddleware<{}, { client: AuthClie } if (req.body.client_id) { - const client = await this.authClientService.findOneBy({ - id: req.body.client_id, - }); + const client = await this.authClientService.findAuthClient(req.body.client_id); if (client && client.isValid) { if (this.checkGivenClientSecretValidOrNotRequired(client, req.body.client_secret)) { return client; @@ -99,9 +92,7 @@ export class OauthTokenMiddleware extends StateMiddleware<{}, { client: AuthClie case "refresh_token": //Request for new token using refresh token //Fallthrough as resfresh token works the same as the initial code (both used to obtain new access token) case "authorization_code": //Request for token based on obtained code - await this.tokenResponseCodeMiddleware.use(req, res, () => { - next(); - }); + next(); break; case "password": // Deprecated => not supported case "client_credentials": //Request for token for stuff on client => not supported diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 078495f3..cf32346a 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -61,10 +61,10 @@ import { ApiOauthModule } from "./api-oauth/api-oauth.module"; ApiInternalModule, ApiOauthModule, RouterModule.register([ - { path: "auth/login", module: AuthModule }, - { path: "auth/login", module: StrategiesModule }, - { path: "auth/sync", module: ApiSyncModule }, - { path: "auth/internal", module: ApiInternalModule }, + { path: "auth/api/login", module: AuthModule }, + { path: "auth/api/login", module: StrategiesModule }, + { path: "auth/api/sync", module: ApiSyncModule }, + { path: "auth/api/internal", module: ApiInternalModule }, { path: "auth/oauth", module: ApiOauthModule} ]), BackendServicesModule, diff --git a/backend/src/backend-services/backend-user.service.ts b/backend/src/backend-services/backend-user.service.ts index 8aa92f8f..b99a073b 100644 --- a/backend/src/backend-services/backend-user.service.ts +++ b/backend/src/backend-services/backend-user.service.ts @@ -169,8 +169,10 @@ export class BackendUserService { const failedLinks = linkResults.filter( (result) => result.status == "rejected" || (result.status == "fulfilled" && !result.value.updateIMSUser.imsUser.id), - ); //.map((result) => (result.status == "fulfilled" ? result.value : result.reason)); - this.logger.warn("Failures during linking ims user and Gropius user:", failedLinks); + ); + if (failedLinks.length > 0) { + this.logger.warn("Failures during linking ims user and Gropius user:", failedLinks); + } } /** diff --git a/backend/src/backend-services/token.service.ts b/backend/src/backend-services/token.service.ts index a4b33310..289b5d4d 100644 --- a/backend/src/backend-services/token.service.ts +++ b/backend/src/backend-services/token.service.ts @@ -17,6 +17,7 @@ export enum TokenScope { LOGIN_SERVICE = "login", LOGIN_SERVICE_REGISTER = "login-register", BACKEND = "backend", + AUTH = "auth", } enum RefreshTokenScope { diff --git a/backend/src/configuration-validator.ts b/backend/src/configuration-validator.ts index c5489b30..3dd34dad 100644 --- a/backend/src/configuration-validator.ts +++ b/backend/src/configuration-validator.ts @@ -40,9 +40,9 @@ export const validationSchema = Joi.object({ GROPIUS_DEFAULT_USER_DISPLAYNAME: Joi.string().optional(), GROPIUS_DEFAULT_USER_PASSWORD: Joi.string().optional(), GROPIUS_DEFAULT_USER_STRATEGY_INSTANCE_NAME: Joi.string(), - GROPIUS_DEFAULT_AUTH_CLIENT_NAME: Joi.string().optional(), - GROPIUS_DEFAULT_AUTH_CLIENT_ID: Joi.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i).optional(), - GROPIUS_DEFAULT_AUTH_CLIENT_REDIRECT: Joi.string().optional(), GROPIUS_DEFAULT_CHECK_DATABASE_CONSISTENT: Joi.string().allow("none", "check", "fix").default("none"), + + GROPIUS_ENDPOINT: Joi.string().uri().required(), + GROPIUS_LOGIN_SERVICE_ENDPOINT: Joi.string().uri().required(), }); diff --git a/backend/src/database-migrations/1720832685920-migration.ts b/backend/src/database-migrations/1720832685920-migration.ts new file mode 100644 index 00000000..c24328f7 --- /dev/null +++ b/backend/src/database-migrations/1720832685920-migration.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Migration1720832685920 implements MigrationInterface { + name = 'Migration1720832685920' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "auth_client" ADD "validScopes" json NOT NULL DEFAULT '[]'`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "auth_client" DROP COLUMN "validScopes"`); + } + +} diff --git a/backend/src/initialization/create-default-auth-client.service.ts b/backend/src/initialization/create-default-auth-client.service.ts deleted file mode 100644 index 90a5e5b3..00000000 --- a/backend/src/initialization/create-default-auth-client.service.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Injectable, Logger } from "@nestjs/common"; -import { BackendUserService } from "src/backend-services/backend-user.service"; -import { AuthClient } from "src/model/postgres/AuthClient.entity"; -import { UserLoginData, LoginState } from "src/model/postgres/UserLoginData.entity"; -import { AuthClientService } from "src/model/services/auth-client.service"; -import { LoginUserService } from "src/model/services/login-user.service"; -import { StrategiesService } from "src/model/services/strategies.service"; -import { StrategyInstanceService } from "src/model/services/strategy-instance.service"; -import { UserLoginDataService } from "src/model/services/user-login-data.service"; - -@Injectable() -export class CreateDefaultAuthClientService { - private readonly logger = new Logger(CreateDefaultAuthClientService.name); - constructor( - private readonly strategiesService: StrategiesService, - private readonly strategyInstanceService: StrategyInstanceService, - private readonly loginUserService: LoginUserService, - private readonly userLoginDataService: UserLoginDataService, - private readonly backendUserService: BackendUserService, - private readonly authClientService: AuthClientService, - ) { } - - async createDefaultAuthClient() { - const clientName = process.env.GROPIUS_DEFAULT_AUTH_CLIENT_NAME; - const clientId = process.env.GROPIUS_DEFAULT_AUTH_CLIENT_ID; - const redirectUri = process.env.GROPIUS_DEFAULT_AUTH_CLIENT_REDIRECT; - - if ((!clientName && !clientId) || !redirectUri) { - return; - } - const nameObject = clientName ? { name: clientName } : {} - const idObject = clientId ? { id: clientId } : {} - let authClient = await this.authClientService.findOneBy({ - ...nameObject, - ...idObject, - requiresSecret: false, - isValid: true, - }); - if (authClient) { - this.logger.log( - `Valid auth client with name ${clientName} without secret already exists. Skipping creation. Id:`, - authClient.id, - ); - if (!authClient.redirectUrls.includes(redirectUri)) { - this.logger.warn(`The existing auth client does not include the redirect url specified as config parameter! -If you require this, remove the existing client OR change the redirect URL via the API`) - } - return; - } - - - authClient = new AuthClient(); - if (clientId) { - authClient.id = clientId; - } - authClient.isValid = true; - authClient.name = clientName; - authClient.requiresSecret = false; - authClient.clientSecrets = []; - if (redirectUri) { - authClient.redirectUrls = [redirectUri]; - } else { - authClient.redirectUrls = []; - } - authClient = await this.authClientService.save(authClient); - - this.logger.log(`Created auth client with name ${clientName} without secret. Id:`, authClient.id); - } -} diff --git a/backend/src/initialization/init-listener.service.ts b/backend/src/initialization/init-listener.service.ts index 9df5dfa2..05d9ec87 100644 --- a/backend/src/initialization/init-listener.service.ts +++ b/backend/src/initialization/init-listener.service.ts @@ -1,5 +1,4 @@ import { Injectable, OnModuleInit } from "@nestjs/common"; -import { CreateDefaultAuthClientService } from "./create-default-auth-client.service"; import { CreateDefaultStrategyInstanceService } from "./create-default-strategy-instance.service"; import { CreateDefaultUserService } from "./create-default-user.service"; import { CheckDatabaseConsistencyService } from "./check-database-consistency.service"; @@ -10,13 +9,11 @@ export class InitListenerService implements OnModuleInit { private readonly dbConsistencyService: CheckDatabaseConsistencyService, private readonly createInstanceService: CreateDefaultStrategyInstanceService, private readonly createUserService: CreateDefaultUserService, - private readonly createClientService: CreateDefaultAuthClientService, ) {} async onModuleInit() { await this.dbConsistencyService.runDatabaseCheck(); await this.createInstanceService.createDefaultStrtegyInstance(); await this.createUserService.createDefaultUser(); - await this.createClientService.createDefaultAuthClient(); } } diff --git a/backend/src/initialization/initialization.module.ts b/backend/src/initialization/initialization.module.ts index 74e4f1e3..3ce68077 100644 --- a/backend/src/initialization/initialization.module.ts +++ b/backend/src/initialization/initialization.module.ts @@ -1,8 +1,6 @@ import { Module } from "@nestjs/common"; import { BackendServicesModule } from "src/backend-services/backend-services.module"; import { ModelModule } from "src/model/model.module"; -import { StrategiesModule } from "src/strategies/strategies.module"; -import { CreateDefaultAuthClientService } from "./create-default-auth-client.service"; import { CreateDefaultStrategyInstanceService } from "./create-default-strategy-instance.service"; import { CreateDefaultUserService } from "./create-default-user.service"; import { InitListenerService } from "./init-listener.service"; @@ -21,7 +19,6 @@ import { CheckDatabaseConsistencyService } from "./check-database-consistency.se CheckDatabaseConsistencyService, CreateDefaultStrategyInstanceService, CreateDefaultUserService, - CreateDefaultAuthClientService, InitListenerService, ], }) diff --git a/backend/src/model/postgres/ActiveLogin.entity.ts b/backend/src/model/postgres/ActiveLogin.entity.ts index 42b0aa02..c902af26 100644 --- a/backend/src/model/postgres/ActiveLogin.entity.ts +++ b/backend/src/model/postgres/ActiveLogin.entity.ts @@ -119,17 +119,6 @@ export class ActiveLogin { @ApiHideProperty() loginInstanceFor: Promise; - /** - * The auth client that asked for the user to be authenticated and caused the creation of this login event. - * - * May be null on creation of the login event and may be set only once token is retrieved. - */ - @ManyToOne(() => AuthClient, (client) => client.loginsOfThisClient, { - nullable: true, - }) - @ApiHideProperty() - createdByClient: Promise; - toJSON() { return { id: this.id, diff --git a/backend/src/model/postgres/AuthClient.entity.ts b/backend/src/model/postgres/AuthClient.entity.ts index ef98d3d3..481993aa 100644 --- a/backend/src/model/postgres/AuthClient.entity.ts +++ b/backend/src/model/postgres/AuthClient.entity.ts @@ -4,6 +4,7 @@ import * as bcrypt from "bcrypt"; import * as crypto from "crypto"; import { promisify } from "util"; import { ApiHideProperty } from "@nestjs/swagger"; +import { TokenScope } from "src/backend-services/token.service"; /** * The minimum length of the client secret in bytes. @@ -81,11 +82,10 @@ export class AuthClient { requiresSecret: boolean; /** - * A list of all login events that this client caused. + * The list of scopes that this client is allowed to request. */ - @OneToMany(() => ActiveLogin, (login) => login.createdByClient) - @ApiHideProperty() - loginsOfThisClient: Promise; + @Column("json") + validScopes: TokenScope[]; /** * Calculated the sha256 hash of the input. diff --git a/backend/src/model/services/auth-client.service.ts b/backend/src/model/services/auth-client.service.ts index 6637172a..86e0c049 100644 --- a/backend/src/model/services/auth-client.service.ts +++ b/backend/src/model/services/auth-client.service.ts @@ -1,10 +1,40 @@ import { Injectable } from "@nestjs/common"; import { DataSource, Repository } from "typeorm"; import { AuthClient } from "../postgres/AuthClient.entity"; +import { TokenScope } from "src/backend-services/token.service"; @Injectable() export class AuthClientService extends Repository { constructor(private dataSource: DataSource) { super(AuthClient, dataSource.createEntityManager()); } + + readonly defaultAuthClients: AuthClient[] = this.createDefaultAuthClients(); + + private createDefaultAuthClients(): AuthClient[] { + const gropiusAuthClient = new AuthClient(); + gropiusAuthClient.name = "Gropius auth client"; + gropiusAuthClient.redirectUrls = [`${process.env.GROPIUS_ENDPOINT}/login`]; + gropiusAuthClient.id = "gropius-auth-client"; + gropiusAuthClient.isValid = true; + gropiusAuthClient.validScopes = [TokenScope.BACKEND, TokenScope.LOGIN_SERVICE, TokenScope.LOGIN_SERVICE_REGISTER]; + + const loginAuthClient = new AuthClient(); + loginAuthClient.name = "Login auth client"; + loginAuthClient.redirectUrls = [`${process.env.GROPIUS_LOGIN_SERVICE_ENDPOINT}/flow/update`]; + loginAuthClient.id = "login-auth-client"; + loginAuthClient.isValid = true; + loginAuthClient.validScopes = [TokenScope.LOGIN_SERVICE_REGISTER, TokenScope.AUTH]; + + return [gropiusAuthClient, loginAuthClient]; + } + + async findAuthClient(id: string): Promise { + const defaultClient = this.defaultAuthClients.find((client) => client.id === id); + if (defaultClient) { + return defaultClient; + } else { + return this.findOneBy({ id }); + } + } } diff --git a/backend/src/strategies/AuthResult.ts b/backend/src/strategies/AuthResult.ts index d058d45f..ac58aabc 100644 --- a/backend/src/strategies/AuthResult.ts +++ b/backend/src/strategies/AuthResult.ts @@ -22,4 +22,5 @@ export interface AuthResult { dataUserLoginData: object; loginData?: UserLoginData; mayRegister: boolean; + noRegisterMessage?: string; } diff --git a/backend/src/strategies/perform-auth-function.service.ts b/backend/src/strategies/perform-auth-function.service.ts index 62ea925c..f29b3eff 100644 --- a/backend/src/strategies/perform-auth-function.service.ts +++ b/backend/src/strategies/perform-auth-function.service.ts @@ -10,6 +10,7 @@ import { AuthStateServerData, AuthFunction, AuthResult } from "./AuthResult"; import { StrategiesService } from "../model/services/strategies.service"; import { Strategy } from "./Strategy"; import { OAuthHttpException } from "src/api-oauth/OAuthHttpException"; +import { AuthException } from "src/api-internal/AuthException"; /** * Contains the logic how the system is supposed to create and link @@ -159,7 +160,7 @@ export class PerformAuthFunctionService { return this.registerNewUser(authResult, instance, authFunction == AuthFunction.REGISTER_WITH_SYNC); } else if (authFunction == AuthFunction.LOGIN && !wantsToDoImplicitRegister) { - throw new OAuthHttpException("server_error", "Invalid user credentials."); + throw new AuthException(authResult.noRegisterMessage ?? "Invalid user credentials.", instance.id); } } throw new OAuthHttpException("server_error", "Unknown error during authentication"); diff --git a/backend/src/strategies/strategies.middleware.ts b/backend/src/strategies/strategies.middleware.ts index 0a51924c..0fbbc866 100644 --- a/backend/src/strategies/strategies.middleware.ts +++ b/backend/src/strategies/strategies.middleware.ts @@ -10,12 +10,12 @@ import { Strategy } from "./Strategy"; import { StateMiddleware } from "src/api-oauth/StateMiddleware"; import { OAuthHttpException } from "src/api-oauth/OAuthHttpException"; import { OAuthAuthorizeServerState } from "src/api-oauth/OAuthAuthorizeServerState"; -import { AuthClientService } from "src/model/services/auth-client.service"; +import { AuthException } from "src/api-internal/AuthException"; @Injectable() export class StrategiesMiddleware extends StateMiddleware< AuthStateServerData & OAuthAuthorizeServerState, - AuthStateServerData & OAuthAuthorizeServerState + AuthStateServerData & OAuthAuthorizeServerState & { strategy: Strategy } > { private readonly logger = new Logger(StrategiesMiddleware.name); constructor( @@ -23,7 +23,6 @@ export class StrategiesMiddleware extends StateMiddleware< private readonly strategyInstanceService: StrategyInstanceService, private readonly performAuthFunctionService: PerformAuthFunctionService, private readonly imsUserFindingService: ImsUserFindingService, - private readonly authClientService: AuthClientService, ) { super(); } @@ -69,6 +68,7 @@ export class StrategiesMiddleware extends StateMiddleware< const id = req.params.id; const instance = await this.idToStrategyInstance(id); const strategy = await this.strategiesService.getStrategyByName(instance.type); + this.appendState(res, { strategy }); const functionError = this.performAuthFunctionService.checkFunctionIsAllowed(state, instance, strategy); if (functionError != null) { @@ -77,9 +77,6 @@ export class StrategiesMiddleware extends StateMiddleware< const result = await strategy.performAuth(instance, state, req, res); this.appendState(res, result.returnedState); - if (!state.client && state.request.clientId) { - state.client = await this.authClientService.findOneBy({ id: state.request.clientId }); - } const authResult = result.result; if (authResult) { @@ -90,11 +87,12 @@ export class StrategiesMiddleware extends StateMiddleware< strategy, ); this.appendState(res, { activeLogin }); + state.authState.activeLogin = activeLogin.id; await this.performImsUserSearchIfNeeded(state, instance, strategy); } else { - throw new OAuthHttpException( - "server_error", + throw new AuthException( result.info?.message?.toString() || JSON.stringify(result.info) || "Login unsuccessfull", + instance.id ); } this.logger.debug("Strategy Middleware completed. Calling next"); diff --git a/backend/src/strategies/userpass/userpass.service.ts b/backend/src/strategies/userpass/userpass.service.ts index ba064d3f..84a4ba70 100644 --- a/backend/src/strategies/userpass/userpass.service.ts +++ b/backend/src/strategies/userpass/userpass.service.ts @@ -85,8 +85,8 @@ export class UserpassStrategyService extends StrategyUsingPassport { const dataUserLoginData = await this.generateLoginDataData(username, password); return done( null, - { dataActiveLogin, dataUserLoginData, mayRegister: true }, - { message: "Username or password incorrect" }, + { dataActiveLogin, dataUserLoginData, mayRegister: true, noRegisterMessage: "Username or password incorrect" }, + { }, ); } else if (loginDataForCorrectUser.length > 1) { return done("More than one user with same username", false, undefined); @@ -98,7 +98,7 @@ export class UserpassStrategyService extends StrategyUsingPassport { if (!hasCorrectPassword) { return done( null, - { dataActiveLogin, dataUserLoginData: {}, mayRegister: false }, + false, { message: "Username or password incorrect" }, ); } diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 3c3e53c9..ca81d0c2 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -7,11 +7,6 @@ const routes: RouteRecordRaw[] = [ name: "login", component: () => import("../views/Login.vue"), }, - { - path: "/logout", - name: "logout", - component: () => import("../views/Logout.vue") - }, { path: "/register", name: "register", @@ -20,7 +15,7 @@ const routes: RouteRecordRaw[] = [ ]; const router = createRouter({ - history: createWebHistory(process.env.BASE_URL), + history: createWebHistory("/auth/flow"), routes }); diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue index 91c31bac..e039b662 100644 --- a/frontend/src/views/Login.vue +++ b/frontend/src/views/Login.vue @@ -3,6 +3,10 @@ diff --git a/frontend/src/views/model.ts b/frontend/src/views/model.ts index 44b4622b..718dc417 100644 --- a/frontend/src/views/model.ts +++ b/frontend/src/views/model.ts @@ -32,13 +32,20 @@ export interface LoginStrategyVariable { nullable?: boolean; } +export interface LoginStrategyUpdateAction { + name: string; + displayName: string; + variables: LoginStrategyVariable[]; +} + export interface LoginStrategy { typeName: string; canLoginRegister: boolean; canSync: boolean; needsRedirectFlow: boolean; allowsImplicitSignup: boolean; - acceptsVariables: { [name: string]: LoginStrategyVariable }; + acceptsVariables: LoginStrategyVariable[]; + updateActions: LoginStrategyUpdateAction[]; } export interface LoginStrategyInstance { From 54af0ad0a044918222756bc52418ccb56fc264e3 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Mon, 22 Jul 2024 14:07:21 +0200 Subject: [PATCH 20/31] misc improvements --- .../api-internal/auth-endpoints.controller.ts | 5 +- .../api-internal/update-action.controller.ts | 2 +- .../dto/create-update-auth-client.dto.ts | 4 +- .../strategy/strategy-instances.controller.ts | 1 - .../api-oauth/oauth-authorize.controller.ts | 2 +- .../src/api-oauth/oauth-token.controller.ts | 6 +- ...ImsTokenResult.ts => get-ims-token.dto.ts} | 2 +- .../api-sync/dto/link-ims-users-input.dto.ts | 20 +++++ .../src/api-sync/sync-ims-user.controller.ts | 27 ++++--- .../github-token/github-token.service.ts | 75 ++++++++++++++++--- .../strategies/userpass/userpass.service.ts | 10 +-- frontend/src/views/Update.vue | 50 ++++++++++--- 12 files changed, 154 insertions(+), 50 deletions(-) rename backend/src/api-sync/dto/{GetImsTokenResult.ts => get-ims-token.dto.ts} (69%) create mode 100644 backend/src/api-sync/dto/link-ims-users-input.dto.ts diff --git a/backend/src/api-internal/auth-endpoints.controller.ts b/backend/src/api-internal/auth-endpoints.controller.ts index 156e97f3..ae5a7e00 100644 --- a/backend/src/api-internal/auth-endpoints.controller.ts +++ b/backend/src/api-internal/auth-endpoints.controller.ts @@ -12,6 +12,7 @@ import { SelfRegisterUserInput } from "src/api-login/dto/user-inputs.dto"; * - Redirect/Callback endpoint */ @Controller("auth") +@ApiTags(OpenApiTag.INTERNAL_API) export class AuthEndpointsController { /** * Authorize endpoint for strategy instance of the given id. @@ -29,7 +30,6 @@ export class AuthEndpointsController { required: false, description: "The function/mode how to authenticate. Defaults to 'login'", }) - @ApiTags(OpenApiTag.INTERNAL_API) authorizeEndpoint(@Param("id") id: string, @Param("mode") mode?: AuthFunctionInput) { throw new HttpException( "This controller shouldn't be reached as all functionality is handeled in middleware", @@ -49,7 +49,6 @@ export class AuthEndpointsController { name: "id", description: "The id of the strategy instance which initiated the funcation calling the callback.", }) - @ApiTags(OpenApiTag.INTERNAL_API) redirectEndpoint() { throw new HttpException( "This controller shouldn't be reached as all functionality is handeled in middleware", @@ -66,7 +65,6 @@ export class AuthEndpointsController { required: false, description: "The function/mode how to authenticate. Defaults to 'login'", }) - @ApiTags(OpenApiTag.INTERNAL_API) submitEndpoint() { throw new HttpException( "This controller shouldn't be reached as all functionality is handeled in middleware", @@ -76,7 +74,6 @@ export class AuthEndpointsController { @Post("register") @ApiOperation({ summary: "Copmplete a registration" }) - @ApiTags(OpenApiTag.INTERNAL_API) registerEndpoint(@Body() input: SelfRegisterUserInput) { throw new HttpException( "This controller shouldn't be reached as all functionality is handeled in middleware", diff --git a/backend/src/api-internal/update-action.controller.ts b/backend/src/api-internal/update-action.controller.ts index 1b08f692..c8803dd8 100644 --- a/backend/src/api-internal/update-action.controller.ts +++ b/backend/src/api-internal/update-action.controller.ts @@ -28,6 +28,7 @@ import { StrategiesService } from "src/model/services/strategies.service"; import { DefaultReturn } from "src/default-return.dto"; @Controller("update-action") +@ApiTags(OpenApiTag.INTERNAL_API) @UseGuards(CheckAuthAccessTokenGuard) export class UpdateActionController { constructor( @@ -47,7 +48,6 @@ export class UpdateActionController { @ApiNotFoundResponse({ description: "If no login data with the given id are found" }) @ApiBadRequestResponse({ description: "If any of the input values are invalid" }) @ApiBearerAuth() - @ApiTags(OpenApiTag.INTERNAL_API) async updateAction( @Body() input: object, @Param("id") id: string, diff --git a/backend/src/api-login/dto/create-update-auth-client.dto.ts b/backend/src/api-login/dto/create-update-auth-client.dto.ts index ac9fbbb8..c7200e69 100644 --- a/backend/src/api-login/dto/create-update-auth-client.dto.ts +++ b/backend/src/api-login/dto/create-update-auth-client.dto.ts @@ -68,7 +68,7 @@ export class CreateOrUpdateAuthClientInput { } } if (input.redirectUrls != undefined) { - if (!(input.redirectUrls instanceof Array) || input.redirectUrls.length == 0) { + if (!Array.isArray(input.redirectUrls) || input.redirectUrls.length == 0) { throw new HttpException( "If redirect URLs are given, they must be an array of valid url strings " + "containing at least one entry", @@ -93,7 +93,7 @@ export class CreateOrUpdateAuthClientInput { throw new HttpException("If requiresSecret is given, it must be a valid boolean", HttpStatus.BAD_REQUEST); } if (input.validScopes != undefined) { - if (!(input.validScopes instanceof Array)) { + if (!Array.isArray(input.validScopes)) { throw new HttpException("validScopes must be an array of strings", HttpStatus.BAD_REQUEST); } } diff --git a/backend/src/api-login/strategy/strategy-instances.controller.ts b/backend/src/api-login/strategy/strategy-instances.controller.ts index ca2d73f6..862b9a50 100644 --- a/backend/src/api-login/strategy/strategy-instances.controller.ts +++ b/backend/src/api-login/strategy/strategy-instances.controller.ts @@ -185,7 +185,6 @@ export class StrategyInstancesController { * @returns If successful, the updated strategy instance. */ @Put(["strategy-instance/:id", "strategy/:type/instance/:id"]) - @ApiTags(OpenApiTag.LOGIN_API, OpenApiTag.LOGIN_API) @UseGuards(CheckLoginServiceAccessTokenGuard) @NeedsAdmin() @ApiOperation({ summary: "Edit an existing strategy instance" }) diff --git a/backend/src/api-oauth/oauth-authorize.controller.ts b/backend/src/api-oauth/oauth-authorize.controller.ts index e7fb4052..f9000a7c 100644 --- a/backend/src/api-oauth/oauth-authorize.controller.ts +++ b/backend/src/api-oauth/oauth-authorize.controller.ts @@ -19,7 +19,7 @@ export class OauthAuthorizeController { * */ @Get("authorize") - @ApiOperation({ summary: "Authorize endpoint" }) + @ApiOperation({ summary: "Authorize OAuth endpoint" }) @ApiQuery({ name: "client_id", type: String, description: "The id of the client to initiate" }) @ApiQuery({ name: "response_type", diff --git a/backend/src/api-oauth/oauth-token.controller.ts b/backend/src/api-oauth/oauth-token.controller.ts index a61650ff..4a9cebfe 100644 --- a/backend/src/api-oauth/oauth-token.controller.ts +++ b/backend/src/api-oauth/oauth-token.controller.ts @@ -1,5 +1,5 @@ import { Controller, Logger, Post, Res } from "@nestjs/common"; -import { ApiTags } from "@nestjs/swagger"; +import { ApiOkResponse, ApiOperation, ApiTags } from "@nestjs/swagger"; import { Response } from "express"; import { TokenScope, TokenService } from "src/backend-services/token.service"; import { ActiveLogin } from "src/model/postgres/ActiveLogin.entity"; @@ -12,7 +12,7 @@ import { AuthStateServerData } from "src/strategies/AuthResult"; import { ensureState } from "src/util/ensureState"; import { OAuthHttpException } from "../api-oauth/OAuthHttpException"; -export interface OauthTokenEndpointResponseDto { +export class OauthTokenEndpointResponseDto { access_token: string; token_type: "bearer"; expires_in: number; @@ -133,6 +133,8 @@ export class OAuthTokenController { } @Post("token") + @ApiOperation({ summary: "Token OAuth Endpoint" }) + @ApiOkResponse({ type: OauthTokenEndpointResponseDto }) async token(@Res({ passthrough: true }) res: Response): Promise { ensureState(res); const currentClient = res.locals.state.client as AuthClient; diff --git a/backend/src/api-sync/dto/GetImsTokenResult.ts b/backend/src/api-sync/dto/get-ims-token.dto.ts similarity index 69% rename from backend/src/api-sync/dto/GetImsTokenResult.ts rename to backend/src/api-sync/dto/get-ims-token.dto.ts index ba4c6d2d..bee5187c 100644 --- a/backend/src/api-sync/dto/GetImsTokenResult.ts +++ b/backend/src/api-sync/dto/get-ims-token.dto.ts @@ -1,4 +1,4 @@ -export interface GetImsTokenResult { +export class GetImsTokenResult { token: string | null; isImsUserKnown: boolean; message: string | null; diff --git a/backend/src/api-sync/dto/link-ims-users-input.dto.ts b/backend/src/api-sync/dto/link-ims-users-input.dto.ts new file mode 100644 index 00000000..ccf9b052 --- /dev/null +++ b/backend/src/api-sync/dto/link-ims-users-input.dto.ts @@ -0,0 +1,20 @@ +import { HttpException, HttpStatus } from "@nestjs/common"; + +export class LinkImsUsersInputDto { + /** + * The username of the user in the IMS + */ + imsUserIds: string[]; + + static check(input: LinkImsUsersInputDto): LinkImsUsersInputDto { + if (!Array.isArray(input.imsUserIds) || input.imsUserIds.length == 0) { + throw new HttpException("The imsUserIds must be given and can't be empty", HttpStatus.BAD_REQUEST); + } + for (const id of input.imsUserIds) { + if (typeof id != "string" || id.trim().length == 0) { + throw new HttpException("The imsUserIds must be an array of non-empty strings", HttpStatus.BAD_REQUEST); + } + } + return input; + } +} \ No newline at end of file diff --git a/backend/src/api-sync/sync-ims-user.controller.ts b/backend/src/api-sync/sync-ims-user.controller.ts index 54239e06..95e40105 100644 --- a/backend/src/api-sync/sync-ims-user.controller.ts +++ b/backend/src/api-sync/sync-ims-user.controller.ts @@ -1,13 +1,14 @@ -import { Controller, Get, HttpException, HttpStatus, Logger, Put, Query, UseGuards } from "@nestjs/common"; +import { Body, Controller, Get, HttpException, HttpStatus, Logger, Param, Put, Query, UseGuards } from "@nestjs/common"; import { ImsUserFindingService } from "src/backend-services/ims-user-finding.service"; import { DefaultReturn } from "src/default-return.dto"; import { UserLoginDataImsUser } from "src/model/postgres/UserLoginDataImsUser.entity"; import { UserLoginDataImsUserService } from "src/model/services/user-login-data-ims-user"; import { StrategiesService } from "src/model/services/strategies.service"; import { CheckSyncSecretGuard } from "./check-sync-secret.guard"; -import { GetImsTokenResult } from "./dto/GetImsTokenResult"; -import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; +import { GetImsTokenResult } from "./dto/get-ims-token.dto"; +import { ApiBadRequestResponse, ApiBearerAuth, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from "@nestjs/swagger"; import { OpenApiTag } from "src/openapi-tag"; +import { LinkImsUsersInputDto } from "./dto/link-ims-users-input.dto"; @Controller() @UseGuards(CheckSyncSecretGuard) @@ -21,8 +22,12 @@ export class SyncImsUserController { private readonly imsUserFindingService: ImsUserFindingService, ) {} - @Get("get-ims-token") - async getIMSToken(@Query("imsUser") imsUserId: string): Promise { + @Get("get-ims-token/:id") + @ApiOperation({ summary: "Get the IMS token for a given IMS user id" }) + @ApiParam({ name: "id", description: "The neo4j id of the IMS user", required: true }) + @ApiOkResponse({ type: GetImsTokenResult }) + @ApiBadRequestResponse({ description: "Missing query parameter imsUser or failed to load referenced IMS user" }) + async getIMSToken(@Param("id") imsUserId: string): Promise { if (!imsUserId || imsUserId.trim().length == 0) { throw new HttpException("Missing query parameter imsUser", HttpStatus.BAD_REQUEST); } @@ -53,12 +58,14 @@ export class SyncImsUserController { } //todo: make endpoint accept list of ims users to be linked all at once in order to optimize finding - @Put("link-ims-user") - async linkIMSUser(@Query("imsUser") imsUserId: string): Promise { - if (!imsUserId || imsUserId.trim().length == 0) { - throw new HttpException("Missing query parameter imsUser", HttpStatus.BAD_REQUEST); + @Put("link-ims-users") + @ApiOperation({ summary: "Link IMS users to the system" }) + @ApiOkResponse({ type: DefaultReturn }) + async linkIMSUser(@Body() input: LinkImsUsersInputDto): Promise { + LinkImsUsersInputDto.check(input); + for (const imsUserId of input.imsUserIds) { + await this.imsUserFindingService.createAndLinkSingleImsUser(imsUserId); } - await this.imsUserFindingService.createAndLinkSingleImsUser(imsUserId); return new DefaultReturn("linkIMSUser"); } } diff --git a/backend/src/strategies/github-token/github-token.service.ts b/backend/src/strategies/github-token/github-token.service.ts index f0835112..7bbee745 100644 --- a/backend/src/strategies/github-token/github-token.service.ts +++ b/backend/src/strategies/github-token/github-token.service.ts @@ -1,5 +1,5 @@ -import { Injectable } from "@nestjs/common"; -import { PerformAuthResult, Strategy, StrategyVariable } from "../Strategy"; +import { HttpException, HttpStatus, Injectable } from "@nestjs/common"; +import { PerformAuthResult, Strategy, StrategyUpdateAction, StrategyVariable } from "../Strategy"; import { OAuthAuthorizeServerState } from "src/api-oauth/OAuthAuthorizeServerState"; import { StrategyInstance } from "src/model/postgres/StrategyInstance.entity"; import { AuthStateServerData } from "../AuthResult"; @@ -7,10 +7,15 @@ import { Schema } from "jtd"; import { StrategiesService } from "src/model/services/strategies.service"; import { StrategyInstanceService } from "src/model/services/strategy-instance.service"; import { UserLoginData } from "src/model/postgres/UserLoginData.entity"; +import { UserLoginDataService } from "src/model/services/user-login-data.service"; @Injectable() export class GithubTokenStrategyService extends Strategy { - constructor(strategiesService: StrategiesService, strategyInstanceService: StrategyInstanceService) { + constructor( + strategiesService: StrategiesService, + strategyInstanceService: StrategyInstanceService, + private readonly loginDataService: UserLoginDataService, + ) { super("github-token", strategyInstanceService, strategiesService, false, true, false, false, false); } @@ -35,6 +40,22 @@ export class GithubTokenStrategyService extends Strategy { ]; } + override get updateActions(): StrategyUpdateAction[] { + return [ + { + name: "update-token", + displayName: "Update personal access token", + variables: [ + { + name: "token", + displayName: "Personal access token", + type: "password", + }, + ], + }, + ]; + } + /** * Chechs the given config is valid for a github (or github enterprise) * @@ -99,13 +120,12 @@ export class GithubTokenStrategyService extends Strategy { }; } - override async performAuth( - strategyInstance: StrategyInstance, - state: (AuthStateServerData & OAuthAuthorizeServerState) | undefined, - req: any, - res: any, - ): Promise { - const token = req.query["token"]; + private async getUserData(token: string, strategyInstance: StrategyInstance): Promise<{ + github_id: string; + username: string; + displayName: string; + email: string; + } | null> { const graphqlQuery = ` { viewer { @@ -126,18 +146,32 @@ export class GithubTokenStrategyService extends Strategy { }); if (!response.ok) { - return { result: null, returnedState: {}, info: { message: "Token invalid" } }; + return null; } const data = await response.json(); const userData = data.data.viewer; - const userLoginData = { + return { github_id: userData.id, username: userData.login, displayName: userData.name, email: userData.email, }; + } + + override async performAuth( + strategyInstance: StrategyInstance, + state: (AuthStateServerData & OAuthAuthorizeServerState) | undefined, + req: any, + res: any, + ): Promise { + const token = req.query["token"]; + + const userLoginData = await this.getUserData(token, strategyInstance); + if (userLoginData == null) { + return { result: null, returnedState: {}, info: { message: "Token invalid" } } + } return { result: { @@ -149,4 +183,21 @@ export class GithubTokenStrategyService extends Strategy { info: {}, }; } + + override async handleAction(loginData: UserLoginData, name: string, data: Record): Promise { + if (name === "update-token") { + const accessToken = data["token"]; + const userLoginData = await this.getUserData(accessToken, await loginData.strategyInstance); + if (userLoginData == null) { + throw new HttpException("Token invalid", HttpStatus.BAD_REQUEST); + } + if (loginData.data["github_id"] !== userLoginData.github_id) { + throw new HttpException("Token does not match the user", HttpStatus.BAD_REQUEST); + } + loginData.data["accessToken"] = accessToken; + this.loginDataService.save(loginData); + } else { + throw new HttpException("Unknown action", HttpStatus.BAD_REQUEST); + } + } } diff --git a/backend/src/strategies/userpass/userpass.service.ts b/backend/src/strategies/userpass/userpass.service.ts index 9f14fbfe..f3b453f0 100644 --- a/backend/src/strategies/userpass/userpass.service.ts +++ b/backend/src/strategies/userpass/userpass.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from "@nestjs/common"; +import { HttpException, HttpStatus, Inject, Injectable } from "@nestjs/common"; import { StrategyInstanceService } from "src/model/services/strategy-instance.service"; import { StrategiesService } from "../../model/services/strategies.service"; import { StrategyUpdateAction, StrategyVariable } from "../Strategy"; @@ -41,7 +41,7 @@ export class UserpassStrategyService extends StrategyUsingPassport { override get updateActions(): StrategyUpdateAction[] { return [{ - name: "updatePassword", + name: "update-password", displayName: "Update password", variables: [ { @@ -140,15 +140,15 @@ export class UserpassStrategyService extends StrategyUsingPassport { } override async handleAction(loginData: UserLoginData, name: string, data: Record): Promise { - if (name === "updatePassword") { + if (name === "update-password") { if (!data.password || data.password.trim().length == 0) { - throw new Error("Password cannot be empty or blank!"); + throw new HttpException("Password cannot be empty or blank!", HttpStatus.BAD_REQUEST); } loginData.data = await this.generateLoginDataData(loginData.data["username"], data.password); await this.loginDataService.save(loginData); } else { - throw new Error("Unknown action"); + throw new HttpException("Unknown action", HttpStatus.BAD_REQUEST); } } } diff --git a/frontend/src/views/Update.vue b/frontend/src/views/Update.vue index 52855f21..e9f2fe82 100644 --- a/frontend/src/views/Update.vue +++ b/frontend/src/views/Update.vue @@ -21,6 +21,15 @@ + + + {{ errorMessage }} +
@@ -61,10 +70,11 @@ const router = useRouter(); const actionTab = ref(0); const showSuccessMessage = ref(false); +const errorMessage = ref(); const id = computed(() => (route.query.id as string | undefined) ?? JSON.parse(route.query.state as string).id); -const accessToken = ref(); +const refreshToken = ref(); const strategy = ref(); @@ -81,35 +91,53 @@ function chooseAction(action: LoginStrategyUpdateAction) { function goBack() { actionTab.value = 0; showSuccessMessage.value = false; + errorMessage.value = undefined; } async function submitForm() { - await axios.put(`/auth/api/internal/update-action/${id.value}/${chosenAction.value?.name}`, formData.value, { - headers: { - Authorization: `Bearer ${accessToken.value}` - } - }); - actionTab.value = 0; - showSuccessMessage.value = true; + const tokenResponse = ( + await axios.post("/auth/oauth/token", { + grant_type: "refresh_token", + client_id: "login-auth-client", + refresh_token: refreshToken.value + }) + ).data; + const accessToken = tokenResponse.access_token; + refreshToken.value = tokenResponse.refresh_token; + + try { + await axios.put(`/auth/api/internal/update-action/${id.value}/${chosenAction.value?.name}`, formData.value, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + actionTab.value = 0; + showSuccessMessage.value = true; + errorMessage.value = undefined; + } catch (e: any) { + errorMessage.value = e.response?.data?.message ?? "An error occurred"; + } } onMounted(async () => { const code = route.query.code!.toString(); const codeVerifier = localStorage.getItem("loginServiceCodeVerifier"); router.replace({ query: { id: id.value } }); - accessToken.value = ( + const tokenResponse = ( await axios.post("/auth/oauth/token", { grant_type: "authorization_code", client_id: "login-auth-client", code, code_verifier: codeVerifier }) - ).data.access_token as string; + ).data; + const accessToken = tokenResponse.access_token; + refreshToken.value = tokenResponse.refresh_token; const strategyInstance = ( await axios.get(`/auth/api/login/login-data/${id.value}`, { headers: { - Authorization: `Bearer ${accessToken.value}` + Authorization: `Bearer ${accessToken}` } }) ).data.strategyInstance as LoginStrategyInstance; From 1fe8a4c7f551dbe442df63fac3970ca899b28ba0 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Mon, 22 Jul 2024 14:10:49 +0200 Subject: [PATCH 21/31] backend format --- backend/package.json | 4 +- backend/src/api-internal/AuthException.ts | 5 +- .../api-internal/auth-register.middleware.ts | 2 +- backend/src/api-login/api-login.module.ts | 2 +- .../src/api-login/auth-clients.controller.ts | 41 +- .../check-registration-token.service.ts | 6 +- .../src/api-login/dto/user-login-data.dto.ts | 10 +- .../dto/get-strategy-instance-detail.dto.ts | 3 +- .../api-oauth/OAuthAuthorizeServerState.ts | 2 +- backend/src/api-oauth/OAuthHttpException.ts | 5 +- .../src/api-oauth/OAuthTokenServerState.ts | 2 +- backend/src/api-oauth/StateMiddleware.ts | 10 +- backend/src/api-oauth/encryption.service.ts | 8 +- .../api-oauth/oauth-authorize.controller.ts | 1 - ...uth-token-authorization-code.middleware.ts | 2 +- .../src/api-oauth/oauth-token.controller.ts | 2 +- .../src/api-oauth/oauth-token.middleware.ts | 4 +- .../api-sync/dto/link-ims-users-input.dto.ts | 2 +- backend/src/app.module.ts | 4 +- .../1678970550245-migration.ts | 59 +- .../1720832685920-migration.ts | 3 +- .../check-database-consistency.service.ts | 12 +- backend/src/main.ts | 3 +- backend/src/model/graphql/generated.ts | 6256 +++++++++-------- backend/src/strategies/Strategy.ts | 10 +- .../src/strategies/StrategyUsingPassport.ts | 2 +- .../github-token/github-token.service.ts | 7 +- .../strategies/userpass/userpass.service.ts | 24 +- backend/src/util/NeedsAdmin.ts | 2 +- 29 files changed, 3517 insertions(+), 2976 deletions(-) diff --git a/backend/package.json b/backend/package.json index 5d9fa8eb..38dedb43 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,12 +7,12 @@ "license": "MIT", "scripts": { "build": "nest build", - "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "format": "prettier --write \"src/**/*.ts\"", "start": "nest start", "start:dev": "cross-env NODE_ENV=development nest start --watch", "start:debug": "cross-env NODE_ENV=development nest start --debug --watch", "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "lint": "eslint \"{src}/**/*.ts\" --fix", "generate-model": "graphql-codegen --config codegen.yml", "generate-migration": "npm run build && npx typeorm migration:generate -d ./dist/migrationDataSource.config.js ./src/database-migrations/migration", "init-database": "npm run build && npx typeorm migration:run -d ./dist/migrationDataSource.config.js" diff --git a/backend/src/api-internal/AuthException.ts b/backend/src/api-internal/AuthException.ts index e388a8a5..ef702b4d 100644 --- a/backend/src/api-internal/AuthException.ts +++ b/backend/src/api-internal/AuthException.ts @@ -1,5 +1,8 @@ export class AuthException extends Error { - constructor(readonly authErrorMessage: string, readonly strategyInstanceId: string) { + constructor( + readonly authErrorMessage: string, + readonly strategyInstanceId: string, + ) { super(authErrorMessage); } } diff --git a/backend/src/api-internal/auth-register.middleware.ts b/backend/src/api-internal/auth-register.middleware.ts index b22eaaab..c4d1f1f2 100644 --- a/backend/src/api-internal/auth-register.middleware.ts +++ b/backend/src/api-internal/auth-register.middleware.ts @@ -41,7 +41,7 @@ export class AuthRegisterMiddleware extends StateMiddleware< } const newUser = await this.backendUserSerivce.createNewUser(input, false); await this.backendUserSerivce.linkAccountToUser(newUser, loginData, activeLogin); - this.appendState(res, { activeLogin, secondToken: true }) + this.appendState(res, { activeLogin, secondToken: true }); next(); } } diff --git a/backend/src/api-login/api-login.module.ts b/backend/src/api-login/api-login.module.ts index e3fad0c1..ef7a894e 100644 --- a/backend/src/api-login/api-login.module.ts +++ b/backend/src/api-login/api-login.module.ts @@ -23,7 +23,7 @@ import { LoginDataController } from "./login-data.controller"; StrategiesController, StrategyInstancesController, AuthClientController, - LoginDataController + LoginDataController, ], providers: [CheckRegistrationTokenService], exports: [CheckRegistrationTokenService], diff --git a/backend/src/api-login/auth-clients.controller.ts b/backend/src/api-login/auth-clients.controller.ts index 299f907b..279a9b51 100644 --- a/backend/src/api-login/auth-clients.controller.ts +++ b/backend/src/api-login/auth-clients.controller.ts @@ -1,15 +1,4 @@ -import { - Body, - Controller, - Delete, - Get, - HttpException, - HttpStatus, - Param, - Post, - Put, - UseGuards, -} from "@nestjs/common"; +import { Body, Controller, Delete, Get, HttpException, HttpStatus, Param, Post, Put, UseGuards } from "@nestjs/common"; import { ApiBadRequestResponse, ApiBearerAuth, @@ -42,9 +31,7 @@ import { NeedsAdmin } from "src/util/NeedsAdmin"; @ApiBearerAuth() @ApiTags(OpenApiTag.LOGIN_API) export class AuthClientController { - constructor( - private readonly authClientService: AuthClientService, - ) { } + constructor(private readonly authClientService: AuthClientService) {} /** * Gets all auth clients that exist in the system. @@ -62,7 +49,7 @@ export class AuthClientController { }) @ApiOperation({ summary: "List all existing auth clients." }) async listAllAuthClients(): Promise { - return [...this.authClientService.defaultAuthClients, ...await this.authClientService.find()]; + return [...this.authClientService.defaultAuthClients, ...(await this.authClientService.find())]; } /** @@ -142,7 +129,7 @@ export class AuthClientController { } else { newClient.isValid = true; } - newClient.clientSecrets = [] + newClient.clientSecrets = []; if (input.requiresSecret !== undefined) { newClient.requiresSecret = input.requiresSecret; } else { @@ -188,7 +175,10 @@ export class AuthClientController { const authClient = await this.authClientService.findOneBy({ id }); if (!authClient) { - throw new HttpException("Auth client with given id not found or is default auth client", HttpStatus.NOT_FOUND); + throw new HttpException( + "Auth client with given id not found or is default auth client", + HttpStatus.NOT_FOUND, + ); } if (input.name) { @@ -241,7 +231,10 @@ export class AuthClientController { async deleteAuthClient(@Param("id") id: string): Promise { const authClient = await this.authClientService.findOneBy({ id }); if (!authClient) { - throw new HttpException("Auth client with given id not found or is default auth client", HttpStatus.NOT_FOUND); + throw new HttpException( + "Auth client with given id not found or is default auth client", + HttpStatus.NOT_FOUND, + ); } await this.authClientService.remove(authClient); @@ -310,7 +303,10 @@ export class AuthClientController { async createClientSecret(@Param("id") id: string): Promise { const authClient = await this.authClientService.findOneBy({ id }); if (!authClient) { - throw new HttpException("Auth client with given id not found or is default auth client", HttpStatus.NOT_FOUND); + throw new HttpException( + "Auth client with given id not found or is default auth client", + HttpStatus.NOT_FOUND, + ); } const result = await authClient.addSecret(); @@ -360,7 +356,10 @@ export class AuthClientController { const authClient = await this.authClientService.findOneBy({ id }); if (!authClient) { - throw new HttpException("Auth client with given id not found or is default auth client", HttpStatus.NOT_FOUND); + throw new HttpException( + "Auth client with given id not found or is default auth client", + HttpStatus.NOT_FOUND, + ); } const allSecrets = authClient.getFullHashesPlusCensoredAndFingerprint(); diff --git a/backend/src/api-login/check-registration-token.service.ts b/backend/src/api-login/check-registration-token.service.ts index ac3b450b..317d3900 100644 --- a/backend/src/api-login/check-registration-token.service.ts +++ b/backend/src/api-login/check-registration-token.service.ts @@ -1,8 +1,4 @@ -import { - Injectable, - Logger, - UnauthorizedException, -} from "@nestjs/common"; +import { Injectable, Logger, UnauthorizedException } from "@nestjs/common"; import { TokenService } from "src/backend-services/token.service"; import { ActiveLogin } from "src/model/postgres/ActiveLogin.entity"; import { LoginUser } from "src/model/postgres/LoginUser.entity"; diff --git a/backend/src/api-login/dto/user-login-data.dto.ts b/backend/src/api-login/dto/user-login-data.dto.ts index 95cbc9e9..ed45451c 100644 --- a/backend/src/api-login/dto/user-login-data.dto.ts +++ b/backend/src/api-login/dto/user-login-data.dto.ts @@ -19,7 +19,7 @@ export class UserLoginDataResponse { * * @example "VALID" */ - state: LoginState + state: LoginState; /** * If not `null`, this authentication should be considered *invalid* on any date+time AFTER this. @@ -28,17 +28,17 @@ export class UserLoginDataResponse { * * If `null`, the authentication should not expire by date. */ - expires: Date | null + expires: Date | null; /** * The strategy instance this authentication uses. * * For example a UserLoginData containing a password would reference a strategy instance of type userpass */ - strategyInstance: StrategyInstance + strategyInstance: StrategyInstance; /** * A description of the authentication */ - description: string -} \ No newline at end of file + description: string; +} diff --git a/backend/src/api-login/strategy/dto/get-strategy-instance-detail.dto.ts b/backend/src/api-login/strategy/dto/get-strategy-instance-detail.dto.ts index eb117af2..f0309eb9 100644 --- a/backend/src/api-login/strategy/dto/get-strategy-instance-detail.dto.ts +++ b/backend/src/api-login/strategy/dto/get-strategy-instance-detail.dto.ts @@ -36,9 +36,8 @@ export class StrategyInstanceDetailResponse { doesImplicitRegister: boolean; /** * Instance config with sensitive data removed - * + * * {@link StrategyInstance.instanceConfig} */ instanceConfig: object; } - diff --git a/backend/src/api-oauth/OAuthAuthorizeServerState.ts b/backend/src/api-oauth/OAuthAuthorizeServerState.ts index 4b61bb55..5e07cb3f 100644 --- a/backend/src/api-oauth/OAuthAuthorizeServerState.ts +++ b/backend/src/api-oauth/OAuthAuthorizeServerState.ts @@ -15,4 +15,4 @@ export interface OAuthAuthorizeServerState { request: OAuthAuthorizeRequest; client: AuthClient; isRegisterAdditional: boolean; -} \ No newline at end of file +} diff --git a/backend/src/api-oauth/OAuthHttpException.ts b/backend/src/api-oauth/OAuthHttpException.ts index 6aa7862f..3fce13c3 100644 --- a/backend/src/api-oauth/OAuthHttpException.ts +++ b/backend/src/api-oauth/OAuthHttpException.ts @@ -1,7 +1,10 @@ import { HttpException, HttpStatus } from "@nestjs/common"; export class OAuthHttpException extends HttpException { - constructor(readonly error_type: string, readonly error_message: string) { + constructor( + readonly error_type: string, + readonly error_message: string, + ) { super( { statusCode: HttpStatus.BAD_REQUEST, diff --git a/backend/src/api-oauth/OAuthTokenServerState.ts b/backend/src/api-oauth/OAuthTokenServerState.ts index d6ebbf4f..6bed3014 100644 --- a/backend/src/api-oauth/OAuthTokenServerState.ts +++ b/backend/src/api-oauth/OAuthTokenServerState.ts @@ -4,4 +4,4 @@ import { AuthClient } from "src/model/postgres/AuthClient.entity"; export interface OAuthTokenServerState { client: AuthClient; activeLogin: ActiveLogin; -} \ No newline at end of file +} diff --git a/backend/src/api-oauth/StateMiddleware.ts b/backend/src/api-oauth/StateMiddleware.ts index d36fb570..8a194939 100644 --- a/backend/src/api-oauth/StateMiddleware.ts +++ b/backend/src/api-oauth/StateMiddleware.ts @@ -29,11 +29,17 @@ export abstract class StateMiddleware = {}, T exte /** * Overwrite this to handle errors */ - protected useWithError(req: Request, res: Response, state: S & { error?: any }, error: any, next: (error?: Error | any) => void) { + protected useWithError( + req: Request, + res: Response, + state: S & { error?: any }, + error: any, + next: (error?: Error | any) => void, + ) { next(); } - protected appendState(res: Response, appendedState: Partial & { error?: any } | { error?: any }) { + protected appendState(res: Response, appendedState: (Partial & { error?: any }) | { error?: any }) { Object.assign(res.locals.state, appendedState); } } diff --git a/backend/src/api-oauth/encryption.service.ts b/backend/src/api-oauth/encryption.service.ts index fd5cf057..699296e8 100644 --- a/backend/src/api-oauth/encryption.service.ts +++ b/backend/src/api-oauth/encryption.service.ts @@ -6,14 +6,13 @@ import { createPrivateKey, createPublicKey, privateDecrypt, publicEncrypt } from */ @Injectable() export class EncryptionService { - private readonly privateKey = createPrivateKey(atob(process.env.GROPIUS_LOGIN_SPECIFIC_PRIVATE_KEY)); private readonly publicKey = createPublicKey(atob(process.env.GROPIUS_LOGIN_SPECIFIC_PUBLIC_KEY)); /** * Encrypts the given data using the LOGIN_SPECIFIC public key - * + * * @param data The data to encrypt * @returns The encrypted data */ @@ -23,12 +22,11 @@ export class EncryptionService { /** * Decrypts the given data using the LOGIN_SPECIFIC private key - * + * * @param data The data to decrypt * @returns The decrypted data */ public decrypt(data: string): string { return privateDecrypt(this.privateKey, Buffer.from(data, "base64")).toString(); } - -} \ No newline at end of file +} diff --git a/backend/src/api-oauth/oauth-authorize.controller.ts b/backend/src/api-oauth/oauth-authorize.controller.ts index f9000a7c..42e95718 100644 --- a/backend/src/api-oauth/oauth-authorize.controller.ts +++ b/backend/src/api-oauth/oauth-authorize.controller.ts @@ -58,5 +58,4 @@ export class OauthAuthorizeController { HttpStatus.INTERNAL_SERVER_ERROR, ); } - } diff --git a/backend/src/api-oauth/oauth-token-authorization-code.middleware.ts b/backend/src/api-oauth/oauth-token-authorization-code.middleware.ts index 38df7536..3a9029ff 100644 --- a/backend/src/api-oauth/oauth-token-authorization-code.middleware.ts +++ b/backend/src/api-oauth/oauth-token-authorization-code.middleware.ts @@ -72,7 +72,7 @@ export class OAuthTokenAuthorizationCodeMiddleware extends StateMiddleware< ); throw new OAuthHttpException("invalid_grant", "Given code was liekely reused. Login and codes invalidated"); } - console.log(tokenData) + console.log(tokenData); if (tokenData.codeChallenge != undefined) { if (codeVerifier == undefined) { this.logger.warn("Code verifier missing"); diff --git a/backend/src/api-oauth/oauth-token.controller.ts b/backend/src/api-oauth/oauth-token.controller.ts index 4a9cebfe..f6cf26c5 100644 --- a/backend/src/api-oauth/oauth-token.controller.ts +++ b/backend/src/api-oauth/oauth-token.controller.ts @@ -147,7 +147,7 @@ export class OAuthTokenController { } for (const requestedScope of scope) { if (!currentClient.validScopes.includes(requestedScope)) { - console.log(requestedScope, currentClient.validScopes) + console.log(requestedScope, currentClient.validScopes); throw new OAuthHttpException("invalid_scope", "Requested scope not valid for client"); } } diff --git a/backend/src/api-oauth/oauth-token.middleware.ts b/backend/src/api-oauth/oauth-token.middleware.ts index 34402dcc..68d5ba7e 100644 --- a/backend/src/api-oauth/oauth-token.middleware.ts +++ b/backend/src/api-oauth/oauth-token.middleware.ts @@ -10,9 +10,7 @@ import { StateMiddleware } from "./StateMiddleware"; export class OauthTokenMiddleware extends StateMiddleware<{}, { client: AuthClient }> { private readonly logger = new Logger(OauthTokenMiddleware.name); - constructor( - private readonly authClientService: AuthClientService, - ) { + constructor(private readonly authClientService: AuthClientService) { super(); } diff --git a/backend/src/api-sync/dto/link-ims-users-input.dto.ts b/backend/src/api-sync/dto/link-ims-users-input.dto.ts index ccf9b052..e541e997 100644 --- a/backend/src/api-sync/dto/link-ims-users-input.dto.ts +++ b/backend/src/api-sync/dto/link-ims-users-input.dto.ts @@ -17,4 +17,4 @@ export class LinkImsUsersInputDto { } return input; } -} \ No newline at end of file +} diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 4eccac5e..31a67c98 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -51,7 +51,7 @@ import { ApiOauthModule } from "./api-oauth/api-oauth.module"; }), ServeStaticModule.forRoot({ rootPath: path.join(__dirname, "..", "static"), - serveRoot: "/auth/flow" + serveRoot: "/auth/flow", }), ModelModule, AuthModule, @@ -64,7 +64,7 @@ import { ApiOauthModule } from "./api-oauth/api-oauth.module"; { path: "auth/api/login", module: StrategiesModule }, { path: "auth/api/sync", module: ApiSyncModule }, { path: "auth/api/internal", module: ApiInternalModule }, - { path: "auth/oauth", module: ApiOauthModule} + { path: "auth/oauth", module: ApiOauthModule }, ]), BackendServicesModule, InitializationModule, diff --git a/backend/src/database-migrations/1678970550245-migration.ts b/backend/src/database-migrations/1678970550245-migration.ts index ac6777e4..ad998a1b 100644 --- a/backend/src/database-migrations/1678970550245-migration.ts +++ b/backend/src/database-migrations/1678970550245-migration.ts @@ -1,22 +1,48 @@ import { MigrationInterface, QueryRunner } from "typeorm"; export class migration1678970550245 implements MigrationInterface { - name = 'migration1678970550245' + name = "migration1678970550245"; public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`CREATE TABLE "auth_client" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying, "redirectUrls" json NOT NULL, "clientSecrets" json NOT NULL, "isValid" boolean NOT NULL, "requiresSecret" boolean NOT NULL, CONSTRAINT "PK_2bf40f6fdea0aba0292591d5d7f" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE TABLE "strategy_instance" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying, "instanceConfig" jsonb NOT NULL, "type" character varying NOT NULL, "isLoginActive" boolean NOT NULL, "isSelfRegisterActive" boolean NOT NULL, "isSyncActive" boolean NOT NULL, "doesImplicitRegister" boolean NOT NULL, CONSTRAINT "PK_52c086dc8bf2108d16a07c53bda" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE TABLE "login_user" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "neo4jId" character varying, "username" character varying NOT NULL, "revokeTokensBefore" TIMESTAMP NOT NULL, CONSTRAINT "UQ_cf2a3a2259c6125ba7cfa99f22f" UNIQUE ("neo4jId"), CONSTRAINT "UQ_d81ec461bbcd8cccda1d5b59740" UNIQUE ("username"), CONSTRAINT "PK_6da2fec3d330c1b6c67c427937e" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE TABLE "user_login_data_ims_user" ("neo4jId" character varying NOT NULL, "loginDataId" uuid, CONSTRAINT "PK_0a7615c83d65e2645cce091099e" PRIMARY KEY ("neo4jId"))`); - await queryRunner.query(`CREATE TYPE "public"."user_login_data_state_enum" AS ENUM('WAITING_FOR_REGISTER', 'VALID', 'BLOCKED')`); - await queryRunner.query(`CREATE TABLE "user_login_data" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "data" jsonb NOT NULL, "state" "public"."user_login_data_state_enum" NOT NULL DEFAULT 'VALID', "expires" TIMESTAMP, "userId" uuid, "strategyInstanceId" uuid, CONSTRAINT "PK_39b8a51e24435c604e5134659dc" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE TABLE "active_login" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created" TIMESTAMP NOT NULL, "expires" TIMESTAMP, "isValid" boolean NOT NULL, "supportsSync" boolean NOT NULL, "nextExpectedRefreshTokenNumber" integer NOT NULL DEFAULT '-1', "data" jsonb NOT NULL, "usedStrategyInstnceId" uuid, "loginInstanceForId" uuid, "createdByClientId" uuid, CONSTRAINT "PK_295816e63c0d0d2ddfb6fa9bfea" PRIMARY KEY ("id"))`); - await queryRunner.query(`ALTER TABLE "user_login_data_ims_user" ADD CONSTRAINT "FK_d5f52c4764009dba415d2d2c1b6" FOREIGN KEY ("loginDataId") REFERENCES "user_login_data"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_login_data" ADD CONSTRAINT "FK_7d63048c2ec0094d6e19828fee7" FOREIGN KEY ("userId") REFERENCES "login_user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_login_data" ADD CONSTRAINT "FK_7e3e7be3d11ee9fa33a289cb3d4" FOREIGN KEY ("strategyInstanceId") REFERENCES "strategy_instance"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "active_login" ADD CONSTRAINT "FK_403e359810fad4b6346fdf0db20" FOREIGN KEY ("usedStrategyInstnceId") REFERENCES "strategy_instance"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "active_login" ADD CONSTRAINT "FK_8e31832ec1fd2564d772d87615c" FOREIGN KEY ("loginInstanceForId") REFERENCES "user_login_data"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "active_login" ADD CONSTRAINT "FK_e6358a5261dc7e791810e04394c" FOREIGN KEY ("createdByClientId") REFERENCES "auth_client"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query( + `CREATE TABLE "auth_client" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying, "redirectUrls" json NOT NULL, "clientSecrets" json NOT NULL, "isValid" boolean NOT NULL, "requiresSecret" boolean NOT NULL, CONSTRAINT "PK_2bf40f6fdea0aba0292591d5d7f" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "strategy_instance" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying, "instanceConfig" jsonb NOT NULL, "type" character varying NOT NULL, "isLoginActive" boolean NOT NULL, "isSelfRegisterActive" boolean NOT NULL, "isSyncActive" boolean NOT NULL, "doesImplicitRegister" boolean NOT NULL, CONSTRAINT "PK_52c086dc8bf2108d16a07c53bda" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "login_user" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "neo4jId" character varying, "username" character varying NOT NULL, "revokeTokensBefore" TIMESTAMP NOT NULL, CONSTRAINT "UQ_cf2a3a2259c6125ba7cfa99f22f" UNIQUE ("neo4jId"), CONSTRAINT "UQ_d81ec461bbcd8cccda1d5b59740" UNIQUE ("username"), CONSTRAINT "PK_6da2fec3d330c1b6c67c427937e" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "user_login_data_ims_user" ("neo4jId" character varying NOT NULL, "loginDataId" uuid, CONSTRAINT "PK_0a7615c83d65e2645cce091099e" PRIMARY KEY ("neo4jId"))`, + ); + await queryRunner.query( + `CREATE TYPE "public"."user_login_data_state_enum" AS ENUM('WAITING_FOR_REGISTER', 'VALID', 'BLOCKED')`, + ); + await queryRunner.query( + `CREATE TABLE "user_login_data" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "data" jsonb NOT NULL, "state" "public"."user_login_data_state_enum" NOT NULL DEFAULT 'VALID', "expires" TIMESTAMP, "userId" uuid, "strategyInstanceId" uuid, CONSTRAINT "PK_39b8a51e24435c604e5134659dc" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "active_login" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created" TIMESTAMP NOT NULL, "expires" TIMESTAMP, "isValid" boolean NOT NULL, "supportsSync" boolean NOT NULL, "nextExpectedRefreshTokenNumber" integer NOT NULL DEFAULT '-1', "data" jsonb NOT NULL, "usedStrategyInstnceId" uuid, "loginInstanceForId" uuid, "createdByClientId" uuid, CONSTRAINT "PK_295816e63c0d0d2ddfb6fa9bfea" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `ALTER TABLE "user_login_data_ims_user" ADD CONSTRAINT "FK_d5f52c4764009dba415d2d2c1b6" FOREIGN KEY ("loginDataId") REFERENCES "user_login_data"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "user_login_data" ADD CONSTRAINT "FK_7d63048c2ec0094d6e19828fee7" FOREIGN KEY ("userId") REFERENCES "login_user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "user_login_data" ADD CONSTRAINT "FK_7e3e7be3d11ee9fa33a289cb3d4" FOREIGN KEY ("strategyInstanceId") REFERENCES "strategy_instance"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "active_login" ADD CONSTRAINT "FK_403e359810fad4b6346fdf0db20" FOREIGN KEY ("usedStrategyInstnceId") REFERENCES "strategy_instance"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "active_login" ADD CONSTRAINT "FK_8e31832ec1fd2564d772d87615c" FOREIGN KEY ("loginInstanceForId") REFERENCES "user_login_data"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "active_login" ADD CONSTRAINT "FK_e6358a5261dc7e791810e04394c" FOREIGN KEY ("createdByClientId") REFERENCES "auth_client"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); } public async down(queryRunner: QueryRunner): Promise { @@ -25,7 +51,9 @@ export class migration1678970550245 implements MigrationInterface { await queryRunner.query(`ALTER TABLE "active_login" DROP CONSTRAINT "FK_403e359810fad4b6346fdf0db20"`); await queryRunner.query(`ALTER TABLE "user_login_data" DROP CONSTRAINT "FK_7e3e7be3d11ee9fa33a289cb3d4"`); await queryRunner.query(`ALTER TABLE "user_login_data" DROP CONSTRAINT "FK_7d63048c2ec0094d6e19828fee7"`); - await queryRunner.query(`ALTER TABLE "user_login_data_ims_user" DROP CONSTRAINT "FK_d5f52c4764009dba415d2d2c1b6"`); + await queryRunner.query( + `ALTER TABLE "user_login_data_ims_user" DROP CONSTRAINT "FK_d5f52c4764009dba415d2d2c1b6"`, + ); await queryRunner.query(`DROP TABLE "active_login"`); await queryRunner.query(`DROP TABLE "user_login_data"`); await queryRunner.query(`DROP TYPE "public"."user_login_data_state_enum"`); @@ -34,5 +62,4 @@ export class migration1678970550245 implements MigrationInterface { await queryRunner.query(`DROP TABLE "strategy_instance"`); await queryRunner.query(`DROP TABLE "auth_client"`); } - } diff --git a/backend/src/database-migrations/1720832685920-migration.ts b/backend/src/database-migrations/1720832685920-migration.ts index c24328f7..251dbea7 100644 --- a/backend/src/database-migrations/1720832685920-migration.ts +++ b/backend/src/database-migrations/1720832685920-migration.ts @@ -1,7 +1,7 @@ import { MigrationInterface, QueryRunner } from "typeorm"; export class Migration1720832685920 implements MigrationInterface { - name = 'Migration1720832685920' + name = "Migration1720832685920"; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(`ALTER TABLE "auth_client" ADD "validScopes" json NOT NULL DEFAULT '[]'`); @@ -10,5 +10,4 @@ export class Migration1720832685920 implements MigrationInterface { public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(`ALTER TABLE "auth_client" DROP COLUMN "validScopes"`); } - } diff --git a/backend/src/initialization/check-database-consistency.service.ts b/backend/src/initialization/check-database-consistency.service.ts index d19ebb37..79aa8d23 100644 --- a/backend/src/initialization/check-database-consistency.service.ts +++ b/backend/src/initialization/check-database-consistency.service.ts @@ -62,9 +62,9 @@ export class CheckDatabaseConsistencyService { const nonExistentUser = ( await Promise.all( - ( - await this.loginUserService.find({ select: ["neo4jId"] }) - ).map(async (u) => ((await this.backendUserService.checkUserExists(u)) ? null : u)), + (await this.loginUserService.find({ select: ["neo4jId"] })).map(async (u) => + (await this.backendUserService.checkUserExists(u)) ? null : u, + ), ) ).filter((u) => u !== null); if (nonExistentUser.length > 0) { @@ -146,9 +146,9 @@ export class CheckDatabaseConsistencyService { private async checkUserLoginDataImsUser(fixBroken: boolean): Promise { const nonExistentImsUser = ( await Promise.all( - ( - await this.userLoginDataImsUserService.find({ select: ["neo4jId"] }) - ).map(async (u) => ((await this.imsUserFindingService.checkImsUserExists(u)) ? null : u)), + (await this.userLoginDataImsUserService.find({ select: ["neo4jId"] })).map(async (u) => + (await this.imsUserFindingService.checkImsUserExists(u)) ? null : u, + ), ) ).filter((u) => u !== null); if (nonExistentImsUser.length > 0) { diff --git a/backend/src/main.ts b/backend/src/main.ts index 4219584c..52bba81c 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -58,5 +58,4 @@ async function bootstrap() { app.enableCors(); await app.listen(portNumber); } -bootstrap() - .catch((err) => console.error("NestJS Application exited with error", err)); +bootstrap().catch((err) => console.error("NestJS Application exited with error", err)); diff --git a/backend/src/model/graphql/generated.ts b/backend/src/model/graphql/generated.ts index c4d79c15..0d12e218 100644 --- a/backend/src/model/graphql/generated.ts +++ b/backend/src/model/graphql/generated.ts @@ -1,3877 +1,4387 @@ -import { GraphQLClient, RequestOptions } from 'graphql-request'; -import gql from 'graphql-tag'; +import { GraphQLClient, RequestOptions } from "graphql-request"; +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 }; -type GraphQLClientRequestHeaders = RequestOptions['requestHeaders']; +export type Incremental = T | { [P in keyof T]?: P extends " $fragmentName" | "__typename" ? T[P] : never }; +type GraphQLClientRequestHeaders = RequestOptions["requestHeaders"]; /** 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; } - Duration: { input: any; output: any; } - JSON: { input: any; output: any; } - URL: { input: any; output: any; } + 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 }; + Duration: { input: any; output: any }; + JSON: { input: any; output: any }; + URL: { input: any; output: any }; }; /** Filter used to filter AffectedByIssue */ export type AffectedByIssueFilterInput = { - /** Filter by affectingIssues */ - affectingIssues?: InputMaybe; - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filters for AffectedByIssues which are related to a Trackable */ - relatedTo?: InputMaybe; + /** Filter by affectingIssues */ + affectingIssues?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filters for AffectedByIssues which are related to a Trackable */ + relatedTo?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type AffectedByIssueListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a AffectedByIssue list */ export type AffectedByIssueOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of AffectedByIssue can be sorted by */ export enum AffectedByIssueOrderField { - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME' + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", } /** Filter used to filter AggregatedIssue */ export type AggregatedIssueFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by count */ - count?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by incomingRelations */ - incomingRelations?: InputMaybe; - /** Filter by isOpen */ - isOpen?: InputMaybe; - /** Filter by issues */ - issues?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by outgoingRelations */ - outgoingRelations?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - relationPartner?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - type?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by count */ + count?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by incomingRelations */ + incomingRelations?: InputMaybe; + /** Filter by isOpen */ + isOpen?: InputMaybe; + /** Filter by issues */ + issues?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by outgoingRelations */ + outgoingRelations?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + relationPartner?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + type?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type AggregatedIssueListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a AggregatedIssue list */ export type AggregatedIssueOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of AggregatedIssue can be sorted by */ export enum AggregatedIssueOrderField { - /** Order by count */ - Count = 'COUNT', - /** Order by id */ - Id = 'ID' + /** Order by count */ + Count = "COUNT", + /** Order by id */ + Id = "ID", } /** Filter used to filter AggregatedIssueRelation */ export type AggregatedIssueRelationFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filters for nodes where the related node match this filter */ - end?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by issueRelations */ - issueRelations?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filters for nodes where the related node match this filter */ - start?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - type?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filters for nodes where the related node match this filter */ + end?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by issueRelations */ + issueRelations?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filters for nodes where the related node match this filter */ + start?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + type?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type AggregatedIssueRelationListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a AggregatedIssueRelation list */ export type AggregatedIssueRelationOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of AggregatedIssueRelation can be sorted by */ export enum AggregatedIssueRelationOrderField { - /** Order by id */ - Id = 'ID' + /** Order by id */ + Id = "ID", } /** Non global permission entries */ export enum AllPermissionEntry { - /** - * Allows to add the Component to Projects - * Note: this should be handled very carefully, as adding a Component to a Project gives - * all users with READ access to the Project READ access to the Component - */ - AddToProjects = 'ADD_TO_PROJECTS', - /** Grants all other permissions on the Node except READ. */ - Admin = 'ADMIN', - /** - * Allows affecting entities part of this Trackable with any Issues. - * Affectable entitites include - * - the Trackable itself - * - in case the Trackable is a Component - * - InterfaceSpecifications, their InterfaceSpecificationVersions and their InterfaceParts of the Component (not inherited ones) - * - Interfaces on the Component - * - ComponentVersions of the Component - */ - AffectEntitiesWithIssues = 'AFFECT_ENTITIES_WITH_ISSUES', - /** - * Allows to create Comments on Issues on this Trackable. - * Also allows editing of your own Comments. - */ - Comment = 'COMMENT', - /** - * Allows to create new Issues on the Trackable. - * This includes adding Issues from other Trackables. - */ - CreateIssues = 'CREATE_ISSUES', - /** Allows adding Issues on this Trackable to other Trackables. */ - ExportIssues = 'EXPORT_ISSUES', - /** Allows adding Labels on this Trackable to other Trackables. */ - ExportLabels = 'EXPORT_LABELS', - /** Allows to add, remove, and update Artefacts on this Trackable. */ - ManageArtefacts = 'MANAGE_ARTEFACTS', - /** Allows to add / remove ComponentVersions to / from this Project. */ - ManageComponents = 'MANAGE_COMPONENTS', - /** - * Allows to add, remove, and update IMSProjects on this Trackable. - * Note: for adding, `IMSPermissionEntry.SYNC_TRACKABLES` is required additionally - */ - ManageIms = 'MANAGE_IMS', - /** - * Allows to manage issues. - * This includes `CREATE_ISSUES` and `COMMENT`. - * This does NOT include `LINK_TO_ISSUES` and `LINK_FROM_ISSUES`. - * Additionaly includes - * - change the Template - * - add / remove Labels - * - add / remove Artefacts - * - change any field on the Issue (title, startDate, dueDate, ...) - * - change templated fields - * In contrast to `MODERATOR`, this does not allow editing / removing Comments of other users - */ - ManageIssues = 'MANAGE_ISSUES', - /** - * Allows to add, remove, and update Labels on this Trackable. - * Also allows to delete a Label, but only if it is allowed on all Trackable the Label is on. - */ - ManageLabels = 'MANAGE_LABELS', - /** - * Allows to moderate Issues on this Trackable. - * This allows everything `MANAGE_ISSUES` allows. - * Additionally, it allows editing and deleting Comments of other Users - */ - Moderator = 'MODERATOR', - /** - * Allows to read the Node (obtain it via the API) and to read certain related Nodes. - * See documentation for specific Node for the specific conditions. - */ - Read = 'READ', - /** - * Allows to create Relations with a version of this Component or an Interface of this Component - * as start. - * Note: as these Relations cannot cause new Interfaces on this Component, this can be granted - * more permissively compared to `RELATE_TO_COMPONENT`. - */ - RelateFromComponent = 'RELATE_FROM_COMPONENT', - /** Allows to create IMSProjects with this IMS. */ - SyncTrackables = 'SYNC_TRACKABLES' + /** + * Allows to add the Component to Projects + * Note: this should be handled very carefully, as adding a Component to a Project gives + * all users with READ access to the Project READ access to the Component + */ + AddToProjects = "ADD_TO_PROJECTS", + /** Grants all other permissions on the Node except READ. */ + Admin = "ADMIN", + /** + * Allows affecting entities part of this Trackable with any Issues. + * Affectable entitites include + * - the Trackable itself + * - in case the Trackable is a Component + * - InterfaceSpecifications, their InterfaceSpecificationVersions and their InterfaceParts of the Component (not inherited ones) + * - Interfaces on the Component + * - ComponentVersions of the Component + */ + AffectEntitiesWithIssues = "AFFECT_ENTITIES_WITH_ISSUES", + /** + * Allows to create Comments on Issues on this Trackable. + * Also allows editing of your own Comments. + */ + Comment = "COMMENT", + /** + * Allows to create new Issues on the Trackable. + * This includes adding Issues from other Trackables. + */ + CreateIssues = "CREATE_ISSUES", + /** Allows adding Issues on this Trackable to other Trackables. */ + ExportIssues = "EXPORT_ISSUES", + /** Allows adding Labels on this Trackable to other Trackables. */ + ExportLabels = "EXPORT_LABELS", + /** Allows to add, remove, and update Artefacts on this Trackable. */ + ManageArtefacts = "MANAGE_ARTEFACTS", + /** Allows to add / remove ComponentVersions to / from this Project. */ + ManageComponents = "MANAGE_COMPONENTS", + /** + * Allows to add, remove, and update IMSProjects on this Trackable. + * Note: for adding, `IMSPermissionEntry.SYNC_TRACKABLES` is required additionally + */ + ManageIms = "MANAGE_IMS", + /** + * Allows to manage issues. + * This includes `CREATE_ISSUES` and `COMMENT`. + * This does NOT include `LINK_TO_ISSUES` and `LINK_FROM_ISSUES`. + * Additionaly includes + * - change the Template + * - add / remove Labels + * - add / remove Artefacts + * - change any field on the Issue (title, startDate, dueDate, ...) + * - change templated fields + * In contrast to `MODERATOR`, this does not allow editing / removing Comments of other users + */ + ManageIssues = "MANAGE_ISSUES", + /** + * Allows to add, remove, and update Labels on this Trackable. + * Also allows to delete a Label, but only if it is allowed on all Trackable the Label is on. + */ + ManageLabels = "MANAGE_LABELS", + /** + * Allows to moderate Issues on this Trackable. + * This allows everything `MANAGE_ISSUES` allows. + * Additionally, it allows editing and deleting Comments of other Users + */ + Moderator = "MODERATOR", + /** + * Allows to read the Node (obtain it via the API) and to read certain related Nodes. + * See documentation for specific Node for the specific conditions. + */ + Read = "READ", + /** + * Allows to create Relations with a version of this Component or an Interface of this Component + * as start. + * Note: as these Relations cannot cause new Interfaces on this Component, this can be granted + * more permissively compared to `RELATE_TO_COMPONENT`. + */ + RelateFromComponent = "RELATE_FROM_COMPONENT", + /** Allows to create IMSProjects with this IMS. */ + SyncTrackables = "SYNC_TRACKABLES", } /** Filter used to filter Artefact */ export type ArtefactFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by createdAt */ - createdAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - createdBy?: InputMaybe; - /** Filter by file */ - file?: InputMaybe; - /** Filter by from */ - from?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by issues */ - issues?: InputMaybe; - /** Filter by lastModifiedAt */ - lastModifiedAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - lastModifiedBy?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by referencingComments */ - referencingComments?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - template?: InputMaybe; - /** Filter for templated fields with matching key and values. Entries are joined by AND */ - templatedFields?: InputMaybe>>; - /** Filter by to */ - to?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - trackable?: InputMaybe; - /** Filter by version */ - version?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by createdAt */ + createdAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + createdBy?: InputMaybe; + /** Filter by file */ + file?: InputMaybe; + /** Filter by from */ + from?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by issues */ + issues?: InputMaybe; + /** Filter by lastModifiedAt */ + lastModifiedAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + lastModifiedBy?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by referencingComments */ + referencingComments?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + template?: InputMaybe; + /** Filter for templated fields with matching key and values. Entries are joined by AND */ + templatedFields?: InputMaybe>>; + /** Filter by to */ + to?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + trackable?: InputMaybe; + /** Filter by version */ + version?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type ArtefactListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a Artefact list */ export type ArtefactOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of Artefact can be sorted by */ export enum ArtefactOrderField { - /** Order by createdAt */ - CreatedAt = 'CREATED_AT', - /** Order by file */ - File = 'FILE', - /** Order by from */ - From = 'FROM', - /** Order by id */ - Id = 'ID', - /** Order by lastModifiedAt */ - LastModifiedAt = 'LAST_MODIFIED_AT', - /** Order by to */ - To = 'TO', - /** Order by version */ - Version = 'VERSION' + /** Order by createdAt */ + CreatedAt = "CREATED_AT", + /** Order by file */ + File = "FILE", + /** Order by from */ + From = "FROM", + /** Order by id */ + Id = "ID", + /** Order by lastModifiedAt */ + LastModifiedAt = "LAST_MODIFIED_AT", + /** Order by to */ + To = "TO", + /** Order by version */ + Version = "VERSION", } /** Filter used to filter ArtefactTemplate */ export type ArtefactTemplateFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by extendedBy */ - extendedBy?: InputMaybe; - /** Filter by extends */ - extends?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by isDeprecated */ - isDeprecated?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by extendedBy */ + extendedBy?: InputMaybe; + /** Filter by extends */ + extends?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by isDeprecated */ + isDeprecated?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type ArtefactTemplateListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a ArtefactTemplate list */ export type ArtefactTemplateOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of ArtefactTemplate can be sorted by */ export enum ArtefactTemplateOrderField { - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME' + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", } /** Filter used to filter Assignment */ export type AssignmentFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by createdAt */ - createdAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - createdBy?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - initialType?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - issue?: InputMaybe; - /** Filter by lastModifiedAt */ - lastModifiedAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - lastModifiedBy?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filters for nodes where the related node match this filter */ - parentItem?: InputMaybe; - /** Filter for specific timeline items. Entries are joined by OR */ - timelineItemTypes?: InputMaybe>; - /** Filters for nodes where the related node match this filter */ - type?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - user?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by createdAt */ + createdAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + createdBy?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + initialType?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + issue?: InputMaybe; + /** Filter by lastModifiedAt */ + lastModifiedAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + lastModifiedBy?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filters for nodes where the related node match this filter */ + parentItem?: InputMaybe; + /** Filter for specific timeline items. Entries are joined by OR */ + timelineItemTypes?: InputMaybe>; + /** Filters for nodes where the related node match this filter */ + type?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + user?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type AssignmentListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a Assignment list */ export type AssignmentOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of Assignment can be sorted by */ export enum AssignmentOrderField { - /** Order by createdAt */ - CreatedAt = 'CREATED_AT', - /** Order by id */ - Id = 'ID', - /** Order by lastModifiedAt */ - LastModifiedAt = 'LAST_MODIFIED_AT' + /** Order by createdAt */ + CreatedAt = "CREATED_AT", + /** Order by id */ + Id = "ID", + /** Order by lastModifiedAt */ + LastModifiedAt = "LAST_MODIFIED_AT", } /** Filter used to filter AssignmentType */ export type AssignmentTypeFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by assignmentsWithType */ - assignmentsWithType?: InputMaybe; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by partOf */ - partOf?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by assignmentsWithType */ + assignmentsWithType?: InputMaybe; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by partOf */ + partOf?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type AssignmentTypeListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a AssignmentType list */ export type AssignmentTypeOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of AssignmentType can be sorted by */ export enum AssignmentTypeOrderField { - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME' + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", } /** Filter used to filter AuditedNode */ export type AuditedNodeFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by createdAt */ - createdAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - createdBy?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by lastModifiedAt */ - lastModifiedAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - lastModifiedBy?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by createdAt */ + createdAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + createdBy?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by lastModifiedAt */ + lastModifiedAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + lastModifiedBy?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type AuditedNodeListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a AuditedNode list */ export type AuditedNodeOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of AuditedNode can be sorted by */ export enum AuditedNodeOrderField { - /** Order by createdAt */ - CreatedAt = 'CREATED_AT', - /** Order by id */ - Id = 'ID', - /** Order by lastModifiedAt */ - LastModifiedAt = 'LAST_MODIFIED_AT' + /** Order by createdAt */ + CreatedAt = "CREATED_AT", + /** Order by id */ + Id = "ID", + /** Order by lastModifiedAt */ + LastModifiedAt = "LAST_MODIFIED_AT", } /** Filter used to filter BasePermission */ export type BasePermissionFilterInput = { - /** Filter by allUsers */ - allUsers?: InputMaybe; - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by users */ - users?: InputMaybe; + /** Filter by allUsers */ + allUsers?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by users */ + users?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type BasePermissionListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a BasePermission list */ export type BasePermissionOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of BasePermission can be sorted by */ export enum BasePermissionOrderField { - /** Order by allUsers */ - AllUsers = 'ALL_USERS', - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME' + /** Order by allUsers */ + AllUsers = "ALL_USERS", + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", } /** Filter used to filter Body */ export type BodyFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by answeredBy */ - answeredBy?: InputMaybe; - /** Filter by bodyLastEditedAt */ - bodyLastEditedAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - bodyLastEditedBy?: InputMaybe; - /** Filter by createdAt */ - createdAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - createdBy?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - issue?: InputMaybe; - /** Filter by lastModifiedAt */ - lastModifiedAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - lastModifiedBy?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filters for nodes where the related node match this filter */ - parentItem?: InputMaybe; - /** Filter for specific timeline items. Entries are joined by OR */ - timelineItemTypes?: InputMaybe>; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by answeredBy */ + answeredBy?: InputMaybe; + /** Filter by bodyLastEditedAt */ + bodyLastEditedAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + bodyLastEditedBy?: InputMaybe; + /** Filter by createdAt */ + createdAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + createdBy?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + issue?: InputMaybe; + /** Filter by lastModifiedAt */ + lastModifiedAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + lastModifiedBy?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filters for nodes where the related node match this filter */ + parentItem?: InputMaybe; + /** Filter for specific timeline items. Entries are joined by OR */ + timelineItemTypes?: InputMaybe>; }; /** Filter which can be used to filter for Nodes with a specific Boolean field */ export type BooleanFilterInput = { - /** Matches values which are equal to the provided value */ - eq?: InputMaybe; - /** Matches values which are equal to any of the provided values */ - in?: InputMaybe>; + /** Matches values which are equal to the provided value */ + eq?: InputMaybe; + /** Matches values which are equal to any of the provided values */ + in?: InputMaybe>; }; /** Filter used to filter Comment */ export type CommentFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by answeredBy */ - answeredBy?: InputMaybe; - /** Filter by bodyLastEditedAt */ - bodyLastEditedAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - bodyLastEditedBy?: InputMaybe; - /** Filter by createdAt */ - createdAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - createdBy?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - issue?: InputMaybe; - /** Filter by lastModifiedAt */ - lastModifiedAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - lastModifiedBy?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filters for nodes where the related node match this filter */ - parentItem?: InputMaybe; - /** Filter for specific timeline items. Entries are joined by OR */ - timelineItemTypes?: InputMaybe>; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by answeredBy */ + answeredBy?: InputMaybe; + /** Filter by bodyLastEditedAt */ + bodyLastEditedAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + bodyLastEditedBy?: InputMaybe; + /** Filter by createdAt */ + createdAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + createdBy?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + issue?: InputMaybe; + /** Filter by lastModifiedAt */ + lastModifiedAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + lastModifiedBy?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filters for nodes where the related node match this filter */ + parentItem?: InputMaybe; + /** Filter for specific timeline items. Entries are joined by OR */ + timelineItemTypes?: InputMaybe>; }; /** Filter used to filter Component */ export type ComponentFilterInput = { - /** Filter by affectingIssues */ - affectingIssues?: InputMaybe; - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by artefacts */ - artefacts?: InputMaybe; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by interfaceSpecifications */ - interfaceSpecifications?: InputMaybe; - /** Filter by issues */ - issues?: InputMaybe; - /** Filter by labels */ - labels?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by permissions */ - permissions?: InputMaybe; - /** Filter by pinnedIssues */ - pinnedIssues?: InputMaybe; - /** Filters for AffectedByIssues which are related to a Trackable */ - relatedTo?: InputMaybe; - /** Filter by repositoryURL */ - repositoryURL?: InputMaybe; - /** Filter by syncsTo */ - syncsTo?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - template?: InputMaybe; - /** Filter for templated fields with matching key and values. Entries are joined by AND */ - templatedFields?: InputMaybe>>; - /** Filter by versions */ - versions?: InputMaybe; + /** Filter by affectingIssues */ + affectingIssues?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by artefacts */ + artefacts?: InputMaybe; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by interfaceSpecifications */ + interfaceSpecifications?: InputMaybe; + /** Filter by issues */ + issues?: InputMaybe; + /** Filter by labels */ + labels?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by permissions */ + permissions?: InputMaybe; + /** Filter by pinnedIssues */ + pinnedIssues?: InputMaybe; + /** Filters for AffectedByIssues which are related to a Trackable */ + relatedTo?: InputMaybe; + /** Filter by repositoryURL */ + repositoryURL?: InputMaybe; + /** Filter by syncsTo */ + syncsTo?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + template?: InputMaybe; + /** Filter for templated fields with matching key and values. Entries are joined by AND */ + templatedFields?: InputMaybe>>; + /** Filter by versions */ + versions?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type ComponentListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a Component list */ export type ComponentOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of Component can be sorted by */ export enum ComponentOrderField { - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME', - /** Order by template_id */ - TemplateId = 'TEMPLATE_ID', - /** Order by template_name */ - TemplateName = 'TEMPLATE_NAME' + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", + /** Order by template_id */ + TemplateId = "TEMPLATE_ID", + /** Order by template_name */ + TemplateName = "TEMPLATE_NAME", } /** ComponentPermission entry enum type. */ export enum ComponentPermissionEntry { - /** - * Allows to add the Component to Projects - * Note: this should be handled very carefully, as adding a Component to a Project gives - * all users with READ access to the Project READ access to the Component - */ - AddToProjects = 'ADD_TO_PROJECTS', - /** Grants all other permissions on the Node except READ. */ - Admin = 'ADMIN', - /** - * Allows affecting entities part of this Trackable with any Issues. - * Affectable entitites include - * - the Trackable itself - * - in case the Trackable is a Component - * - InterfaceSpecifications, their InterfaceSpecificationVersions and their InterfaceParts of the Component (not inherited ones) - * - Interfaces on the Component - * - ComponentVersions of the Component - */ - AffectEntitiesWithIssues = 'AFFECT_ENTITIES_WITH_ISSUES', - /** - * Allows to create Comments on Issues on this Trackable. - * Also allows editing of your own Comments. - */ - Comment = 'COMMENT', - /** - * Allows to create new Issues on the Trackable. - * This includes adding Issues from other Trackables. - */ - CreateIssues = 'CREATE_ISSUES', - /** Allows adding Issues on this Trackable to other Trackables. */ - ExportIssues = 'EXPORT_ISSUES', - /** Allows adding Labels on this Trackable to other Trackables. */ - ExportLabels = 'EXPORT_LABELS', - /** Allows to add, remove, and update Artefacts on this Trackable. */ - ManageArtefacts = 'MANAGE_ARTEFACTS', - /** - * Allows to add, remove, and update IMSProjects on this Trackable. - * Note: for adding, `IMSPermissionEntry.SYNC_TRACKABLES` is required additionally - */ - ManageIms = 'MANAGE_IMS', - /** - * Allows to manage issues. - * This includes `CREATE_ISSUES` and `COMMENT`. - * This does NOT include `LINK_TO_ISSUES` and `LINK_FROM_ISSUES`. - * Additionaly includes - * - change the Template - * - add / remove Labels - * - add / remove Artefacts - * - change any field on the Issue (title, startDate, dueDate, ...) - * - change templated fields - * In contrast to `MODERATOR`, this does not allow editing / removing Comments of other users - */ - ManageIssues = 'MANAGE_ISSUES', - /** - * Allows to add, remove, and update Labels on this Trackable. - * Also allows to delete a Label, but only if it is allowed on all Trackable the Label is on. - */ - ManageLabels = 'MANAGE_LABELS', - /** - * Allows to moderate Issues on this Trackable. - * This allows everything `MANAGE_ISSUES` allows. - * Additionally, it allows editing and deleting Comments of other Users - */ - Moderator = 'MODERATOR', - /** - * Allows to read the Node (obtain it via the API) and to read certain related Nodes. - * See documentation for specific Node for the specific conditions. - */ - Read = 'READ', - /** - * Allows to create Relations with a version of this Component or an Interface of this Component - * as start. - * Note: as these Relations cannot cause new Interfaces on this Component, this can be granted - * more permissively compared to `RELATE_TO_COMPONENT`. - */ - RelateFromComponent = 'RELATE_FROM_COMPONENT' + /** + * Allows to add the Component to Projects + * Note: this should be handled very carefully, as adding a Component to a Project gives + * all users with READ access to the Project READ access to the Component + */ + AddToProjects = "ADD_TO_PROJECTS", + /** Grants all other permissions on the Node except READ. */ + Admin = "ADMIN", + /** + * Allows affecting entities part of this Trackable with any Issues. + * Affectable entitites include + * - the Trackable itself + * - in case the Trackable is a Component + * - InterfaceSpecifications, their InterfaceSpecificationVersions and their InterfaceParts of the Component (not inherited ones) + * - Interfaces on the Component + * - ComponentVersions of the Component + */ + AffectEntitiesWithIssues = "AFFECT_ENTITIES_WITH_ISSUES", + /** + * Allows to create Comments on Issues on this Trackable. + * Also allows editing of your own Comments. + */ + Comment = "COMMENT", + /** + * Allows to create new Issues on the Trackable. + * This includes adding Issues from other Trackables. + */ + CreateIssues = "CREATE_ISSUES", + /** Allows adding Issues on this Trackable to other Trackables. */ + ExportIssues = "EXPORT_ISSUES", + /** Allows adding Labels on this Trackable to other Trackables. */ + ExportLabels = "EXPORT_LABELS", + /** Allows to add, remove, and update Artefacts on this Trackable. */ + ManageArtefacts = "MANAGE_ARTEFACTS", + /** + * Allows to add, remove, and update IMSProjects on this Trackable. + * Note: for adding, `IMSPermissionEntry.SYNC_TRACKABLES` is required additionally + */ + ManageIms = "MANAGE_IMS", + /** + * Allows to manage issues. + * This includes `CREATE_ISSUES` and `COMMENT`. + * This does NOT include `LINK_TO_ISSUES` and `LINK_FROM_ISSUES`. + * Additionaly includes + * - change the Template + * - add / remove Labels + * - add / remove Artefacts + * - change any field on the Issue (title, startDate, dueDate, ...) + * - change templated fields + * In contrast to `MODERATOR`, this does not allow editing / removing Comments of other users + */ + ManageIssues = "MANAGE_ISSUES", + /** + * Allows to add, remove, and update Labels on this Trackable. + * Also allows to delete a Label, but only if it is allowed on all Trackable the Label is on. + */ + ManageLabels = "MANAGE_LABELS", + /** + * Allows to moderate Issues on this Trackable. + * This allows everything `MANAGE_ISSUES` allows. + * Additionally, it allows editing and deleting Comments of other Users + */ + Moderator = "MODERATOR", + /** + * Allows to read the Node (obtain it via the API) and to read certain related Nodes. + * See documentation for specific Node for the specific conditions. + */ + Read = "READ", + /** + * Allows to create Relations with a version of this Component or an Interface of this Component + * as start. + * Note: as these Relations cannot cause new Interfaces on this Component, this can be granted + * more permissively compared to `RELATE_TO_COMPONENT`. + */ + RelateFromComponent = "RELATE_FROM_COMPONENT", } /** Filter used to filter ComponentPermission */ export type ComponentPermissionFilterInput = { - /** Filter by allUsers */ - allUsers?: InputMaybe; - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Filter by nodesWithPermission */ - nodesWithPermission?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by users */ - users?: InputMaybe; + /** Filter by allUsers */ + allUsers?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Filter by nodesWithPermission */ + nodesWithPermission?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by users */ + users?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type ComponentPermissionListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a ComponentPermission list */ export type ComponentPermissionOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of ComponentPermission can be sorted by */ export enum ComponentPermissionOrderField { - /** Order by allUsers */ - AllUsers = 'ALL_USERS', - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME' + /** Order by allUsers */ + AllUsers = "ALL_USERS", + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", } /** Filter used to filter ComponentTemplate */ export type ComponentTemplateFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by extendedBy */ - extendedBy?: InputMaybe; - /** Filter by extends */ - extends?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by isDeprecated */ - isDeprecated?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by possibleEndOfRelations */ - possibleEndOfRelations?: InputMaybe; - /** Filter by possibleInvisibleInterfaceSpecifications */ - possibleInvisibleInterfaceSpecifications?: InputMaybe; - /** Filter by possibleStartOfRelations */ - possibleStartOfRelations?: InputMaybe; - /** Filter by possibleVisibleInterfaceSpecifications */ - possibleVisibleInterfaceSpecifications?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by extendedBy */ + extendedBy?: InputMaybe; + /** Filter by extends */ + extends?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by isDeprecated */ + isDeprecated?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by possibleEndOfRelations */ + possibleEndOfRelations?: InputMaybe; + /** Filter by possibleInvisibleInterfaceSpecifications */ + possibleInvisibleInterfaceSpecifications?: InputMaybe; + /** Filter by possibleStartOfRelations */ + possibleStartOfRelations?: InputMaybe; + /** Filter by possibleVisibleInterfaceSpecifications */ + possibleVisibleInterfaceSpecifications?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type ComponentTemplateListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a ComponentTemplate list */ export type ComponentTemplateOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of ComponentTemplate can be sorted by */ export enum ComponentTemplateOrderField { - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME' + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", } /** Filter used to filter ComponentVersion */ export type ComponentVersionFilterInput = { - /** Filter by affectingIssues */ - affectingIssues?: InputMaybe; - /** Filter by aggregatedIssues */ - aggregatedIssues?: InputMaybe; - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filters for nodes where the related node match this filter */ - component?: InputMaybe; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by includingProjects */ - includingProjects?: InputMaybe; - /** Filter by incomingRelations */ - incomingRelations?: InputMaybe; - /** Filter by interfaceDefinitions */ - interfaceDefinitions?: InputMaybe; - /** Filter by intraComponentDependencySpecifications */ - intraComponentDependencySpecifications?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by outgoingRelations */ - outgoingRelations?: InputMaybe; - /** Filters for RelationPartners which are part of a Project's component graph */ - partOfProject?: InputMaybe; - /** Filters for AffectedByIssues which are related to a Trackable */ - relatedTo?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - template?: InputMaybe; - /** Filter for templated fields with matching key and values. Entries are joined by AND */ - templatedFields?: InputMaybe>>; - /** Filter by version */ - version?: InputMaybe; + /** Filter by affectingIssues */ + affectingIssues?: InputMaybe; + /** Filter by aggregatedIssues */ + aggregatedIssues?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filters for nodes where the related node match this filter */ + component?: InputMaybe; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by includingProjects */ + includingProjects?: InputMaybe; + /** Filter by incomingRelations */ + incomingRelations?: InputMaybe; + /** Filter by interfaceDefinitions */ + interfaceDefinitions?: InputMaybe; + /** Filter by intraComponentDependencySpecifications */ + intraComponentDependencySpecifications?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by outgoingRelations */ + outgoingRelations?: InputMaybe; + /** Filters for RelationPartners which are part of a Project's component graph */ + partOfProject?: InputMaybe; + /** Filters for AffectedByIssues which are related to a Trackable */ + relatedTo?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + template?: InputMaybe; + /** Filter for templated fields with matching key and values. Entries are joined by AND */ + templatedFields?: InputMaybe>>; + /** Filter by version */ + version?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type ComponentVersionListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a ComponentVersion list */ export type ComponentVersionOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of ComponentVersion can be sorted by */ export enum ComponentVersionOrderField { - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME', - /** Order by template_id */ - TemplateId = 'TEMPLATE_ID', - /** Order by template_name */ - TemplateName = 'TEMPLATE_NAME', - /** Order by version */ - Version = 'VERSION' + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", + /** Order by template_id */ + TemplateId = "TEMPLATE_ID", + /** Order by template_name */ + TemplateName = "TEMPLATE_NAME", + /** Order by version */ + Version = "VERSION", } /** Filter used to filter ComponentVersionTemplate */ export type ComponentVersionTemplateFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; }; /** Input for the createGropiusUser mutation */ export type CreateGropiusUserInput = { - /** The avatar of the created GropiusUser */ - avatar?: InputMaybe; - /** The displayName of the created User */ - displayName: Scalars['String']['input']; - /** The email of the created User if present */ - email?: InputMaybe; - /** If true, the created GropiusUser is a global admin */ - isAdmin: Scalars['Boolean']['input']; - /** The username of the created GropiusUser, must be unique, must match /^[a-zA-Z0-9_-]+$/ */ - username: Scalars['String']['input']; + /** The avatar of the created GropiusUser */ + avatar?: InputMaybe; + /** The displayName of the created User */ + displayName: Scalars["String"]["input"]; + /** The email of the created User if present */ + email?: InputMaybe; + /** If true, the created GropiusUser is a global admin */ + isAdmin: Scalars["Boolean"]["input"]; + /** The username of the created GropiusUser, must be unique, must match /^[a-zA-Z0-9_-]+$/ */ + username: Scalars["String"]["input"]; }; /** Input for the createIMSUser mutation */ export type CreateImsUserInput = { - /** The displayName of the created User */ - displayName: Scalars['String']['input']; - /** The email of the created User if present */ - email?: InputMaybe; - /** If present, the id of the GropiusUser the created IMSUser is associated with */ - gropiusUser?: InputMaybe; - /** The id of the IMS the created IMSUser is part of */ - ims: Scalars['ID']['input']; - /** Initial values for all templatedFields */ - templatedFields: Array; - /** The username of the created IMSUser, must be unique */ - username?: InputMaybe; + /** The displayName of the created User */ + displayName: Scalars["String"]["input"]; + /** The email of the created User if present */ + email?: InputMaybe; + /** If present, the id of the GropiusUser the created IMSUser is associated with */ + gropiusUser?: InputMaybe; + /** The id of the IMS the created IMSUser is part of */ + ims: Scalars["ID"]["input"]; + /** Initial values for all templatedFields */ + templatedFields: Array; + /** The username of the created IMSUser, must be unique */ + username?: InputMaybe; }; /** Filter which can be used to filter for Nodes with a specific DateTime field */ export type DateTimeFilterInput = { - /** Matches values which are equal to the provided value */ - eq?: InputMaybe; - /** Matches values which are greater than the provided value */ - gt?: InputMaybe; - /** Matches values which are greater than or equal to the provided value */ - gte?: InputMaybe; - /** Matches values which are equal to any of the provided values */ - in?: InputMaybe>; - /** Matches values which are lesser than the provided value */ - lt?: InputMaybe; - /** Matches values which are lesser than or equal to the provided value */ - lte?: InputMaybe; + /** Matches values which are equal to the provided value */ + eq?: InputMaybe; + /** Matches values which are greater than the provided value */ + gt?: InputMaybe; + /** Matches values which are greater than or equal to the provided value */ + gte?: InputMaybe; + /** Matches values which are equal to any of the provided values */ + in?: InputMaybe>; + /** Matches values which are lesser than the provided value */ + lt?: InputMaybe; + /** Matches values which are lesser than or equal to the provided value */ + lte?: InputMaybe; }; /** Filter which can be used to filter for Nodes with a specific Float field */ export type FloatFilterInput = { - /** Matches values which are equal to the provided value */ - eq?: InputMaybe; - /** Matches values which are greater than the provided value */ - gt?: InputMaybe; - /** Matches values which are greater than or equal to the provided value */ - gte?: InputMaybe; - /** Matches values which are equal to any of the provided values */ - in?: InputMaybe>; - /** Matches values which are lesser than the provided value */ - lt?: InputMaybe; - /** Matches values which are lesser than or equal to the provided value */ - lte?: InputMaybe; + /** Matches values which are equal to the provided value */ + eq?: InputMaybe; + /** Matches values which are greater than the provided value */ + gt?: InputMaybe; + /** Matches values which are greater than or equal to the provided value */ + gte?: InputMaybe; + /** Matches values which are equal to any of the provided values */ + in?: InputMaybe>; + /** Matches values which are lesser than the provided value */ + lt?: InputMaybe; + /** Matches values which are lesser than or equal to the provided value */ + lte?: InputMaybe; }; /** Filter used to filter GlobalPermission */ export type GlobalPermissionFilterInput = { - /** Filter by allUsers */ - allUsers?: InputMaybe; - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by users */ - users?: InputMaybe; + /** Filter by allUsers */ + allUsers?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by users */ + users?: InputMaybe; }; /** Defines the order of a GlobalPermission list */ export type GlobalPermissionOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of GlobalPermission can be sorted by */ export enum GlobalPermissionOrderField { - /** Order by allUsers */ - AllUsers = 'ALL_USERS', - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME' + /** Order by allUsers */ + AllUsers = "ALL_USERS", + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", } /** Filter used to filter GropiusUser */ export type GropiusUserFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by assignments */ - assignments?: InputMaybe; - /** Filter by canSyncOthers */ - canSyncOthers?: InputMaybe; - /** Filter by canSyncSelf */ - canSyncSelf?: InputMaybe; - /** Filter by createdNodes */ - createdNodes?: InputMaybe; - /** Filter by displayName */ - displayName?: InputMaybe; - /** Filter by email */ - email?: InputMaybe; - /** Filter for users with a specific permission on a node */ - hasNodePermission?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by imsUsers */ - imsUsers?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by participatedIssues */ - participatedIssues?: InputMaybe; - /** Filter by permissions */ - permissions?: InputMaybe; - /** Filter by username */ - username?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by assignments */ + assignments?: InputMaybe; + /** Filter by canSyncOthers */ + canSyncOthers?: InputMaybe; + /** Filter by canSyncSelf */ + canSyncSelf?: InputMaybe; + /** Filter by createdNodes */ + createdNodes?: InputMaybe; + /** Filter by displayName */ + displayName?: InputMaybe; + /** Filter by email */ + email?: InputMaybe; + /** Filter for users with a specific permission on a node */ + hasNodePermission?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by imsUsers */ + imsUsers?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by participatedIssues */ + participatedIssues?: InputMaybe; + /** Filter by permissions */ + permissions?: InputMaybe; + /** Filter by username */ + username?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type GropiusUserListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a GropiusUser list */ export type GropiusUserOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of GropiusUser can be sorted by */ export enum GropiusUserOrderField { - /** Order by displayName */ - DisplayName = 'DISPLAY_NAME', - /** Order by email */ - Email = 'EMAIL', - /** Order by id */ - Id = 'ID', - /** Order by username */ - Username = 'USERNAME' + /** Order by displayName */ + DisplayName = "DISPLAY_NAME", + /** Order by email */ + Email = "EMAIL", + /** Order by id */ + Id = "ID", + /** Order by username */ + Username = "USERNAME", } /** Filter which can be used to filter for Nodes with a specific ID field */ export type IdFilterInput = { - /** Matches values which are equal to the provided value */ - eq?: InputMaybe; - /** Matches values which are equal to any of the provided values */ - in?: InputMaybe>; + /** Matches values which are equal to the provided value */ + eq?: InputMaybe; + /** Matches values which are equal to any of the provided values */ + in?: InputMaybe>; }; /** Filter used to filter IMS */ export type ImsFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by permissions */ - permissions?: InputMaybe; - /** Filter by projects */ - projects?: InputMaybe; - /** Filter by syncOthersAllowedBy */ - syncOthersAllowedBy?: InputMaybe; - /** Filter by syncSelfAllowedBy */ - syncSelfAllowedBy?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - template?: InputMaybe; - /** Filter for templated fields with matching key and values. Entries are joined by AND */ - templatedFields?: InputMaybe>>; - /** Filter by users */ - users?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by permissions */ + permissions?: InputMaybe; + /** Filter by projects */ + projects?: InputMaybe; + /** Filter by syncOthersAllowedBy */ + syncOthersAllowedBy?: InputMaybe; + /** Filter by syncSelfAllowedBy */ + syncSelfAllowedBy?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + template?: InputMaybe; + /** Filter for templated fields with matching key and values. Entries are joined by AND */ + templatedFields?: InputMaybe>>; + /** Filter by users */ + users?: InputMaybe; }; /** Filter used to filter IMSIssue */ export type ImsIssueFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by id */ - id?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - imsProject?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - issue?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filters for nodes where the related node match this filter */ - template?: InputMaybe; - /** Filter for templated fields with matching key and values. Entries are joined by AND */ - templatedFields?: InputMaybe>>; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by id */ + id?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + imsProject?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + issue?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filters for nodes where the related node match this filter */ + template?: InputMaybe; + /** Filter for templated fields with matching key and values. Entries are joined by AND */ + templatedFields?: InputMaybe>>; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type ImsIssueListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a IMSIssue list */ export type ImsIssueOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of IMSIssue can be sorted by */ export enum ImsIssueOrderField { - /** Order by id */ - Id = 'ID', - /** Order by template_id */ - TemplateId = 'TEMPLATE_ID', - /** Order by template_name */ - TemplateName = 'TEMPLATE_NAME' + /** Order by id */ + Id = "ID", + /** Order by template_id */ + TemplateId = "TEMPLATE_ID", + /** Order by template_name */ + TemplateName = "TEMPLATE_NAME", } /** Filter used to filter IMSIssueTemplate */ export type ImsIssueTemplateFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type ImsListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a IMS list */ export type ImsOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of IMS can be sorted by */ export enum ImsOrderField { - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME', - /** Order by template_id */ - TemplateId = 'TEMPLATE_ID', - /** Order by template_name */ - TemplateName = 'TEMPLATE_NAME' + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", + /** Order by template_id */ + TemplateId = "TEMPLATE_ID", + /** Order by template_name */ + TemplateName = "TEMPLATE_NAME", } /** IMSPermission entry enum type. */ export enum ImsPermissionEntry { - /** Grants all other permissions on the Node except READ. */ - Admin = 'ADMIN', - /** - * Allows to read the Node (obtain it via the API) and to read certain related Nodes. - * See documentation for specific Node for the specific conditions. - */ - Read = 'READ', - /** Allows to create IMSProjects with this IMS. */ - SyncTrackables = 'SYNC_TRACKABLES' + /** Grants all other permissions on the Node except READ. */ + Admin = "ADMIN", + /** + * Allows to read the Node (obtain it via the API) and to read certain related Nodes. + * See documentation for specific Node for the specific conditions. + */ + Read = "READ", + /** Allows to create IMSProjects with this IMS. */ + SyncTrackables = "SYNC_TRACKABLES", } /** Filter used to filter IMSPermission */ export type ImsPermissionFilterInput = { - /** Filter by allUsers */ - allUsers?: InputMaybe; - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Filter by nodesWithPermission */ - nodesWithPermission?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by users */ - users?: InputMaybe; + /** Filter by allUsers */ + allUsers?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Filter by nodesWithPermission */ + nodesWithPermission?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by users */ + users?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type ImsPermissionListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a IMSPermission list */ export type ImsPermissionOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of IMSPermission can be sorted by */ export enum ImsPermissionOrderField { - /** Order by allUsers */ - AllUsers = 'ALL_USERS', - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME' + /** Order by allUsers */ + AllUsers = "ALL_USERS", + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", } /** Filter used to filter IMSProject */ export type ImsProjectFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - ims?: InputMaybe; - /** Filter by imsIssues */ - imsIssues?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by syncOthersAllowedBy */ - syncOthersAllowedBy?: InputMaybe; - /** Filter by syncSelfAllowedBy */ - syncSelfAllowedBy?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - template?: InputMaybe; - /** Filter for templated fields with matching key and values. Entries are joined by AND */ - templatedFields?: InputMaybe>>; - /** Filters for nodes where the related node match this filter */ - trackable?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + ims?: InputMaybe; + /** Filter by imsIssues */ + imsIssues?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by syncOthersAllowedBy */ + syncOthersAllowedBy?: InputMaybe; + /** Filter by syncSelfAllowedBy */ + syncSelfAllowedBy?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + template?: InputMaybe; + /** Filter for templated fields with matching key and values. Entries are joined by AND */ + templatedFields?: InputMaybe>>; + /** Filters for nodes where the related node match this filter */ + trackable?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type ImsProjectListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a IMSProject list */ export type ImsProjectOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of IMSProject can be sorted by */ export enum ImsProjectOrderField { - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME', - /** Order by template_id */ - TemplateId = 'TEMPLATE_ID', - /** Order by template_name */ - TemplateName = 'TEMPLATE_NAME' + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", + /** Order by template_id */ + TemplateId = "TEMPLATE_ID", + /** Order by template_name */ + TemplateName = "TEMPLATE_NAME", } /** Filter used to filter IMSProjectTemplate */ export type ImsProjectTemplateFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; }; /** Filter used to filter IMSTemplate */ export type ImsTemplateFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by extendedBy */ - extendedBy?: InputMaybe; - /** Filter by extends */ - extends?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by isDeprecated */ - isDeprecated?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by extendedBy */ + extendedBy?: InputMaybe; + /** Filter by extends */ + extends?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by isDeprecated */ + isDeprecated?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type ImsTemplateListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a IMSTemplate list */ export type ImsTemplateOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of IMSTemplate can be sorted by */ export enum ImsTemplateOrderField { - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME' + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", } /** Filter used to filter IMSUser */ export type ImsUserFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by assignments */ - assignments?: InputMaybe; - /** Filter by createdNodes */ - createdNodes?: InputMaybe; - /** Filter by displayName */ - displayName?: InputMaybe; - /** Filter by email */ - email?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - gropiusUser?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - ims?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by participatedIssues */ - participatedIssues?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - template?: InputMaybe; - /** Filter for templated fields with matching key and values. Entries are joined by AND */ - templatedFields?: InputMaybe>>; - /** Filter by username */ - username?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by assignments */ + assignments?: InputMaybe; + /** Filter by createdNodes */ + createdNodes?: InputMaybe; + /** Filter by displayName */ + displayName?: InputMaybe; + /** Filter by email */ + email?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + gropiusUser?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + ims?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by participatedIssues */ + participatedIssues?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + template?: InputMaybe; + /** Filter for templated fields with matching key and values. Entries are joined by AND */ + templatedFields?: InputMaybe>>; + /** Filter by username */ + username?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type ImsUserListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a IMSUser list */ export type ImsUserOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of IMSUser can be sorted by */ export enum ImsUserOrderField { - /** Order by displayName */ - DisplayName = 'DISPLAY_NAME', - /** Order by email */ - Email = 'EMAIL', - /** Order by id */ - Id = 'ID', - /** Order by username */ - Username = 'USERNAME' + /** Order by displayName */ + DisplayName = "DISPLAY_NAME", + /** Order by email */ + Email = "EMAIL", + /** Order by id */ + Id = "ID", + /** Order by username */ + Username = "USERNAME", } /** Filter used to filter IMSUserTemplate */ export type ImsUserTemplateFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; }; /** Filter which can be used to filter for Nodes with a specific Int field */ export type IntFilterInput = { - /** Matches values which are equal to the provided value */ - eq?: InputMaybe; - /** Matches values which are greater than the provided value */ - gt?: InputMaybe; - /** Matches values which are greater than or equal to the provided value */ - gte?: InputMaybe; - /** Matches values which are equal to any of the provided values */ - in?: InputMaybe>; - /** Matches values which are lesser than the provided value */ - lt?: InputMaybe; - /** Matches values which are lesser than or equal to the provided value */ - lte?: InputMaybe; + /** Matches values which are equal to the provided value */ + eq?: InputMaybe; + /** Matches values which are greater than the provided value */ + gt?: InputMaybe; + /** Matches values which are greater than or equal to the provided value */ + gte?: InputMaybe; + /** Matches values which are equal to any of the provided values */ + in?: InputMaybe>; + /** Matches values which are lesser than the provided value */ + lt?: InputMaybe; + /** Matches values which are lesser than or equal to the provided value */ + lte?: InputMaybe; }; /** Filter used to filter InterfaceDefinition */ export type InterfaceDefinitionFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filters for nodes where the related node match this filter */ - componentVersion?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - interfaceSpecificationVersion?: InputMaybe; - /** Filter by invisibleDerivedBy */ - invisibleDerivedBy?: InputMaybe; - /** Filter by invisibleSelfDefined */ - invisibleSelfDefined?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filters for nodes where the related node match this filter */ - template?: InputMaybe; - /** Filter for templated fields with matching key and values. Entries are joined by AND */ - templatedFields?: InputMaybe>>; - /** Filter by visibleDerivedBy */ - visibleDerivedBy?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - visibleInterface?: InputMaybe; - /** Filter by visibleSelfDefined */ - visibleSelfDefined?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filters for nodes where the related node match this filter */ + componentVersion?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + interfaceSpecificationVersion?: InputMaybe; + /** Filter by invisibleDerivedBy */ + invisibleDerivedBy?: InputMaybe; + /** Filter by invisibleSelfDefined */ + invisibleSelfDefined?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filters for nodes where the related node match this filter */ + template?: InputMaybe; + /** Filter for templated fields with matching key and values. Entries are joined by AND */ + templatedFields?: InputMaybe>>; + /** Filter by visibleDerivedBy */ + visibleDerivedBy?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + visibleInterface?: InputMaybe; + /** Filter by visibleSelfDefined */ + visibleSelfDefined?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type InterfaceDefinitionListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a InterfaceDefinition list */ export type InterfaceDefinitionOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of InterfaceDefinition can be sorted by */ export enum InterfaceDefinitionOrderField { - /** Order by id */ - Id = 'ID', - /** Order by invisibleSelfDefined */ - InvisibleSelfDefined = 'INVISIBLE_SELF_DEFINED', - /** Order by template_id */ - TemplateId = 'TEMPLATE_ID', - /** Order by template_name */ - TemplateName = 'TEMPLATE_NAME', - /** Order by visibleSelfDefined */ - VisibleSelfDefined = 'VISIBLE_SELF_DEFINED' + /** Order by id */ + Id = "ID", + /** Order by invisibleSelfDefined */ + InvisibleSelfDefined = "INVISIBLE_SELF_DEFINED", + /** Order by template_id */ + TemplateId = "TEMPLATE_ID", + /** Order by template_name */ + TemplateName = "TEMPLATE_NAME", + /** Order by visibleSelfDefined */ + VisibleSelfDefined = "VISIBLE_SELF_DEFINED", } /** Filter used to filter InterfaceDefinitionTemplate */ export type InterfaceDefinitionTemplateFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; }; /** Filter used to filter Interface */ export type InterfaceFilterInput = { - /** Filter by affectingIssues */ - affectingIssues?: InputMaybe; - /** Filter by aggregatedIssues */ - aggregatedIssues?: InputMaybe; - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by incomingRelations */ - incomingRelations?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - interfaceDefinition?: InputMaybe; - /** Filter by intraComponentDependencyParticipants */ - intraComponentDependencyParticipants?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by outgoingRelations */ - outgoingRelations?: InputMaybe; - /** Filters for RelationPartners which are part of a Project's component graph */ - partOfProject?: InputMaybe; - /** Filters for AffectedByIssues which are related to a Trackable */ - relatedTo?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - template?: InputMaybe; - /** Filter for templated fields with matching key and values. Entries are joined by AND */ - templatedFields?: InputMaybe>>; + /** Filter by affectingIssues */ + affectingIssues?: InputMaybe; + /** Filter by aggregatedIssues */ + aggregatedIssues?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by incomingRelations */ + incomingRelations?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + interfaceDefinition?: InputMaybe; + /** Filter by intraComponentDependencyParticipants */ + intraComponentDependencyParticipants?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by outgoingRelations */ + outgoingRelations?: InputMaybe; + /** Filters for RelationPartners which are part of a Project's component graph */ + partOfProject?: InputMaybe; + /** Filters for AffectedByIssues which are related to a Trackable */ + relatedTo?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + template?: InputMaybe; + /** Filter for templated fields with matching key and values. Entries are joined by AND */ + templatedFields?: InputMaybe>>; }; /** Defines the order of a Interface list */ export type InterfaceOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of Interface can be sorted by */ export enum InterfaceOrderField { - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME', - /** Order by template_id */ - TemplateId = 'TEMPLATE_ID', - /** Order by template_name */ - TemplateName = 'TEMPLATE_NAME' + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", + /** Order by template_id */ + TemplateId = "TEMPLATE_ID", + /** Order by template_name */ + TemplateName = "TEMPLATE_NAME", } /** Filter used to filter InterfacePart */ export type InterfacePartFilterInput = { - /** Filter by affectingIssues */ - affectingIssues?: InputMaybe; - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by includingIncomingRelations */ - includingIncomingRelations?: InputMaybe; - /** Filter by includingIntraComponentDependencyParticipants */ - includingIntraComponentDependencyParticipants?: InputMaybe; - /** Filter by includingOutgoingRelations */ - includingOutgoingRelations?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filters for nodes where the related node match this filter */ - partOf?: InputMaybe; - /** Filters for AffectedByIssues which are related to a Trackable */ - relatedTo?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - template?: InputMaybe; - /** Filter for templated fields with matching key and values. Entries are joined by AND */ - templatedFields?: InputMaybe>>; + /** Filter by affectingIssues */ + affectingIssues?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by includingIncomingRelations */ + includingIncomingRelations?: InputMaybe; + /** Filter by includingIntraComponentDependencyParticipants */ + includingIntraComponentDependencyParticipants?: InputMaybe; + /** Filter by includingOutgoingRelations */ + includingOutgoingRelations?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filters for nodes where the related node match this filter */ + partOf?: InputMaybe; + /** Filters for AffectedByIssues which are related to a Trackable */ + relatedTo?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + template?: InputMaybe; + /** Filter for templated fields with matching key and values. Entries are joined by AND */ + templatedFields?: InputMaybe>>; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type InterfacePartListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a InterfacePart list */ export type InterfacePartOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of InterfacePart can be sorted by */ export enum InterfacePartOrderField { - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME', - /** Order by template_id */ - TemplateId = 'TEMPLATE_ID', - /** Order by template_name */ - TemplateName = 'TEMPLATE_NAME' + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", + /** Order by template_id */ + TemplateId = "TEMPLATE_ID", + /** Order by template_name */ + TemplateName = "TEMPLATE_NAME", } /** Filter used to filter InterfacePartTemplate */ export type InterfacePartTemplateFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; }; /** Filter used to filter InterfaceSpecificationDerivationCondition */ export type InterfaceSpecificationDerivationConditionFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by derivableInterfaceSpecifications */ - derivableInterfaceSpecifications?: InputMaybe; - /** Filter by derivesInvisibleDerived */ - derivesInvisibleDerived?: InputMaybe; - /** Filter by derivesInvisibleSelfDefined */ - derivesInvisibleSelfDefined?: InputMaybe; - /** Filter by derivesVisibleDerived */ - derivesVisibleDerived?: InputMaybe; - /** Filter by derivesVisibleSelfDefined */ - derivesVisibleSelfDefined?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by isInvisibleDerived */ - isInvisibleDerived?: InputMaybe; - /** Filter by isVisibleDerived */ - isVisibleDerived?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filters for nodes where the related node match this filter */ - partOf?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by derivableInterfaceSpecifications */ + derivableInterfaceSpecifications?: InputMaybe; + /** Filter by derivesInvisibleDerived */ + derivesInvisibleDerived?: InputMaybe; + /** Filter by derivesInvisibleSelfDefined */ + derivesInvisibleSelfDefined?: InputMaybe; + /** Filter by derivesVisibleDerived */ + derivesVisibleDerived?: InputMaybe; + /** Filter by derivesVisibleSelfDefined */ + derivesVisibleSelfDefined?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by isInvisibleDerived */ + isInvisibleDerived?: InputMaybe; + /** Filter by isVisibleDerived */ + isVisibleDerived?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filters for nodes where the related node match this filter */ + partOf?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type InterfaceSpecificationDerivationConditionListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a InterfaceSpecificationDerivationCondition list */ export type InterfaceSpecificationDerivationConditionOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of InterfaceSpecificationDerivationCondition can be sorted by */ export enum InterfaceSpecificationDerivationConditionOrderField { - /** Order by id */ - Id = 'ID' + /** Order by id */ + Id = "ID", } /** Filter used to filter InterfaceSpecification */ export type InterfaceSpecificationFilterInput = { - /** Filter by affectingIssues */ - affectingIssues?: InputMaybe; - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filters for nodes where the related node match this filter */ - component?: InputMaybe; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filters for AffectedByIssues which are related to a Trackable */ - relatedTo?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - template?: InputMaybe; - /** Filter for templated fields with matching key and values. Entries are joined by AND */ - templatedFields?: InputMaybe>>; - /** Filter by versions */ - versions?: InputMaybe; + /** Filter by affectingIssues */ + affectingIssues?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filters for nodes where the related node match this filter */ + component?: InputMaybe; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filters for AffectedByIssues which are related to a Trackable */ + relatedTo?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + template?: InputMaybe; + /** Filter for templated fields with matching key and values. Entries are joined by AND */ + templatedFields?: InputMaybe>>; + /** Filter by versions */ + versions?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type InterfaceSpecificationListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a InterfaceSpecification list */ export type InterfaceSpecificationOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of InterfaceSpecification can be sorted by */ export enum InterfaceSpecificationOrderField { - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME', - /** Order by template_id */ - TemplateId = 'TEMPLATE_ID', - /** Order by template_name */ - TemplateName = 'TEMPLATE_NAME' + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", + /** Order by template_id */ + TemplateId = "TEMPLATE_ID", + /** Order by template_name */ + TemplateName = "TEMPLATE_NAME", } /** Filter used to filter InterfaceSpecificationTemplate */ export type InterfaceSpecificationTemplateFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by canBeInvisibleOnComponents */ - canBeInvisibleOnComponents?: InputMaybe; - /** Filter by canBeVisibleOnComponents */ - canBeVisibleOnComponents?: InputMaybe; - /** Filter by derivableBy */ - derivableBy?: InputMaybe; - /** Filter by description */ - description?: InputMaybe; - /** Filter by extendedBy */ - extendedBy?: InputMaybe; - /** Filter by extends */ - extends?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by isDeprecated */ - isDeprecated?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by possibleEndOfRelations */ - possibleEndOfRelations?: InputMaybe; - /** Filter by possibleStartOfRelations */ - possibleStartOfRelations?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by canBeInvisibleOnComponents */ + canBeInvisibleOnComponents?: InputMaybe; + /** Filter by canBeVisibleOnComponents */ + canBeVisibleOnComponents?: InputMaybe; + /** Filter by derivableBy */ + derivableBy?: InputMaybe; + /** Filter by description */ + description?: InputMaybe; + /** Filter by extendedBy */ + extendedBy?: InputMaybe; + /** Filter by extends */ + extends?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by isDeprecated */ + isDeprecated?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by possibleEndOfRelations */ + possibleEndOfRelations?: InputMaybe; + /** Filter by possibleStartOfRelations */ + possibleStartOfRelations?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type InterfaceSpecificationTemplateListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a InterfaceSpecificationTemplate list */ export type InterfaceSpecificationTemplateOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of InterfaceSpecificationTemplate can be sorted by */ export enum InterfaceSpecificationTemplateOrderField { - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME' + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", } /** Filter used to filter InterfaceSpecificationVersion */ export type InterfaceSpecificationVersionFilterInput = { - /** Filter by affectingIssues */ - affectingIssues?: InputMaybe; - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by interfaceDefinitions */ - interfaceDefinitions?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - interfaceSpecification?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by parts */ - parts?: InputMaybe; - /** Filters for AffectedByIssues which are related to a Trackable */ - relatedTo?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - template?: InputMaybe; - /** Filter for templated fields with matching key and values. Entries are joined by AND */ - templatedFields?: InputMaybe>>; - /** Filter by version */ - version?: InputMaybe; + /** Filter by affectingIssues */ + affectingIssues?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by interfaceDefinitions */ + interfaceDefinitions?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + interfaceSpecification?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by parts */ + parts?: InputMaybe; + /** Filters for AffectedByIssues which are related to a Trackable */ + relatedTo?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + template?: InputMaybe; + /** Filter for templated fields with matching key and values. Entries are joined by AND */ + templatedFields?: InputMaybe>>; + /** Filter by version */ + version?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type InterfaceSpecificationVersionListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a InterfaceSpecificationVersion list */ export type InterfaceSpecificationVersionOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of InterfaceSpecificationVersion can be sorted by */ export enum InterfaceSpecificationVersionOrderField { - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME', - /** Order by template_id */ - TemplateId = 'TEMPLATE_ID', - /** Order by template_name */ - TemplateName = 'TEMPLATE_NAME', - /** Order by version */ - Version = 'VERSION' + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", + /** Order by template_id */ + TemplateId = "TEMPLATE_ID", + /** Order by template_name */ + TemplateName = "TEMPLATE_NAME", + /** Order by version */ + Version = "VERSION", } /** Filter used to filter InterfaceSpecificationVersionTemplate */ export type InterfaceSpecificationVersionTemplateFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; }; /** Filter used to filter InterfaceTemplate */ export type InterfaceTemplateFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; }; /** Filter used to filter IntraComponentDependencyParticipant */ export type IntraComponentDependencyParticipantFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by id */ - id?: InputMaybe; - /** Filter by includedParts */ - includedParts?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - interface?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filters for nodes where the related node match this filter */ - usedAsIncomingAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - usedAsOutgoingAt?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by id */ + id?: InputMaybe; + /** Filter by includedParts */ + includedParts?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + interface?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filters for nodes where the related node match this filter */ + usedAsIncomingAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + usedAsOutgoingAt?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type IntraComponentDependencyParticipantListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a IntraComponentDependencyParticipant list */ export type IntraComponentDependencyParticipantOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of IntraComponentDependencyParticipant can be sorted by */ export enum IntraComponentDependencyParticipantOrderField { - /** Order by id */ - Id = 'ID' + /** Order by id */ + Id = "ID", } /** Filter used to filter IntraComponentDependencySpecification */ export type IntraComponentDependencySpecificationFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filters for nodes where the related node match this filter */ - componentVersion?: InputMaybe; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by incomingParticipants */ - incomingParticipants?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by outgoingParticipants */ - outgoingParticipants?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filters for nodes where the related node match this filter */ + componentVersion?: InputMaybe; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by incomingParticipants */ + incomingParticipants?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by outgoingParticipants */ + outgoingParticipants?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type IntraComponentDependencySpecificationListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a IntraComponentDependencySpecification list */ export type IntraComponentDependencySpecificationOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of IntraComponentDependencySpecification can be sorted by */ export enum IntraComponentDependencySpecificationOrderField { - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME' + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", } /** Filter used to filter IssueComment */ export type IssueCommentFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by answeredBy */ - answeredBy?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - answers?: InputMaybe; - /** Filter by bodyLastEditedAt */ - bodyLastEditedAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - bodyLastEditedBy?: InputMaybe; - /** Filter by createdAt */ - createdAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - createdBy?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by isCommentDeleted */ - isCommentDeleted?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - issue?: InputMaybe; - /** Filter by lastModifiedAt */ - lastModifiedAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - lastModifiedBy?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filters for nodes where the related node match this filter */ - parentItem?: InputMaybe; - /** Filter by referencedArtefacts */ - referencedArtefacts?: InputMaybe; - /** Filter for specific timeline items. Entries are joined by OR */ - timelineItemTypes?: InputMaybe>; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by answeredBy */ + answeredBy?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + answers?: InputMaybe; + /** Filter by bodyLastEditedAt */ + bodyLastEditedAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + bodyLastEditedBy?: InputMaybe; + /** Filter by createdAt */ + createdAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + createdBy?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by isCommentDeleted */ + isCommentDeleted?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + issue?: InputMaybe; + /** Filter by lastModifiedAt */ + lastModifiedAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + lastModifiedBy?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filters for nodes where the related node match this filter */ + parentItem?: InputMaybe; + /** Filter by referencedArtefacts */ + referencedArtefacts?: InputMaybe; + /** Filter for specific timeline items. Entries are joined by OR */ + timelineItemTypes?: InputMaybe>; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type IssueCommentListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a IssueComment list */ export type IssueCommentOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of IssueComment can be sorted by */ export enum IssueCommentOrderField { - /** Order by bodyLastEditedAt */ - BodyLastEditedAt = 'BODY_LAST_EDITED_AT', - /** Order by createdAt */ - CreatedAt = 'CREATED_AT', - /** Order by id */ - Id = 'ID', - /** Order by lastModifiedAt */ - LastModifiedAt = 'LAST_MODIFIED_AT' + /** Order by bodyLastEditedAt */ + BodyLastEditedAt = "BODY_LAST_EDITED_AT", + /** Order by createdAt */ + CreatedAt = "CREATED_AT", + /** Order by id */ + Id = "ID", + /** Order by lastModifiedAt */ + LastModifiedAt = "LAST_MODIFIED_AT", } /** Filter used to filter Issue */ export type IssueFilterInput = { - /** Filter by affects */ - affects?: InputMaybe; - /** Filter by aggregatedBy */ - aggregatedBy?: InputMaybe; - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by artefacts */ - artefacts?: InputMaybe; - /** Filter by assignments */ - assignments?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - body?: InputMaybe; - /** Filter by createdAt */ - createdAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - createdBy?: InputMaybe; - /** Filter by dueDate */ - dueDate?: InputMaybe; - /** Filter by estimatedTime */ - estimatedTime?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by imsIssues */ - imsIssues?: InputMaybe; - /** Filter by incomingRelations */ - incomingRelations?: InputMaybe; - /** Filter by issueComments */ - issueComments?: InputMaybe; - /** Filter by labels */ - labels?: InputMaybe; - /** Filter by lastModifiedAt */ - lastModifiedAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - lastModifiedBy?: InputMaybe; - /** Filter by lastUpdatedAt */ - lastUpdatedAt?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by outgoingRelations */ - outgoingRelations?: InputMaybe; - /** Filter by participants */ - participants?: InputMaybe; - /** Filter by pinnedOn */ - pinnedOn?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - priority?: InputMaybe; - /** Filter by spentTime */ - spentTime?: InputMaybe; - /** Filter by startDate */ - startDate?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - state?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - template?: InputMaybe; - /** Filter for templated fields with matching key and values. Entries are joined by AND */ - templatedFields?: InputMaybe>>; - /** Filter by timelineItems */ - timelineItems?: InputMaybe; - /** Filter by title */ - title?: InputMaybe; - /** Filter by trackables */ - trackables?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - type?: InputMaybe; + /** Filter by affects */ + affects?: InputMaybe; + /** Filter by aggregatedBy */ + aggregatedBy?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by artefacts */ + artefacts?: InputMaybe; + /** Filter by assignments */ + assignments?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + body?: InputMaybe; + /** Filter by createdAt */ + createdAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + createdBy?: InputMaybe; + /** Filter by dueDate */ + dueDate?: InputMaybe; + /** Filter by estimatedTime */ + estimatedTime?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by imsIssues */ + imsIssues?: InputMaybe; + /** Filter by incomingRelations */ + incomingRelations?: InputMaybe; + /** Filter by issueComments */ + issueComments?: InputMaybe; + /** Filter by labels */ + labels?: InputMaybe; + /** Filter by lastModifiedAt */ + lastModifiedAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + lastModifiedBy?: InputMaybe; + /** Filter by lastUpdatedAt */ + lastUpdatedAt?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by outgoingRelations */ + outgoingRelations?: InputMaybe; + /** Filter by participants */ + participants?: InputMaybe; + /** Filter by pinnedOn */ + pinnedOn?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + priority?: InputMaybe; + /** Filter by spentTime */ + spentTime?: InputMaybe; + /** Filter by startDate */ + startDate?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + state?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + template?: InputMaybe; + /** Filter for templated fields with matching key and values. Entries are joined by AND */ + templatedFields?: InputMaybe>>; + /** Filter by timelineItems */ + timelineItems?: InputMaybe; + /** Filter by title */ + title?: InputMaybe; + /** Filter by trackables */ + trackables?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + type?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type IssueListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a Issue list */ export type IssueOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of Issue can be sorted by */ export enum IssueOrderField { - /** Order by createdAt */ - CreatedAt = 'CREATED_AT', - /** Order by dueDate */ - DueDate = 'DUE_DATE', - /** Order by estimatedTime */ - EstimatedTime = 'ESTIMATED_TIME', - /** Order by id */ - Id = 'ID', - /** Order by lastModifiedAt */ - LastModifiedAt = 'LAST_MODIFIED_AT', - /** Order by lastUpdatedAt */ - LastUpdatedAt = 'LAST_UPDATED_AT', - /** Order by priority_id */ - PriorityId = 'PRIORITY_ID', - /** Order by priority_name */ - PriorityName = 'PRIORITY_NAME', - /** Order by priority_value */ - PriorityValue = 'PRIORITY_VALUE', - /** Order by spentTime */ - SpentTime = 'SPENT_TIME', - /** Order by startDate */ - StartDate = 'START_DATE', - /** Order by state_id */ - StateId = 'STATE_ID', - /** Order by state_isOpen */ - StateIsOpen = 'STATE_IS_OPEN', - /** Order by state_name */ - StateName = 'STATE_NAME', - /** Order by template_id */ - TemplateId = 'TEMPLATE_ID', - /** Order by template_name */ - TemplateName = 'TEMPLATE_NAME', - /** Order by title */ - Title = 'TITLE', - /** Order by type_id */ - TypeId = 'TYPE_ID', - /** Order by type_name */ - TypeName = 'TYPE_NAME' + /** Order by createdAt */ + CreatedAt = "CREATED_AT", + /** Order by dueDate */ + DueDate = "DUE_DATE", + /** Order by estimatedTime */ + EstimatedTime = "ESTIMATED_TIME", + /** Order by id */ + Id = "ID", + /** Order by lastModifiedAt */ + LastModifiedAt = "LAST_MODIFIED_AT", + /** Order by lastUpdatedAt */ + LastUpdatedAt = "LAST_UPDATED_AT", + /** Order by priority_id */ + PriorityId = "PRIORITY_ID", + /** Order by priority_name */ + PriorityName = "PRIORITY_NAME", + /** Order by priority_value */ + PriorityValue = "PRIORITY_VALUE", + /** Order by spentTime */ + SpentTime = "SPENT_TIME", + /** Order by startDate */ + StartDate = "START_DATE", + /** Order by state_id */ + StateId = "STATE_ID", + /** Order by state_isOpen */ + StateIsOpen = "STATE_IS_OPEN", + /** Order by state_name */ + StateName = "STATE_NAME", + /** Order by template_id */ + TemplateId = "TEMPLATE_ID", + /** Order by template_name */ + TemplateName = "TEMPLATE_NAME", + /** Order by title */ + Title = "TITLE", + /** Order by type_id */ + TypeId = "TYPE_ID", + /** Order by type_name */ + TypeName = "TYPE_NAME", } /** Filter used to filter IssuePriority */ export type IssuePriorityFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by partOf */ - partOf?: InputMaybe; - /** Filter by prioritizedIssues */ - prioritizedIssues?: InputMaybe; - /** Filter by value */ - value?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by partOf */ + partOf?: InputMaybe; + /** Filter by prioritizedIssues */ + prioritizedIssues?: InputMaybe; + /** Filter by value */ + value?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type IssuePriorityListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a IssuePriority list */ export type IssuePriorityOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of IssuePriority can be sorted by */ export enum IssuePriorityOrderField { - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME', - /** Order by value */ - Value = 'VALUE' + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", + /** Order by value */ + Value = "VALUE", } /** Filter used to filter IssueRelation */ export type IssueRelationFilterInput = { - /** Filter by aggregatedBy */ - aggregatedBy?: InputMaybe; - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by createdAt */ - createdAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - createdBy?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - initialType?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - issue?: InputMaybe; - /** Filter by lastModifiedAt */ - lastModifiedAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - lastModifiedBy?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filters for nodes where the related node match this filter */ - parentItem?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - relatedIssue?: InputMaybe; - /** Filter for specific timeline items. Entries are joined by OR */ - timelineItemTypes?: InputMaybe>; - /** Filters for nodes where the related node match this filter */ - type?: InputMaybe; + /** Filter by aggregatedBy */ + aggregatedBy?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by createdAt */ + createdAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + createdBy?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + initialType?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + issue?: InputMaybe; + /** Filter by lastModifiedAt */ + lastModifiedAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + lastModifiedBy?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filters for nodes where the related node match this filter */ + parentItem?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + relatedIssue?: InputMaybe; + /** Filter for specific timeline items. Entries are joined by OR */ + timelineItemTypes?: InputMaybe>; + /** Filters for nodes where the related node match this filter */ + type?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type IssueRelationListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a IssueRelation list */ export type IssueRelationOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of IssueRelation can be sorted by */ export enum IssueRelationOrderField { - /** Order by createdAt */ - CreatedAt = 'CREATED_AT', - /** Order by id */ - Id = 'ID', - /** Order by lastModifiedAt */ - LastModifiedAt = 'LAST_MODIFIED_AT' + /** Order by createdAt */ + CreatedAt = "CREATED_AT", + /** Order by id */ + Id = "ID", + /** Order by lastModifiedAt */ + LastModifiedAt = "LAST_MODIFIED_AT", } /** Filter used to filter IssueRelationType */ export type IssueRelationTypeFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by inverseName */ - inverseName?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by partOf */ - partOf?: InputMaybe; - /** Filter by relationsWithType */ - relationsWithType?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by inverseName */ + inverseName?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by partOf */ + partOf?: InputMaybe; + /** Filter by relationsWithType */ + relationsWithType?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type IssueRelationTypeListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a IssueRelationType list */ export type IssueRelationTypeOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of IssueRelationType can be sorted by */ export enum IssueRelationTypeOrderField { - /** Order by id */ - Id = 'ID', - /** Order by inverseName */ - InverseName = 'INVERSE_NAME', - /** Order by name */ - Name = 'NAME' + /** Order by id */ + Id = "ID", + /** Order by inverseName */ + InverseName = "INVERSE_NAME", + /** Order by name */ + Name = "NAME", } /** Filter used to filter IssueState */ export type IssueStateFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by isOpen */ - isOpen?: InputMaybe; - /** Filter by issuesWithState */ - issuesWithState?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by partOf */ - partOf?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by isOpen */ + isOpen?: InputMaybe; + /** Filter by issuesWithState */ + issuesWithState?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by partOf */ + partOf?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type IssueStateListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a IssueState list */ export type IssueStateOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of IssueState can be sorted by */ export enum IssueStateOrderField { - /** Order by id */ - Id = 'ID', - /** Order by isOpen */ - IsOpen = 'IS_OPEN', - /** Order by name */ - Name = 'NAME' + /** Order by id */ + Id = "ID", + /** Order by isOpen */ + IsOpen = "IS_OPEN", + /** Order by name */ + Name = "NAME", } /** Filter used to filter IssueTemplate */ export type IssueTemplateFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by assignmentTypes */ - assignmentTypes?: InputMaybe; - /** Filter by description */ - description?: InputMaybe; - /** Filter by extendedBy */ - extendedBy?: InputMaybe; - /** Filter by extends */ - extends?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by isDeprecated */ - isDeprecated?: InputMaybe; - /** Filter by issuePriorities */ - issuePriorities?: InputMaybe; - /** Filter by issueStates */ - issueStates?: InputMaybe; - /** Filter by issueTypes */ - issueTypes?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by relationTypes */ - relationTypes?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by assignmentTypes */ + assignmentTypes?: InputMaybe; + /** Filter by description */ + description?: InputMaybe; + /** Filter by extendedBy */ + extendedBy?: InputMaybe; + /** Filter by extends */ + extends?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by isDeprecated */ + isDeprecated?: InputMaybe; + /** Filter by issuePriorities */ + issuePriorities?: InputMaybe; + /** Filter by issueStates */ + issueStates?: InputMaybe; + /** Filter by issueTypes */ + issueTypes?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by relationTypes */ + relationTypes?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type IssueTemplateListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a IssueTemplate list */ export type IssueTemplateOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of IssueTemplate can be sorted by */ export enum IssueTemplateOrderField { - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME' + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", } /** Filter used to filter IssueType */ export type IssueTypeFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by issuesWithType */ - issuesWithType?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by partOf */ - partOf?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by issuesWithType */ + issuesWithType?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by partOf */ + partOf?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type IssueTypeListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a IssueType list */ export type IssueTypeOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of IssueType can be sorted by */ export enum IssueTypeOrderField { - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME' + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", } /** Input set update the value of a JSON field, like an extension field or a templated field. */ export type JsonFieldInput = { - /** The name of the field */ - name: Scalars['String']['input']; - /** The new value of the field */ - value?: InputMaybe; + /** The name of the field */ + name: Scalars["String"]["input"]; + /** The new value of the field */ + value?: InputMaybe; }; /** Filter used to filter Label */ export type LabelFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by color */ - color?: InputMaybe; - /** Filter by createdAt */ - createdAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - createdBy?: InputMaybe; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by issues */ - issues?: InputMaybe; - /** Filter by lastModifiedAt */ - lastModifiedAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - lastModifiedBy?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by trackables */ - trackables?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by color */ + color?: InputMaybe; + /** Filter by createdAt */ + createdAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + createdBy?: InputMaybe; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by issues */ + issues?: InputMaybe; + /** Filter by lastModifiedAt */ + lastModifiedAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + lastModifiedBy?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by trackables */ + trackables?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type LabelListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a Label list */ export type LabelOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of Label can be sorted by */ export enum LabelOrderField { - /** Order by color */ - Color = 'COLOR', - /** Order by createdAt */ - CreatedAt = 'CREATED_AT', - /** Order by id */ - Id = 'ID', - /** Order by lastModifiedAt */ - LastModifiedAt = 'LAST_MODIFIED_AT', - /** Order by name */ - Name = 'NAME' + /** Order by color */ + Color = "COLOR", + /** Order by createdAt */ + CreatedAt = "CREATED_AT", + /** Order by id */ + Id = "ID", + /** Order by lastModifiedAt */ + LastModifiedAt = "LAST_MODIFIED_AT", + /** Order by name */ + Name = "NAME", } /** Type of a Relation marker */ export enum MarkerType { - /** A regular arrow */ - Arrow = 'ARROW', - /** A circle */ - Circle = 'CIRCLE', - /** A diamond */ - Diamond = 'DIAMOND', - /** A filled circle */ - FilledCircle = 'FILLED_CIRCLE', - /** A filled diamond */ - FilledDiamond = 'FILLED_DIAMOND', - /** A filled triangle */ - FilledTriangle = 'FILLED_TRIANGLE', - /** A triangle */ - Triangle = 'TRIANGLE' + /** A regular arrow */ + Arrow = "ARROW", + /** A circle */ + Circle = "CIRCLE", + /** A diamond */ + Diamond = "DIAMOND", + /** A filled circle */ + FilledCircle = "FILLED_CIRCLE", + /** A filled diamond */ + FilledDiamond = "FILLED_DIAMOND", + /** A filled triangle */ + FilledTriangle = "FILLED_TRIANGLE", + /** A triangle */ + Triangle = "TRIANGLE", } export type NodePermissionFilterEntry = { - /** The node where the user must have the permission */ - node: Scalars['ID']['input']; - /** The permission the user must have on the node */ - permission: AllPermissionEntry; + /** The node where the user must have the permission */ + node: Scalars["ID"]["input"]; + /** The permission the user must have on the node */ + permission: AllPermissionEntry; }; /** Filter which can be used to filter for Nodes with a specific DateTime field */ export type NullableDateTimeFilterInput = { - /** Matches values which are equal to the provided value */ - eq?: InputMaybe; - /** Matches values which are greater than the provided value */ - gt?: InputMaybe; - /** Matches values which are greater than or equal to the provided value */ - gte?: InputMaybe; - /** Matches values which are equal to any of the provided values */ - in?: InputMaybe>; - /** If true, matches only null values, if false, matches only non-null values */ - isNull?: InputMaybe; - /** Matches values which are lesser than the provided value */ - lt?: InputMaybe; - /** Matches values which are lesser than or equal to the provided value */ - lte?: InputMaybe; + /** Matches values which are equal to the provided value */ + eq?: InputMaybe; + /** Matches values which are greater than the provided value */ + gt?: InputMaybe; + /** Matches values which are greater than or equal to the provided value */ + gte?: InputMaybe; + /** Matches values which are equal to any of the provided values */ + in?: InputMaybe>; + /** If true, matches only null values, if false, matches only non-null values */ + isNull?: InputMaybe; + /** Matches values which are lesser than the provided value */ + lt?: InputMaybe; + /** Matches values which are lesser than or equal to the provided value */ + lte?: InputMaybe; }; /** Filter which can be used to filter for Nodes with a specific Duration field */ export type NullableDurationFilterInputFilterInput = { - /** Matches values which are equal to the provided value */ - eq?: InputMaybe; - /** Matches values which are greater than the provided value */ - gt?: InputMaybe; - /** Matches values which are greater than or equal to the provided value */ - gte?: InputMaybe; - /** Matches values which are equal to any of the provided values */ - in?: InputMaybe>; - /** If true, matches only null values, if false, matches only non-null values */ - isNull?: InputMaybe; - /** Matches values which are lesser than the provided value */ - lt?: InputMaybe; - /** Matches values which are lesser than or equal to the provided value */ - lte?: InputMaybe; + /** Matches values which are equal to the provided value */ + eq?: InputMaybe; + /** Matches values which are greater than the provided value */ + gt?: InputMaybe; + /** Matches values which are greater than or equal to the provided value */ + gte?: InputMaybe; + /** Matches values which are equal to any of the provided values */ + in?: InputMaybe>; + /** If true, matches only null values, if false, matches only non-null values */ + isNull?: InputMaybe; + /** Matches values which are lesser than the provided value */ + lt?: InputMaybe; + /** Matches values which are lesser than or equal to the provided value */ + lte?: InputMaybe; }; /** Filter which can be used to filter for Nodes with a specific Int field */ export type NullableIntFilterInput = { - /** Matches values which are equal to the provided value */ - eq?: InputMaybe; - /** Matches values which are greater than the provided value */ - gt?: InputMaybe; - /** Matches values which are greater than or equal to the provided value */ - gte?: InputMaybe; - /** Matches values which are equal to any of the provided values */ - in?: InputMaybe>; - /** If true, matches only null values, if false, matches only non-null values */ - isNull?: InputMaybe; - /** Matches values which are lesser than the provided value */ - lt?: InputMaybe; - /** Matches values which are lesser than or equal to the provided value */ - lte?: InputMaybe; + /** Matches values which are equal to the provided value */ + eq?: InputMaybe; + /** Matches values which are greater than the provided value */ + gt?: InputMaybe; + /** Matches values which are greater than or equal to the provided value */ + gte?: InputMaybe; + /** Matches values which are equal to any of the provided values */ + in?: InputMaybe>; + /** If true, matches only null values, if false, matches only non-null values */ + isNull?: InputMaybe; + /** Matches values which are lesser than the provided value */ + lt?: InputMaybe; + /** Matches values which are lesser than or equal to the provided value */ + lte?: InputMaybe; }; /** Filter which can be used to filter for Nodes with a specific String field */ export type NullableStringFilterInput = { - /** Matches Strings which contain the provided value */ - contains?: InputMaybe; - /** Matches Strings which end with the provided value */ - endsWith?: InputMaybe; - /** Matches values which are equal to the provided value */ - eq?: InputMaybe; - /** Matches values which are greater than the provided value */ - gt?: InputMaybe; - /** Matches values which are greater than or equal to the provided value */ - gte?: InputMaybe; - /** Matches values which are equal to any of the provided values */ - in?: InputMaybe>; - /** If true, matches only null values, if false, matches only non-null values */ - isNull?: InputMaybe; - /** Matches values which are lesser than the provided value */ - lt?: InputMaybe; - /** Matches values which are lesser than or equal to the provided value */ - lte?: InputMaybe; - /** Matches Strings using the provided RegEx */ - matches?: InputMaybe; - /** Matches Strings which start with the provided value */ - startsWith?: InputMaybe; + /** Matches Strings which contain the provided value */ + contains?: InputMaybe; + /** Matches Strings which end with the provided value */ + endsWith?: InputMaybe; + /** Matches values which are equal to the provided value */ + eq?: InputMaybe; + /** Matches values which are greater than the provided value */ + gt?: InputMaybe; + /** Matches values which are greater than or equal to the provided value */ + gte?: InputMaybe; + /** Matches values which are equal to any of the provided values */ + in?: InputMaybe>; + /** If true, matches only null values, if false, matches only non-null values */ + isNull?: InputMaybe; + /** Matches values which are lesser than the provided value */ + lt?: InputMaybe; + /** Matches values which are lesser than or equal to the provided value */ + lte?: InputMaybe; + /** Matches Strings using the provided RegEx */ + matches?: InputMaybe; + /** Matches Strings which start with the provided value */ + startsWith?: InputMaybe; }; /** Possible direction in which a list of nodes can be ordered */ export enum OrderDirection { - /** Ascending */ - Asc = 'ASC', - /** Descending */ - Desc = 'DESC' + /** Ascending */ + Asc = "ASC", + /** Descending */ + Desc = "DESC", } /** Filter used to filter ParentTimelineItem */ export type ParentTimelineItemFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by childItems */ - childItems?: InputMaybe; - /** Filter by createdAt */ - createdAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - createdBy?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - issue?: InputMaybe; - /** Filter by lastModifiedAt */ - lastModifiedAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - lastModifiedBy?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filters for nodes where the related node match this filter */ - parentItem?: InputMaybe; - /** Filter for specific timeline items. Entries are joined by OR */ - timelineItemTypes?: InputMaybe>; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by childItems */ + childItems?: InputMaybe; + /** Filter by createdAt */ + createdAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + createdBy?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + issue?: InputMaybe; + /** Filter by lastModifiedAt */ + lastModifiedAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + lastModifiedBy?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filters for nodes where the related node match this filter */ + parentItem?: InputMaybe; + /** Filter for specific timeline items. Entries are joined by OR */ + timelineItemTypes?: InputMaybe>; }; /** Permission entry enum type. */ export enum PermissionEntry { - /** Allows to create new Components. */ - CanCreateComponents = 'CAN_CREATE_COMPONENTS', - /** Allows to create new IMSs. */ - CanCreateImss = 'CAN_CREATE_IMSS', - /** Allows to create new Projects. */ - CanCreateProjects = 'CAN_CREATE_PROJECTS', - /** Allows to create new Templates. */ - CanCreateTemplates = 'CAN_CREATE_TEMPLATES' + /** Allows to create new Components. */ + CanCreateComponents = "CAN_CREATE_COMPONENTS", + /** Allows to create new IMSs. */ + CanCreateImss = "CAN_CREATE_IMSS", + /** Allows to create new Projects. */ + CanCreateProjects = "CAN_CREATE_PROJECTS", + /** Allows to create new Templates. */ + CanCreateTemplates = "CAN_CREATE_TEMPLATES", } /** Filter used to filter Project */ export type ProjectFilterInput = { - /** Filter by affectingIssues */ - affectingIssues?: InputMaybe; - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by artefacts */ - artefacts?: InputMaybe; - /** Filter by components */ - components?: InputMaybe; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by issues */ - issues?: InputMaybe; - /** Filter by labels */ - labels?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by permissions */ - permissions?: InputMaybe; - /** Filter by pinnedIssues */ - pinnedIssues?: InputMaybe; - /** Filters for AffectedByIssues which are related to a Trackable */ - relatedTo?: InputMaybe; - /** Filter by repositoryURL */ - repositoryURL?: InputMaybe; - /** Filter by syncsTo */ - syncsTo?: InputMaybe; + /** Filter by affectingIssues */ + affectingIssues?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by artefacts */ + artefacts?: InputMaybe; + /** Filter by components */ + components?: InputMaybe; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by issues */ + issues?: InputMaybe; + /** Filter by labels */ + labels?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by permissions */ + permissions?: InputMaybe; + /** Filter by pinnedIssues */ + pinnedIssues?: InputMaybe; + /** Filters for AffectedByIssues which are related to a Trackable */ + relatedTo?: InputMaybe; + /** Filter by repositoryURL */ + repositoryURL?: InputMaybe; + /** Filter by syncsTo */ + syncsTo?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type ProjectListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a Project list */ export type ProjectOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of Project can be sorted by */ export enum ProjectOrderField { - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME' + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", } /** ProjectPermission entry enum type. */ export enum ProjectPermissionEntry { - /** Grants all other permissions on the Node except READ. */ - Admin = 'ADMIN', - /** - * Allows affecting entities part of this Trackable with any Issues. - * Affectable entitites include - * - the Trackable itself - * - in case the Trackable is a Component - * - InterfaceSpecifications, their InterfaceSpecificationVersions and their InterfaceParts of the Component (not inherited ones) - * - Interfaces on the Component - * - ComponentVersions of the Component - */ - AffectEntitiesWithIssues = 'AFFECT_ENTITIES_WITH_ISSUES', - /** - * Allows to create Comments on Issues on this Trackable. - * Also allows editing of your own Comments. - */ - Comment = 'COMMENT', - /** - * Allows to create new Issues on the Trackable. - * This includes adding Issues from other Trackables. - */ - CreateIssues = 'CREATE_ISSUES', - /** Allows adding Issues on this Trackable to other Trackables. */ - ExportIssues = 'EXPORT_ISSUES', - /** Allows adding Labels on this Trackable to other Trackables. */ - ExportLabels = 'EXPORT_LABELS', - /** Allows to add, remove, and update Artefacts on this Trackable. */ - ManageArtefacts = 'MANAGE_ARTEFACTS', - /** Allows to add / remove ComponentVersions to / from this Project. */ - ManageComponents = 'MANAGE_COMPONENTS', - /** - * Allows to add, remove, and update IMSProjects on this Trackable. - * Note: for adding, `IMSPermissionEntry.SYNC_TRACKABLES` is required additionally - */ - ManageIms = 'MANAGE_IMS', - /** - * Allows to manage issues. - * This includes `CREATE_ISSUES` and `COMMENT`. - * This does NOT include `LINK_TO_ISSUES` and `LINK_FROM_ISSUES`. - * Additionaly includes - * - change the Template - * - add / remove Labels - * - add / remove Artefacts - * - change any field on the Issue (title, startDate, dueDate, ...) - * - change templated fields - * In contrast to `MODERATOR`, this does not allow editing / removing Comments of other users - */ - ManageIssues = 'MANAGE_ISSUES', - /** - * Allows to add, remove, and update Labels on this Trackable. - * Also allows to delete a Label, but only if it is allowed on all Trackable the Label is on. - */ - ManageLabels = 'MANAGE_LABELS', - /** - * Allows to moderate Issues on this Trackable. - * This allows everything `MANAGE_ISSUES` allows. - * Additionally, it allows editing and deleting Comments of other Users - */ - Moderator = 'MODERATOR', - /** - * Allows to read the Node (obtain it via the API) and to read certain related Nodes. - * See documentation for specific Node for the specific conditions. - */ - Read = 'READ' + /** Grants all other permissions on the Node except READ. */ + Admin = "ADMIN", + /** + * Allows affecting entities part of this Trackable with any Issues. + * Affectable entitites include + * - the Trackable itself + * - in case the Trackable is a Component + * - InterfaceSpecifications, their InterfaceSpecificationVersions and their InterfaceParts of the Component (not inherited ones) + * - Interfaces on the Component + * - ComponentVersions of the Component + */ + AffectEntitiesWithIssues = "AFFECT_ENTITIES_WITH_ISSUES", + /** + * Allows to create Comments on Issues on this Trackable. + * Also allows editing of your own Comments. + */ + Comment = "COMMENT", + /** + * Allows to create new Issues on the Trackable. + * This includes adding Issues from other Trackables. + */ + CreateIssues = "CREATE_ISSUES", + /** Allows adding Issues on this Trackable to other Trackables. */ + ExportIssues = "EXPORT_ISSUES", + /** Allows adding Labels on this Trackable to other Trackables. */ + ExportLabels = "EXPORT_LABELS", + /** Allows to add, remove, and update Artefacts on this Trackable. */ + ManageArtefacts = "MANAGE_ARTEFACTS", + /** Allows to add / remove ComponentVersions to / from this Project. */ + ManageComponents = "MANAGE_COMPONENTS", + /** + * Allows to add, remove, and update IMSProjects on this Trackable. + * Note: for adding, `IMSPermissionEntry.SYNC_TRACKABLES` is required additionally + */ + ManageIms = "MANAGE_IMS", + /** + * Allows to manage issues. + * This includes `CREATE_ISSUES` and `COMMENT`. + * This does NOT include `LINK_TO_ISSUES` and `LINK_FROM_ISSUES`. + * Additionaly includes + * - change the Template + * - add / remove Labels + * - add / remove Artefacts + * - change any field on the Issue (title, startDate, dueDate, ...) + * - change templated fields + * In contrast to `MODERATOR`, this does not allow editing / removing Comments of other users + */ + ManageIssues = "MANAGE_ISSUES", + /** + * Allows to add, remove, and update Labels on this Trackable. + * Also allows to delete a Label, but only if it is allowed on all Trackable the Label is on. + */ + ManageLabels = "MANAGE_LABELS", + /** + * Allows to moderate Issues on this Trackable. + * This allows everything `MANAGE_ISSUES` allows. + * Additionally, it allows editing and deleting Comments of other Users + */ + Moderator = "MODERATOR", + /** + * Allows to read the Node (obtain it via the API) and to read certain related Nodes. + * See documentation for specific Node for the specific conditions. + */ + Read = "READ", } /** Filter used to filter ProjectPermission */ export type ProjectPermissionFilterInput = { - /** Filter by allUsers */ - allUsers?: InputMaybe; - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Filter by nodesWithPermission */ - nodesWithPermission?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by users */ - users?: InputMaybe; + /** Filter by allUsers */ + allUsers?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Filter by nodesWithPermission */ + nodesWithPermission?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by users */ + users?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type ProjectPermissionListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a ProjectPermission list */ export type ProjectPermissionOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of ProjectPermission can be sorted by */ export enum ProjectPermissionOrderField { - /** Order by allUsers */ - AllUsers = 'ALL_USERS', - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME' + /** Order by allUsers */ + AllUsers = "ALL_USERS", + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", } /** Filter used to filter RelationCondition */ export type RelationConditionFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by from */ - from?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by interfaceSpecificationDerivationConditions */ - interfaceSpecificationDerivationConditions?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by partOf */ - partOf?: InputMaybe; - /** Filter by to */ - to?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by from */ + from?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by interfaceSpecificationDerivationConditions */ + interfaceSpecificationDerivationConditions?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by partOf */ + partOf?: InputMaybe; + /** Filter by to */ + to?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type RelationConditionListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a RelationCondition list */ export type RelationConditionOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of RelationCondition can be sorted by */ export enum RelationConditionOrderField { - /** Order by id */ - Id = 'ID' + /** Order by id */ + Id = "ID", } /** Filter used to filter Relation */ export type RelationFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by derivesInvisible */ - derivesInvisible?: InputMaybe; - /** Filter by derivesVisible */ - derivesVisible?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - end?: InputMaybe; - /** Filter by endParts */ - endParts?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filters for nodes where the related node match this filter */ - start?: InputMaybe; - /** Filter by startParts */ - startParts?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - template?: InputMaybe; - /** Filter for templated fields with matching key and values. Entries are joined by AND */ - templatedFields?: InputMaybe>>; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by derivesInvisible */ + derivesInvisible?: InputMaybe; + /** Filter by derivesVisible */ + derivesVisible?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + end?: InputMaybe; + /** Filter by endParts */ + endParts?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filters for nodes where the related node match this filter */ + start?: InputMaybe; + /** Filter by startParts */ + startParts?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + template?: InputMaybe; + /** Filter for templated fields with matching key and values. Entries are joined by AND */ + templatedFields?: InputMaybe>>; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type RelationListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a Relation list */ export type RelationOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of Relation can be sorted by */ export enum RelationOrderField { - /** Order by end_id */ - EndId = 'END_ID', - /** Order by end_name */ - EndName = 'END_NAME', - /** Order by id */ - Id = 'ID', - /** Order by start_id */ - StartId = 'START_ID', - /** Order by start_name */ - StartName = 'START_NAME', - /** Order by template_id */ - TemplateId = 'TEMPLATE_ID', - /** Order by template_name */ - TemplateName = 'TEMPLATE_NAME' + /** Order by end_id */ + EndId = "END_ID", + /** Order by end_name */ + EndName = "END_NAME", + /** Order by id */ + Id = "ID", + /** Order by start_id */ + StartId = "START_ID", + /** Order by start_name */ + StartName = "START_NAME", + /** Order by template_id */ + TemplateId = "TEMPLATE_ID", + /** Order by template_name */ + TemplateName = "TEMPLATE_NAME", } /** Filter used to filter RelationPartner */ export type RelationPartnerFilterInput = { - /** Filter by affectingIssues */ - affectingIssues?: InputMaybe; - /** Filter by aggregatedIssues */ - aggregatedIssues?: InputMaybe; - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by incomingRelations */ - incomingRelations?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by outgoingRelations */ - outgoingRelations?: InputMaybe; - /** Filters for RelationPartners which are part of a Project's component graph */ - partOfProject?: InputMaybe; - /** Filters for AffectedByIssues which are related to a Trackable */ - relatedTo?: InputMaybe; - /** Filter for templated fields with matching key and values. Entries are joined by AND */ - templatedFields?: InputMaybe>>; + /** Filter by affectingIssues */ + affectingIssues?: InputMaybe; + /** Filter by aggregatedIssues */ + aggregatedIssues?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by incomingRelations */ + incomingRelations?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by outgoingRelations */ + outgoingRelations?: InputMaybe; + /** Filters for RelationPartners which are part of a Project's component graph */ + partOfProject?: InputMaybe; + /** Filters for AffectedByIssues which are related to a Trackable */ + relatedTo?: InputMaybe; + /** Filter for templated fields with matching key and values. Entries are joined by AND */ + templatedFields?: InputMaybe>>; }; /** Filter used to filter RelationPartnerTemplate */ export type RelationPartnerTemplateFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by isDeprecated */ - isDeprecated?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by possibleEndOfRelations */ - possibleEndOfRelations?: InputMaybe; - /** Filter by possibleStartOfRelations */ - possibleStartOfRelations?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by isDeprecated */ + isDeprecated?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by possibleEndOfRelations */ + possibleEndOfRelations?: InputMaybe; + /** Filter by possibleStartOfRelations */ + possibleStartOfRelations?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type RelationPartnerTemplateListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a RelationPartnerTemplate list */ export type RelationPartnerTemplateOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of RelationPartnerTemplate can be sorted by */ export enum RelationPartnerTemplateOrderField { - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME' + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", } /** Filter used to filter RelationTemplate */ export type RelationTemplateFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by extendedBy */ - extendedBy?: InputMaybe; - /** Filter by extends */ - extends?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by isDeprecated */ - isDeprecated?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by relationConditions */ - relationConditions?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by extendedBy */ + extendedBy?: InputMaybe; + /** Filter by extends */ + extends?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by isDeprecated */ + isDeprecated?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by relationConditions */ + relationConditions?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type RelationTemplateListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a RelationTemplate list */ export type RelationTemplateOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of RelationTemplate can be sorted by */ export enum RelationTemplateOrderField { - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME' + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", } /** Type of a Shape */ export enum ShapeType { - /** A Circle */ - Circle = 'CIRCLE', - /** An Ellipse */ - Ellipse = 'ELLIPSE', - /** A Hexagon */ - Hexagon = 'HEXAGON', - /** A Rectangle */ - Rect = 'RECT', - /** A Rhombus */ - Rhombus = 'RHOMBUS' + /** A Circle */ + Circle = "CIRCLE", + /** An Ellipse */ + Ellipse = "ELLIPSE", + /** A Hexagon */ + Hexagon = "HEXAGON", + /** A Rectangle */ + Rect = "RECT", + /** A Rhombus */ + Rhombus = "RHOMBUS", } /** Filter which can be used to filter for Nodes with a specific String field */ export type StringFilterInput = { - /** Matches Strings which contain the provided value */ - contains?: InputMaybe; - /** Matches Strings which end with the provided value */ - endsWith?: InputMaybe; - /** Matches values which are equal to the provided value */ - eq?: InputMaybe; - /** Matches values which are greater than the provided value */ - gt?: InputMaybe; - /** Matches values which are greater than or equal to the provided value */ - gte?: InputMaybe; - /** Matches values which are equal to any of the provided values */ - in?: InputMaybe>; - /** Matches values which are lesser than the provided value */ - lt?: InputMaybe; - /** Matches values which are lesser than or equal to the provided value */ - lte?: InputMaybe; - /** Matches Strings using the provided RegEx */ - matches?: InputMaybe; - /** Matches Strings which start with the provided value */ - startsWith?: InputMaybe; + /** Matches Strings which contain the provided value */ + contains?: InputMaybe; + /** Matches Strings which end with the provided value */ + endsWith?: InputMaybe; + /** Matches values which are equal to the provided value */ + eq?: InputMaybe; + /** Matches values which are greater than the provided value */ + gt?: InputMaybe; + /** Matches values which are greater than or equal to the provided value */ + gte?: InputMaybe; + /** Matches values which are equal to any of the provided values */ + in?: InputMaybe>; + /** Matches values which are lesser than the provided value */ + lt?: InputMaybe; + /** Matches values which are lesser than or equal to the provided value */ + lte?: InputMaybe; + /** Matches Strings using the provided RegEx */ + matches?: InputMaybe; + /** Matches Strings which start with the provided value */ + startsWith?: InputMaybe; }; /** Filter used to filter SyncPermissionTarget */ export type SyncPermissionTargetFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by syncOthersAllowedBy */ - syncOthersAllowedBy?: InputMaybe; - /** Filter by syncSelfAllowedBy */ - syncSelfAllowedBy?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by syncOthersAllowedBy */ + syncOthersAllowedBy?: InputMaybe; + /** Filter by syncSelfAllowedBy */ + syncSelfAllowedBy?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type SyncPermissionTargetListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a SyncPermissionTarget list */ export type SyncPermissionTargetOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of SyncPermissionTarget can be sorted by */ export enum SyncPermissionTargetOrderField { - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME' + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", } /** Filter used to filter TimelineItem */ export type TimelineItemFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by createdAt */ - createdAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - createdBy?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - issue?: InputMaybe; - /** Filter by lastModifiedAt */ - lastModifiedAt?: InputMaybe; - /** Filters for nodes where the related node match this filter */ - lastModifiedBy?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filters for nodes where the related node match this filter */ - parentItem?: InputMaybe; - /** Filter for specific timeline items. Entries are joined by OR */ - timelineItemTypes?: InputMaybe>; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by createdAt */ + createdAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + createdBy?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + issue?: InputMaybe; + /** Filter by lastModifiedAt */ + lastModifiedAt?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + lastModifiedBy?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filters for nodes where the related node match this filter */ + parentItem?: InputMaybe; + /** Filter for specific timeline items. Entries are joined by OR */ + timelineItemTypes?: InputMaybe>; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type TimelineItemListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a TimelineItem list */ export type TimelineItemOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of TimelineItem can be sorted by */ export enum TimelineItemOrderField { - /** Order by createdAt */ - CreatedAt = 'CREATED_AT', - /** Order by id */ - Id = 'ID', - /** Order by lastModifiedAt */ - LastModifiedAt = 'LAST_MODIFIED_AT' + /** Order by createdAt */ + CreatedAt = "CREATED_AT", + /** Order by id */ + Id = "ID", + /** Order by lastModifiedAt */ + LastModifiedAt = "LAST_MODIFIED_AT", } /** All timeline items types */ export enum TimelineItemType { - /** AbstractTypeChangedEvent timeline item */ - AbstractTypeChangedEvent = 'ABSTRACT_TYPE_CHANGED_EVENT', - /** AddedAffectedEntityEvent timeline item */ - AddedAffectedEntityEvent = 'ADDED_AFFECTED_ENTITY_EVENT', - /** AddedArtefactEvent timeline item */ - AddedArtefactEvent = 'ADDED_ARTEFACT_EVENT', - /** AddedLabelEvent timeline item */ - AddedLabelEvent = 'ADDED_LABEL_EVENT', - /** AddedToPinnedIssuesEvent timeline item */ - AddedToPinnedIssuesEvent = 'ADDED_TO_PINNED_ISSUES_EVENT', - /** AddedToTrackableEvent timeline item */ - AddedToTrackableEvent = 'ADDED_TO_TRACKABLE_EVENT', - /** Assignment timeline item */ - Assignment = 'ASSIGNMENT', - /** AssignmentTypeChangedEvent timeline item */ - AssignmentTypeChangedEvent = 'ASSIGNMENT_TYPE_CHANGED_EVENT', - /** Body timeline item */ - Body = 'BODY', - /** Comment timeline item */ - Comment = 'COMMENT', - /** IncomingRelationTypeChangedEvent timeline item */ - IncomingRelationTypeChangedEvent = 'INCOMING_RELATION_TYPE_CHANGED_EVENT', - /** IssueComment timeline item */ - IssueComment = 'ISSUE_COMMENT', - /** IssueRelation timeline item */ - IssueRelation = 'ISSUE_RELATION', - /** OutgoingRelationTypeChangedEvent timeline item */ - OutgoingRelationTypeChangedEvent = 'OUTGOING_RELATION_TYPE_CHANGED_EVENT', - /** ParentTimelineItem timeline item */ - ParentTimelineItem = 'PARENT_TIMELINE_ITEM', - /** PriorityChangedEvent timeline item */ - PriorityChangedEvent = 'PRIORITY_CHANGED_EVENT', - /** PublicTimelineItem timeline item */ - PublicTimelineItem = 'PUBLIC_TIMELINE_ITEM', - /** RelatedByIssueEvent timeline item */ - RelatedByIssueEvent = 'RELATED_BY_ISSUE_EVENT', - /** RelationTypeChangedEvent timeline item */ - RelationTypeChangedEvent = 'RELATION_TYPE_CHANGED_EVENT', - /** RemovedAffectedEntityEvent timeline item */ - RemovedAffectedEntityEvent = 'REMOVED_AFFECTED_ENTITY_EVENT', - /** RemovedArtefactEvent timeline item */ - RemovedArtefactEvent = 'REMOVED_ARTEFACT_EVENT', - /** RemovedAssignmentEvent timeline item */ - RemovedAssignmentEvent = 'REMOVED_ASSIGNMENT_EVENT', - /** RemovedFromPinnedIssuesEvent timeline item */ - RemovedFromPinnedIssuesEvent = 'REMOVED_FROM_PINNED_ISSUES_EVENT', - /** RemovedFromTrackableEvent timeline item */ - RemovedFromTrackableEvent = 'REMOVED_FROM_TRACKABLE_EVENT', - /** RemovedIncomingRelationEvent timeline item */ - RemovedIncomingRelationEvent = 'REMOVED_INCOMING_RELATION_EVENT', - /** RemovedLabelEvent timeline item */ - RemovedLabelEvent = 'REMOVED_LABEL_EVENT', - /** RemovedOutgoingRelationEvent timeline item */ - RemovedOutgoingRelationEvent = 'REMOVED_OUTGOING_RELATION_EVENT', - /** RemovedRelationEvent timeline item */ - RemovedRelationEvent = 'REMOVED_RELATION_EVENT', - /** RemovedTemplatedFieldEvent timeline item */ - RemovedTemplatedFieldEvent = 'REMOVED_TEMPLATED_FIELD_EVENT', - /** StateChangedEvent timeline item */ - StateChangedEvent = 'STATE_CHANGED_EVENT', - /** TemplatedFieldChangedEvent timeline item */ - TemplatedFieldChangedEvent = 'TEMPLATED_FIELD_CHANGED_EVENT', - /** TemplateChangedEvent timeline item */ - TemplateChangedEvent = 'TEMPLATE_CHANGED_EVENT', - /** TimelineItem timeline item */ - TimelineItem = 'TIMELINE_ITEM', - /** TitleChangedEvent timeline item */ - TitleChangedEvent = 'TITLE_CHANGED_EVENT', - /** TypeChangedEvent timeline item */ - TypeChangedEvent = 'TYPE_CHANGED_EVENT' + /** AbstractTypeChangedEvent timeline item */ + AbstractTypeChangedEvent = "ABSTRACT_TYPE_CHANGED_EVENT", + /** AddedAffectedEntityEvent timeline item */ + AddedAffectedEntityEvent = "ADDED_AFFECTED_ENTITY_EVENT", + /** AddedArtefactEvent timeline item */ + AddedArtefactEvent = "ADDED_ARTEFACT_EVENT", + /** AddedLabelEvent timeline item */ + AddedLabelEvent = "ADDED_LABEL_EVENT", + /** AddedToPinnedIssuesEvent timeline item */ + AddedToPinnedIssuesEvent = "ADDED_TO_PINNED_ISSUES_EVENT", + /** AddedToTrackableEvent timeline item */ + AddedToTrackableEvent = "ADDED_TO_TRACKABLE_EVENT", + /** Assignment timeline item */ + Assignment = "ASSIGNMENT", + /** AssignmentTypeChangedEvent timeline item */ + AssignmentTypeChangedEvent = "ASSIGNMENT_TYPE_CHANGED_EVENT", + /** Body timeline item */ + Body = "BODY", + /** Comment timeline item */ + Comment = "COMMENT", + /** IncomingRelationTypeChangedEvent timeline item */ + IncomingRelationTypeChangedEvent = "INCOMING_RELATION_TYPE_CHANGED_EVENT", + /** IssueComment timeline item */ + IssueComment = "ISSUE_COMMENT", + /** IssueRelation timeline item */ + IssueRelation = "ISSUE_RELATION", + /** OutgoingRelationTypeChangedEvent timeline item */ + OutgoingRelationTypeChangedEvent = "OUTGOING_RELATION_TYPE_CHANGED_EVENT", + /** ParentTimelineItem timeline item */ + ParentTimelineItem = "PARENT_TIMELINE_ITEM", + /** PriorityChangedEvent timeline item */ + PriorityChangedEvent = "PRIORITY_CHANGED_EVENT", + /** PublicTimelineItem timeline item */ + PublicTimelineItem = "PUBLIC_TIMELINE_ITEM", + /** RelatedByIssueEvent timeline item */ + RelatedByIssueEvent = "RELATED_BY_ISSUE_EVENT", + /** RelationTypeChangedEvent timeline item */ + RelationTypeChangedEvent = "RELATION_TYPE_CHANGED_EVENT", + /** RemovedAffectedEntityEvent timeline item */ + RemovedAffectedEntityEvent = "REMOVED_AFFECTED_ENTITY_EVENT", + /** RemovedArtefactEvent timeline item */ + RemovedArtefactEvent = "REMOVED_ARTEFACT_EVENT", + /** RemovedAssignmentEvent timeline item */ + RemovedAssignmentEvent = "REMOVED_ASSIGNMENT_EVENT", + /** RemovedFromPinnedIssuesEvent timeline item */ + RemovedFromPinnedIssuesEvent = "REMOVED_FROM_PINNED_ISSUES_EVENT", + /** RemovedFromTrackableEvent timeline item */ + RemovedFromTrackableEvent = "REMOVED_FROM_TRACKABLE_EVENT", + /** RemovedIncomingRelationEvent timeline item */ + RemovedIncomingRelationEvent = "REMOVED_INCOMING_RELATION_EVENT", + /** RemovedLabelEvent timeline item */ + RemovedLabelEvent = "REMOVED_LABEL_EVENT", + /** RemovedOutgoingRelationEvent timeline item */ + RemovedOutgoingRelationEvent = "REMOVED_OUTGOING_RELATION_EVENT", + /** RemovedRelationEvent timeline item */ + RemovedRelationEvent = "REMOVED_RELATION_EVENT", + /** RemovedTemplatedFieldEvent timeline item */ + RemovedTemplatedFieldEvent = "REMOVED_TEMPLATED_FIELD_EVENT", + /** StateChangedEvent timeline item */ + StateChangedEvent = "STATE_CHANGED_EVENT", + /** TemplatedFieldChangedEvent timeline item */ + TemplatedFieldChangedEvent = "TEMPLATED_FIELD_CHANGED_EVENT", + /** TemplateChangedEvent timeline item */ + TemplateChangedEvent = "TEMPLATE_CHANGED_EVENT", + /** TimelineItem timeline item */ + TimelineItem = "TIMELINE_ITEM", + /** TitleChangedEvent timeline item */ + TitleChangedEvent = "TITLE_CHANGED_EVENT", + /** TypeChangedEvent timeline item */ + TypeChangedEvent = "TYPE_CHANGED_EVENT", } /** Filter used to filter Trackable */ export type TrackableFilterInput = { - /** Filter by affectingIssues */ - affectingIssues?: InputMaybe; - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by artefacts */ - artefacts?: InputMaybe; - /** Filter by description */ - description?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Filter by issues */ - issues?: InputMaybe; - /** Filter by labels */ - labels?: InputMaybe; - /** Filter by name */ - name?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by pinnedIssues */ - pinnedIssues?: InputMaybe; - /** Filters for AffectedByIssues which are related to a Trackable */ - relatedTo?: InputMaybe; - /** Filter by repositoryURL */ - repositoryURL?: InputMaybe; - /** Filter by syncsTo */ - syncsTo?: InputMaybe; + /** Filter by affectingIssues */ + affectingIssues?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by artefacts */ + artefacts?: InputMaybe; + /** Filter by description */ + description?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by issues */ + issues?: InputMaybe; + /** Filter by labels */ + labels?: InputMaybe; + /** Filter by name */ + name?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by pinnedIssues */ + pinnedIssues?: InputMaybe; + /** Filters for AffectedByIssues which are related to a Trackable */ + relatedTo?: InputMaybe; + /** Filter by repositoryURL */ + repositoryURL?: InputMaybe; + /** Filter by syncsTo */ + syncsTo?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type TrackableListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a Trackable list */ export type TrackableOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of Trackable can be sorted by */ export enum TrackableOrderField { - /** Order by id */ - Id = 'ID', - /** Order by name */ - Name = 'NAME' + /** Order by id */ + Id = "ID", + /** Order by name */ + Name = "NAME", } /** Input for the updateIMSUser mutation */ export type UpdateImsUserInput = { - /** The new displayName of the User to update */ - displayName?: InputMaybe; - /** The new email of the User to update */ - email?: InputMaybe; - /** - * The id of the GropiusUser the updated IMSUser is associated with, replaces existing association - * or removes it if null is provided. - * - */ - gropiusUser?: InputMaybe; - /** The id of the node to update */ - id: Scalars['ID']['input']; - /** Values for templatedFields to update */ - templatedFields?: InputMaybe>; - /** The new username of the updated IMSUser */ - username?: InputMaybe; + /** The new displayName of the User to update */ + displayName?: InputMaybe; + /** The new email of the User to update */ + email?: InputMaybe; + /** + * The id of the GropiusUser the updated IMSUser is associated with, replaces existing association + * or removes it if null is provided. + * + */ + gropiusUser?: InputMaybe; + /** The id of the node to update */ + id: Scalars["ID"]["input"]; + /** Values for templatedFields to update */ + templatedFields?: InputMaybe>; + /** The new username of the updated IMSUser */ + username?: InputMaybe; }; /** Filter used to filter User */ export type UserFilterInput = { - /** Connects all subformulas via and */ - and?: InputMaybe>; - /** Filter by assignments */ - assignments?: InputMaybe; - /** Filter by createdNodes */ - createdNodes?: InputMaybe; - /** Filter by displayName */ - displayName?: InputMaybe; - /** Filter by email */ - email?: InputMaybe; - /** Filter by id */ - id?: InputMaybe; - /** Negates the subformula */ - not?: InputMaybe; - /** Connects all subformulas via or */ - or?: InputMaybe>; - /** Filter by participatedIssues */ - participatedIssues?: InputMaybe; - /** Filter by username */ - username?: InputMaybe; + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by assignments */ + assignments?: InputMaybe; + /** Filter by createdNodes */ + createdNodes?: InputMaybe; + /** Filter by displayName */ + displayName?: InputMaybe; + /** Filter by email */ + email?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by participatedIssues */ + participatedIssues?: InputMaybe; + /** Filter by username */ + username?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ export type UserListFilterInput = { - /** Filters for nodes where all of the related nodes match this filter */ - all?: InputMaybe; - /** Filters for nodes where any of the related nodes match this filter */ - any?: InputMaybe; - /** Filters for nodes where none of the related nodes match this filter */ - none?: InputMaybe; + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; }; /** Defines the order of a User list */ export type UserOrder = { - /** The direction to order by, defaults to ASC */ - direction?: InputMaybe; - /** The field to order by, defaults to ID */ - field?: InputMaybe; + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; }; /** Fields a list of User can be sorted by */ export enum UserOrderField { - /** Order by displayName */ - DisplayName = 'DISPLAY_NAME', - /** Order by email */ - Email = 'EMAIL', - /** Order by id */ - Id = 'ID', - /** Order by username */ - Username = 'USERNAME' + /** Order by displayName */ + DisplayName = "DISPLAY_NAME", + /** Order by email */ + Email = "EMAIL", + /** Order by id */ + Id = "ID", + /** Order by username */ + Username = "USERNAME", } -export type ImsUserWithDetailFragment = { __typename: 'IMSUser', id: string, username?: string | null, displayName: string, email?: string | null, templatedFields: Array<{ __typename: 'JSONField', name: string, value?: any | null }>, ims: { __typename: 'IMS', id: string, name: string, description: string, templatedFields: Array<{ __typename: 'JSONField', name: string, value?: any | null }> } }; +export type ImsUserWithDetailFragment = { + __typename: "IMSUser"; + id: string; + username?: string | null; + displayName: string; + email?: string | null; + templatedFields: Array<{ __typename: "JSONField"; name: string; value?: any | null }>; + ims: { + __typename: "IMS"; + id: string; + name: string; + description: string; + templatedFields: Array<{ __typename: "JSONField"; name: string; value?: any | null }>; + }; +}; export type GetBasicImsUserDataQueryVariables = Exact<{ - imsUserId: Scalars['ID']['input']; + imsUserId: Scalars["ID"]["input"]; }>; - -export type GetBasicImsUserDataQuery = { __typename?: 'Query', node?: { __typename: 'AddedAffectedEntityEvent', id: string } | { __typename: 'AddedArtefactEvent', id: string } | { __typename: 'AddedLabelEvent', id: string } | { __typename: 'AddedToPinnedIssuesEvent', id: string } | { __typename: 'AddedToTrackableEvent', id: string } | { __typename: 'AggregatedIssue', id: string } | { __typename: 'AggregatedIssueRelation', id: string } | { __typename: 'Artefact', id: string } | { __typename: 'ArtefactTemplate', id: string } | { __typename: 'Assignment', id: string } | { __typename: 'AssignmentType', id: string } | { __typename: 'AssignmentTypeChangedEvent', id: string } | { __typename: 'Body', id: string } | { __typename: 'Component', id: string } | { __typename: 'ComponentPermission', id: string } | { __typename: 'ComponentTemplate', id: string } | { __typename: 'ComponentVersion', id: string } | { __typename: 'ComponentVersionTemplate', id: string } | { __typename: 'FillStyle', id: string } | { __typename: 'GlobalPermission', id: string } | { __typename: 'GropiusUser', id: string } | { __typename: 'IMS', id: string } | { __typename: 'IMSIssue', id: string } | { __typename: 'IMSIssueTemplate', id: string } | { __typename: 'IMSPermission', id: string } | { __typename: 'IMSProject', id: string } | { __typename: 'IMSProjectTemplate', id: string } | { __typename: 'IMSTemplate', id: string } | { __typename: 'IMSUser', id: string } | { __typename: 'IMSUserTemplate', id: string } | { __typename: 'IncomingRelationTypeChangedEvent', id: string } | { __typename: 'Interface', id: string } | { __typename: 'InterfaceDefinition', id: string } | { __typename: 'InterfaceDefinitionTemplate', id: string } | { __typename: 'InterfacePart', id: string } | { __typename: 'InterfacePartTemplate', id: string } | { __typename: 'InterfaceSpecification', id: string } | { __typename: 'InterfaceSpecificationDerivationCondition', id: string } | { __typename: 'InterfaceSpecificationTemplate', id: string } | { __typename: 'InterfaceSpecificationVersion', id: string } | { __typename: 'InterfaceSpecificationVersionTemplate', id: string } | { __typename: 'InterfaceTemplate', id: string } | { __typename: 'IntraComponentDependencyParticipant', id: string } | { __typename: 'IntraComponentDependencySpecification', id: string } | { __typename: 'Issue', id: string } | { __typename: 'IssueComment', id: string } | { __typename: 'IssuePriority', id: string } | { __typename: 'IssueRelation', id: string } | { __typename: 'IssueRelationType', id: string } | { __typename: 'IssueState', id: string } | { __typename: 'IssueTemplate', id: string } | { __typename: 'IssueType', id: string } | { __typename: 'Label', id: string } | { __typename: 'OutgoingRelationTypeChangedEvent', id: string } | { __typename: 'PriorityChangedEvent', id: string } | { __typename: 'Project', id: string } | { __typename: 'ProjectPermission', id: string } | { __typename: 'RelatedByIssueEvent', id: string } | { __typename: 'Relation', id: string } | { __typename: 'RelationCondition', id: string } | { __typename: 'RelationTemplate', id: string } | { __typename: 'RemovedAffectedEntityEvent', id: string } | { __typename: 'RemovedArtefactEvent', id: string } | { __typename: 'RemovedAssignmentEvent', id: string } | { __typename: 'RemovedFromPinnedIssuesEvent', id: string } | { __typename: 'RemovedFromTrackableEvent', id: string } | { __typename: 'RemovedIncomingRelationEvent', id: string } | { __typename: 'RemovedLabelEvent', id: string } | { __typename: 'RemovedOutgoingRelationEvent', id: string } | { __typename: 'RemovedTemplatedFieldEvent', id: string } | { __typename: 'StateChangedEvent', id: string } | { __typename: 'StrokeStyle', id: string } | { __typename: 'TemplateChangedEvent', id: string } | { __typename: 'TemplatedFieldChangedEvent', id: string } | { __typename: 'TitleChangedEvent', id: string } | { __typename: 'TypeChangedEvent', id: string } | null }; +export type GetBasicImsUserDataQuery = { + __typename?: "Query"; + node?: + | { __typename: "AddedAffectedEntityEvent"; id: string } + | { __typename: "AddedArtefactEvent"; id: string } + | { __typename: "AddedLabelEvent"; id: string } + | { __typename: "AddedToPinnedIssuesEvent"; id: string } + | { __typename: "AddedToTrackableEvent"; id: string } + | { __typename: "AggregatedIssue"; id: string } + | { __typename: "AggregatedIssueRelation"; id: string } + | { __typename: "Artefact"; id: string } + | { __typename: "ArtefactTemplate"; id: string } + | { __typename: "Assignment"; id: string } + | { __typename: "AssignmentType"; id: string } + | { __typename: "AssignmentTypeChangedEvent"; id: string } + | { __typename: "Body"; id: string } + | { __typename: "Component"; id: string } + | { __typename: "ComponentPermission"; id: string } + | { __typename: "ComponentTemplate"; id: string } + | { __typename: "ComponentVersion"; id: string } + | { __typename: "ComponentVersionTemplate"; id: string } + | { __typename: "FillStyle"; id: string } + | { __typename: "GlobalPermission"; id: string } + | { __typename: "GropiusUser"; id: string } + | { __typename: "IMS"; id: string } + | { __typename: "IMSIssue"; id: string } + | { __typename: "IMSIssueTemplate"; id: string } + | { __typename: "IMSPermission"; id: string } + | { __typename: "IMSProject"; id: string } + | { __typename: "IMSProjectTemplate"; id: string } + | { __typename: "IMSTemplate"; id: string } + | { __typename: "IMSUser"; id: string } + | { __typename: "IMSUserTemplate"; id: string } + | { __typename: "IncomingRelationTypeChangedEvent"; id: string } + | { __typename: "Interface"; id: string } + | { __typename: "InterfaceDefinition"; id: string } + | { __typename: "InterfaceDefinitionTemplate"; id: string } + | { __typename: "InterfacePart"; id: string } + | { __typename: "InterfacePartTemplate"; id: string } + | { __typename: "InterfaceSpecification"; id: string } + | { __typename: "InterfaceSpecificationDerivationCondition"; id: string } + | { __typename: "InterfaceSpecificationTemplate"; id: string } + | { __typename: "InterfaceSpecificationVersion"; id: string } + | { __typename: "InterfaceSpecificationVersionTemplate"; id: string } + | { __typename: "InterfaceTemplate"; id: string } + | { __typename: "IntraComponentDependencyParticipant"; id: string } + | { __typename: "IntraComponentDependencySpecification"; id: string } + | { __typename: "Issue"; id: string } + | { __typename: "IssueComment"; id: string } + | { __typename: "IssuePriority"; id: string } + | { __typename: "IssueRelation"; id: string } + | { __typename: "IssueRelationType"; id: string } + | { __typename: "IssueState"; id: string } + | { __typename: "IssueTemplate"; id: string } + | { __typename: "IssueType"; id: string } + | { __typename: "Label"; id: string } + | { __typename: "OutgoingRelationTypeChangedEvent"; id: string } + | { __typename: "PriorityChangedEvent"; id: string } + | { __typename: "Project"; id: string } + | { __typename: "ProjectPermission"; id: string } + | { __typename: "RelatedByIssueEvent"; id: string } + | { __typename: "Relation"; id: string } + | { __typename: "RelationCondition"; id: string } + | { __typename: "RelationTemplate"; id: string } + | { __typename: "RemovedAffectedEntityEvent"; id: string } + | { __typename: "RemovedArtefactEvent"; id: string } + | { __typename: "RemovedAssignmentEvent"; id: string } + | { __typename: "RemovedFromPinnedIssuesEvent"; id: string } + | { __typename: "RemovedFromTrackableEvent"; id: string } + | { __typename: "RemovedIncomingRelationEvent"; id: string } + | { __typename: "RemovedLabelEvent"; id: string } + | { __typename: "RemovedOutgoingRelationEvent"; id: string } + | { __typename: "RemovedTemplatedFieldEvent"; id: string } + | { __typename: "StateChangedEvent"; id: string } + | { __typename: "StrokeStyle"; id: string } + | { __typename: "TemplateChangedEvent"; id: string } + | { __typename: "TemplatedFieldChangedEvent"; id: string } + | { __typename: "TitleChangedEvent"; id: string } + | { __typename: "TypeChangedEvent"; id: string } + | null; +}; export type GetImsUserDetailsQueryVariables = Exact<{ - imsUserId: Scalars['ID']['input']; + imsUserId: Scalars["ID"]["input"]; }>; - -export type GetImsUserDetailsQuery = { __typename?: 'Query', node?: { __typename?: 'AddedAffectedEntityEvent' } | { __typename?: 'AddedArtefactEvent' } | { __typename?: 'AddedLabelEvent' } | { __typename?: 'AddedToPinnedIssuesEvent' } | { __typename?: 'AddedToTrackableEvent' } | { __typename?: 'AggregatedIssue' } | { __typename?: 'AggregatedIssueRelation' } | { __typename?: 'Artefact' } | { __typename?: 'ArtefactTemplate' } | { __typename?: 'Assignment' } | { __typename?: 'AssignmentType' } | { __typename?: 'AssignmentTypeChangedEvent' } | { __typename?: 'Body' } | { __typename?: 'Component' } | { __typename?: 'ComponentPermission' } | { __typename?: 'ComponentTemplate' } | { __typename?: 'ComponentVersion' } | { __typename?: 'ComponentVersionTemplate' } | { __typename?: 'FillStyle' } | { __typename?: 'GlobalPermission' } | { __typename?: 'GropiusUser' } | { __typename?: 'IMS' } | { __typename?: 'IMSIssue' } | { __typename?: 'IMSIssueTemplate' } | { __typename?: 'IMSPermission' } | { __typename?: 'IMSProject' } | { __typename?: 'IMSProjectTemplate' } | { __typename?: 'IMSTemplate' } | { __typename: 'IMSUser', id: string, username?: string | null, displayName: string, email?: string | null, templatedFields: Array<{ __typename: 'JSONField', name: string, value?: any | null }>, ims: { __typename: 'IMS', id: string, name: string, description: string, templatedFields: Array<{ __typename: 'JSONField', name: string, value?: any | null }> } } | { __typename?: 'IMSUserTemplate' } | { __typename?: 'IncomingRelationTypeChangedEvent' } | { __typename?: 'Interface' } | { __typename?: 'InterfaceDefinition' } | { __typename?: 'InterfaceDefinitionTemplate' } | { __typename?: 'InterfacePart' } | { __typename?: 'InterfacePartTemplate' } | { __typename?: 'InterfaceSpecification' } | { __typename?: 'InterfaceSpecificationDerivationCondition' } | { __typename?: 'InterfaceSpecificationTemplate' } | { __typename?: 'InterfaceSpecificationVersion' } | { __typename?: 'InterfaceSpecificationVersionTemplate' } | { __typename?: 'InterfaceTemplate' } | { __typename?: 'IntraComponentDependencyParticipant' } | { __typename?: 'IntraComponentDependencySpecification' } | { __typename?: 'Issue' } | { __typename?: 'IssueComment' } | { __typename?: 'IssuePriority' } | { __typename?: 'IssueRelation' } | { __typename?: 'IssueRelationType' } | { __typename?: 'IssueState' } | { __typename?: 'IssueTemplate' } | { __typename?: 'IssueType' } | { __typename?: 'Label' } | { __typename?: 'OutgoingRelationTypeChangedEvent' } | { __typename?: 'PriorityChangedEvent' } | { __typename?: 'Project' } | { __typename?: 'ProjectPermission' } | { __typename?: 'RelatedByIssueEvent' } | { __typename?: 'Relation' } | { __typename?: 'RelationCondition' } | { __typename?: 'RelationTemplate' } | { __typename?: 'RemovedAffectedEntityEvent' } | { __typename?: 'RemovedArtefactEvent' } | { __typename?: 'RemovedAssignmentEvent' } | { __typename?: 'RemovedFromPinnedIssuesEvent' } | { __typename?: 'RemovedFromTrackableEvent' } | { __typename?: 'RemovedIncomingRelationEvent' } | { __typename?: 'RemovedLabelEvent' } | { __typename?: 'RemovedOutgoingRelationEvent' } | { __typename?: 'RemovedTemplatedFieldEvent' } | { __typename?: 'StateChangedEvent' } | { __typename?: 'StrokeStyle' } | { __typename?: 'TemplateChangedEvent' } | { __typename?: 'TemplatedFieldChangedEvent' } | { __typename?: 'TitleChangedEvent' } | { __typename?: 'TypeChangedEvent' } | null }; +export type GetImsUserDetailsQuery = { + __typename?: "Query"; + node?: + | { __typename?: "AddedAffectedEntityEvent" } + | { __typename?: "AddedArtefactEvent" } + | { __typename?: "AddedLabelEvent" } + | { __typename?: "AddedToPinnedIssuesEvent" } + | { __typename?: "AddedToTrackableEvent" } + | { __typename?: "AggregatedIssue" } + | { __typename?: "AggregatedIssueRelation" } + | { __typename?: "Artefact" } + | { __typename?: "ArtefactTemplate" } + | { __typename?: "Assignment" } + | { __typename?: "AssignmentType" } + | { __typename?: "AssignmentTypeChangedEvent" } + | { __typename?: "Body" } + | { __typename?: "Component" } + | { __typename?: "ComponentPermission" } + | { __typename?: "ComponentTemplate" } + | { __typename?: "ComponentVersion" } + | { __typename?: "ComponentVersionTemplate" } + | { __typename?: "FillStyle" } + | { __typename?: "GlobalPermission" } + | { __typename?: "GropiusUser" } + | { __typename?: "IMS" } + | { __typename?: "IMSIssue" } + | { __typename?: "IMSIssueTemplate" } + | { __typename?: "IMSPermission" } + | { __typename?: "IMSProject" } + | { __typename?: "IMSProjectTemplate" } + | { __typename?: "IMSTemplate" } + | { + __typename: "IMSUser"; + id: string; + username?: string | null; + displayName: string; + email?: string | null; + templatedFields: Array<{ __typename: "JSONField"; name: string; value?: any | null }>; + ims: { + __typename: "IMS"; + id: string; + name: string; + description: string; + templatedFields: Array<{ __typename: "JSONField"; name: string; value?: any | null }>; + }; + } + | { __typename?: "IMSUserTemplate" } + | { __typename?: "IncomingRelationTypeChangedEvent" } + | { __typename?: "Interface" } + | { __typename?: "InterfaceDefinition" } + | { __typename?: "InterfaceDefinitionTemplate" } + | { __typename?: "InterfacePart" } + | { __typename?: "InterfacePartTemplate" } + | { __typename?: "InterfaceSpecification" } + | { __typename?: "InterfaceSpecificationDerivationCondition" } + | { __typename?: "InterfaceSpecificationTemplate" } + | { __typename?: "InterfaceSpecificationVersion" } + | { __typename?: "InterfaceSpecificationVersionTemplate" } + | { __typename?: "InterfaceTemplate" } + | { __typename?: "IntraComponentDependencyParticipant" } + | { __typename?: "IntraComponentDependencySpecification" } + | { __typename?: "Issue" } + | { __typename?: "IssueComment" } + | { __typename?: "IssuePriority" } + | { __typename?: "IssueRelation" } + | { __typename?: "IssueRelationType" } + | { __typename?: "IssueState" } + | { __typename?: "IssueTemplate" } + | { __typename?: "IssueType" } + | { __typename?: "Label" } + | { __typename?: "OutgoingRelationTypeChangedEvent" } + | { __typename?: "PriorityChangedEvent" } + | { __typename?: "Project" } + | { __typename?: "ProjectPermission" } + | { __typename?: "RelatedByIssueEvent" } + | { __typename?: "Relation" } + | { __typename?: "RelationCondition" } + | { __typename?: "RelationTemplate" } + | { __typename?: "RemovedAffectedEntityEvent" } + | { __typename?: "RemovedArtefactEvent" } + | { __typename?: "RemovedAssignmentEvent" } + | { __typename?: "RemovedFromPinnedIssuesEvent" } + | { __typename?: "RemovedFromTrackableEvent" } + | { __typename?: "RemovedIncomingRelationEvent" } + | { __typename?: "RemovedLabelEvent" } + | { __typename?: "RemovedOutgoingRelationEvent" } + | { __typename?: "RemovedTemplatedFieldEvent" } + | { __typename?: "StateChangedEvent" } + | { __typename?: "StrokeStyle" } + | { __typename?: "TemplateChangedEvent" } + | { __typename?: "TemplatedFieldChangedEvent" } + | { __typename?: "TitleChangedEvent" } + | { __typename?: "TypeChangedEvent" } + | null; +}; export type GetImsUsersByTemplatedFieldValuesQueryVariables = Exact<{ - imsFilterInput: ImsFilterInput; - userFilterInput: ImsUserFilterInput; + imsFilterInput: ImsFilterInput; + userFilterInput: ImsUserFilterInput; }>; - -export type GetImsUsersByTemplatedFieldValuesQuery = { __typename?: 'Query', imss: { __typename: 'IMSConnection', nodes: Array<{ __typename: 'IMS', id: string, users: { __typename: 'IMSUserConnection', nodes: Array<{ __typename: 'IMSUser', id: string }> } }> } }; +export type GetImsUsersByTemplatedFieldValuesQuery = { + __typename?: "Query"; + imss: { + __typename: "IMSConnection"; + nodes: Array<{ + __typename: "IMS"; + id: string; + users: { __typename: "IMSUserConnection"; nodes: Array<{ __typename: "IMSUser"; id: string }> }; + }>; + }; +}; export type CreateNewImsUserInImsMutationVariables = Exact<{ - input: CreateImsUserInput; + input: CreateImsUserInput; }>; - -export type CreateNewImsUserInImsMutation = { __typename?: 'Mutation', createIMSUser: { __typename: 'CreateIMSUserPayload', imsUser: { __typename: 'IMSUser', id: string } } }; +export type CreateNewImsUserInImsMutation = { + __typename?: "Mutation"; + createIMSUser: { __typename: "CreateIMSUserPayload"; imsUser: { __typename: "IMSUser"; id: string } }; +}; export type GetBasicGropiusUserDataQueryVariables = Exact<{ - id: Scalars['ID']['input']; + id: Scalars["ID"]["input"]; }>; - -export type GetBasicGropiusUserDataQuery = { __typename?: 'Query', node?: { __typename?: 'AddedAffectedEntityEvent' } | { __typename?: 'AddedArtefactEvent' } | { __typename?: 'AddedLabelEvent' } | { __typename?: 'AddedToPinnedIssuesEvent' } | { __typename?: 'AddedToTrackableEvent' } | { __typename?: 'AggregatedIssue' } | { __typename?: 'AggregatedIssueRelation' } | { __typename?: 'Artefact' } | { __typename?: 'ArtefactTemplate' } | { __typename?: 'Assignment' } | { __typename?: 'AssignmentType' } | { __typename?: 'AssignmentTypeChangedEvent' } | { __typename?: 'Body' } | { __typename?: 'Component' } | { __typename?: 'ComponentPermission' } | { __typename?: 'ComponentTemplate' } | { __typename?: 'ComponentVersion' } | { __typename?: 'ComponentVersionTemplate' } | { __typename?: 'FillStyle' } | { __typename?: 'GlobalPermission' } | { __typename: 'GropiusUser', id: string, username: string, displayName: string, email?: string | null } | { __typename?: 'IMS' } | { __typename?: 'IMSIssue' } | { __typename?: 'IMSIssueTemplate' } | { __typename?: 'IMSPermission' } | { __typename?: 'IMSProject' } | { __typename?: 'IMSProjectTemplate' } | { __typename?: 'IMSTemplate' } | { __typename?: 'IMSUser' } | { __typename?: 'IMSUserTemplate' } | { __typename?: 'IncomingRelationTypeChangedEvent' } | { __typename?: 'Interface' } | { __typename?: 'InterfaceDefinition' } | { __typename?: 'InterfaceDefinitionTemplate' } | { __typename?: 'InterfacePart' } | { __typename?: 'InterfacePartTemplate' } | { __typename?: 'InterfaceSpecification' } | { __typename?: 'InterfaceSpecificationDerivationCondition' } | { __typename?: 'InterfaceSpecificationTemplate' } | { __typename?: 'InterfaceSpecificationVersion' } | { __typename?: 'InterfaceSpecificationVersionTemplate' } | { __typename?: 'InterfaceTemplate' } | { __typename?: 'IntraComponentDependencyParticipant' } | { __typename?: 'IntraComponentDependencySpecification' } | { __typename?: 'Issue' } | { __typename?: 'IssueComment' } | { __typename?: 'IssuePriority' } | { __typename?: 'IssueRelation' } | { __typename?: 'IssueRelationType' } | { __typename?: 'IssueState' } | { __typename?: 'IssueTemplate' } | { __typename?: 'IssueType' } | { __typename?: 'Label' } | { __typename?: 'OutgoingRelationTypeChangedEvent' } | { __typename?: 'PriorityChangedEvent' } | { __typename?: 'Project' } | { __typename?: 'ProjectPermission' } | { __typename?: 'RelatedByIssueEvent' } | { __typename?: 'Relation' } | { __typename?: 'RelationCondition' } | { __typename?: 'RelationTemplate' } | { __typename?: 'RemovedAffectedEntityEvent' } | { __typename?: 'RemovedArtefactEvent' } | { __typename?: 'RemovedAssignmentEvent' } | { __typename?: 'RemovedFromPinnedIssuesEvent' } | { __typename?: 'RemovedFromTrackableEvent' } | { __typename?: 'RemovedIncomingRelationEvent' } | { __typename?: 'RemovedLabelEvent' } | { __typename?: 'RemovedOutgoingRelationEvent' } | { __typename?: 'RemovedTemplatedFieldEvent' } | { __typename?: 'StateChangedEvent' } | { __typename?: 'StrokeStyle' } | { __typename?: 'TemplateChangedEvent' } | { __typename?: 'TemplatedFieldChangedEvent' } | { __typename?: 'TitleChangedEvent' } | { __typename?: 'TypeChangedEvent' } | null }; +export type GetBasicGropiusUserDataQuery = { + __typename?: "Query"; + node?: + | { __typename?: "AddedAffectedEntityEvent" } + | { __typename?: "AddedArtefactEvent" } + | { __typename?: "AddedLabelEvent" } + | { __typename?: "AddedToPinnedIssuesEvent" } + | { __typename?: "AddedToTrackableEvent" } + | { __typename?: "AggregatedIssue" } + | { __typename?: "AggregatedIssueRelation" } + | { __typename?: "Artefact" } + | { __typename?: "ArtefactTemplate" } + | { __typename?: "Assignment" } + | { __typename?: "AssignmentType" } + | { __typename?: "AssignmentTypeChangedEvent" } + | { __typename?: "Body" } + | { __typename?: "Component" } + | { __typename?: "ComponentPermission" } + | { __typename?: "ComponentTemplate" } + | { __typename?: "ComponentVersion" } + | { __typename?: "ComponentVersionTemplate" } + | { __typename?: "FillStyle" } + | { __typename?: "GlobalPermission" } + | { __typename: "GropiusUser"; id: string; username: string; displayName: string; email?: string | null } + | { __typename?: "IMS" } + | { __typename?: "IMSIssue" } + | { __typename?: "IMSIssueTemplate" } + | { __typename?: "IMSPermission" } + | { __typename?: "IMSProject" } + | { __typename?: "IMSProjectTemplate" } + | { __typename?: "IMSTemplate" } + | { __typename?: "IMSUser" } + | { __typename?: "IMSUserTemplate" } + | { __typename?: "IncomingRelationTypeChangedEvent" } + | { __typename?: "Interface" } + | { __typename?: "InterfaceDefinition" } + | { __typename?: "InterfaceDefinitionTemplate" } + | { __typename?: "InterfacePart" } + | { __typename?: "InterfacePartTemplate" } + | { __typename?: "InterfaceSpecification" } + | { __typename?: "InterfaceSpecificationDerivationCondition" } + | { __typename?: "InterfaceSpecificationTemplate" } + | { __typename?: "InterfaceSpecificationVersion" } + | { __typename?: "InterfaceSpecificationVersionTemplate" } + | { __typename?: "InterfaceTemplate" } + | { __typename?: "IntraComponentDependencyParticipant" } + | { __typename?: "IntraComponentDependencySpecification" } + | { __typename?: "Issue" } + | { __typename?: "IssueComment" } + | { __typename?: "IssuePriority" } + | { __typename?: "IssueRelation" } + | { __typename?: "IssueRelationType" } + | { __typename?: "IssueState" } + | { __typename?: "IssueTemplate" } + | { __typename?: "IssueType" } + | { __typename?: "Label" } + | { __typename?: "OutgoingRelationTypeChangedEvent" } + | { __typename?: "PriorityChangedEvent" } + | { __typename?: "Project" } + | { __typename?: "ProjectPermission" } + | { __typename?: "RelatedByIssueEvent" } + | { __typename?: "Relation" } + | { __typename?: "RelationCondition" } + | { __typename?: "RelationTemplate" } + | { __typename?: "RemovedAffectedEntityEvent" } + | { __typename?: "RemovedArtefactEvent" } + | { __typename?: "RemovedAssignmentEvent" } + | { __typename?: "RemovedFromPinnedIssuesEvent" } + | { __typename?: "RemovedFromTrackableEvent" } + | { __typename?: "RemovedIncomingRelationEvent" } + | { __typename?: "RemovedLabelEvent" } + | { __typename?: "RemovedOutgoingRelationEvent" } + | { __typename?: "RemovedTemplatedFieldEvent" } + | { __typename?: "StateChangedEvent" } + | { __typename?: "StrokeStyle" } + | { __typename?: "TemplateChangedEvent" } + | { __typename?: "TemplatedFieldChangedEvent" } + | { __typename?: "TitleChangedEvent" } + | { __typename?: "TypeChangedEvent" } + | null; +}; export type GetUserByNameQueryVariables = Exact<{ - username: Scalars['String']['input']; + username: Scalars["String"]["input"]; }>; - -export type GetUserByNameQuery = { __typename?: 'Query', gropiusUser: { __typename: 'GropiusUser', id: string, username: string, displayName: string, email?: string | null } }; +export type GetUserByNameQuery = { + __typename?: "Query"; + gropiusUser: { + __typename: "GropiusUser"; + id: string; + username: string; + displayName: string; + email?: string | null; + }; +}; export type CheckUserIsAdminQueryVariables = Exact<{ - id: Scalars['ID']['input']; + id: Scalars["ID"]["input"]; }>; - -export type CheckUserIsAdminQuery = { __typename?: 'Query', node?: { __typename: 'AddedAffectedEntityEvent' } | { __typename: 'AddedArtefactEvent' } | { __typename: 'AddedLabelEvent' } | { __typename: 'AddedToPinnedIssuesEvent' } | { __typename: 'AddedToTrackableEvent' } | { __typename: 'AggregatedIssue' } | { __typename: 'AggregatedIssueRelation' } | { __typename: 'Artefact' } | { __typename: 'ArtefactTemplate' } | { __typename: 'Assignment' } | { __typename: 'AssignmentType' } | { __typename: 'AssignmentTypeChangedEvent' } | { __typename: 'Body' } | { __typename: 'Component' } | { __typename: 'ComponentPermission' } | { __typename: 'ComponentTemplate' } | { __typename: 'ComponentVersion' } | { __typename: 'ComponentVersionTemplate' } | { __typename: 'FillStyle' } | { __typename: 'GlobalPermission' } | { __typename: 'GropiusUser', id: string, isAdmin: boolean } | { __typename: 'IMS' } | { __typename: 'IMSIssue' } | { __typename: 'IMSIssueTemplate' } | { __typename: 'IMSPermission' } | { __typename: 'IMSProject' } | { __typename: 'IMSProjectTemplate' } | { __typename: 'IMSTemplate' } | { __typename: 'IMSUser' } | { __typename: 'IMSUserTemplate' } | { __typename: 'IncomingRelationTypeChangedEvent' } | { __typename: 'Interface' } | { __typename: 'InterfaceDefinition' } | { __typename: 'InterfaceDefinitionTemplate' } | { __typename: 'InterfacePart' } | { __typename: 'InterfacePartTemplate' } | { __typename: 'InterfaceSpecification' } | { __typename: 'InterfaceSpecificationDerivationCondition' } | { __typename: 'InterfaceSpecificationTemplate' } | { __typename: 'InterfaceSpecificationVersion' } | { __typename: 'InterfaceSpecificationVersionTemplate' } | { __typename: 'InterfaceTemplate' } | { __typename: 'IntraComponentDependencyParticipant' } | { __typename: 'IntraComponentDependencySpecification' } | { __typename: 'Issue' } | { __typename: 'IssueComment' } | { __typename: 'IssuePriority' } | { __typename: 'IssueRelation' } | { __typename: 'IssueRelationType' } | { __typename: 'IssueState' } | { __typename: 'IssueTemplate' } | { __typename: 'IssueType' } | { __typename: 'Label' } | { __typename: 'OutgoingRelationTypeChangedEvent' } | { __typename: 'PriorityChangedEvent' } | { __typename: 'Project' } | { __typename: 'ProjectPermission' } | { __typename: 'RelatedByIssueEvent' } | { __typename: 'Relation' } | { __typename: 'RelationCondition' } | { __typename: 'RelationTemplate' } | { __typename: 'RemovedAffectedEntityEvent' } | { __typename: 'RemovedArtefactEvent' } | { __typename: 'RemovedAssignmentEvent' } | { __typename: 'RemovedFromPinnedIssuesEvent' } | { __typename: 'RemovedFromTrackableEvent' } | { __typename: 'RemovedIncomingRelationEvent' } | { __typename: 'RemovedLabelEvent' } | { __typename: 'RemovedOutgoingRelationEvent' } | { __typename: 'RemovedTemplatedFieldEvent' } | { __typename: 'StateChangedEvent' } | { __typename: 'StrokeStyle' } | { __typename: 'TemplateChangedEvent' } | { __typename: 'TemplatedFieldChangedEvent' } | { __typename: 'TitleChangedEvent' } | { __typename: 'TypeChangedEvent' } | null }; - -export type GetAllGrpiusUsersQueryVariables = Exact<{ [key: string]: never; }>; - - -export type GetAllGrpiusUsersQuery = { __typename?: 'Query', gropiusUserIds: Array }; +export type CheckUserIsAdminQuery = { + __typename?: "Query"; + node?: + | { __typename: "AddedAffectedEntityEvent" } + | { __typename: "AddedArtefactEvent" } + | { __typename: "AddedLabelEvent" } + | { __typename: "AddedToPinnedIssuesEvent" } + | { __typename: "AddedToTrackableEvent" } + | { __typename: "AggregatedIssue" } + | { __typename: "AggregatedIssueRelation" } + | { __typename: "Artefact" } + | { __typename: "ArtefactTemplate" } + | { __typename: "Assignment" } + | { __typename: "AssignmentType" } + | { __typename: "AssignmentTypeChangedEvent" } + | { __typename: "Body" } + | { __typename: "Component" } + | { __typename: "ComponentPermission" } + | { __typename: "ComponentTemplate" } + | { __typename: "ComponentVersion" } + | { __typename: "ComponentVersionTemplate" } + | { __typename: "FillStyle" } + | { __typename: "GlobalPermission" } + | { __typename: "GropiusUser"; id: string; isAdmin: boolean } + | { __typename: "IMS" } + | { __typename: "IMSIssue" } + | { __typename: "IMSIssueTemplate" } + | { __typename: "IMSPermission" } + | { __typename: "IMSProject" } + | { __typename: "IMSProjectTemplate" } + | { __typename: "IMSTemplate" } + | { __typename: "IMSUser" } + | { __typename: "IMSUserTemplate" } + | { __typename: "IncomingRelationTypeChangedEvent" } + | { __typename: "Interface" } + | { __typename: "InterfaceDefinition" } + | { __typename: "InterfaceDefinitionTemplate" } + | { __typename: "InterfacePart" } + | { __typename: "InterfacePartTemplate" } + | { __typename: "InterfaceSpecification" } + | { __typename: "InterfaceSpecificationDerivationCondition" } + | { __typename: "InterfaceSpecificationTemplate" } + | { __typename: "InterfaceSpecificationVersion" } + | { __typename: "InterfaceSpecificationVersionTemplate" } + | { __typename: "InterfaceTemplate" } + | { __typename: "IntraComponentDependencyParticipant" } + | { __typename: "IntraComponentDependencySpecification" } + | { __typename: "Issue" } + | { __typename: "IssueComment" } + | { __typename: "IssuePriority" } + | { __typename: "IssueRelation" } + | { __typename: "IssueRelationType" } + | { __typename: "IssueState" } + | { __typename: "IssueTemplate" } + | { __typename: "IssueType" } + | { __typename: "Label" } + | { __typename: "OutgoingRelationTypeChangedEvent" } + | { __typename: "PriorityChangedEvent" } + | { __typename: "Project" } + | { __typename: "ProjectPermission" } + | { __typename: "RelatedByIssueEvent" } + | { __typename: "Relation" } + | { __typename: "RelationCondition" } + | { __typename: "RelationTemplate" } + | { __typename: "RemovedAffectedEntityEvent" } + | { __typename: "RemovedArtefactEvent" } + | { __typename: "RemovedAssignmentEvent" } + | { __typename: "RemovedFromPinnedIssuesEvent" } + | { __typename: "RemovedFromTrackableEvent" } + | { __typename: "RemovedIncomingRelationEvent" } + | { __typename: "RemovedLabelEvent" } + | { __typename: "RemovedOutgoingRelationEvent" } + | { __typename: "RemovedTemplatedFieldEvent" } + | { __typename: "StateChangedEvent" } + | { __typename: "StrokeStyle" } + | { __typename: "TemplateChangedEvent" } + | { __typename: "TemplatedFieldChangedEvent" } + | { __typename: "TitleChangedEvent" } + | { __typename: "TypeChangedEvent" } + | null; +}; + +export type GetAllGrpiusUsersQueryVariables = Exact<{ [key: string]: never }>; + +export type GetAllGrpiusUsersQuery = { __typename?: "Query"; gropiusUserIds: Array }; export type CreateNewUserMutationVariables = Exact<{ - input: CreateGropiusUserInput; + input: CreateGropiusUserInput; }>; - -export type CreateNewUserMutation = { __typename?: 'Mutation', createGropiusUser: { __typename?: 'CreateGropiusUserPayload', gropiusUser: { __typename: 'GropiusUser', id: string, username: string, displayName: string, email?: string | null } } }; +export type CreateNewUserMutation = { + __typename?: "Mutation"; + createGropiusUser: { + __typename?: "CreateGropiusUserPayload"; + gropiusUser: { + __typename: "GropiusUser"; + id: string; + username: string; + displayName: string; + email?: string | null; + }; + }; +}; export type SetImsUserLinkMutationVariables = Exact<{ - gropiusUserId: Scalars['ID']['input']; - imsUserId: Scalars['ID']['input']; + gropiusUserId: Scalars["ID"]["input"]; + imsUserId: Scalars["ID"]["input"]; }>; +export type SetImsUserLinkMutation = { + __typename?: "Mutation"; + updateIMSUser: { __typename: "UpdateIMSUserPayload"; imsUser: { __typename: "IMSUser"; id: string } }; +}; -export type SetImsUserLinkMutation = { __typename?: 'Mutation', updateIMSUser: { __typename: 'UpdateIMSUserPayload', imsUser: { __typename: 'IMSUser', id: string } } }; - -export type UserDataFragment = { __typename: 'GropiusUser', id: string, username: string, displayName: string, email?: string | null }; +export type UserDataFragment = { + __typename: "GropiusUser"; + id: string; + username: string; + displayName: string; + email?: string | null; +}; export const ImsUserWithDetailFragmentDoc = gql` fragment ImsUserWithDetail on IMSUser { - __typename - id - username - displayName - email - templatedFields { - __typename - name - value - } - ims { - __typename - id - name - description - templatedFields { - __typename - name - value + __typename + id + username + displayName + email + templatedFields { + __typename + name + value + } + ims { + __typename + id + name + description + templatedFields { + __typename + name + value + } + } } - } -} - `; +`; export const UserDataFragmentDoc = gql` fragment UserData on GropiusUser { - __typename - id - username - displayName - email -} - `; + __typename + id + username + displayName + email + } +`; export const GetBasicImsUserDataDocument = gql` query getBasicImsUserData($imsUserId: ID!) { - node(id: $imsUserId) { - __typename - id - } -} - `; + node(id: $imsUserId) { + __typename + id + } + } +`; export const GetImsUserDetailsDocument = gql` query getImsUserDetails($imsUserId: ID!) { - node(id: $imsUserId) { - ...ImsUserWithDetail - } -} - ${ImsUserWithDetailFragmentDoc}`; + node(id: $imsUserId) { + ...ImsUserWithDetail + } + } + ${ImsUserWithDetailFragmentDoc} +`; export const GetImsUsersByTemplatedFieldValuesDocument = gql` query getImsUsersByTemplatedFieldValues($imsFilterInput: IMSFilterInput!, $userFilterInput: IMSUserFilterInput!) { - imss(filter: $imsFilterInput) { - __typename - nodes { - __typename - id - users(filter: $userFilterInput) { - __typename - nodes { - __typename - id + imss(filter: $imsFilterInput) { + __typename + nodes { + __typename + id + users(filter: $userFilterInput) { + __typename + nodes { + __typename + id + } + } + } } - } } - } -} - `; +`; export const CreateNewImsUserInImsDocument = gql` mutation createNewImsUserInIms($input: CreateIMSUserInput!) { - createIMSUser(input: $input) { - __typename - imsUser { - __typename - id + createIMSUser(input: $input) { + __typename + imsUser { + __typename + id + } + } } - } -} - `; +`; export const GetBasicGropiusUserDataDocument = gql` query getBasicGropiusUserData($id: ID!) { - node(id: $id) { - ...UserData - } -} - ${UserDataFragmentDoc}`; + node(id: $id) { + ...UserData + } + } + ${UserDataFragmentDoc} +`; export const GetUserByNameDocument = gql` query getUserByName($username: String!) { - gropiusUser(username: $username) { - ...UserData - } -} - ${UserDataFragmentDoc}`; + gropiusUser(username: $username) { + ...UserData + } + } + ${UserDataFragmentDoc} +`; export const CheckUserIsAdminDocument = gql` query checkUserIsAdmin($id: ID!) { - node(id: $id) { - __typename - ... on GropiusUser { - __typename - id - isAdmin + node(id: $id) { + __typename + ... on GropiusUser { + __typename + id + isAdmin + } + } } - } -} - `; +`; export const GetAllGrpiusUsersDocument = gql` query getAllGrpiusUsers { - gropiusUserIds -} - `; + gropiusUserIds + } +`; export const CreateNewUserDocument = gql` mutation createNewUser($input: CreateGropiusUserInput!) { - createGropiusUser(input: $input) { - gropiusUser { - ...UserData + createGropiusUser(input: $input) { + gropiusUser { + ...UserData + } + } } - } -} - ${UserDataFragmentDoc}`; + ${UserDataFragmentDoc} +`; export const SetImsUserLinkDocument = gql` mutation setImsUserLink($gropiusUserId: ID!, $imsUserId: ID!) { - updateIMSUser(input: {id: $imsUserId, gropiusUser: $gropiusUserId}) { - __typename - imsUser { - __typename - id + updateIMSUser(input: { id: $imsUserId, gropiusUser: $gropiusUserId }) { + __typename + imsUser { + __typename + id + } + } } - } -} - `; - -export type SdkFunctionWrapper = (action: (requestHeaders?:Record) => Promise, operationName: string, operationType?: string, variables?: any) => Promise; +`; +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 { - getBasicImsUserData(variables: GetBasicImsUserDataQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { - return withWrapper((wrappedRequestHeaders) => client.request(GetBasicImsUserDataDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getBasicImsUserData', 'query', variables); - }, - getImsUserDetails(variables: GetImsUserDetailsQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { - return withWrapper((wrappedRequestHeaders) => client.request(GetImsUserDetailsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getImsUserDetails', 'query', variables); - }, - getImsUsersByTemplatedFieldValues(variables: GetImsUsersByTemplatedFieldValuesQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { - return withWrapper((wrappedRequestHeaders) => client.request(GetImsUsersByTemplatedFieldValuesDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getImsUsersByTemplatedFieldValues', 'query', variables); - }, - createNewImsUserInIms(variables: CreateNewImsUserInImsMutationVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { - return withWrapper((wrappedRequestHeaders) => client.request(CreateNewImsUserInImsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'createNewImsUserInIms', 'mutation', variables); - }, - getBasicGropiusUserData(variables: GetBasicGropiusUserDataQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { - return withWrapper((wrappedRequestHeaders) => client.request(GetBasicGropiusUserDataDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getBasicGropiusUserData', 'query', variables); - }, - getUserByName(variables: GetUserByNameQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { - return withWrapper((wrappedRequestHeaders) => client.request(GetUserByNameDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getUserByName', 'query', variables); - }, - checkUserIsAdmin(variables: CheckUserIsAdminQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { - return withWrapper((wrappedRequestHeaders) => client.request(CheckUserIsAdminDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'checkUserIsAdmin', 'query', variables); - }, - getAllGrpiusUsers(variables?: GetAllGrpiusUsersQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { - return withWrapper((wrappedRequestHeaders) => client.request(GetAllGrpiusUsersDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getAllGrpiusUsers', 'query', variables); - }, - createNewUser(variables: CreateNewUserMutationVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { - return withWrapper((wrappedRequestHeaders) => client.request(CreateNewUserDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'createNewUser', 'mutation', variables); - }, - setImsUserLink(variables: SetImsUserLinkMutationVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { - return withWrapper((wrappedRequestHeaders) => client.request(SetImsUserLinkDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'setImsUserLink', 'mutation', variables); - } - }; + return { + getBasicImsUserData( + variables: GetBasicImsUserDataQueryVariables, + requestHeaders?: GraphQLClientRequestHeaders, + ): Promise { + return withWrapper( + (wrappedRequestHeaders) => + client.request(GetBasicImsUserDataDocument, variables, { + ...requestHeaders, + ...wrappedRequestHeaders, + }), + "getBasicImsUserData", + "query", + variables, + ); + }, + getImsUserDetails( + variables: GetImsUserDetailsQueryVariables, + requestHeaders?: GraphQLClientRequestHeaders, + ): Promise { + return withWrapper( + (wrappedRequestHeaders) => + client.request(GetImsUserDetailsDocument, variables, { + ...requestHeaders, + ...wrappedRequestHeaders, + }), + "getImsUserDetails", + "query", + variables, + ); + }, + getImsUsersByTemplatedFieldValues( + variables: GetImsUsersByTemplatedFieldValuesQueryVariables, + requestHeaders?: GraphQLClientRequestHeaders, + ): Promise { + return withWrapper( + (wrappedRequestHeaders) => + client.request( + GetImsUsersByTemplatedFieldValuesDocument, + variables, + { ...requestHeaders, ...wrappedRequestHeaders }, + ), + "getImsUsersByTemplatedFieldValues", + "query", + variables, + ); + }, + createNewImsUserInIms( + variables: CreateNewImsUserInImsMutationVariables, + requestHeaders?: GraphQLClientRequestHeaders, + ): Promise { + return withWrapper( + (wrappedRequestHeaders) => + client.request(CreateNewImsUserInImsDocument, variables, { + ...requestHeaders, + ...wrappedRequestHeaders, + }), + "createNewImsUserInIms", + "mutation", + variables, + ); + }, + getBasicGropiusUserData( + variables: GetBasicGropiusUserDataQueryVariables, + requestHeaders?: GraphQLClientRequestHeaders, + ): Promise { + return withWrapper( + (wrappedRequestHeaders) => + client.request(GetBasicGropiusUserDataDocument, variables, { + ...requestHeaders, + ...wrappedRequestHeaders, + }), + "getBasicGropiusUserData", + "query", + variables, + ); + }, + getUserByName( + variables: GetUserByNameQueryVariables, + requestHeaders?: GraphQLClientRequestHeaders, + ): Promise { + return withWrapper( + (wrappedRequestHeaders) => + client.request(GetUserByNameDocument, variables, { + ...requestHeaders, + ...wrappedRequestHeaders, + }), + "getUserByName", + "query", + variables, + ); + }, + checkUserIsAdmin( + variables: CheckUserIsAdminQueryVariables, + requestHeaders?: GraphQLClientRequestHeaders, + ): Promise { + return withWrapper( + (wrappedRequestHeaders) => + client.request(CheckUserIsAdminDocument, variables, { + ...requestHeaders, + ...wrappedRequestHeaders, + }), + "checkUserIsAdmin", + "query", + variables, + ); + }, + getAllGrpiusUsers( + variables?: GetAllGrpiusUsersQueryVariables, + requestHeaders?: GraphQLClientRequestHeaders, + ): Promise { + return withWrapper( + (wrappedRequestHeaders) => + client.request(GetAllGrpiusUsersDocument, variables, { + ...requestHeaders, + ...wrappedRequestHeaders, + }), + "getAllGrpiusUsers", + "query", + variables, + ); + }, + createNewUser( + variables: CreateNewUserMutationVariables, + requestHeaders?: GraphQLClientRequestHeaders, + ): Promise { + return withWrapper( + (wrappedRequestHeaders) => + client.request(CreateNewUserDocument, variables, { + ...requestHeaders, + ...wrappedRequestHeaders, + }), + "createNewUser", + "mutation", + variables, + ); + }, + setImsUserLink( + variables: SetImsUserLinkMutationVariables, + requestHeaders?: GraphQLClientRequestHeaders, + ): Promise { + return withWrapper( + (wrappedRequestHeaders) => + client.request(SetImsUserLinkDocument, variables, { + ...requestHeaders, + ...wrappedRequestHeaders, + }), + "setImsUserLink", + "mutation", + variables, + ); + }, + }; } -export type Sdk = ReturnType; \ No newline at end of file +export type Sdk = ReturnType; diff --git a/backend/src/strategies/Strategy.ts b/backend/src/strategies/Strategy.ts index 211d541f..2ffdb63a 100644 --- a/backend/src/strategies/Strategy.ts +++ b/backend/src/strategies/Strategy.ts @@ -29,7 +29,7 @@ export interface PerformAuthResult { /** * Base class for all strategies. - * + * * @param typeName The name of the strategy * @param strategyInstanceService The service to use for strategy instances * @param strategiesService The service to use for strategies @@ -224,7 +224,7 @@ export abstract class Strategy { /** * Gets a description of the login data, e.g. a username or email. - * + * * @param loginData The login data for which to get the description * @returns A description of the login data */ @@ -277,9 +277,9 @@ export abstract class Strategy { /** * Returns the instance config of the strategy instance, but with sensitive data censored. - * + * * **WARNING**: The result of this function WILL be exposed to the user. - * + * * @param instance The strategy instance for which to get the censored instance config * @returns The censored instance config */ @@ -290,7 +290,7 @@ export abstract class Strategy { /** * Handles an action that was triggered by the user. * Actions are defined via {@link updateActions}. - * + * * @param loginData the login data of the user that triggered the action * @param name the name of the action * @param data the data for the action diff --git a/backend/src/strategies/StrategyUsingPassport.ts b/backend/src/strategies/StrategyUsingPassport.ts index ee08a2d1..cb6a9bc0 100644 --- a/backend/src/strategies/StrategyUsingPassport.ts +++ b/backend/src/strategies/StrategyUsingPassport.ts @@ -49,7 +49,7 @@ export abstract class StrategyUsingPassport extends Strategy { res: any, ): Promise { return new Promise((resolve, reject) => { - const passportStrategy = this.createPassportStrategyInstance(strategyInstance) + const passportStrategy = this.createPassportStrategyInstance(strategyInstance); const jwtService = this.stateJwtService; passport.authenticate( passportStrategy, diff --git a/backend/src/strategies/github-token/github-token.service.ts b/backend/src/strategies/github-token/github-token.service.ts index 7bbee745..4f84bbd3 100644 --- a/backend/src/strategies/github-token/github-token.service.ts +++ b/backend/src/strategies/github-token/github-token.service.ts @@ -120,7 +120,10 @@ export class GithubTokenStrategyService extends Strategy { }; } - private async getUserData(token: string, strategyInstance: StrategyInstance): Promise<{ + private async getUserData( + token: string, + strategyInstance: StrategyInstance, + ): Promise<{ github_id: string; username: string; displayName: string; @@ -170,7 +173,7 @@ export class GithubTokenStrategyService extends Strategy { const userLoginData = await this.getUserData(token, strategyInstance); if (userLoginData == null) { - return { result: null, returnedState: {}, info: { message: "Token invalid" } } + return { result: null, returnedState: {}, info: { message: "Token invalid" } }; } return { diff --git a/backend/src/strategies/userpass/userpass.service.ts b/backend/src/strategies/userpass/userpass.service.ts index f3b453f0..3b2dc9ff 100644 --- a/backend/src/strategies/userpass/userpass.service.ts +++ b/backend/src/strategies/userpass/userpass.service.ts @@ -40,17 +40,19 @@ export class UserpassStrategyService extends StrategyUsingPassport { } override get updateActions(): StrategyUpdateAction[] { - return [{ - name: "update-password", - displayName: "Update password", - variables: [ - { - name: "password", - displayName: "Password", - type: "password", - }, - ], - }] + return [ + { + name: "update-password", + displayName: "Update password", + variables: [ + { + name: "password", + displayName: "Password", + type: "password", + }, + ], + }, + ]; } protected override checkAndExtendInstanceConfig(instanceConfig: object): object { diff --git a/backend/src/util/NeedsAdmin.ts b/backend/src/util/NeedsAdmin.ts index b7d8b781..c7d8f20b 100644 --- a/backend/src/util/NeedsAdmin.ts +++ b/backend/src/util/NeedsAdmin.ts @@ -1,3 +1,3 @@ import { SetMetadata } from "@nestjs/common"; -export const NeedsAdmin = () => SetMetadata("needsAdmin", true); \ No newline at end of file +export const NeedsAdmin = () => SetMetadata("needsAdmin", true); From ef1aaaa18e1d184be4f2a868947077700bf3234f Mon Sep 17 00:00:00 2001 From: nk-coding Date: Mon, 22 Jul 2024 14:14:13 +0200 Subject: [PATCH 22/31] backend lint, frontend format --- backend/package.json | 2 +- backend/src/api-sync/check-sync-secret.guard.ts | 2 +- backend/src/api-sync/sync-ims-user.controller.ts | 2 +- backend/src/app.module.ts | 1 - frontend/src/App.vue | 4 +--- frontend/src/router/navigationGuards.ts | 2 +- 6 files changed, 5 insertions(+), 8 deletions(-) diff --git a/backend/package.json b/backend/package.json index 38dedb43..98838a25 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,7 +12,7 @@ "start:dev": "cross-env NODE_ENV=development nest start --watch", "start:debug": "cross-env NODE_ENV=development nest start --debug --watch", "start:prod": "node dist/main", - "lint": "eslint \"{src}/**/*.ts\" --fix", + "lint": "eslint \"src/**/*.ts\" --fix", "generate-model": "graphql-codegen --config codegen.yml", "generate-migration": "npm run build && npx typeorm migration:generate -d ./dist/migrationDataSource.config.js ./src/database-migrations/migration", "init-database": "npm run build && npx typeorm migration:run -d ./dist/migrationDataSource.config.js" diff --git a/backend/src/api-sync/check-sync-secret.guard.ts b/backend/src/api-sync/check-sync-secret.guard.ts index bd660d55..b61b3789 100644 --- a/backend/src/api-sync/check-sync-secret.guard.ts +++ b/backend/src/api-sync/check-sync-secret.guard.ts @@ -1,4 +1,4 @@ -import { CanActivate, ExecutionContext, Injectable, SetMetadata, UnauthorizedException } from "@nestjs/common"; +import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common"; import { Request } from "express"; @Injectable() diff --git a/backend/src/api-sync/sync-ims-user.controller.ts b/backend/src/api-sync/sync-ims-user.controller.ts index 95e40105..92eb5222 100644 --- a/backend/src/api-sync/sync-ims-user.controller.ts +++ b/backend/src/api-sync/sync-ims-user.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, HttpException, HttpStatus, Logger, Param, Put, Query, UseGuards } from "@nestjs/common"; +import { Body, Controller, Get, HttpException, HttpStatus, Logger, Param, Put, UseGuards } from "@nestjs/common"; import { ImsUserFindingService } from "src/backend-services/ims-user-finding.service"; import { DefaultReturn } from "src/default-return.dto"; import { UserLoginDataImsUser } from "src/model/postgres/UserLoginDataImsUser.entity"; diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 31a67c98..16b3735f 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -9,7 +9,6 @@ import { StrategiesModule } from "./strategies/strategies.module"; import { BackendServicesModule } from "./backend-services/backend-services.module"; import { validationSchema } from "./configuration-validator"; import { ApiInternalModule } from "./api-internal/api-internal.module"; -import { DefaultReturn } from "./default-return.dto"; import { InitializationModule } from "./initialization/initialization.module"; import * as path from "path"; import { ServeStaticModule } from "@nestjs/serve-static"; diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 49aa7d63..30bcb623 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -2,9 +2,7 @@ - + - - - - -
-

Gropius-Login-Tester

- Note: To easily send manual requests using the URL and token specified below, open the developer console - (F12 in most browsers) and enter the request in the following structure (for example requesting data - suggestions):
- request("login/registration/data-suggestion", "POST", {"register_token": "token"}) -

Settings:

- - - - - - - - - - - - - - - - - - - -
Gropius login API endpoint:
Replace filled token fields
Access token:
Output responses to: - -
- -

Requests

- -

Show all existing strategies of type - -

-
    -
  • GET {{ loginUrl }}login/strategy/{{ showStrategyType }}
  • -
  • -
- -

Show all existing - - strategy instances -

-
    -
  • GET {{ loginUrl }}login/strategy/{{ showInstanceType }}/instance
  • -
  • -
- -

Log in using username and password

-
    -
  • Use strategy instance of type userpass with id - (request above)
  • -
  • Username: Password:
  • -
  • Mode: - -
  • -
  • Id of the client (leave empty if in dev or testing): -
  • Secret of the client (leave empy if none):
  • - -
  • POST {{ loginUrl }}authenticate/oauth/{{ userpassLoginInstanceId }}/token/{{ userpassLoginMode }} -
  • -
  • {
    - "grant_type": "password", - "username": "{{ userpassLoginUsername }}", - "password": "{{ userpassLoginPassword }}" - ,
    "client_id": "{{ oauthFlowClientId }}" - , "client_secret": "{{ oauthFlowClientSecret - }}"

    - } -
  • -
  • -
- -

Refresh token

-
    -
  • Refresh token:
  • -
  • Id of the client (leave empty if in dev or testing): -
  • Secret of the client (leave empy if none):
  • -
  • POST {{ loginUrl }}authenticate/oauth/this-does-not-matter/token
  • -
  • {
    - "grant_type": "refresh_token", - "refresh_token": "{{ refreshToken }}" - ,
    "client_id": "{{ oauthFlowClientId }}" - , "client_secret": "{{ oauthFlowClientSecret - }}"

    - } -
  • -
  • -
- -

List all gropius users (Check if you have admin access) -

-
    -
  • GET {{ loginUrl }}login/user
  • -
  • -
- -

- github strategy instance (Nedees admin) -

-
    -
  • - Id of instance to edit: -
  • -
  • GitHub OAuth App Settings: Client id:
    - Client secret:
  • -
  • Settings for this instance: - ; - ; - ; - -
  • -
  • Optional (if set, unique) name:
  • -
  • {{ createGithubInstanceMethod }} {{ loginUrl }}login/strategy/github/instance/{{ createGithubInstanceEditId }} -
  • -
  • {
    - "instanceConfig": { - "clientId": "{{ createGithubInstanceClientId }}", - "clientSecret": "{{ createGithubInstanceClientSecret }}" - },
    - "isLoginActive": {{ createGithubInstanceIsLoginActive }}, "isSelfRegisterActive": {{ - createGithubInstanceIsSelfRegisterActive }},
    - "isSyncActive": {{ createGithubInstanceIsSyncActive }}, "doesImplicitRegister": {{ - createGithubInstanceDoesImplicitRegister }},
    - "name": "{{ createGithubInstanceName }}"

    - } -
  • -
  • -
- -

- jira strategy instance (Nedees admin) -

-
    -
  • - Id of instance to edit: -
  • -
  • Jira OAuth App Settings: Client id:
    - Client secret:
  • -
  • Root URL:
    - Callback Root:
  • -
  • Settings for this instance: - ; - ; - ; - -
  • -
  • Optional (if set, unique) name:
  • -
  • {{ createJiraInstanceMethod }} {{ loginUrl }}login/strategy/jira/instance/{{ createJiraInstanceEditId }} -
  • -
  • {
    - "instanceConfig": { - "clientId": "{{ createJiraInstanceClientId }}", - "imsTemplatedFieldsFilter": {"root-url": "{{ createJiraInstanceRootURL }}"}, - "clientSecret": "{{ createJiraInstanceClientSecret }}", - "callbackRoot": "{{ createJiraInstanceCallbackRoot }}" - },
    - "isLoginActive": {{ createJiraInstanceIsLoginActive }}, "isSelfRegisterActive": {{ - createJiraInstanceIsSelfRegisterActive }},
    - "isSyncActive": {{ createJiraInstanceIsSyncActive }}, "doesImplicitRegister": {{ - createJiraInstanceDoesImplicitRegister }},
    - "name": "{{ createJiraInstanceName }}"

    - } -
  • -
  • -
- -

List all OAuth clients (Needs admin) -

-
    -
  • GET {{ loginUrl }}login/client
  • -
  • -
  • -
- -

- OAuth Client (Needs admin) -

-
    -
  • Id of OAuth client to edit:
  • -
  • Valid redirect URLs (separated by ";") Add - debug page URL
  • -
  • Settings for this client: - ; - -
  • -
  • Optional (if set, unique) name:
  • -
  • {{ createClientMethod }} {{ loginUrl }}login/client/{{ createClientEditId }} -
  • -
  • {
    - "redirectUrls": {{ '[' + createClientUrls.filter(url => url.length > 0).map(url => "\"" + url + - "\"").join(', ') + ']' }},
    - "isValid": {{ createClientIsValid }}, "requiresSecret": {{ createClientRequiresSecret }},
    - "name": "{{ createClientName }}"

    - } -
  • -
  • -
- -

- OAuth Client secret (Needs admin) -

-
    -
  • Id of OAuth client:
  • -
  • Fingerprint string of secret to delete:
  • -
  • {{ createClientSecretMethod }} {{ loginUrl }}login/client/{{ createClientEditId }}/clientSecret/{{ createClientSecretFingerprint }} -
  • -
  • -
- -

Run OAuth flow

-
    -
  • Jira-Strategy instance id to use: (request at the - top) -
  • -
  • Id of the client to initiate: (request above)
  • -
  • Start flow by choosing a mode and clicking initiate (opens in new tab; DON'T close that tab): -
      -
    • Mode: - -
    • -
    • Redirect user to: GET {{ loginUrl }}authenticate/oauth/{{ oauthFlowInstanceId }}/authorize/{{ - oauthFlowMode }}?client_id={{ oauthFlowClientId }}
    • -
    • -
    -
  • -
  • - Request an access token using the retrieved code:
      -
    • Code:
    • -
    • Secret of the client (leave empy if none):
    • -
    • POST {{ loginUrl }}authenticate/oauth/{{ oauthFlowInstanceId }}/token -
    • -
    • {
      - "grant_type": "authorization_code", "client_id": "{{ oauthFlowClientId }}", - "client_secret": "{{ oauthFlowClientSecret }}",
      - "code": "{{ oauthFlowAuthorizationCode }}" -
      } -
    • -
    • -
    -
  • -
- -

- - user with authentication -

-
    -
  • Registration token:
  • -
  • Data for new user: -
      -
    • - Username: - Display name: - Email: -
    • -
    • Request suggested user data and prefill fields above:
    • -
    • POST {{ loginUrl }}login/registration/data-suggestion
    • -
    • { "register_token": "{{ registerTokenValue }}" }
    • -
    • -
    -
  • -
  • - Id of gropius user to link to: -
  • -
  • POST {{ loginUrl }}login/registration/{{ registerType }}
  • -
  • { - - username: "{{ registerNewUsername }}", - displayName: "{{ registerNewDisplayName }}", - email: "{{ registerNewEmail }}",
    -
    - - userIdToLink: "{{ registerAdminLinkUserId }}", - - register_token: "{{ registerTokenValue }}" - } -
  • -
  • -
- - -
- - - - - \ No newline at end of file diff --git a/backend/static/login-debug/login-debug.js b/backend/static/login-debug/login-debug.js deleted file mode 100644 index 97fea3b0..00000000 --- a/backend/static/login-debug/login-debug.js +++ /dev/null @@ -1,200 +0,0 @@ -import { allMethods } from "./requests.js"; - -export default { - components: {}, - watch: { - hostname: { - handler(newVal, oldVal) { - this.loginUrl = this.hostname + "/"; - if (newVal != oldVal && newVal != window.location.origin) { - this.storeToStorage(); - } - }, - immediate: true, - }, - accessToken: { - handler(newVal) { - if (newVal != "") { - this.storeToStorage(); - } - }, - }, - refreshToken: { - handler(newVal) { - if (newVal != "") { - this.storeToStorage(); - } - }, - }, - }, - created() { - const currentUrl = new URL(window.location.href); - if (currentUrl.searchParams.has("code")) { - const oauthCode = currentUrl.searchParams.get("code"); - currentUrl.searchParams.delete("code"); - if (window.opener) { - window.opener.postMessage(oauthCode); - window.close(); - return; - } else { - oauthFlowAuthorizationCode = oauthCode; - window.history.replaceState(history.state, document.title, currentUrl); - } - } - - const stored = JSON.parse(localStorage.getItem("gropius-login-debug") || "{}"); - this.hostname = stored.host || this.hostname; - this.accessToken = stored.accessToken || ""; - this.refreshToken = stored.refreshToken || ""; - }, - mounted() { - window.addEventListener("message", this.onMessageReceived); - window.request = this.request.bind(this); - }, - methods: { - ...allMethods, - - createClientUrlsInput(e) { - this.createClientUrls = (e.target.value || "").split(";").map((url) => url.trim()); - }, - log(...data) { - data = data.filter((d) => !!d); - if (this.outputLocation.includes("console")) { - console.log(...data.map((d) => (typeof d == "string" ? d + "\n" : d))); - } - if (this.outputLocation.includes("textarea")) { - this.logData += - data.map((d) => (typeof d == "object" ? JSON.stringify(d, undefined, 4) : d)).join("\n") + "\n\n"; - } - if (this.outputLocation.includes("alert")) { - alert(data.map((d) => (typeof d == "object" ? JSON.stringify(d, undefined, 4) : d)).join("\n")); - } - }, - - jwtBodyParse(json) { - return JSON.parse(atob(json.access_token.split(".")[1])); - }, - - getAccessTokenStrInfo(json) { - if (json.access_token) { - try { - const decoded = this.jwtBodyParse(json); - return `Returned access token scope ${decoded.aud.join(",")} valid until ${new Date( - decoded.exp * 1000, - ).toISOString()}`; - } catch (e) { - console.warn("Error parsing jwt", e); - } - } - return undefined; - }, - - addDebugUrlToRedirect() { - if (!this.createClientUrls.includes(window.location.href)) { - this.createClientUrls.push(window.location.href); - } - }, - - async request(url, method = "GET", body = undefined, token = this.accessToken) { - if (url.startsWith("/")) { - url = url.substring(1); - } - let headers = {}; - if (body) { - headers = { ...headers, "content-type": "application/json" }; - } - if (token) { - headers = { ...headers, authorization: "Bearer " + token }; - } - const res = await fetch(this.loginUrl + url, { - headers, - method: method, - body: JSON.stringify(body), - }); - if (res.status <= 299 || res.status >= 200) { - if (res.headers.get("content-type").startsWith("application/json")) { - const json = await res.json(); - this.log(`${method} ${this.loginUrl}${url}`, json, this.getAccessTokenStrInfo(json)); - return json; - } else { - this.log(`${method} ${this.loginUrl}${url} did not return JSON?!`); - throw new Error(); - } - } else { - if (res.headers.get("content-type").startsWith("application/json")) { - this.log(`${method} ${this.loginUrl}${url} failed with code ${res.status}`, await res.json()); - throw new Error(); - } else { - this.log(`${method} ${this.loginUrl}${url} failed with code ${res.status}`, res.statusText); - throw new Error(); - } - } - }, - }, - data() { - return { - hostname: window.location.origin, - loginUrl: "", - replacePrefilled: true, - accessToken: "", - refreshToken: "", - outputLocation: "console", - logData: "", - - showStrategyType: "", - - showInstanceType: "userpass", - - userpassLoginInstanceId: "", - userpassLoginUsername: "", - userpassLoginPassword: "", - userpassLoginMode: "login", - - createGithubInstanceMethod: "POST", - createGithubInstanceEditId: "", - createGithubInstanceClientId: "", - createGithubInstanceClientSecret: "", - createGithubInstanceIsLoginActive: false, - createGithubInstanceIsSelfRegisterActive: false, - createGithubInstanceIsSyncActive: false, - createGithubInstanceDoesImplicitRegister: false, - createGithubInstanceName: "", - - createJiraInstanceMethod: "POST", - createJiraInstanceEditId: "", - createJiraInstanceClientId: "", - createJiraInstanceClientSecret: "", - createJiraInstanceIsLoginActive: false, - createJiraInstanceIsSelfRegisterActive: false, - createJiraInstanceIsSyncActive: false, - createJiraInstanceDoesImplicitRegister: false, - createJiraInstanceName: "", - createJiraInstanceRootURL: "", - createJiraInstanceCallbackRoot: "", - - createClientMethod: "PUT", - createClientEditId: "", - createClientUrls: [window.location.href], - createClientIsValid: true, - createClientRequiresSecret: false, - createClientName: "", - - createClientSecretMethod: "POST", - createClientSecretFingerprint: "", - - oauthFlowInstanceId: "", - oauthFlowClientId: "", - oauthFlowMode: "login", - oauthFlowAuthorizationCode: "", - oauthFlowClientSecret: "", - openedWindows: [], - - registerType: "self-register", - registerTokenValue: "", - registerNewUsername: "", - registerNewDisplayName: "", - registerNewEmail: "", - registerAdminLinkUserId: "", - }; - }, -}; diff --git a/backend/static/login-debug/requests.js b/backend/static/login-debug/requests.js deleted file mode 100644 index 8e6d5e75..00000000 --- a/backend/static/login-debug/requests.js +++ /dev/null @@ -1,245 +0,0 @@ -export async function runShowStrategy() { - const r = await this.request(`login/strategy/${this.showStrategyType}`); - if (r.length <= 0) { - return; - } -} - -export async function runShowInstance() { - const r = await this.request(`login/strategy/${this.showInstanceType}/instance`); - if (r.length <= 0) { - return; - } - if (r[0].type == "userpass") { - this.userpassLoginInstanceId = r[0].id; - } else if (r[0].type == "github") { - this.createGithubInstanceEditId = r[0].id; - this.oauthFlowInstanceId = r[0].id; - this.createGithubInstanceIsLoginActive = r[0].isLoginActive; - this.createGithubInstanceIsSelfRegisterActive = r[0].isSelfRegisterActive; - this.createGithubInstanceIsSyncActive = r[0].isSyncActive; - this.createGithubInstanceDoesImplicitRegister = r[0].doesImplicitRegister; - } if (r[0].type == "jira") { - this.createJiraInstanceEditId = r[0].id; - this.oauthFlowInstanceId = r[0].id; - this.createJiraInstanceIsLoginActive = r[0].isLoginActive; - this.createJiraInstanceIsSelfRegisterActive = r[0].isSelfRegisterActive; - this.createJiraInstanceIsSyncActive = r[0].isSyncActive; - this.createJiraInstanceDoesImplicitRegister = r[0].doesImplicitRegister; - } -} - -export async function runUserpassLogin() { - const r = await this.request( - `authenticate/oauth/${this.userpassLoginInstanceId}/token/${this.userpassLoginMode}`, - "POST", - { - grant_type: "password", - username: this.userpassLoginUsername, - password: this.userpassLoginPassword, - client_id: this.oauthFlowClientId || undefined, - client_secret: this.oauthFlowClientSecret || undefined, - }, - ); - if (r.access_token) { - const token = this.jwtBodyParse(r); - if (token.aud.includes("login-register")) { - this.registerTokenValue = r.access_token; - } - if (token.aud.includes("login")) { - this.accessToken = this.replacePrefilled ? r.access_token : this.accessToken || r.access_token; - this.refreshToken = this.replacePrefilled ? r.refresh_token : this.refreshToken || r.refresh_token; - this.log("Successfully logged in using userpass."); - } - } -} - -export async function runRefreshToken() { - const r = await this.request(`authenticate/oauth/a/token`, "POST", { - grant_type: "refresh_token", - refresh_token: this.refreshToken, - client_id: this.oauthFlowClientId || undefined, - client_secret: this.oauthFlowClientSecret || undefined, - }); - if (!r.error && r.access_token) { - const token = this.jwtBodyParse(r); - if (token.aud.includes("login-register")) { - this.registerTokenValue = r.access_token; - } - if (token.aud.includes("login")) { - this.accessToken = r.access_token; - this.refreshToken = r.refresh_token; - } - } -} - -export async function runListAllUsers() { - await this.request(`login/user`); -} - -export async function runCreateGithubInstance() { - const r = await this.request( - `login/strategy/github/instance${ - this.createGithubInstanceMethod == "PUT" ? "/" + this.createGithubInstanceEditId : "" - }`, - this.createGithubInstanceMethod, - { - instanceConfig: { - clientId: this.createGithubInstanceClientId, - clientSecret: this.createGithubInstanceClientSecret, - }, - isLoginActive: this.createGithubInstanceIsLoginActive, - isSelfRegisterActive: this.createGithubInstanceIsSelfRegisterActive, - isSyncActive: this.createGithubInstanceIsSyncActive, - doesImplicitRegister: this.createGithubInstanceDoesImplicitRegister, - name: this.createGithubInstanceName || undefined, - }, - ); - this.createGithubInstanceEditId = r.id; - this.oauthFlowInstanceId = r.id; -} - -export async function runCreateJiraInstance() { - const r = await this.request( - `login/strategy/jira/instance${ - this.createJiraInstanceMethod == "PUT" ? "/" + this.createJiraInstanceEditId : "" - }`, - this.createJiraInstanceMethod, - { - instanceConfig: { - clientId: this.createJiraInstanceClientId, - clientSecret: this.createJiraInstanceClientSecret, - callbackRoot: this.createJiraInstanceCallbackRoot, - imsTemplatedFieldsFilter: {"root-url": this.createJiraInstanceRootURL}, - }, - isLoginActive: this.createJiraInstanceIsLoginActive, - isSelfRegisterActive: this.createJiraInstanceIsSelfRegisterActive, - isSyncActive: this.createJiraInstanceIsSyncActive, - doesImplicitRegister: this.createJiraInstanceDoesImplicitRegister, - name: this.createJiraInstanceName || undefined, - }, - ); - this.createJiraInstanceEditId = r.id; - this.oauthFlowInstanceId = r.id; -} - -export async function runListAllClients() { - const r = await this.request(`login/client`); - const client = r.filter((c) => !c.requiresSecret)[0] || r[0]; - this.oauthFlowClientId = client.id; - this.createClientEditId = client.id; - this.createClientUrls = client.redirectUrls; - this.createClientIsValid = client.isValid; - this.createClientRequiresSecret = client.requiresSecret; -} - -export async function runCreateClient() { - const r = await this.request( - `login/client${this.createClientMethod == "PUT" ? "/" + this.createClientEditId : ""}`, - this.createClientMethod, - { - redirectUrls: this.createClientUrls, - isValid: this.createClientIsValid, - requiresSecret: this.createClientRequiresSecret, - name: this.createClientName || undefined, - }, - ); - this.createClientEditId = r.id; - this.oauthFlowClientId = r.id; -} - -export async function oauthFlowInitiate() { - for (const tab of this.openedWindows) { - if (!tab.closed) { - tab.close(); - } - } - this.openedWindows.splice(0, this.openedWindows.length); - const oauthTab = window.open( - //eslint-disable-next-line max-len - `${this.loginUrl}authenticate/oauth/${this.oauthFlowInstanceId}/authorize/${this.oauthFlowMode}?client_id=${this.oauthFlowClientId}`, - "_blank", - ); - this.openedWindows.push(oauthTab); -} - -export function onMessageReceived(e) { - const code = e.data; - if (typeof code != "string" || code.split(".").length != 3) { - return; - } - this.log(`Received code from OAuth flow:`, e.data); - this.oauthFlowAuthorizationCode = e.data; -} - -export async function oauthFlowGetToken() { - const r = await this.request(`authenticate/oauth/${this.oauthFlowClientId}/token`, "POST", { - grant_type: "authorization_code", - code: this.oauthFlowAuthorizationCode, - client_id: this.oauthFlowClientId, - client_secret: this.oauthFlowClientSecret || undefined, - }); - if (r.access_token) { - const token = this.jwtBodyParse(r); - if (token.aud.includes("login-register")) { - this.registerTokenValue = r.access_token; - } - if (token.aud.includes("login")) { - this.accessToken = this.replacePrefilled ? r.access_token : this.accessToken || r.access_token; - this.refreshToken = this.replacePrefilled ? r.refresh_token : this.refreshToken || r.refresh_token; - this.log("Successfully logged in using OAuth."); - } - } -} - -export async function runRegisterDataSuggestion() { - const r = await this.request(`login/registration/data-suggestion`, "POST", { - register_token: this.registerTokenValue, - }); - this.registerNewUsername = r.username || ""; - this.registerNewDisplayName = r.displayName || ""; - this.registerNewEmail = r.email || ""; -} - -export async function runRegister() { - let body = { - register_token: this.registerTokenValue, - }; - if (this.registerType == "self-register") { - body.username = this.registerNewUsername; - body.displayName = this.registerNewDisplayName; - body.email = this.registerNewEmail; - } else if (this.registerType == "admin-link") { - body.userIdToLink = this.registerAdminLinkUserId; - } - await this.request(`login/registration/${this.registerType}`, "POST", body); -} - -export function storeToStorage() { - localStorage.setItem( - "gropius-login-debug", - JSON.stringify({ - host: this.hostname, - accessToken: this.accessToken || "", - refreshToken: this.refreshToken, - }), - ); -} - -export const allMethods = { - runShowStrategy, - runShowInstance, - runUserpassLogin, - runRefreshToken, - runListAllUsers, - runCreateGithubInstance, - runCreateJiraInstance, - runListAllClients, - runCreateClient, - oauthFlowInitiate, - onMessageReceived, - oauthFlowGetToken, - runRegisterDataSuggestion, - runRegister, - storeToStorage, -}; From b95010079b5a03649440f7c32179d816f2a769d1 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Tue, 30 Jul 2024 18:00:47 +0200 Subject: [PATCH 30/31] document strange token logic --- backend/src/api-internal/auth-redirect.middleware.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/api-internal/auth-redirect.middleware.ts b/backend/src/api-internal/auth-redirect.middleware.ts index 1ba64609..6ad845aa 100644 --- a/backend/src/api-internal/auth-redirect.middleware.ts +++ b/backend/src/api-internal/auth-redirect.middleware.ts @@ -61,6 +61,7 @@ export class AuthRedirectMiddleware extends StateMiddleware< if (!state.activeLogin.isValid) { throw new Error("Active login invalid"); } + // if the login service handles the registration, two tokens were already generated: the code and the access token if ( state.activeLogin.nextExpectedRefreshTokenNumber != ActiveLogin.LOGGED_IN_BUT_TOKEN_NOT_YET_RETRIVED + (state.secondToken ? 2 : 0) From d129d62f9a5e45a63b006ba7fdd3f579df6266b0 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Fri, 2 Aug 2024 02:52:53 +0200 Subject: [PATCH 31/31] support client credentials flow --- .../auth/check-registration-token.service.ts | 2 - .../src/api-login/auth/register.controller.ts | 3 - backend/src/api-oauth/api-oauth.module.ts | 5 +- .../api-oauth/dto/oauth-token-response.dto.ts | 2 +- ...uth-token-authorization-code.middleware.ts | 131 ++++++++++++++- ...uth-token-client-credentials.middleware.ts | 56 +++++++ .../src/api-oauth/oauth-token.controller.ts | 150 +----------------- .../src/api-oauth/oauth-token.middleware.ts | 57 ++++--- .../src/model/services/auth-client.service.ts | 6 +- 9 files changed, 222 insertions(+), 190 deletions(-) create mode 100644 backend/src/api-oauth/oauth-token-client-credentials.middleware.ts diff --git a/backend/src/api-login/auth/check-registration-token.service.ts b/backend/src/api-login/auth/check-registration-token.service.ts index 317d3900..1980cb9e 100644 --- a/backend/src/api-login/auth/check-registration-token.service.ts +++ b/backend/src/api-login/auth/check-registration-token.service.ts @@ -4,7 +4,6 @@ import { ActiveLogin } from "src/model/postgres/ActiveLogin.entity"; import { LoginUser } from "src/model/postgres/LoginUser.entity"; import { LoginState, UserLoginData } from "src/model/postgres/UserLoginData.entity"; import { ActiveLoginService } from "src/model/services/active-login.service"; -import { UserLoginDataService } from "src/model/services/user-login-data.service"; /** * Service to validate a registration token and retrieve the referenced nodes @@ -14,7 +13,6 @@ export class CheckRegistrationTokenService { private readonly logger = new Logger(CheckRegistrationTokenService.name); constructor( private readonly tokenService: TokenService, - private readonly loginDataService: UserLoginDataService, private readonly activeLoginService: ActiveLoginService, ) {} diff --git a/backend/src/api-login/auth/register.controller.ts b/backend/src/api-login/auth/register.controller.ts index 2890bb68..e2028e11 100644 --- a/backend/src/api-login/auth/register.controller.ts +++ b/backend/src/api-login/auth/register.controller.ts @@ -58,7 +58,6 @@ export class RegisterController { @Res({ passthrough: true }) res: Response, ): Promise { RegistrationTokenInput.check(input); - //todo: potentially move to POST user/:id/loginData if (!(res.locals.state as ApiStateData).loggedInUser) { throw new HttpException("Not logged in", HttpStatus.UNAUTHORIZED); } @@ -105,8 +104,6 @@ export class RegisterController { @Body() input: AdminLinkUserInput, @Res({ passthrough: true }) res: Response, ): Promise { - // requires: admin and specification of user id to link with - //todo: potentially move to POST user/:id/loginData AdminLinkUserInput.check(input); const linkToUser = await this.userService.findOneBy({ id: input.userIdToLink, diff --git a/backend/src/api-oauth/api-oauth.module.ts b/backend/src/api-oauth/api-oauth.module.ts index af793ac6..72847700 100644 --- a/backend/src/api-oauth/api-oauth.module.ts +++ b/backend/src/api-oauth/api-oauth.module.ts @@ -12,6 +12,7 @@ import { OAuthErrorRedirectMiddleware } from "./oauth-error-redirect.middleware" import { BackendServicesModule } from "src/backend-services/backend-services.module"; import { StrategiesModule } from "src/strategies/strategies.module"; import { EncryptionService } from "./encryption.service"; +import { OAuthTokenClientCredentialsMiddleware } from "./oauth-token-client-credentials.middleware"; @Module({ imports: [ModelModule, BackendServicesModule, StrategiesModule], @@ -21,6 +22,7 @@ import { EncryptionService } from "./encryption.service"; OAuthAuthorizeRedirectMiddleware, OAuthTokenMiddleware, OAuthTokenAuthorizationCodeMiddleware, + OAuthTokenClientCredentialsMiddleware, ErrorHandlerMiddleware, OAuthErrorRedirectMiddleware, EncryptionService, @@ -36,7 +38,6 @@ export class ApiOauthModule { private readonly oauthAuthorizeValidate: OAuthAuthorizeValidateMiddleware, private readonly oauthAuthorizeRedirect: OAuthAuthorizeRedirectMiddleware, private readonly oauthToken: OAuthTokenMiddleware, - private readonly oauthTokenAuthorizationCode: OAuthTokenAuthorizationCodeMiddleware, private readonly errorHandler: ErrorHandlerMiddleware, private readonly oauthErrorRedirect: OAuthErrorRedirectMiddleware, ) { @@ -52,7 +53,7 @@ export class ApiOauthModule { }); this.middlewares.push({ - middlewares: [this.oauthToken, this.oauthTokenAuthorizationCode, this.errorHandler], + middlewares: [this.oauthToken, this.errorHandler], path: "auth/oauth/token", }); } diff --git a/backend/src/api-oauth/dto/oauth-token-response.dto.ts b/backend/src/api-oauth/dto/oauth-token-response.dto.ts index 2542b9ec..4541074d 100644 --- a/backend/src/api-oauth/dto/oauth-token-response.dto.ts +++ b/backend/src/api-oauth/dto/oauth-token-response.dto.ts @@ -2,6 +2,6 @@ export class OAuthTokenResponseDto { access_token: string; token_type: "bearer"; expires_in: number; - refresh_token: string; + refresh_token?: string; scope: string; } \ No newline at end of file diff --git a/backend/src/api-oauth/oauth-token-authorization-code.middleware.ts b/backend/src/api-oauth/oauth-token-authorization-code.middleware.ts index 3a9029ff..282a4669 100644 --- a/backend/src/api-oauth/oauth-token-authorization-code.middleware.ts +++ b/backend/src/api-oauth/oauth-token-authorization-code.middleware.ts @@ -3,16 +3,15 @@ import { Request, Response } from "express"; import { ActiveLoginTokenResult, TokenScope, TokenService } from "src/backend-services/token.service"; import { AuthClient } from "src/model/postgres/AuthClient.entity"; import { ActiveLoginService } from "src/model/services/active-login.service"; -import { AuthStateServerData } from "src/strategies/AuthResult"; import { OAuthHttpException } from "./OAuthHttpException"; import { StateMiddleware } from "./StateMiddleware"; import { EncryptionService } from "./encryption.service"; +import { LoginState, UserLoginData } from "src/model/postgres/UserLoginData.entity"; +import { ActiveLogin } from "src/model/postgres/ActiveLogin.entity"; +import { OAuthTokenResponseDto } from "./dto/oauth-token-response.dto"; @Injectable() -export class OAuthTokenAuthorizationCodeMiddleware extends StateMiddleware< - { client: AuthClient }, - AuthStateServerData & { scope: TokenScope[] } -> { +export class OAuthTokenAuthorizationCodeMiddleware extends StateMiddleware<{ client: AuthClient }> { private readonly logger = new Logger(OAuthTokenAuthorizationCodeMiddleware.name); constructor( private readonly activeLoginService: ActiveLoginService, @@ -26,6 +25,123 @@ export class OAuthTokenAuthorizationCodeMiddleware extends StateMiddleware< throw new OAuthHttpException("invalid_grant", "Given code was invalid or expired"); } + private async checkLoginDataIsVaild(loginData?: UserLoginData, activeLogin?: ActiveLogin) { + if (!loginData) { + this.logger.warn("Login data not found"); + throw new OAuthHttpException("invalid_grant", "No login found for given grant (refresh token/code)"); + } + if (loginData.expires != null && loginData.expires <= new Date()) { + this.logger.warn("Login data has expired", loginData); + throw new OAuthHttpException( + "invalid_grant", + "Login has expired. Try restarting login/register/link process.", + ); + } + switch (loginData.state) { + case LoginState.VALID: + if (!(await loginData.user)) { + throw new OAuthHttpException("invalid_state", "No user for valid login"); + } + break; + case LoginState.WAITING_FOR_REGISTER: + if (await loginData.user) { + throw new OAuthHttpException( + "invalid_state", + "Login still in register state but user already existing", + ); + } + break; + default: + throw new OAuthHttpException( + "invalid_grant", + "Login for given grant is not valid any more; Please re-login", + ); + } + if (!activeLogin) { + this.logger.warn("Active login not found"); + throw new OAuthHttpException("invalid_grant", "No login found for given grant (refresh token/code)"); + } + if (activeLogin.expires != null && activeLogin.expires <= new Date()) { + this.logger.warn("Active login has expired", activeLogin.id); + throw new OAuthHttpException( + "invalid_grant", + "Login has expired. Try restarting login/register/link process.", + ); + } + if (!activeLogin.isValid) { + this.logger.warn("Active login is set invalid", activeLogin.id); + throw new OAuthHttpException("invalid_grant", "Login is set invalid/disabled"); + } + } + + private async updateRefreshTokenIdAndExpirationDate( + activeLogin: ActiveLogin, + isRegisterLogin: boolean, + ): Promise { + const loginExpiresIn = parseInt(process.env.GROPIUS_REGULAR_LOGINS_INACTIVE_EXPIRATION_TIME_MS, 10); + this.logger.debug("Updating active login", isRegisterLogin, loginExpiresIn, activeLogin.supportsSync); + if (!isRegisterLogin) { + activeLogin = await this.activeLoginService.setActiveLoginExpiration(activeLogin); + } + activeLogin.nextExpectedRefreshTokenNumber++; + return await this.activeLoginService.save(activeLogin); + } + + private async createAccessToken( + loginData: UserLoginData, + activeLogin: ActiveLogin, + currentClient: AuthClient, + scope: TokenScope[], + ): Promise { + const tokenExpiresInMs: number = parseInt(process.env.GROPIUS_ACCESS_TOKEN_EXPIRATION_TIME_MS, 10); + + let accessToken: string; + if (loginData.state == LoginState.WAITING_FOR_REGISTER) { + accessToken = await this.tokenService.signRegistrationToken(activeLogin.id, tokenExpiresInMs); + } else { + accessToken = await this.tokenService.signAccessToken(await loginData.user, scope, tokenExpiresInMs); + } + + activeLogin = await this.updateRefreshTokenIdAndExpirationDate( + activeLogin, + loginData.state == LoginState.WAITING_FOR_REGISTER, + ); + + const refreshToken = + loginData.state != LoginState.WAITING_FOR_REGISTER + ? await this.tokenService.signActiveLoginCode( + activeLogin.id, + currentClient.id, + activeLogin.nextExpectedRefreshTokenNumber, + scope, + activeLogin.expires ?? undefined, + undefined, + ) + : undefined; + return { + access_token: accessToken, + token_type: "bearer", + expires_in: Math.floor(tokenExpiresInMs / 1000), + refresh_token: refreshToken, + scope: scope.join(" "), + }; + } + + private async createResponse( + client: AuthClient, + scope: TokenScope[], + activeLogin: ActiveLogin, + ): Promise { + for (const requestedScope of scope) { + if (!client.validScopes.includes(requestedScope)) { + throw new OAuthHttpException("invalid_scope", "Requested scope not valid for client"); + } + } + const loginData = await activeLogin.loginInstanceFor; + await this.checkLoginDataIsVaild(loginData, activeLogin); + return await this.createAccessToken(loginData, activeLogin, client, scope); + } + protected override async useWithState( req: Request, res: Response, @@ -72,7 +188,6 @@ export class OAuthTokenAuthorizationCodeMiddleware extends StateMiddleware< ); throw new OAuthHttpException("invalid_grant", "Given code was liekely reused. Login and codes invalidated"); } - console.log(tokenData); if (tokenData.codeChallenge != undefined) { if (codeVerifier == undefined) { this.logger.warn("Code verifier missing"); @@ -90,7 +205,7 @@ export class OAuthTokenAuthorizationCodeMiddleware extends StateMiddleware< throw new OAuthHttpException("invalid_request", "Code verifier not required"); } } - this.appendState(res, { activeLogin, scope: tokenData.scope }); - next(); + const response = await this.createResponse(currentClient, tokenData.scope, activeLogin); + res.json(response); } } diff --git a/backend/src/api-oauth/oauth-token-client-credentials.middleware.ts b/backend/src/api-oauth/oauth-token-client-credentials.middleware.ts new file mode 100644 index 00000000..5bde013e --- /dev/null +++ b/backend/src/api-oauth/oauth-token-client-credentials.middleware.ts @@ -0,0 +1,56 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { StateMiddleware } from "./StateMiddleware"; +import { AuthClient } from "src/model/postgres/AuthClient.entity"; +import { Request, Response } from "express"; +import { TokenScope, TokenService } from "src/backend-services/token.service"; +import { OAuthTokenResponseDto } from "./dto/oauth-token-response.dto"; +import { OAuthHttpException } from "./OAuthHttpException"; + +@Injectable() +export class OAuthTokenClientCredentialsMiddleware extends StateMiddleware<{ client: AuthClient }> { + private readonly logger = new Logger(OAuthTokenClientCredentialsMiddleware.name); + constructor(private readonly tokenService: TokenService) { + super(); + } + + private async createAccessToken(currentClient: AuthClient, scope: TokenScope[]): Promise { + const tokenExpiresInMs: number = parseInt(process.env.GROPIUS_ACCESS_TOKEN_EXPIRATION_TIME_MS, 10); + const user = await currentClient.clientCredentialFlowUser; + if (!user) { + throw new OAuthHttpException("invalid_client", "Client does not support client credentials flow"); + } + + const accessToken = await this.tokenService.signAccessToken(user, scope, tokenExpiresInMs); + + return { + access_token: accessToken, + token_type: "bearer", + expires_in: Math.floor(tokenExpiresInMs / 1000), + scope: scope.join(" "), + }; + } + + private async createResponse(client: AuthClient, scope: TokenScope[]): Promise { + for (const requestedScope of scope) { + if (!client.validScopes.includes(requestedScope)) { + throw new OAuthHttpException("invalid_scope", "Requested scope not valid for client"); + } + } + return await this.createAccessToken(client, scope); + } + + protected async useWithState( + req: Request, + res: Response, + state: { client: AuthClient } & { error?: any }, + next: (error?: Error | any) => void, + ): Promise { + const currentClient = state.client; + if (!currentClient.requiresSecret) { + throw new OAuthHttpException("invalid_client", "Client does not support client credentials flow"); + } + const scope = req.body.scope?.split(" ") ?? currentClient.validScopes; + const response = await this.createResponse(currentClient, scope); + res.json(response); + } +} diff --git a/backend/src/api-oauth/oauth-token.controller.ts b/backend/src/api-oauth/oauth-token.controller.ts index b28c1a6a..9167a651 100644 --- a/backend/src/api-oauth/oauth-token.controller.ts +++ b/backend/src/api-oauth/oauth-token.controller.ts @@ -1,157 +1,19 @@ -import { Controller, Logger, Post, Res } from "@nestjs/common"; +import { Controller, HttpException, HttpStatus, Post, Res } from "@nestjs/common"; import { ApiOkResponse, ApiOperation, ApiTags } from "@nestjs/swagger"; -import { Response } from "express"; -import { TokenScope, TokenService } from "src/backend-services/token.service"; -import { ActiveLogin } from "src/model/postgres/ActiveLogin.entity"; -import { AuthClient } from "src/model/postgres/AuthClient.entity"; -import { LoginState, UserLoginData } from "src/model/postgres/UserLoginData.entity"; -import { ActiveLoginService } from "src/model/services/active-login.service"; -import { AuthClientService } from "src/model/services/auth-client.service"; import { OpenApiTag } from "src/openapi-tag"; -import { AuthStateServerData } from "src/strategies/AuthResult"; -import { ensureState } from "src/util/ensureState"; -import { OAuthHttpException } from "../api-oauth/OAuthHttpException"; import { OAuthTokenResponseDto } from "./dto/oauth-token-response.dto"; @Controller() @ApiTags(OpenApiTag.OAUTH_API) export class OAuthTokenController { - private readonly logger = new Logger(OAuthTokenController.name); - constructor( - private readonly authClientService: AuthClientService, - private readonly activeLoginService: ActiveLoginService, - private readonly tokenService: TokenService, - ) {} - - private async checkLoginDataIsVaild(loginData?: UserLoginData, activeLogin?: ActiveLogin) { - if (!loginData) { - this.logger.warn("Login data not found"); - throw new OAuthHttpException("invalid_grant", "No login found for given grant (refresh token/code)"); - } - if (loginData.expires != null && loginData.expires <= new Date()) { - this.logger.warn("Login data has expired", loginData); - throw new OAuthHttpException( - "invalid_grant", - "Login has expired. Try restarting login/register/link process.", - ); - } - switch (loginData.state) { - case LoginState.VALID: - if (!(await loginData.user)) { - throw new OAuthHttpException("invalid_state", "No user for valid login"); - } - break; - case LoginState.WAITING_FOR_REGISTER: - if (await loginData.user) { - throw new OAuthHttpException( - "invalid_state", - "Login still in register state but user already existing", - ); - } - break; - default: - throw new OAuthHttpException( - "invalid_grant", - "Login for given grant is not valid any more; Please re-login", - ); - } - if (!activeLogin) { - this.logger.warn("Active login not found"); - throw new OAuthHttpException("invalid_grant", "No login found for given grant (refresh token/code)"); - } - if (activeLogin.expires != null && activeLogin.expires <= new Date()) { - this.logger.warn("Active login has expired", activeLogin.id); - throw new OAuthHttpException( - "invalid_grant", - "Login has expired. Try restarting login/register/link process.", - ); - } - if (!activeLogin.isValid) { - this.logger.warn("Active login is set invalid", activeLogin.id); - throw new OAuthHttpException("invalid_grant", "Login is set invalid/disabled"); - } - } - - private async updateRefreshTokenIdAndExpirationDate( - activeLogin: ActiveLogin, - isRegisterLogin: boolean, - ): Promise { - const loginExpiresIn = parseInt(process.env.GROPIUS_REGULAR_LOGINS_INACTIVE_EXPIRATION_TIME_MS, 10); - this.logger.debug("Updating active login", isRegisterLogin, loginExpiresIn, activeLogin.supportsSync); - if (!isRegisterLogin) { - activeLogin = await this.activeLoginService.setActiveLoginExpiration(activeLogin); - } - activeLogin.nextExpectedRefreshTokenNumber++; - return await this.activeLoginService.save(activeLogin); - } - - private async createAccessToken( - loginData: UserLoginData, - activeLogin: ActiveLogin, - currentClient: AuthClient, - scope: TokenScope[], - ): Promise { - const tokenExpiresInMs: number = parseInt(process.env.GROPIUS_ACCESS_TOKEN_EXPIRATION_TIME_MS, 10); - - let accessToken: string; - if (loginData.state == LoginState.WAITING_FOR_REGISTER) { - accessToken = await this.tokenService.signRegistrationToken(activeLogin.id, tokenExpiresInMs); - } else { - accessToken = await this.tokenService.signAccessToken(await loginData.user, scope, tokenExpiresInMs); - } - - activeLogin = await this.updateRefreshTokenIdAndExpirationDate( - activeLogin, - loginData.state == LoginState.WAITING_FOR_REGISTER, - ); - - const refreshToken = - loginData.state != LoginState.WAITING_FOR_REGISTER - ? await this.tokenService.signActiveLoginCode( - activeLogin.id, - currentClient.id, - activeLogin.nextExpectedRefreshTokenNumber, - scope, - activeLogin.expires ?? undefined, - undefined, - ) - : undefined; - return { - access_token: accessToken, - token_type: "bearer", - expires_in: Math.floor(tokenExpiresInMs / 1000), - refresh_token: refreshToken, - scope: scope.join(" "), - }; - } @Post("token") @ApiOperation({ summary: "Token OAuth Endpoint" }) @ApiOkResponse({ type: OAuthTokenResponseDto }) - async token(@Res({ passthrough: true }) res: Response): Promise { - ensureState(res); - const currentClient = res.locals.state.client as AuthClient; - const scope = res.locals.state.scope as TokenScope[]; - if (!currentClient) { - throw new OAuthHttpException( - "invalid_client", - "No client id/authentication given or authentication invalid", - ); - } - for (const requestedScope of scope) { - if (!currentClient.validScopes.includes(requestedScope)) { - console.log(requestedScope, currentClient.validScopes); - throw new OAuthHttpException("invalid_scope", "Requested scope not valid for client"); - } - } - let activeLogin = (res.locals.state as AuthStateServerData)?.activeLogin; - if (typeof activeLogin == "string") { - activeLogin = await this.activeLoginService.findOneByOrFail({ - id: activeLogin, - }); - } - const loginData = await activeLogin.loginInstanceFor; - await this.checkLoginDataIsVaild(loginData, activeLogin); - return await this.createAccessToken(loginData, activeLogin, currentClient, scope); + async token(): Promise { + throw new HttpException( + "This controller shouldn't be reached as all functionality is handeled in middleware", + HttpStatus.INTERNAL_SERVER_ERROR, + ); } } diff --git a/backend/src/api-oauth/oauth-token.middleware.ts b/backend/src/api-oauth/oauth-token.middleware.ts index 68d5ba7e..d3bc7c40 100644 --- a/backend/src/api-oauth/oauth-token.middleware.ts +++ b/backend/src/api-oauth/oauth-token.middleware.ts @@ -1,16 +1,22 @@ -import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common"; +import { Injectable, Logger } from "@nestjs/common"; import { Request, Response } from "express"; import { AuthClient } from "src/model/postgres/AuthClient.entity"; import { AuthClientService } from "src/model/services/auth-client.service"; import * as bcrypt from "bcrypt"; import { OAuthHttpException } from "./OAuthHttpException"; import { StateMiddleware } from "./StateMiddleware"; +import { OAuthTokenAuthorizationCodeMiddleware } from "./oauth-token-authorization-code.middleware"; +import { OAuthTokenClientCredentialsMiddleware } from "./oauth-token-client-credentials.middleware"; @Injectable() export class OauthTokenMiddleware extends StateMiddleware<{}, { client: AuthClient }> { private readonly logger = new Logger(OauthTokenMiddleware.name); - constructor(private readonly authClientService: AuthClientService) { + constructor( + private readonly authClientService: AuthClientService, + private readonly oauthTokenAuthorizationCodeMiddleware: OAuthTokenAuthorizationCodeMiddleware, + private readonly oauthTokenClientCredentialsMiddleware: OAuthTokenClientCredentialsMiddleware, + ) { super(); } @@ -41,6 +47,9 @@ export class OauthTokenMiddleware extends StateMiddleware<{}, { client: AuthClie * or `null` if credentials invalid or none given */ private async getCallingClient(req: Request): Promise { + let clientId: string; + let clientSecret: string | undefined; + const auth_head = req.headers["authorization"]; if (auth_head && auth_head.startsWith("Basic ")) { const clientIdSecret = Buffer.from(auth_head.substring(6), "base64") @@ -49,24 +58,21 @@ export class OauthTokenMiddleware extends StateMiddleware<{}, { client: AuthClie ?.map((text) => decodeURIComponent(text)); if (clientIdSecret && clientIdSecret.length == 2) { - const client = await this.authClientService.findAuthClient(clientIdSecret[0]); - if (client && client.isValid) { - if (this.checkGivenClientSecretValidOrNotRequired(client, clientIdSecret[1])) { - return client; - } - } - return null; + clientId = clientIdSecret[0]; + clientSecret = clientIdSecret[1]; } } if (req.body.client_id) { - const client = await this.authClientService.findAuthClient(req.body.client_id); - if (client && client.isValid) { - if (this.checkGivenClientSecretValidOrNotRequired(client, req.body.client_secret)) { - return client; - } + clientId = req.body.client_id; + clientSecret = req.body.client_secret; + } + + const client = await this.authClientService.findAuthClient(clientId); + if (client && client.isValid) { + if (await this.checkGivenClientSecretValidOrNotRequired(client, clientSecret)) { + return client; } - return null; } return null; @@ -87,21 +93,14 @@ export class OauthTokenMiddleware extends StateMiddleware<{}, { client: AuthClie this.appendState(res, { client }); switch (grant_type) { - case "refresh_token": //Request for new token using refresh token - //Fallthrough as resfresh token works the same as the initial code (both used to obtain new access token) - case "authorization_code": //Request for token based on obtained code - next(); - break; - case "password": // Deprecated => not supported - case "client_credentials": //Request for token for stuff on client => not supported + case "refresh_token": + return this.oauthTokenAuthorizationCodeMiddleware.use(req, res, next); + case "authorization_code": + return this.oauthTokenAuthorizationCodeMiddleware.use(req, res, next); + case "client_credentials": + return this.oauthTokenClientCredentialsMiddleware.use(req, res, next); default: - throw new HttpException( - { - error: "unsupported_grant_type", - error_description: "No grant_type given or unsupported type", - }, - HttpStatus.BAD_REQUEST, - ); + throw new OAuthHttpException("unsupported_grant_type", "No grant_type given or unsupported type"); } } } diff --git a/backend/src/model/services/auth-client.service.ts b/backend/src/model/services/auth-client.service.ts index 82a67b18..48dca4e9 100644 --- a/backend/src/model/services/auth-client.service.ts +++ b/backend/src/model/services/auth-client.service.ts @@ -42,7 +42,11 @@ export class AuthClientService extends Repository { if (defaultClient) { return defaultClient; } else { - return this.findOneBy({ id }); + try { + return await this.findOneBy({ id }); + } catch { + return undefined; + } } } }