common-ui/src/lib/users/services/iqser-user.service.ts
2023-01-18 12:52:43 +08:00

174 lines
6.7 KiB
TypeScript

import { inject, Injectable } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';
import { BehaviorSubject, firstValueFrom, Observable, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { BASE_HREF, List, mapEach, RequiredParam, Validate } from '../../utils';
import { QueryParam, Toaster } from '../../services';
import { CacheApiService } from '../../caching';
import { EntitiesService } from '../../listing';
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';
import { IqserUser } from '../iqser-user.model';
import { IqserPermissionsService, IqserRolesService } from '../../permissions';
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { IqserConfigService } from '../../services';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@Injectable()
export abstract class IqserUserService<
Interface extends IIqserUser = IIqserUser,
Class extends IqserUser & Interface = IqserUser & Interface,
> extends EntitiesService<Interface, Class> {
readonly currentUser$: Observable<Class | undefined>;
protected abstract readonly _defaultModelPath: string;
protected abstract readonly _permissionsFilter: (role: string) => boolean;
protected abstract readonly _rolesFilter: (role: string) => boolean;
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 _toaster = inject(Toaster);
protected readonly _configService = inject(IqserConfigService);
protected readonly _keycloakService = inject(KeycloakService);
protected readonly _cacheApiService = inject(CacheApiService);
protected readonly _permissionsService = inject(IqserPermissionsService, { optional: true });
protected readonly _rolesService = inject(IqserRolesService, { optional: true });
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());
}
async logout() {
try {
await this._keycloakService.loadUserProfile(true);
await this._cacheApiService.wipeCaches();
await this._keycloakService.logout();
} catch (e) {
await this.redirectToLogin();
}
}
async redirectToLogin() {
await this._cacheApiService.wipeCaches();
window.location.href = this._keycloakService.getKeycloakInstance().createLoginUrl({
redirectUri: window.location.origin + this._baseHref,
idpHint: this._configService.values.OAUTH_IDP_HINT
});
}
loadAll() {
return this.getAll().pipe(
mapEach(user => new this._entityClass(user, user.roles, user.userId)),
tap(users => this.setEntities(users)),
);
}
async loadCurrentUser(): Promise<Class | undefined> {
let profile: KeycloakProfile | undefined;
try {
profile = await this._keycloakService.loadUserProfile(true);
if (!profile.id) {
throw new Error('No user id');
}
} catch (e) {
console.log('Load KC profile failed');
await this.redirectToLogin();
return;
}
const all = this._keycloakService.getUserRoles(true);
const permissions = all.filter(role => this._permissionsFilter(role));
const roles = all.filter(role => this._rolesFilter(role));
this._permissionsService?.load(permissions);
this._rolesService?.load(roles);
const user = new this._entityClass(profile, roles, profile.id);
this.replace(user);
this._currentUser$.next(this.find(profile.id));
return user;
}
getName(user: string | Interface | Class): string | undefined {
if (!user) {
return;
}
const userId = typeof user === 'string' ? user : user.userId;
return this.find(userId)?.name;
}
getAll(url = this._defaultModelPath): Observable<Interface[]> {
return super.getAll(url, [{ 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) {
const showToast = (error: HttpErrorResponse) => {
if (error.status === HttpStatusCode.BadRequest) {
const { message } = error.error;
this._toaster.error(_('update-profile.errors.bad-request'), { params: { message } });
} else {
this._toaster.error(_('update-profile.errors.generic'));
}
return throwError(() => error);
};
return this._post<unknown>(body, `${this._defaultModelPath}/my-profile`).pipe(catchError(showToast));
}
@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');
}
if (!id) {
return undefined;
}
return super.find(id) ?? new this._entityClass({ username: 'Deleted User' }, [], 'deleted');
}
}
export function getCurrentUser<Class extends IqserUser = IqserUser>() {
return inject<IqserUserService<IIqserUser, Class>>(IqserUserService).currentUser;
}