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 @@