add base user service

This commit is contained in:
Dan Percic 2022-07-26 17:53:13 +03:00
parent 93d3f294d6
commit f1fa9464a9
12 changed files with 230 additions and 0 deletions

View File

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

View File

@ -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<Interface extends IIqserUser, Class extends IqserUser & Interface> extends EntitiesService<
Interface,
Class
> {
readonly currentUser$: Observable<Class | undefined>;
protected abstract readonly _defaultModelPath: string;
protected abstract readonly _entityClass: new (entityInterface: Interface | KeycloakProfile, ...args: unknown[]) => Class;
protected readonly _currentUser$ = new BehaviorSubject<Class | undefined>(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<void> {
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<Class> {
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<Interface[]> {
return super.getAll(this._defaultModelPath, [{ key: 'refreshCache', value: true }]);
}
@Validate()
updateProfile<T = IProfileUpdateRequest>(@RequiredParam() body: T, @RequiredParam() userId: string) {
return this._post<unknown>(body, `${this._defaultModelPath}/profile/${userId}`);
}
@Validate()
updateMyProfile<T = IMyProfileUpdateRequest>(@RequiredParam() body: T) {
return this._post<unknown>(body, `${this._defaultModelPath}/my-profile`);
}
@Validate()
resetPassword<T = IResetPasswordRequest>(@RequiredParam() body: T, @RequiredParam() userId: string) {
return this._post<unknown>(body, `${this._defaultModelPath}/${userId}/reset-password`);
}
@Validate()
create<T = ICreateUserRequest>(@RequiredParam() body: T) {
return this._post(body);
}
delete(userIds: List) {
const queryParams = userIds.map<QueryParam>(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;
}

View File

@ -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<IIqserUser, IqserUser> {
protected readonly _defaultModelPath = 'user';
protected readonly _entityClass = IqserUser;
}

7
src/lib/auth/index.ts Normal file
View File

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

View File

@ -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<I extends IIqserUser, C extends IqserUser & I, T extends BaseUserService<I, C> = BaseUserService<I, C>> {
existingUserService: Type<T>;
}

View File

@ -0,0 +1,8 @@
import { List } from '../../utils';
export interface ICreateUserRequest {
email: string;
firstName?: string;
lastName?: string;
roles?: List;
}

View File

@ -0,0 +1,5 @@
export interface IMyProfileUpdateRequest {
email?: string;
firstName?: string;
lastName?: string;
}

View File

@ -0,0 +1,8 @@
import { List } from '../../utils';
export interface IProfileUpdateRequest {
email?: string;
firstName?: string;
lastName?: string;
roles?: List;
}

View File

@ -0,0 +1,4 @@
export interface IResetPasswordRequest {
password: string;
temporary: boolean;
}

View File

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

View File

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

View File

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