From f1fa9464a9f0f9a16b1a6cb915c235b3982dac79 Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Tue, 26 Jul 2022 17:53:13 +0300 Subject: [PATCH] add base user service --- src/index.ts | 1 + src/lib/auth/base-user.service.ts | 121 ++++++++++++++++++ src/lib/auth/default-user.service.ts | 10 ++ src/lib/auth/index.ts | 7 + src/lib/auth/types/auth-module-options.ts | 8 ++ src/lib/auth/types/create-user.request.ts | 8 ++ .../auth/types/my-profile-update.request.ts | 5 + src/lib/auth/types/profile-update.request.ts | 8 ++ src/lib/auth/types/reset-password.request.ts | 4 + src/lib/auth/types/user.response.ts | 10 ++ src/lib/auth/user.model.ts | 47 +++++++ src/lib/utils/index.ts | 1 + 12 files changed, 230 insertions(+) create mode 100644 src/lib/auth/base-user.service.ts create mode 100644 src/lib/auth/default-user.service.ts create mode 100644 src/lib/auth/index.ts create mode 100644 src/lib/auth/types/auth-module-options.ts create mode 100644 src/lib/auth/types/create-user.request.ts create mode 100644 src/lib/auth/types/my-profile-update.request.ts create mode 100644 src/lib/auth/types/profile-update.request.ts create mode 100644 src/lib/auth/types/reset-password.request.ts create mode 100644 src/lib/auth/types/user.response.ts create mode 100644 src/lib/auth/user.model.ts diff --git a/src/index.ts b/src/index.ts index 65baca2..bab98dc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,3 +17,4 @@ export * from './lib/search'; export * from './lib/empty-states'; export * from './lib/scrollbar'; export * from './lib/caching'; +export * from './lib/auth'; diff --git a/src/lib/auth/base-user.service.ts b/src/lib/auth/base-user.service.ts new file mode 100644 index 0000000..f5acb54 --- /dev/null +++ b/src/lib/auth/base-user.service.ts @@ -0,0 +1,121 @@ +import { inject, Injectable } from '@angular/core'; +import { KeycloakService } from 'keycloak-angular'; +import { BehaviorSubject, firstValueFrom, Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import jwt_decode from 'jwt-decode'; +import { BASE_HREF, List, mapEach, RequiredParam, Validate } from '../utils'; +import { QueryParam } from '../services'; +import { CacheApiService } from '../caching'; +import { EntitiesService } from '../listing'; +import { IqserUser } from './user.model'; +import { IIqserUser } from './types/user.response'; +import { ICreateUserRequest } from './types/create-user.request'; +import { IResetPasswordRequest } from './types/reset-password.request'; +import { IMyProfileUpdateRequest } from './types/my-profile-update.request'; +import { IProfileUpdateRequest } from './types/profile-update.request'; +import { KeycloakProfile } from 'keycloak-js'; + +@Injectable() +export abstract class BaseUserService extends EntitiesService< + Interface, + Class +> { + readonly currentUser$: Observable; + protected abstract readonly _defaultModelPath: string; + protected abstract readonly _entityClass: new (entityInterface: Interface | KeycloakProfile, ...args: unknown[]) => Class; + protected readonly _currentUser$ = new BehaviorSubject(undefined); + protected readonly _baseHref = inject(BASE_HREF); + protected readonly _keycloakService = inject(KeycloakService); + protected readonly _cacheApiService = inject(CacheApiService); + + constructor() { + super(); + this.currentUser$ = this._currentUser$.asObservable(); + } + + get currentUser(): Class | undefined { + return this._currentUser$.value; + } + + async initialize(): Promise { + await this.loadCurrentUser(); + + console.log('Initializing users for: ', this.currentUser); + if (!this.currentUser) { + return; + } + + await firstValueFrom(this.loadAll()); + } + + logout() { + void this._cacheApiService.wipeCaches().then(); + void this._keycloakService.logout(window.location.origin + this._baseHref).then(); + } + + loadAll() { + return this.getAll().pipe( + mapEach(user => new this._entityClass(user, user.roles, user.userId)), + tap(users => this.setEntities(users)), + ); + } + + async loadCurrentUser(): Promise { + const token = await this._keycloakService.getToken(); + const decoded = jwt_decode(token); + const userId = (<{ sub: string }>decoded).sub; + + const roles = this._keycloakService.getUserRoles(true).filter(role => role.startsWith('RED_')); + const user = new this._entityClass(await this._keycloakService.loadUserProfile(true), roles, userId); + this.replace(user); + + this._currentUser$.next(this.find(userId)); + return user; + } + + getName(user: string | Interface | Class): string | undefined { + const userId = typeof user === 'string' ? user : user.userId; + return this.find(userId)?.name; + } + + getAll(): Observable { + return super.getAll(this._defaultModelPath, [{ key: 'refreshCache', value: true }]); + } + + @Validate() + updateProfile(@RequiredParam() body: T, @RequiredParam() userId: string) { + return this._post(body, `${this._defaultModelPath}/profile/${userId}`); + } + + @Validate() + updateMyProfile(@RequiredParam() body: T) { + return this._post(body, `${this._defaultModelPath}/my-profile`); + } + + @Validate() + resetPassword(@RequiredParam() body: T, @RequiredParam() userId: string) { + return this._post(body, `${this._defaultModelPath}/${userId}/reset-password`); + } + + @Validate() + create(@RequiredParam() body: T) { + return this._post(body); + } + + delete(userIds: List) { + const queryParams = userIds.map(userId => ({ key: 'userId', value: userId })); + return super.delete(userIds, this._defaultModelPath, queryParams); + } + + find(id: string): Class | undefined { + if (id?.toLowerCase() === 'system') { + return new this._entityClass({ username: 'System' }, [], 'system'); + } + + return super.find(id) || new this._entityClass({ username: 'Deleted User' }, [], 'deleted'); + } +} + +export function getCurrentUser() { + return inject(BaseUserService).currentUser; +} diff --git a/src/lib/auth/default-user.service.ts b/src/lib/auth/default-user.service.ts new file mode 100644 index 0000000..83a4d8c --- /dev/null +++ b/src/lib/auth/default-user.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@angular/core'; +import { IqserUser } from './user.model'; +import { IIqserUser } from './types/user.response'; +import { BaseUserService } from './base-user.service'; + +@Injectable() +export class DefaultUserService extends BaseUserService { + protected readonly _defaultModelPath = 'user'; + protected readonly _entityClass = IqserUser; +} diff --git a/src/lib/auth/index.ts b/src/lib/auth/index.ts new file mode 100644 index 0000000..5e9e86f --- /dev/null +++ b/src/lib/auth/index.ts @@ -0,0 +1,7 @@ +export * from './types/user.response'; +export * from './types/create-user.request'; +export * from './types/reset-password.request'; +export * from './types/my-profile-update.request'; +export * from './types/profile-update.request'; +export * from './user.model'; +export * from './base-user.service'; diff --git a/src/lib/auth/types/auth-module-options.ts b/src/lib/auth/types/auth-module-options.ts new file mode 100644 index 0000000..4174072 --- /dev/null +++ b/src/lib/auth/types/auth-module-options.ts @@ -0,0 +1,8 @@ +import { Type } from '@angular/core'; +import { BaseUserService } from '../base-user.service'; +import { IIqserUser } from './user.response'; +import { IqserUser } from '../user.model'; + +export interface AuthModuleOptions = BaseUserService> { + existingUserService: Type; +} diff --git a/src/lib/auth/types/create-user.request.ts b/src/lib/auth/types/create-user.request.ts new file mode 100644 index 0000000..e03a148 --- /dev/null +++ b/src/lib/auth/types/create-user.request.ts @@ -0,0 +1,8 @@ +import { List } from '../../utils'; + +export interface ICreateUserRequest { + email: string; + firstName?: string; + lastName?: string; + roles?: List; +} diff --git a/src/lib/auth/types/my-profile-update.request.ts b/src/lib/auth/types/my-profile-update.request.ts new file mode 100644 index 0000000..074b990 --- /dev/null +++ b/src/lib/auth/types/my-profile-update.request.ts @@ -0,0 +1,5 @@ +export interface IMyProfileUpdateRequest { + email?: string; + firstName?: string; + lastName?: string; +} diff --git a/src/lib/auth/types/profile-update.request.ts b/src/lib/auth/types/profile-update.request.ts new file mode 100644 index 0000000..973e81d --- /dev/null +++ b/src/lib/auth/types/profile-update.request.ts @@ -0,0 +1,8 @@ +import { List } from '../../utils'; + +export interface IProfileUpdateRequest { + email?: string; + firstName?: string; + lastName?: string; + roles?: List; +} diff --git a/src/lib/auth/types/reset-password.request.ts b/src/lib/auth/types/reset-password.request.ts new file mode 100644 index 0000000..235c160 --- /dev/null +++ b/src/lib/auth/types/reset-password.request.ts @@ -0,0 +1,4 @@ +export interface IResetPasswordRequest { + password: string; + temporary: boolean; +} diff --git a/src/lib/auth/types/user.response.ts b/src/lib/auth/types/user.response.ts new file mode 100644 index 0000000..c5e3775 --- /dev/null +++ b/src/lib/auth/types/user.response.ts @@ -0,0 +1,10 @@ +import { List } from '../../utils'; + +export interface IIqserUser { + readonly userId: string; + readonly username: string; + readonly roles?: List; + readonly email?: string; + readonly firstName?: string; + readonly lastName?: string; +} diff --git a/src/lib/auth/user.model.ts b/src/lib/auth/user.model.ts new file mode 100644 index 0000000..74c272c --- /dev/null +++ b/src/lib/auth/user.model.ts @@ -0,0 +1,47 @@ +import { KeycloakProfile } from 'keycloak-js'; +import { IIqserUser } from './types/user.response'; +import { IListable } from '../listing'; +import { List } from '../utils'; + +export class IqserUser implements IIqserUser, IListable { + readonly username: string; + readonly email?: string; + readonly firstName?: string; + readonly lastName?: string; + readonly name: string; + readonly searchKey: string; + + readonly hasAnyRole = this.roles.length > 0; + + constructor(user: KeycloakProfile | IIqserUser, ...args: unknown[]); + constructor(user: KeycloakProfile | IIqserUser, readonly roles: List, readonly userId: string) { + this.email = user.email; + this.username = user.username ?? this.email ?? 'unknown user'; + this.firstName = user.firstName; + this.lastName = user.lastName; + this.name = this.firstName && this.lastName ? `${this.firstName} ${this.lastName}` : this.username; + this.searchKey = `${this.name || '-'}${this.username || '-'}${this.email || ''}`; + } + + get id() { + return this.userId; + } + + has(role: string): boolean { + return this.roles.includes(role); + } + + hasAny(roles: List): boolean { + if (roles?.length > 0) { + for (const role of roles) { + if (this.has(role)) { + return true; + } + } + + return false; + } + + return true; + } +} diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 005e9ab..3d537e6 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -19,3 +19,4 @@ export * from './headers-configuration'; export * from './context.component'; export * from './tokens'; export * from './base-app-config'; +export * from './types/common-ui-options';