mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-07-30 07:44:54 +00:00
353 lines
11 KiB
TypeScript
353 lines
11 KiB
TypeScript
import { Clipboard } from '@angular/cdk/clipboard'
|
|
import { Component, OnInit, inject, signal } from '@angular/core'
|
|
import {
|
|
FormControl,
|
|
FormGroup,
|
|
FormsModule,
|
|
ReactiveFormsModule,
|
|
} from '@angular/forms'
|
|
import {
|
|
NgbAccordionModule,
|
|
NgbActiveModal,
|
|
NgbPopoverModule,
|
|
} from '@ng-bootstrap/ng-bootstrap'
|
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
|
import { takeUntil } from 'rxjs'
|
|
import {
|
|
SocialAccount,
|
|
SocialAccountProvider,
|
|
TotpSettings,
|
|
} from 'src/app/data/user-profile'
|
|
import { ProfileService } from 'src/app/services/profile.service'
|
|
import { ToastService } from 'src/app/services/toast.service'
|
|
import { setLocationHref } from 'src/app/utils/navigation'
|
|
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
|
import { ConfirmButtonComponent } from '../confirm-button/confirm-button.component'
|
|
import { PasswordComponent } from '../input/password/password.component'
|
|
import { TextComponent } from '../input/text/text.component'
|
|
|
|
@Component({
|
|
selector: 'pngx-profile-edit-dialog',
|
|
templateUrl: './profile-edit-dialog.component.html',
|
|
styleUrls: ['./profile-edit-dialog.component.scss'],
|
|
imports: [
|
|
ConfirmButtonComponent,
|
|
TextComponent,
|
|
PasswordComponent,
|
|
FormsModule,
|
|
ReactiveFormsModule,
|
|
NgbAccordionModule,
|
|
NgbPopoverModule,
|
|
NgxBootstrapIconsModule,
|
|
],
|
|
})
|
|
export class ProfileEditDialogComponent
|
|
extends LoadingComponentWithPermissions
|
|
implements OnInit
|
|
{
|
|
private profileService = inject(ProfileService)
|
|
activeModal = inject(NgbActiveModal)
|
|
private toastService = inject(ToastService)
|
|
private clipboard = inject(Clipboard)
|
|
|
|
readonly networkActive = signal(false)
|
|
readonly error = signal<any>(undefined)
|
|
readonly showPasswordConfirm = signal(false)
|
|
readonly showEmailConfirm = signal(false)
|
|
readonly copied = signal(false)
|
|
readonly codesCopied = signal(false)
|
|
readonly hasUsablePassword = signal(false)
|
|
readonly isTotpEnabled = signal(false)
|
|
readonly totpSettings = signal<TotpSettings>(undefined)
|
|
readonly totpSettingsLoading = signal(false)
|
|
readonly totpLoading = signal(false)
|
|
readonly recoveryCodes = signal<string[]>(undefined)
|
|
readonly socialAccounts = signal<SocialAccount[]>([])
|
|
readonly socialAccountProviders = signal<SocialAccountProvider[]>([])
|
|
|
|
public form = new FormGroup({
|
|
email: new FormControl(''),
|
|
email_confirm: new FormControl({ value: null, disabled: true }),
|
|
password: new FormControl(null),
|
|
password_confirm: new FormControl({ value: null, disabled: true }),
|
|
first_name: new FormControl(''),
|
|
last_name: new FormControl(''),
|
|
auth_token: new FormControl(''),
|
|
totp_code: new FormControl(''),
|
|
})
|
|
|
|
private currentPassword: string
|
|
private newPassword: string
|
|
private passwordConfirm: string
|
|
|
|
private currentEmail: string
|
|
private newEmail: string
|
|
private emailConfirm: string
|
|
|
|
get qrSvgDataUrl(): string | null {
|
|
if (!this.totpSettings()?.qr_svg) {
|
|
return null
|
|
}
|
|
return `data:image/svg+xml;utf8,${encodeURIComponent(this.totpSettings().qr_svg)}`
|
|
}
|
|
|
|
ngOnInit(): void {
|
|
this.networkActive.set(true)
|
|
this.profileService
|
|
.get()
|
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
|
.subscribe((profile) => {
|
|
this.networkActive.set(false)
|
|
this.form.patchValue(profile)
|
|
this.currentEmail = profile.email
|
|
this.form.get('email').valueChanges.subscribe((newEmail) => {
|
|
this.newEmail = newEmail
|
|
this.onEmailChange()
|
|
})
|
|
this.currentPassword = profile.password
|
|
this.hasUsablePassword.set(profile.has_usable_password)
|
|
this.form.get('password').valueChanges.subscribe((newPassword) => {
|
|
this.newPassword = newPassword
|
|
this.onPasswordChange()
|
|
})
|
|
this.socialAccounts.set(profile.social_accounts ?? [])
|
|
this.isTotpEnabled.set(profile.is_mfa_enabled)
|
|
})
|
|
|
|
this.profileService
|
|
.getSocialAccountProviders()
|
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
|
.subscribe((providers) => {
|
|
this.socialAccountProviders.set(providers ?? [])
|
|
})
|
|
}
|
|
|
|
get saveDisabled(): boolean {
|
|
return this.error()?.password_confirm || this.error()?.email_confirm
|
|
}
|
|
|
|
onEmailKeyUp(event: KeyboardEvent): void {
|
|
this.newEmail = (event.target as HTMLInputElement)?.value
|
|
this.onEmailChange()
|
|
}
|
|
|
|
onEmailConfirmKeyUp(event: KeyboardEvent): void {
|
|
this.emailConfirm = (event.target as HTMLInputElement)?.value
|
|
this.onEmailChange()
|
|
}
|
|
|
|
onEmailChange(): void {
|
|
this.showEmailConfirm.set(this.currentEmail !== this.newEmail)
|
|
if (this.showEmailConfirm()) {
|
|
this.form.get('email_confirm').enable()
|
|
if (this.newEmail !== this.emailConfirm) {
|
|
this.setFieldError('email_confirm', $localize`Emails must match`)
|
|
} else {
|
|
this.clearFieldError('email_confirm')
|
|
}
|
|
} else {
|
|
this.form.get('email_confirm').disable()
|
|
this.clearFieldError('email_confirm')
|
|
}
|
|
}
|
|
|
|
onPasswordKeyUp(event: KeyboardEvent): void {
|
|
if ((event.target as HTMLElement).tagName !== 'input') return // toggle button can trigger this handler
|
|
this.newPassword = (event.target as HTMLInputElement)?.value
|
|
this.onPasswordChange()
|
|
}
|
|
|
|
onPasswordConfirmKeyUp(event: KeyboardEvent): void {
|
|
this.passwordConfirm = (event.target as HTMLInputElement)?.value
|
|
this.onPasswordChange()
|
|
}
|
|
|
|
onPasswordChange(): void {
|
|
this.showPasswordConfirm.set(this.currentPassword !== this.newPassword)
|
|
|
|
if (this.showPasswordConfirm()) {
|
|
this.form.get('password_confirm').enable()
|
|
if (this.newPassword !== this.passwordConfirm) {
|
|
this.setFieldError('password_confirm', $localize`Passwords must match`)
|
|
} else {
|
|
this.clearFieldError('password_confirm')
|
|
}
|
|
} else {
|
|
this.form.get('password_confirm').disable()
|
|
this.clearFieldError('password_confirm')
|
|
}
|
|
}
|
|
|
|
private setFieldError(fieldName: string, message: string): void {
|
|
this.error.set({
|
|
...this.error(),
|
|
[fieldName]: message,
|
|
})
|
|
}
|
|
|
|
private clearFieldError(fieldName: string): void {
|
|
if (!this.error()) return
|
|
const error = { ...this.error() }
|
|
delete error[fieldName]
|
|
this.error.set(Object.keys(error).length ? error : undefined)
|
|
}
|
|
|
|
save(): void {
|
|
const passwordChanged =
|
|
this.newPassword && this.currentPassword !== this.newPassword
|
|
const profile = Object.assign({}, this.form.value)
|
|
delete profile.totp_code
|
|
this.error.set(null)
|
|
this.networkActive.set(true)
|
|
this.profileService
|
|
.update(profile)
|
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
|
.subscribe({
|
|
next: () => {
|
|
this.toastService.showInfo($localize`Profile updated successfully`)
|
|
if (passwordChanged) {
|
|
this.toastService.showInfo(
|
|
$localize`Password has been changed, you will be logged out momentarily.`
|
|
)
|
|
setTimeout(() => {
|
|
setLocationHref(
|
|
`${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
|
|
)
|
|
}, 2500)
|
|
}
|
|
this.activeModal.close()
|
|
},
|
|
error: (error) => {
|
|
this.toastService.showError($localize`Error saving profile`, error)
|
|
this.error.set(error?.error)
|
|
this.networkActive.set(false)
|
|
},
|
|
})
|
|
}
|
|
|
|
cancel(): void {
|
|
this.activeModal.close()
|
|
}
|
|
|
|
generateAuthToken(): void {
|
|
this.profileService.generateAuthToken().subscribe({
|
|
next: (token: string) => {
|
|
this.form.patchValue({ auth_token: token })
|
|
},
|
|
error: (error) => {
|
|
this.toastService.showError(
|
|
$localize`Error generating auth token`,
|
|
error
|
|
)
|
|
},
|
|
})
|
|
}
|
|
|
|
copyAuthToken(): void {
|
|
this.clipboard.copy(this.form.get('auth_token').value)
|
|
this.copied.set(true)
|
|
setTimeout(() => {
|
|
this.copied.set(false)
|
|
}, 3000)
|
|
}
|
|
|
|
disconnectSocialAccount(id: number): void {
|
|
this.profileService
|
|
.disconnectSocialAccount(id)
|
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
|
.subscribe({
|
|
next: (id: number) => {
|
|
this.socialAccounts.update((accounts) =>
|
|
accounts.filter((account) => account.id != id)
|
|
)
|
|
},
|
|
error: (error) => {
|
|
this.toastService.showError(
|
|
$localize`Error disconnecting social account`,
|
|
error
|
|
)
|
|
},
|
|
})
|
|
}
|
|
|
|
public gettotpSettings(): void {
|
|
this.totpSettingsLoading.set(true)
|
|
this.profileService
|
|
.getTotpSettings()
|
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
|
.subscribe({
|
|
next: (totpSettings) => {
|
|
this.totpSettingsLoading.set(false)
|
|
this.totpSettings.set(totpSettings)
|
|
},
|
|
error: (error) => {
|
|
this.toastService.showError(
|
|
$localize`Error fetching TOTP settings`,
|
|
error
|
|
)
|
|
this.totpSettingsLoading.set(false)
|
|
},
|
|
})
|
|
}
|
|
|
|
public activateTotp(): void {
|
|
this.totpLoading.set(true)
|
|
this.form.get('totp_code').disable()
|
|
this.profileService
|
|
.activateTotp(
|
|
this.totpSettings().secret,
|
|
this.form.get('totp_code').value
|
|
)
|
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
|
.subscribe({
|
|
next: (activationResponse) => {
|
|
this.totpLoading.set(false)
|
|
this.isTotpEnabled.set(activationResponse.success)
|
|
this.recoveryCodes.set(activationResponse.recovery_codes)
|
|
this.form.get('totp_code').enable()
|
|
if (activationResponse.success) {
|
|
this.toastService.showInfo($localize`TOTP activated successfully`)
|
|
} else {
|
|
this.toastService.showError($localize`Error activating TOTP`)
|
|
}
|
|
},
|
|
error: (error) => {
|
|
this.totpLoading.set(false)
|
|
this.form.get('totp_code').enable()
|
|
this.toastService.showError($localize`Error activating TOTP`, error)
|
|
},
|
|
})
|
|
}
|
|
|
|
public deactivateTotp(): void {
|
|
this.totpLoading.set(true)
|
|
this.profileService
|
|
.deactivateTotp()
|
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
|
.subscribe({
|
|
next: (success) => {
|
|
this.totpLoading.set(false)
|
|
this.isTotpEnabled.set(!success)
|
|
this.recoveryCodes.set(null)
|
|
if (success) {
|
|
this.toastService.showInfo($localize`TOTP deactivated successfully`)
|
|
} else {
|
|
this.toastService.showError($localize`Error deactivating TOTP`)
|
|
}
|
|
},
|
|
error: (error) => {
|
|
this.totpLoading.set(false)
|
|
this.toastService.showError($localize`Error deactivating TOTP`, error)
|
|
},
|
|
})
|
|
}
|
|
|
|
public copyRecoveryCodes(): void {
|
|
this.clipboard.copy(this.recoveryCodes().join('\n'))
|
|
this.codesCopied.set(true)
|
|
setTimeout(() => {
|
|
this.codesCopied.set(false)
|
|
}, 3000)
|
|
}
|
|
}
|