RED-4718: use common-ui user related stuff

This commit is contained in:
Dan Percic 2022-07-27 12:08:27 +03:00
parent 164c527ce7
commit b57b600fc0
31 changed files with 108 additions and 368 deletions

View File

@ -9,7 +9,7 @@ import { ApiPathInterceptor } from '@utils/api-path-interceptor';
import { MissingTranslationHandler, TranslateCompiler, TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { MissingTranslationHandler, TranslateCompiler, TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { import {
BASE_HREF, BASE_HREF,
BaseConfigService, BaseUserService,
CachingModule, CachingModule,
CommonUiModule, CommonUiModule,
HELP_DOCS, HELP_DOCS,
@ -24,7 +24,6 @@ import {
import { ToastrModule } from 'ngx-toastr'; import { ToastrModule } from 'ngx-toastr';
import { ServiceWorkerModule } from '@angular/service-worker'; import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '@environments/environment'; import { environment } from '@environments/environment';
import { AuthModule } from './modules/auth/auth.module';
import { AuthErrorComponent } from '@components/auth-error/auth-error.component'; import { AuthErrorComponent } from '@components/auth-error/auth-error.component';
import { NotificationsComponent } from '@components/notifications/notifications.component'; import { NotificationsComponent } from '@components/notifications/notifications.component';
import { DownloadsListScreenComponent } from '@components/downloads-list-screen/downloads-list-screen.component'; import { DownloadsListScreenComponent } from '@components/downloads-list-screen/downloads-list-screen.component';
@ -60,6 +59,7 @@ import { PdfViewerModule } from './modules/pdf-viewer/pdf-viewer.module';
import { LicenseService } from '@services/license.service'; import { LicenseService } from '@services/license.service';
import { TenantIdInterceptor } from '@utils/tenant-id-interceptor'; import { TenantIdInterceptor } from '@utils/tenant-id-interceptor';
import { UI_CACHES } from '@utils/constants'; import { UI_CACHES } from '@utils/constants';
import { AuthModule } from './modules/auth/auth.module';
export function httpLoaderFactory(httpClient: HttpClient, configService: ConfigService): PruningTranslationLoader { export function httpLoaderFactory(httpClient: HttpClient, configService: ConfigService): PruningTranslationLoader {
return new PruningTranslationLoader(httpClient, '/assets/i18n/', `.json?version=${configService.values.FRONTEND_APP_VERSION}`); return new PruningTranslationLoader(httpClient, '/assets/i18n/', `.json?version=${configService.values.FRONTEND_APP_VERSION}`);
@ -78,14 +78,19 @@ export const appModuleFactory = (config: AppConfig) => {
SharedModule, SharedModule,
FileUploadDownloadModule, FileUploadDownloadModule,
HttpClientModule, HttpClientModule,
AuthModule,
AppRoutingModule, AppRoutingModule,
MonacoEditorModule, MonacoEditorModule,
IqserHelpModeModule, IqserHelpModeModule,
CommonUiModule.forRoot({ CommonUiModule.forRoot({
existingUserPreferenceService: UserPreferenceService, existingUserPreferenceService: UserPreferenceService,
configServiceFactory: () => new BaseConfigService(config), configService: ConfigService,
configServiceFactory: () => new ConfigService(config),
}), }),
AuthModule,
// AuthModule.forRoot({
// existingUserService: UserService,
// existingRoleGuard: RedRoleGuard,
// }),
CachingModule.forRoot(UI_CACHES), CachingModule.forRoot(UI_CACHES),
PdfViewerModule, PdfViewerModule,
ToastrModule.forRoot({ ToastrModule.forRoot({
@ -155,6 +160,10 @@ export const appModuleFactory = (config: AppConfig) => {
provide: ErrorHandler, provide: ErrorHandler,
useClass: GlobalErrorHandler, useClass: GlobalErrorHandler,
}, },
{
provide: BaseUserService,
useExisting: UserService,
},
{ {
provide: HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,
multi: true, multi: true,

View File

@ -1,10 +1,10 @@
import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { AdminDialogService } from '../../../services/admin-dialog.service'; import { AdminDialogService } from '../../../services/admin-dialog.service';
import { BaseFormComponent, IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui'; import { BaseFormComponent, IconButtonTypes, IProfileUpdateRequest, LoadingService, Toaster } from '@iqser/common-ui';
import { rolesTranslations } from '@translations/roles-translations'; import { rolesTranslations } from '@translations/roles-translations';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { IProfileUpdateRequest, User } from '@red/domain'; import { User } from '@red/domain';
import { UserService } from '@services/user.service'; import { UserService } from '@services/user.service';
import { HttpStatusCode } from '@angular/common/http'; import { HttpStatusCode } from '@angular/common/http';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
@ -53,7 +53,7 @@ export class UserDetailsComponent extends BaseFormComponent implements OnChanges
...prev, ...prev,
[role]: [ [role]: [
{ {
value: this.user && this.user.hasRole(role), value: this.user && this.user.has(role),
disabled: this.shouldBeDisabled(role), disabled: this.shouldBeDisabled(role),
}, },
], ],

View File

@ -52,7 +52,7 @@
<div class="center cell"> <div class="center cell">
<mat-slide-toggle <mat-slide-toggle
(toggleChange)="toggleActive(user)" (toggleChange)="toggleActive(user)"
[checked]="user.isActive" [checked]="user.hasAnyRole"
[disabled]="!canDeactivate(user)" [disabled]="!canDeactivate(user)"
color="primary" color="primary"
></mat-slide-toggle> ></mat-slide-toggle>

View File

@ -109,7 +109,7 @@ export class UserListingScreenComponent extends ListingComponent<User> implement
async toggleActive(user: User) { async toggleActive(user: User) {
this._loadingService.start(); this._loadingService.start();
const requestBody = { ...user, roles: user.isActive ? [] : ['RED_USER'] }; const requestBody = { ...user, roles: user.hasAnyRole ? [] : ['RED_USER'] };
await firstValueFrom(this._userService.updateProfile(requestBody, user.id)); await firstValueFrom(this._userService.updateProfile(requestBody, user.id));
await this.#loadData(); await this.#loadData();
} }

View File

@ -43,7 +43,7 @@ export class ConfigService {
userId => userId =>
new NestedFilter({ new NestedFilter({
id: userId, id: userId,
label: this._userService.getNameForId(userId), label: this._userService.getName(userId),
}), }),
); );

View File

@ -5,7 +5,6 @@ import { HttpClientModule } from '@angular/common/http';
import { KeycloakAngularModule, KeycloakOptions, KeycloakService } from 'keycloak-angular'; import { KeycloakAngularModule, KeycloakOptions, KeycloakService } from 'keycloak-angular';
import { ConfigService } from '@services/config.service'; import { ConfigService } from '@services/config.service';
import { BASE_HREF } from '@iqser/common-ui'; import { BASE_HREF } from '@iqser/common-ui';
import { firstValueFrom } from 'rxjs';
function getKeycloakOptions(configService: ConfigService, baseUrl: string) { function getKeycloakOptions(configService: ConfigService, baseUrl: string) {
let url: string = configService.values.OAUTH_URL; let url: string = configService.values.OAUTH_URL;
@ -37,11 +36,9 @@ function configureAutomaticRedirectToLoginScreen(keyCloakService: KeycloakServic
export function keycloakInitializer(keycloakService: KeycloakService, configService: ConfigService, baseUrl: string): () => Promise<void> { export function keycloakInitializer(keycloakService: KeycloakService, configService: ConfigService, baseUrl: string): () => Promise<void> {
return () => return () =>
firstValueFrom(configService.loadLocalConfig()).then(() =>
keycloakService keycloakService
.init(getKeycloakOptions(configService, baseUrl)) .init(getKeycloakOptions(configService, baseUrl))
.then(() => configureAutomaticRedirectToLoginScreen(keycloakService)), .then(() => configureAutomaticRedirectToLoginScreen(keycloakService));
);
} }
@NgModule({ @NgModule({

View File

@ -1,71 +1,51 @@
import { Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { UserService } from '@services/user.service'; import { UserService } from '@services/user.service';
import { LoadingService } from '@iqser/common-ui'; import { RoleGuard } from '@iqser/common-ui';
import { Observable } from 'rxjs';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class RedRoleGuard implements CanActivate { export class RedRoleGuard extends RoleGuard {
constructor( protected readonly _userService = inject(UserService);
protected readonly _router: Router,
private readonly _loadingService: LoadingService,
private readonly _userService: UserService,
) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> { async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
return new Observable(obs => { const currentUser = this._userService.currentUser;
if (!this._userService.currentUser.hasAnyREDRoles) {
this._router.navigate(['/auth-error']); if (!currentUser.hasAnyREDRoles) {
await this._router.navigate(['/auth-error']);
this._loadingService.stop(); this._loadingService.stop();
obs.next(false); return false;
obs.complete();
return;
} }
// we have at least 1 RED Role -> if it's not user he must be admin
if ( // we have at least 1 RED Role -> if it's not user he must be an admin
this._userService.currentUser.isUserAdmin && if (currentUser.isUserAdmin && !currentUser.isAdmin && state.url.includes('admin') && !state.url.includes('users')) {
!this._userService.currentUser.isAdmin && await this._router.navigate(['/main/admin/users']);
state.url.includes('admin') && return false;
!state.url.includes('users')
) {
this._router.navigate(['/main/admin/users']);
obs.next(false);
obs.complete();
return;
} }
if ( if (
this._userService.currentUser.isUserAdmin && currentUser.isUserAdmin &&
!this._userService.currentUser.isAdmin && !currentUser.isAdmin &&
!this._userService.currentUser.isUser && !currentUser.isUser &&
!(state.url.startsWith('/main/admin/users') || state.url.startsWith('/main/my-profile')) !(state.url.startsWith('/main/admin/users') || state.url.startsWith('/main/my-profile'))
) { ) {
this._router.navigate(['/main/admin/users']); await this._router.navigate(['/main/admin/users']);
obs.next(false); return false;
obs.complete();
return;
} }
if (route.data.requiredRoles) { if (route.data.requiredRoles) {
if (this._userService.hasAnyRole(route.data.requiredRoles)) { if (currentUser.hasAny(route.data.requiredRoles)) {
obs.next(true); return true;
obs.complete();
} else {
if (!this._userService.currentUser.isUser) {
this._router.navigate(['/main/admin']);
} else {
this._router.navigate(['/']);
}
obs.next(false);
obs.complete();
} }
if (!currentUser.isUser) {
await this._router.navigate(['/main/admin']);
} else { } else {
obs.next(true); await this._router.navigate(['/']);
obs.complete();
} }
}); return false;
}
return true;
} }
} }

View File

@ -65,7 +65,7 @@ export class DossierDetailsComponent {
const dossierRequest: IDossierRequest = { ...dossier, ownerId: owner.id }; const dossierRequest: IDossierRequest = { ...dossier, ownerId: owner.id };
await firstValueFrom(this._dossiersService.createOrUpdate(dossierRequest)); await firstValueFrom(this._dossiersService.createOrUpdate(dossierRequest));
const ownerName = this._userService.getNameForId(owner.id); const ownerName = this._userService.getName(owner.id);
const dossierName = dossier.dossierName; const dossierName = dossier.dossierName;
this._toaster.info(_('assignment.owner'), { params: { ownerName, dossierName } }); this._toaster.info(_('assignment.owner'), { params: { ownerName, dossierName } });
} }

View File

@ -68,7 +68,7 @@ export class DossierOverviewScreenHeaderComponent implements OnInit {
const fileName = this.dossier.dossierName + '.export.csv'; const fileName = this.dossier.dossierName + '.export.csv';
const mapper = (file?: File) => ({ const mapper = (file?: File) => ({
...file, ...file,
assignee: this._userService.getNameForId(file.assignee) || '-', assignee: this._userService.getName(file.assignee) || '-',
primaryAttribute: this._primaryFileAttributeService.getPrimaryFileAttributeValue(file, this.dossier.dossierTemplateId), primaryAttribute: this._primaryFileAttributeService.getPrimaryFileAttributeValue(file, this.dossier.dossierTemplateId),
}); });
const fileFields = [ const fileFields = [

View File

@ -284,7 +284,7 @@ export class ConfigService {
peopleFilters.push( peopleFilters.push(
new NestedFilter({ new NestedFilter({
id: userId, id: userId,
label: this._userService.getNameForId(userId), label: this._userService.getName(userId),
}), }),
); );
}); });

View File

@ -142,7 +142,7 @@ export class ConfigService {
userId => userId =>
new NestedFilter({ new NestedFilter({
id: userId, id: userId,
label: this._userService.getNameForId(userId), label: this._userService.getName(userId),
}), }),
); );

View File

@ -97,7 +97,7 @@ export class UserManagementComponent {
async assignReviewer(file: File, user: User | string) { async assignReviewer(file: File, user: User | string) {
const assigneeId = typeof user === 'string' ? user : user?.id; const assigneeId = typeof user === 'string' ? user : user?.id;
const reviewerName = this.userService.getNameForId(assigneeId); const reviewerName = this.userService.getName(assigneeId);
const { dossierId, filename } = file; const { dossierId, filename } = file;
this.loadingService.start(); this.loadingService.start();

View File

@ -196,7 +196,7 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
const assignee = this._routeAssignee; const assignee = this._routeAssignee;
const assigneeToFilter = (userId: string) => { const assigneeToFilter = (userId: string) => {
const checked = assignee === userId; const checked = assignee === userId;
return new NestedFilter({ id: userId, label: this._userService.getNameForId(userId), checked }); return new NestedFilter({ id: userId, label: this._userService.getName(userId), checked });
}; };
const assigneeFilter: IFilterGroup = { const assigneeFilter: IFilterGroup = {
slug: 'assignee', slug: 'assignee',

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { UserService } from '@services/user.service'; import { UserService } from '@services/user.service';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Dossier, IDossierRequest } from '@red/domain'; import { Dossier, IDossierRequest } from '@red/domain';
@ -135,7 +135,7 @@ export class EditDossierTeamComponent implements EditDossierSectionInterface, On
setMembersSelectOptions(value = this.searchQuery): void { setMembersSelectOptions(value = this.searchQuery): void {
this.membersSelectOptions = this.userService.eligibleUsers this.membersSelectOptions = this.userService.eligibleUsers
.filter(user => this.userService.getNameForId(user.id).toLowerCase().includes(value.toLowerCase())) .filter(user => this.userService.getName(user.id).toLowerCase().includes(value.toLowerCase()))
.filter(user => this.selectedOwnerId !== user.id) .filter(user => this.selectedOwnerId !== user.id)
.map(user => user.id); .map(user => user.id);
} }

View File

@ -39,7 +39,7 @@ export class InitialsAvatarComponent implements OnInit, OnChanges {
} }
get disabled(): boolean { get disabled(): boolean {
return this._user && !this._isSystemUser && !this._user.isActive; return this._user && !this._isSystemUser && !this._user.hasAnyRole;
} }
get isCurrentUser(): boolean { get isCurrentUser(): boolean {

View File

@ -54,6 +54,6 @@ export class NamePipe implements PipeTransform {
if (!user) { if (!user) {
return undefined; return undefined;
} }
return typeof user === 'string' ? this._userService.getNameForId(user) : user.name; return typeof user === 'string' ? this._userService.getName(user) : user.name;
} }
} }

View File

@ -1,55 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { BaseConfigService } from '@iqser/common-ui';
import { Title } from '@angular/platform-browser';
import packageInfo from '../../../../../package.json';
import envConfig from '../../assets/config/config.json';
import { CacheApiService, wipeAllCaches } from '@iqser/common-ui';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { AppConfig } from '@red/domain'; import { AppConfig } from '@red/domain';
const version = packageInfo.version; @Injectable()
export class ConfigService extends BaseConfigService<AppConfig> {}
@Injectable({
providedIn: 'root',
})
export class ConfigService {
constructor(
private readonly _httpClient: HttpClient,
private readonly _cacheApiService: CacheApiService,
private readonly _titleService: Title,
) {
this._checkFrontendVersion();
}
private _values: AppConfig = { ...envConfig, FRONTEND_APP_VERSION: version } as const;
get values() {
return this._values;
}
loadLocalConfig(): Observable<any> {
return this._httpClient.get<any>('/assets/config/config.json').pipe(
tap(config => {
console.log('[REDACTION] Started with local config: ', config);
this._values = config;
}),
);
}
updateDisplayName(name: string): void {
this._values = { ...this._values, APP_NAME: name } as const;
this._titleService.setTitle(this._values.APP_NAME || 'RedactManager');
}
private _checkFrontendVersion(): void {
this._cacheApiService.getCachedValue('FRONTEND_APP_VERSION').then(async lastVersion => {
console.log('[REDACTION] Last app version: ', lastVersion, ' current version ', version);
if (lastVersion !== version) {
console.warn('[REDACTION] Version-mismatch - wiping caches!');
await wipeAllCaches();
}
await this._cacheApiService.cacheValue('FRONTEND_APP_VERSION', version);
});
}
}

View File

@ -71,7 +71,7 @@ export class TrashService extends EntitiesService<TrashItem, TrashItem> {
this._systemPreferencesService.values.softDeleteCleanupTime, this._systemPreferencesService.values.softDeleteCleanupTime,
this._permissionsService.canRestoreDossier(dossier), this._permissionsService.canRestoreDossier(dossier),
this._permissionsService.canHardDeleteDossier(dossier), this._permissionsService.canHardDeleteDossier(dossier),
this._userService.getNameForId(dossier.ownerId), this._userService.getName(dossier.ownerId),
), ),
), ),
switchMap(dossiers => this._dossierStatsService.getFor(dossiers.map(d => d.id)).pipe(map(() => dossiers))), switchMap(dossiers => this._dossierStatsService.getFor(dossiers.map(d => d.id)).pipe(map(() => dossiers))),
@ -81,7 +81,7 @@ export class TrashService extends EntitiesService<TrashItem, TrashItem> {
getFiles(dossierIds = this._activeDossiersService.all.map(d => d.id)): Observable<TrashFile[]> { getFiles(dossierIds = this._activeDossiersService.all.map(d => d.id)): Observable<TrashFile[]> {
return this._post<Record<string, IFile[]>>(dossierIds, 'status/softdeleted').pipe( return this._post<Record<string, IFile[]>>(dossierIds, 'status/softdeleted').pipe(
map(res => flatMap(Object.values(res))), map(res => flatMap(Object.values(res))),
mapEach(file => new File(file, this._userService.getNameForId(file.assignee))), mapEach(file => new File(file, this._userService.getName(file.assignee))),
mapEach(file => { mapEach(file => {
const dossier = this._activeDossiersService.find(file.dossierId); const dossier = this._activeDossiersService.find(file.dossierId);
return new TrashFile( return new TrashFile(
@ -90,7 +90,7 @@ export class TrashService extends EntitiesService<TrashItem, TrashItem> {
this._systemPreferencesService.values.softDeleteCleanupTime, this._systemPreferencesService.values.softDeleteCleanupTime,
this._permissionsService.canRestoreFile(file, dossier), this._permissionsService.canRestoreFile(file, dossier),
this._permissionsService.canHardDeleteFile(file, dossier), this._permissionsService.canHardDeleteFile(file, dossier),
this._userService.getNameForId(file.assignee), this._userService.getName(file.assignee),
dossier.dossierName, dossier.dossierName,
); );
}), }),

View File

@ -27,7 +27,7 @@ export class FilesService extends EntitiesService<IFile, File> {
/** Reload dossier files + stats. */ /** Reload dossier files + stats. */
loadAll(dossierId: string) { loadAll(dossierId: string) {
const files$ = this.getFor(dossierId).pipe( const files$ = this.getFor(dossierId).pipe(
mapEach(file => new File(file, this._userService.getNameForId(file.assignee))), mapEach(file => new File(file, this._userService.getName(file.assignee))),
tap(() => this._logger.info('[FILE] Loaded')), tap(() => this._logger.info('[FILE] Loaded')),
); );
const loadStats$ = files$.pipe(switchMap(files => this._dossierStatsService.getFor([dossierId]).pipe(map(() => files)))); const loadStats$ = files$.pipe(switchMap(files => this._dossierStatsService.getFor([dossierId]).pipe(map(() => files))));
@ -36,7 +36,7 @@ export class FilesService extends EntitiesService<IFile, File> {
reload(dossierId: string, file: File): Observable<boolean> { reload(dossierId: string, file: File): Observable<boolean> {
return super._getOne([dossierId, file.id]).pipe( return super._getOne([dossierId, file.id]).pipe(
map(_file => new File(_file, this._userService.getNameForId(_file.assignee))), map(_file => new File(_file, this._userService.getName(_file.assignee))),
switchMap(_file => this._dossierStatsService.getFor([dossierId]).pipe(map(() => _file))), switchMap(_file => this._dossierStatsService.getFor([dossierId]).pipe(map(() => _file))),
map(_file => this._filesMapService.replace([_file])), map(_file => this._filesMapService.replace([_file])),
tap(() => this._logger.info('[FILE] Reloaded')), tap(() => this._logger.info('[FILE] Reloaded')),

View File

@ -102,6 +102,6 @@ export class NotificationsService extends EntitiesService<INotification, Notific
} }
private _getUsername(userId: string | undefined) { private _getUsername(userId: string | undefined) {
return this._userService.getNameForId(userId) || this._translateService.instant(_('unknown')); return this._userService.getName(userId) || this._translateService.instant(_('unknown'));
} }
} }

View File

@ -1,32 +1,13 @@
import { inject, Inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { KeycloakService } from 'keycloak-angular'; import { User } from '@red/domain';
import jwt_decode from 'jwt-decode'; import { BaseUserService, IIqserUser } from '@iqser/common-ui';
import { ICreateUserRequest, IMyProfileUpdateRequest, IProfileUpdateRequest, IResetPasswordRequest, IUser, User } from '@red/domain';
import { BASE_HREF, CacheApiService, EntitiesService, List, mapEach, QueryParam, RequiredParam, Validate } from '@iqser/common-ui';
import { BehaviorSubject, firstValueFrom, Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class UserService extends EntitiesService<IUser, User> { export class UserService extends BaseUserService<IIqserUser, User> {
readonly currentUser$: Observable<User>;
protected readonly _defaultModelPath = 'user'; protected readonly _defaultModelPath = 'user';
protected readonly _entityClass = User; protected readonly _entityClass = User;
readonly #currentUser$ = new BehaviorSubject<User>(null);
constructor(
@Inject(BASE_HREF) private readonly _baseHref: string,
private readonly _keycloakService: KeycloakService,
private readonly _cacheApiService: CacheApiService,
) {
super();
this.currentUser$ = this.#currentUser$.asObservable();
}
get currentUser(): User {
return this.#currentUser$.value;
}
get managerUsers(): User[] { get managerUsers(): User[] {
return this.all.filter(user => user.isManager); return this.all.filter(user => user.isManager);
@ -36,109 +17,18 @@ export class UserService extends EntitiesService<IUser, User> {
return this.all.filter(user => user.isUser || user.isManager); return this.all.filter(user => user.isUser || user.isManager);
} }
async initialize(): Promise<void> {
console.log('[Redaction] Initializing users for: ', this.currentUser);
if (!this.currentUser) {
return;
}
if (this.currentUser.isUserAdmin || this.currentUser.isUser || this.currentUser.isAdmin) {
await firstValueFrom(this.loadAll());
}
}
logout() {
this._cacheApiService.wipeCaches().then();
this._keycloakService.logout().then();
}
loadAll() { loadAll() {
const all$ = this.currentUser.isUserAdmin ? this.getUsers() : this.getUsers(true); if (this.currentUser.isUserAdmin || this.currentUser.isUser || this.currentUser.isAdmin) {
return super.loadAll();
return all$.pipe(
mapEach(user => new User(user, user.roles, user.userId)),
tap(users => this.setEntities(users)),
);
}
async loadCurrentUser(): Promise<User> {
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_'));
let profile;
try {
profile = await this._keycloakService.loadUserProfile(true);
} catch (e) {
await this._keycloakService.logout();
console.log(e);
}
const user = new User(profile, roles, userId);
this.replace(user);
this.#currentUser$.next(this.find(userId));
return user;
}
getNameForId(userId: string): string | undefined {
return this.find(userId)?.name;
}
hasAnyRole(requiredRoles: string[], user = this.currentUser): boolean {
if (requiredRoles?.length > 0) {
for (const role of requiredRoles) {
if (user.hasRole(role)) {
return true;
} }
} }
return false; getAll() {
const url = this.currentUser.isUserAdmin ? this._defaultModelPath : `${this._defaultModelPath}/red`;
return super.getAll(url);
} }
return true; protected readonly _rolesFilter = (role: string) => role.startsWith('RED_');
}
getUsers(onlyRed = false, refreshCache = true): Observable<IUser[]> {
const url = onlyRed ? `${this._defaultModelPath}/red` : this._defaultModelPath;
return super.getAll(url, [{ key: 'refreshCache', value: refreshCache }]);
}
@Validate()
updateProfile(@RequiredParam() body: IProfileUpdateRequest, @RequiredParam() userId: string) {
return this._post<unknown>(body, `${this._defaultModelPath}/profile/${userId}`);
}
@Validate()
updateMyProfile(@RequiredParam() body: IMyProfileUpdateRequest) {
return this._post<unknown>(body, `${this._defaultModelPath}/my-profile`);
}
@Validate()
resetPassword(@RequiredParam() body: IResetPasswordRequest, @RequiredParam() userId: string) {
return this._post<unknown>(body, `${this._defaultModelPath}/${userId}/reset-password`);
}
@Validate()
create(@RequiredParam() body: ICreateUserRequest) {
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): User | undefined {
if (id?.toLowerCase() === 'system') {
return new User({ username: 'System' }, [], 'system');
}
if (!id) {
return undefined;
}
return super.find(id) || new User({ username: 'Deleted User' }, [], 'deleted');
}
} }
export function getCurrentUser() { export function getCurrentUser() {

View File

@ -1,7 +1,7 @@
import { catchError, filter, mergeMap, switchMap, take, tap } from 'rxjs/operators'; import { catchError, filter, mergeMap, switchMap, tap } from 'rxjs/operators';
import { ConfigService } from '@services/config.service'; import { ConfigService } from '@services/config.service';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { firstValueFrom, from, map, of, throwError } from 'rxjs'; import { firstValueFrom, map, of, throwError } from 'rxjs';
import { KeycloakEventType, KeycloakService } from 'keycloak-angular'; import { KeycloakEventType, KeycloakService } from 'keycloak-angular';
import { GeneralSettingsService } from '@services/general-settings.service'; import { GeneralSettingsService } from '@services/general-settings.service';
import { LanguageService } from '@iqser/common-ui'; import { LanguageService } from '@iqser/common-ui';
@ -35,10 +35,10 @@ export function configurationInitializer(
const setup = keycloakService.keycloakEvents$.pipe( const setup = keycloakService.keycloakEvents$.pipe(
filter(event => event.type === KeycloakEventType.OnReady), filter(event => event.type === KeycloakEventType.OnReady),
map(() => featuresService.loadConfig()), map(() => featuresService.loadConfig()),
switchMap(() => from(keycloakService.isLoggedIn())), switchMap(() => keycloakService.isLoggedIn()),
switchMap(loggedIn => (!loggedIn ? throwError('Not Logged In') : of({}))), switchMap(loggedIn => (!loggedIn ? throwError(() => 'Not Logged In') : of({}))),
switchMap(() => from(userService.loadCurrentUser())), switchMap(() => userService.initialize()),
switchMap(user => (!user.hasAnyREDRoles ? throwError('Not user has no red roles') : of({}))), switchMap(() => (!userService.currentUser.hasAnyREDRoles ? throwError(() => 'Not user has no red roles') : of({}))),
mergeMap(() => generalSettingsService.getGeneralConfigurations()), mergeMap(() => generalSettingsService.getGeneralConfigurations()),
tap(configuration => configService.updateDisplayName(configuration.displayName)), tap(configuration => configService.updateDisplayName(configuration.displayName)),
switchMap(() => systemPreferencesService.loadPreferences()), switchMap(() => systemPreferencesService.loadPreferences()),
@ -52,9 +52,7 @@ export function configurationInitializer(
lastDossierTemplateRedirect(baseHref.replace('/', ''), userPreferenceService); lastDossierTemplateRedirect(baseHref.replace('/', ''), userPreferenceService);
}), }),
switchMap(() => languageService.setInitialLanguage()), switchMap(() => languageService.setInitialLanguage()),
tap(() => userService.initialize()),
switchMap(() => licenseService.loadLicense()), switchMap(() => licenseService.loadLicense()),
take(1),
); );
return () => firstValueFrom(setup); return () => firstValueFrom(setup);
} }

View File

@ -83,7 +83,7 @@ export const dossierStateChecker = (dw: Dossier, filter: INestedFilter) =>
export const dossierApproverChecker = (dw: Dossier, filter: INestedFilter) => dw.approverIds.includes(filter.id); export const dossierApproverChecker = (dw: Dossier, filter: INestedFilter) => dw.approverIds.includes(filter.id);
export const userTypeFilters: { [key in UserType]: (user: User) => boolean } = { export const userTypeFilters: { [key in UserType]: (user: User) => boolean } = {
INACTIVE: (user: User) => !user.isActive, INACTIVE: (user: User) => !user.hasAnyRole,
REGULAR: (user: User) => user.roles.length === 1 && user.roles[0] === 'RED_USER', REGULAR: (user: User) => user.roles.length === 1 && user.roles[0] === 'RED_USER',
RED_MANAGER: (user: User) => user.isManager && !user.isAdmin, RED_MANAGER: (user: User) => user.isManager && !user.isAdmin,
MANAGER_ADMIN: (user: User) => user.isManager && user.isAdmin, MANAGER_ADMIN: (user: User) => user.isManager && user.isAdmin,

View File

@ -1,7 +1,7 @@
{ {
"ADMIN_CONTACT_NAME": null, "ADMIN_CONTACT_NAME": null,
"ADMIN_CONTACT_URL": null, "ADMIN_CONTACT_URL": null,
"API_URL": "https://dev-05.iqser.cloud/redaction-gateway-v1", "API_URL": "https://dev-04.iqser.cloud/redaction-gateway-v1",
"APP_NAME": "RedactManager", "APP_NAME": "RedactManager",
"AUTO_READ_TIME": 3, "AUTO_READ_TIME": 3,
"BACKEND_APP_VERSION": "4.4.40", "BACKEND_APP_VERSION": "4.4.40",
@ -11,7 +11,7 @@
"MAX_RETRIES_ON_SERVER_ERROR": 3, "MAX_RETRIES_ON_SERVER_ERROR": 3,
"OAUTH_CLIENT_ID": "redaction", "OAUTH_CLIENT_ID": "redaction",
"OAUTH_IDP_HINT": null, "OAUTH_IDP_HINT": null,
"OAUTH_URL": "https://dev-05.iqser.cloud/auth/realms/redaction", "OAUTH_URL": "https://dev-04.iqser.cloud/auth/realms/redaction",
"RECENT_PERIOD_IN_HOURS": 24, "RECENT_PERIOD_IN_HOURS": 24,
"SELECTION_MODE": "structural", "SELECTION_MODE": "structural",
"MANUAL_BASE_URL": "https://docs.redactmanager.com/preview" "MANUAL_BASE_URL": "https://docs.redactmanager.com/preview"

View File

@ -1,20 +0,0 @@
import { List } from '@iqser/common-ui';
export interface ICreateUserRequest {
/**
* Email of user.
*/
email?: string;
/**
* First name of user.
*/
firstName?: string;
/**
* Last name of user.
*/
lastName?: string;
/**
* The list of RED_* roles.
*/
roles?: List;
}

View File

@ -1,8 +1,3 @@
export * from './user.model'; export * from './user.model';
export * from './user';
export * from './my-profile-update.request';
export * from './profile-update.request';
export * from './profile'; export * from './profile';
export * from './types'; export * from './types';
export * from './create-user.request';
export * from './reset-password.request';

View File

@ -1,14 +0,0 @@
export interface IMyProfileUpdateRequest {
/**
* Email of user.
*/
email?: string;
/**
* First name of user.
*/
firstName?: string;
/**
* Last name of user.
*/
lastName?: string;
}

View File

@ -1,9 +0,0 @@
import { IMyProfileUpdateRequest } from './my-profile-update.request';
import { List } from '@iqser/common-ui';
export interface IProfileUpdateRequest extends IMyProfileUpdateRequest {
/**
* Roles.
*/
roles?: List;
}

View File

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

View File

@ -1,32 +1,14 @@
import { IListable, List } from '@iqser/common-ui'; import { IIqserUser, IqserUser, List } from '@iqser/common-ui';
import { KeycloakProfile } from 'keycloak-js'; import { KeycloakProfile } from 'keycloak-js';
import { IUser } from './user';
export class User implements IUser, IListable { export class User extends IqserUser {
readonly email?: string; readonly isManager = this.has('RED_MANAGER');
readonly username?: string; readonly isUserAdmin = this.has('RED_USER_ADMIN');
readonly firstName?: string; readonly isUser = this.has('RED_USER');
readonly lastName?: string; readonly isAdmin = this.has('RED_ADMIN');
readonly name?: string;
readonly searchKey: string;
readonly isActive = this.roles.length > 0;
readonly isManager = this.hasRole('RED_MANAGER');
readonly isUserAdmin = this.hasRole('RED_USER_ADMIN');
readonly isUser = this.hasRole('RED_USER');
readonly isAdmin = this.hasRole('RED_ADMIN');
readonly hasAnyREDRoles = this.isUser || this.isManager || this.isAdmin || this.isUserAdmin; readonly hasAnyREDRoles = this.isUser || this.isManager || this.isAdmin || this.isUserAdmin;
constructor(user: KeycloakProfile | IUser, readonly roles: List, readonly id: string) { constructor(user: KeycloakProfile | IIqserUser, readonly roles: List, readonly userId: string) {
this.email = user.email; super(user, roles, userId);
this.username = user.username || this.email;
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 || ''}`;
}
hasRole(role: string): boolean {
return this.roles.indexOf(role) >= 0;
} }
} }

View File

@ -1,15 +0,0 @@
/**
* Object containing information of user and roles.
*/
import { IProfileUpdateRequest } from './profile-update.request';
export interface IUser extends IProfileUpdateRequest {
/**
* Id of user.
*/
readonly userId?: string;
/**
* Username for login.
*/
readonly username?: string;
}