paperless-ngx/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts

334 lines
10 KiB
TypeScript
Raw Normal View History

2024-12-13 00:27:30 -08:00
import { Clipboard } from '@angular/cdk/clipboard'
2025-06-27 14:06:40 -07:00
import { Component, OnInit, inject } from '@angular/core'
2025-01-01 22:26:53 -08:00
import {
FormControl,
FormGroup,
FormsModule,
ReactiveFormsModule,
} from '@angular/forms'
import {
NgbAccordionModule,
NgbActiveModal,
NgbPopoverModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
2024-12-13 00:27:30 -08:00
import { takeUntil } from 'rxjs'
import {
SocialAccount,
SocialAccountProvider,
2024-12-13 00:27:30 -08:00
TotpSettings,
} from 'src/app/data/user-profile'
2025-01-01 22:26:53 -08:00
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
2024-12-13 00:27:30 -08:00
import { ProfileService } from 'src/app/services/profile.service'
2023-12-02 08:26:42 -08:00
import { ToastService } from 'src/app/services/toast.service'
Chore(deps-dev): Bump the frontend-jest-dependencies group in /src-ui with 4 updates (#10497) * Chore(deps-dev): Bump the frontend-jest-dependencies group Bumps the frontend-jest-dependencies group in /src-ui with 4 updates: [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest), [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest), [jest-environment-jsdom](https://github.com/jestjs/jest/tree/HEAD/packages/jest-environment-jsdom) and [jest-preset-angular](https://github.com/thymikee/jest-preset-angular). Updates `jest` from 29.7.0 to 30.0.5 - [Release notes](https://github.com/jestjs/jest/releases) - [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md) - [Commits](https://github.com/jestjs/jest/commits/v30.0.5/packages/jest) Updates `@types/jest` from 29.5.14 to 30.0.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest) Updates `jest-environment-jsdom` from 29.7.0 to 30.0.5 - [Release notes](https://github.com/jestjs/jest/releases) - [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md) - [Commits](https://github.com/jestjs/jest/commits/v30.0.5/packages/jest-environment-jsdom) Updates `jest-preset-angular` from 14.5.5 to 15.0.0 - [Release notes](https://github.com/thymikee/jest-preset-angular/releases) - [Changelog](https://github.com/thymikee/jest-preset-angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/thymikee/jest-preset-angular/compare/v14.5.5...v15.0.0) --- updated-dependencies: - dependency-name: jest dependency-version: 30.0.5 dependency-type: direct:development update-type: version-update:semver-major dependency-group: frontend-jest-dependencies - dependency-name: "@types/jest" dependency-version: 30.0.0 dependency-type: direct:development update-type: version-update:semver-major dependency-group: frontend-jest-dependencies - dependency-name: jest-environment-jsdom dependency-version: 30.0.5 dependency-type: direct:development update-type: version-update:semver-major dependency-group: frontend-jest-dependencies - dependency-name: jest-preset-angular dependency-version: 15.0.0 dependency-type: direct:development update-type: version-update:semver-major dependency-group: frontend-jest-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> * Update Jest setup for Node util imports and typings * Refactor navigation actions to utility functions --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2025-08-02 03:43:31 +00:00
import { setLocationHref } from 'src/app/utils/navigation'
2024-12-06 00:26:38 -08:00
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
2025-01-01 22:26:53 -08:00
import { ConfirmButtonComponent } from '../confirm-button/confirm-button.component'
import { PasswordComponent } from '../input/password/password.component'
import { TextComponent } from '../input/text/text.component'
2023-12-02 08:26:42 -08:00
@Component({
selector: 'pngx-profile-edit-dialog',
templateUrl: './profile-edit-dialog.component.html',
styleUrls: ['./profile-edit-dialog.component.scss'],
2025-01-01 22:26:53 -08:00
imports: [
ConfirmButtonComponent,
TextComponent,
PasswordComponent,
FormsModule,
ReactiveFormsModule,
SafeHtmlPipe,
NgbAccordionModule,
NgbPopoverModule,
NgxBootstrapIconsModule,
],
2023-12-02 08:26:42 -08:00
})
2024-12-06 00:26:38 -08:00
export class ProfileEditDialogComponent
extends LoadingComponentWithPermissions
implements OnInit
{
2025-06-27 14:06:40 -07:00
private profileService = inject(ProfileService)
activeModal = inject(NgbActiveModal)
private toastService = inject(ToastService)
private clipboard = inject(Clipboard)
2023-12-02 08:26:42 -08:00
public networkActive: boolean = false
public error: any
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(''),
2023-12-02 08:26:42 -08:00
})
private currentPassword: string
private newPassword: string
private passwordConfirm: string
public showPasswordConfirm: boolean = false
public hasUsablePassword: boolean = false
2023-12-02 08:26:42 -08:00
private currentEmail: string
private newEmail: string
private emailConfirm: string
public showEmailConfirm: boolean = false
public isTotpEnabled: boolean = false
public totpSettings: TotpSettings
public totpSettingsLoading: boolean = false
public totpLoading: boolean = false
public recoveryCodes: string[]
2023-12-02 08:26:42 -08:00
public copied: boolean = false
public codesCopied: boolean = false
2023-12-02 08:26:42 -08:00
public socialAccounts: SocialAccount[] = []
public socialAccountProviders: SocialAccountProvider[] = []
2023-12-02 08:26:42 -08:00
ngOnInit(): void {
this.networkActive = true
this.profileService
.get()
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((profile) => {
this.networkActive = 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 = profile.has_usable_password
2023-12-02 08:26:42 -08:00
this.form.get('password').valueChanges.subscribe((newPassword) => {
this.newPassword = newPassword
this.onPasswordChange()
})
this.socialAccounts = profile.social_accounts
this.isTotpEnabled = profile.is_mfa_enabled
})
this.profileService
.getSocialAccountProviders()
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((providers) => {
this.socialAccountProviders = providers
2023-12-02 08:26:42 -08:00
})
}
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 = this.currentEmail !== this.newEmail
if (this.showEmailConfirm) {
this.form.get('email_confirm').enable()
if (this.newEmail !== this.emailConfirm) {
if (!this.error) this.error = {}
this.error.email_confirm = $localize`Emails must match`
} else {
delete this.error?.email_confirm
}
} else {
this.form.get('email_confirm').disable()
delete this.error?.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 = this.currentPassword !== this.newPassword
if (this.showPasswordConfirm) {
this.form.get('password_confirm').enable()
if (this.newPassword !== this.passwordConfirm) {
if (!this.error) this.error = {}
this.error.password_confirm = $localize`Passwords must match`
} else {
delete this.error?.password_confirm
}
} else {
this.form.get('password_confirm').disable()
delete this.error?.password_confirm
}
}
save(): void {
const passwordChanged =
this.newPassword && this.currentPassword !== this.newPassword
2023-12-02 08:26:42 -08:00
const profile = Object.assign({}, this.form.value)
delete profile.totp_code
this.error = null
2023-12-02 08:26:42 -08:00
this.networkActive = 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(() => {
Chore(deps-dev): Bump the frontend-jest-dependencies group in /src-ui with 4 updates (#10497) * Chore(deps-dev): Bump the frontend-jest-dependencies group Bumps the frontend-jest-dependencies group in /src-ui with 4 updates: [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest), [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest), [jest-environment-jsdom](https://github.com/jestjs/jest/tree/HEAD/packages/jest-environment-jsdom) and [jest-preset-angular](https://github.com/thymikee/jest-preset-angular). Updates `jest` from 29.7.0 to 30.0.5 - [Release notes](https://github.com/jestjs/jest/releases) - [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md) - [Commits](https://github.com/jestjs/jest/commits/v30.0.5/packages/jest) Updates `@types/jest` from 29.5.14 to 30.0.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest) Updates `jest-environment-jsdom` from 29.7.0 to 30.0.5 - [Release notes](https://github.com/jestjs/jest/releases) - [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md) - [Commits](https://github.com/jestjs/jest/commits/v30.0.5/packages/jest-environment-jsdom) Updates `jest-preset-angular` from 14.5.5 to 15.0.0 - [Release notes](https://github.com/thymikee/jest-preset-angular/releases) - [Changelog](https://github.com/thymikee/jest-preset-angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/thymikee/jest-preset-angular/compare/v14.5.5...v15.0.0) --- updated-dependencies: - dependency-name: jest dependency-version: 30.0.5 dependency-type: direct:development update-type: version-update:semver-major dependency-group: frontend-jest-dependencies - dependency-name: "@types/jest" dependency-version: 30.0.0 dependency-type: direct:development update-type: version-update:semver-major dependency-group: frontend-jest-dependencies - dependency-name: jest-environment-jsdom dependency-version: 30.0.5 dependency-type: direct:development update-type: version-update:semver-major dependency-group: frontend-jest-dependencies - dependency-name: jest-preset-angular dependency-version: 15.0.0 dependency-type: direct:development update-type: version-update:semver-major dependency-group: frontend-jest-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> * Update Jest setup for Node util imports and typings * Refactor navigation actions to utility functions --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2025-08-02 03:43:31 +00:00
setLocationHref(
`${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
)
2023-12-02 08:26:42 -08:00
}, 2500)
}
this.activeModal.close()
},
error: (error) => {
this.toastService.showError($localize`Error saving profile`, error)
this.error = error?.error
2023-12-02 08:26:42 -08:00
this.networkActive = 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 = true
setTimeout(() => {
this.copied = false
}, 3000)
}
disconnectSocialAccount(id: number): void {
this.profileService
.disconnectSocialAccount(id)
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (id: number) => {
this.socialAccounts = this.socialAccounts.filter((a) => a.id != id)
},
error: (error) => {
this.toastService.showError(
$localize`Error disconnecting social account`,
error
)
},
})
}
public gettotpSettings(): void {
this.totpSettingsLoading = true
this.profileService
.getTotpSettings()
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (totpSettings) => {
this.totpSettingsLoading = false
this.totpSettings = totpSettings
},
error: (error) => {
this.toastService.showError(
$localize`Error fetching TOTP settings`,
error
)
this.totpSettingsLoading = false
},
})
}
public activateTotp(): void {
this.totpLoading = 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 = false
this.isTotpEnabled = activationResponse.success
this.recoveryCodes = 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 = false
this.form.get('totp_code').enable()
this.toastService.showError($localize`Error activating TOTP`, error)
},
})
}
public deactivateTotp(): void {
this.totpLoading = true
this.profileService
.deactivateTotp()
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (success) => {
this.totpLoading = false
this.isTotpEnabled = !success
this.recoveryCodes = null
if (success) {
this.toastService.showInfo($localize`TOTP deactivated successfully`)
} else {
this.toastService.showError($localize`Error deactivating TOTP`)
}
},
error: (error) => {
this.totpLoading = false
this.toastService.showError($localize`Error deactivating TOTP`, error)
},
})
}
public copyRecoveryCodes(): void {
this.clipboard.copy(this.recoveryCodes.join('\n'))
this.codesCopied = true
setTimeout(() => {
this.codesCopied = false
}, 3000)
}
2023-12-02 08:26:42 -08:00
}