diff --git a/package.json b/package.json index 50219bdcd..080cff7c2 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "katex": "^0.16.21", "material-icons": "^1.13.12", "mermaid": "^11.15.0", + "minimatch": "^10.2.5", "minimatch-browser": "^1.0.0", "ngx-markdown": "19.1.1", "pdfjs-dist": "5.1.91", diff --git a/projects/aca-shared/src/lib/services/app.service.spec.ts b/projects/aca-shared/src/lib/services/app.service.spec.ts index c35210063..bcdfe990e 100644 --- a/projects/aca-shared/src/lib/services/app.service.spec.ts +++ b/projects/aca-shared/src/lib/services/app.service.spec.ts @@ -25,6 +25,7 @@ import { AppService } from './app.service'; import { TestBed } from '@angular/core/testing'; import { + AppConfigService, AuthenticationService, NoopTranslateModule, NotificationService, @@ -43,15 +44,24 @@ import { SharedLinksApiService, UploadService } from '@alfresco/adf-content-services'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { provideMockStore } from '@ngrx/store/testing'; import { RepositoryInfo, VersionInfo } from '@alfresco/js-api'; -import { MatDialogModule } from '@angular/material/dialog'; +import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { ContentApiService } from './content-api.service'; import { AppSettingsService, UserProfileService } from '@alfresco/aca-shared'; import { MatSnackBarModule } from '@angular/material/snack-bar'; +interface ApiUnauthorizedError { + status: number; + response: { + req?: { + url?: string; + }; + }; +} + describe('AppService', () => { let service: AppService; let auth: AuthenticationService; @@ -95,7 +105,9 @@ describe('AppService', () => { { provide: ActivatedRoute, useValue: { - snapshot: {} + snapshot: { + queryParams: {} + } } }, { @@ -222,6 +234,63 @@ describe('AppService', () => { expect(contentApi.getRepositoryInformation).toHaveBeenCalled(); }); + describe('Init with unauthorized api error', () => { + let appConfigService: AppConfigService; + let router: Router; + let matDialog: MatDialog; + let alfrescoApiService: AlfrescoApiService; + let navigateSpy: jasmine.Spy; + let closeAllSpy: jasmine.Spy; + let apiErrorListener: (error: ApiUnauthorizedError) => void; + + const setupUnauthorizedErrorListener = (currentUrl: string): void => { + spyOn(auth, 'isLoggedIn').and.returnValue(false); + spyOn(alfrescoApiService, 'isExcludedErrorListener').and.returnValue(false); + spyOn(appConfigService, 'get').and.callFake((key: string, defaultValue?: any) => (key === 'oauth2.publicUrls' ? ['/public/**'] : defaultValue)); + spyOnProperty(router, 'url', 'get').and.returnValue(currentUrl); + closeAllSpy = spyOn(matDialog, 'closeAll'); + navigateSpy = spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); + + const apiInstance = alfrescoApiService.getInstance(); + spyOn(apiInstance, 'on').and.callFake((eventName: string | symbol, listener: (...args: unknown[]) => void) => { + if (eventName === 'error') { + apiErrorListener = listener; + } + + return apiInstance; + }); + + service.init(); + }; + + beforeEach(() => { + appConfigService = TestBed.inject(AppConfigService); + router = TestBed.inject(Router); + matDialog = TestBed.inject(MatDialog); + alfrescoApiService = TestBed.inject(AlfrescoApiService); + }); + + it('should navigate to login on 401 for non-public url when user is logged out', () => { + setupUnauthorizedErrorListener('/private/page'); + + apiErrorListener({ status: 401, response: { req: { url: '/api/private' } } }); + + expect(closeAllSpy).toHaveBeenCalled(); + expect(navigateSpy).toHaveBeenCalledWith(['/login'], { + queryParams: { redirectUrl: '/private/page' } + }); + }); + + it('should not navigate to login on 401 for public url when user is logged out', () => { + setupUnauthorizedErrorListener('/public/home'); + + apiErrorListener({ status: 401, response: { req: { url: '/api/public' } } }); + + expect(closeAllSpy).not.toHaveBeenCalled(); + expect(navigateSpy).not.toHaveBeenCalled(); + }); + }); + it('should load user profile on login', async () => { const person: any = { id: 'person' }; diff --git a/projects/aca-shared/src/lib/services/app.service.ts b/projects/aca-shared/src/lib/services/app.service.ts index 8bb0f244b..b129c06df 100644 --- a/projects/aca-shared/src/lib/services/app.service.ts +++ b/projects/aca-shared/src/lib/services/app.service.ts @@ -52,6 +52,7 @@ import { ShellAppService } from '@alfresco/adf-core/shell'; import { AppSettingsService } from './app-settings.service'; import { UserProfileService } from './user-profile.service'; import { MatDialog } from '@angular/material/dialog'; +import { minimatch } from 'minimatch'; @Injectable({ providedIn: 'root' @@ -133,7 +134,10 @@ export class AppService implements ShellAppService { init(): void { this.alfrescoApiService.getInstance().on('error', (error: { status: number; response: any }) => { if (error.status === 401 && !this.alfrescoApiService.isExcludedErrorListener(error?.response?.req?.url)) { - if (!this.authenticationService.isLoggedIn()) { + const publicUrls: string[] = this.config.get('oauth2.publicUrls', []); + const isPublicUrl = publicUrls.some((pattern) => minimatch(this.router.url, pattern)); + + if (!this.authenticationService.isLoggedIn() && !isPublicUrl) { this.matDialog.closeAll(); let redirectUrl = this.activatedRoute.snapshot.queryParams['redirectUrl'];