Browse Source

Task/consolidate sign-out logic (#6526)

* Consolidate sign-out logic

* Update changelog
pull/6523/head^2
Thomas Kaul 1 week ago
committed by GitHub
parent
commit
439af5f21d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 7
      apps/client/src/app/app.component.ts
  3. 5
      apps/client/src/app/components/admin-users/admin-users.component.ts
  4. 5
      apps/client/src/app/components/user-account-access/user-account-access.component.ts
  5. 5
      apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
  6. 2
      apps/client/src/app/core/auth.guard.ts
  7. 6
      apps/client/src/app/core/http-response.interceptor.ts
  8. 6
      apps/client/src/app/pages/register/register-page.component.ts
  9. 26
      apps/client/src/app/services/token-storage.service.ts
  10. 36
      apps/client/src/app/services/user/user.service.ts

1
CHANGELOG.md

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Consolidated the sign-out logic within the user service to unify cookie, state and token clearance
- Upgraded `svgmap` from version `2.14.0` to `2.19.2` - Upgraded `svgmap` from version `2.14.0` to `2.19.2`
## 2.249.0 - 2026-03-10 ## 2.249.0 - 2026-03-10

7
apps/client/src/app/app.component.ts

@ -38,7 +38,6 @@ import { GfHeaderComponent } from './components/header/header.component';
import { GfHoldingDetailDialogComponent } from './components/holding-detail-dialog/holding-detail-dialog.component'; import { GfHoldingDetailDialogComponent } from './components/holding-detail-dialog/holding-detail-dialog.component';
import { HoldingDetailDialogParams } from './components/holding-detail-dialog/interfaces/interfaces'; import { HoldingDetailDialogParams } from './components/holding-detail-dialog/interfaces/interfaces';
import { ImpersonationStorageService } from './services/impersonation-storage.service'; import { ImpersonationStorageService } from './services/impersonation-storage.service';
import { TokenStorageService } from './services/token-storage.service';
import { UserService } from './services/user/user.service'; import { UserService } from './services/user/user.service';
@Component({ @Component({
@ -82,7 +81,6 @@ export class GfAppComponent implements OnDestroy, OnInit {
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private title: Title, private title: Title,
private tokenStorageService: TokenStorageService,
private userService: UserService private userService: UserService
) { ) {
this.initializeTheme(); this.initializeTheme();
@ -236,12 +234,11 @@ export class GfAppComponent implements OnDestroy, OnInit {
} }
public onCreateAccount() { public onCreateAccount() {
this.tokenStorageService.signOut(); this.userService.signOut();
} }
public onSignOut() { public onSignOut() {
this.tokenStorageService.signOut(); this.userService.signOut();
this.userService.remove();
document.location.href = `/${document.documentElement.lang}`; document.location.href = `/${document.documentElement.lang}`;
} }

5
apps/client/src/app/components/admin-users/admin-users.component.ts

@ -1,7 +1,6 @@
import { UserDetailDialogParams } from '@ghostfolio/client/components/user-detail-dialog/interfaces/interfaces'; import { UserDetailDialogParams } from '@ghostfolio/client/components/user-detail-dialog/interfaces/interfaces';
import { GfUserDetailDialogComponent } from '@ghostfolio/client/components/user-detail-dialog/user-detail-dialog.component'; import { GfUserDetailDialogComponent } from '@ghostfolio/client/components/user-detail-dialog/user-detail-dialog.component';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { ConfirmationDialogType } from '@ghostfolio/common/enums';
@ -106,7 +105,6 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
private notificationService: NotificationService, private notificationService: NotificationService,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private tokenStorageService: TokenStorageService,
private userService: UserService private userService: UserService
) { ) {
this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.deviceType = this.deviceService.getDeviceInfo().deviceType;
@ -229,8 +227,7 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
this.notificationService.alert({ this.notificationService.alert({
discardFn: () => { discardFn: () => {
if (aUserId === this.user.id) { if (aUserId === this.user.id) {
this.tokenStorageService.signOut(); this.userService.signOut();
this.userService.remove();
document.location.href = `/${document.documentElement.lang}`; document.location.href = `/${document.documentElement.lang}`;
} }

5
apps/client/src/app/components/user-account-access/user-account-access.component.ts

@ -1,5 +1,4 @@
import { GfAccessTableComponent } from '@ghostfolio/client/components/access-table/access-table.component'; import { GfAccessTableComponent } from '@ghostfolio/client/components/access-table/access-table.component';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { CreateAccessDto } from '@ghostfolio/common/dtos'; import { CreateAccessDto } from '@ghostfolio/common/dtos';
import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { ConfirmationDialogType } from '@ghostfolio/common/enums';
@ -76,7 +75,6 @@ export class GfUserAccountAccessComponent implements OnDestroy, OnInit {
private notificationService: NotificationService, private notificationService: NotificationService,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private tokenStorageService: TokenStorageService,
private userService: UserService private userService: UserService
) { ) {
const { globalPermissions } = this.dataService.fetchInfo(); const { globalPermissions } = this.dataService.fetchInfo();
@ -161,8 +159,7 @@ export class GfUserAccountAccessComponent implements OnDestroy, OnInit {
.subscribe(({ accessToken }) => { .subscribe(({ accessToken }) => {
this.notificationService.alert({ this.notificationService.alert({
discardFn: () => { discardFn: () => {
this.tokenStorageService.signOut(); this.userService.signOut();
this.userService.remove();
document.location.href = `/${document.documentElement.lang}`; document.location.href = `/${document.documentElement.lang}`;
}, },

5
apps/client/src/app/components/user-account-settings/user-account-settings.component.ts

@ -3,7 +3,6 @@ import {
KEY_TOKEN, KEY_TOKEN,
SettingsStorageService SettingsStorageService
} from '@ghostfolio/client/services/settings-storage.service'; } from '@ghostfolio/client/services/settings-storage.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service'; import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { ConfirmationDialogType } from '@ghostfolio/common/enums';
@ -108,7 +107,6 @@ export class GfUserAccountSettingsComponent implements OnDestroy, OnInit {
private notificationService: NotificationService, private notificationService: NotificationService,
private settingsStorageService: SettingsStorageService, private settingsStorageService: SettingsStorageService,
private snackBar: MatSnackBar, private snackBar: MatSnackBar,
private tokenStorageService: TokenStorageService,
private userService: UserService, private userService: UserService,
public webAuthnService: WebAuthnService public webAuthnService: WebAuthnService
) { ) {
@ -198,8 +196,7 @@ export class GfUserAccountSettingsComponent implements OnDestroy, OnInit {
takeUntil(this.unsubscribeSubject) takeUntil(this.unsubscribeSubject)
) )
.subscribe(() => { .subscribe(() => {
this.tokenStorageService.signOut(); this.userService.signOut();
this.userService.remove();
document.location.href = `/${document.documentElement.lang}`; document.location.href = `/${document.documentElement.lang}`;
}); });

2
apps/client/src/app/core/auth.guard.ts

@ -68,7 +68,7 @@ export class AuthGuard {
this.dataService this.dataService
.putUserSetting({ language: document.documentElement.lang }) .putUserSetting({ language: document.documentElement.lang })
.subscribe(() => { .subscribe(() => {
this.userService.remove(); this.userService.reset();
setTimeout(() => { setTimeout(() => {
window.location.reload(); window.location.reload();

6
apps/client/src/app/core/http-response.interceptor.ts

@ -1,4 +1,4 @@
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service'; import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { InfoItem } from '@ghostfolio/common/interfaces'; import { InfoItem } from '@ghostfolio/common/interfaces';
import { internalRoutes, publicRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes, publicRoutes } from '@ghostfolio/common/routes/routes';
@ -32,8 +32,8 @@ export class HttpResponseInterceptor implements HttpInterceptor {
public constructor( public constructor(
private dataService: DataService, private dataService: DataService,
private router: Router, private router: Router,
private tokenStorageService: TokenStorageService,
private snackBar: MatSnackBar, private snackBar: MatSnackBar,
private userService: UserService,
private webAuthnService: WebAuthnService private webAuthnService: WebAuthnService
) { ) {
this.info = this.dataService.fetchInfo(); this.info = this.dataService.fetchInfo();
@ -115,7 +115,7 @@ export class HttpResponseInterceptor implements HttpInterceptor {
if (this.webAuthnService.isEnabled()) { if (this.webAuthnService.isEnabled()) {
this.router.navigate(internalRoutes.webauthn.routerLink); this.router.navigate(internalRoutes.webauthn.routerLink);
} else { } else {
this.tokenStorageService.signOut(); this.userService.signOut();
} }
} }
} }

6
apps/client/src/app/pages/register/register-page.component.ts

@ -1,4 +1,5 @@
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { InfoItem, LineChartItem } from '@ghostfolio/common/interfaces'; import { InfoItem, LineChartItem } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GfLogoComponent } from '@ghostfolio/ui/logo'; import { GfLogoComponent } from '@ghostfolio/ui/logo';
@ -42,11 +43,12 @@ export class GfRegisterPageComponent implements OnInit {
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private dialog: MatDialog, private dialog: MatDialog,
private router: Router, private router: Router,
private tokenStorageService: TokenStorageService private tokenStorageService: TokenStorageService,
private userService: UserService
) { ) {
this.info = this.dataService.fetchInfo(); this.info = this.dataService.fetchInfo();
this.tokenStorageService.signOut(); this.userService.signOut();
} }
public ngOnInit() { public ngOnInit() {

26
apps/client/src/app/services/token-storage.service.ts

@ -1,19 +1,11 @@
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { KEY_TOKEN } from './settings-storage.service'; import { KEY_TOKEN } from './settings-storage.service';
import { UserService } from './user/user.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class TokenStorageService { export class TokenStorageService {
public constructor(
private userService: UserService,
private webAuthnService: WebAuthnService
) {}
public getToken(): string { public getToken(): string {
return ( return (
window.sessionStorage.getItem(KEY_TOKEN) || window.sessionStorage.getItem(KEY_TOKEN) ||
@ -25,23 +17,7 @@ export class TokenStorageService {
if (staySignedIn) { if (staySignedIn) {
window.localStorage.setItem(KEY_TOKEN, token); window.localStorage.setItem(KEY_TOKEN, token);
} }
window.sessionStorage.setItem(KEY_TOKEN, token);
}
public signOut() {
const utmSource = window.localStorage.getItem('utm_source');
if (this.webAuthnService.isEnabled()) { window.sessionStorage.setItem(KEY_TOKEN, token);
this.webAuthnService.deregister().subscribe();
}
window.localStorage.clear();
window.sessionStorage.clear();
this.userService.remove();
if (utmSource) {
window.localStorage.setItem('utm_source', utmSource);
}
} }
} }

36
apps/client/src/app/services/user/user.service.ts

@ -1,3 +1,4 @@
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { Filter, User } from '@ghostfolio/common/interfaces'; import { Filter, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -26,7 +27,8 @@ export class UserService extends ObservableStore<UserStoreState> {
public constructor( public constructor(
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private dialog: MatDialog, private dialog: MatDialog,
private http: HttpClient private http: HttpClient,
private webAuthnService: WebAuthnService
) { ) {
super({ trackStateHistory: true }); super({ trackStateHistory: true });
@ -93,10 +95,40 @@ export class UserService extends ObservableStore<UserStoreState> {
return this.getFilters().length > 0; return this.getFilters().length > 0;
} }
public remove() { public reset() {
this.setState({ user: null }, UserStoreActions.RemoveUser); this.setState({ user: null }, UserStoreActions.RemoveUser);
} }
public signOut() {
const utmSource = window.localStorage.getItem('utm_source');
if (this.webAuthnService.isEnabled()) {
this.webAuthnService.deregister().subscribe();
}
window.localStorage.clear();
window.sessionStorage.clear();
void this.clearAllCookies();
this.reset();
if (utmSource) {
window.localStorage.setItem('utm_source', utmSource);
}
}
private async clearAllCookies() {
if (!('cookieStore' in window)) {
console.warn('Cookie Store API not available in this browser');
return;
}
const cookies = await cookieStore.getAll();
await Promise.all(cookies.map(({ name }) => cookieStore.delete(name)));
}
private fetchUser(): Observable<User> { private fetchUser(): Observable<User> {
return this.http.get<any>('/api/v1/user').pipe( return this.http.get<any>('/api/v1/user').pipe(
map((user) => { map((user) => {

Loading…
Cancel
Save