@ -38,11 +38,11 @@ import {
MatSlideToggleModule
MatSlideToggleModule
} from '@angular/material/slide-toggle' ;
} from '@angular/material/slide-toggle' ;
import { MatSnackBar } from '@angular/material/snack-bar' ;
import { MatSnackBar } from '@angular/material/snack-bar' ;
import { RouterModule } from '@angular/router' ;
import { ActivatedRoute , RouterModule } from '@angular/router' ;
import { IonIcon } from '@ionic/angular/standalone' ;
import { IonIcon } from '@ionic/angular/standalone' ;
import { format , parseISO } from 'date-fns' ;
import { format , parseISO } from 'date-fns' ;
import { addIcons } from 'ionicons' ;
import { addIcons } from 'ionicons' ;
import { eyeOffOutline , eyeOutline } from 'ionicons/icons' ;
import { eyeOffOutline , eyeOutline , linkOutline } from 'ionicons/icons' ;
import ms from 'ms' ;
import ms from 'ms' ;
import { EMPTY , Subject , throwError } from 'rxjs' ;
import { EMPTY , Subject , throwError } from 'rxjs' ;
import { catchError , takeUntil } from 'rxjs/operators' ;
import { catchError , takeUntil } from 'rxjs/operators' ;
@ -69,10 +69,14 @@ import { catchError, takeUntil } from 'rxjs/operators';
export class GfUserAccountSettingsComponent implements OnDestroy , OnInit {
export class GfUserAccountSettingsComponent implements OnDestroy , OnInit {
public appearancePlaceholder = $localize ` Auto ` ;
public appearancePlaceholder = $localize ` Auto ` ;
public baseCurrency : string ;
public baseCurrency : string ;
public canLinkOidc = false ;
public currencies : string [ ] = [ ] ;
public currencies : string [ ] = [ ] ;
public deleteOwnUserForm = this . formBuilder . group ( {
public deleteOwnUserForm = this . formBuilder . group ( {
accessToken : [ '' , Validators . required ]
accessToken : [ '' , Validators . required ]
} ) ;
} ) ;
public hasOidcLinked = false ;
public hasPermissionForAuthOidc = false ;
public hasPermissionForAuthToken = false ;
public hasPermissionToDeleteOwnUser : boolean ;
public hasPermissionToDeleteOwnUser : boolean ;
public hasPermissionToUpdateViewMode : boolean ;
public hasPermissionToUpdateViewMode : boolean ;
public hasPermissionToUpdateUserSettings : boolean ;
public hasPermissionToUpdateUserSettings : boolean ;
@ -101,6 +105,7 @@ export class GfUserAccountSettingsComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject < void > ( ) ;
private unsubscribeSubject = new Subject < void > ( ) ;
public constructor (
public constructor (
private activatedRoute : ActivatedRoute ,
private changeDetectorRef : ChangeDetectorRef ,
private changeDetectorRef : ChangeDetectorRef ,
private dataService : DataService ,
private dataService : DataService ,
private formBuilder : FormBuilder ,
private formBuilder : FormBuilder ,
@ -111,17 +116,40 @@ export class GfUserAccountSettingsComponent implements OnDestroy, OnInit {
private userService : UserService ,
private userService : UserService ,
public webAuthnService : WebAuthnService
public webAuthnService : WebAuthnService
) {
) {
const { baseCurrency , currencies } = this . dataService . fetchInfo ( ) ;
const { baseCurrency , currencies , globalPermissions } =
this . dataService . fetchInfo ( ) ;
this . baseCurrency = baseCurrency ;
this . baseCurrency = baseCurrency ;
this . currencies = currencies ;
this . currencies = currencies ;
this . hasPermissionForAuthOidc = hasPermission (
globalPermissions ,
permissions . enableAuthOidc
) ;
this . hasPermissionForAuthToken = hasPermission (
globalPermissions ,
permissions . enableAuthToken
) ;
this . userService . stateChanged
this . userService . stateChanged
. pipe ( takeUntil ( this . unsubscribeSubject ) )
. pipe ( takeUntil ( this . unsubscribeSubject ) )
. subscribe ( ( state ) = > {
. subscribe ( ( state ) = > {
if ( state ? . user ) {
if ( state ? . user ) {
this . user = state . user ;
this . user = state . user ;
this . hasOidcLinked =
this . hasPermissionForAuthOidc &&
this . hasPermissionForAuthToken &&
this . user . provider === 'ANONYMOUS' &&
! ! this . user . thirdPartyId ;
this . canLinkOidc =
this . hasPermissionForAuthOidc &&
this . hasPermissionForAuthToken &&
this . user . provider === 'ANONYMOUS' &&
! this . user . thirdPartyId ;
this . hasPermissionToDeleteOwnUser = hasPermission (
this . hasPermissionToDeleteOwnUser = hasPermission (
this . user . permissions ,
this . user . permissions ,
permissions . deleteOwnUser
permissions . deleteOwnUser
@ -144,11 +172,55 @@ export class GfUserAccountSettingsComponent implements OnDestroy, OnInit {
}
}
} ) ;
} ) ;
addIcons ( { eyeOffOutline , eyeOutline } ) ;
addIcons ( { eyeOffOutline , eyeOutline , linkOutline } ) ;
}
}
public ngOnInit() {
public ngOnInit() {
this . update ( ) ;
this . update ( ) ;
// Handle query params for link results
this . activatedRoute . queryParams
. pipe ( takeUntil ( this . unsubscribeSubject ) )
. subscribe ( ( params ) = > {
if ( params [ 'linkSuccess' ] === 'true' ) {
this . snackBar . open (
$localize ` Your OIDC account has been successfully linked. ` ,
undefined ,
{ duration : ms ( '5 seconds' ) }
) ;
// Refresh user data
this . userService . get ( true ) . subscribe ( ) ;
} else if ( params [ 'linkError' ] ) {
let errorMessage = $localize ` Failed to link OIDC account. ` ;
switch ( params [ 'linkError' ] ) {
case 'already-linked' :
errorMessage = $localize ` This OIDC account is already linked to another user. ` ;
break ;
case 'invalid-session' :
errorMessage = $localize ` Your session is invalid. Please log in again. ` ;
break ;
case 'invalid-provider' :
errorMessage = $localize ` Only token-authenticated users can link OIDC. ` ;
break ;
}
this . snackBar . open ( errorMessage , undefined , {
duration : ms ( '5 seconds' )
} ) ;
}
} ) ;
}
public getAuthProviderDisplayName ( ) : string {
switch ( this . user ? . provider ) {
case 'ANONYMOUS' :
return 'Security Token' ;
case 'GOOGLE' :
return 'Google' ;
case 'OIDC' :
return 'OpenID Connect (OIDC)' ;
default :
return this . user ? . provider || 'Unknown' ;
}
}
}
public isCommunityLanguage() {
public isCommunityLanguage() {
@ -179,6 +251,38 @@ export class GfUserAccountSettingsComponent implements OnDestroy, OnInit {
} ) ;
} ) ;
}
}
public onLinkOidc() {
this . notificationService . confirm ( {
confirmFn : ( ) = > {
const token = this . tokenStorageService . getToken ( ) ;
if ( token ) {
const form = document . createElement ( 'form' ) ;
form . method = 'GET' ;
form . action = '../api/auth/oidc' ;
const input = document . createElement ( 'input' ) ;
input . type = 'hidden' ;
input . name = 'linkToken' ;
input . value = token ;
form . appendChild ( input ) ;
document . body . appendChild ( form ) ;
form . submit ( ) ;
} else {
this . snackBar . open (
$localize ` Unable to initiate linking. Please log in again. ` ,
undefined ,
{ duration : ms ( '3 seconds' ) }
) ;
}
} ,
confirmType : ConfirmationDialogType.Warn ,
discardLabel : $localize ` Cancel ` ,
title : $localize ` Link OIDC Provider ` ,
message : $localize ` This will link your current account to an OIDC provider. After linking, you will be able to sign in using both your Security Token and OIDC. This action cannot be undone. Do you want to continue? `
} ) ;
}
public onCloseAccount() {
public onCloseAccount() {
this . notificationService . confirm ( {
this . notificationService . confirm ( {
confirmFn : ( ) = > {
confirmFn : ( ) = > {
@ -353,7 +457,7 @@ export class GfUserAccountSettingsComponent implements OnDestroy, OnInit {
this . update ( ) ;
this . update ( ) ;
resolve ( ) ;
resolve ( ) ;
} ,
} ,
error : ( error ) = > {
error : ( error : Error ) = > {
reject ( error ) ;
reject ( error ) ;
}
}
} ) ;
} ) ;