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
- 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`
## 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 { HoldingDetailDialogParams } from './components/holding-detail-dialog/interfaces/interfaces';
import { ImpersonationStorageService } from './services/impersonation-storage.service';
import { TokenStorageService } from './services/token-storage.service';
import { UserService } from './services/user/user.service';
@Component({
@ -82,7 +81,6 @@ export class GfAppComponent implements OnDestroy, OnInit {
private route: ActivatedRoute,
private router: Router,
private title: Title,
private tokenStorageService: TokenStorageService,
private userService: UserService
) {
this.initializeTheme();
@ -236,12 +234,11 @@ export class GfAppComponent implements OnDestroy, OnInit {
}
public onCreateAccount() {
this.tokenStorageService.signOut();
this.userService.signOut();
}
public onSignOut() {
this.tokenStorageService.signOut();
this.userService.remove();
this.userService.signOut();
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 { GfUserDetailDialogComponent } from '@ghostfolio/client/components/user-detail-dialog/user-detail-dialog.component';
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 { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { ConfirmationDialogType } from '@ghostfolio/common/enums';
@ -106,7 +105,6 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
private notificationService: NotificationService,
private route: ActivatedRoute,
private router: Router,
private tokenStorageService: TokenStorageService,
private userService: UserService
) {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
@ -229,8 +227,7 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
this.notificationService.alert({
discardFn: () => {
if (aUserId === this.user.id) {
this.tokenStorageService.signOut();
this.userService.remove();
this.userService.signOut();
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 { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { CreateAccessDto } from '@ghostfolio/common/dtos';
import { ConfirmationDialogType } from '@ghostfolio/common/enums';
@ -76,7 +75,6 @@ export class GfUserAccountAccessComponent implements OnDestroy, OnInit {
private notificationService: NotificationService,
private route: ActivatedRoute,
private router: Router,
private tokenStorageService: TokenStorageService,
private userService: UserService
) {
const { globalPermissions } = this.dataService.fetchInfo();
@ -161,8 +159,7 @@ export class GfUserAccountAccessComponent implements OnDestroy, OnInit {
.subscribe(({ accessToken }) => {
this.notificationService.alert({
discardFn: () => {
this.tokenStorageService.signOut();
this.userService.remove();
this.userService.signOut();
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,
SettingsStorageService
} 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 { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { ConfirmationDialogType } from '@ghostfolio/common/enums';
@ -108,7 +107,6 @@ export class GfUserAccountSettingsComponent implements OnDestroy, OnInit {
private notificationService: NotificationService,
private settingsStorageService: SettingsStorageService,
private snackBar: MatSnackBar,
private tokenStorageService: TokenStorageService,
private userService: UserService,
public webAuthnService: WebAuthnService
) {
@ -198,8 +196,7 @@ export class GfUserAccountSettingsComponent implements OnDestroy, OnInit {
takeUntil(this.unsubscribeSubject)
)
.subscribe(() => {
this.tokenStorageService.signOut();
this.userService.remove();
this.userService.signOut();
document.location.href = `/${document.documentElement.lang}`;
});

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

@ -68,7 +68,7 @@ export class AuthGuard {
this.dataService
.putUserSetting({ language: document.documentElement.lang })
.subscribe(() => {
this.userService.remove();
this.userService.reset();
setTimeout(() => {
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 { InfoItem } from '@ghostfolio/common/interfaces';
import { internalRoutes, publicRoutes } from '@ghostfolio/common/routes/routes';
@ -32,8 +32,8 @@ export class HttpResponseInterceptor implements HttpInterceptor {
public constructor(
private dataService: DataService,
private router: Router,
private tokenStorageService: TokenStorageService,
private snackBar: MatSnackBar,
private userService: UserService,
private webAuthnService: WebAuthnService
) {
this.info = this.dataService.fetchInfo();
@ -115,7 +115,7 @@ export class HttpResponseInterceptor implements HttpInterceptor {
if (this.webAuthnService.isEnabled()) {
this.router.navigate(internalRoutes.webauthn.routerLink);
} 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 { UserService } from '@ghostfolio/client/services/user/user.service';
import { InfoItem, LineChartItem } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GfLogoComponent } from '@ghostfolio/ui/logo';
@ -42,11 +43,12 @@ export class GfRegisterPageComponent implements OnInit {
private deviceService: DeviceDetectorService,
private dialog: MatDialog,
private router: Router,
private tokenStorageService: TokenStorageService
private tokenStorageService: TokenStorageService,
private userService: UserService
) {
this.info = this.dataService.fetchInfo();
this.tokenStorageService.signOut();
this.userService.signOut();
}
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 { KEY_TOKEN } from './settings-storage.service';
import { UserService } from './user/user.service';
@Injectable({
providedIn: 'root'
})
export class TokenStorageService {
public constructor(
private userService: UserService,
private webAuthnService: WebAuthnService
) {}
public getToken(): string {
return (
window.sessionStorage.getItem(KEY_TOKEN) ||
@ -25,23 +17,7 @@ export class TokenStorageService {
if (staySignedIn) {
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()) {
this.webAuthnService.deregister().subscribe();
}
window.localStorage.clear();
window.sessionStorage.clear();
this.userService.remove();
if (utmSource) {
window.localStorage.setItem('utm_source', utmSource);
}
window.sessionStorage.setItem(KEY_TOKEN, token);
}
}

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 { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -26,7 +27,8 @@ export class UserService extends ObservableStore<UserStoreState> {
public constructor(
private deviceService: DeviceDetectorService,
private dialog: MatDialog,
private http: HttpClient
private http: HttpClient,
private webAuthnService: WebAuthnService
) {
super({ trackStateHistory: true });
@ -93,10 +95,40 @@ export class UserService extends ObservableStore<UserStoreState> {
return this.getFilters().length > 0;
}
public remove() {
public reset() {
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> {
return this.http.get<any>('/api/v1/user').pipe(
map((user) => {

Loading…
Cancel
Save