diff --git a/alfa-client/.gitignore b/alfa-client/.gitignore index 6116ea3d904921685f791527ee0ec33f12136fa8..82940fc64c73f76b3c97bc2e4f5a4d69a2aef4cc 100644 --- a/alfa-client/.gitignore +++ b/alfa-client/.gitignore @@ -49,3 +49,5 @@ testem.log .DS_Store Thumbs.db + +.angular diff --git a/alfa-client/apps/admin/src/app/app.component.html b/alfa-client/apps/admin/src/app/app.component.html index f9f6573f38f14c261b0d6c431ad1fb0a1cd327c5..d75f9414d1097d6d62358170a21086cb916e3412 100644 --- a/alfa-client/apps/admin/src/app/app.component.html +++ b/alfa-client/apps/admin/src/app/app.component.html @@ -1,4 +1,4 @@ -<ng-container *ngIf="(apiRoot$ | async)?.resource as apiRoot"> +<ng-container *ngIf="(apiRootStateResource$ | async)?.resource as apiRoot"> <header class="flex items-center justify-between bg-white p-6" data-test-id="admin-header"> <div class="text-ozgblue font-extrabold">OZG-Cloud Administration</div> <div> diff --git a/alfa-client/apps/admin/src/app/app.component.spec.ts b/alfa-client/apps/admin/src/app/app.component.spec.ts index 0fd3f5bb0cc54356d34ab02ddcff92a344d730bc..52e41ae571ae5ef0fc7a8c025e58bfe1a815abfb 100644 --- a/alfa-client/apps/admin/src/app/app.component.spec.ts +++ b/alfa-client/apps/admin/src/app/app.component.spec.ts @@ -3,7 +3,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { AppComponent } from './app.component'; import { AuthService } from '../common/auth/auth.service'; import { Mock, mock, existsAsHtmlElement, notExistsAsHtmlElement } from '@alfa-client/test-utils'; -import { ApiRootService } from '@alfa-client/api-root-shared'; +import { ApiRootResource, ApiRootService } from '@alfa-client/api-root-shared'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { createEmptyStateResource, createStateResource } from '@alfa-client/tech-shared'; import { of } from 'rxjs'; @@ -11,6 +11,7 @@ import { createApiRootResource } from 'libs/api-root-shared/test/api-root'; import { MockComponent } from 'ng-mocks'; import { UserProfileButtonContainerComponent } from '../common/user-profile-button-container/user-profile.button-container.component'; import { PostfachNavigationItemComponent } from '../pages/postfach/postfach-navigation-item/postfach-navigation-item.component'; +import { Router } from '@angular/router'; describe('AppComponent', () => { let component: AppComponent; @@ -21,7 +22,12 @@ describe('AppComponent', () => { const userProfileButton: string = getDataTestIdOf('user-profile-button'); const postfachNavigationItem: string = getDataTestIdOf('postfach-navigation-item'); - const authService: Mock<AuthService> = mock(AuthService); + const authService: Mock<AuthService> = { + ...mock(AuthService), + login: jest.fn().mockResolvedValue(Promise.resolve()), + }; + + const router: Mock<Router> = mock(Router); const apiRootService: Mock<ApiRootService> = mock(ApiRootService); beforeEach(async () => { @@ -41,6 +47,10 @@ describe('AppComponent', () => { provide: ApiRootService, useValue: apiRootService, }, + { + provide: Router, + useValue: router, + }, ], }).compileComponents(); }); @@ -65,10 +75,10 @@ describe('AppComponent', () => { }); it('should call doAfterLoggedIn', async () => { - authService.login.mockImplementation(() => Promise.resolve()); component.doAfterLoggedIn = jest.fn(); - await component.ngOnInit(); + component.ngOnInit(); + await fixture.whenStable(); expect(component.doAfterLoggedIn).toHaveBeenCalled(); }); @@ -79,18 +89,24 @@ describe('AppComponent', () => { expect(apiRootService.getApiRoot).toHaveBeenCalled(); }); + + it('should navigate to default route', () => { + component.doAfterLoggedIn(); + + expect(router.navigate).toHaveBeenCalledWith(['/']); + }); }); }); it('show not show header if apiRoot is not loaded', () => { - component.apiRoot$ = of(createEmptyStateResource()); + component.apiRootStateResource$ = of(createEmptyStateResource<ApiRootResource>()); notExistsAsHtmlElement(fixture, adminHeader); }); describe('user profile button', () => { beforeEach(() => { - component.apiRoot$ = of(createStateResource(createApiRootResource())); + component.apiRootStateResource$ = of(createStateResource(createApiRootResource())); }); it('should show if apiRoot exists', () => { @@ -102,7 +118,7 @@ describe('AppComponent', () => { describe('navigation', () => { beforeEach(() => { - component.apiRoot$ = of(createStateResource(createApiRootResource())); + component.apiRootStateResource$ = of(createStateResource(createApiRootResource())); }); it('should have postfach item', () => { fixture.detectChanges(); @@ -113,7 +129,7 @@ describe('AppComponent', () => { describe('build version', () => { beforeEach(() => { - component.apiRoot$ = of(createStateResource(createApiRootResource())); + component.apiRootStateResource$ = of(createStateResource(createApiRootResource())); }); it('should show after apiRoot loaded', () => { diff --git a/alfa-client/apps/admin/src/app/app.component.ts b/alfa-client/apps/admin/src/app/app.component.ts index 2e3c2a3e66b5ec5da01f6ff99f389c19b4b20f6d..198bc3207d5b1a4e5242d3126e636c254dde891c 100644 --- a/alfa-client/apps/admin/src/app/app.component.ts +++ b/alfa-client/apps/admin/src/app/app.component.ts @@ -1,7 +1,9 @@ -import { ApiRootService } from '@alfa-client/api-root-shared'; +import { ApiRootResource, ApiRootService } from '@alfa-client/api-root-shared'; import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; -import { AuthService } from '../common/auth/auth.service'; +import { StateResource } from '@alfa-client/tech-shared'; +import { Router } from '@angular/router'; +import { AuthenticationService } from 'libs/authentication/src/lib/authentication.service'; @Component({ selector: 'app-root', @@ -11,18 +13,20 @@ import { AuthService } from '../common/auth/auth.service'; export class AppComponent implements OnInit { readonly title = 'admin'; - public apiRoot$: Observable<any>; + public apiRootStateResource$: Observable<StateResource<ApiRootResource>>; constructor( - public authService: AuthService, + public authenticationService: AuthenticationService, private apiRootService: ApiRootService, + private router: Router, ) {} - public async ngOnInit(): Promise<void> { - await this.authService.login().then(() => this.doAfterLoggedIn()); + ngOnInit(): void { + this.authenticationService.login().then(() => this.doAfterLoggedIn()); } doAfterLoggedIn(): void { - this.apiRoot$ = this.apiRootService.getApiRoot(); + this.apiRootStateResource$ = this.apiRootService.getApiRoot(); + this.router.navigate(['/']); } } diff --git a/alfa-client/apps/admin/src/app/app.config.ts b/alfa-client/apps/admin/src/app/app.config.ts deleted file mode 100644 index a6c4c188b85cc5b17d9716af420e62e1b2801d6f..0000000000000000000000000000000000000000 --- a/alfa-client/apps/admin/src/app/app.config.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Environment } from '@alfa-client/environment-shared'; -import { KeycloakService } from 'keycloak-angular'; - -export function initializeKeycloak(keycloak: KeycloakService, envConfig: Environment) { - return () => - keycloak.init({ - config: { - url: envConfig.authServer, - realm: envConfig.realm, - clientId: envConfig.clientId, - }, - initOptions: { - adapter: 'default', - checkLoginIframe: false, - enableLogging: true, - scope: 'offline_access', - onLoad: 'login-required', - silentCheckSsoRedirectUri: window.location.origin + '/assets/silent-check-sso.html', - silentCheckSsoFallback: true, - }, - updateMinValidity: 1, - bearerExcludedUrls: ['/assets', '/clients/public'], - }); -} diff --git a/alfa-client/apps/admin/src/app/app.module.ts b/alfa-client/apps/admin/src/app/app.module.ts index e13deaf980ec4811058fd72666bd6a47659f2c47..9245ba29623a0ce3954336f25e74ed5319269dcf 100644 --- a/alfa-client/apps/admin/src/app/app.module.ts +++ b/alfa-client/apps/admin/src/app/app.module.ts @@ -1,8 +1,8 @@ import { ApiRootModule } from '@alfa-client/api-root-shared'; -import { ENVIRONMENT_CONFIG, EnvironmentModule } from '@alfa-client/environment-shared'; +import { EnvironmentModule } from '@alfa-client/environment-shared'; import { CommonModule } from '@angular/common'; -import { HttpClientModule } from '@angular/common/http'; -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; +import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouterModule } from '@angular/router'; @@ -11,16 +11,16 @@ import { StoreRouterConnectingModule } from '@ngrx/router-store'; import { StoreModule } from '@ngrx/store'; import { StoreDevtoolsModule } from '@ngrx/store-devtools'; import { TestbtnComponent } from 'design-system'; -import { KeycloakAngularModule, KeycloakService } from 'keycloak-angular'; import { environment } from '../environments/environment'; import { AppComponent } from './app.component'; -import { initializeKeycloak } from './app.config'; import { appRoutes } from './app.routes'; import { PostfachPageComponent } from '../pages/postfach/postfach-page/postfach-page.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { UserProfileButtonContainerComponent } from '../common/user-profile-button-container/user-profile.button-container.component'; import { AdminSettingsModule } from '@admin-client/admin-settings'; import { PostfachNavigationItemComponent } from '../pages/postfach/postfach-navigation-item/postfach-navigation-item.component'; +import { OAuthModule } from 'angular-oauth2-oidc'; +import { HttpUnauthorizedInterceptor } from 'libs/authentication/src/lib/http-unauthorized.interceptor'; @NgModule({ declarations: [ @@ -36,7 +36,6 @@ import { PostfachNavigationItemComponent } from '../pages/postfach/postfach-navi BrowserModule, BrowserAnimationsModule, HttpClientModule, - KeycloakAngularModule, ApiRootModule, EnvironmentModule, environment.production ? [] : StoreDevtoolsModule.instrument(), @@ -46,13 +45,17 @@ import { PostfachNavigationItemComponent } from '../pages/postfach/postfach-navi FormsModule, ReactiveFormsModule, AdminSettingsModule, + OAuthModule.forRoot({ + resourceServer: { + sendAccessToken: true, + }, + }), ], providers: [ { - provide: APP_INITIALIZER, - useFactory: initializeKeycloak, + provide: HTTP_INTERCEPTORS, + useClass: HttpUnauthorizedInterceptor, multi: true, - deps: [KeycloakService, ENVIRONMENT_CONFIG], }, ], bootstrap: [AppComponent], diff --git a/alfa-client/apps/admin/src/common/auth/auth.service.spec.ts b/alfa-client/apps/admin/src/common/auth/auth.service.spec.ts deleted file mode 100644 index 8d44ffa2b0eb2c1d0d5853bd506f846ba5920454..0000000000000000000000000000000000000000 --- a/alfa-client/apps/admin/src/common/auth/auth.service.spec.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; -import { AuthService } from './auth.service'; -import { KeycloakService, KeycloakEvent, KeycloakEventType } from 'keycloak-angular'; -import { of } from 'rxjs'; -import { KeycloakProfile } from 'keycloak-js'; -import { StateResource, createStateResource } from '@alfa-client/tech-shared'; -import { UserProfileResource } from '@alfa-client/user-profile-shared'; -import { createUserProfileResource } from '../../../../../libs/user-profile-shared/test/user-profile'; - -describe('AuthService', () => { - let service: AuthService; - let keycloakService: Mock<KeycloakService>; - - const event: KeycloakEvent = <any>{}; - - beforeEach(() => { - keycloakService = { ...mock(KeycloakService), keycloakEvents$: <any>of(event) }; - - service = new AuthService(useFromMock(keycloakService)); - }); - - describe('listenToKeycloakEvent', () => { - it('should call handleEvent', () => { - service.handleEvent = jest.fn(); - - service.listenToKeycloakEvent(); - - expect(service.handleEvent).toHaveBeenCalledWith(event); - }); - }); - - describe('handleEvent', () => { - describe('on token expired event', () => { - const tokenExpiredEvent: KeycloakEvent = { type: KeycloakEventType.OnTokenExpired }; - - it('should updateToken', () => { - service.handleEvent(tokenExpiredEvent); - - expect(keycloakService.updateToken).toHaveBeenCalledWith(10); - }); - - it('should call handleTokenExpireEvent', async () => { - const updateTokenReturnValue: boolean = true; - service.handleTokenExpiredEvent = jest.fn(); - keycloakService.updateToken.mockImplementation(() => - Promise.resolve(updateTokenReturnValue), - ); - - await service.handleEvent(tokenExpiredEvent); - - expect(service.handleTokenExpiredEvent).toHaveBeenCalledWith(updateTokenReturnValue); - }); - }); - }); - - describe('handleTokenExpiredEvent', () => { - it('should getToken if refresh was success', () => { - service.handleTokenExpiredEvent(true); - - expect(keycloakService.getToken).toHaveBeenCalled(); - }); - - it('should store token', async () => { - const token: string = 'tokenDummy'; - service.storeAccessToken = jest.fn(); - keycloakService.getToken.mockImplementation(() => Promise.resolve(token)); - - await service.handleTokenExpiredEvent(true); - - expect(service.storeAccessToken).toHaveBeenCalledWith(token); - }); - }); - - describe('login', () => { - it('should call isLoggedIn', () => { - service.login(); - - expect(keycloakService.isLoggedIn()); - }); - - describe('after loggedIn', () => { - const isLoggedIn: boolean = true; - - beforeEach(() => { - keycloakService.isLoggedIn.mockImplementation(() => Promise.resolve(isLoggedIn)); - service.setCurrentUser = jest.fn(); - }); - it('should store token', async () => { - const token: string = 'tokenDummy'; - keycloakService.getToken.mockImplementation(() => Promise.resolve(token)); - service.storeAccessToken = jest.fn(); - - await service.login(); - - expect(service.storeAccessToken).toHaveBeenCalledWith(token); - }); - - it('should set currentUser', async () => { - await service.login(); - - expect(service.setCurrentUser).toHaveBeenCalled(); - }); - }); - describe('if not logged in yet', () => { - it('should call login', async () => { - const isLoggedIn: boolean = false; - keycloakService.isLoggedIn.mockImplementation(() => Promise.resolve(isLoggedIn)); - - await service.login(); - - expect(keycloakService.login).toHaveBeenCalledWith({ scope: 'offline_access' }); - }); - }); - }); - - describe('store access token', () => { - it('should put token in sessionStorage', () => { - const token: string = 'tokenDummy'; - - service.storeAccessToken(token); - - const accessTokenInSessionStorage: string = sessionStorage.getItem('access_token'); - - expect(accessTokenInSessionStorage).toStrictEqual(token); - }); - }); - describe('logout', () => { - it('should call keycloakservice logout', () => { - service.logout(); - - expect(keycloakService.logout).toHaveBeenCalled(); - }); - }); - - describe('set current user', () => { - it('should load user profile', () => { - service.setCurrentUser(); - - expect(keycloakService.loadUserProfile).toHaveBeenCalled(); - }); - it('should update current user state resource', async () => { - const profile: KeycloakProfile = { firstName: 'blubb', lastName: 'blabb' }; - keycloakService.loadUserProfile.mockImplementation(() => Promise.resolve(profile)); - - await service.setCurrentUser(); - - expect(service.currentUserStateResource()).toEqual(createStateResource(profile)); - }); - }); - - describe('getCurrentUserInitials', () => { - it('should return currentUserStateResource', () => { - const userProfile: UserProfileResource = createUserProfileResource(); - const userStateResource: StateResource<UserProfileResource> = - createStateResource(userProfile); - service.currentUserStateResource.set(userStateResource); - const currentUserInitials: string = service.getCurrentUserInitials(); - - const initials: string = - userProfile.firstName.substring(0, 1) + '' + userProfile.lastName.substring(0, 1); - expect(currentUserInitials).toEqual(initials); - }); - }); -}); diff --git a/alfa-client/apps/admin/src/common/auth/auth.service.ts b/alfa-client/apps/admin/src/common/auth/auth.service.ts deleted file mode 100644 index 6f7ceb589abc8064c61f878dda5494f2051c5d52..0000000000000000000000000000000000000000 --- a/alfa-client/apps/admin/src/common/auth/auth.service.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { - StateResource, - createEmptyStateResource, - createStateResource, -} from '@alfa-client/tech-shared'; -import { Injectable, WritableSignal, signal } from '@angular/core'; -import { KeycloakEvent, KeycloakEventType, KeycloakService } from 'keycloak-angular'; -import { KeycloakProfile } from 'keycloak-js'; -import { UserProfileResource } from 'libs/user-profile-shared/src/lib/user-profile.model'; -import { getUserNameInitials } from 'libs/user-profile-shared/src/lib/user-profile.util'; - -@Injectable({ providedIn: 'root' }) -export class AuthService { - currentUserStateResource: WritableSignal<StateResource<UserProfileResource>> = signal< - StateResource<UserProfileResource> - >(createEmptyStateResource()); - - public isLoggedIn = false; - - constructor(private keycloak: KeycloakService) { - this.listenToKeycloakEvent(); - } - - listenToKeycloakEvent(): void { - this.keycloak.keycloakEvents$.subscribe((event: KeycloakEvent) => this.handleEvent(event)); - } - - async handleEvent(event: KeycloakEvent): Promise<void> { - if (event.type === KeycloakEventType.OnTokenExpired) { - await this.keycloak - .updateToken(10) - .then((refreshed) => this.handleTokenExpiredEvent(refreshed)); - } - } - - async handleTokenExpiredEvent(refreshed: boolean): Promise<void> { - if (refreshed) { - await this.keycloak.getToken().then((token) => this.storeAccessToken(token)); - } - } - - public async login(): Promise<void> { - this.isLoggedIn = await this.keycloak.isLoggedIn(); - if (this.isLoggedIn) { - const token: string = await this.keycloak.getToken(); - this.storeAccessToken(token); - await this.setCurrentUser(); - } else { - this.keycloak.login({ scope: 'offline_access' }); - } - } - - storeAccessToken(token: string): void { - sessionStorage.setItem('access_token', token); - } - - async setCurrentUser(): Promise<void> { - const profile: KeycloakProfile = await this.keycloak.loadUserProfile(); - - this.currentUserStateResource.set(createStateResource(this.buildUserProfileResource(profile))); - } - - private buildUserProfileResource(profile: KeycloakProfile): UserProfileResource { - return <any>{ - firstName: profile.firstName, - lastName: profile.lastName, - }; - } - - public logout(): void { - this.keycloak.logout(); - } - - public getCurrentUserInitials(): string { - return getUserNameInitials(this.currentUserStateResource().resource); - } -} diff --git a/alfa-client/apps/admin/src/common/user-profile-button-container/user-profile-button-container.component.html b/alfa-client/apps/admin/src/common/user-profile-button-container/user-profile-button-container.component.html index e1e3a249b36d73847158d3245a0337ada0d48063..e27ec82f62a522e589002e75ed0830ed483ed079 100644 --- a/alfa-client/apps/admin/src/common/user-profile-button-container/user-profile-button-container.component.html +++ b/alfa-client/apps/admin/src/common/user-profile-button-container/user-profile-button-container.component.html @@ -1,6 +1,10 @@ <div class="dropdown"> - <button (click)="showDropDown()" class="dropbtn" data-test-id="drop-down-button">{{ currentUserInitials }}</button> + <button (click)="showDropDown()" class="dropbtn" data-test-id="drop-down-button"> + {{ currentUserInitials }} + </button> <div id="myDropdown" class="dropdown-content"> - <span style="cursor: pointer" (click)="authService.logout()" data-test-id="logout">Abmelden</span> + <span style="cursor: pointer" (click)="authenticationService.logout()" data-test-id="logout" + >Abmelden</span + > </div> </div> diff --git a/alfa-client/apps/admin/src/common/user-profile-button-container/user-profile-button-container.component.spec.ts b/alfa-client/apps/admin/src/common/user-profile-button-container/user-profile-button-container.component.spec.ts index 95ea94f355b6c2918b543cdf401c10bef55b9fcf..f96d0b911d20d6e9c4da08c2c7cce135b3d4f863 100644 --- a/alfa-client/apps/admin/src/common/user-profile-button-container/user-profile-button-container.component.spec.ts +++ b/alfa-client/apps/admin/src/common/user-profile-button-container/user-profile-button-container.component.spec.ts @@ -6,15 +6,15 @@ import { } from '@alfa-client/test-utils'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; -import { AuthService } from '../auth/auth.service'; import { UserProfileButtonContainerComponent } from './user-profile.button-container.component'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; +import { AuthenticationService } from 'authentication'; describe('UserProfileButtonContainerComponent', () => { let component: UserProfileButtonContainerComponent; let fixture: ComponentFixture<UserProfileButtonContainerComponent>; - const authService: Mock<AuthService> = mock(AuthService); + const authenticationService: Mock<AuthenticationService> = mock(AuthenticationService); const dropDownButton: string = getDataTestIdOf('drop-down-button'); const logout: string = getDataTestIdOf('logout'); @@ -25,8 +25,8 @@ describe('UserProfileButtonContainerComponent', () => { imports: [RouterTestingModule], providers: [ { - provide: AuthService, - useValue: authService, + provide: AuthenticationService, + useValue: authenticationService, }, ], }).compileComponents(); @@ -46,7 +46,7 @@ describe('UserProfileButtonContainerComponent', () => { it('should call authService to get current user initials', () => { component.ngOnInit(); - expect(authService.getCurrentUserInitials).toHaveBeenCalled(); + expect(authenticationService.getCurrentUserInitials).toHaveBeenCalled(); }); }); @@ -66,7 +66,7 @@ describe('UserProfileButtonContainerComponent', () => { fixture.detectChanges(); const buttonElement: HTMLElement = getElementFromFixture(fixture, dropDownButton); - expect(buttonElement.textContent).toEqual('AV'); + expect(buttonElement.textContent.trim()).toEqual('AV'); }); }); @@ -74,7 +74,7 @@ describe('UserProfileButtonContainerComponent', () => { it('should call authService logout', () => { dispatchEventFromFixture(fixture, logout, 'click'); - expect(authService.logout).toHaveBeenCalled(); + expect(authenticationService.logout).toHaveBeenCalled(); }); }); }); diff --git a/alfa-client/apps/admin/src/common/user-profile-button-container/user-profile.button-container.component.ts b/alfa-client/apps/admin/src/common/user-profile-button-container/user-profile.button-container.component.ts index 5e64bef4c865e11f97e41c3ee59f54f7cf956a2c..e61c01de1c3797616a0fad8d69ccc375af2bb39d 100644 --- a/alfa-client/apps/admin/src/common/user-profile-button-container/user-profile.button-container.component.ts +++ b/alfa-client/apps/admin/src/common/user-profile-button-container/user-profile.button-container.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { AuthService } from '../auth/auth.service'; +import { AuthenticationService } from 'libs/authentication/src/lib/authentication.service'; @Component({ selector: 'user-profile-button-container', @@ -9,10 +9,10 @@ import { AuthService } from '../auth/auth.service'; export class UserProfileButtonContainerComponent implements OnInit { public currentUserInitials: string; - constructor(public authService: AuthService) {} + constructor(public authenticationService: AuthenticationService) {} ngOnInit(): void { - this.currentUserInitials = this.authService.getCurrentUserInitials(); + this.currentUserInitials = this.authenticationService.getCurrentUserInitials(); } public showDropDown(): void { diff --git a/alfa-client/apps/admin/src/main.ts b/alfa-client/apps/admin/src/main.ts index 5e03b925b784465a49e2f49ead63c0801124b6b0..3199db2879e8a93ad807dd20615329948b7e0373 100644 --- a/alfa-client/apps/admin/src/main.ts +++ b/alfa-client/apps/admin/src/main.ts @@ -14,9 +14,6 @@ loadEnvironment(environment.environmentUrl).then((env) => { if (env.production) { enableProdMode(); } - console.info('init bootstrap application...'); - //Für Standalone AppComponent - //bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err)); platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.log(err)); diff --git a/alfa-client/apps/alfa-e2e/Jenkinsfile b/alfa-client/apps/alfa-e2e/Jenkinsfile index eca38dd16248ecdb8ffd648482e74a0cbbd5f168..3c6516e22b426719c7453bacf30da69c60eb0826 100644 --- a/alfa-client/apps/alfa-e2e/Jenkinsfile +++ b/alfa-client/apps/alfa-e2e/Jenkinsfile @@ -203,14 +203,17 @@ pipeline { } } - stage('Run E2E-Tests') { - when { - expression { !SKIP_RUN } - } - failFast false +// stage('Run E2E-Tests') { +// when { +// expression { !SKIP_RUN } +// } +// failFast false - parallel { +// parallel { stage('E2E-EA') { + when { + expression { !SKIP_RUN } + } steps { catchError(buildResult: 'FAILURE', stageResult: 'FAILURE') { script { @@ -238,6 +241,9 @@ pipeline { } stage('E2E-Main') { + when { + expression { !SKIP_RUN } + } steps { catchError(buildResult: 'FAILURE', stageResult: 'FAILURE') { script { @@ -263,8 +269,8 @@ pipeline { } } } - } - } +// } +// } stage('Delete E2E Namespaces') { when { @@ -452,9 +458,6 @@ Void generateNamespaceYaml(String bezeichner, String valuesPathSuffix, String us envValues.user_manager.put("image", ['tag': env.USER_MANAGER_IMAGE_TAG]) envValues.user_manager.put("helm", ['version': env.USER_MANAGER_HELM_CHART_VERSION, 'repoUrl': env.USER_MANAGER_HELM_REPO_URL]) - - envValues.alfa.sso.put("keycloak_groups", generateKeycloakGroupsForHelmChart()) - envValues.alfa.sso.put("keycloak_users", generateKeycloakUserForHelmChart(userFolder)) } writeYaml file: "gitops/dev/namespace/namespaces/by-${bezeichner}-dev.yaml", data: envValues, overwrite: true @@ -467,66 +470,6 @@ Void generateNamespaceYaml(String bezeichner, String valuesPathSuffix, String us } } -List generateKeycloakUserForHelmChart(String userFolder) { - def helmUsers = [] - - readUsersFixtures(userFolder).each { username, userFixture -> - def user = [ - "name" : userFixture.name, - "password" : userFixture.password, - "first_name": userFixture.get("firstName", ""), - "last_name" : userFixture.get("lastName", ""), - "email" : userFixture.get("email", "") - ] - - if (userFixture.containsKey("clientRoles")) { - user.put("client_roles", mapUserClientRoles(userFixture.clientRoles)) - } - - if (userFixture.containsKey("groups")) { - user.put("groups", userFixture.groups) - } - - helmUsers.add(user) - } - - return helmUsers -} - -List mapUserClientRoles(userClientRoles) { - def clientRoles = [] - - for(clientRole in userClientRoles) { - clientRoles.add(['name': 'alfa', 'role': clientRole]) - } - - return clientRoles -} - -List generateKeycloakGroupsForHelmChart() { - def groupFiles = sh (script: 'ls src/fixtures/group', returnStdout: true) - - def helmGroups = [] - - groupFiles.split("\\n").each { groupFile -> - def groupJson = readJSON file: "src/fixtures/group/${groupFile}" - def group = ["name": groupJson.name] - - groupJson.attributes.each { key, values -> - if (!group.containsKey("attributes")) { - group.put("attributes", [["name": key, "value": values]]) - } - else { - group.attributes.add(["name": key, "value": values]) - } - } - - helmGroups.add(group) - } - - return helmGroups -} - Void deleteOzgCloudStack(ozgCloudBezeichner) { for(bezeichner in ozgCloudBezeichner) { if (hasNamespaceFile(bezeichner)) { diff --git a/alfa-client/apps/alfa-e2e/src/e2e/einheitlicher-ansprechpartner/vorgang-detail/vorgang-wiedereroeffnen.ea.cy.ts b/alfa-client/apps/alfa-e2e/src/e2e/einheitlicher-ansprechpartner/vorgang-detail/vorgang-wiedereroeffnen.ea.cy.ts index 24108803003bd16018090b7b71ee0e9a6180c29d..1f5f74d71c2f8ef90e78c0332c01d5d206cf0969 100644 --- a/alfa-client/apps/alfa-e2e/src/e2e/einheitlicher-ansprechpartner/vorgang-detail/vorgang-wiedereroeffnen.ea.cy.ts +++ b/alfa-client/apps/alfa-e2e/src/e2e/einheitlicher-ansprechpartner/vorgang-detail/vorgang-wiedereroeffnen.ea.cy.ts @@ -106,11 +106,10 @@ describe('Vorgang wiedereroeffnen', () => { }); it('should have status In Bearbeitung', () => { - vorgangPage - .getVorgangDetailHeader() - .getStatus() - .should('exist') - .should('have.text', vorgangStatusLabelE2E[VorgangStatusE2E.IN_BEARBEITUNG]); + haveText( + vorgangPage.getVorgangDetailHeader().getStatus(), + vorgangStatusLabelE2E[VorgangStatusE2E.IN_BEARBEITUNG], + ); }); it('back to vorgang list', () => { diff --git a/alfa-client/apps/alfa-e2e/src/fixtures/argocd/by-ea-dev.yaml b/alfa-client/apps/alfa-e2e/src/fixtures/argocd/by-ea-dev.yaml index 20927351eff8ee54afdc44510f6c2ef4e6304cf2..c51a91fbe8027ec15a23a83008bb94c0ca39b838 100644 --- a/alfa-client/apps/alfa-e2e/src/fixtures/argocd/by-ea-dev.yaml +++ b/alfa-client/apps/alfa-e2e/src/fixtures/argocd/by-ea-dev.yaml @@ -6,20 +6,25 @@ project: destinations: - namespace: '*' server: https://kubernetes.default.svc + alfa: env: overrideSpringProfiles: 'oc,ea,e2e,dev' sso: - serverUrl: https://sso.dev.by.ozg-cloud.de - apiPassword: 'Test1234!' keycloak_clients: - client_name: alfa client_roles: - name: EINHEITLICHER_ANSPRECHPARTNER + keycloak_users: + - name: emil + first_name: Emil + last_name: Ansprechpartner + password: "Y9nk43yrQ_zzIPpfFU-I" + client_roles: + - name: alfa + role: EINHEITLICHER_ANSPRECHPARTNER ingress: use_staging_cert: true - className: openshift-default - baseUrl: dev.by.ozg-cloud.de vorgang_manager: env: @@ -44,28 +49,8 @@ user_manager: ozgcloud: usersync: onstart: true - keycloak: - api: - password: 'Test1234!' - sso: - serverUrl: https://sso.dev.by.ozg-cloud.de - api_user: - name: usermanagerapiuser - first_name: UserManager - last_name: ApiUser - realm_roles: - - offline_access - - uma_authorization - client_roles: - - name: realm-management - role: view-users - - name: realm-management - role: manage-users - baseUrl: dev.by.ozg-cloud.de - ingress: use_staging_cert: true - className: openshift-default smocker: enabled: false diff --git a/alfa-client/apps/alfa-e2e/src/fixtures/argocd/by-main-dev.yaml b/alfa-client/apps/alfa-e2e/src/fixtures/argocd/by-main-dev.yaml index c7234b086328954fdd04f1754e44d79cdc5d22a4..569f411da3884478eefd25a7e5636e906673a860 100644 --- a/alfa-client/apps/alfa-e2e/src/fixtures/argocd/by-main-dev.yaml +++ b/alfa-client/apps/alfa-e2e/src/fixtures/argocd/by-main-dev.yaml @@ -9,13 +9,8 @@ project: alfa: env: overrideSpringProfiles: 'oc,e2e,dev' - sso: - serverUrl: https://sso.dev.by.ozg-cloud.de - apiPassword: 'Test1234!' ingress: use_staging_cert: true - className: openshift-default - baseUrl: dev.by.ozg-cloud.de vorgang_manager: env: @@ -40,28 +35,8 @@ user_manager: ozgcloud: usersync: onstart: true - keycloak: - api: - password: 'Test1234!' - sso: - serverUrl: https://sso.dev.by.ozg-cloud.de - api_user: - name: usermanagerapiuser - first_name: UserManager - last_name: ApiUser - realm_roles: - - offline_access - - uma_authorization - client_roles: - - name: realm-management - role: view-users - - name: realm-management - role: manage-users - baseUrl: dev.by.ozg-cloud.de - ingress: use_staging_cert: true - className: openshift-default smocker: enabled: false diff --git a/alfa-client/apps/alfa-e2e/src/support/cypress.util.ts b/alfa-client/apps/alfa-e2e/src/support/cypress.util.ts index 935ac6d7042933627651ef0d7aa425ff9ab7b183..9d4300f76bf0b807f639f732bd36902d7406aa35 100644 --- a/alfa-client/apps/alfa-e2e/src/support/cypress.util.ts +++ b/alfa-client/apps/alfa-e2e/src/support/cypress.util.ts @@ -41,7 +41,7 @@ export function notExist(element: any): void { } export function haveText(element: any, text: string): void { - element.should('have.text', text); + element.invoke("text").then((elementText) => elementText.trim()).should("equal", text); } export function haveValue(element: any, value: string): void { diff --git a/alfa-client/libs/admin-settings/src/lib/postfach/postfach-container/postfach-form/postfach.formservice.ts b/alfa-client/libs/admin-settings/src/lib/postfach/postfach-container/postfach-form/postfach.formservice.ts index 53829e1fce73f0d3dbb83fcb30d91fafc1a576d0..3e795f982fbc6d9f97d5d2e44e2b73f20a535261 100644 --- a/alfa-client/libs/admin-settings/src/lib/postfach/postfach-container/postfach-form/postfach.formservice.ts +++ b/alfa-client/libs/admin-settings/src/lib/postfach/postfach-container/postfach-form/postfach.formservice.ts @@ -33,7 +33,6 @@ export class PostfachFormService extends AbstractFormService { } protected doSubmit(): Observable<StateResource<Resource>> { - console.info('FormValue: ', this.getFormValue()); return of(createEmptyStateResource<Resource>()); } diff --git a/alfa-client/libs/authentication/.eslintrc.json b/alfa-client/libs/authentication/.eslintrc.json new file mode 100644 index 0000000000000000000000000000000000000000..b953e5d37d40148217ab14a01859ea0cb43d9ebc --- /dev/null +++ b/alfa-client/libs/authentication/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "extends": ["plugin:@nx/angular", "plugin:@angular-eslint/template/process-inline-templates"], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "lib", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "lib", + "style": "kebab-case" + } + ] + } + }, + { + "files": ["*.html"], + "extends": ["plugin:@nx/angular-template"], + "rules": {} + } + ] +} diff --git a/alfa-client/libs/authentication/README.md b/alfa-client/libs/authentication/README.md new file mode 100644 index 0000000000000000000000000000000000000000..927d463c287071179f81c12ec68a6f88fb777e18 --- /dev/null +++ b/alfa-client/libs/authentication/README.md @@ -0,0 +1,7 @@ +# authentication + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test authentication` to execute the unit tests. diff --git a/alfa-client/libs/authentication/jest.config.ts b/alfa-client/libs/authentication/jest.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..3d9be36ab97b9dcdffca41df8ae346c05abbc719 --- /dev/null +++ b/alfa-client/libs/authentication/jest.config.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +export default { + displayName: 'authentication', + preset: '../../jest.preset.js', + setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'], + coverageDirectory: '../../coverage/libs/authentication', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '<rootDir>/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/alfa-client/libs/authentication/project.json b/alfa-client/libs/authentication/project.json new file mode 100644 index 0000000000000000000000000000000000000000..32c2f511abfdc54eaf66bbff2b72e38af9484fad --- /dev/null +++ b/alfa-client/libs/authentication/project.json @@ -0,0 +1,31 @@ +{ + "name": "authentication", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/authentication/src", + "prefix": "lib", + "tags": [], + "projectType": "library", + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/authentication/jest.config.ts", + "passWithNoTests": true + }, + "configurations": { + "ci": { + "ci": true, + "codeCoverage": true + } + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/authentication/**/*.ts", "libs/authentication/**/*.html"] + } + } + } +} diff --git a/alfa-client/libs/authentication/src/index.ts b/alfa-client/libs/authentication/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..0cba51147eaa7a921027ceb37672deac3ed07be3 --- /dev/null +++ b/alfa-client/libs/authentication/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/authentication.module'; +export * from './lib/authentication.service'; diff --git a/alfa-client/libs/authentication/src/lib/authentication.module.spec.ts b/alfa-client/libs/authentication/src/lib/authentication.module.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..5dc84817fd14e1d2306ccc6aa6cb62e1f9e79eb0 --- /dev/null +++ b/alfa-client/libs/authentication/src/lib/authentication.module.spec.ts @@ -0,0 +1,14 @@ +import { AuthenticationModule } from './authentication.module'; +import { TestBed } from '@angular/core/testing'; + +describe('AuthenticationModule', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [AuthenticationModule], + }).compileComponents(); + }); + + it('should create', () => { + expect(AuthenticationModule).toBeDefined(); + }); +}); diff --git a/alfa-client/libs/authentication/src/lib/authentication.module.ts b/alfa-client/libs/authentication/src/lib/authentication.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..9dd1d02ea48c35dcc1a7a86c4e77718233578d9b --- /dev/null +++ b/alfa-client/libs/authentication/src/lib/authentication.module.ts @@ -0,0 +1,9 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { HttpUnauthorizedInterceptor } from './http-unauthorized.interceptor'; + +@NgModule({ + imports: [CommonModule], + providers: [HttpUnauthorizedInterceptor], +}) +export class AuthenticationModule {} diff --git a/alfa-client/libs/authentication/src/lib/authentication.service.spec.ts b/alfa-client/libs/authentication/src/lib/authentication.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..16b30c377fd4926c36a7d15d8cb4c635f08f7b00 --- /dev/null +++ b/alfa-client/libs/authentication/src/lib/authentication.service.spec.ts @@ -0,0 +1,110 @@ +import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; +import { UserProfileResource } from '@alfa-client/user-profile-shared'; +import { createUserProfileResource } from '../../../../libs/user-profile-shared/test/user-profile'; +import { AuthConfig, OAuthService } from 'angular-oauth2-oidc'; +import { fakeAsync, tick } from '@angular/core/testing'; +import { AuthenticationService } from './authentication.service'; +import { createAuthConfig } from '../../test/authentication.test'; +import { createEnvironment } from '../../../environment-shared/test/environment'; + +describe('AuthenticationService', () => { + let service: AuthenticationService; + let oAuthService: Mock<OAuthService>; + let environmentConfig; + + beforeEach(() => { + oAuthService = <any>{ + ...mock(OAuthService), + loadDiscoveryDocumentAndLogin: jest.fn().mockResolvedValue(() => Promise.resolve()), + }; + environmentConfig = createEnvironment(); + + service = new AuthenticationService(useFromMock(oAuthService), environmentConfig); + }); + + describe('login', () => { + it('should configure service with authConfig', () => { + const authConfig: AuthConfig = createAuthConfig(); + service.buildConfiguration = jest.fn().mockReturnValue(authConfig); + + service.login(); + + expect(oAuthService.configure).toHaveBeenCalledWith(authConfig); + }); + + it('should setup automatic silent refresh', () => { + service.login(); + + expect(oAuthService.setupAutomaticSilentRefresh).toHaveBeenCalled(); + }); + + it('should set tokenValidator', () => { + oAuthService.tokenValidationHandler = null; + + service.login(); + + expect(oAuthService.tokenValidationHandler).not.toBeNull(); + }); + + it('should load discovery document and login', () => { + service.login(); + + expect(oAuthService.loadDiscoveryDocumentAndLogin).toHaveBeenCalled(); + }); + + it('should set current user', fakeAsync(() => { + service.setCurrentUser = jest.fn(); + + service.login(); + tick(); + + expect(service.setCurrentUser).toHaveBeenCalled(); + })); + }); + + describe('set current user', () => { + const identityClaims: Record<string, any> = { + ['given_name']: 'Marco', + ['family_name']: 'Polo', + }; + + beforeEach(() => { + oAuthService.getIdentityClaims.mockReturnValue(identityClaims); + }); + it('should call oAuthservice to get claims', () => { + service.setCurrentUser(); + + expect(oAuthService.getIdentityClaims).toHaveBeenCalled(); + }); + + it('should update currentUser', () => { + service.setCurrentUser(); + + expect(service.currentUserResource.firstName).toEqual('Marco'); + expect(service.currentUserResource.lastName).toEqual('Polo'); + }); + }); + + describe('getCurrentUserInitials', () => { + it('should return currentUserResource', () => { + const userProfile: UserProfileResource = { + ...createUserProfileResource(), + firstName: 'Marco', + lastName: 'Polo', + }; + service.currentUserResource = userProfile; + + const currentUserInitials: string = service.getCurrentUserInitials(); + + expect(currentUserInitials).toEqual('MP'); + }); + }); + + describe('logout', () => { + it('should call oAuthService revokeTokenAndLogout', () => { + service.logout(); + + expect(oAuthService.revokeTokenAndLogout).toHaveBeenCalled(); + }); + }); +}); diff --git a/alfa-client/libs/authentication/src/lib/authentication.service.ts b/alfa-client/libs/authentication/src/lib/authentication.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..0e45de7eec32e9788212415f86e98db6b5012fce --- /dev/null +++ b/alfa-client/libs/authentication/src/lib/authentication.service.ts @@ -0,0 +1,58 @@ +import { ENVIRONMENT_CONFIG, Environment } from '@alfa-client/environment-shared'; +import { Inject, Injectable } from '@angular/core'; +import { OAuthService, AuthConfig } from 'angular-oauth2-oidc'; +import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks'; +import { UserProfileResource } from 'libs/user-profile-shared/src/lib/user-profile.model'; +import { getUserNameInitials } from 'libs/user-profile-shared/src/lib/user-profile.util'; + +@Injectable({ providedIn: 'root' }) +export class AuthenticationService { + currentUserResource: UserProfileResource; + + constructor( + private oAuthService: OAuthService, + @Inject(ENVIRONMENT_CONFIG) private envConfig: Environment, + ) {} + + public async login(): Promise<void> { + this.oAuthService.configure(this.buildConfiguration()); + this.oAuthService.setupAutomaticSilentRefresh(); + this.oAuthService.tokenValidationHandler = new JwksValidationHandler(); + await this.oAuthService.loadDiscoveryDocumentAndLogin(); + this.setCurrentUser(); + } + + buildConfiguration(): AuthConfig { + return { + issuer: this.envConfig.authServer + '/realms/' + this.envConfig.realm, + tokenEndpoint: + this.envConfig.authServer + + '/realms/' + + this.envConfig.realm + + '/protocol/openid-connect/token', + redirectUri: window.location.origin + '/', + clientId: this.envConfig.clientId, + scope: 'openid profile', + requireHttps: false, + responseType: 'code', + showDebugInformation: false, + }; + } + + setCurrentUser(): void { + const claims: Record<string, any> = this.oAuthService.getIdentityClaims(); + const userResource: UserProfileResource = <any>{ + firstName: claims['given_name'], + lastName: claims['family_name'], + }; + this.currentUserResource = userResource; + } + + public getCurrentUserInitials(): string { + return getUserNameInitials(this.currentUserResource); + } + + public logout(): void { + this.oAuthService.revokeTokenAndLogout(); + } +} diff --git a/alfa-client/libs/authentication/src/lib/http-unauthorized.interceptor.spec.ts b/alfa-client/libs/authentication/src/lib/http-unauthorized.interceptor.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..d22dd199212580a77eeab3506aa9d17d105546ea --- /dev/null +++ b/alfa-client/libs/authentication/src/lib/http-unauthorized.interceptor.spec.ts @@ -0,0 +1,79 @@ +import { Mock, mock } from '@alfa-client/test-utils'; +import { TestBed } from '@angular/core/testing'; +import { MatDialogModule } from '@angular/material/dialog'; +import { HttpUnauthorizedInterceptor } from './http-unauthorized.interceptor'; +import { HttpErrorResponse, HttpHandler, HttpRequest } from '@angular/common/http'; +import { Subject, isEmpty } from 'rxjs'; +import { AuthenticationService } from './authentication.service'; + +describe('HttpUnauthorizedInterceptor', () => { + let interceptor: HttpUnauthorizedInterceptor; + + const authenticationService: Mock<AuthenticationService> = mock(AuthenticationService); + + beforeEach(() => + TestBed.configureTestingModule({ + imports: [MatDialogModule], + providers: [ + HttpUnauthorizedInterceptor, + { + provide: AuthenticationService, + useValue: authenticationService, + }, + ], + }), + ); + + beforeEach(() => { + interceptor = TestBed.inject(HttpUnauthorizedInterceptor); + }); + + it('should be created', () => { + expect(interceptor).toBeTruthy(); + }); + + describe('intercept', () => { + const error: HttpErrorResponse = new HttpErrorResponse({}); + + const handleSubject: Subject<any> = new Subject(); + const httpHandler: HttpHandler = { handle: () => handleSubject }; + const request: HttpRequest<unknown> = new HttpRequest('GET', '/test'); + + it('should call handleError with error', () => { + interceptor.handleError = jest.fn(); + + interceptor.intercept(request, httpHandler).subscribe(); + handleSubject.error(error); + + expect(interceptor.handleError).toHaveBeenCalledWith(error); + }); + }); + + describe('handleError', () => { + describe('on unauthorized status', () => { + const unauthorizedError: any = new HttpErrorResponse({ status: 401 }); + + it('should call logout on authService ', () => { + interceptor.handleError(unauthorizedError); + + expect(authenticationService.logout).toHaveBeenCalled(); + }); + + it('should return EMPTY', (done) => { + interceptor + .handleError(unauthorizedError) + .pipe(isEmpty()) + .subscribe((res) => { + expect(res).toBeTruthy; + done(); + }); + }); + }); + + it('should rethrow error if not unauthorized status', () => { + const anyError: any = new HttpErrorResponse({ status: 500 }); + + expect(() => interceptor.handleError(anyError)).toThrowError(); + }); + }); +}); diff --git a/alfa-client/libs/authentication/src/lib/http-unauthorized.interceptor.ts b/alfa-client/libs/authentication/src/lib/http-unauthorized.interceptor.ts new file mode 100644 index 0000000000000000000000000000000000000000..0ce59555abc0f54337de086fdc3111c474935252 --- /dev/null +++ b/alfa-client/libs/authentication/src/lib/http-unauthorized.interceptor.ts @@ -0,0 +1,34 @@ +import { + HttpErrorResponse, + HttpEvent, + HttpHandler, + HttpInterceptor, + HttpRequest, +} from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { isUnauthorized } from '@alfa-client/tech-shared'; +import { EMPTY, Observable } from 'rxjs'; +import { catchError } from 'rxjs/operators'; +import { AuthenticationService } from './authentication.service'; + +@Injectable() +export class HttpUnauthorizedInterceptor implements HttpInterceptor { + constructor(private authenticationService: AuthenticationService) {} + + public intercept( + request: HttpRequest<unknown>, + next: HttpHandler, + ): Observable<HttpEvent<unknown>> { + return next + .handle(request) + .pipe(catchError((error: HttpErrorResponse) => this.handleError(error))); + } + + handleError(error: HttpErrorResponse): Observable<any> { + if (isUnauthorized(error.status)) { + this.authenticationService.logout(); + return EMPTY; + } + throw error; + } +} diff --git a/alfa-client/libs/authentication/src/test-setup.ts b/alfa-client/libs/authentication/src/test-setup.ts new file mode 100644 index 0000000000000000000000000000000000000000..e3361fb01b7b562d0a94995eb9c00a6ad5b2949d --- /dev/null +++ b/alfa-client/libs/authentication/src/test-setup.ts @@ -0,0 +1,12 @@ +import 'jest-preset-angular/setup-jest'; + +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting, +} from '@angular/platform-browser-dynamic/testing'; + +getTestBed().resetTestEnvironment(); +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { + teardown: { destroyAfterEach: false }, +}); diff --git a/alfa-client/libs/authentication/test/authentication.test.ts b/alfa-client/libs/authentication/test/authentication.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..a33cb3c48a772b5327e9251eaa4a892743c7d2c2 --- /dev/null +++ b/alfa-client/libs/authentication/test/authentication.test.ts @@ -0,0 +1,5 @@ +import { AuthConfig } from 'angular-oauth2-oidc'; + +export function createAuthConfig(): AuthConfig { + return {}; +} diff --git a/alfa-client/libs/authentication/tsconfig.json b/alfa-client/libs/authentication/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..03261df5a47903bb7d4de17fbef9e9a14b048bed --- /dev/null +++ b/alfa-client/libs/authentication/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "target": "es2020" + } +} diff --git a/alfa-client/libs/authentication/tsconfig.lib.json b/alfa-client/libs/authentication/tsconfig.lib.json new file mode 100644 index 0000000000000000000000000000000000000000..12787567547cf6ba3faf59c8257b0750964c8741 --- /dev/null +++ b/alfa-client/libs/authentication/tsconfig.lib.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": ["src/**/*.spec.ts", "src/test-setup.ts", "jest.config.ts", "src/**/*.test.ts"], + "include": ["src/**/*.ts", "test/authentication.test.ts"] +} diff --git a/alfa-client/libs/authentication/tsconfig.spec.json b/alfa-client/libs/authentication/tsconfig.spec.json new file mode 100644 index 0000000000000000000000000000000000000000..5ad7a4c64f427f468da7e6a1b593fafd898ff25a --- /dev/null +++ b/alfa-client/libs/authentication/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts", + "test/authentication.test.ts" + ] +} diff --git a/alfa-client/tsconfig.base.json b/alfa-client/tsconfig.base.json index 273aa6f0fd7e8b8dc7bfe7e87a4f644820431bf8..1f7364bba7606a74a3d102427b82bb77ac3049cb 100644 --- a/alfa-client/tsconfig.base.json +++ b/alfa-client/tsconfig.base.json @@ -53,6 +53,7 @@ "@alfa-client/vorgang-shared-ui": ["libs/vorgang-shared-ui/src/index.ts"], "@alfa-client/wiedervorlage": ["libs/wiedervorlage/src/index.ts"], "@alfa-client/wiedervorlage-shared": ["libs/wiedervorlage-shared/src/index.ts"], + "authentication": ["libs/authentication/src/index.ts"], "design-system": ["libs/design-system/src/index.ts"] } },