diff --git a/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts b/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts index ad270663d4..053bf0a7f6 100644 --- a/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts +++ b/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts @@ -8,12 +8,15 @@ const fakeUser = { const gnBaseUrl = 'http://localhost:8080/geonetwork/srv/eng/' -describe('dashboard', () => { +describe('dashboard (authenticated)', () => { + beforeEach(() => { + cy.login('admin', 'admin', false) + }) + let pageOne describe('avatar', () => { describe('display avatar for user without gravatar hash', () => { it('should display placeholder url', () => { - cy.login('admin', 'admin', false) cy.visit(`${gnBaseUrl}admin.console#/organization`) cy.get('#gn-btn-user-add').click() cy.get('#username').type(fakeUser.username) @@ -35,7 +38,6 @@ describe('dashboard', () => { .should('eq', 'https://www.gravatar.com/avatar/?d=mp') }) it('should display monsterid', () => { - cy.login('admin', 'admin', false) cy.visit(`${gnBaseUrl}admin.console#/settings`) cy.get('[id="system/users/identicon"]').type( '{selectAll}gravatar:monsterid' @@ -160,7 +162,6 @@ describe('dashboard', () => { }) describe('columns', () => { beforeEach(() => { - cy.login('admin', 'admin', false) cy.visit('/catalog/search') }) it('should display the right info for unpublished records', () => { @@ -216,7 +217,6 @@ describe('dashboard', () => { describe('navigation', () => { beforeEach(() => { - cy.login('admin', 'admin', false) cy.visit('/catalog/search') }) describe('all records', () => { @@ -277,7 +277,6 @@ describe('dashboard', () => { } describe('allRecords search input', () => { beforeEach(() => { - cy.login('admin', 'admin', false) cy.visit('/catalog/search') }) it('should filter the dashboard based on the search input', () => { @@ -315,3 +314,12 @@ describe('dashboard', () => { }) }) }) + +describe('when the user is not logged in', () => { + beforeEach(() => { + cy.visit('/catalog/search') + }) + it('redirects to the login page', () => { + cy.url().should('include', '/catalog.signin?redirect=') + }) +}) diff --git a/apps/metadata-editor/src/app/app.module.ts b/apps/metadata-editor/src/app/app.module.ts index db79237697..c098de62f3 100644 --- a/apps/metadata-editor/src/app/app.module.ts +++ b/apps/metadata-editor/src/app/app.module.ts @@ -24,7 +24,11 @@ import { provideAnimations } from '@angular/platform-browser/animations' import { extModules } from './build-specifics' import { DashboardPageComponent } from './dashboard/dashboard-page.component' import { EditorRouterService } from './router.service' -import { provideGn4, provideRepositoryUrl } from '@geonetwork-ui/api/repository' +import { + LOGIN_URL, + provideGn4, + provideRepositoryUrl, +} from '@geonetwork-ui/api/repository' import { FeatureEditorModule } from '@geonetwork-ui/feature/editor' @NgModule({ @@ -62,6 +66,10 @@ import { FeatureEditorModule } from '@geonetwork-ui/feature/editor' importProvidersFrom(EffectsModule.forRoot()), provideGn4(), provideAnimations(), + { + provide: LOGIN_URL, + useFactory: () => getGlobalConfig().LOGIN_URL, + }, ], bootstrap: [AppComponent], }) diff --git a/apps/metadata-editor/src/app/app.routes.ts b/apps/metadata-editor/src/app/app.routes.ts index f2eac576b8..a4096ae9af 100644 --- a/apps/metadata-editor/src/app/app.routes.ts +++ b/apps/metadata-editor/src/app/app.routes.ts @@ -1,6 +1,5 @@ import { Route } from '@angular/router' import { DashboardPageComponent } from './dashboard/dashboard-page.component' -import { SignInPageComponent } from './sign-in/sign-in-page.component' import { EditPageComponent } from './edit/edit-page.component' import { EditRecordResolver } from './edit-record.resolver' import { MyDraftComponent } from './records/my-draft/my-draft.component' @@ -10,100 +9,110 @@ import { NewRecordResolver } from './new-record.resolver' import { DuplicateRecordResolver } from './duplicate-record.resolver' import { AllRecordsComponent } from './records/all-records/all-records.component' import { MyRecordsStateWrapperComponent } from './records/my-records/my-records-state-wrapper.component' +import { AuthGuardService } from './auth-guard.service' export const appRoutes: Route[] = [ - { path: '', redirectTo: 'catalog/search', pathMatch: 'prefix' }, { - path: 'catalog', - component: DashboardPageComponent, - outlet: 'primary', + path: '', + canActivate: [AuthGuardService], children: [ { path: '', - redirectTo: 'search', + redirectTo: 'catalog/search', pathMatch: 'prefix', }, { - path: 'discussion', - component: AllRecordsComponent, - pathMatch: 'prefix', + path: 'catalog', + component: DashboardPageComponent, + outlet: 'primary', + children: [ + { + path: '', + redirectTo: 'search', + pathMatch: 'prefix', + }, + { + path: 'discussion', + component: AllRecordsComponent, + pathMatch: 'prefix', + }, + { + path: 'calendar', + component: AllRecordsComponent, + pathMatch: 'prefix', + }, + { + path: 'contacts', + component: AllRecordsComponent, + pathMatch: 'prefix', + }, + { + path: 'thesaurus', + component: AllRecordsComponent, + pathMatch: 'prefix', + }, + { + path: 'search', + title: 'Search Records', + component: AllRecordsComponent, + pathMatch: 'prefix', + }, + ], }, { - path: 'calendar', - component: AllRecordsComponent, - pathMatch: 'prefix', + path: 'my-space', + component: DashboardPageComponent, + outlet: 'primary', + title: 'My space', + children: [ + { + path: 'my-records', + title: 'My Records', + component: MyRecordsStateWrapperComponent, + pathMatch: 'prefix', + }, + { + path: 'my-draft', + title: 'My Draft', + component: MyDraftComponent, + pathMatch: 'prefix', + }, + { + path: 'templates', + title: 'Templates', + component: TemplatesComponent, + pathMatch: 'prefix', + }, + ], }, { - path: 'contacts', - component: AllRecordsComponent, - pathMatch: 'prefix', + path: 'users', + component: DashboardPageComponent, + outlet: 'primary', + title: 'Users', + children: [ + { + path: 'my-org', + component: MyOrgUsersComponent, + pathMatch: 'prefix', + }, + ], }, { - path: 'thesaurus', - component: AllRecordsComponent, - pathMatch: 'prefix', + path: 'create', + component: EditPageComponent, + resolve: { record: NewRecordResolver }, }, { - path: 'search', - title: 'Search Records', - component: AllRecordsComponent, - pathMatch: 'prefix', + path: 'duplicate/:uuid', + component: EditPageComponent, + resolve: { record: DuplicateRecordResolver }, }, - ], - }, - { - path: 'my-space', - component: DashboardPageComponent, - outlet: 'primary', - title: 'My space', - children: [ { - path: 'my-records', - title: 'My Records', - component: MyRecordsStateWrapperComponent, - pathMatch: 'prefix', - }, - { - path: 'my-draft', - title: 'My Draft', - component: MyDraftComponent, - pathMatch: 'prefix', - }, - { - path: 'templates', - title: 'Templates', - component: TemplatesComponent, - pathMatch: 'prefix', - }, - ], - }, - { - path: 'users', - component: DashboardPageComponent, - outlet: 'primary', - title: 'Users', - children: [ - { - path: 'my-org', - component: MyOrgUsersComponent, - pathMatch: 'prefix', + path: 'edit/:uuid', + component: EditPageComponent, + resolve: { record: EditRecordResolver }, }, ], }, - { path: 'sign-in', component: SignInPageComponent }, - { - path: 'create', - component: EditPageComponent, - resolve: { record: NewRecordResolver }, - }, - { - path: 'duplicate/:uuid', - component: EditPageComponent, - resolve: { record: DuplicateRecordResolver }, - }, - { - path: 'edit/:uuid', - component: EditPageComponent, - resolve: { record: EditRecordResolver }, - }, ] diff --git a/apps/metadata-editor/src/app/auth-guard.service.spec.ts b/apps/metadata-editor/src/app/auth-guard.service.spec.ts new file mode 100644 index 0000000000..8966947d11 --- /dev/null +++ b/apps/metadata-editor/src/app/auth-guard.service.spec.ts @@ -0,0 +1,44 @@ +import { AuthGuardService } from './auth-guard.service' +import { MockBuilder, MockProvider } from 'ng-mocks' +import { AuthService } from '@geonetwork-ui/api/repository' +import { TestBed } from '@angular/core/testing' +import { of } from 'rxjs' +import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' + +Object.defineProperty(window, 'location', { + value: new URL('http://localhost'), +}) + +describe('AuthGuardService', () => { + let service: AuthGuardService + beforeEach(() => { + return MockBuilder(AuthGuardService) + }) + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + MockProvider(AuthService, { + loginUrl: 'http://login.com/bla?', + }), + MockProvider(PlatformServiceInterface, { + isAnonymous: () => of(true), + }), + ], + }) + window.location.href = 'http://original.path' + service = TestBed.inject(AuthGuardService) + }) + + it('returns true if the user is logged in', async () => { + jest + .spyOn(TestBed.inject(PlatformServiceInterface), 'isAnonymous') + .mockReturnValue(of(false)) + expect(await service.canActivate()).toBe(true) + expect(window.location.href).toBe('http://original.path/') + }) + it('redirects the user to the login page if the user is not logged in', async () => { + expect(await service.canActivate()).toBe(false) + expect(window.location.href).toBe('http://login.com/bla?') + }) +}) diff --git a/apps/metadata-editor/src/app/auth-guard.service.ts b/apps/metadata-editor/src/app/auth-guard.service.ts new file mode 100644 index 0000000000..fa7087f610 --- /dev/null +++ b/apps/metadata-editor/src/app/auth-guard.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core' +import { AuthService } from '@geonetwork-ui/api/repository' +import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' +import { firstValueFrom } from 'rxjs' + +@Injectable({ providedIn: 'root' }) +export class AuthGuardService { + constructor( + private platformService: PlatformServiceInterface, + private authService: AuthService + ) {} + + // this will redirect the user to the authentication form if required + async canActivate(): Promise { + const notLoggedIn = await firstValueFrom(this.platformService.isAnonymous()) + if (notLoggedIn) { + window.location.href = this.authService.loginUrl + return false + } + return true + } +} diff --git a/apps/metadata-editor/src/app/sign-in/sign-in-page.component.css b/apps/metadata-editor/src/app/sign-in/sign-in-page.component.css deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/metadata-editor/src/app/sign-in/sign-in-page.component.html b/apps/metadata-editor/src/app/sign-in/sign-in-page.component.html deleted file mode 100644 index 5b2382f971..0000000000 --- a/apps/metadata-editor/src/app/sign-in/sign-in-page.component.html +++ /dev/null @@ -1 +0,0 @@ -

sign-in-page works!

diff --git a/apps/metadata-editor/src/app/sign-in/sign-in-page.component.spec.ts b/apps/metadata-editor/src/app/sign-in/sign-in-page.component.spec.ts deleted file mode 100644 index 3dbbabbc30..0000000000 --- a/apps/metadata-editor/src/app/sign-in/sign-in-page.component.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing' - -import { SignInPageComponent } from './sign-in-page.component' - -describe('SignInPageComponent', () => { - let component: SignInPageComponent - let fixture: ComponentFixture - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [SignInPageComponent], - }).compileComponents() - - fixture = TestBed.createComponent(SignInPageComponent) - component = fixture.componentInstance - fixture.detectChanges() - }) - - it('should create', () => { - expect(component).toBeTruthy() - }) -}) diff --git a/apps/metadata-editor/src/app/sign-in/sign-in-page.component.ts b/apps/metadata-editor/src/app/sign-in/sign-in-page.component.ts deleted file mode 100644 index 209e08e0f0..0000000000 --- a/apps/metadata-editor/src/app/sign-in/sign-in-page.component.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Component } from '@angular/core' - -@Component({ - selector: 'md-editor-sign-in', - templateUrl: './sign-in-page.component.html', - styleUrls: ['./sign-in-page.component.css'], - standalone: true, -}) -export class SignInPageComponent {} diff --git a/docs/guide/faq.md b/docs/guide/faq.md index 2f3ee9bb1d..0bd1e3b2b4 100644 --- a/docs/guide/faq.md +++ b/docs/guide/faq.md @@ -2,8 +2,13 @@ outline: deep --- -# FAQ +# Frequently Asked Questions -## Chapter 1 +[[toc]] -## Chapter 2 +### _I have deployed Application Name alongside GeoNetwork, but somehow all the HTTP requests going to GeoNetwork end up failing with a 403 error, why?_ + +There are several possible reasons for this: + +- The attempted requests necessitate authentication (e.g. creating a record) but the session of the current user has expired; in this case, the user should log in again. +- The XSRF protection mechanism is not working correctly; this can be complicated to set up, please refer to [this part of the documentation](./deploy.md#authentication) to know more.