diff --git a/backend/README.md b/backend/README.md index 2aaa52940b..127c7150a1 100644 --- a/backend/README.md +++ b/backend/README.md @@ -7,12 +7,31 @@ This is the Spring Boot backend for the OKR-Tool. Build only the Backend - Build backend without frontend: `mvn clean package` + Build Frontend + Backend together + +NATIVE - Build Frontend for production im `frontend` dir: `npm run build` - Build Backend with frontend: `mvn clean package -P build-for-docker` - Setup DB - Start Backend `java -jar {path to .jar file}` +USING DOCKER +- cd into the root directory of the project +- cd `docker/local-prod` +- Run `docker compose up` +- You have to restart after every code change `docker compose down spring && docker compose up spring` +- Get the logs with `docker compose logs -f spring` +
+ +***!IMPORTANT!*** +- If after the first start the backend is not reachable, restart the backend container with `docker compose restart spring` +- if any permission issues occur, delete all of the following folders + - .angular + - frontend/dist + - frontend/node_modules + - backend/target + Formatting: - Check code formatting: `mvn formatter:validate` - Format the code: `mvn formatter:format` diff --git a/backend/src/main/java/ch/puzzle/okr/ForwardFilter.java b/backend/src/main/java/ch/puzzle/okr/ForwardFilter.java index bb76f5444c..2e517f4d59 100644 --- a/backend/src/main/java/ch/puzzle/okr/ForwardFilter.java +++ b/backend/src/main/java/ch/puzzle/okr/ForwardFilter.java @@ -16,20 +16,11 @@ public class ForwardFilter extends GenericFilterBean { private static final Logger logger = LoggerFactory.getLogger(ForwardFilter.class); - private final String[] allowedRoutes = { "/keyresult", "/objective", "/?state" }; @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; - String path = request.getRequestURI(); - - if (Arrays.stream(this.allowedRoutes).anyMatch(path::startsWith)) { - logger.info(String.format("Keycloak state parameter detected ====> make a forward from '%s' to '%s'", - request.getRequestURI(), "/")); - servletRequest.getRequestDispatcher("/").forward(servletRequest, servletResponse); - return; - } logger.debug(String.format("====> pass through the filter '%s'", request.getRequestURI())); filterChain.doFilter(servletRequest, servletResponse); } diff --git a/backend/src/main/java/ch/puzzle/okr/controller/ClientConfigController.java b/backend/src/main/java/ch/puzzle/okr/controller/ClientConfigController.java index 218da02f8a..e784aaf236 100644 --- a/backend/src/main/java/ch/puzzle/okr/controller/ClientConfigController.java +++ b/backend/src/main/java/ch/puzzle/okr/controller/ClientConfigController.java @@ -1,15 +1,17 @@ package ch.puzzle.okr.controller; +import ch.puzzle.okr.ForwardFilter; import ch.puzzle.okr.dto.ClientConfigDto; import ch.puzzle.okr.service.clientconfig.ClientConfigService; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; -@RestController -@RequestMapping("/config") +@Controller public class ClientConfigController { private final ClientConfigService configService; @@ -18,8 +20,21 @@ public ClientConfigController(ClientConfigService configService) { this.configService = configService; } - @GetMapping + @GetMapping("/config") public ResponseEntity getConfig() { return ResponseEntity.status(HttpStatus.OK).body(configService.getConfigBasedOnActiveEnv()); } + + @RequestMapping(value = "/**/{[path:[^\\.]*}") + public String redirect(HttpServletRequest request) { + String path = request.getRequestURI(); + // Serve static resources or paths containing a dot directly + if (path.startsWith("/assets/") || path.contains(".")) { + return "forward:" + path; + } + + // Forward all other requests to index.html + return "forward:/"; + } + } diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index bfaf3c94bb..f9d7743cf1 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -1,5 +1,6 @@ #environment spring.profiles.active=prod +spring.mvc.pathmatch.matching-strategy=ant_path_matcher # database connection spring.datasource.url=jdbc:postgresql://localhost:5432/okr diff --git a/backend/src/test/java/ch/puzzle/okr/ForwardFilterTest.java b/backend/src/test/java/ch/puzzle/okr/ForwardFilterTest.java index 1ab29486ac..1c27667c3e 100644 --- a/backend/src/test/java/ch/puzzle/okr/ForwardFilterTest.java +++ b/backend/src/test/java/ch/puzzle/okr/ForwardFilterTest.java @@ -55,19 +55,4 @@ void shouldNotFilterTheRootPath(String requestUri) throws ServletException, IOEx verify(filterChain, times(1)).doFilter(Mockito.eq(request), Mockito.eq(response)); verify(request, never()).getRequestDispatcher(anyString()); } - - @Test - void shouldFilterAuthPath() throws ServletException, IOException { - // given - when(request.getRequestURI()).thenReturn("/?state=''"); - when(request.getRequestDispatcher(anyString())).thenReturn(requestDispatcher); - doNothing().when(requestDispatcher).forward(Mockito.eq(request), Mockito.eq(response)); - - // when - forwardFilter.doFilter(request, response, filterChain); - - // then - verify(filterChain, never()).doFilter(Mockito.eq(request), Mockito.eq(response)); - verify(request, times(1)).getRequestDispatcher(anyString()); - } } \ No newline at end of file diff --git a/backend/src/test/java/ch/puzzle/okr/architecture/OkrArchitectureTest.java b/backend/src/test/java/ch/puzzle/okr/architecture/OkrArchitectureTest.java index 5fda073556..0607ffd3c1 100644 --- a/backend/src/test/java/ch/puzzle/okr/architecture/OkrArchitectureTest.java +++ b/backend/src/test/java/ch/puzzle/okr/architecture/OkrArchitectureTest.java @@ -10,8 +10,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvFileSource; import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.stereotype.Component; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.*; import org.springframework.web.bind.annotation.RestController; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; @@ -91,7 +90,8 @@ void controllersAreAnnotatedWithRestController() { JavaClasses importedClasses = getMainSourceClasses(); ArchRule rule = classes().that().areNotAnonymousClasses().and().resideInAPackage("ch.puzzle.okr.controller..") - .should().beAnnotatedWith(RestController.class).andShould().notBeInterfaces(); + .should().beAnnotatedWith(RestController.class).orShould().beAnnotatedWith(Controller.class).andShould() + .notBeInterfaces(); rule.check(importedClasses); } diff --git a/docker/Dockerfile b/docker/Dockerfile index a4f2298a17..4af256761c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -9,6 +9,8 @@ USER 1001 WORKDIR app-root/backend -COPY --chown=1001 backend/target . +RUN if [ -d "../backend/target" ]; then \ + cp -r --chown=1001 ../backend/target .; \ + fi ENTRYPOINT ["/bin/sh", "-c", "export BACKEND_VERSION=$(find . -type f -name 'backend-*.jar' -print -quit | sed -n 's/.*backend-\\(.*\\)\\.jar/\\1/p'); java -jar backend-${BACKEND_VERSION}.jar"] \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 9cf6bedb79..b771f743d5 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.5' - services: okr-db: container_name: okr-dev-db diff --git a/docker/local-prod/docker-compose.yml b/docker/local-prod/docker-compose.yml new file mode 100644 index 0000000000..d719d12bfc --- /dev/null +++ b/docker/local-prod/docker-compose.yml @@ -0,0 +1,54 @@ +include: + - ../docker-compose.yml +services: + spring: + container_name: spring + build: + context: ./.. + dockerfile: Dockerfile + restart: always + ports: + - 8080:8080 + environment: + SPRING_PROFILES_ACTIVE: staging + LOGGING_LEVEL_ORG_SPRINGFRAMEWORK: debug + SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER-URI: http://localhost:8544/realms/pitc + SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK-SET-URI: http://keycloak:8080/realms/pitc/protocol/openid-connect/certs +# SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER-URI: https://sso.puzzle.ch/auth/realms/pitc +# SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK-SET-URI: https://sso.puzzle.ch/auth/realms/pitc/protocol/openid-connect/certs + SPRING_SECURITY_OAUTH2_RESOURCESERVER_OPAQUETOKEN_CLIENT-ID: pitc_okr_staging + ## Postgres DB + SPRING_DATASOURCE_URL: jdbc:postgresql://okr-dev-db:5432/okr + SPRING_DATASOURCE_USERNAME: user + SPRING_DATASOURCE_PASSWORD: pwd + SPRING_FLYWAY_LOCATIONS: classpath:db/data-migration,classpath:db/migration,classpath:db/callback + + ## In memory DB +# SPRING_DATASOURCE_URL: jdbc:h2:mem:db;DB_CLOSE_DELAY=-1 +# SPRING_DATASOURCE_USERNAME: user +# SPRING_DATASOURCE_PASSWORD: sa +# SPRING_FLYWAY_LOCATIONS: classpath:db/h2-db/database-h2-schema,classpath:db/h2-db/data-test-h2 + volumes: + - ../../../okr/backend/target:/app-root/backend + depends_on: + maven: + condition: service_completed_successfully + + maven: + container_name: maven + image: maven:3.8.3-openjdk-17 + volumes: + - ../../../okr:/opt + - /etc/passwd:/etc/passwd:ro + - /etc/group:/etc/group:ro + command: [ "/bin/bash", "-c", "cd /opt && mvn -B clean package --file pom.xml -P build-for-docker && chown -R 1000:1000 ./backend/target" ] + + angular: + container_name: angular + image: node:20 + user: "${UID:-1000}:${GID:-1000}" + volumes: + - ../../../okr:/opt + - /etc/passwd:/etc/passwd:ro + - /etc/group:/etc/group:ro + command: [ "/bin/bash", "-c", "cd /opt/frontend && npm ci && npm run watch:prod" ] diff --git a/frontend/cypress/support/commands.ts b/frontend/cypress/support/commands.ts index a5d8151bd8..8b7984c01b 100644 --- a/frontend/cypress/support/commands.ts +++ b/frontend/cypress/support/commands.ts @@ -253,7 +253,7 @@ function loginWithCredentials(username: string, password: string) { cy.url().then((url) => { const currentUrl = new URL(url); const baseURL = new URL(Cypress.config().baseUrl!); - expect(currentUrl.pathname).equal(baseURL.pathname); + expect(currentUrl.pathname).equal(baseURL.pathname + 'callback'); }); } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 62f5abeef5..31cd04ab91 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -21,7 +21,7 @@ "@angular/router": "^17.0.6", "@ngx-translate/core": "^15.0.0", "@ngx-translate/http-loader": "^8.0.0", - "angular-oauth2-oidc": "^17.0.0", + "angular-auth-oidc-client": "^18.0.1", "bootstrap": "^5.3.2", "moment": "^2.30.1", "ngx-toastr": "^18.0.0", @@ -6332,17 +6332,20 @@ "ajv": "^8.8.2" } }, - "node_modules/angular-oauth2-oidc": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/angular-oauth2-oidc/-/angular-oauth2-oidc-17.0.2.tgz", - "integrity": "sha512-zYgeLmAnu1g8XAYZK+csAsCQBDhgp9ffBv/eArEnujGxNPTeK00bREHWObtehflpQdSn+k9rY2D15ChCSydyVw==", + "node_modules/angular-auth-oidc-client": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/angular-auth-oidc-client/-/angular-auth-oidc-client-18.0.1.tgz", + "integrity": "sha512-r+PWZuni5msVEKFyA8HQ1lTTbSrrrsgFnU3qGK6P3TMl6+G3d2KdsXztBaRCNFU4oZq8mbDnmwShyvvVYSvxig==", "license": "MIT", "dependencies": { - "tslib": "^2.5.2" + "rfc4648": "^1.5.0", + "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/common": ">=14.0.0", - "@angular/core": ">=14.0.0" + "@angular/common": ">=15.0.0", + "@angular/core": ">=15.0.0", + "@angular/router": ">=15.0.0", + "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/ansi-colors": { @@ -15343,6 +15346,12 @@ "node": ">=0.10.0" } }, + "node_modules/rfc4648": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.3.tgz", + "integrity": "sha512-MjOWxM065+WswwnmNONOT+bD1nXzY9Km6u3kzvnx8F8/HXGZdz3T6e6vZJ8Q/RIMUSp/nxqjH3GwvJDy8ijeQQ==", + "license": "MIT" + }, "node_modules/rfdc": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 378772ff9f..0ddec04cb1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,6 +7,7 @@ "build": "ng build", "build:staging": "ng build --configuration staging", "watch": "ng build --watch --configuration development", + "watch:prod": "ng build --watch", "test": "jest --silent", "cypress:open": "cypress open", "cypress:run": "cypress run --browser chrome --headed", @@ -34,7 +35,7 @@ "@angular/router": "^17.0.6", "@ngx-translate/core": "^15.0.0", "@ngx-translate/http-loader": "^8.0.0", - "angular-oauth2-oidc": "^17.0.0", + "angular-auth-oidc-client": "^18.0.1", "bootstrap": "^5.3.2", "moment": "^2.30.1", "ngx-toastr": "^18.0.0", diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 16770e989a..b62a90a4f7 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -3,7 +3,8 @@ import { ActivatedRouteSnapshot, ResolveFn, RouterModule, Routes } from '@angula import { OverviewComponent } from './overview/overview.component'; import { EMPTY, of } from 'rxjs'; import { SidepanelComponent } from './shared/custom/sidepanel/sidepanel.component'; -import { authGuard } from './shared/guards/auth.guard'; +import { AutoLoginPartialRoutesGuard } from 'angular-auth-oidc-client'; +import { CallbackComponent } from './callback/callback.component'; /** * Resolver for get the id from url like `/objective/42` or `/keyresult/42`. @@ -36,8 +37,9 @@ const routes: Routes = [ data: { type: 'KeyResult' }, }, ], - canActivate: [authGuard], + canActivate: [AutoLoginPartialRoutesGuard], }, + { path: 'callback', component: CallbackComponent }, { path: '**', redirectTo: '', pathMatch: 'full' }, ]; diff --git a/frontend/src/app/app.component.spec.ts b/frontend/src/app/app.component.spec.ts index cf03dc6c2f..0359ed6237 100644 --- a/frontend/src/app/app.component.spec.ts +++ b/frontend/src/app/app.component.spec.ts @@ -1,9 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; import { AppComponent } from './app.component'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { TranslateTestingModule } from 'ngx-translate-testing'; -import { AuthConfig, OAuthModule, OAuthService } from 'angular-oauth2-oidc'; import { HttpClientTestingModule } from '@angular/common/http/testing'; // @ts-ignore import * as de from '../assets/i18n/de.json'; @@ -11,30 +9,12 @@ import { HarnessLoader } from '@angular/cdk/testing'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { MatSidenavModule } from '@angular/material/sidenav'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { NavigationEnd, Routes } from '@angular/router'; +import { RouterModule, Routes } from '@angular/router'; import { of } from 'rxjs'; import { OverviewComponent } from './overview/overview.component'; import { ObjectiveDetailComponent } from './objective-detail/objective-detail.component'; import { CommonModule } from '@angular/common'; - -const oauthServiceMock = { - configure(environment: AuthConfig): void {}, - initCodeFlow(): void {}, - setupAutomaticSilentRefresh(): void {}, - hasValidAccessToken(): boolean { - return true; - }, - loadDiscoveryDocumentAndTryLogin(): Promise { - this.initCodeFlow(); - return Promise.resolve(); - }, -}; - -const routerMock = { - root: jest.fn(), - // Router - events: of(new NavigationEnd(0, 'http://localhost:4200/objective/2', 'http://localhost:4200/objective/2')), -}; +import { StsConfigLoader } from 'angular-auth-oidc-client'; const routes: Routes = [ { @@ -52,17 +32,24 @@ describe('AppComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ - RouterTestingModule.withRoutes(routes), + RouterModule.forRoot(routes), HttpClientTestingModule, TranslateTestingModule.withTranslations({ de: de, }), - OAuthModule.forRoot(), MatSidenavModule, NoopAnimationsModule, CommonModule, ], - providers: [{ provide: OAuthService, useValue: oauthServiceMock }], + providers: [ + { + provide: StsConfigLoader, + useValue: { + loadConfig: () => of({}), + loadConfigs: () => of({}), + }, + }, + ], declarations: [AppComponent, OverviewComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA], }) diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 6cdc4580e9..6f601629c9 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,4 +1,5 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { OidcSecurityService } from 'angular-auth-oidc-client'; @Component({ selector: 'app-root', @@ -6,4 +7,9 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; styleUrls: ['./app.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class AppComponent {} +export class AppComponent implements OnInit { + constructor(private oidcSecurityService: OidcSecurityService) {} + ngOnInit(): void { + this.oidcSecurityService.checkAuth().subscribe(); + } +} diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 59dab8cddb..05ed1b9104 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -20,14 +20,13 @@ import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-transla import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core'; import { MomentDateAdapter } from '@angular/material-moment-adapter'; -import { OAuthModule, OAuthService, OAuthStorage } from 'angular-oauth2-oidc'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { A11yModule } from '@angular/cdk/a11y'; import { ExampleDialogComponent } from './shared/dialog/example-dialog/example-dialog.component'; import { MatRadioModule } from '@angular/material/radio'; import { ConfigService } from './config.service'; -import { firstValueFrom } from 'rxjs'; +import { config, firstValueFrom, map } from 'rxjs'; import { environment } from '../environments/environment'; import { OauthInterceptor } from './shared/interceptors/oauth.interceptor'; import { ApplicationTopBarComponent } from './application-top-bar/application-top-bar.component'; @@ -72,22 +71,29 @@ import { CdkDrag, CdkDragHandle, CdkDropList } from '@angular/cdk/drag-drop'; import { TeamManagementComponent } from './shared/dialog/team-management/team-management.component'; import { KeyresultDialogComponent } from './shared/dialog/keyresult-dialog/keyresult-dialog.component'; import { CustomizationService } from './shared/services/customization.service'; +import { + AuthModule, + DefaultLocalStorageService, + AbstractSecurityStorage, + StsConfigLoader, + StsConfigHttpLoader, + AuthInterceptor, +} from 'angular-auth-oidc-client'; +import { CallbackComponent } from './callback/callback.component'; -function initOauthFactory(configService: ConfigService, oauthService: OAuthService) { - return async () => { - const config = await firstValueFrom(configService.config$); - oauthService.configure({ ...environment.oauth, issuer: config.issuer, clientId: config.clientId }); - }; +function initOauthFactory(configService: ConfigService) { + const config$ = configService.config$.pipe( + map((config) => { + return { ...environment.oauth, authority: config.issuer, clientId: config.clientId }; + }), + ); + return new StsConfigHttpLoader(config$); } export function createTranslateLoader(http: HttpBackend) { return new TranslateHttpLoader(new HttpClient(http), './assets/i18n/', '.json'); } -export function storageFactory(): OAuthStorage { - return localStorage; -} - export const MY_FORMATS = { parse: { dateInput: 'LL', @@ -134,12 +140,20 @@ export const MY_FORMATS = { ActionPlanComponent, TeamManagementComponent, KeyresultDialogComponent, + CallbackComponent, ], imports: [ CommonModule, BrowserModule, AppRoutingModule, HttpClientModule, + AuthModule.forRoot({ + loader: { + provide: StsConfigLoader, + useFactory: initOauthFactory, + deps: [ConfigService], + }, + }), MatFormFieldModule, MatSelectModule, MatIconModule, @@ -164,7 +178,6 @@ export const MY_FORMATS = { deps: [HttpBackend], }, }), - OAuthModule.forRoot(), A11yModule, MatRadioModule, NgOptimizedImage, @@ -191,8 +204,10 @@ export const MY_FORMATS = { { provide: MAT_DATE_FORMATS, useValue: MY_FORMATS }, { provide: HTTP_INTERCEPTORS, useClass: OauthInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }, - { provide: OAuthStorage, useFactory: storageFactory }, - { provide: APP_INITIALIZER, useFactory: initOauthFactory, deps: [ConfigService, OAuthService], multi: true }, + { + provide: AbstractSecurityStorage, + useClass: DefaultLocalStorageService, + }, { provide: Router, useClass: CustomRouter, diff --git a/frontend/src/app/application-top-bar/application-top-bar.component.html b/frontend/src/app/application-top-bar/application-top-bar.component.html index 9ceb0f491e..c6183a9593 100644 --- a/frontend/src/app/application-top-bar/application-top-bar.component.html +++ b/frontend/src/app/application-top-bar/application-top-bar.component.html @@ -28,7 +28,7 @@ class="topBarEntry flex-nowrap btn" > - {{ username | async }} + {{ username() | async }} expand_more expand_less diff --git a/frontend/src/app/application-top-bar/application-top-bar.component.spec.ts b/frontend/src/app/application-top-bar/application-top-bar.component.spec.ts index ce6e117313..8dd77c88f6 100644 --- a/frontend/src/app/application-top-bar/application-top-bar.component.spec.ts +++ b/frontend/src/app/application-top-bar/application-top-bar.component.spec.ts @@ -1,8 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - import { ApplicationTopBarComponent } from './application-top-bar.component'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { DateTimeProvider, OAuthLogger, OAuthService, UrlHelperService } from 'angular-oauth2-oidc'; import { HttpClient, HttpHandler } from '@angular/common/http'; import { MatMenuModule } from '@angular/material/menu'; import { HarnessLoader } from '@angular/cdk/testing'; @@ -12,11 +10,12 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { of } from 'rxjs'; import { team1 } from '../shared/testData'; +import { OidcSecurityService } from 'angular-auth-oidc-client'; +import { NgOptimizedImage } from '@angular/common'; const oAuthMock = { - getIdentityClaims: jest.fn(), - logOut: jest.fn(), - hasValidIdToken: jest.fn(), + getUserData: jest.fn(), + logOff: jest.fn(), }; const dialogMock = { @@ -30,15 +29,12 @@ describe('ApplicationHeaderComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [MatMenuModule, NoopAnimationsModule, MatDialogModule], + imports: [MatMenuModule, NoopAnimationsModule, MatDialogModule, NgOptimizedImage], declarations: [ApplicationTopBarComponent], providers: [ - { provide: OAuthService, useValue: oAuthMock }, + { provide: OidcSecurityService, useValue: oAuthMock }, { provide: HttpClient }, { provide: HttpHandler }, - { provide: UrlHelperService }, - { provide: OAuthLogger }, - { provide: DateTimeProvider }, { provide: MatDialog, useValue: dialogMock, @@ -57,12 +53,14 @@ describe('ApplicationHeaderComponent', () => { }); it('logout function should get called on button click', async () => { + oAuthMock.getUserData.mockReturnValue(of({ name: 'Username' })); + const harness = await loader.getHarness(MatMenuHarness); await harness.open(); fixture.detectChanges(); harness.getItems().then((items) => { items[0].click(); - expect(oAuthMock.logOut).toBeCalledTimes(1); + expect(oAuthMock.logOff).toHaveBeenCalledTimes(1); }); }); diff --git a/frontend/src/app/application-top-bar/application-top-bar.component.ts b/frontend/src/app/application-top-bar/application-top-bar.component.ts index 873cf185a0..7f99d2f4ae 100644 --- a/frontend/src/app/application-top-bar/application-top-bar.component.ts +++ b/frontend/src/app/application-top-bar/application-top-bar.component.ts @@ -1,13 +1,11 @@ import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { OAuthService } from 'angular-oauth2-oidc'; -import { BehaviorSubject, ReplaySubject, Subscription } from 'rxjs'; +import { BehaviorSubject, map, of, ReplaySubject, Subscription, switchMap } from 'rxjs'; import { ConfigService } from '../config.service'; import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { TeamManagementComponent } from '../shared/dialog/team-management/team-management.component'; - -import { Router } from '@angular/router'; import { RefreshDataService } from '../shared/services/refresh-data.service'; import { isMobileDevice } from '../shared/common'; +import { OidcSecurityService } from 'angular-auth-oidc-client'; @Component({ selector: 'app-application-top-bar', @@ -16,7 +14,6 @@ import { isMobileDevice } from '../shared/common'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ApplicationTopBarComponent implements OnInit, OnDestroy { - username: ReplaySubject = new ReplaySubject(); menuIsOpen = false; @Input() @@ -26,10 +23,9 @@ export class ApplicationTopBarComponent implements OnInit, OnDestroy { private subscription?: Subscription; constructor( - private oauthService: OAuthService, + private oauthService: OidcSecurityService, private configService: ConfigService, private dialog: MatDialog, - private router: Router, private refreshDataService: RefreshDataService, ) {} @@ -41,10 +37,6 @@ export class ApplicationTopBarComponent implements OnInit, OnDestroy { } }, }); - - if (this.oauthService.hasValidIdToken()) { - this.username.next(this.oauthService.getIdentityClaims()['name']); - } } ngOnDestroy(): void { @@ -52,10 +44,11 @@ export class ApplicationTopBarComponent implements OnInit, OnDestroy { } logOut() { - const currentUrlTree = this.router.createUrlTree([], { queryParams: {} }); - this.router.navigateByUrl(currentUrlTree).then(() => { - this.oauthService.logOut(); - }); + this.oauthService.logoff().subscribe(); + } + + username() { + return this.oauthService.getUserData().pipe(map((user) => user?.name || '')); } openTeamManagement() { diff --git a/frontend/src/app/callback/callback.component.css b/frontend/src/app/callback/callback.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/app/callback/callback.component.html b/frontend/src/app/callback/callback.component.html new file mode 100644 index 0000000000..9a5f2cf345 --- /dev/null +++ b/frontend/src/app/callback/callback.component.html @@ -0,0 +1 @@ +

You are getting forwarded!

diff --git a/frontend/src/app/callback/callback.component.ts b/frontend/src/app/callback/callback.component.ts new file mode 100644 index 0000000000..ac042fdc53 --- /dev/null +++ b/frontend/src/app/callback/callback.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-callback', + templateUrl: './callback.component.html', + styleUrl: './callback.component.css', +}) +export class CallbackComponent {} diff --git a/frontend/src/app/objective-filter/objective-filter.component.spec.ts b/frontend/src/app/objective-filter/objective-filter.component.spec.ts index 8a7c431786..0eab0ac2af 100644 --- a/frontend/src/app/objective-filter/objective-filter.component.spec.ts +++ b/frontend/src/app/objective-filter/objective-filter.component.spec.ts @@ -15,6 +15,8 @@ import { Router } from '@angular/router'; import { RouterTestingHarness } from '@angular/router/testing'; import { authGuard } from '../shared/guards/auth.guard'; import { OverviewComponent } from '../overview/overview.component'; +import { AbstractLoggerService, AutoLoginPartialRoutesGuard, StsConfigLoader } from 'angular-auth-oidc-client'; +import { of } from 'rxjs'; describe('ObjectiveFilterComponent', () => { let component: ObjectiveFilterComponent; @@ -22,17 +24,26 @@ describe('ObjectiveFilterComponent', () => { let loader: HarnessLoader; let router: Router; - const authGuardMock = () => { - return Promise.resolve(true); - }; - beforeEach(() => { TestBed.configureTestingModule({ declarations: [ObjectiveFilterComponent, OverviewComponent], providers: [ { - provide: authGuard, - useValue: authGuardMock, + provide: StsConfigLoader, + useValue: { + loadConfig: () => of({}), + loadConfigs: () => of([{}]), + }, + }, + { + provide: AbstractLoggerService, + useValue: { + logError: () => of({}), + }, + }, + { + provide: AutoLoginPartialRoutesGuard, + useValue: () => Promise.resolve(true), }, ], imports: [ diff --git a/frontend/src/app/overview/overview.component.spec.ts b/frontend/src/app/overview/overview.component.spec.ts index 4599cb1e60..661e6838d4 100644 --- a/frontend/src/app/overview/overview.component.spec.ts +++ b/frontend/src/app/overview/overview.component.spec.ts @@ -1,5 +1,4 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - import { OverviewComponent } from './overview.component'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { overViewEntity1 } from '../shared/testData'; @@ -11,11 +10,12 @@ import { RefreshDataService } from '../shared/services/refresh-data.service'; import { authGuard } from '../shared/guards/auth.guard'; import { ApplicationBannerComponent } from '../application-banner/application-banner.component'; import { ApplicationTopBarComponent } from '../application-top-bar/application-top-bar.component'; -import { DateTimeProvider, OAuthLogger, OAuthService, UrlHelperService } from 'angular-oauth2-oidc'; import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { MatMenuModule } from '@angular/material/menu'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { AbstractLoggerService, AutoLoginPartialRoutesGuard, StsConfigLoader } from 'angular-auth-oidc-client'; +import { NgOptimizedImage } from '@angular/common'; const overviewService = { getOverview: jest.fn(), @@ -46,7 +46,14 @@ describe('OverviewComponent', () => { let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [HttpClientTestingModule, AppRoutingModule, MatDialogModule, MatIconModule, MatMenuModule], + imports: [ + HttpClientTestingModule, + AppRoutingModule, + MatDialogModule, + MatIconModule, + MatMenuModule, + NgOptimizedImage, + ], declarations: [OverviewComponent, ApplicationBannerComponent, ApplicationTopBarComponent], providers: [ { @@ -54,8 +61,8 @@ describe('OverviewComponent', () => { useValue: overviewService, }, { - provide: authGuard, - useValue: authGuardMock, + provide: AutoLoginPartialRoutesGuard, + useValue: () => Promise.resolve(true), }, { provide: RefreshDataService, @@ -65,10 +72,19 @@ describe('OverviewComponent', () => { provide: MatDialogRef, useValue: {}, }, - OAuthService, - UrlHelperService, - OAuthLogger, - DateTimeProvider, + { + provide: StsConfigLoader, + useValue: { + loadConfig: () => of({}), + loadConfigs: () => of([{}]), + }, + }, + { + provide: AbstractLoggerService, + useValue: { + logError: () => of({}), + }, + }, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); diff --git a/frontend/src/app/shared/dialog/key-result-form/key-result-form.component.spec.ts b/frontend/src/app/shared/dialog/key-result-form/key-result-form.component.spec.ts index 70df372799..1b4292e762 100644 --- a/frontend/src/app/shared/dialog/key-result-form/key-result-form.component.spec.ts +++ b/frontend/src/app/shared/dialog/key-result-form/key-result-form.component.spec.ts @@ -15,7 +15,6 @@ import { MatRadioModule } from '@angular/material/radio'; import { KeyResultObjective } from '../../types/model/KeyResultObjective'; import { User } from '../../types/model/User'; import { DialogHeaderComponent } from '../../custom/dialog-header/dialog-header.component'; -import { OAuthService } from 'angular-oauth2-oidc'; import { KeyresultTypeComponent } from '../../../keyresult-type/keyresult-type.component'; import { ActionPlanComponent } from '../../../action-plan/action-plan.component'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; @@ -26,15 +25,17 @@ import { Action } from '../../types/model/Action'; import { KeyResultMetric } from '../../types/model/KeyResultMetric'; import { KeyResultOrdinal } from '../../types/model/KeyResultOrdinal'; import { TranslateTestingModule } from 'ngx-translate-testing'; +// @ts-ignore import * as de from '../../../../assets/i18n/de.json'; +import { OidcSecurityService } from 'angular-auth-oidc-client'; describe('KeyResultFormComponent', () => { let component: KeyResultFormComponent; let fixture: ComponentFixture; const oauthMockService = { - getIdentityClaims() { - return { name: users[1].firstname + ' ' + users[1].lastname }; + getUserData() { + return of({ name: users[1].firstname + ' ' + users[1].lastname }); }, }; @@ -115,7 +116,7 @@ describe('KeyResultFormComponent', () => { useValue: matDialogRefMock, }, { - provide: OAuthService, + provide: OidcSecurityService, useValue: oauthMockService, }, ], @@ -233,7 +234,9 @@ describe('KeyResultFormComponent', () => { }); it('should get username from oauthService right', () => { - expect(component.getUserName()).toEqual('Paco Egiman'); + component.getUserName().subscribe((userName) => { + expect(userName).toEqual('Paco Egiman'); + }); }); }); }); diff --git a/frontend/src/app/shared/dialog/key-result-form/key-result-form.component.ts b/frontend/src/app/shared/dialog/key-result-form/key-result-form.component.ts index e273ab0e93..04dd1535b2 100644 --- a/frontend/src/app/shared/dialog/key-result-form/key-result-form.component.ts +++ b/frontend/src/app/shared/dialog/key-result-form/key-result-form.component.ts @@ -4,12 +4,12 @@ import { User } from '../../types/model/User'; import { KeyResult } from '../../types/model/KeyResult'; import { KeyResultMetric } from '../../types/model/KeyResultMetric'; import { KeyResultOrdinal } from '../../types/model/KeyResultOrdinal'; -import { BehaviorSubject, filter, map, Observable, of, startWith, switchMap } from 'rxjs'; +import { BehaviorSubject, filter, map, Observable, of, startWith, switchMap, tap } from 'rxjs'; import { UserService } from '../../services/user.service'; import { Action } from '../../types/model/Action'; import { formInputCheck, hasFormFieldErrors } from '../../common'; -import { OAuthService } from 'angular-oauth2-oidc'; import { TranslateService } from '@ngx-translate/core'; +import { OidcSecurityService } from 'angular-auth-oidc-client'; @Component({ selector: 'app-key-result-form', @@ -31,7 +31,7 @@ export class KeyResultFormComponent implements OnInit { constructor( public userService: UserService, - private oauthService: OAuthService, + private oauthService: OidcSecurityService, private translate: TranslateService, ) {} @@ -62,11 +62,12 @@ export class KeyResultFormComponent implements OnInit { ]); this.users$.subscribe((users) => { - const loggedInUser = this.getUserName(); - users.forEach((user) => { - if (user.firstname + ' ' + user.lastname === loggedInUser) { - this.keyResultForm.controls['owner'].setValue(user); - } + this.getUserName().subscribe((userName) => { + users.forEach((user) => { + if (user.firstname + ' ' + user.lastname === userName) { + this.keyResultForm.controls['owner'].setValue(user); + } + }); }); }); } @@ -131,6 +132,6 @@ export class KeyResultFormComponent implements OnInit { updateFormValidity() {} getUserName() { - return this.oauthService.getIdentityClaims()['name']; + return this.oauthService.getUserData().pipe(map((user) => user?.name || '')); } } diff --git a/frontend/src/app/shared/dialog/keyresult-dialog/keyresult-dialog.component.spec.ts b/frontend/src/app/shared/dialog/keyresult-dialog/keyresult-dialog.component.spec.ts index 0f94499113..4ed5d5d686 100644 --- a/frontend/src/app/shared/dialog/keyresult-dialog/keyresult-dialog.component.spec.ts +++ b/frontend/src/app/shared/dialog/keyresult-dialog/keyresult-dialog.component.spec.ts @@ -17,13 +17,13 @@ import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { KeyResultObjective } from '../../types/model/KeyResultObjective'; import { DialogHeaderComponent } from '../../custom/dialog-header/dialog-header.component'; -import { OAuthService } from 'angular-oauth2-oidc'; import { KeyresultTypeComponent } from '../../../keyresult-type/keyresult-type.component'; import { ActionPlanComponent } from '../../../action-plan/action-plan.component'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { DragDropModule } from '@angular/cdk/drag-drop'; import { UserService } from '../../services/user.service'; import { KeyResultFormComponent } from '../key-result-form/key-result-form.component'; +import { OidcSecurityService } from 'angular-auth-oidc-client'; describe('KeyresultDialogComponent', () => { let component: KeyresultDialogComponent; @@ -31,8 +31,8 @@ describe('KeyresultDialogComponent', () => { let keyResultService: KeyresultService; const oauthMockService = { - getIdentityClaims() { - return { name: users[1].firstname + ' ' + users[1].lastname }; + getUserData() { + return of({ name: users[1].firstname + ' ' + users[1].lastname }); }, }; @@ -277,7 +277,7 @@ describe('KeyresultDialogComponent', () => { useValue: { objective: fullObjective, keyResult: undefined }, }, { - provide: OAuthService, + provide: OidcSecurityService, useValue: oauthMockService, }, ], @@ -419,7 +419,7 @@ describe('KeyresultDialogComponent', () => { }, }, { - provide: OAuthService, + provide: OidcSecurityService, useValue: oauthMockService, }, { @@ -562,7 +562,7 @@ describe('KeyresultDialogComponent', () => { useValue: matDialogRefMock, }, { - provide: OAuthService, + provide: OidcSecurityService, useValue: oauthMockService, }, { diff --git a/frontend/src/app/shared/guards/auth.guard.spec.ts b/frontend/src/app/shared/guards/auth.guard.spec.ts index 4e13f0c262..318fae4558 100644 --- a/frontend/src/app/shared/guards/auth.guard.spec.ts +++ b/frontend/src/app/shared/guards/auth.guard.spec.ts @@ -1,61 +1,56 @@ import { TestBed } from '@angular/core/testing'; -import { CanActivateFn } from '@angular/router'; +import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from '@angular/router'; import { authGuard } from './auth.guard'; -import { OAuthService } from 'angular-oauth2-oidc'; +import { OidcSecurityService } from 'angular-auth-oidc-client'; +import { of } from 'rxjs'; const oAuthMock = { - initCodeFlow: jest.fn(), - loadDiscoveryDocumentAndTryLogin: jest.fn(), - hasValidIdToken: jest.fn(), - setupAutomaticSilentRefresh: jest.fn(), + get isAuthenticated$() { + return of({}); + }, }; describe('authGuard', () => { const executeGuard: CanActivateFn = (...guardParameters) => TestBed.runInInjectionContext(() => authGuard(...guardParameters)); + const route: ActivatedRouteSnapshot = {} as any; + const state: RouterStateSnapshot = {} as any; beforeEach(() => { TestBed.configureTestingModule({ providers: [ { - provide: OAuthService, + provide: OidcSecurityService, useValue: oAuthMock, }, ], }); - jest.spyOn(oAuthMock, 'initCodeFlow').mockReturnValue(true); - jest.spyOn(oAuthMock, 'loadDiscoveryDocumentAndTryLogin').mockReturnValue(Promise.resolve(true)); - jest.spyOn(oAuthMock, 'setupAutomaticSilentRefresh').mockReturnValue(true); - oAuthMock.initCodeFlow.mockReset(); - oAuthMock.setupAutomaticSilentRefresh.mockReset(); }); it('should be created', () => { expect(executeGuard).toBeTruthy(); }); - it('should not call initCodeFlow if token is valid', async () => { - jest.spyOn(oAuthMock, 'hasValidIdToken').mockReturnValue(true); - const result = await runAuthGuardWithContext(authGuard); + it('should return true if token valid ', async () => { + jest.spyOn(oAuthMock, 'isAuthenticated$', 'get').mockReturnValue(of({ isAuthenticated: true })); + const result$ = runAuthGuardWithContext(authGuard, route, state); - expect(result).toBe(true); - expect(oAuthMock.loadDiscoveryDocumentAndTryLogin).toHaveBeenCalled(); - expect(oAuthMock.initCodeFlow).not.toHaveBeenCalled(); - expect(oAuthMock.setupAutomaticSilentRefresh).toHaveBeenCalled(); + result$.then((result) => { + expect(result).toBe(true); + }); }); - it('should call initCodeFlow if token is invalid', async () => { - jest.spyOn(oAuthMock, 'hasValidIdToken').mockReturnValue(false); - const result = await runAuthGuardWithContext(authGuard); + it('should return false if token invalid', async () => { + jest.spyOn(oAuthMock, 'isAuthenticated$', 'get').mockReturnValue(of({ isAuthenticated: false })); + const result$ = runAuthGuardWithContext(authGuard, route, state); - expect(result).toBe(false); - expect(oAuthMock.loadDiscoveryDocumentAndTryLogin).toHaveBeenCalled(); - expect(oAuthMock.initCodeFlow).toHaveBeenCalled(); - expect(oAuthMock.setupAutomaticSilentRefresh).not.toHaveBeenCalled(); + result$.then((result) => { + expect(result).toBe(false); + }); }); - async function runAuthGuardWithContext(authGuard: any): Promise { - return await TestBed.runInInjectionContext(authGuard); + async function runAuthGuardWithContext(authGuard: any, route: any, state: any): Promise { + return await TestBed.runInInjectionContext(() => authGuard(route, state)); } }); diff --git a/frontend/src/app/shared/guards/auth.guard.ts b/frontend/src/app/shared/guards/auth.guard.ts index 137380edbe..6160c00ac4 100644 --- a/frontend/src/app/shared/guards/auth.guard.ts +++ b/frontend/src/app/shared/guards/auth.guard.ts @@ -1,18 +1,20 @@ -import { CanActivateFn } from '@angular/router'; +import { CanActivateFn, Router } from '@angular/router'; import { inject } from '@angular/core'; -import { OAuthService } from 'angular-oauth2-oidc'; +import { OidcSecurityService } from 'angular-auth-oidc-client'; +import { map, take, tap } from 'rxjs'; +//Not used, can be deleted export const authGuard: CanActivateFn = (route, state) => { - const oauthService = inject(OAuthService); - return oauthService.loadDiscoveryDocumentAndTryLogin().then(async () => { - // if the login failed initialize code flow - let validToken = oauthService.hasValidIdToken(); - if (!validToken) { - oauthService.initCodeFlow(); - return false; - } - oauthService.setupAutomaticSilentRefresh(); - location.hash = ''; - return true; - }); + const oidcSecurityService = inject(OidcSecurityService); + // console.log(oidcSecurityService.isAuthenticated$.subscribe(console.log)); + + return oidcSecurityService.isAuthenticated$.pipe( + take(1), + map(({ isAuthenticated }) => { + // allow navigation if authenticated + return isAuthenticated; + + // redirect if not authenticated + }), + ); }; diff --git a/frontend/src/app/shared/interceptors/oauth.interceptor.spec.ts b/frontend/src/app/shared/interceptors/oauth.interceptor.spec.ts index 8650d459c9..1ee37d9d4d 100644 --- a/frontend/src/app/shared/interceptors/oauth.interceptor.spec.ts +++ b/frontend/src/app/shared/interceptors/oauth.interceptor.spec.ts @@ -2,13 +2,23 @@ import { TestBed } from '@angular/core/testing'; import { OauthInterceptor } from './oauth.interceptor'; import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { DateTimeProvider, OAuthLogger, OAuthModule, OAuthService, UrlHelperService } from 'angular-oauth2-oidc'; +import { OidcSecurityService, StsConfigLoader } from 'angular-auth-oidc-client'; +import { of } from 'rxjs'; describe('OauthInterceptor', () => { beforeEach(() => TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [OAuthService, UrlHelperService, OAuthLogger, DateTimeProvider, OAuthService], + providers: [ + OidcSecurityService, + { + provide: StsConfigLoader, + useValue: { + loadConfig: () => of({}), + loadConfigs: () => of([{}]), + }, + }, + ], }), ); diff --git a/frontend/src/app/shared/interceptors/oauth.interceptor.ts b/frontend/src/app/shared/interceptors/oauth.interceptor.ts index 0b96fa28b6..a4ec56f49b 100644 --- a/frontend/src/app/shared/interceptors/oauth.interceptor.ts +++ b/frontend/src/app/shared/interceptors/oauth.interceptor.ts @@ -1,26 +1,19 @@ import { Injectable } from '@angular/core'; import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; import { filter, map, merge, mergeMap, Observable, of, take, timeout } from 'rxjs'; -import { OAuthService } from 'angular-oauth2-oidc'; +import { OidcSecurityService } from 'angular-auth-oidc-client'; @Injectable({ providedIn: 'root', }) export class OauthInterceptor implements HttpInterceptor { - constructor(private oauthService: OAuthService) {} + constructor(private oauthService: OidcSecurityService) {} intercept(req: HttpRequest, next: HttpHandler): Observable> { if (!req.url.match(/^(\/)?api/)) { return next.handle(req); } - return merge( - of(this.oauthService.getAccessToken()).pipe(filter((token) => !!token)), - this.oauthService.events.pipe( - filter((e) => e.type === 'token_received'), - timeout(500), - map((_) => this.oauthService.getAccessToken()), - ), - ).pipe( + return this.oauthService.getAccessToken().pipe( take(1), mergeMap((token) => { if (token) { @@ -30,7 +23,6 @@ export class OauthInterceptor implements HttpInterceptor { } return next.handle(req); - // .pipe(catchError((err) => this.errorHandler.handleError(err))); }), ); } diff --git a/frontend/src/environments/environment.prod.ts b/frontend/src/environments/environment.prod.ts index 0bfbf9d09f..0f14293fee 100644 --- a/frontend/src/environments/environment.prod.ts +++ b/frontend/src/environments/environment.prod.ts @@ -1,19 +1,22 @@ -import { AuthConfig } from 'angular-oauth2-oidc'; +import { OpenIdConfiguration } from 'angular-auth-oidc-client'; export const environment = { production: true, staging: false, oauth: { - decreaseExpirationBySec: 30, - clearHashAfterLogin: true, - issuer: 'https://sso.puzzle.ch/auth/realms/pitc', - strictDiscoveryDocumentValidation: false, - // redirectUri: 'http://localhost:8080/auth/keycloakopenid/callback', + authority: 'https://sso.puzzle.ch/auth/realms/pitc', // redirectUri: `${window.location.protocol}//${window.location.hostname}:${window.location.port}/auth/keycloakopenid/callback${window.location.search}`, - redirectUri: `${window.location.protocol}//${window.location.hostname}:${window.location.port}`, - scope: 'profile openid', + redirectUrl: window.location.origin + '/callback', + postLogoutRedirectUri: window.location.origin, + renewTimeBeforeTokenExpiresInSeconds: 30, + scope: 'profile openid offline_access', clientId: 'pitc_okr_prod', responseType: 'code', - showDebugInformation: true, - } as AuthConfig, + // showDebugInformation: true, + customParamsRefreshTokenRequest: { + scope: 'openid profile offline_access', + }, + ignoreNonceAfterRefresh: true, // this is required if the id_token is not returned + triggerRefreshWhenIdTokenExpired: false, + } as OpenIdConfiguration, }; diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts index 6b65b98b9b..6980970211 100644 --- a/frontend/src/environments/environment.ts +++ b/frontend/src/environments/environment.ts @@ -2,22 +2,28 @@ // `ng build` replaces `environment.ts` with `environment.prod.ts`. // The list of file replacements can be found in `angular.json`. -import { AuthConfig } from 'angular-oauth2-oidc'; +import { LogLevel, OpenIdConfiguration } from 'angular-auth-oidc-client'; export const environment = { production: false, staging: false, oauth: { - decreaseExpirationBySec: 30, - clearHashAfterLogin: true, - issuer: '', - strictDiscoveryDocumentValidation: false, - redirectUri: 'http://localhost:4200', - scope: 'openid profile', + issuer: 'http://localhost:8095/auth/realms/pitc', + redirectUrl: 'http://localhost:4200/callback', + postLogoutRedirectUri: window.location.origin, + renewTimeBeforeTokenExpiresInSeconds: 30, + scope: 'openid profile offline_access', clientId: '', responseType: 'code', - showDebugInformation: true, - } as AuthConfig, + logLevel: LogLevel.Debug, + silentRenew: true, + useRefreshToken: true, + customParamsRefreshTokenRequest: { + scope: 'openid profile offline_access', + }, + ignoreNonceAfterRefresh: true, // this is required if the id_token is not returned + triggerRefreshWhenIdTokenExpired: false, + } as OpenIdConfiguration, }; /*