diff --git a/projects/hslayers/assets/locales/cs.json b/projects/hslayers/assets/locales/cs.json index c7db79892f..85aa6545c8 100644 --- a/projects/hslayers/assets/locales/cs.json +++ b/projects/hslayers/assets/locales/cs.json @@ -1,4 +1,12 @@ { + "AUTH": { + "signingIn": "Přihlašování ...", + "logoutSuccessful": "Byli jste velmi odhlášeni!", + "loginSuccessful": "Úspěšně přihlášen", + "authenticationFailed": "Chyba ověření", + "userInfoFailed": "Chyba při načítání uživatelských údajů", + "loginFailed": "Přihlášení se nezdařilo" + }, "ADDDATA": { "CATALOGUE": { "DESC": { @@ -322,7 +330,9 @@ "base": "Podkladová mapa", "thematic": "Tematická mapa", "contains": "obsahuje", - "property": "Atribut" + "property": "Atribut", + "error": "Chyba", + "success": "Úspěch" }, "COMPOSITIONS": { "addByAddress": "Přidat kompozici z adresy", diff --git a/projects/hslayers/assets/locales/en.json b/projects/hslayers/assets/locales/en.json index 1c9e749e10..63373a3b64 100644 --- a/projects/hslayers/assets/locales/en.json +++ b/projects/hslayers/assets/locales/en.json @@ -1,4 +1,14 @@ { + "AUTH": { + "signingIn": "Signing in...", + "logoutSuccessful": "You have been sucesfully logged out!", + "loginSuccessful": "Successfully logged in", + "authenticationFailed": "Authentication failed", + "userInfoFailed": "Failed to retrieve user information", + "loginFailed": "Login failed", + "Login": "Log in", + "Logout": "Log out" + }, "ADDDATA": { "CATALOGUE": { "DESC": { @@ -219,8 +229,6 @@ "lineColor": "Line color", "lineWidth": "Line width", "loading": "Loading", - "Login": "Log in", - "Logout": "Log out", "mapExtentFilterMissing": "Please enable Filter by map extent checkbox to be able to sort data by Bounding box", "menu": "Menu", "metadata": "Metadata", @@ -322,7 +330,9 @@ "base": "Basemap", "thematic": "Thematic map", "contains": "contains", - "property": "Property" + "property": "Property", + "error": "Error", + "success": "Success" }, "COMPOSITIONS": { "addByAddress": "Add composition by address", diff --git a/projects/hslayers/assets/locales/sk.json b/projects/hslayers/assets/locales/sk.json index 0347705697..855a85ae6f 100644 --- a/projects/hslayers/assets/locales/sk.json +++ b/projects/hslayers/assets/locales/sk.json @@ -1,4 +1,12 @@ { + "AUTH": { + "signingIn": "Prihlasovanie ...", + "logoutSuccessful": "Boli ste úspešne odhlásený!", + "loginSuccessful": "Úspešne prihlásený", + "authenticationFailed": "Chyba prihlásenia", + "userInfoFailed": "Chyba pri načítaní údajov o užívateľovi", + "loginFailed": "Prihlasovanie nebolo úspešné" + }, "ADDDATA": { "CATALOGUE": { "DESC": { @@ -322,7 +330,9 @@ "base": "Podkladová mapa", "thematic": "Tematická mapa", "contains": "obsahuje", - "property": "Atribút" + "property": "Atribút", + "error": "Chyba", + "success": "Úspech" }, "COMPOSITIONS": { "addByAddress": "Pridať kompozíciu z adresy", diff --git a/projects/hslayers/common/dialog-set-permissions/set-permissions.component.ts b/projects/hslayers/common/dialog-set-permissions/set-permissions.component.ts index 9aa2fb56ab..26ea3b61dd 100644 --- a/projects/hslayers/common/dialog-set-permissions/set-permissions.component.ts +++ b/projects/hslayers/common/dialog-set-permissions/set-permissions.component.ts @@ -57,7 +57,7 @@ export class HsSetPermissionsDialogComponent ngOnInit(): void { // Can set permission for Layman endpoint only - this.endpoint = this.hsCommonLaymanService.layman; + this.endpoint = this.hsCommonLaymanService.layman(); if (!this.data.selectedRecord?.access_rights || !this.endpoint) { this.close(); return; @@ -70,7 +70,7 @@ export class HsSetPermissionsDialogComponent * so it can be used for Layman access rights component */ parseCurrentPermissions(): void { - const currentUser = this.endpoint.user; + const currentUser = this.hsCommonLaymanService.user(); let read: string[] = this.data.selectedRecord.access_rights.read; let write: string[] = this.data.selectedRecord.access_rights.write; if (read.includes('EVERYONE')) { @@ -105,7 +105,7 @@ export class HsSetPermissionsDialogComponent const layerDesc: UpsertLayerObject = { name: this.data.selectedRecord.name, title: this.data.selectedRecord.title, - workspace: this.endpoint.user, + workspace: this.hsCommonLaymanService.user(), access_rights: this.access_rights, }; response = await this.hsLaymanService.makeUpsertLayerRequest( diff --git a/projects/hslayers/common/layman/access-rights/layman-access-rights.component.html b/projects/hslayers/common/layman/access-rights/layman-access-rights.component.html index f7ea90e03f..102ea2edd0 100644 --- a/projects/hslayers/common/layman/access-rights/layman-access-rights.component.html +++ b/projects/hslayers/common/layman/access-rights/layman-access-rights.component.html @@ -56,11 +56,11 @@
@for (user of allUsers | filter: userFilter; track user) {
+ [class.bg-primary-subtle]="user.username === hsCommonLaymanService.user()"> {{user.hslDisplayName}} @for (type of rightsOptions; track type) { } diff --git a/projects/hslayers/common/layman/access-rights/layman-access-rights.component.ts b/projects/hslayers/common/layman/access-rights/layman-access-rights.component.ts index 15dc024137..497fde3576 100644 --- a/projects/hslayers/common/layman/access-rights/layman-access-rights.component.ts +++ b/projects/hslayers/common/layman/access-rights/layman-access-rights.component.ts @@ -68,12 +68,12 @@ export class HsCommonLaymanAccessRightsComponent implements OnInit { userSearch: string; endpoint: HsEndpoint; constructor( - private hsCommonLaymanService: HsCommonLaymanService, + public hsCommonLaymanService: HsCommonLaymanService, private $http: HttpClient, private hsLog: HsLogService, ) {} async ngOnInit(): Promise { - this.endpoint = this.hsCommonLaymanService.layman; + this.endpoint = this.hsCommonLaymanService.layman(); this.defaultAccessRights = JSON.parse(JSON.stringify(this.access_rights)); this.setUpAccessRights(); } @@ -113,7 +113,7 @@ export class HsCommonLaymanAccessRightsComponent implements OnInit { this.allUsers.length / 2 >= this.allUsers.filter((u) => u[type]).length; this.allUsers.forEach((user) => { - const isCurrentUser = user.name === this.endpoint.user; + const isCurrentUser = user.name === this.hsCommonLaymanService.user(); //Value for current user won't be changed user[type] = isCurrentUser ? user[type] : value; //In case write permission is being added make sure read is granted as well @@ -199,7 +199,7 @@ export class HsCommonLaymanAccessRightsComponent implements OnInit { : [ ...this.allRoles, //Add current user as he has got to retain both read and write rights - {name: this.endpoint.user, read: true, write: true}, + {name: this.hsCommonLaymanService.user(), read: true, write: true}, ]; this.access_rights[`access_rights.${type}`] = source .reduce((acc, curr) => { @@ -291,7 +291,7 @@ export class HsCommonLaymanAccessRightsComponent implements OnInit { * Get user roles */ async getRoles(access_rights?: AccessRightsModel): Promise { - if (this.endpoint?.authenticated) { + if (this.hsCommonLaymanService.isAuthenticated()) { const url = `${this.endpoint.url}/rest/roles`; access_rights ??= this.access_rights; @@ -390,7 +390,7 @@ export class HsCommonLaymanAccessRightsComponent implements OnInit { * Get all registered users from Layman's endpoint service */ async getAllUsers(access_rights?: AccessRightsModel): Promise { - if (this.endpoint?.authenticated) { + if (this.hsCommonLaymanService.isAuthenticated()) { access_rights ??= this.access_rights; const read = access_rights[AccessRights.READ].split(','); const write = access_rights[AccessRights.WRITE].split(','); @@ -403,7 +403,8 @@ export class HsCommonLaymanAccessRightsComponent implements OnInit { ), map((res: LaymanUser[]) => { return res.map((user) => { - const isCurrentUser = user.username === this.endpoint.user; + const isCurrentUser = + user.username === this.hsCommonLaymanService.user(); const laymanUser: LaymanUser = { ...user, name: user.username, diff --git a/projects/hslayers/common/layman/layman-current-user.component.html b/projects/hslayers/common/layman/layman-current-user.component.html index 02dc1f95e4..d5cf73668c 100644 --- a/projects/hslayers/common/layman/layman-current-user.component.html +++ b/projects/hslayers/common/layman/layman-current-user.component.html @@ -1,23 +1,53 @@ -@if ((inAppLogin | async) && endpoint) { -
- @if (!isAuthenticated() && sameDomain()) { - - - {{'COMPOSITIONS.loginToCatalogue' | translateHs }} - - } - @if (!isAuthenticated() && !sameDomain()) { - - - {{'COMPOSITIONS.loginToCatalogue' | translateHs }} - - } - @if (isAuthenticated()) { - {{'COMMON.Logout' - | translateHs }} - {{endpoint.user}} - } -
-} \ No newline at end of file +@if ((inAppLogin | async) && hsCommonLaymanService.layman()) { +
+ @if (!hsCommonLaymanService.isAuthenticated()) { + @if (hsCommonLaymanService.isAuthenticating()) { + + } @else { + + } + } @else { +
+ +
+
+
+
+ +
+
+
{{hsCommonLaymanService.user()}}
+ Logged in +
+
+
+
+ +
+
+
+ } +
+} diff --git a/projects/hslayers/common/layman/layman-current-user.component.ts b/projects/hslayers/common/layman/layman-current-user.component.ts index 37c10785eb..fd6d5c694b 100644 --- a/projects/hslayers/common/layman/layman-current-user.component.ts +++ b/projects/hslayers/common/layman/layman-current-user.component.ts @@ -1,103 +1,108 @@ -import {Component, Input, OnInit} from '@angular/core'; -import {Observable, map} from 'rxjs'; +import {Component, computed, inject, signal} from '@angular/core'; +import {map} from 'rxjs'; import {HsCommonLaymanService} from './layman.service'; import {HsDialogContainerService} from 'hslayers-ng/common/dialogs'; -import {HsEndpoint} from 'hslayers-ng/types'; -import {HsLaymanLoginComponent} from './layman-login.component'; +import {HsConfig} from 'hslayers-ng/config'; +import {HsToastService} from 'hslayers-ng/common/toast'; @Component({ selector: 'hs-layman-current-user', templateUrl: './layman-current-user.component.html', standalone: false, -}) -export class HsLaymanCurrentUserComponent implements OnInit { - @Input() endpoint?: HsEndpoint; - monitorTries = 0; - DEFAULT_TIMER_INTERVAL = 2000; - MAX_MONITOR_TRIES = 100; - timerInterval = this.DEFAULT_TIMER_INTERVAL; - getCurrentUserTimer; + styles: ` + .user-auth-container { + .user-dropdown { + .user-avatar { + width: 1.75rem; + height: 1.75rem; + background-color: rgba(255, 255, 255, 0.2); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.875rem; + } - /** - * Controls availability of "Log in" button in HSL components. - * Not available for Wagtail endpoints as login is handled via separate hub proxy - */ - inAppLogin: Observable; - constructor( - public HsCommonLaymanService: HsCommonLaymanService, - public HsDialogContainerService: HsDialogContainerService, - ) {} + .user-dropdown-menu { + min-width: 15rem; + border-radius: 0.5rem; + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); - ngOnInit(): void { - this.inAppLogin = this.HsCommonLaymanService.layman$.pipe( - map((layman) => { - if (layman) { - //Assign received layman endpoint to local variable - this.endpoint = layman; - return this.endpoint?.type === 'layman'; - } - }), - ); - } + .user-header { + border-radius: 0.5rem 0.5rem 0 0; - isAuthenticated() { - return this.HsCommonLaymanService.isAuthenticated(); - } + .user-avatar-large { + width: 3rem; + height: 3rem; + background-color: #e9ecef; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.25rem; + } + } - logout(): void { - this.monitorUser(); - this.HsCommonLaymanService.logout(this.endpoint); - } + button.dropdown-item { + border-radius: 0.25rem; + margin: 0.125rem 0; - sameDomain() { - const endpointUrl = new URL(this.endpoint.url); - return ( - location.protocol == endpointUrl.protocol && - location.host == endpointUrl.host - ); - } + &:hover { + background-color: #f8f9fa; + } - authUrl() { - return this.endpoint.url + '/login'; - } + &.text-danger:hover { + background-color: #fff5f5; + } + } + } + } + } + `, +}) +export class HsLaymanCurrentUserComponent { + hsCommonLaymanService = inject(HsCommonLaymanService); + HsDialogContainerService = inject(HsDialogContainerService); + hsConfig = inject(HsConfig); + hsToastService = inject(HsToastService); + + dropdownOpen = signal(false); + + sameDomain = computed(() => { + const laymanEndpoint = this.hsCommonLaymanService.layman(); + if (!laymanEndpoint) { + return false; + } + return laymanEndpoint.url.includes(window.location.origin); + }); + + inAppLogin = this.hsCommonLaymanService.layman$.pipe( + map((layman) => layman?.type === 'layman'), + ); /** - * Periodically poll layman client endpoint for auth change. - * This is used for hiding login iframe and toggling state for login buttons, - * which is done in separate modules by subscribing to HsCommonLaymanService.authChange + * Log out the current user */ - monitorUser(): void { - if (this.getCurrentUserTimer) { - clearTimeout(this.getCurrentUserTimer); - } - this.monitorTries = 0; - this.timerInterval = this.DEFAULT_TIMER_INTERVAL; - const poll = () => { - this.HsCommonLaymanService.detectAuthChange(this.endpoint).then( - (somethingChanged) => { - if (somethingChanged && this.getCurrentUserTimer) { - clearTimeout(this.getCurrentUserTimer); - this.monitorTries = this.MAX_MONITOR_TRIES; - } - }, - ); - this.monitorTries++; - if (this.monitorTries > this.MAX_MONITOR_TRIES) { - clearTimeout(this.getCurrentUserTimer); - } - this.getCurrentUserTimer = setTimeout(poll, this.timerInterval); - }; - this.getCurrentUserTimer = setTimeout(poll, this.timerInterval); + logout(): void { + this.hsCommonLaymanService.logout(); } - login(): void { - this.monitorUser(); + /** + * Open login dialog or redirect to login page + */ + async login(): Promise { + this.hsCommonLaymanService.login$.next(); + const authUrl = this.hsCommonLaymanService.layman()?.url + '/login'; if (!this.sameDomain()) { + window.open(authUrl, 'AuthWindow'); return; } + + // For same-domain logins, open the dialog + const {HsLaymanLoginComponent} = await import('./layman-login.component'); this.HsDialogContainerService.create(HsLaymanLoginComponent, { - url: this.authUrl(), + url: authUrl, }); } } diff --git a/projects/hslayers/common/layman/layman-login.component.html b/projects/hslayers/common/layman/layman-login.component.html index 191ab7654c..1de2bb6b3d 100644 --- a/projects/hslayers/common/layman/layman-login.component.html +++ b/projects/hslayers/common/layman/layman-login.component.html @@ -3,7 +3,7 @@
-
\ No newline at end of file + diff --git a/projects/hslayers/common/layman/layman-login.component.ts b/projects/hslayers/common/layman/layman-login.component.ts index 29a9bfd4c2..4affef4d44 100644 --- a/projects/hslayers/common/layman/layman-login.component.ts +++ b/projects/hslayers/common/layman/layman-login.component.ts @@ -1,41 +1,36 @@ -import {Component, Input, OnDestroy, OnInit, ViewRef} from '@angular/core'; +import {Component, Input, OnInit, ViewRef} from '@angular/core'; import {DomSanitizer, SafeResourceUrl} from '@angular/platform-browser'; -import {Subscription} from 'rxjs'; - import {HsCommonLaymanService} from './layman.service'; import { HsDialogComponent, HsDialogContainerService, } from 'hslayers-ng/common/dialogs'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; @Component({ selector: 'hs-layman-login', templateUrl: './layman-login.component.html', standalone: false, }) -export class HsLaymanLoginComponent - implements HsDialogComponent, OnDestroy, OnInit -{ +export class HsLaymanLoginComponent implements HsDialogComponent, OnInit { @Input() data: { url: string; }; viewRef: ViewRef; url: SafeResourceUrl; - authChangeSubscription: Subscription; constructor( public HsCommonLaymanService: HsCommonLaymanService, public HsDialogContainerService: HsDialogContainerService, private sanitizer: DomSanitizer, ) { - this.authChangeSubscription = - this.HsCommonLaymanService.authChange.subscribe(() => { + this.HsCommonLaymanService.layman$ + .pipe(takeUntilDestroyed()) + .subscribe(() => { this.close(); }); } - ngOnDestroy(): void { - this.authChangeSubscription.unsubscribe(); - } + ngOnInit(): void { this.url = this.sanitizer.bypassSecurityTrustResourceUrl(this.data.url); } diff --git a/projects/hslayers/common/layman/layman.module.ts b/projects/hslayers/common/layman/layman.module.ts index f30623351a..74f1fa48f9 100644 --- a/projects/hslayers/common/layman/layman.module.ts +++ b/projects/hslayers/common/layman/layman.module.ts @@ -7,6 +7,7 @@ import {HsCommonLaymanAccessRightsComponent} from './access-rights/layman-access import {HsLaymanCurrentUserComponent} from './layman-current-user.component'; import {HsLaymanLoginComponent} from './layman-login.component'; import {TranslateCustomPipe} from 'hslayers-ng/services/language'; +import {NgbDropdownModule} from '@ng-bootstrap/ng-bootstrap'; @NgModule({ schemas: [CUSTOM_ELEMENTS_SCHEMA], @@ -15,7 +16,13 @@ import {TranslateCustomPipe} from 'hslayers-ng/services/language'; HsLaymanCurrentUserComponent, HsCommonLaymanAccessRightsComponent, ], - imports: [CommonModule, TranslateCustomPipe, FilterPipe, FormsModule], + imports: [ + CommonModule, + TranslateCustomPipe, + FilterPipe, + FormsModule, + NgbDropdownModule, + ], exports: [ HsLaymanLoginComponent, HsLaymanCurrentUserComponent, diff --git a/projects/hslayers/common/layman/layman.service.ts b/projects/hslayers/common/layman/layman.service.ts index ab3175e597..2bd1606411 100644 --- a/projects/hslayers/common/layman/layman.service.ts +++ b/projects/hslayers/common/layman/layman.service.ts @@ -1,28 +1,218 @@ import {HttpClient, HttpHeaders} from '@angular/common/http'; -import {Injectable} from '@angular/core'; +import {computed, inject, Injectable} from '@angular/core'; +import {toObservable, toSignal} from '@angular/core/rxjs-interop'; -import {BehaviorSubject, Subject, lastValueFrom, map} from 'rxjs'; +import { + Observable, + Subject, + catchError, + distinctUntilChanged, + lastValueFrom, + map, + merge, + of, + retry, + shareReplay, + startWith, + switchMap, + tap, + timer, +} from 'rxjs'; import {CurrentUserResponse} from './types/current-user-response.type'; -import {HsEndpoint} from 'hslayers-ng/types'; +import {AboutLayman, HsEndpoint} from 'hslayers-ng/types'; import {HsLanguageService} from 'hslayers-ng/services/language'; import {HsLogService} from 'hslayers-ng/services/log'; import {HsToastService} from 'hslayers-ng/common/toast'; +import {HsCommonEndpointsService} from 'hslayers-ng/services/endpoints'; +import {HsUtilsService} from 'hslayers-ng/services/utils'; import {parseBase64Style} from './parse-base64-style'; +import {AuthAction, AuthState} from 'hslayers-ng/types/authentication'; @Injectable({ providedIn: 'root', }) export class HsCommonLaymanService { - authChange: Subject = new Subject(); + endpointService = inject(HsCommonEndpointsService); + http = inject(HttpClient); + hsUtilsService = inject(HsUtilsService); - layman$: BehaviorSubject = new BehaviorSubject( - undefined, + private readonly MAX_USER_POLL_ATTEMPTS = 7; + private readonly USER_POLL_DELAY = 2500; + + readonly layman$: Observable = toObservable( + this.endpointService.endpoints, + ).pipe( + map((endpoints) => endpoints.find((ep) => ep.type.includes('layman'))), + switchMap((endpoint) => { + if (!endpoint) { + return of(undefined); + } + // Query version information when endpoint is available + return this.http + .get(endpoint.url + '/rest/about/version') + .pipe( + map((version) => { + if (version) { + return { + ...endpoint, + version: version.about.applications.layman.version, + }; + } + return endpoint; + }), + catchError((error) => { + this.hsToastService.createToastPopupMessage( + 'COMMON.error', + 'Could not get layman version', + ); + return of(endpoint); + }), + ); + }), + // Register the endpoint with HsUtilsService to avoid circular dependency + tap((endpoint) => { + if (endpoint) { + this.hsUtilsService.registerLaymanEndpoints(endpoint.url); + } + }), + shareReplay(1), ); - public get layman(): HsEndpoint { - return this.layman$.getValue(); - } + readonly layman = toSignal(this.layman$); + + // Action streams + login$ = new Subject(); + logout$ = new Subject(); + + // Combined auth actions + authActions$ = merge( + this.login$.pipe(map(() => ({type: 'login'}))), + this.logout$.pipe(map(() => ({type: 'logout'}))), + ); + + authState$: Observable = this.layman$.pipe( + // Initial check for current user when endpoint is available + switchMap((endpoint) => { + if (!endpoint) { + return of(undefined); + } + /** + * Check for current user. In wagtail we might recive authentication right of the bat. + * The rest is controlled via login and logout triggers. + */ + return this.getCurrentUser(endpoint.url).pipe( + // Listen for auth actions after initial check + switchMap((currentUser) => { + return this.authActions$.pipe( + // Start with the initial state + startWith(null), + // For each auth action, update the state + switchMap((action) => { + if (!action) { + return of({ + user: currentUser.username, + authenticated: currentUser.authenticated, + }); // Initial state + } + + if (action.type === 'login') { + // On login poll for user + return this.pollForUser().pipe( + map((cu) => { + const authenticated = !!cu?.authenticated; + const username = cu?.username; + + // Show success toast if authentication was successful + if (authenticated && username) { + this.hsToastService.createToastPopupMessage( + 'COMMON.success', + 'AUTH.loginSuccessful', + { + type: 'success', + serviceCalledFrom: 'HsCommonLaymanService', + }, + ); + } + + return { + user: username, + authenticated, + }; + }), + catchError((error) => { + // Handle authentication errors + this.hsToastService.createToastPopupMessage( + 'COMMON.error', + 'AUTH.loginFailed', + { + type: 'danger', + serviceCalledFrom: 'HsCommonLaymanService', + details: [error.message || 'AUTH.loginFailed'], + }, + ); + + // Return endpoint with authentication failed + return of({ + user: undefined, + authenticated: false, + }); + }), + ); + } + if (action.type === 'logout') { + this.hsToastService.createToastPopupMessage( + 'AUTH.Logout', + 'AUTH.logoutSuccessful', + { + type: 'success', + }, + ); + // On logout action, clear user data + return of({ + ...endpoint, + user: undefined, + authenticated: false, + }); + } + }), + ); + }), + catchError((error) => { + // Handle errors from getCurrentUser + this.hsToastService.createToastPopupMessage( + 'COMMON.Error', + 'AUTH.userInfoFailed', + { + type: 'danger', + serviceCalledFrom: 'HsCommonLaymanService', + details: [error.message || 'AUTH.userInfoFailed'], + }, + ); + + return of(undefined); + }), + ); + }), + shareReplay(1), + ); + + authState = toSignal(this.authState$); + + // Always return a boolean value, defaulting to false when layman is undefined + isAuthenticated = computed(() => !!this.authState()?.authenticated); + user = computed(() => this.authState()?.user); + + // Create an observable for the authenticating state + isAuthenticating = toSignal( + merge( + // Set to true when login action is triggered + this.login$.pipe(map(() => true)), + + // Set to false when layman$ emits a new value (authentication completed) + this.layman$.pipe(map(() => false)), + ).pipe(startWith(false), distinctUntilChanged(), shareReplay(1)), + ); constructor( private $http: HttpClient, @@ -31,64 +221,83 @@ export class HsCommonLaymanService { private hsLog: HsLogService, ) {} - isAuthenticated() { - return this.layman?.authenticated; + /** + * Get current user from layman endpoint + * @param endpoint - Layman endpoint + */ + getCurrentUser(endpoint_url: HsEndpoint['url']) { + if (!endpoint_url) { + return of(undefined); + } + const url = `${endpoint_url}/rest/current-user`; + return this.$http.get(url, {withCredentials: true}); } /** - * Monitor if authorization state has changed and - * return true and broadcast authChange event if so . - * @param endpoint - Endpoint definition - usually Layman - * @returns Promise true if authorization state changed (user logged in or out) + * Log into existing account by opening a login endpoint in new window + * and polling for the results */ - async detectAuthChange(endpoint: HsEndpoint): Promise { - const url = `${endpoint.url}/rest/current-user`; - try { - const res: CurrentUserResponse = await lastValueFrom( - this.$http.get(url, {withCredentials: true}), - ); - - let somethingChanged = false; - if (res.code === 32) { - endpoint.authenticated = false; - endpoint.user = undefined; - } - if (res.username) { - if (endpoint.user != res.username) { - endpoint.user = res.username; - endpoint.authenticated = res.authenticated; - somethingChanged = true; - this.authChange.next(endpoint); - } - } else { - if (endpoint.user != undefined) { - somethingChanged = true; - } - endpoint.user = undefined; - } - return somethingChanged; - } catch (e) { - this.hsLog.warn(e); - return e; + pollForUser(): Observable { + const laymanEndpoint = this.layman(); + if (!laymanEndpoint) { + return of(undefined); } + + return timer(1000).pipe( + switchMap(() => + this.getCurrentUser(laymanEndpoint.url).pipe( + switchMap((response) => { + if (!response || !response.authenticated) { + // Handle case where authenticated is false + throw new Error('AUTH.notAuthenticated'); + } + return of(response); + }), + ), + ), + retry({ + count: this.MAX_USER_POLL_ATTEMPTS, + delay: this.USER_POLL_DELAY, + }), + catchError((error) => { + // Handle error after all retries are exhausted + console.error('Error retrieving profile:', error); + + // Show error toast after retries are exhausted + this.hsToastService.createToastPopupMessage( + 'COMMON.error', + 'AUTH.authenticationFailed', + { + type: 'danger', + serviceCalledFrom: 'HsCommonLaymanService', + details: [ + 'AUTH.authenticationFailed', + error.message || 'Unknown error occurred', + ], + }, + ); + + return of(null); + }), + ); } - async getCurrentUserIfNeeded(endpoint: HsEndpoint): Promise { - if (endpoint.type.includes('layman') && endpoint.user === undefined) { - await this.detectAuthChange(endpoint); + async logout(): Promise { + const laymanEndpoint = this.layman(); + if (!laymanEndpoint) { + return; } - } - async logout(endpoint): Promise { - const url = `${endpoint.url}/logout`; + const url = `${laymanEndpoint.url}/logout`; try { await lastValueFrom(this.$http.get(url, {withCredentials: true})); } catch (ex) { this.hsLog.warn(ex); } finally { - endpoint.user = undefined; - endpoint.authenticated = false; - this.authChange.next(endpoint); + /*** + * TODO: this might go first + */ + this.logout$.next(); } } @@ -107,7 +316,7 @@ export class HsCommonLaymanService { break; case 32: simplifiedResponse = 'Authentication failed. Login to the catalogue.'; - this.detectAuthChange(endpoint); + //this.detectAuthChange(endpoint); break; default: simplifiedResponse = responseBody.message; @@ -125,7 +334,11 @@ export class HsCommonLaymanService { simplifiedResponse, undefined, ), - {disableLocalization: true, serviceCalledFrom: 'HsCommonLaymanService'}, + { + disableLocalization: true, + serviceCalledFrom: 'HsCommonLaymanService', + type: 'danger', + }, ); } diff --git a/projects/hslayers/components/add-data/catalogue/catalogue-list-item/catalogue-list-item.component.ts b/projects/hslayers/components/add-data/catalogue/catalogue-list-item/catalogue-list-item.component.ts index c3502d4135..e3683beda5 100644 --- a/projects/hslayers/components/add-data/catalogue/catalogue-list-item/catalogue-list-item.component.ts +++ b/projects/hslayers/components/add-data/catalogue/catalogue-list-item/catalogue-list-item.component.ts @@ -15,6 +15,7 @@ import {HsDialogContainerService} from 'hslayers-ng/common/dialogs'; import {HsLogService} from 'hslayers-ng/services/log'; import {HsRemoveLayerDialogService} from 'hslayers-ng/common/remove-multiple'; import {HsSetPermissionsDialogComponent} from 'hslayers-ng/common/dialog-set-permissions'; +import {HsCommonLaymanService} from 'hslayers-ng/common/layman'; @Component({ selector: 'hs-catalogue-list-item', @@ -49,6 +50,7 @@ export class HsCatalogueListItemComponent implements OnInit { private hsLaymanBrowserService: HsLaymanBrowserService, private hsLog: HsLogService, private hsRemoveLayerDialogService: HsRemoveLayerDialogService, + private hsCommonLaymanService: HsCommonLaymanService, ) {} ngOnInit() { @@ -177,7 +179,7 @@ export class HsCatalogueListItemComponent implements OnInit { * @param layer - Metadata record of selected layer */ async showPermissions(layer: HsAddDataLayerDescriptor): Promise { - if (!this.layer.endpoint?.authenticated) { + if (!this.hsCommonLaymanService.isAuthenticated()) { return; } this.hsDialogContainerService.create(HsSetPermissionsDialogComponent, { diff --git a/projects/hslayers/components/add-data/catalogue/catalogue.component.html b/projects/hslayers/components/add-data/catalogue/catalogue.component.html index 6d1de84e07..fd4bdcde2b 100644 --- a/projects/hslayers/components/add-data/catalogue/catalogue.component.html +++ b/projects/hslayers/components/add-data/catalogue/catalogue.component.html @@ -1,4 +1,3 @@ -@if ({layman: hsCommonLaymanService.layman$ | async }; as ctx) {
@@ -9,7 +8,7 @@
- +
@@ -18,7 +17,7 @@ name="filterByExtent"> {{'COMPOSITIONS.filterByMap' | translateHs }}
- @if (ctx.layman?.authenticated) { + @if (hsCommonLaymanService.isAuthenticated()) {
@@ -138,4 +137,3 @@ }
-} diff --git a/projects/hslayers/components/add-data/common/new-layer-form/new-layer-form.component.html b/projects/hslayers/components/add-data/common/new-layer-form/new-layer-form.component.html index 4697bca117..713945dba3 100644 --- a/projects/hslayers/components/add-data/common/new-layer-form/new-layer-form.component.html +++ b/projects/hslayers/components/add-data/common/new-layer-form/new-layer-form.component.html @@ -18,7 +18,7 @@
diff --git a/projects/hslayers/components/add-data/file/file-base.component.ts b/projects/hslayers/components/add-data/file/file-base.component.ts index 3eddeb4532..85d88838ec 100644 --- a/projects/hslayers/components/add-data/file/file-base.component.ts +++ b/projects/hslayers/components/add-data/file/file-base.component.ts @@ -79,7 +79,6 @@ export class HsAddDataFileBaseComponent implements OnInit, AfterViewInit { }); this.setDataToDefault(); - this.hsAddDataCommonFileService.pickEndpoint(); } /** diff --git a/projects/hslayers/components/add-data/vector/vector-file/vector-file.component.ts b/projects/hslayers/components/add-data/vector/vector-file/vector-file.component.ts index 45c03d3445..5232b9defb 100644 --- a/projects/hslayers/components/add-data/vector/vector-file/vector-file.component.ts +++ b/projects/hslayers/components/add-data/vector/vector-file/vector-file.component.ts @@ -209,8 +209,7 @@ export class HsAddDataVectorFileComponent implements OnInit, AfterViewInit { this.data.saveAvailable = false; } else { this.data.saveAvailable = true; - this.data.saveToLayman = - this.hsCommonLaymanService.layman?.authenticated; + this.data.saveToLayman = this.hsCommonLaymanService.isAuthenticated(); if (this.data.saveToLayman) { this.data.loadAsType = DEFAULT_VECTOR_LOAD_TYPE; } @@ -221,7 +220,7 @@ export class HsAddDataVectorFileComponent implements OnInit, AfterViewInit { undefined && this.data.saveToLayman ) { - this.data.url = this.hsCommonLaymanService.layman?.url; + this.data.url = this.hsCommonLaymanService.layman()?.url; } this.data.showDetails = true; } else { diff --git a/projects/hslayers/components/compositions/compositions-catalogue.service.ts b/projects/hslayers/components/compositions/compositions-catalogue.service.ts index 11d524d30e..a027a2c87c 100644 --- a/projects/hslayers/components/compositions/compositions-catalogue.service.ts +++ b/projects/hslayers/components/compositions/compositions-catalogue.service.ts @@ -59,7 +59,7 @@ export class HsCompositionsCatalogueService { */ dataLoading: boolean; loadCompositionsQuery: any; - endpoints: HsEndpoint[]; + endpoints: HsEndpoint[] = this.hsCommonEndpointsService.endpoints(); extentChangeSuppressed = false; constructor( public hsMapService: HsMapService, @@ -72,9 +72,6 @@ export class HsCompositionsCatalogueService { public hsCommonLaymanService: HsCommonLaymanService, private _zone: NgZone, ) { - this.hsCommonEndpointsService.endpointsFilled.subscribe((endpoints) => { - this.endpoints = endpoints ?? []; - }); this.hsLayoutService.mainpanel$.subscribe((which) => { if ( this.hsLayoutService.mainpanel === 'compositions' || @@ -121,7 +118,7 @@ export class HsCompositionsCatalogueService { }); }); - this.hsCommonLaymanService.authChange.subscribe((endpoint) => { + this.hsCommonLaymanService.layman$.subscribe((endpoint) => { if ( this.hsLayoutService.mainpanel != 'compositions' && this.hsLayoutService.mainpanel != 'composition' @@ -358,10 +355,6 @@ export class HsCompositionsCatalogueService { this.data.type = TYPES[0].name; this.data.keywords.forEach((kw) => (kw.selected = false)); this.data.themes.forEach((th) => (th.selected = false)); - const laymanEndpoint = this.hsCommonLaymanService.layman; - if (laymanEndpoint) { - this.endpoints.push(laymanEndpoint); - } this.loadFilteredCompositions(); } } diff --git a/projects/hslayers/components/compositions/compositions-list-item.component.html b/projects/hslayers/components/compositions/compositions-list-item.component.html index 3824a91f0b..8b43344171 100644 --- a/projects/hslayers/components/compositions/compositions-list-item.component.html +++ b/projects/hslayers/components/compositions/compositions-list-item.component.html @@ -14,7 +14,7 @@ {{'COMMON.share' | translateHs }} -@if (composition.endpoint?.authenticated) { +@if (hsCommonLaymanService.isAuthenticated()) { diff --git a/projects/hslayers/components/compositions/compositions-list-item.component.ts b/projects/hslayers/components/compositions/compositions-list-item.component.ts index f37b41bae1..27284a149c 100644 --- a/projects/hslayers/components/compositions/compositions-list-item.component.ts +++ b/projects/hslayers/components/compositions/compositions-list-item.component.ts @@ -11,6 +11,7 @@ import {HsLanguageService} from 'hslayers-ng/services/language'; import {HsMapCompositionDescriptor} from 'hslayers-ng/types'; import {HsSetPermissionsDialogComponent} from 'hslayers-ng/common/dialog-set-permissions'; import {HsToastService} from 'hslayers-ng/common/toast'; +import {HsCommonLaymanService} from 'hslayers-ng/common/layman'; @Component({ selector: 'hs-compositions-list-item', @@ -28,6 +29,7 @@ export class HsCompositionsListItemComponent { private hsConfig: HsConfig, private hsLanguageService: HsLanguageService, private hsCompositionsCatalogueService: HsCompositionsCatalogueService, + public hsCommonLaymanService: HsCommonLaymanService, ) {} /** diff --git a/projects/hslayers/components/compositions/compositions-map.service.ts b/projects/hslayers/components/compositions/compositions-map.service.ts index de7638ac5c..7c211e95ae 100644 --- a/projects/hslayers/components/compositions/compositions-map.service.ts +++ b/projects/hslayers/components/compositions/compositions-map.service.ts @@ -111,9 +111,9 @@ export class HsCompositionsMapService { const featuresUnderMouse = this.extentLayer .getSource() .getFeaturesAtCoordinate(evt.coordinate); - for (const endpoint of this.hsCommonEndpointsService.endpoints.filter( - (ep) => ep.compositions, - )) { + for (const endpoint of this.hsCommonEndpointsService + .endpoints() + .filter((ep) => ep.compositions)) { this.hsLayerUtilsService.highlightFeatures( featuresUnderMouse, this.extentLayer, diff --git a/projects/hslayers/components/compositions/compositions.component.html b/projects/hslayers/components/compositions/compositions.component.html index fe086075fc..e750141dba 100644 --- a/projects/hslayers/components/compositions/compositions.component.html +++ b/projects/hslayers/components/compositions/compositions.component.html @@ -39,8 +39,7 @@
- - +
@@ -50,7 +49,7 @@ (change)='loadFilteredCompositions()' name="hsCompositionsCatalogueService.filterByExtent"> {{'COMPOSITIONS.filterByMap' | translateHs }}
- @if (ctx.layman?.authenticated) { + @if (hsCommonLaymanService.isAuthenticated()) {
{ - for (const endpoint of this.hsCommonEndpointsService.endpoints) { + for (const endpoint of this.hsCommonEndpointsService.endpoints()) { const record = this.hsCompositionsMapService.getFeatureRecordAndUnhighlight( e.feature, @@ -118,7 +118,7 @@ export class HsCompositionsService { * Reset composition counters for datasource endpoints */ resetCompositionCounter(): void { - this.hsCommonEndpointsService.endpoints.forEach((ep) => { + this.hsCommonEndpointsService.endpoints().forEach((ep) => { switch (ep.type) { case 'micka': return this.hsCompositionsMickaService.resetCompositionCounter(ep); diff --git a/projects/hslayers/components/compositions/endpoints/compositions-layman.service.ts b/projects/hslayers/components/compositions/endpoints/compositions-layman.service.ts index 7ac3a7e83d..23286c84fb 100644 --- a/projects/hslayers/components/compositions/endpoints/compositions-layman.service.ts +++ b/projects/hslayers/components/compositions/endpoints/compositions-layman.service.ts @@ -46,9 +46,8 @@ export class HsCompositionsLaymanService { extentFeatureCreated, _bbox, ): Observable { - endpoint.getCurrentUserIfNeeded(endpoint); endpoint.compositionsPaging.loaded = false; - const loggedIn = endpoint.authenticated; + const loggedIn = this.hsCommonLaymanService.isAuthenticated(); const query = params.query.title ? params.query.title : ''; const sortBy = params.sortBy == 'date:D' @@ -65,11 +64,12 @@ export class HsCompositionsLaymanService { this.hsMapService.getCurrentProj(), 'EPSG:3857', ); - const bbox = params.filterByExtent ? b.join(',') : ''; + const bbox = params.filterByExtent ? b.join(',') : ''; + const workspace = this.hsCommonLaymanService.user(); const withPermissionOrMine = params.filterByOnlyMine ? loggedIn - ? `workspaces/${endpoint.user}/` + ? `workspaces/${workspace}/` : '' : ''; const url = `${endpoint.url}/rest/${withPermissionOrMine}maps`; @@ -154,6 +154,7 @@ export class HsCompositionsLaymanService { ? parseInt(response.headers.get('x-total-count')) : response.body.length; + const currentUser = this.hsCommonLaymanService.user(); endpoint.compositions = response.body.map((record) => { const tmp: HsMapCompositionDescriptor = { name: record.name, @@ -162,7 +163,7 @@ export class HsCompositionsLaymanService { featureId: undefined, highlighted: false, editable: record.access_rights.write.some((user) => { - return [endpoint.user, 'EVERYONE'].includes(user); + return [currentUser, 'EVERYONE'].includes(user); }), url: `${endpoint.url}/rest/workspaces/${record.workspace}/maps/${record.name}`, endpoint, diff --git a/projects/hslayers/components/draw/draw-layer-metadata/draw-layer-metadata.component.html b/projects/hslayers/components/draw/draw-layer-metadata/draw-layer-metadata.component.html index 06f8ec1af4..23dece7f1d 100644 --- a/projects/hslayers/components/draw/draw-layer-metadata/draw-layer-metadata.component.html +++ b/projects/hslayers/components/draw/draw-layer-metadata/draw-layer-metadata.component.html @@ -19,7 +19,7 @@
} @if (data.service.drawableLaymanLayers.length > 0 && data.service.isAuthenticated) { @@ -45,8 +45,7 @@
@if (data.service.isAuthenticated) { - - + }
{{'COMMON.advancedOptions' | diff --git a/projects/hslayers/components/layer-manager/layer-manager.component.ts b/projects/hslayers/components/layer-manager/layer-manager.component.ts index f18a59bee5..b0e6efaccc 100644 --- a/projects/hslayers/components/layer-manager/layer-manager.component.ts +++ b/projects/hslayers/components/layer-manager/layer-manager.component.ts @@ -355,7 +355,7 @@ export class HsLayerManagerComponent .map((l) => { return l.layer; }), - this.hsCommonLaymanService.layman?.authenticated + this.hsCommonLaymanService.isAuthenticated() ? ['map', 'mapcatalogue'] : ['map'], ); diff --git a/projects/hslayers/components/save-map/dialog-result/dialog-result.component.ts b/projects/hslayers/components/save-map/dialog-result/dialog-result.component.ts index e146e3dafc..a0273c4832 100644 --- a/projects/hslayers/components/save-map/dialog-result/dialog-result.component.ts +++ b/projects/hslayers/components/save-map/dialog-result/dialog-result.component.ts @@ -35,7 +35,7 @@ export class HsSaveMapResultDialogComponent implements HsDialogComponent { * Thus - this has to always be making a request for current user workspace */ this.hsSaveMapManagerService.compoData.patchValue({ - workspace: this.hsSaveMapManagerService.currentUser, + workspace: this.hsSaveMapManagerService.currentUser(), }); await this.hsSaveMapManagerService.initiateSave(newSave); diff --git a/projects/hslayers/components/save-map/form/form.component.html b/projects/hslayers/components/save-map/form/form.component.html index 33ffc9ebfc..66d3e93884 100644 --- a/projects/hslayers/components/save-map/form/form.component.html +++ b/projects/hslayers/components/save-map/form/form.component.html @@ -22,7 +22,7 @@ - @if (endpoint?.type.includes('layman')) { + @if (endpoint()?.type.includes('layman')) { @@ -65,4 +65,4 @@
} -} \ No newline at end of file +} diff --git a/projects/hslayers/components/save-map/form/form.component.ts b/projects/hslayers/components/save-map/form/form.component.ts index 9e8d112a5f..9902e1126d 100644 --- a/projects/hslayers/components/save-map/form/form.component.ts +++ b/projects/hslayers/components/save-map/form/form.component.ts @@ -1,5 +1,5 @@ import {Component, DestroyRef, OnInit, inject} from '@angular/core'; -import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {takeUntilDestroyed, toSignal} from '@angular/core/rxjs-interop'; import {Observable, map, startWith} from 'rxjs'; @@ -14,7 +14,9 @@ import {HsUtilsService} from 'hslayers-ng/services/utils'; standalone: false, }) export class HsSaveMapAdvancedFormComponent implements OnInit { - endpoint: HsEndpoint; + endpoint = toSignal(this.hsSaveMapManagerService.endpointSelected, { + initialValue: null, + }); overwrite = false; downloadableData: string; extraFormOpened = ''; @@ -36,12 +38,6 @@ export class HsSaveMapAdvancedFormComponent implements OnInit { } ngOnInit(): void { - this.hsSaveMapManagerService.endpointSelected - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((endpoint) => { - this.endpoint = endpoint; - }); - this.hsSaveMapManagerService.saveMapResulted .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((statusData) => { @@ -136,7 +132,7 @@ export class HsSaveMapAdvancedFormComponent implements OnInit { if (this.endpoint === null) { return false; } - if (this.endpoint.type.includes('layman')) { + if (this.endpoint().type.includes('layman')) { return true; } } @@ -150,7 +146,7 @@ export class HsSaveMapAdvancedFormComponent implements OnInit { * Overwriting composition of other user and making it private * = access for owner + current user */ - const currentUser = this.hsSaveMapManagerService.currentUser; + const currentUser = this.hsSaveMapManagerService.currentUser(); const workspace = this.hsSaveMapManagerService.compoData.get('workspace').value; if (newSave == false && this.canOverwrite() && currentUser !== workspace) { diff --git a/projects/hslayers/components/save-map/save-map-manager.service.ts b/projects/hslayers/components/save-map/save-map-manager.service.ts index 54f8e6a886..3bae1a0ccf 100644 --- a/projects/hslayers/components/save-map/save-map-manager.service.ts +++ b/projects/hslayers/components/save-map/save-map-manager.service.ts @@ -1,7 +1,7 @@ import {BehaviorSubject, Subject, withLatestFrom} from 'rxjs'; import {FormControl, FormGroup, Validators} from '@angular/forms'; import {HttpClient} from '@angular/common/http'; -import {Injectable} from '@angular/core'; +import {computed, Injectable, Signal} from '@angular/core'; import { AccessRightsModel, @@ -25,6 +25,7 @@ import {HsLogService} from 'hslayers-ng/services/log'; import {HsMapService} from 'hslayers-ng/services/map'; import {HsShareService} from 'hslayers-ng/components/share'; import {HsUtilsService} from 'hslayers-ng/services/utils'; +import {HsCommonLaymanService} from 'hslayers-ng/common/layman'; export class HsSaveMapManagerParams { statusData: StatusData = { @@ -65,7 +66,7 @@ export class HsSaveMapManagerParams { endpointSelected: BehaviorSubject = new BehaviorSubject(null); preSaveCheckCompleted: Subject = new Subject(); changeTitle: boolean; - currentUser: string; + currentUser: Signal; missingName = false; missingAbstract = false; @@ -76,6 +77,8 @@ export class HsSaveMapManagerParams { providedIn: 'root', }) export class HsSaveMapManagerService extends HsSaveMapManagerParams { + currentUser = this.hsCommonLaymanService.user; + constructor( private hsMapService: HsMapService, private hsSaveMapService: HsSaveMapService, @@ -88,6 +91,7 @@ export class HsSaveMapManagerService extends HsSaveMapManagerParams { private hsEventBusService: HsEventBusService, private hsLogService: HsLogService, private hsCompositionsParserService: HsCompositionsParserService, + private hsCommonLaymanService: HsCommonLaymanService, ) { super(); @@ -99,7 +103,7 @@ export class HsSaveMapManagerService extends HsSaveMapManagerParams { this.currentComposition = responseData; const workspace = metadata['error'] - ? this.currentUser + ? this.currentUser() : this.parseAccessRights(metadata); this.compoData.patchValue({ @@ -133,11 +137,11 @@ export class HsSaveMapManagerService extends HsSaveMapManagerParams { : null; const write = metadata.access_rights.write; const read = metadata.access_rights.read; - if (this.currentUser === workspace) { - this.privateOrPublic(write, 'write', this.currentUser); - this.privateOrPublic(read, 'read', this.currentUser); + if (this.currentUser() === workspace) { + this.privateOrPublic(write, 'write', this.currentUser()); + this.privateOrPublic(read, 'read', this.currentUser()); } else if ( - write.includes(this.currentUser) || + write.includes(this.currentUser()) || /** * Different user + PUBLIC write */ diff --git a/projects/hslayers/components/save-map/save-map.component.html b/projects/hslayers/components/save-map/save-map.component.html index dccd8d36ed..1bd62f5d0c 100644 --- a/projects/hslayers/components/save-map/save-map.component.html +++ b/projects/hslayers/components/save-map/save-map.component.html @@ -7,21 +7,22 @@
{{'COMMON.saveTo' | translateHs }} - @for (e of endpoints; track e) { }
- @if (endpoint?.type === 'layman') { - - + @if (selectedEndpoint()?.type === 'layman') { + } - @if (isAuthenticated || !endpoint?.type.includes('layman')) { - - + @if (isAuthenticated() || !selectedEndpoint()?.type?.includes('layman')) { + } @else {
{{'SAVECOMPOSITION.panelMd.youAreNotAuthorized' | translateHs }}
diff --git a/projects/hslayers/components/save-map/save-map.component.ts b/projects/hslayers/components/save-map/save-map.component.ts index bc38e27a70..1de64e617d 100644 --- a/projects/hslayers/components/save-map/save-map.component.ts +++ b/projects/hslayers/components/save-map/save-map.component.ts @@ -1,5 +1,9 @@ -import {Component, OnInit} from '@angular/core'; -import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {Component, linkedSignal, OnInit} from '@angular/core'; +import { + takeUntilDestroyed, + toObservable, + toSignal, +} from '@angular/core/rxjs-interop'; import {HsCommonEndpointsService} from 'hslayers-ng/services/endpoints'; import {HsCommonLaymanService} from 'hslayers-ng/common/layman'; @@ -16,9 +20,15 @@ import {HsSaveMapService} from 'hslayers-ng/services/save-map'; standalone: false, }) export class HsSaveMapComponent extends HsPanelBaseComponent implements OnInit { - endpoint: HsEndpoint = null; + selectedEndpoint = toSignal(this.hsSaveMapManagerService.endpointSelected); + + compareEndpoints(a: HsEndpoint, b: HsEndpoint): boolean { + // Compare endpoints by their URL or other unique identifier + return a?.url === b?.url; + } + endpoints: HsEndpoint[]; - isAuthenticated = false; + isAuthenticated = this.hsCommonLaymanService.isAuthenticated; name = 'saveMap'; constructor( private hsConfig: HsConfig, @@ -30,16 +40,12 @@ export class HsSaveMapComponent extends HsPanelBaseComponent implements OnInit { private hsSaveMapService: HsSaveMapService, ) { super(); - } - ngOnInit() { - super.ngOnInit(); - this.endpoints = this.hsCommonEndpointsService.endpoints; - this.hsCommonEndpointsService.endpointsFilled + toObservable(this.hsCommonEndpointsService.endpoints) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((endpoints) => { - if (endpoints?.length > 0 && !this.endpoint) { - const laymanEp = this.hsCommonLaymanService.layman; + if (endpoints?.length > 0 && !this.selectedEndpoint()) { + const laymanEp = this.hsCommonLaymanService.layman(); if (laymanEp) { this.hsSaveMapManagerService.selectEndpoint(laymanEp); } else { @@ -47,33 +53,19 @@ export class HsSaveMapComponent extends HsPanelBaseComponent implements OnInit { } } }); - - this.hsCommonLaymanService.authChange - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((endpoint) => { - this.isAuthenticated = endpoint.authenticated; - this.hsSaveMapManagerService.currentUser = endpoint.user; - }); - - this.hsSaveMapManagerService.endpointSelected - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((endpoint) => { - if (endpoint) { - this.endpoint = endpoint; - if (endpoint.getCurrentUserIfNeeded) { - endpoint.getCurrentUserIfNeeded(endpoint); - } - } - }); + } + ngOnInit() { + super.ngOnInit(); + this.endpoints = this.hsCommonEndpointsService.endpoints(); this.hsSaveMapManagerService.panelOpened .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((composition) => { if (composition && composition.endpoint) { const openedType = composition.endpoint.type; - const found = this.hsCommonEndpointsService.endpoints.filter((ep) => - ep.type.includes(openedType), - ); + const found = this.hsCommonEndpointsService + .endpoints() + .filter((ep) => ep.type.includes(openedType)); if (found.length > 0) { this.hsSaveMapManagerService.selectEndpoint(found[0]); } diff --git a/projects/hslayers/core/auth.interceptor.ts b/projects/hslayers/core/auth.interceptor.ts new file mode 100644 index 0000000000..278537e39b --- /dev/null +++ b/projects/hslayers/core/auth.interceptor.ts @@ -0,0 +1,21 @@ +import {HttpInterceptorFn} from '@angular/common/http'; +import {catchError} from 'rxjs/operators'; +import {inject} from '@angular/core'; +import {throwError} from 'rxjs'; + +import {HsCommonLaymanService} from 'hslayers-ng/common/layman'; + +export const authInterceptor: HttpInterceptorFn = (req, next) => { + const authService = inject(HsCommonLaymanService); + + return next(req).pipe( + catchError((err) => { + if (err.status === 401 && err?.authenticated === false) { + //Unauthorized request sent to backend -> logout + authService.logout$.next(); + } + // Re-throw the error so it can be handled elsewhere if needed + return throwError(() => err); + }), + ); +}; diff --git a/projects/hslayers/css/icons/hslayers-ng-fa-icons.css b/projects/hslayers/css/icons/hslayers-ng-fa-icons.css index b0b19ce53d..e1e1e015c6 100644 --- a/projects/hslayers/css/icons/hslayers-ng-fa-icons.css +++ b/projects/hslayers/css/icons/hslayers-ng-fa-icons.css @@ -65,7 +65,7 @@ font-style: normal; font-weight: 900; font-display: block; - src: url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); + src: url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); } @font-face { @@ -73,7 +73,7 @@ font-style: normal; font-weight: 400; font-display: block; - src: url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); + src: url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); } @font-face { @@ -133,6 +133,10 @@ --fa: "\f077"; } +.fa-user { + --fa: "\f007"; + } + .fa-globe { --fa: "\f0ac"; } @@ -153,6 +157,10 @@ --fa: "\f715"; } +.fa-right-to-bracket { + --fa: "\f2f6"; + } + .fa-map-location-dot { --fa: "\f5a0"; } diff --git a/projects/hslayers/css/icons/icons.txt b/projects/hslayers/css/icons/icons.txt index 215f5d2753..5cefbb1f0f 100644 --- a/projects/hslayers/css/icons/icons.txt +++ b/projects/hslayers/css/icons/icons.txt @@ -74,3 +74,5 @@ fa-triangle-exclamation fa-circle-info eraser globe +user +right-to-bracket diff --git a/projects/hslayers/services/add-data/catalogue/catalogue-map.service.ts b/projects/hslayers/services/add-data/catalogue/catalogue-map.service.ts index 71daac046d..9044e0b6c9 100644 --- a/projects/hslayers/services/add-data/catalogue/catalogue-map.service.ts +++ b/projects/hslayers/services/add-data/catalogue/catalogue-map.service.ts @@ -95,9 +95,9 @@ export class HsAddDataCatalogueMapService { const featuresUnderMouse = this.extentLayer .getSource() .getFeaturesAtCoordinate(evt.coordinate); - for (const endpoint of this.hsCommonEndpointsService.endpoints.filter( - (ep) => ep.layers, - )) { + for (const endpoint of this.hsCommonEndpointsService + .endpoints() + .filter((ep) => ep.layers)) { this.hsLayerUtilsService.highlightFeatures( featuresUnderMouse, this.extentLayer, diff --git a/projects/hslayers/services/add-data/catalogue/catalogue.service.ts b/projects/hslayers/services/add-data/catalogue/catalogue.service.ts index 7e449cb70b..83cc8eeb37 100644 --- a/projects/hslayers/services/add-data/catalogue/catalogue.service.ts +++ b/projects/hslayers/services/add-data/catalogue/catalogue.service.ts @@ -23,6 +23,7 @@ import {HsLayoutService} from 'hslayers-ng/services/layout'; import {HsMapService} from 'hslayers-ng/services/map'; import {HsMickaBrowserService} from './micka.service'; import {HsUtilsService} from 'hslayers-ng/services/utils'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; class HsAddDataCatalogueParams { data: any = { @@ -90,11 +91,13 @@ export class HsAddDataCatalogueService extends HsAddDataCatalogueParams { this.calcExtentLayerVisibility(); }); - this.hsCommonLaymanService.authChange.subscribe(() => { - if (this.panelVisible()) { - this.reloadData(); - } - }); + this.hsCommonLaymanService.layman$ + .pipe(takeUntilDestroyed()) + .subscribe(() => { + if (this.panelVisible()) { + this.reloadData(); + } + }); this.hsAddDataService.datasetTypeSelected.subscribe((type) => { if (type == 'catalogue' && this.panelVisible()) { @@ -102,7 +105,7 @@ export class HsAddDataCatalogueService extends HsAddDataCatalogueParams { } }); - this.endpoints = this.hsCommonEndpointsService.endpoints; + this.endpoints = this.hsCommonEndpointsService.endpoints(); if (this.dataSourceExistsAndEmpty() && this.panelVisible()) { this.queryCatalogs(); @@ -135,7 +138,7 @@ export class HsAddDataCatalogueService extends HsAddDataCatalogueParams { } reloadData(): void { - this.endpoints = this.hsCommonEndpointsService.endpoints; + this.endpoints = this.hsCommonEndpointsService.endpoints(); this.resetList(); this.queryCatalogs(); // this.hsMickaFilterService.fillCodesets(); diff --git a/projects/hslayers/services/add-data/catalogue/layman.service.ts b/projects/hslayers/services/add-data/catalogue/layman.service.ts index c884dc5008..1e570b0c6a 100644 --- a/projects/hslayers/services/add-data/catalogue/layman.service.ts +++ b/projects/hslayers/services/add-data/catalogue/layman.service.ts @@ -52,11 +52,11 @@ export class HsLaymanBrowserService { }, extentFeatureCreated?: (feature: Feature) => void, ): Observable { - endpoint.getCurrentUserIfNeeded(endpoint); - const loggedIn = endpoint.authenticated; + const loggedIn = this.hsCommonLaymanService.isAuthenticated(); + const workspace = this.hsCommonLaymanService.user(); const withPermissionOrMine = data?.onlyMine ? loggedIn - ? `workspaces/${endpoint.user}/` + ? `workspaces/${workspace}/` : '' : ''; const url = `${endpoint.url}/rest/${withPermissionOrMine}layers`; @@ -246,16 +246,13 @@ export class HsLaymanBrowserService { ), ); if (data.code || data.message) { - if (data.code == 32) { - endpoint.user = undefined; - endpoint.authenticated = false; - this.hsCommonLaymanService.authChange.next(endpoint); + if (data.code != 32) { + this.hsToastService.createToastPopupMessage( + data.message ?? 'ADDLAYERS.ERROR.errorWhileRequestingLayers', + data.detail ?? `${data.code}/${data.sub_code}`, + {}, + ); } - this.hsToastService.createToastPopupMessage( - data.message ?? 'ADDLAYERS.ERROR.errorWhileRequestingLayers', - data.detail ?? `${data.code}/${data.sub_code}`, - {}, - ); return; } diff --git a/projects/hslayers/services/add-data/common-file.service.ts b/projects/hslayers/services/add-data/common-file.service.ts index 30ad1a4a73..b77af88b77 100644 --- a/projects/hslayers/services/add-data/common-file.service.ts +++ b/projects/hslayers/services/add-data/common-file.service.ts @@ -5,7 +5,6 @@ import {Subject} from 'rxjs'; import {HsAddDataOwsService} from './url/add-data-ows.service'; import {HsAddDataService} from './add-data.service'; import {HsAddDataUrlService} from './url/add-data-url.service'; -import {HsCommonEndpointsService} from 'hslayers-ng/services/endpoints'; import {HsLanguageService} from 'hslayers-ng/services/language'; import {HsLaymanService} from 'hslayers-ng/services/save-map'; import {HsLogService} from 'hslayers-ng/services/log'; @@ -45,7 +44,7 @@ export class HsAddDataCommonFileServiceParams { readingData = false; loadingToLayman = false; asyncLoading = false; - endpoint: HsEndpoint = null; + endpoint: HsEndpoint; /** * @param success - true when layer added successfully */ @@ -60,7 +59,6 @@ export class HsAddDataCommonFileService extends HsAddDataCommonFileServiceParams private hsAddDataOwsService: HsAddDataOwsService, private hsAddDataUrlService: HsAddDataUrlService, private hsAddDataService: HsAddDataService, - private hsCommonEndpointsService: HsCommonEndpointsService, private hsDialogContainerService: HsDialogContainerService, private hsLanguageService: HsLanguageService, private hsLaymanService: HsLaymanService, @@ -72,12 +70,12 @@ export class HsAddDataCommonFileService extends HsAddDataCommonFileServiceParams super(); } + endpoint = this.hsCommonLaymanService.layman(); /** * Clear service param values to default values */ clearParams(): void { this.asyncLoading = false; - this.endpoint = null; this.loadingToLayman = false; this.hsLaymanService.totalProgress = 0; } @@ -115,23 +113,6 @@ export class HsAddDataCommonFileService extends HsAddDataCommonFileServiceParams } } - /** - * From available endpoints picks one - * - either Layman endpoint if available or any other if not - */ - pickEndpoint(): void { - const endpoints = this.hsCommonEndpointsService.endpoints; - if (endpoints && endpoints.length > 0) { - const layman = this.hsCommonLaymanService.layman; - if (layman) { - this.endpoint = layman; - this.endpoint.getCurrentUserIfNeeded(this.endpoint); - } else { - this.endpoint = endpoints[0]; - } - } - } - /** * Get tooltip translated text * @param data - File data object provided @@ -236,7 +217,7 @@ export class HsAddDataCommonFileService extends HsAddDataCommonFileServiceParams overwrite?: boolean, ): Promise { try { - const formData = await this.constructFormData(endpoint, formDataParams); + const formData = await this.constructFormData(formDataParams); const asyncUpload: AsyncUpload = this.hsLaymanService.prepareAsyncUpload(formData); this.asyncLoading = asyncUpload.async; @@ -263,12 +244,11 @@ export class HsAddDataCommonFileService extends HsAddDataCommonFileServiceParams const friendlyName = getLaymanFriendlyLayerName(name); let descriptor: HsLaymanLayerDescriptor; if (this.isAuthenticated()) { - this.pickEndpoint(); try { descriptor = await this.hsLaymanService.describeLayer( this.endpoint, name, - this.endpoint.user, + this.hsCommonLaymanService.user(), true, ); } catch (error) { @@ -323,7 +303,6 @@ export class HsAddDataCommonFileService extends HsAddDataCommonFileServiceParams /** * Construct a set of key/value pairs from, that can be easily sent using HTTP - * @param endpoint - Layman endpoint description (url, name, user) * @param files - Array of files * @param name - Name of new layer * @param title - Title of new layer @@ -333,10 +312,7 @@ export class HsAddDataCommonFileService extends HsAddDataCommonFileServiceParams * @param access_rights - User access rights for the new layer, * @returns FormData object for HTTP request */ - async constructFormData( - endpoint: HsEndpoint, - formDataParams: FileFormData, - ): Promise { + async constructFormData(formDataParams: FileFormData): Promise { this.readingData = true; const {files, name, abstract, srs, access_rights, timeRegex} = formDataParams; @@ -380,10 +356,8 @@ export class HsAddDataCommonFileService extends HsAddDataCommonFileServiceParams formData.append('abstract', abstract); formData.append('crs', srs); - const rights = this.hsLaymanService.parseAccessRightsForLayman( - endpoint, - access_rights, - ); + const rights = + this.hsLaymanService.parseAccessRightsForLayman(access_rights); formData.append('access_rights.write', rights.write); formData.append('access_rights.read', rights.read); @@ -408,7 +382,7 @@ export class HsAddDataCommonFileService extends HsAddDataCommonFileServiceParams try { this.loadingToLayman = true; if (!this.endpoint) { - this.pickEndpoint(); + throw new Error('No endpoint available'); } if (!this.isSRSSupported(data)) { throw new Error( @@ -598,7 +572,7 @@ export class HsAddDataCommonFileService extends HsAddDataCommonFileServiceParams const descriptor = await this.hsLaymanService.describeLayer( endpoint, layerName, - endpoint.user, + this.hsCommonLaymanService.user(), ); if ( pendingParams.some((param) => @@ -640,9 +614,9 @@ export class HsAddDataCommonFileService extends HsAddDataCommonFileServiceParams * @returns True, if srs is supported, false otherwise */ isSRSSupported(data: FileDataObject): boolean { - return this.hsLaymanService.supportedCRRList.some((epsg) => - data.srs.endsWith(epsg), - ); + return this.hsLaymanService + .supportedCRRList() + .some((epsg) => data.srs.endsWith(epsg)); } /** @@ -650,7 +624,7 @@ export class HsAddDataCommonFileService extends HsAddDataCommonFileServiceParams * @returns True, if user is authorized, false otherwise */ isAuthenticated(): boolean { - return this.hsCommonLaymanService.layman?.authenticated ?? false; + return this.hsCommonLaymanService.isAuthenticated(); } /** diff --git a/projects/hslayers/services/add-data/vector/vector-utils.service.ts b/projects/hslayers/services/add-data/vector/vector-utils.service.ts index a327669891..3688486621 100644 --- a/projects/hslayers/services/add-data/vector/vector-utils.service.ts +++ b/projects/hslayers/services/add-data/vector/vector-utils.service.ts @@ -39,7 +39,8 @@ export class HsAddDataVectorUtilsService { .map((proj) => proj.getCode()) .some((code) => code === projection.getCode()) ? getProjection('EPSG:4326') - : this.hsLaymanService.supportedCRRList.indexOf(projection.getCode()) > -1 + : this.hsLaymanService.supportedCRRList().indexOf(projection.getCode()) > + -1 ? projection : getProjection('EPSG:4326'); //Features in map CRS diff --git a/projects/hslayers/services/add-data/vector/vector.service.ts b/projects/hslayers/services/add-data/vector/vector.service.ts index 5dc92c1eae..bc2563bdd8 100644 --- a/projects/hslayers/services/add-data/vector/vector.service.ts +++ b/projects/hslayers/services/add-data/vector/vector.service.ts @@ -228,7 +228,7 @@ export class HsAddDataVectorService { idAttribute: `?${data.idProperty}`, path: this.hsUtilsService.undefineEmptyString(data.folder_name), access_rights: data.access_rights, - workspace: this.hsAddDataCommonFileService.endpoint?.user, + workspace: this.hsCommonLaymanService.user(), query: data.query, queryCapabilities: data.type != 'kml' && @@ -248,7 +248,8 @@ export class HsAddDataVectorService { */ async addNewLayer(data: VectorDataObject) { if (!this.hsAddDataCommonFileService.endpoint) { - this.hsAddDataCommonFileService.pickEndpoint(); + console.error('No endpoint available'); + return; } const addLayerRes: { layer: VectorLayer>; @@ -312,9 +313,9 @@ export class HsAddDataVectorService { async upsertLayer(data: VectorDataObject): Promise { const commonFileRef = this.hsAddDataCommonFileService; - const crsSupported = this.hsLaymanService.supportedCRRList.includes( - data.nativeSRS, - ); + const crsSupported = this.hsLaymanService + .supportedCRRList() + .includes(data.nativeSRS); const style = typeof data.serializedStyle == 'string' ? data.serializedStyle @@ -325,7 +326,7 @@ export class HsAddDataVectorService { crs: this.hsAddDataVectorUtilsService .getFeaturesProjection(getProjection(data.nativeSRS)) .getCode(), - workspace: commonFileRef.endpoint.user, + workspace: this.hsCommonLaymanService.user(), access_rights: data.access_rights, style, }; @@ -382,7 +383,7 @@ export class HsAddDataVectorService { await this.hsLaymanService.describeLayer( commonFileRef.endpoint, upsertReq.name, - commonFileRef.endpoint.user, + this.hsCommonLaymanService.user(), ); return OverwriteResponse.overwrite; } diff --git a/projects/hslayers/services/compositions/compositions-parser.service.ts b/projects/hslayers/services/compositions/compositions-parser.service.ts index bfe8033858..23af2c6a74 100644 --- a/projects/hslayers/services/compositions/compositions-parser.service.ts +++ b/projects/hslayers/services/compositions/compositions-parser.service.ts @@ -105,14 +105,14 @@ export class HsCompositionsParserService { switchMap((_) => { const fromLayman = isLaymanUrl( this.current_composition_url, - this.hsCommonLaymanService.layman, + this.hsCommonLaymanService.layman(), ); return this.$http .get( this.current_composition_url.replace('/file', ''), { withCredentials: - this.hsCommonLaymanService.layman?.user && fromLayman, + this.hsCommonLaymanService.isAuthenticated() && fromLayman, }, ) .pipe( @@ -160,8 +160,8 @@ export class HsCompositionsParserService { options['responseType'] = 'text'; } options['withCredentials'] = - isLaymanUrl(url, this.hsCommonLaymanService.layman) && - this.hsCommonLaymanService.layman.user; + isLaymanUrl(url, this.hsCommonLaymanService.layman()) && + this.hsCommonLaymanService.isAuthenticated(); const data: any = await lastValueFrom(this.$http.get(url, options)).catch( (e) => { @@ -576,7 +576,7 @@ export class HsCompositionsParserService { response = await lastValueFrom( this.$http.get(url, { responseType: 'json', - withCredentials: !!this.hsCommonLaymanService.layman.user, + withCredentials: !!this.hsCommonLaymanService.isAuthenticated(), }), ); } diff --git a/projects/hslayers/services/draw/draw.service.params.ts b/projects/hslayers/services/draw/draw.service.params.ts index 19d8c0207d..6bc9030c9b 100644 --- a/projects/hslayers/services/draw/draw.service.params.ts +++ b/projects/hslayers/services/draw/draw.service.params.ts @@ -4,6 +4,7 @@ import {EventsKey} from 'ol/events'; import {Layer, Vector as VectorLayer} from 'ol/layer'; import {Source, Vector as VectorSource} from 'ol/source'; import {Subject} from 'rxjs'; +import {Signal} from '@angular/core'; export class HsDrawServiceParams { drawableLayers: Array> = []; @@ -54,7 +55,7 @@ export class HsDrawServiceParams { onDeselected: any; laymanEndpoint: any; previouslySelected: any; - isAuthenticated: boolean; + isAuthenticated: Signal; onlyMine = true; addedLayersRemoved = false; eventHandlers: EventsKey[] = []; diff --git a/projects/hslayers/services/draw/draw.service.ts b/projects/hslayers/services/draw/draw.service.ts index cb750b148b..771aed5ed5 100644 --- a/projects/hslayers/services/draw/draw.service.ts +++ b/projects/hslayers/services/draw/draw.service.ts @@ -50,6 +50,7 @@ import { setTitle, setWorkspace, } from 'hslayers-ng/common/extensions'; +import {takeUntilDestroyed, toObservable} from '@angular/core/rxjs-interop'; type ActivateParams = { onDrawStart?; @@ -66,6 +67,8 @@ export const TMP_LAYER_TITLE = 'tmpDrawLayer'; providedIn: 'root', }) export class HsDrawService extends HsDrawServiceParams { + isAuthenticated = this.hsCommonLaymanService.isAuthenticated; + constructor( public hsMapService: HsMapService, public hsLayerUtilsService: HsLayerUtilsService, @@ -139,22 +142,23 @@ export class HsDrawService extends HsDrawServiceParams { this.fillDrawableLayers(); } }); + }); - this.hsCommonLaymanService.authChange.subscribe((endpoint) => { + toObservable(this.hsCommonLaymanService.isAuthenticated) + .pipe(takeUntilDestroyed()) + .subscribe((isAuthenticated) => { this.fillDrawableLayers(); - this.isAuthenticated = endpoint?.authenticated; //When metadata dialog window opened. Layer is being added if (this.selectedLayer && this.tmpDrawLayer) { - setWorkspace(this.selectedLayer, endpoint?.user); + setWorkspace(this.selectedLayer, this.hsCommonLaymanService.user()); setDefinition(this.selectedLayer, { - format: this.isAuthenticated ? 'WFS' : null, - url: this.isAuthenticated - ? this.hsCommonLaymanService.layman?.url + '/wfs' + format: isAuthenticated ? 'WFS' : null, + url: isAuthenticated + ? this.hsCommonLaymanService.layman()?.url + '/wfs' : null, }); } }); - }); } /** @@ -205,7 +209,7 @@ export class HsDrawService extends HsDrawServiceParams { while (this.hsMapService.findLayerByTitle(tmpTitle)) { tmpTitle = `${this.translate('DRAW.drawLayer')} ${i++}`; } - const layman = this.hsCommonLaymanService.layman; + const layman = this.hsCommonLaymanService.layman(); const drawLayer = new VectorLayer>({ //TODO: Also name should be set, but take care in case a layer with that name already exists in layman source: tmpSource, @@ -221,7 +225,7 @@ export class HsDrawService extends HsDrawServiceParams { format: this.isAuthenticated ? 'WFS' : null, url: this.isAuthenticated ? layman.url + '/wfs' : null, }); - setWorkspace(drawLayer, layman?.user); + setWorkspace(drawLayer, this.hsCommonLaymanService.user()); this.tmpDrawLayer = true; this.selectedLayer = drawLayer; this.layerMetadataDialog.next(); @@ -516,7 +520,7 @@ export class HsDrawService extends HsDrawServiceParams { } this.addedLayersRemoved = false; this.drawableLayers = drawables; - this.laymanEndpoint = this.hsCommonLaymanService.layman; + this.laymanEndpoint = this.hsCommonLaymanService.layman(); if (this.laymanEndpoint) { await lastValueFrom( this.hsLaymanBrowserService.queryCatalog(this.laymanEndpoint, { @@ -851,10 +855,10 @@ export class HsDrawService extends HsDrawServiceParams { */ private async layerRemoval(multi: boolean = false) { let confirmed; - const a: ['map', 'mapcatalogue'] | ['map'] = this.hsCommonLaymanService - .layman?.authenticated - ? ['map', 'mapcatalogue'] - : ['map']; + const a: ['map', 'mapcatalogue'] | ['map'] = + this.hsCommonLaymanService.isAuthenticated() + ? ['map', 'mapcatalogue'] + : ['map']; if (multi) { confirmed = await this.hsRemoveLayerDialogService.removeMultipleLayers( this.drawableLayers, diff --git a/projects/hslayers/services/endpoints/endpoints.service.ts b/projects/hslayers/services/endpoints/endpoints.service.ts index 71048da725..1025e1b5b4 100644 --- a/projects/hslayers/services/endpoints/endpoints.service.ts +++ b/projects/hslayers/services/endpoints/endpoints.service.ts @@ -1,32 +1,25 @@ -import {Injectable} from '@angular/core'; +import {inject, Injectable} from '@angular/core'; -import {BehaviorSubject} from 'rxjs'; - -import {HsCommonLaymanService} from 'hslayers-ng/common/layman'; import {HsConfig} from 'hslayers-ng/config'; import {HsEndpoint} from 'hslayers-ng/types'; import {HsUtilsService} from 'hslayers-ng/services/utils'; +import {toSignal} from '@angular/core/rxjs-interop'; +import {of, switchMap} from 'rxjs'; @Injectable({providedIn: 'root'}) export class HsCommonEndpointsService { - endpointsFilled: BehaviorSubject = new BehaviorSubject(null); - endpoints: HsEndpoint[]; + hsConfig = inject(HsConfig); + hsUtilsService = inject(HsUtilsService); - constructor( - public hsConfig: HsConfig, - public hsCommonLaymanService: HsCommonLaymanService, - public hsUtilsService: HsUtilsService, - ) { - this.fillEndpoints(); - this.hsConfig.configChanges.subscribe(() => { - this.fillEndpoints(); - }); - } + endpoints = toSignal( + this.hsConfig.configChanges.pipe(switchMap(() => of(this.fillEndpoints()))), + {initialValue: [] as HsEndpoint[]}, + ); - private fillEndpoints() { - this.endpoints = [ - ...(this.hsConfig.datasources || []).map((ds) => { - const tmp = { + private fillEndpoints(): HsEndpoint[] { + const endpoints = (this.hsConfig.datasources || []).map( + (ds) => + ({ url: ds.url, id: this.hsUtilsService.generateUuid(), type: ds.type, @@ -45,26 +38,17 @@ export class HsCommonEndpointsService { paging: { itemsPerPage: this.getItemsPerPageConfig(ds), }, - user: undefined, - getCurrentUserIfNeeded: async () => - await this.hsCommonLaymanService.getCurrentUserIfNeeded(tmp), - }; - return tmp; - }), - ] - /** - * Sort endpoints in order to give layman's - * layers priority in duplicate filtering. - */ - .sort((a, b) => a.type.localeCompare(b.type)); + }) as HsEndpoint, + ); - if (this.endpoints) { - this.hsCommonLaymanService.layman$.next( - this.endpoints.find((ep) => ep.type.includes('layman')), - ); - this.endpointsFilled.next(this.endpoints); - } + // Sort endpoints to give layman's layers priority in duplicate filtering + return endpoints.sort((a, b) => a.type.localeCompare(b.type)); } + /** + * Get items per page config + * @param endpoint - Endpoint + * @returns number + */ getItemsPerPageConfig(endpoint): number { return endpoint.paging !== undefined && endpoint.paging.itemsPerPage !== undefined diff --git a/projects/hslayers/services/get-capabilities/wms-get-capabilities.service.ts b/projects/hslayers/services/get-capabilities/wms-get-capabilities.service.ts index e5fe80bb1b..90907472af 100644 --- a/projects/hslayers/services/get-capabilities/wms-get-capabilities.service.ts +++ b/projects/hslayers/services/get-capabilities/wms-get-capabilities.service.ts @@ -109,7 +109,7 @@ export class HsWmsGetCapabilitiesService implements IGetCapabilities { responseType: 'text', withCredentials: isLaymanUrl( url, - this.hsCommonLaymanService.layman, + this.hsCommonLaymanService.layman(), ), observe: 'response', // Set observe to 'response' to get headers as well }) diff --git a/projects/hslayers/services/map/map.service.ts b/projects/hslayers/services/map/map.service.ts index b86668875d..c972314e10 100644 --- a/projects/hslayers/services/map/map.service.ts +++ b/projects/hslayers/services/map/map.service.ts @@ -1041,7 +1041,7 @@ export class HsMapService { (image.getImage() as HTMLImageElement).src = src; return; } - const laymanEp = this.hsCommonLaymanService.layman; + const laymanEp = this.hsCommonLaymanService.layman(); if (laymanEp && src.startsWith(laymanEp.url)) { this.laymanWmsLoadingFunction(image, src) .then((_) => { diff --git a/projects/hslayers/services/save-map/layer-synchronizer.service.ts b/projects/hslayers/services/save-map/layer-synchronizer.service.ts index 2e0f89f37b..ffff6e0d67 100644 --- a/projects/hslayers/services/save-map/layer-synchronizer.service.ts +++ b/projects/hslayers/services/save-map/layer-synchronizer.service.ts @@ -58,13 +58,17 @@ export class HsLayerSynchronizerService { element: lyr, }); }); - this.hsCommonLaymanService.authChange.subscribe(() => { - this.reloadLayersOnAuthChange(); - }); + this.crs = this.hsMapService.getCurrentProj().getCode(); this.hsLaymanService.crs = this.crs; }); + this.hsCommonLaymanService.layman$ + .pipe(takeUntilDestroyed()) + .subscribe((layman) => { + this.reloadLayersOnAuthChange(); + }); + this.hsEventBusService.refreshLaymanLayer .pipe(takeUntilDestroyed()) .subscribe((layer) => { @@ -75,11 +79,13 @@ export class HsLayerSynchronizerService { * Reload all the synchronized layers after Layman's authorization change */ private reloadLayersOnAuthChange(): void { - for (const layer of this.syncedLayers) { - const layerSource = layer.getSource(); - setLaymanLayerDescriptor(layer, undefined); - layerSource.clear(); - this.pull(layer, layerSource); + if (this.syncedLayers.length > 0) { + for (const layer of this.syncedLayers) { + const layerSource = layer.getSource(); + setLaymanLayerDescriptor(layer, undefined); + layerSource.clear(); + this.pull(layer, layerSource); + } } } @@ -123,7 +129,7 @@ export class HsLayerSynchronizerService { if (!desc) { if (retryCount < maxRetryCount) { const desc = await this.hsLaymanService.describeLayer( - this.hsCommonLaymanService.layman, + this.hsCommonLaymanService.layman(), getName(layer), getWorkspace(layer), ); @@ -180,7 +186,7 @@ export class HsLayerSynchronizerService { */ findLaymanForWfsLayer(layer: VectorLayer>) { const definitionUrl = getDefinition(layer).url; - const laymanEp = this.hsCommonLaymanService?.layman; + const laymanEp = this.hsCommonLaymanService?.layman(); if (!laymanEp || !definitionUrl) { return undefined; } diff --git a/projects/hslayers/services/save-map/layman.service.ts b/projects/hslayers/services/save-map/layman.service.ts index 7f7e6a159e..0b8d3fa3bf 100644 --- a/projects/hslayers/services/save-map/layman.service.ts +++ b/projects/hslayers/services/save-map/layman.service.ts @@ -1,5 +1,5 @@ import {HttpClient, HttpHeaders} from '@angular/common/http'; -import {Injectable} from '@angular/core'; +import {computed, Injectable} from '@angular/core'; import Resumable from 'resumablejs'; import { @@ -21,7 +21,6 @@ import {Layer, Vector as VectorLayer} from 'ol/layer'; import {Source, Vector as VectorSource} from 'ol/source'; import { - AboutLayman, AccessRightsModel, AsyncUpload, CompoData, @@ -45,13 +44,11 @@ import { wfsNotAvailable, PostPatchLayerResponse, } from 'hslayers-ng/common/layman'; -import {HsCommonEndpointsService} from 'hslayers-ng/services/endpoints'; import {HsLanguageService} from 'hslayers-ng/services/language'; import {HsLogService} from 'hslayers-ng/services/log'; import {HsMapService} from 'hslayers-ng/services/map'; import {HsSaverService} from './saver-service.interface'; import {HsToastService} from 'hslayers-ng/common/toast'; -import {HsUtilsService} from 'hslayers-ng/services/utils'; import { createGetFeatureRequest, createPostFeatureRequest, @@ -76,65 +73,23 @@ export class HsLaymanService implements HsSaverService { laymanLayerPending: Subject = new Subject(); totalProgress = 0; deleteQuery: Subscription; - supportedCRRList: string[] = SUPPORTED_SRS_LIST; + supportedCRRList = computed(() => { + const laymanEP = this.hsCommonLaymanService.layman(); + if (laymanEP) { + return getSupportedSrsList(laymanEP); + } + return SUPPORTED_SRS_LIST; + }); pendingRequests: Map> = new Map(); constructor( - private hsUtilsService: HsUtilsService, private http: HttpClient, private hsMapService: HsMapService, private hsLogService: HsLogService, - private hsCommonEndpointsService: HsCommonEndpointsService, private hsToastService: HsToastService, private hsLanguageService: HsLanguageService, private hsCommonLaymanService: HsCommonLaymanService, - ) { - this.hsCommonEndpointsService.endpointsFilled.subscribe( - async (endpoints) => { - if (endpoints) { - const laymanEP = endpoints.find((ep) => ep.type.includes('layman')); - if (laymanEP) { - const laymanVersion: AboutLayman = await lastValueFrom( - this.http - .get(laymanEP.url + '/rest/about/version') - .pipe( - map((res: any) => { - return { - about: { - applications: { - layman: { - version: res.about.applications.layman.version, - releaseTimestamp: - res.about.applications.layman[ - 'release-timestamp' - ], - }, - laymanTestClient: { - version: - res.about.applications['layman-test-client'] - .version, - }, - }, - data: { - layman: { - lastSchemaMigration: - res.about.applications['last-schema-migration'], - lastDataMigration: - res.about.applications['last-data-migration'], - }, - }, - }, - }; - }), - ), - ); - laymanEP.version = laymanVersion.about.applications.layman.version; - this.supportedCRRList = getSupportedSrsList(laymanEP); - } - } - }, - ); - } + ) {} /** * Update composition's access rights @@ -148,14 +103,14 @@ export class HsLaymanService implements HsSaverService { endpoint: HsEndpoint, access_rights: AccessRightsModel, ): Promise { - const rights = this.parseAccessRightsForLayman(endpoint, access_rights); + const rights = this.parseAccessRightsForLayman(access_rights); const formdata = new FormData(); formdata.append('name', compName); formdata.append('access_rights.read', rights.read); formdata.append('access_rights.write', rights.write); return await this.makeMapPostPatchRequest( endpoint, - endpoint.user, + this.hsCommonLaymanService.user(), compName, formdata, false, @@ -175,10 +130,7 @@ export class HsLaymanService implements HsSaverService { compoData: CompoData, saveAsNew: boolean, ): Promise { - const rights = this.parseAccessRightsForLayman( - endpoint, - compoData.access_rights, - ); + const rights = this.parseAccessRightsForLayman(compoData.access_rights); const formdata = new FormData(); formdata.append( 'file', @@ -192,11 +144,12 @@ export class HsLaymanService implements HsSaverService { formdata.append('access_rights.read', rights.read); formdata.append('access_rights.write', rights.write); + const user = this.hsCommonLaymanService.user(); const workspace = - compoData.workspace === endpoint.user - ? endpoint.user + compoData.workspace === user + ? user : saveAsNew - ? endpoint.user + ? user : compoData.workspace; return await this.makeMapPostPatchRequest( @@ -215,20 +168,18 @@ export class HsLaymanService implements HsSaverService { * @param access_rights - Provided access rights * @returns Access rights object as two strings, one for read access and the other for write access */ - parseAccessRightsForLayman( - endpoint: HsEndpoint, - access_rights: AccessRightsModel, - ): { + parseAccessRightsForLayman(access_rights: AccessRightsModel): { write: string; read: string; } { + const user = this.hsCommonLaymanService.user(); const write = access_rights['access_rights.write'] == 'private' - ? endpoint.user + ? user : access_rights['access_rights.write']; const read = access_rights['access_rights.read'] == 'private' - ? endpoint.user + ? user : access_rights['access_rights.read']; return {write, read}; } @@ -346,10 +297,7 @@ export class HsLaymanService implements HsSaverService { formData.append('crs', description.crs); } if (description.access_rights) { - const rights = this.parseAccessRightsForLayman( - endpoint, - description.access_rights, - ); + const rights = this.parseAccessRightsForLayman(description.access_rights); formData.append('access_rights.write', rights.write); formData.append('access_rights.read', rights.read); @@ -406,7 +354,8 @@ export class HsLaymanService implements HsSaverService { layerName = getLaymanFriendlyLayerName(layerName); try { const postOrPatch = overwrite ? 'patch' : 'post'; - const url = `${endpoint.url}/rest/workspaces/${endpoint.user}/layers${ + const workspace = this.hsCommonLaymanService.user(); + const url = `${endpoint.url}/rest/workspaces/${workspace}/layers${ overwrite ? `/${layerName}` : `?${Math.random()}` }`; let data: PostPatchLayerResponse = await lastValueFrom( @@ -503,8 +452,9 @@ export class HsLaymanService implements HsSaverService { ), ); const layername = data['name']; + const workspace = this.hsCommonLaymanService.user(); const resumable = new Resumable({ - target: `${endpoint.url}/rest/workspaces/${endpoint.user}/layers/${layername}/chunk`, + target: `${endpoint.url}/rest/workspaces/${workspace}/layers/${layername}/chunk`, query: { 'layman_original_parameter': 'file', }, @@ -559,7 +509,7 @@ export class HsLaymanService implements HsSaverService { } const layerName = getLayerName(layer); let layerTitle = getTitle(layer); - const crsSupported = this.supportedCRRList.includes(this.crs); + const crsSupported = this.supportedCRRList().includes(this.crs); if ((endpoint?.version?.split('.').join() as unknown as number) < 171) { layerTitle = getLaymanFriendlyLayerName(layerTitle); @@ -641,7 +591,7 @@ export class HsLaymanService implements HsSaverService { try { if (!desc) { desc = await this.describeLayer(endpoint, name, getWorkspace(layer)); - this.cacheLaymanDescriptor(layer, desc, endpoint); + this.cacheLaymanDescriptor(layer, desc); } if (desc.name == undefined || desc.wfs.url == undefined) { throw `Layer or its name/url didn't exist`; @@ -679,11 +629,12 @@ export class HsLaymanService implements HsSaverService { try { const srsName = this.hsMapService.getCurrentProj().getCode(); const featureType = getLayerName(layer); + const user = this.hsCommonLaymanService.user(); const {default: WFS} = await import('ol/format/WFS'); const wfsFormat = new WFS(); const options = { - featureNS: 'http://' + ep.user, - featurePrefix: ep.user, + featureNS: 'http://' + user, + featurePrefix: user, featureType, srsName, nativeElements: null, @@ -718,14 +669,12 @@ export class HsLaymanService implements HsSaverService { * Cache Layman's layer descriptor so it can be used later on * @param layer - Layer interacted with * @param layer - Layman's layer descriptor - * @param endpoint - Layman's endpoint description */ private cacheLaymanDescriptor( layer: VectorLayer>, desc: HsLaymanLayerDescriptor, - endpoint: HsEndpoint, ): void { - if (endpoint.user != 'browser') { + if (this.hsCommonLaymanService.user() != 'browser') { setLaymanLayerDescriptor(layer, desc); } } @@ -756,7 +705,7 @@ export class HsLaymanService implements HsSaverService { return null; } if (desc?.name && !wfsNotAvailable(desc)) { - this.cacheLaymanDescriptor(layer, desc, endpoint); + this.cacheLaymanDescriptor(layer, desc); } } catch (ex) { //If Layman returned 404 @@ -948,14 +897,15 @@ export class HsLaymanService implements HsSaverService { } const observables: Observable[] = []; - const ds = this.hsCommonLaymanService.layman; + const ds = this.hsCommonLaymanService.layman(); + const workspace = this.hsCommonLaymanService.user(); let url; if (layer) { const layerName = typeof layer == 'string' ? layer : getLayerName(layer); - url = `${ds.url}/rest/workspaces/${ds.user}/layers/${layerName}`; + url = `${ds.url}/rest/workspaces/${workspace}/layers/${layerName}`; } else { - url = `${ds.url}/rest/workspaces/${ds.user}/layers`; + url = `${ds.url}/rest/workspaces/${workspace}/layers`; } const response = this.http diff --git a/projects/hslayers/services/styler/styler.service.ts b/projects/hslayers/services/styler/styler.service.ts index fc13baf87d..f43783dd53 100644 --- a/projects/hslayers/services/styler/styler.service.ts +++ b/projects/hslayers/services/styler/styler.service.ts @@ -65,7 +65,7 @@ export class HsStylerService { sld: string; qml: string; - isAuthenticated: boolean; + isAuthenticated = this.hsCommonLaymanService.isAuthenticated; pin_white_blue; pin_white_blue_highlight; @@ -120,10 +120,6 @@ export class HsStylerService { ]; }; - this.hsCommonLaymanService.authChange.subscribe((endpoint) => { - this.isAuthenticated = endpoint?.authenticated; - }); - this.hsMapService.loaded().then((map) => { for (const layer of this.hsMapService .getLayersArray() @@ -755,7 +751,7 @@ export class HsStylerService { * Indicate only when user is logged in Layman and layer is being monitored otherwise save */ resolveSldChange() { - if (this.isAuthenticated && this.layerBeingMonitored) { + if (this.isAuthenticated() && this.layerBeingMonitored) { this.changesStore.set(getUid(this.layer), { sld: this.sld, qml: this.qml, diff --git a/projects/hslayers/services/utils/utils.service.ts b/projects/hslayers/services/utils/utils.service.ts index e0ea884157..b460b9cc9d 100644 --- a/projects/hslayers/services/utils/utils.service.ts +++ b/projects/hslayers/services/utils/utils.service.ts @@ -1,5 +1,5 @@ import {HttpClient} from '@angular/common/http'; -import {Inject, Injectable, PLATFORM_ID} from '@angular/core'; +import {Inject, Injectable, Injector, PLATFORM_ID} from '@angular/core'; import {isPlatformBrowser} from '@angular/common'; import {LineString, Polygon} from 'ol/geom'; @@ -7,8 +7,7 @@ import {ProjectionLike, get as getProjection, transform} from 'ol/proj'; import {getArea, getDistance} from 'ol/sphere'; import {lastValueFrom} from 'rxjs'; -import {BoundingBoxObject} from 'hslayers-ng/types'; -import {HsCommonLaymanService} from 'hslayers-ng/common/layman'; +import {BoundingBoxObject, HsEndpoint} from 'hslayers-ng/types'; import {HsConfig} from 'hslayers-ng/config'; import {HsLogService} from 'hslayers-ng/services/log'; @@ -22,30 +21,45 @@ export type Measurement = { providedIn: 'root', }) export class HsUtilsService { + private laymanUrl: string; + constructor( public hsConfig: HsConfig, private http: HttpClient, private LogService: HsLogService, - private hsCommonLaymanService: HsCommonLaymanService, + private injector: Injector, @Inject(PLATFORM_ID) private platformId: any, ) {} + /** + * Register Layman endpoints to avoid proxifying them + * @param endpoints - Layman endpoints to register + */ + registerLaymanEndpoints(url: string): void { + this.laymanUrl = url; + } + /** * Proxify URL if enabled. * @param url - URL to proxify * @returns Encoded URL with path to hslayers-server proxy */ proxify(url: string): string { - const laymanEp = this.hsCommonLaymanService.layman; - if ( - url.startsWith(this.hsConfig.proxyPrefix) || - (laymanEp && url.startsWith(laymanEp.url)) - ) { + // Don't proxify if it's already proxified + if (url.startsWith(this.hsConfig.proxyPrefix)) { return url; } + + // Don't proxify data URLs if (url.startsWith('data:application')) { return url; } + + // Don't proxify Layman endpoints + if (url.startsWith(this.laymanUrl)) { + return url; + } + let outUrl = url; //Not using location because don't know if port 80 was specified explicitly or not const windowUrlPosition = url.indexOf(window.location.origin); diff --git a/projects/hslayers/types/authentication.ts b/projects/hslayers/types/authentication.ts new file mode 100644 index 0000000000..1dfae2cfd1 --- /dev/null +++ b/projects/hslayers/types/authentication.ts @@ -0,0 +1,8 @@ +export interface AuthState { + user?: string; + authenticated?: boolean; +} + +export interface AuthAction { + type: 'login' | 'logout'; +} diff --git a/projects/hslayers/types/endpoint.interface.ts b/projects/hslayers/types/endpoint.interface.ts index d003046519..13793a27c7 100644 --- a/projects/hslayers/types/endpoint.interface.ts +++ b/projects/hslayers/types/endpoint.interface.ts @@ -20,8 +20,6 @@ export interface HsEndpoint { language?; listLoading?; layers?: HsAddDataLayerDescriptor[]; - user?: string; - authenticated?: boolean; code_list_url?: string; code_lists?; version?: string; @@ -53,7 +51,6 @@ export interface HsEndpoint { compositionLoad?: EndpointErrorHandling | EndpointErrorHandler; addDataCatalogueLoad?: EndpointErrorHandling | EndpointErrorHandler; }; - getCurrentUserIfNeeded?(endpoint: HsEndpoint): Promise; } function isErrorHandlerFunction(object: any): object is EndpointErrorHandler {