Skip to content

Commit 40550c8

Browse files
authored
Merge pull request #436 from medizininformatik-initiative/434-userprofileservice-for-centralized-user-authentication-and-authorization-management
Refactor user profile handling across services and components; add Us…
2 parents eb5e51e + 4498619 commit 40550c8

File tree

7 files changed

+172
-47
lines changed

7 files changed

+172
-47
lines changed

cypress/docker/docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# Wieso hoch und runterfahren des Containers wenn settings geändert werden
2+
# DataPortalAdmin hat keine rechte
13
services:
24
dataportal-backend:
35
container_name: dataportal-backend

src/app/CoreInit.service.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,17 @@
1+
import { ActuatorApiService } from './service/Backend/Api/ActuatorApi.service';
12
import { AppConfigService } from './config/app-config.service';
23
import { catchError, concatMap, map, tap } from 'rxjs/operators';
34
import { DataSelectionMainProfileInitializerService } from './service/DataSelectionMainProfileInitializerService';
45
import { DataSelectionProfile } from './model/DataSelection/Profile/DataSelectionProfile';
56
import { FeatureProviderService } from './service/FeatureProvider.service';
67
import { FeatureService } from './service/Feature.service';
7-
import { HttpClient } from '@angular/common/http';
88
import { IAppConfig } from './config/app-config.model';
99
import { Injectable } from '@angular/core';
1010
import { OAuthInitService } from './core/auth/oauth-init.service';
1111
import { Observable, of, throwError } from 'rxjs';
1212
import { ProvidersInitService } from './service/Provider/ProvidersInit.service';
1313
import { TerminologySystemProvider } from './service/Provider/TerminologySystemProvider.service';
14-
import { ActuatorApiService } from './service/Backend/Api/ActuatorApi.service';
15-
16-
interface PatientProfileInitResult {
17-
config: IAppConfig
18-
patientProfileResult: DataSelectionProfile
19-
}
20-
14+
import { UserProfileService } from './service/User/UserProfile.service';
2115
@Injectable({ providedIn: 'root' })
2216
export class CoreInitService {
2317
constructor(
@@ -28,13 +22,21 @@ export class CoreInitService {
2822
private featureService: FeatureService,
2923
private featureProviderService: FeatureProviderService,
3024
private providersInitService: ProvidersInitService,
31-
private http: HttpClient,
32-
private actuatorApiService: ActuatorApiService
25+
private actuatorApiService: ActuatorApiService,
26+
private userProfileService: UserProfileService
3327
) {}
3428

29+
/**
30+
* @see Once the pipe has more than nine operators the return type will
31+
* be Observable<unknown> therefore it needs to be casted explicitly
32+
* to the return desired type
33+
* Initializes core services and features.
34+
* @returns An observable of the application configuration.
35+
*/
3536
public init(): Observable<IAppConfig> {
3637
return this.loadConfig().pipe(
3738
concatMap((config) => this.initOAuth(config)),
39+
concatMap((config) => this.initUserProfile(config)),
3840
concatMap((config) => this.initFeatureService(config)),
3941
concatMap((config) => this.initFeatureProviderService(config)),
4042
concatMap((config) => this.checkBackendHealth(config)),
@@ -52,7 +54,7 @@ export class CoreInitService {
5254
console.error('CoreInitService failed:', err);
5355
return throwError(() => err);
5456
})
55-
);
57+
) as Observable<IAppConfig>;
5658
}
5759

5860
private loadConfig(): Observable<IAppConfig> {
@@ -78,6 +80,18 @@ export class CoreInitService {
7880
);
7981
}
8082

83+
private initUserProfile(config: IAppConfig): Observable<IAppConfig> {
84+
return this.userProfileService.initializeProfile().pipe(
85+
tap((result) => console.log('UserProfile initialized:', result)),
86+
concatMap((result: boolean) =>
87+
result === true
88+
? of(config)
89+
: throwError(() => new Error('UserProfile initialization failed'))
90+
),
91+
map(() => config)
92+
);
93+
}
94+
8195
private initFeatureService(config: IAppConfig): Observable<IAppConfig> {
8296
return this.featureService.initFeatureService(config).pipe(
8397
tap((result) => console.log('FeatureService initialized:', result === true)),

src/app/core/auth/guards/role.guard.ts

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
1+
import { FeatureService } from '../../../service/Feature.service';
12
import { Injectable } from '@angular/core';
3+
import { UserProfileService } from 'src/app/service/User/UserProfile.service';
24
import {
35
CanActivate,
46
CanLoad,
57
Route,
68
ActivatedRouteSnapshot,
79
RouterStateSnapshot,
810
} from '@angular/router';
9-
import { OAuthService } from 'angular-oauth2-oidc';
10-
import { IUserProfile } from '../../../shared/models/user/user-profile.interface';
11-
import { FeatureService } from '../../../service/Feature.service';
1211

1312
@Injectable({
1413
providedIn: 'root',
1514
})
1615
export class RoleGuard implements CanActivate, CanLoad {
17-
constructor(private oauthService: OAuthService, public featureService: FeatureService) {}
16+
constructor(
17+
public featureService: FeatureService,
18+
private userProfileService: UserProfileService
19+
) {}
1820

1921
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
2022
const redirectUri = window.location.origin + state.url;
@@ -25,10 +27,8 @@ export class RoleGuard implements CanActivate, CanLoad {
2527
const redirectUri = window.location.origin + '/' + route.path;
2628
return this.isAllowed(route, redirectUri);
2729
}
28-
2930
async isAllowed(route: ActivatedRouteSnapshot | Route, redirectUri: string): Promise<boolean> {
3031
const allowedRoles = route.data?.roles;
31-
3232
if (!(allowedRoles instanceof Array) || allowedRoles.length === 0) {
3333
return Promise.resolve(true);
3434
}
@@ -47,18 +47,7 @@ export class RoleGuard implements CanActivate, CanLoad {
4747
}
4848
});
4949

50-
const isLoggedIn =
51-
this.oauthService.hasValidIdToken() && this.oauthService.hasValidAccessToken();
52-
53-
if (!isLoggedIn) {
54-
await this.oauthService.loadDiscoveryDocumentAndLogin({ customRedirectUri: redirectUri });
55-
}
56-
57-
let userRoles: string[];
58-
await this.oauthService.loadUserProfile().then((userinfo: IUserProfile) => {
59-
userRoles = userinfo.info.realm_access.roles;
60-
});
61-
50+
const userRoles: string[] = this.userProfileService.getCurrentProfile().info.realm_access.roles;
6251
if (userRoles) {
6352
return Promise.resolve(expandedAllowedRoles.some((role) => userRoles.indexOf(role) >= 0));
6453
}

src/app/layout/components/header/header.component.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { FeatureService } from '../../../service/Feature.service';
66
import { IUserProfile } from '../../../shared/models/user/user-profile.interface';
77
import { MatDialog } from '@angular/material/dialog';
88
import { OAuthService } from 'angular-oauth2-oidc';
9+
import { UserProfileService } from 'src/app/service/User/UserProfile.service';
910

1011
@Component({
1112
selector: 'num-dataportal-header',
@@ -23,8 +24,8 @@ export class HeaderComponent implements OnInit, AfterViewInit {
2324
private oauthService: OAuthService,
2425
private featureProviderService: FeatureProviderService,
2526
public featureService: FeatureService,
26-
private actuatorInformationService: ActuatorInformationService,
27-
private matDialog: MatDialog
27+
private matDialog: MatDialog,
28+
private userProfileService: UserProfileService
2829
) {}
2930

3031
ngOnInit(): void {
@@ -38,7 +39,7 @@ export class HeaderComponent implements OnInit, AfterViewInit {
3839
async initProfile(): Promise<void> {
3940
const isLoggedIn = this.oauthService.hasValidAccessToken();
4041
if (isLoggedIn) {
41-
this.profile = (await this.oauthService.loadUserProfile()) as IUserProfile;
42+
this.profile = this.userProfileService.getCurrentProfile();
4243
}
4344
}
4445
public logout() {

src/app/modules/dashboard/components/dashboard/dashboard.component.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import { OAuthService } from 'angular-oauth2-oidc';
77
import { TranslateService } from '@ngx-translate/core';
88
import { ErrorCodes, SnackbarService } from 'src/app/shared/service/Snackbar/Snackbar.service';
99

10+
/**
11+
* @todo Needs to be refactored
12+
* User directive possibly not needed
13+
*/
1014
@Component({
1115
selector: 'num-dashboard',
1216
templateUrl: './dashboard.component.html',
@@ -33,7 +37,7 @@ export class DashboardComponent implements OnInit {
3337
this.roles = this.featureService.getRoles('main');
3438
this.init();
3539
this.displayInfoMessage = this.featureService.showInfoPage();
36-
this.proposalPortalLink = this.featureService.getproposalPortalLink();
40+
this.proposalPortalLink = this.featureService.getProposalPortalLink();
3741

3842
if (this.featureService.showUpdateInfo()) {
3943
this.snackbar.displayInfoMessage('UPDATE_NOTE');
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { Injectable } from '@angular/core';
2+
import { OAuthService } from 'angular-oauth2-oidc';
3+
import { BehaviorSubject, Observable, from, of } from 'rxjs';
4+
import { catchError, map, take, tap } from 'rxjs/operators';
5+
import { IUserProfile } from '../../shared/models/user/user-profile.interface';
6+
7+
/**
8+
* Provides centralized access to user profile loaded by OAuth service.
9+
*/
10+
@Injectable({
11+
providedIn: 'root',
12+
})
13+
export class UserProfileService {
14+
/**
15+
* Current user profile data, null if not loaded or user not authenticated
16+
*/
17+
private readonly profileSubject = new BehaviorSubject<IUserProfile | null>(null);
18+
19+
/**
20+
* Flag to ensure initializeProfile is only executed once
21+
*/
22+
private isInitialized = false;
23+
24+
constructor(private oauthService: OAuthService) {}
25+
26+
/**
27+
* Initialize profile loading on service creation
28+
* This method can only be executed once to prevent duplicate profile loading
29+
*/
30+
public initializeProfile(): Observable<boolean> {
31+
if (this.isInitialized) {
32+
console.warn('UserProfileService.initializeProfile() already executed, skipping...');
33+
return of(false);
34+
}
35+
36+
this.isInitialized = true;
37+
38+
if (this.isUserAuthenticated()) {
39+
return from(this.oauthService.loadUserProfile()).pipe(
40+
take(1),
41+
tap((profile: IUserProfile) => {
42+
this.profileSubject.next(profile);
43+
}),
44+
map(() => true)
45+
);
46+
} else {
47+
return of(false);
48+
}
49+
}
50+
51+
/**
52+
* Checks if the user has valid OAuth tokens
53+
*/
54+
private isUserAuthenticated(): boolean {
55+
return this.oauthService.hasValidIdToken() && this.oauthService.hasValidAccessToken();
56+
}
57+
58+
/**
59+
* Observable stream of user profile data
60+
*/
61+
public getProfile(): Observable<IUserProfile | null> {
62+
return this.profileSubject.asObservable();
63+
}
64+
65+
/**
66+
* Current profile data (synchronous access)
67+
*/
68+
public getCurrentProfile(): IUserProfile | null {
69+
return this.profileSubject.value;
70+
}
71+
72+
/**
73+
* Clears the cached profile data
74+
*/
75+
public clearProfile(): void {
76+
this.profileSubject.next(null);
77+
}
78+
79+
/**
80+
* Checks if the current user has any of the specified roles
81+
*/
82+
public hasRole(roles: string[]): boolean {
83+
const profile = this.getCurrentProfile();
84+
if (!profile?.info?.realm_access?.roles) {
85+
return false;
86+
}
87+
88+
const userRoles = profile.info.realm_access.roles;
89+
return roles.some((role) => userRoles.includes(role));
90+
}
91+
92+
/**
93+
* Gets all user roles
94+
*/
95+
public getUserRoles(): string[] {
96+
const profile = this.getCurrentProfile();
97+
return profile?.info?.realm_access?.roles || [];
98+
}
99+
100+
/**
101+
* Gets user display name
102+
*/
103+
public getUserName(): string {
104+
const profile = this.getCurrentProfile();
105+
return profile?.info?.name || '';
106+
}
107+
108+
/**
109+
* Checks if user is currently authenticated and has profile loaded
110+
*/
111+
public isAuthenticated(): boolean {
112+
return this.getCurrentProfile() !== null;
113+
}
114+
}
Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,37 @@
11
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
22
import { IUserProfile } from '../models/user/user-profile.interface';
33
import { OAuthService } from 'angular-oauth2-oidc';
4+
import { UserProfileService } from 'src/app/service/User/UserProfile.service';
45

6+
/**
7+
* @deprecated Verify
8+
*/
59
@Directive({
610
selector: '[numUserHasRole]',
711
})
812
export class UserHasRoleDirective {
913
constructor(
1014
private templateRef: TemplateRef<any>,
1115
private viewContainer: ViewContainerRef,
12-
private oauthService: OAuthService
16+
private oauthService: OAuthService,
17+
private userProfileService: UserProfileService
1318
) {}
1419

1520
@Input() set numUserHasRole(allowedRoles: string[]) {
16-
let userRoles: string[];
21+
const userRoles: string[] = this.userProfileService.getCurrentProfile().info.realm_access.roles;
1722

18-
this.oauthService.loadUserProfile().then((userinfo: IUserProfile) => {
19-
userRoles = userinfo.info.realm_access.roles;
20-
21-
if (allowedRoles && allowedRoles.length) {
22-
if (userRoles && userRoles.length) {
23-
if (allowedRoles.some((role) => userRoles.indexOf(role) >= 0)) {
24-
this.viewContainer.createEmbeddedView(this.templateRef);
25-
} else {
26-
this.viewContainer.clear();
27-
}
23+
if (allowedRoles && allowedRoles.length) {
24+
if (userRoles && userRoles.length) {
25+
if (allowedRoles.some((role) => userRoles.indexOf(role) >= 0)) {
26+
this.viewContainer.createEmbeddedView(this.templateRef);
2827
} else {
2928
this.viewContainer.clear();
3029
}
3130
} else {
32-
this.viewContainer.createEmbeddedView(this.templateRef);
31+
this.viewContainer.clear();
3332
}
34-
});
33+
} else {
34+
this.viewContainer.createEmbeddedView(this.templateRef);
35+
}
3536
}
3637
}

0 commit comments

Comments
 (0)