diff --git a/README.md b/README.md index d92e0646..0ff91cdf 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ The following steps must be carried out once: | pgAdmin (docker) | [localhost:5051](http://localhost:5051/) | .env `$PGADMIN_EMAIL` |.env `$PGADMIN_PASSWORD` | | MinIO (docker) | [localhost:9001](http://localhost:9001/) | .env `$STORAGE_USER` |.env `$STORAGE_PASSWORD` | | smtp4dev (docker) | [localhost:5000](http://localhost:5000/) | n/a | n/a | +| oidc-server (docker) | [localhost:4011](http://localhost:4011/) | n/a | n/a | ### Creating elastic-search index diff --git a/apps/client-asset-sg/project.json b/apps/client-asset-sg/project.json index dceecf5a..b99fba3a 100644 --- a/apps/client-asset-sg/project.json +++ b/apps/client-asset-sg/project.json @@ -1,93 +1,120 @@ { - "name": "client-asset-sg", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "projectType": "application", - "sourceRoot": "apps/client-asset-sg/src", - "prefix": "asset-sg", - "targets": { - "build": { - "executor": "@angular-devkit/build-angular:browser", - "outputs": ["{options.outputPath}"], - "options": { - "outputPath": "dist/apps/client-asset-sg", - "index": "apps/client-asset-sg/src/index.html", - "main": "apps/client-asset-sg/src/main.ts", - "polyfills": ["zone.js"], - "tsConfig": "apps/client-asset-sg/tsconfig.app.json", - "inlineStyleLanguage": "scss", - "assets": ["apps/client-asset-sg/src/favicon.ico", "apps/client-asset-sg/src/assets"], - "styles": ["apps/client-asset-sg/src/styles.scss"], - "scripts": [] + "name": "client-asset-sg", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "application", + "sourceRoot": "apps/client-asset-sg/src", + "prefix": "asset-sg", + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "outputs": [ + "{options.outputPath}" + ], + "options": { + "outputPath": "dist/apps/client-asset-sg", + "index": "apps/client-asset-sg/src/index.html", + "main": "apps/client-asset-sg/src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "apps/client-asset-sg/tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + "apps/client-asset-sg/src/favicon.ico", + "apps/client-asset-sg/src/assets" + ], + "styles": [ + "apps/client-asset-sg/src/styles.scss" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "1.2mb", + "maximumError": "1.3mb" }, - "configurations": { - "production": { - "budgets": [ - { - "type": "initial", - "maximumWarning": "1.2mb", - "maximumError": "1.3mb" - }, - { - "type": "anyComponentStyle", - "maximumWarning": "2kb", - "maximumError": "4kb" - } - ], - "outputHashing": "all", - "fileReplacements": [ - { - "replace": "apps/client-asset-sg/src/environments/environment.ts", - "with": "apps/client-asset-sg/src/environments/environment.prod.ts" - } - ] - }, - "development": { - "buildOptimizer": false, - "optimization": false, - "vendorChunk": true, - "extractLicenses": false, - "sourceMap": true, - "namedChunks": true - } - }, - "defaultConfiguration": "production" - }, - "serve": { - "executor": "@angular-devkit/build-angular:dev-server", - "configurations": { - "production": { - "browserTarget": "client-asset-sg:build:production" - }, - "development": { - "browserTarget": "client-asset-sg:build:development" - } - }, - "defaultConfiguration": "development", - "options": { - "proxyConfig": "apps/client-asset-sg/proxy.conf.json" + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" } - }, - "extract-i18n": { - "executor": "@angular-devkit/build-angular:extract-i18n", - "options": { - "browserTarget": "client-asset-sg:build" + ], + "outputHashing": "all", + "fileReplacements": [ + { + "replace": "apps/client-asset-sg/src/environments/environment.ts", + "with": "apps/client-asset-sg/src/environments/environment.prod.ts" } + ] }, - "lint": { - "executor": "@nrwl/linter:eslint", - "outputs": ["{options.outputFile}"], - "options": { - "lintFilePatterns": ["apps/client-asset-sg/**/*.ts", "apps/client-asset-sg/**/*.html"] + "int": { + "fileReplacements": [ + { + "replace": "apps/client-asset-sg/src/environments/environment.ts", + "with": "apps/client-asset-sg/src/environments/environment.int.ts" } + ] }, - "test": { - "executor": "@nrwl/jest:jest", - "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], - "options": { - "jestConfig": "apps/client-asset-sg/jest.config.ts", - "passWithNoTests": true - } + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "client-asset-sg:build:production" + }, + "int": { + "browserTarget": "client-asset-sg:build:int" + }, + "development": { + "browserTarget": "client-asset-sg:build:development" + } + }, + "defaultConfiguration": "development", + "options": { + "proxyConfig": "apps/client-asset-sg/proxy.conf.json" + } + }, + "extract-i18n": { + "executor": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "client-asset-sg:build" + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": [ + "{options.outputFile}" + ], + "options": { + "lintFilePatterns": [ + "apps/client-asset-sg/**/*.ts", + "apps/client-asset-sg/**/*.html" + ] + } }, - "tags": [] + "test": { + "executor": "@nrwl/jest:jest", + "outputs": [ + "{workspaceRoot}/coverage/{projectRoot}" + ], + "options": { + "jestConfig": "apps/client-asset-sg/jest.config.ts", + "passWithNoTests": true + } + } + }, + "tags": [] } diff --git a/apps/client-asset-sg/src/app/app.component.html b/apps/client-asset-sg/src/app/app.component.html index 51447d42..f559a448 100644 --- a/apps/client-asset-sg/src/app/app.component.html +++ b/apps/client-asset-sg/src/app/app.component.html @@ -1,10 +1,10 @@ - +
- +
- +
diff --git a/apps/client-asset-sg/src/app/app.component.ts b/apps/client-asset-sg/src/app/app.component.ts index 7ec1d29a..baf9fa64 100644 --- a/apps/client-asset-sg/src/app/app.component.ts +++ b/apps/client-asset-sg/src/app/app.component.ts @@ -1,11 +1,15 @@ +import { HttpClient } from '@angular/common/http'; import { Component, inject } from '@angular/core'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { Store } from '@ngrx/store'; import { WINDOW } from 'ngx-window-token'; -import { debounceTime, fromEvent, startWith } from 'rxjs'; +import { debounceTime, fromEvent, map, startWith } from 'rxjs'; import { assert } from 'tsafe'; -import { AppPortalService, setCssCustomProperties } from '@asset-sg/client-shared'; -import { FavouriteService } from '@asset-sg/favourite'; +import { AuthService } from '@asset-sg/auth'; +import { AppPortalService, appSharedStateActions, setCssCustomProperties } from '@asset-sg/client-shared'; + +import { AppState } from './state/app-state'; const fullHdWidth = 1920; @@ -17,18 +21,38 @@ const fullHdWidth = 1920; }) export class AppComponent { private _wndw = inject(WINDOW); - + private _httpClient = inject(HttpClient); public appPortalService = inject(AppPortalService); - private _favouriteService = inject(FavouriteService); + private store = inject(Store); + + constructor(private readonly _authService: AuthService) { + this._httpClient + .get('api/oauth-config/config') + .pipe( + map((response: any) => { + return response; + }), + ) + .subscribe(async oAuthConfig => { + await this._authService.configureOAuth( + oAuthConfig.oauth_issuer, + oAuthConfig.oauth_clientId, + oAuthConfig.oauth_scope, + oAuthConfig.oauth_showDebugInformation, + oAuthConfig.oauth_tokenEndpoint, + ); + + this.store.dispatch(appSharedStateActions.loadUserProfile()); + this.store.dispatch(appSharedStateActions.loadReferenceData()); + }); - constructor() { const wndw = this._wndw; assert(wndw != null); fromEvent(wndw, 'resize') .pipe(debounceTime(50), startWith(null), untilDestroyed(this)) .subscribe(() => { - let fontSize = '1rem'; + let fontSize; const width = window.innerWidth; if (width >= fullHdWidth) { fontSize = '1rem'; diff --git a/apps/client-asset-sg/src/app/app.module.ts b/apps/client-asset-sg/src/app/app.module.ts index 2cfdf690..53b6b0fe 100644 --- a/apps/client-asset-sg/src/app/app.module.ts +++ b/apps/client-asset-sg/src/app/app.module.ts @@ -19,7 +19,7 @@ import { PushModule } from '@rx-angular/template/push'; import * as O from 'fp-ts/Option'; import * as C from 'io-ts/Codec'; -import { AuthInterceptor } from '@asset-sg/auth'; +import { AuthInterceptor, AuthModule } from '@asset-sg/auth'; import { AnchorComponent, ButtonComponent, @@ -104,6 +104,7 @@ registerLocaleData(locale_deCH, 'de-CH'); ButtonComponent, DialogModule, A11yModule, + AuthModule, ], providers: [ provideSvgIcons(icons), @@ -115,6 +116,7 @@ registerLocaleData(locale_deCH, 'de-CH'); }) export class AppModule { private _translateService = inject(TranslateService); + constructor() { this._translateService.setDefaultLang('de'); } @@ -123,9 +125,11 @@ export class AppModule { export interface Encoder { readonly encode: (a: A) => O; } + function optionFromNullable(encoder: Encoder): Encoder> { return { encode: O.fold(() => null, encoder.encode), }; } + const foooobar = optionFromNullable(C.string); diff --git a/apps/client-asset-sg/src/app/components/not-found/not-found.component.ts b/apps/client-asset-sg/src/app/components/not-found/not-found.component.ts index ecf8d128..0088f5a9 100644 --- a/apps/client-asset-sg/src/app/components/not-found/not-found.component.ts +++ b/apps/client-asset-sg/src/app/components/not-found/not-found.component.ts @@ -2,6 +2,6 @@ import { Component } from '@angular/core'; @Component({ selector: 'asset-sg-not-found', - template: `
Not Found
`, + template: `
Not Found
`, }) export class NotFoundComponent {} diff --git a/apps/client-asset-sg/src/app/components/redirect-to-lang/redirect-to-lang.component.ts b/apps/client-asset-sg/src/app/components/redirect-to-lang/redirect-to-lang.component.ts index 320d2c24..0a193649 100644 --- a/apps/client-asset-sg/src/app/components/redirect-to-lang/redirect-to-lang.component.ts +++ b/apps/client-asset-sg/src/app/components/redirect-to-lang/redirect-to-lang.component.ts @@ -5,15 +5,16 @@ import * as E from 'fp-ts/Either'; import * as D from 'io-ts/Decoder'; import queryString from 'query-string'; +import { AuthService } from '@asset-sg/auth'; import { Lang } from '@asset-sg/shared'; @UntilDestroy() @Component({ selector: 'asset-sg-redirect-to-lang', - template: 'redirect', + template: 'Redirect to main page...', }) export class RedirectToLangComponent { - constructor(route: ActivatedRoute, router: Router) { + constructor(route: ActivatedRoute, router: Router, authService: AuthService) { route.queryParams.pipe(untilDestroyed(this)).subscribe(params => { const paramsDecoded = D.struct({ lang: Lang, @@ -25,6 +26,10 @@ export class RedirectToLangComponent { const { lang, ...rest } = query; const newUrl = `/${paramsDecoded.right.lang}${url}?${queryString.stringify(rest)}`; router.navigateByUrl(newUrl); + } else if (!authService.isLoggedIn()) { + setTimeout(() => { + router.navigate(['/de']); + }, 500); } else { router.navigate(['/de']); } diff --git a/apps/client-asset-sg/src/environments/environment.int.ts b/apps/client-asset-sg/src/environments/environment.int.ts new file mode 100644 index 00000000..453523f3 --- /dev/null +++ b/apps/client-asset-sg/src/environments/environment.int.ts @@ -0,0 +1,5 @@ +import { CompileTimeEnvironment } from './environment-type'; + +export const environment: CompileTimeEnvironment = { + ngrxStoreLoggerEnabled: false, +}; diff --git a/apps/server-asset-sg/src/app/admin/admin.controller.ts b/apps/server-asset-sg/src/app/admin/admin.controller.ts index b322f8e9..5d0ab92c 100644 --- a/apps/server-asset-sg/src/app/admin/admin.controller.ts +++ b/apps/server-asset-sg/src/app/admin/admin.controller.ts @@ -1,11 +1,11 @@ -import { Body, Controller, Delete, Get, HttpCode, HttpException, Param, Patch, Post, Req } from '@nestjs/common'; +import { Body, Controller, Delete, Get, HttpCode, HttpException, Param, Patch, Req } from '@nestjs/common'; import * as E from 'fp-ts/Either'; import { pipe } from 'fp-ts/function'; import * as TE from 'fp-ts/TaskEither'; import * as D from 'io-ts/Decoder'; import { decodeError, serializeError } from '@asset-sg/core'; -import { UserPatch, UserPost } from '@asset-sg/shared'; +import { UserPatch } from '@asset-sg/shared'; import { AuthenticatedRequest } from '../models/request'; @@ -45,25 +45,6 @@ export class AdminController { } } - @Post('user') - @HttpCode(201) - async createUser(@Req() req: AuthenticatedRequest, @Body() body: UserPost) { - const e = await pipe( - UserPost.decode(body), - E.mapLeft(decodeError), - TE.fromEither, - TE.chainW(user => this.adminService.createUser(req.accessToken, user)), - )(); - if (E.isLeft(e)) { - console.error(e.left); - if (e.left._tag === 'decodeError') { - console.log(D.draw(e.left.cause)); - throw new HttpException(e.left.message, 400); - } - throw new HttpException(e.left.message, 500); - } - } - @Delete('user/:id') @HttpCode(201) async deleteUser(@Req() req: AuthenticatedRequest, @Param('id') id: string) { diff --git a/apps/server-asset-sg/src/app/admin/admin.service.ts b/apps/server-asset-sg/src/app/admin/admin.service.ts index 3cb0cb41..26bc5485 100644 --- a/apps/server-asset-sg/src/app/admin/admin.service.ts +++ b/apps/server-asset-sg/src/app/admin/admin.service.ts @@ -1,26 +1,22 @@ -import * as path from 'node:path'; - import { Injectable } from '@nestjs/common'; import * as A from 'fp-ts/Array'; import * as E from 'fp-ts/Either'; import { flow, pipe } from 'fp-ts/function'; import * as TE from 'fp-ts/TaskEither'; -import * as D from 'io-ts/Decoder'; import { DecodeError, TypedError, UnknownError, decodeError, - isNil, unknownErrorTag, unknownToError, unknownToUnknownError, } from '@asset-sg/core'; -import { UserPatch, UserPost, UserRoleEnum, Users } from '@asset-sg/shared'; +import { UserPatch, Users } from '@asset-sg/shared'; import { PrismaService } from '../prisma/prisma.service'; -import { PrismaUsersDecoder, RawUserMetaData } from '../user/models'; +import { PrismaUsersDecoder } from '../user/models'; const _importDynamic = new Function('modulePath', 'return import(modulePath)'); export const _fetch = async function (...args: unknown[]) { @@ -37,7 +33,7 @@ export class AdminService { TE.tryCatch( () => this.prismaService.assetUser.findMany({ - select: { id: true, role: true, user: { select: { email: true, raw_user_meta_data: true } } }, + select: { id: true, role: true, email: true, lang: true }, }), unknownToUnknownError, ), @@ -52,81 +48,13 @@ export class AdminService { where: { id }, data: { role: user.role, - user: { - update: { - raw_user_meta_data: RawUserMetaData.encode({ lang: user.lang }), - }, - }, + lang: user.lang, }, }), unknownToUnknownError, ); } - createUser(accessToken: string, user: UserPost) { - return pipe( - pipe( - TE.tryCatch( - () => this.prismaService.assetUser.findFirst({ where: { user: { email: user.email } } }), - unknownToUnknownError, - ), - ), - TE.filterOrElseW(isNil, () => new TypedError('PermissionDeniedError', `User already exists`)), - TE.chainW(() => - TE.tryCatch(() => { - const inviteUrl = new URL('/invite', process.env.AUTH_URL).href; - const redirect_to = new URL( - path.join(user.lang, `/a/set-password?ts=${Date.now()}`), - process.env.FRONTEND_URL, - ).href; - console.log( - 'createUser', - JSON.stringify({ - userLang: user.lang, - frontendUrl: process.env.FRONTEND_URL, - redirect_to, - }), - ); - return _fetch(inviteUrl, { - method: 'POST', - headers: { Authorization: 'Bearer ' + accessToken, redirect_to, referer: redirect_to }, - body: JSON.stringify({ email: user.email, data: RawUserMetaData.encode({ lang: user.lang }) }), - }); - }, unknownToUnknownError), - ), - TE.filterOrElseW( - res => res.status === 200, - res => - new TypedError( - 'PermissionDeniedError', - `Failed to create user. status ${res.status}. statusText: ${res.statusText} `, - ), - ), - TE.chainW(res => TE.tryCatch(() => res.json(), unknownToUnknownError)), - TE.chainEitherKW(flow(D.struct({ id: D.string }).decode, E.mapLeft(decodeError))), - TE.chainW(({ id }) => - TE.tryCatch( - () => - this.prismaService.assetUser.create({ - data: { - id, - role: user.role, - }, - }), - unknownToUnknownError, - ), - ), - TE.chain(({ id }) => - user.role === UserRoleEnum.admin - ? TE.tryCatch( - () => this.prismaService.users.update({ where: { id }, data: { role: 'service_role' } }), - unknownToUnknownError, - ) - : TE.right(null), - ), - ); - } - deleteUser(accessToken: string, id: string) { const tasks: TE.TaskEither, any>[] = [ TE.tryCatch( diff --git a/apps/server-asset-sg/src/app/app.module.ts b/apps/server-asset-sg/src/app/app.module.ts index 8d48a702..2bfced2d 100644 --- a/apps/server-asset-sg/src/app/app.module.ts +++ b/apps/server-asset-sg/src/app/app.module.ts @@ -1,4 +1,5 @@ import { HttpModule } from '@nestjs/axios'; +import { CacheModule } from '@nestjs/cache-manager'; import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { ScheduleModule } from '@nestjs/schedule'; @@ -11,13 +12,14 @@ import { AssetEditService } from './asset-edit/asset-edit.service'; import { ContactEditController } from './contact-edit/contact-edit.controller'; import { ContactEditService } from './contact-edit/contact-edit.service'; import { JwtMiddleware } from './jwt/jwt-middleware'; +import { OAuthController } from './oauth-config/oauth-config.controller'; import { OcrController } from './ocr/ocr.controller'; import { PrismaService } from './prisma/prisma.service'; import { UserController } from './user/user.controller'; import { UserService } from './user/user.service'; @Module({ - imports: [HttpModule, ScheduleModule.forRoot()], + imports: [HttpModule, ScheduleModule.forRoot(), CacheModule.register()], controllers: [ AppController, AdminController, @@ -25,11 +27,12 @@ import { UserService } from './user/user.service'; AssetEditController, ContactEditController, OcrController, + OAuthController, ], providers: [AppService, AdminService, PrismaService, UserService, AssetEditService, ContactEditService], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { - consumer.apply(JwtMiddleware).exclude('api/ocr/(.*)').forRoutes('*'); + consumer.apply(JwtMiddleware).exclude('api/oauth-config/config', 'api/ocr/(.*)').forRoutes('*'); } } diff --git a/apps/server-asset-sg/src/app/asset-edit/asset-edit.controller.ts b/apps/server-asset-sg/src/app/asset-edit/asset-edit.controller.ts index df74a637..a6e23672 100644 --- a/apps/server-asset-sg/src/app/asset-edit/asset-edit.controller.ts +++ b/apps/server-asset-sg/src/app/asset-edit/asset-edit.controller.ts @@ -79,9 +79,9 @@ export class AssetEditController { )(); if (E.isLeft(e)) { console.error(e.left); - if (e.left._tag === 'decodeError') { - throw new HttpException(e.left.message, 400); - } + // if (e.left._tag === 'decodeError') { + // throw new HttpException(e.left.message, 400); + // } throw new HttpException(e.left.message, 500); } return e.right; @@ -113,9 +113,9 @@ export class AssetEditController { )(); if (E.isLeft(e)) { console.error(e.left); - if (e.left._tag === 'decodeError') { - throw new HttpException(e.left.message, 400); - } + // if (e.left._tag === 'decodeError') { + // throw new HttpException(e.left.message, 400); + // } throw new HttpException(e.left.message, 500); } return e.right; @@ -140,9 +140,9 @@ export class AssetEditController { )(); if (E.isLeft(e)) { console.error(e.left); - if (e.left._tag === 'decodeError') { - throw new HttpException(e.left.message, 400); - } + // if (e.left._tag === 'decodeError') { + // throw new HttpException(e.left.message, 400); + // } throw new HttpException(e.left.message, 500); } return e.right; @@ -163,9 +163,9 @@ export class AssetEditController { )(); if (E.isLeft(e)) { console.error(e.left); - if (e.left._tag === 'decodeError') { - throw new HttpException(e.left.message, 400); - } + // if (e.left._tag === 'decodeError') { + // throw new HttpException(e.left.message, 400); + // } throw new HttpException(e.left.message, 500); } return e.right; diff --git a/apps/server-asset-sg/src/app/contact-edit/contact-edit.controller.ts b/apps/server-asset-sg/src/app/contact-edit/contact-edit.controller.ts index ff77e315..a95d479d 100644 --- a/apps/server-asset-sg/src/app/contact-edit/contact-edit.controller.ts +++ b/apps/server-asset-sg/src/app/contact-edit/contact-edit.controller.ts @@ -30,9 +30,9 @@ export class ContactEditController { if (E.isLeft(e)) { console.error(e.left); - if (e.left._tag === 'decodeError') { - throw new HttpException(e.left.message, 400); - } + // if (e.left._type === 'decodeError') { + // throw new HttpException(e.left.message, 400); + // } throw new HttpException(e.left.message, 500); } return e.right; @@ -58,9 +58,9 @@ export class ContactEditController { if (E.isLeft(e)) { console.error(e.left); - if (e.left._tag === 'decodeError') { - throw new HttpException(e.left.message, 400); - } + // if (e.left._tag === 'decodeError') { + // throw new HttpException(e.left.message, 400); + // } throw new HttpException(e.left.message, 500); } return e.right; diff --git a/apps/server-asset-sg/src/app/jwt/jwt-middleware.ts b/apps/server-asset-sg/src/app/jwt/jwt-middleware.ts index b78dd794..1c96feab 100644 --- a/apps/server-asset-sg/src/app/jwt/jwt-middleware.ts +++ b/apps/server-asset-sg/src/app/jwt/jwt-middleware.ts @@ -1,30 +1,138 @@ -import { HttpException, Injectable, NestMiddleware } from '@nestjs/common'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Inject, Injectable, NestMiddleware } from '@nestjs/common'; +import axios from 'axios'; +import { Cache } from 'cache-manager'; import { NextFunction, Request, Response } from 'express'; import * as E from 'fp-ts/Either'; import { pipe } from 'fp-ts/function'; -import { assert } from 'tsafe'; - -import { isTruthy } from '@asset-sg/core'; +import * as TE from 'fp-ts/TaskEither'; +import * as jwt from 'jsonwebtoken'; +import { Jwt, JwtPayload } from 'jsonwebtoken'; +import * as jwkToPem from 'jwk-to-pem'; import { AuthenticatedRequest } from '../models/request'; -import { jwtFromCookie } from './jwt'; - -export const cookieKey = 'asset-sg-access-token'; - @Injectable() export class JwtMiddleware implements NestMiddleware { - use(req: Request, _res: Response, next: NextFunction) { - const jwtSecret = process.env['GOTRUE_JWT_SECRET']; - assert(isTruthy(jwtSecret), 'GOTRUE_JWT_SECRET is not defined'); - const result = pipe(req.headers.cookie, jwtFromCookie(cookieKey, jwtSecret)); + constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {} + + async use(req: Request, _res: Response, next: NextFunction) { + // Get JWK from cache if exists, otherwise fetch from issuer and set to cache for 1 minute + const cachedJwk = await this.getJwkFromCache()(); + const jwk = E.isRight(cachedJwk) ? cachedJwk : await this.getJwkTE()(); + await this.cacheManager.set('jwk', E.isRight(jwk) ? jwk.right : [], 60 * 1000); + + const token = this.extractTokenFromHeaderE(req); + + // Decode token, get JWK, convert JWK to PEM, and verify token + const result = pipe( + token, + E.chain(this.decodeTokenE), + E.chain(decoded => + pipe( + jwk, + E.chain(jwk => this.getSigningKeyE(decoded, jwk)), + ), + ), + this.jwkToPemE, + E.chain(pem => this.verifyToken(token, pem)), + ); + + // Set accessToken and jwtPayload to request if verification is successful if (E.isRight(result)) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any (req as AuthenticatedRequest).accessToken = result.right.accessToken; - (req as AuthenticatedRequest).jwtPayload = result.right.jwtPayload; + (req as AuthenticatedRequest).jwtPayload = result.right.jwtPayload as JwtPayload; next(); } else { - throw new HttpException('Unauthorised', 401); + throw result.left; } } + + private getJwkFromCache(): TE.TaskEither { + return pipe( + TE.tryCatch( + () => this.cacheManager.get('jwk'), + reason => new Error(`${reason}`), + ), + TE.chain(cache => (cache ? TE.right(cache as JwksKey[]) : TE.left(new Error('No cache found')))), + ); + } + + private extractTokenFromHeaderE(request: Request): E.Either { + const [type, token] = request.headers.authorization?.split(' ') ?? []; + return pipe( + token, + E.fromNullable(new Error('No token found')), + E.chain( + E.fromPredicate( + () => type === 'Bearer', + () => new Error('Invalid token type'), + ), + ), + ); + } + + private decodeTokenE(token: string): E.Either { + return pipe( + E.tryCatch(() => jwt.decode(token, { complete: true }), E.toError), + E.chain(decoded => + decoded !== null ? E.right(decoded) : E.left(new Error('Token decoding resulted in null')), + ), + ); + } + + private getJwkTE(): TE.TaskEither { + return pipe( + TE.tryCatch( + () => axios.get(`${process.env.OAUTH_ISSUER}/.well-known/jwks.json`), + reason => new Error(`${reason}`), + ), + TE.map(response => response.data.keys), + ); + } + + private getSigningKeyE(decoded: Jwt, jwks: JwksKey[]): E.Either { + return pipe( + jwks.find((key: any) => key.kid === decoded.header.kid), + signingKey => (signingKey ? E.right(signingKey) : E.left(new Error('Matching object not found'))), + ); + } + + private jwkToPemE(signingKey: E.Either): E.Either { + return pipe( + signingKey, + E.chain(signingKeyObject => + E.tryCatch( + () => jwkToPem(signingKeyObject as jwkToPem.JWK), // Attempt to convert JWK to PEM + error => new Error(`Failed to convert JWK to PEM: ${error}`), // Catch and wrap any errors + ), + ), + ); + } + + private verifyToken( + token: E.Either, + pem: string, + ): E.Either< + Error, + { + accessToken: string; + jwtPayload: string | JwtPayload; + } + > { + return pipe( + token, + E.chain(tokenString => + pipe( + E.tryCatch( + () => jwt.verify(tokenString, pem, { algorithms: ['RS256'] }), + (err: unknown) => new Error(`Invalid token: ${(err as Error).message}`), + ), + E.map(verified => ({ accessToken: tokenString, jwtPayload: verified })), + ), + ), + ); + } } + +interface JwksKey {} // TODO implement JwksKey interface diff --git a/apps/server-asset-sg/src/app/jwt/jwt.ts b/apps/server-asset-sg/src/app/jwt/jwt.ts index 6afd63f7..ff6b6e8e 100644 --- a/apps/server-asset-sg/src/app/jwt/jwt.ts +++ b/apps/server-asset-sg/src/app/jwt/jwt.ts @@ -1,33 +1,14 @@ -import * as A from 'fp-ts/Array'; import * as E from 'fp-ts/Either'; -import { flow, pipe } from 'fp-ts/function'; -import * as R from 'fp-ts/ReadonlyRecord'; -import * as D from 'io-ts/Decoder'; import * as jwt from 'jsonwebtoken'; -export const jwtFromCookie = (cookieKey: string, jwtSecret: string) => (reqHeaderCookie: unknown) => - pipe( - reqHeaderCookie, - E.fromNullable('No cookie header' as const), - E.chainW( - flow( - D.string.decode, - E.chain( - flow( - a => a.split(';'), - A.map(c => c.trim().split('=')), - A.map(([k, v]) => [k, v] as const), - R.fromEntries, - E.of, - E.chainW(D.struct({ [cookieKey]: D.string }).decode), - E.map(a => a[cookieKey]), - E.bindTo('accessToken'), - E.bindW('jwtPayload', ({ accessToken }) => verifyJwt(jwtSecret)(accessToken)), - ), - ), - ), - ), - ); +export const jwtFromToken = (token: string, jwtSecret: string) => { + const decoded = jwt.decode(token, { complete: true }); + console.log('decoded', decoded); + if (decoded?.payload) { + console.log('verify', jwt.verify(token, jwtSecret, { algorithms: ['HS256'], complete: true })); + } + return decoded; +}; const verifyJwt = (jwtSecret: string) => (token: string) => E.tryCatch( diff --git a/apps/server-asset-sg/src/app/oauth-config/oauth-config.controller.ts b/apps/server-asset-sg/src/app/oauth-config/oauth-config.controller.ts new file mode 100644 index 00000000..9747ae0b --- /dev/null +++ b/apps/server-asset-sg/src/app/oauth-config/oauth-config.controller.ts @@ -0,0 +1,16 @@ +import { Controller, Get } from '@nestjs/common'; + +@Controller('oauth-config') +export class OAuthController { + @Get('config') + getConfig() { + return { + oauth_issuer: process.env.OAUTH_ISSUER, + oauth_clientId: process.env.OAUTH_CLIENT_ID, + oauth_scope: process.env.OAUTH_SCOPE, + oauth_responseType: process.env.OAUTH_RESPONSE_TYPE, + oauth_showDebugInformation: !!process.env.OAUTH_SHOW_DEBUG_INFORMATION, + oauth_tokenEndpoint: process.env.OAUTH_TOKEN_ENDPOINT, + }; + } +} diff --git a/apps/server-asset-sg/src/app/prisma/migrations/20240318103937_update_user_schema/migration.sql b/apps/server-asset-sg/src/app/prisma/migrations/20240318103937_update_user_schema/migration.sql new file mode 100644 index 00000000..986fd7ef --- /dev/null +++ b/apps/server-asset-sg/src/app/prisma/migrations/20240318103937_update_user_schema/migration.sql @@ -0,0 +1,18 @@ +/* + Warnings: + + - Added the required column `email` to the `asset_user` table without a default value. This is not possible if the table is not empty. + - Added the required column `lang` to the `asset_user` table without a default value. This is not possible if the table is not empty. + - Added the required column `oidcId` to the `asset_user` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "public"."asset_user" DROP CONSTRAINT "asset_user_id_fkey"; + +-- AlterTable +ALTER TABLE "auth"."users" ALTER COLUMN "confirmed_at" DROP EXPRESSION; + +-- AlterTable +ALTER TABLE "public"."asset_user" ADD COLUMN "email" TEXT NOT NULL, +ADD COLUMN "lang" TEXT NOT NULL, +ADD COLUMN "oidcId" TEXT NOT NULL; diff --git a/apps/server-asset-sg/src/app/prisma/schema.prisma b/apps/server-asset-sg/src/app/prisma/schema.prisma index 4dbe36eb..d824891b 100644 --- a/apps/server-asset-sg/src/app/prisma/schema.prisma +++ b/apps/server-asset-sg/src/app/prisma/schema.prisma @@ -7,6 +7,7 @@ generator client { datasource db { provider = "postgresql" url = env("DATABASE_URL") + shadowDatabaseUrl = env("SHADOW_DATABASE_URL") schemas = ["public", "auth"] } @@ -107,7 +108,6 @@ model users { reauthentication_token String? @default("") @db.VarChar(255) reauthentication_sent_at DateTime? @db.Timestamptz(6) identities identities[] - AssetUser AssetUser? @@index([instance_id]) @@schema("auth") @@ -770,8 +770,10 @@ model StatusWorkItem { model AssetUser { id String @id @db.Uuid - user users @relation(fields: [id], references: [id]) role String + email String + lang String + oidcId String AssetUserFavourite AssetUserFavourite[] @@map("asset_user") diff --git a/apps/server-asset-sg/src/app/scripts/create-admin-user.ts b/apps/server-asset-sg/src/app/scripts/create-admin-user.ts deleted file mode 100644 index 797a783c..00000000 --- a/apps/server-asset-sg/src/app/scripts/create-admin-user.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { PrismaClient } from '@prisma/client'; -import { pipe } from 'fp-ts/function'; -import * as TE from 'fp-ts/TaskEither'; - -const _importDynamic = new Function('modulePath', 'return import(modulePath)'); - -export const _fetch = async function (...args: unknown[]) { - const { default: fetch } = await _importDynamic('node-fetch'); - return fetch(...args); -}; - -// eslint-disable-next-line @nrwl/nx/enforce-module-boundaries -import { unknownToError } from '../../../../../libs/core/src'; - -import { httpJson, printResult } from './http-utils'; - -const createUser = (email: string, password: string) => - httpJson(() => { - console.log(JSON.stringify({ email, password, data: { lang: 'de' } })); - - return _fetch('http://localhost:4200/auth/signup', { - method: 'POST', - body: JSON.stringify({ email, password, data: { lang: 'de' } }), - }); - }); - -const confirmAdminUser = (email: string) => { - const prisma = new PrismaClient(); - return pipe( - TE.tryCatch( - () => - prisma.$executeRawUnsafe( - `update auth.users set email_confirmed_at = now(), role = 'service_role' where email = '${email}';`, - ), - unknownToError, - ), - TE.chain(() => - TE.tryCatch( - () => - prisma.$executeRawUnsafe( - `insert into asset_user select id, 'admin' as role from auth.users where email = '${email}';`, - ), - unknownToError, - ), - ), - // TE.filterOrElse( - // n => n === 1, - // unknownToError, - // // () => new Error(`No user found with email ${email}`), - // ), - TE.map(() => `User ${email} create and confirmed`), - ); -}; - -const email = 'wayne.maurer@lambda-it.ch'; -const password = 'secret'; - -const program = pipe( - createUser(email, password), - TE.chain(() => confirmAdminUser(email)), -); - -program().then(printResult); diff --git a/apps/server-asset-sg/src/app/user/models/user.ts b/apps/server-asset-sg/src/app/user/models/user.ts index e0e6ca9e..9a9a893a 100644 --- a/apps/server-asset-sg/src/app/user/models/user.ts +++ b/apps/server-asset-sg/src/app/user/models/user.ts @@ -1,6 +1,3 @@ -import * as E from 'fp-ts/Either'; -import { identity, pipe } from 'fp-ts/function'; -import * as C from 'io-ts/Codec'; import * as D from 'io-ts/Decoder'; import { Equals, assert } from 'tsafe'; @@ -9,24 +6,12 @@ import { User, UserRole } from '@asset-sg/shared'; const PrismaUserRaw = D.struct({ id: D.string, role: UserRole, - user: D.struct({ - email: D.string, - raw_user_meta_data: D.UnknownRecord, - }), -}); + email: D.string, + lang: D.string, -const RawUserMetaDataDecoder = D.struct({ lang: D.string }); -export const RawUserMetaData = C.make(RawUserMetaDataDecoder, { encode: identity }); +}); -export const PrismaUserDecoder = pipe( - PrismaUserRaw, - D.parse(u => - pipe( - RawUserMetaDataDecoder.decode(u.user.raw_user_meta_data), - E.map(rawUserMetaData => ({ id: u.id, role: u.role, email: u.user.email, lang: rawUserMetaData.lang })), - ), - ), -); +export const PrismaUserDecoder = PrismaUserRaw assert, User>>(); diff --git a/apps/server-asset-sg/src/app/user/user.controller.ts b/apps/server-asset-sg/src/app/user/user.controller.ts index 880e5f7e..9b9dd789 100644 --- a/apps/server-asset-sg/src/app/user/user.controller.ts +++ b/apps/server-asset-sg/src/app/user/user.controller.ts @@ -1,12 +1,10 @@ -import { Controller, Delete, Get, HttpCode, HttpException, Param, Put, Req, Res } from '@nestjs/common'; -import { Response } from 'express'; +import { Controller, Delete, Get, HttpCode, HttpException, Param, Put, Req } from '@nestjs/common'; import * as E from 'fp-ts/Either'; import * as D from 'io-ts/Decoder'; import { DT } from '@asset-sg/core'; import { isNotFoundError } from '../errors'; -import { cookieKey } from '../jwt/jwt-middleware'; import { AuthenticatedRequest } from '../models/request'; import { UserService } from './user.service'; @@ -17,11 +15,9 @@ export class UserController { @Get('') async getUser(@Req() req: AuthenticatedRequest) { - const e = await this.userService.getUser(req.jwtPayload.sub || '')(); + const e = await this.userService.getUser(req.jwtPayload.sub ?? '', req.jwtPayload.username.split('_')[1])(); if (E.isLeft(e)) { console.error(e.left); - req.res?.clearCookie(cookieKey); - req.res?.clearCookie('asset-sg-refresh-token'); throw new HttpException('Resource not found', 400); } return e.right; diff --git a/apps/server-asset-sg/src/app/user/user.service.ts b/apps/server-asset-sg/src/app/user/user.service.ts index 1a3b90e4..3bfffc4c 100644 --- a/apps/server-asset-sg/src/app/user/user.service.ts +++ b/apps/server-asset-sg/src/app/user/user.service.ts @@ -5,6 +5,7 @@ import * as TE from 'fp-ts/TaskEither'; import { DecodeError, UnknownError, decode, unknownToUnknownError } from '@asset-sg/core'; import { SearchAssetResult, User } from '@asset-sg/shared'; +import { AdminService } from '../admin/admin.service'; import { PrismaService } from '../prisma/prisma.service'; import { getFavourites } from './get-favourites'; @@ -12,18 +13,54 @@ import { PrismaUserDecoder } from './models'; @Injectable() export class UserService { - constructor(private readonly prismaService: PrismaService) {} + constructor(private readonly prismaService: PrismaService, private readonly adminServie: AdminService) {} - getUser(assetUserId: string): TE.TaskEither { + getUser(assetUserId: string, email?: string): TE.TaskEither { return pipe( TE.tryCatch( () => - this.prismaService.assetUser.findUnique({ - select: { id: true, role: true, user: { select: { email: true, raw_user_meta_data: true } } }, + this.prismaService.assetUser.findFirst({ + select: { id: true, role: true, email: true, lang: true }, where: { id: assetUserId }, }), unknownToUnknownError, ), + TE.chain(userOrNull => { + return userOrNull + ? TE.right(userOrNull) + : pipe( + this.createDefaultUser(assetUserId, email ?? ''), + TE.map(newUser => { + return newUser; + }), + ); + }), + TE.chainEitherKW(decode(PrismaUserDecoder)), + ); + } + + createDefaultUser(assetUserId: string, email: string): TE.TaskEither { + return pipe( + TE.tryCatch( + () => + this.prismaService.assetUser.create({ + data: { + id: assetUserId, + oidcId: assetUserId, + email: email ?? 'example@email.com', + role: 'viewer', + lang: 'de', + }, + select: { + id: true, + email: true, + role: true, + lang: true, + }, + }), + (error: unknown) => + new Error('Failed to create user: ' + (error instanceof Error ? error.message : String(error))), + ), TE.chainEitherKW(decode(PrismaUserDecoder)), ); } diff --git a/development/docker-compose.yaml b/development/docker-compose.yaml index 47d9c18c..0264172a 100644 --- a/development/docker-compose.yaml +++ b/development/docker-compose.yaml @@ -115,4 +115,38 @@ services: volumes: - ./volumes/smtp4dev/data:/smtp4dev environment: - - ServerOptions__HostName=smtp4dev \ No newline at end of file + - ServerOptions__HostName=smtp4dev + + oidc-server: + container_name: swissgeol-assets-oidc + image: soluto/oidc-server-mock + restart: unless-stopped + ports: + - "4011:80" + environment: + CLIENTS_CONFIGURATION_PATH: /tmp/config/clients-config.json + USERS_CONFIGURATION_PATH: /tmp/config/users-config.json + IDENTITY_RESOURCES_INLINE: | + [ + { + "Name": "local_groups_scope", + "ClaimTypes": [ + "local_groups_claim" + ] + } + ] + SERVER_OPTIONS_INLINE: | + { + "IssuerUri": "http://localhost:4011", + "AccessTokenJwtType": "JWT", + "Discovery": { + "ShowKeySet": true + }, + "Authentication": { + "CookieSameSiteMode": "Lax", + "CheckSessionCookieSameSiteMode": "Lax" + } + } + volumes: + - ./init/oidc/oidc-mock-clients.json:/tmp/config/clients-config.json:ro + - ./init/oidc/oidc-mock-users.json:/tmp/config/users-config.json:ro \ No newline at end of file diff --git a/development/init/oidc/oidc-mock-clients.json b/development/init/oidc/oidc-mock-clients.json new file mode 100644 index 00000000..1d4a1b3d --- /dev/null +++ b/development/init/oidc/oidc-mock-clients.json @@ -0,0 +1,29 @@ +[{ + "ClientId": "assets-client", + "Description": "Client for Authorization Code flow with PKCE", + "RequireClientSecret": false, + "AlwaysIncludeUserClaimsInIdToken": true, + "AllowedGrantTypes": [ + "authorization_code" + ], + "AllowedResponseTypes": [ + "code", + "id_token" + ], + "AllowAccessTokensViaBrowser": true, + "RedirectUris": [ + "http://localhost:4200" + ], + "PostLogoutRedirectUris" : [ + "http://localhost:4200" + ], + "AllowedScopes": [ + "openid", + "profile", + "local_groups_scope" + ], + "AccessTokenType": "JWT", + "IdentityTokenLifetime": 3600, + "AccessTokenLifetime": 3600 +} +] diff --git a/development/init/oidc/oidc-mock-users.json b/development/init/oidc/oidc-mock-users.json new file mode 100644 index 00000000..c2919e49 --- /dev/null +++ b/development/init/oidc/oidc-mock-users.json @@ -0,0 +1,76 @@ +[ + { + "SubjectId":"10f95aa3-fb95-41eb-b754-5f729a092e30", + "Username":"admin@swissgeol.assets", + "Password":"swissgeol_assets", + "Claims": [ + { + "Type": "name", + "Value": "Admin User", + "ValueType": "string" + }, + { + "Type": "family_name", + "Value": "User", + "ValueType": "string" + }, + { + "Type": "given_name", + "Value": "Admin", + "ValueType": "string" + }, + { + "Type": "email", + "Value": "admin.user@local.dev", + "ValueType": "string" + }, + { + "Type": "email_verified", + "Value": "true", + "ValueType": "boolean" + }, + { + "Type": "local_groups_claim", + "Value": "[\"boreholes_dev_group\"]", + "ValueType": "json" + } + ] + }, + { + "SubjectId":"sub_editor", + "Username":"editor", + "Password":"swissforages", + "Claims": [ + { + "Type": "name", + "Value": "Editor User", + "ValueType": "string" + }, + { + "Type": "family_name", + "Value": "User", + "ValueType": "string" + }, + { + "Type": "given_name", + "Value": "Editor", + "ValueType": "string" + }, + { + "Type": "email", + "Value": "editor.user@local.dev", + "ValueType": "string" + }, + { + "Type": "email_verified", + "Value": "true", + "ValueType": "boolean" + }, + { + "Type": "local_groups_claim", + "Value": "[\"boreholes_dev_group\"]", + "ValueType": "json" + } + ] + } +] diff --git a/libs/admin/src/lib/components/admin-page/admin-page.component.html b/libs/admin/src/lib/components/admin-page/admin-page.component.html index 36152ce6..67542516 100644 --- a/libs/admin/src/lib/components/admin-page/admin-page.component.html +++ b/libs/admin/src/lib/components/admin-page/admin-page.component.html @@ -5,14 +5,6 @@
- this._adminService.getUsers(), updateUser: user => this._adminService.updateUser(user), - createUser: user => this._adminService.createUser(user), deleteUser: id => this._adminService.deleteUser(id), }); @@ -66,7 +65,6 @@ export class AdminPageComponent { public handleUserExpandedOutput(output: UserExpandedOutput): void { UserExpandedOutput.match({ - userCreated: user => this.sm.saveCreatedUser(user), userEdited: user => this.sm.saveEditedUser(user), userExpandCanceled: () => this.sm.cancelEditOrSave(), userDelete: user => this.sm.deleteUser(user.id), diff --git a/libs/admin/src/lib/components/admin-page/admin-page.state-machine.ts b/libs/admin/src/lib/components/admin-page/admin-page.state-machine.ts index 6a369192..7aa52951 100644 --- a/libs/admin/src/lib/components/admin-page/admin-page.state-machine.ts +++ b/libs/admin/src/lib/components/admin-page/admin-page.state-machine.ts @@ -5,7 +5,7 @@ import { ReplaySubject, forkJoin, map } from 'rxjs'; import { ApiError } from '@asset-sg/client-shared'; import { ORD } from '@asset-sg/core'; -import { User, UserWithoutId, Users, byEmail } from '@asset-sg/shared'; +import { User, Users, byEmail } from '@asset-sg/shared'; type AdminPageState = Loading | StateApiError | ReadMode | EditMode | CreateMode; @@ -31,7 +31,6 @@ export class AdminPageStateMachine { rdUserId$: ORD.ObservableRemoteData; getUsers: () => ORD.ObservableRemoteData; updateUser: (user: User) => ORD.ObservableRemoteData; - createUser: (user: Omit) => ORD.ObservableRemoteData; deleteUser: (id: string) => ORD.ObservableRemoteData; }, ) { @@ -91,22 +90,6 @@ export class AdminPageStateMachine { } } - public createUser() { - if (AdminPageState.is.readMode(this._state)) { - this._state = AdminPageState.of.createMode({ - ...this._state, - usersVM: this._state.usersVM.map(u => ({ ...u, expanded: false, disableEdit: true })), - }); - } - } - - public saveCreatedUser(user: UserWithoutId) { - if (AdminPageState.is.createMode(this._state)) { - this._state = AdminPageState.as.createMode({ ...this._state, showProgressBar: true }); - this.modifyAndReload(this.effects.createUser(user), this._state._userId); - } - } - public deleteUser(id: string) { if (AdminPageState.is.editMode(this._state)) { this._state = AdminPageState.as.editMode({ ...this._state, showProgressBar: true }); diff --git a/libs/admin/src/lib/components/user-expanded/user-expanded.component.html b/libs/admin/src/lib/components/user-expanded/user-expanded.component.html index 64803c77..f8ec418f 100644 --- a/libs/admin/src/lib/components/user-expanded/user-expanded.component.html +++ b/libs/admin/src/lib/components/user-expanded/user-expanded.component.html @@ -1,6 +1,6 @@
-
+
@@ -20,13 +20,6 @@
- - - - Admin diff --git a/libs/admin/src/lib/components/user-expanded/user-expanded.component.ts b/libs/admin/src/lib/components/user-expanded/user-expanded.component.ts index 8e982d2d..ae734a1c 100644 --- a/libs/admin/src/lib/components/user-expanded/user-expanded.component.ts +++ b/libs/admin/src/lib/components/user-expanded/user-expanded.component.ts @@ -15,11 +15,7 @@ import { makeADT, ofType } from '@morphic-ts/adt'; import * as O from 'fp-ts/Option'; import { ButtonComponent, ViewChildMarker } from '@asset-sg/client-shared'; -import { User, UserRoleEnum, UserWithoutId } from '@asset-sg/shared'; - -interface UserCreated extends UserWithoutId { - _tag: 'userCreated'; -} +import { User, UserRoleEnum } from '@asset-sg/shared'; interface UserEdited extends User { _tag: 'userEdited'; @@ -34,13 +30,12 @@ interface UserExpandCanceled { } export const UserExpandedOutput = makeADT('_tag')({ - userCreated: ofType(), userEdited: ofType(), userExpandCanceled: ofType(), userDelete: ofType(), }); -export type UserExpandedOutput = UserCreated | UserEdited | UserDelete | UserExpandCanceled; +export type UserExpandedOutput = UserEdited | UserDelete | UserExpandCanceled; @Component({ selector: 'asset-sg-user-expanded', @@ -97,10 +92,6 @@ export class UserExpandedComponent { if (!role || !lang) return; this.userExpandedOutput.emit(UserExpandedOutput.of.userEdited({ ...this.user, role, lang })); - } else { - const { email, role, lang } = this.editForm.getRawValue(); - if (!email || !role || !lang) return; - this.userExpandedOutput.emit(UserExpandedOutput.of.userCreated({ email, role, lang })); } } diff --git a/libs/admin/src/lib/services/admin.service.ts b/libs/admin/src/lib/services/admin.service.ts index 257814fe..6516be10 100644 --- a/libs/admin/src/lib/services/admin.service.ts +++ b/libs/admin/src/lib/services/admin.service.ts @@ -25,22 +25,6 @@ export class AdminService { ); } - public createUser(user: Omit): ORD.ObservableRemoteData { - return this._httpClient - .post(`/api/admin/user`, { - email: user.email, - role: user.role, - lang: user.lang, - }) - .pipe( - map(() => E.right(undefined)), - // TODO need to test instance of HttpErrorResponse here - OE.catchErrorW(httpErrorResponseError), - map(RD.fromEither), - startWith(RD.pending), - ); - } - public updateUser(user: User): ORD.ObservableRemoteData { return this._httpClient .patch(`/api/admin/user/${user.id}`, { @@ -58,7 +42,6 @@ export class AdminService { public deleteUser(id: string): ORD.ObservableRemoteData { console.log('888', id); - // return delayObservable(2000, ORD.success(undefined)); return this._httpClient.delete(`/api/admin/user/${id}`).pipe( tap(a => console.log('deleteUser', a)), map(() => E.right(undefined)), diff --git a/libs/auth/src/lib/auth.module.ts b/libs/auth/src/lib/auth.module.ts index ffffd012..950a711f 100644 --- a/libs/auth/src/lib/auth.module.ts +++ b/libs/auth/src/lib/auth.module.ts @@ -11,6 +11,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { ForModule } from '@rx-angular/template/for'; import { LetModule } from '@rx-angular/template/let'; import { PushModule } from '@rx-angular/template/push'; +import { OAuthModule } from 'angular-oauth2-oidc'; import { AnchorComponent, ButtonComponent, icons } from '@asset-sg/client-shared'; @@ -46,10 +47,17 @@ import { SetPasswordPageComponent } from './components/set-password-page'; MatFormFieldModule, MatInputModule, MatProgressBarModule, - ButtonComponent, AnchorComponent, + OAuthModule.forRoot({ + resourceServer: { + sendAccessToken: true, + allowedUrls: [], + customUrlValidation: (url: string) => !url.includes('oauth2/token'), + }, + }), ], + exports: [OAuthModule], providers: [provideSvgIcons(icons)], declarations: [ResetPasswordDialogComponent, SetPasswordDialogComponent], }) diff --git a/libs/auth/src/lib/services/auth.interceptor.ts b/libs/auth/src/lib/services/auth.interceptor.ts index f629c414..8d148523 100644 --- a/libs/auth/src/lib/services/auth.interceptor.ts +++ b/libs/auth/src/lib/services/auth.interceptor.ts @@ -1,62 +1,32 @@ -import { Dialog } from '@angular/cdk/dialog'; -import { - HttpErrorResponse, - HttpEvent, - HttpHandler, - HttpInterceptor, - HttpRequest, - HttpResponse, -} from '@angular/common/http'; +import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; import { Injectable, inject } from '@angular/core'; -import * as RD from '@devexperts/remote-data-ts'; -import { Observable, Subject, exhaustMap, filter, map, retry, shareReplay, startWith, take } from 'rxjs'; - -import { LoginComponent } from '../components/login'; +import { OAuthService } from 'angular-oauth2-oidc'; +import { Observable, throwError } from 'rxjs'; @Injectable() export class AuthInterceptor implements HttpInterceptor { - private loginSignal$ = new Subject(); - private loginResult$ = this.loginSignal$.pipe( - exhaustMap(() => this.attemptRefreshTokenThenLoginFromComponent().pipe(map(RD.success), startWith(RD.pending))), - shareReplay({ bufferSize: 1, refCount: false }), - ); - - private dialog = inject(Dialog); + private _oauthService = inject(OAuthService); intercept(req: HttpRequest, next: HttpHandler): Observable> { - return next.handle(req).pipe( - filter(event => event instanceof HttpResponse), - retry({ - delay: (error: HttpErrorResponse) => { - if (error.status !== 401) { - throw error; - } - return this.login(); - }, - }), - ); - } - - private login() { - setTimeout(() => { - this.loginSignal$.next(); - }); - return this.loginResult$.pipe(filter(RD.isSuccess), take(1)); - } - - private attemptRefreshTokenThenLoginFromComponent() { - return this.loginFromComponent(); - } + const token = sessionStorage.getItem('access_token'); - private loginFromComponent() { - try { - const dialogRef = this.dialog.open(LoginComponent, { - disableClose: true, + if ( + (this._oauthService.issuer && req.url.includes(this._oauthService.issuer)) || + (this._oauthService.tokenEndpoint && req.url.includes(this._oauthService.tokenEndpoint)) || + req.url.includes('oauth-config/config') + ) { + return next.handle(req); + } else if (token && !this._oauthService.hasValidAccessToken()) { + this._oauthService.logOut({ + client_id: this._oauthService.clientId, + redirect_uri: window.location.origin, + response_type: this._oauthService.responseType, }); - return dialogRef.closed; - } catch (e) { - console.log(e); - throw e; + return throwError(new HttpErrorResponse({ status: 401, statusText: 'Unauthorized' })); + } else if (!token) { + return throwError(new HttpErrorResponse({ status: 401, statusText: 'No Token' })); + } else { + return next.handle(req); } } } diff --git a/libs/auth/src/lib/services/auth.service.ts b/libs/auth/src/lib/services/auth.service.ts index 80045d64..bde8b6ae 100644 --- a/libs/auth/src/lib/services/auth.service.ts +++ b/libs/auth/src/lib/services/auth.service.ts @@ -3,6 +3,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable, inject } from '@angular/core'; import * as RD from '@devexperts/remote-data-ts'; import { TranslateService } from '@ngx-translate/core'; +import { OAuthService } from 'angular-oauth2-oidc'; import * as E from 'fp-ts/Either'; import { flow } from 'fp-ts/function'; import * as D from 'io-ts/Decoder'; @@ -17,6 +18,7 @@ const TokenEndpointResponse = D.struct({ access_token: D.string, refresh_token: D.string, }); + interface TokenEndpointResponse extends D.TypeOf {} @Injectable({ providedIn: 'root' }) @@ -25,16 +27,38 @@ export class AuthService { private _dcmnt = inject(DOCUMENT); private _translateService = inject(TranslateService); - private _getUserProfile() { - return this._httpClient - .get('/api/user') - .pipe(map(decode(User)), OE.catchErrorW(httpErrorResponseOrUnknownError)); + private _oauthService = inject(OAuthService); + + public async configureOAuth( + issuer: string, + clientId: string, + scope: string, + showDebugInformation: boolean, + tokenEndpoint: string, + ) { + this._oauthService.configure({ + issuer, + redirectUri: window.location.origin, + postLogoutRedirectUri: window.location.origin, + clientId, + scope, + responseType: 'code', + showDebugInformation, + strictDiscoveryDocumentValidation: false, + tokenEndpoint, + }); + await this._oauthService.loadDiscoveryDocumentAndLogin(); + this._oauthService.setupAutomaticSilentRefresh(); } getUserProfile(): ORD.ObservableRemoteData { return this._getUserProfile().pipe(map(RD.fromEither), startWith(RD.pending)); } + isLoggedIn(): boolean { + return this._oauthService.hasValidAccessToken(); + } + login(email: string, password: string): ORD.ObservableRemoteData { return this._httpClient.post(this.buildAuthUrl('token?grant_type=password'), { email, password }).pipe( map(E.right), @@ -55,7 +79,22 @@ export class AuthService { ); } + logOut(): void { + this._oauthService.logOut({ + client_id: this._oauthService.clientId, + redirect_uri: window.location.origin, + response_type: this._oauthService.responseType, + }); + } + + private _getUserProfile() { + return this._httpClient + .get('/api/user') + .pipe(map(decode(User)), OE.catchErrorW(httpErrorResponseOrUnknownError)); + } + logout(): ORD.ObservableRemoteData { + this._oauthService.logOut(); const accessToken = localStorage.getItem('accessToken'); return this._httpClient .post( diff --git a/libs/profile/src/lib/components/profile/profile.component.ts b/libs/profile/src/lib/components/profile/profile.component.ts index 88d568b0..d43fcfc8 100644 --- a/libs/profile/src/lib/components/profile/profile.component.ts +++ b/libs/profile/src/lib/components/profile/profile.component.ts @@ -55,11 +55,9 @@ export class ProfileComponent extends RxState { this._cd.detectChanges(); }); - this.logoutClicked$.pipe(switchMap(() => this._authService.logout())).subscribe(rd => { - if (rdIsComplete(rd)) { + this.logoutClicked$.pipe(switchMap(async () => this._authService.logOut())).subscribe(rd => { this._store.dispatch(appSharedStateActions.logout()); this._router.navigate(['/']); - } }); } } diff --git a/libs/shared/src/lib/models/user.ts b/libs/shared/src/lib/models/user.ts index 4885a07d..70d0363e 100644 --- a/libs/shared/src/lib/models/user.ts +++ b/libs/shared/src/lib/models/user.ts @@ -42,6 +42,8 @@ export const UserPost = D.struct({ email: D.string, role: UserRoleDecoder, lang: D.string, + oidcId: D.string, + id: D.string }); export interface UserPost extends D.TypeOf {} diff --git a/package-lock.json b/package-lock.json index ac995337..ce82f632 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "@kobalte/core": "^0.4.0", "@morphic-ts/adt": "^3.0.0", "@nestjs/axios": "^2.0.0", + "@nestjs/cache-manager": "^2.2.2", "@nestjs/common": "^9.0.0", "@nestjs/core": "^9.0.0", "@nestjs/platform-express": "^9.0.0", @@ -45,12 +46,15 @@ "@turf/boolean-within": "^6.5.0", "@turf/helpers": "^6.5.0", "@turf/midpoint": "^6.5.0", + "angular-oauth2-oidc": "^17.0.1", "bezier-easing": "^2.1.0", + "cache-manager": "^5.4.0", "csv-parse": "^5.3.3", "cuid": "^3.0.0", "fp-ts": "^2.13.1", "io-ts": "^2.2.20", "jsonwebtoken": "^9.0.0", + "jwk-to-pem": "^2.0.5", "monocle-ts": "^2.3.13", "newtype-ts": "^0.3.5", "ngx-window-token": "^6.0.0", @@ -91,6 +95,7 @@ "@nrwl/workspace": "15.5.2", "@types/jest": "28.1.1", "@types/jsonwebtoken": "^9.0.1", + "@types/jwk-to-pem": "^2.0.3", "@types/multer": "^1.4.7", "@types/node": "18.7.1", "@types/validator": "^13.7.10", @@ -7074,6 +7079,17 @@ "rxjs": "^6.0.0 || ^7.0.0" } }, + "node_modules/@nestjs/cache-manager": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-2.2.2.tgz", + "integrity": "sha512-+n7rpU1QABeW2WV17Dl1vZCG3vWjJU1MaamWgZvbGxYE9EeCM0lVLfw3z7acgDTNwOy+K68xuQPoIMxD0bhjlA==", + "peerDependencies": { + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "cache-manager": "<=5", + "rxjs": "^7.0.0" + } + }, "node_modules/@nestjs/common": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-9.2.1.tgz", @@ -10756,24 +10772,25 @@ "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==" }, "node_modules/@types/express": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.15.tgz", - "integrity": "sha512-Yv0k4bXGOH+8a+7bELd2PqHQsuiANB+A8a4gnQrkRWzrkKlb6KHaVvyXhqs04sVW/OWlbPyYxRgYlIXLfrufMQ==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "dependencies": { "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.31", + "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.32", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.32.tgz", - "integrity": "sha512-aI5h/VOkxOF2Z1saPy0Zsxs5avets/iaiAJYznQFm5By/pamU31xWKL//epiF4OfUA2qTOc9PV6tCUjhO8wlZA==", + "version": "4.17.43", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz", + "integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==", "dependencies": { "@types/node": "*", "@types/qs": "*", - "@types/range-parser": "*" + "@types/range-parser": "*", + "@types/send": "*" } }, "node_modules/@types/graceful-fs": { @@ -10846,14 +10863,20 @@ "dev": true }, "node_modules/@types/jsonwebtoken": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz", - "integrity": "sha512-c5ltxazpWabia/4UzhIoaDcIza4KViOQhdbjRlfcIGVnsE3c3brkz9Z+F/EeJIECOQP7W7US2hNE930cWWkPiw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", + "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", "dev": true, "dependencies": { "@types/node": "*" } }, + "node_modules/@types/jwk-to-pem": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/jwk-to-pem/-/jwk-to-pem-2.0.3.tgz", + "integrity": "sha512-I/WFyFgk5GrNbkpmt14auGO3yFK1Wt4jXzkLuI+fDBNtO5ZI2rbymyGd6bKzfSBEuyRdM64ZUwxU1+eDcPSOEQ==", + "dev": true + }, "node_modules/@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -10921,6 +10944,20 @@ "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", "dev": true }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/send/node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, "node_modules/@types/serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", @@ -11803,6 +11840,23 @@ "ajv": "^8.8.2" } }, + "node_modules/angular-oauth2-oidc": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/angular-oauth2-oidc/-/angular-oauth2-oidc-17.0.1.tgz", + "integrity": "sha512-Yl4It9zFsYmoNS73sUvNJstbMW1x73ejKonzXLgU4XnSuBCt/0x8PnY5R3mHX4ZC/WmXBqQ/RfFwClrYW9Ywcg==", + "dependencies": { + "tslib": "^2.5.2" + }, + "peerDependencies": { + "@angular/common": ">=14.0.0", + "@angular/core": ">=14.0.0" + } + }, + "node_modules/angular-oauth2-oidc/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -12051,6 +12105,17 @@ "safer-buffer": "~2.1.0" } }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -12555,6 +12620,11 @@ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "devOptional": true }, + "node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, "node_modules/body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -12637,6 +12707,11 @@ "node": ">=8" } }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, "node_modules/browser-process-hrtime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", @@ -12904,6 +12979,24 @@ "node": ">=12" } }, + "node_modules/cache-manager": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-5.4.0.tgz", + "integrity": "sha512-FS7o8vqJosnLpu9rh2gQTo8EOzCRJLF1BJ4XDEUDMqcfvs7SJZs5iuoFTXLauzQ3S5v8sBAST1pCwMaurpyi1A==", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "^10.1.0", + "promise-coalesce": "^1.1.2" + } + }, + "node_modules/cache-manager/node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/cachedir": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", @@ -14835,6 +14928,20 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==" }, + "node_modules/elliptic": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.5.tgz", + "integrity": "sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw==", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/emittery": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", @@ -17200,6 +17307,15 @@ "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", "dev": true }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "node_modules/hdr-histogram-js": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz", @@ -17231,6 +17347,16 @@ "he": "bin/he" } }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/hosted-git-info": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.2.1.tgz", @@ -21363,20 +21489,56 @@ ] }, "node_modules/jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", "dependencies": { "jws": "^3.2.2", - "lodash": "^4.17.21", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", "ms": "^2.1.1", - "semver": "^7.3.8" + "semver": "^7.5.4" }, "engines": { "node": ">=12", "npm": ">=6" } }, + "node_modules/jsonwebtoken/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jsonwebtoken/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/jsprim": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", @@ -21402,6 +21564,16 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/jwk-to-pem": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.5.tgz", + "integrity": "sha512-L90jwellhO8jRKYwbssU9ifaMVqajzj3fpRjDKcsDzrslU9syRbFqfkXtT4B89HYAap+xsxNcxgBSB09ig+a7A==", + "dependencies": { + "asn1.js": "^5.3.0", + "elliptic": "^6.5.4", + "safe-buffer": "^5.0.1" + } + }, "node_modules/jws": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", @@ -21674,6 +21846,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -21686,12 +21863,42 @@ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "dev": true }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "dev": true }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -21706,8 +21913,7 @@ "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "devOptional": true + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, "node_modules/lodash.uniq": { "version": "4.5.0", @@ -22257,6 +22463,11 @@ "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + }, "node_modules/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz", @@ -25460,6 +25671,14 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/promise-coalesce": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/promise-coalesce/-/promise-coalesce-1.1.2.tgz", + "integrity": "sha512-zLaJ9b8hnC564fnJH6NFSOGZYYdzrAJn2JUUIwzoQb32fG2QAakpDNM+CZo1km6keXkRXRM+hml1BFAPVnPkxg==", + "engines": { + "node": ">=16" + } + }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -35160,6 +35379,12 @@ "integrity": "sha512-F6oceoQLEn031uun8NiommeMkRIojQqVryxQy/mK7fx0CI0KbgkJL3SloCQcsOD+agoEnqKJKXZpEvL6FNswJg==", "requires": {} }, + "@nestjs/cache-manager": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-2.2.2.tgz", + "integrity": "sha512-+n7rpU1QABeW2WV17Dl1vZCG3vWjJU1MaamWgZvbGxYE9EeCM0lVLfw3z7acgDTNwOy+K68xuQPoIMxD0bhjlA==", + "requires": {} + }, "@nestjs/common": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-9.2.1.tgz", @@ -38027,24 +38252,25 @@ "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==" }, "@types/express": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.15.tgz", - "integrity": "sha512-Yv0k4bXGOH+8a+7bELd2PqHQsuiANB+A8a4gnQrkRWzrkKlb6KHaVvyXhqs04sVW/OWlbPyYxRgYlIXLfrufMQ==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "requires": { "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.31", + "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "@types/express-serve-static-core": { - "version": "4.17.32", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.32.tgz", - "integrity": "sha512-aI5h/VOkxOF2Z1saPy0Zsxs5avets/iaiAJYznQFm5By/pamU31xWKL//epiF4OfUA2qTOc9PV6tCUjhO8wlZA==", + "version": "4.17.43", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz", + "integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==", "requires": { "@types/node": "*", "@types/qs": "*", - "@types/range-parser": "*" + "@types/range-parser": "*", + "@types/send": "*" } }, "@types/graceful-fs": { @@ -38117,14 +38343,20 @@ "dev": true }, "@types/jsonwebtoken": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz", - "integrity": "sha512-c5ltxazpWabia/4UzhIoaDcIza4KViOQhdbjRlfcIGVnsE3c3brkz9Z+F/EeJIECOQP7W7US2hNE930cWWkPiw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", + "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", "dev": true, "requires": { "@types/node": "*" } }, + "@types/jwk-to-pem": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/jwk-to-pem/-/jwk-to-pem-2.0.3.tgz", + "integrity": "sha512-I/WFyFgk5GrNbkpmt14auGO3yFK1Wt4jXzkLuI+fDBNtO5ZI2rbymyGd6bKzfSBEuyRdM64ZUwxU1+eDcPSOEQ==", + "dev": true + }, "@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -38192,6 +38424,22 @@ "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", "dev": true }, + "@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "requires": { + "@types/mime": "^1", + "@types/node": "*" + }, + "dependencies": { + "@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + } + } + }, "@types/serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", @@ -38843,6 +39091,21 @@ "fast-deep-equal": "^3.1.3" } }, + "angular-oauth2-oidc": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/angular-oauth2-oidc/-/angular-oauth2-oidc-17.0.1.tgz", + "integrity": "sha512-Yl4It9zFsYmoNS73sUvNJstbMW1x73ejKonzXLgU4XnSuBCt/0x8PnY5R3mHX4ZC/WmXBqQ/RfFwClrYW9Ywcg==", + "requires": { + "tslib": "^2.5.2" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, "ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -39018,6 +39281,17 @@ "safer-buffer": "~2.1.0" } }, + "asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -39390,6 +39664,11 @@ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "devOptional": true }, + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, "body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -39469,6 +39748,11 @@ "fill-range": "^7.0.1" } }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, "browser-process-hrtime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", @@ -39656,6 +39940,23 @@ } } }, + "cache-manager": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-5.4.0.tgz", + "integrity": "sha512-FS7o8vqJosnLpu9rh2gQTo8EOzCRJLF1BJ4XDEUDMqcfvs7SJZs5iuoFTXLauzQ3S5v8sBAST1pCwMaurpyi1A==", + "requires": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "^10.1.0", + "promise-coalesce": "^1.1.2" + }, + "dependencies": { + "lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==" + } + } + }, "cachedir": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", @@ -41065,6 +41366,20 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==" }, + "elliptic": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.5.tgz", + "integrity": "sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw==", + "requires": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "emittery": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", @@ -42837,6 +43152,15 @@ "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", "dev": true }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "hdr-histogram-js": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz", @@ -42867,6 +43191,16 @@ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "hosted-git-info": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.2.1.tgz", @@ -45898,14 +46232,43 @@ "dev": true }, "jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", "requires": { "jws": "^3.2.2", - "lodash": "^4.17.21", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", "ms": "^2.1.1", - "semver": "^7.3.8" + "semver": "^7.5.4" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } } }, "jsprim": { @@ -45930,6 +46293,16 @@ "safe-buffer": "^5.0.1" } }, + "jwk-to-pem": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.5.tgz", + "integrity": "sha512-L90jwellhO8jRKYwbssU9ifaMVqajzj3fpRjDKcsDzrslU9syRbFqfkXtT4B89HYAap+xsxNcxgBSB09ig+a7A==", + "requires": { + "asn1.js": "^5.3.0", + "elliptic": "^6.5.4", + "safe-buffer": "^5.0.1" + } + }, "jws": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", @@ -46115,6 +46488,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -46127,12 +46505,42 @@ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "dev": true }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, "lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "dev": true }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -46147,8 +46555,7 @@ "lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "devOptional": true + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, "lodash.uniq": { "version": "4.5.0", @@ -46557,6 +46964,11 @@ "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + }, "minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz", @@ -48783,6 +49195,11 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "promise-coalesce": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/promise-coalesce/-/promise-coalesce-1.1.2.tgz", + "integrity": "sha512-zLaJ9b8hnC564fnJH6NFSOGZYYdzrAJn2JUUIwzoQb32fG2QAakpDNM+CZo1km6keXkRXRM+hml1BFAPVnPkxg==" + }, "promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", diff --git a/package.json b/package.json index 1fa9fa48..afade40b 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@kobalte/core": "^0.4.0", "@morphic-ts/adt": "^3.0.0", "@nestjs/axios": "^2.0.0", + "@nestjs/cache-manager": "^2.2.2", "@nestjs/common": "^9.0.0", "@nestjs/core": "^9.0.0", "@nestjs/platform-express": "^9.0.0", @@ -47,12 +48,15 @@ "@turf/boolean-within": "^6.5.0", "@turf/helpers": "^6.5.0", "@turf/midpoint": "^6.5.0", + "angular-oauth2-oidc": "^17.0.1", "bezier-easing": "^2.1.0", + "cache-manager": "^5.4.0", "csv-parse": "^5.3.3", "cuid": "^3.0.0", "fp-ts": "^2.13.1", "io-ts": "^2.2.20", "jsonwebtoken": "^9.0.0", + "jwk-to-pem": "^2.0.5", "monocle-ts": "^2.3.13", "newtype-ts": "^0.3.5", "ngx-window-token": "^6.0.0", @@ -93,6 +97,7 @@ "@nrwl/workspace": "15.5.2", "@types/jest": "28.1.1", "@types/jsonwebtoken": "^9.0.1", + "@types/jwk-to-pem": "^2.0.3", "@types/multer": "^1.4.7", "@types/node": "18.7.1", "@types/validator": "^13.7.10",