mirror of
https://github.com/bitwarden/browser
synced 2025-12-23 11:43:46 +00:00
move web settings to auth (#7022)
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
<div class="page-header">
|
||||
<h1>{{ "myAccount" | i18n }}</h1>
|
||||
</div>
|
||||
<app-profile></app-profile>
|
||||
<ng-container *ngIf="showChangeEmail">
|
||||
<div class="secondary-header">
|
||||
<h1>{{ "changeEmail" | i18n }}</h1>
|
||||
</div>
|
||||
<app-change-email></app-change-email>
|
||||
</ng-container>
|
||||
<div class="secondary-header text-danger border-0 mb-0">
|
||||
<h1>{{ "dangerZone" | i18n }}</h1>
|
||||
</div>
|
||||
<div class="card border-danger">
|
||||
<div class="card-body">
|
||||
<p>{{ "dangerZoneDesc" | i18n }}</p>
|
||||
<button type="button" bitButton buttonType="danger" (click)="deauthorizeSessions()">
|
||||
{{ "deauthorizeSessions" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="danger" (click)="purgeVault()">
|
||||
{{ "purgeVault" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="danger" (click)="deleteAccount()">
|
||||
{{ "deleteAccount" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #deauthorizeSessionsTemplate></ng-template>
|
||||
<ng-template #purgeVaultTemplate></ng-template>
|
||||
<ng-template #deleteAccountTemplate></ng-template>
|
||||
<ng-template #viewUserApiKeyTemplate></ng-template>
|
||||
<ng-template #rotateUserApiKeyTemplate></ng-template>
|
||||
45
apps/web/src/app/auth/settings/account/account.component.ts
Normal file
45
apps/web/src/app/auth/settings/account/account.component.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
|
||||
import { PurgeVaultComponent } from "../../../vault/settings/purge-vault.component";
|
||||
|
||||
import { DeauthorizeSessionsComponent } from "./deauthorize-sessions.component";
|
||||
import { DeleteAccountComponent } from "./delete-account.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-account",
|
||||
templateUrl: "account.component.html",
|
||||
})
|
||||
export class AccountComponent {
|
||||
@ViewChild("deauthorizeSessionsTemplate", { read: ViewContainerRef, static: true })
|
||||
deauthModalRef: ViewContainerRef;
|
||||
@ViewChild("purgeVaultTemplate", { read: ViewContainerRef, static: true })
|
||||
purgeModalRef: ViewContainerRef;
|
||||
@ViewChild("deleteAccountTemplate", { read: ViewContainerRef, static: true })
|
||||
deleteModalRef: ViewContainerRef;
|
||||
|
||||
showChangeEmail = true;
|
||||
|
||||
constructor(
|
||||
private modalService: ModalService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.showChangeEmail = await this.userVerificationService.hasMasterPassword();
|
||||
}
|
||||
|
||||
async deauthorizeSessions() {
|
||||
await this.modalService.openViewRef(DeauthorizeSessionsComponent, this.deauthModalRef);
|
||||
}
|
||||
|
||||
async purgeVault() {
|
||||
await this.modalService.openViewRef(PurgeVaultComponent, this.purgeModalRef);
|
||||
}
|
||||
|
||||
async deleteAccount() {
|
||||
await this.modalService.openViewRef(DeleteAccountComponent, this.deleteModalRef);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<!-- Please remove this disable statement when editing this file! -->
|
||||
<!-- eslint-disable tailwindcss/no-custom-classname -->
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="customizeTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable tw-w-[600px] tw-max-w-none" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="customizeTitle">{{ "customizeAvatar" | i18n }}</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="card-body text-center" *ngIf="loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
{{ "loading" | i18n }}
|
||||
</div>
|
||||
<app-callout type="error" *ngIf="error">
|
||||
{{ error }}
|
||||
</app-callout>
|
||||
<p class="tw-text-lg">{{ "pickAnAvatarColor" | i18n }}</p>
|
||||
<div class="tw-flex tw-flex-wrap tw-justify-center tw-gap-8">
|
||||
<ng-container *ngFor="let c of defaultColorPalette">
|
||||
<selectable-avatar
|
||||
appStopClick
|
||||
(select)="setSelection(c.color)"
|
||||
[selected]="c.selected"
|
||||
[title]="c.name"
|
||||
text="{{ profile | userName }}"
|
||||
[color]="c.color"
|
||||
[border]="true"
|
||||
>
|
||||
</selectable-avatar>
|
||||
</ng-container>
|
||||
<span>
|
||||
<span
|
||||
[tabIndex]="0"
|
||||
(keyup.enter)="showCustomPicker()"
|
||||
(click)="showCustomPicker()"
|
||||
title="{{ 'customColor' | i18n }}"
|
||||
[ngClass]="{
|
||||
'!tw-outline-[3px] tw-outline-primary-500 hover:tw-outline-[3px] hover:tw-outline-primary-500':
|
||||
customColorSelected
|
||||
}"
|
||||
class="tw-outline-solid tw-bg-white tw-relative tw-flex tw-h-24 tw-w-24 tw-cursor-pointer tw-place-content-center tw-content-center tw-justify-center tw-rounded-full tw-border tw-border-solid tw-border-secondary-500 tw-outline tw-outline-0 tw-outline-offset-1 hover:tw-outline-1 hover:tw-outline-primary-300 focus:tw-outline-2 focus:tw-outline-primary-500"
|
||||
[style.background-color]="customColor$ | async"
|
||||
>
|
||||
<i
|
||||
[style.color]="customTextColor$ | async"
|
||||
class="bwi bwi-pencil tw-m-auto tw-text-3xl"
|
||||
></i>
|
||||
<input
|
||||
tabindex="-1"
|
||||
class="tw-absolute tw-bottom-0 tw-right-0 tw-h-px tw-w-px tw-border-none tw-bg-transparent tw-opacity-0"
|
||||
#colorPicker
|
||||
type="color"
|
||||
[ngModel]="customColor$ | async"
|
||||
(ngModelChange)="customColor$.next($event)"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-submit"
|
||||
[disabled]="loading"
|
||||
(click)="submit()"
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "save" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,138 @@
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
ViewEncapsulation,
|
||||
} from "@angular/core";
|
||||
import { BehaviorSubject, debounceTime, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { AvatarUpdateService } from "@bitwarden/common/abstractions/account/avatar-update.service";
|
||||
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
@Component({
|
||||
selector: "app-change-avatar",
|
||||
templateUrl: "change-avatar.component.html",
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class ChangeAvatarComponent implements OnInit, OnDestroy {
|
||||
@Input() profile: ProfileResponse;
|
||||
|
||||
@Output() changeColor: EventEmitter<string | null> = new EventEmitter();
|
||||
@Output() onSaved = new EventEmitter();
|
||||
|
||||
@ViewChild("colorPicker") colorPickerElement: ElementRef<HTMLElement>;
|
||||
|
||||
loading = false;
|
||||
error: string;
|
||||
defaultColorPalette: NamedAvatarColor[] = [
|
||||
{ name: "brightBlue", color: "#16cbfc" },
|
||||
{ name: "green", color: "#94cc4b" },
|
||||
{ name: "orange", color: "#ffb520" },
|
||||
{ name: "lavender", color: "#e5beed" },
|
||||
{ name: "yellow", color: "#fcff41" },
|
||||
{ name: "indigo", color: "#acbdf7" },
|
||||
{ name: "teal", color: "#8ecdc5" },
|
||||
{ name: "salmon", color: "#ffa3a3" },
|
||||
{ name: "pink", color: "#ffa2d4" },
|
||||
];
|
||||
customColorSelected = false;
|
||||
currentSelection: string;
|
||||
|
||||
protected customColor$ = new BehaviorSubject<string | null>(null);
|
||||
protected customTextColor$ = new BehaviorSubject<string>("#000000");
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService,
|
||||
private accountUpdateService: AvatarUpdateService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
//localize the default colors
|
||||
this.defaultColorPalette.forEach((c) => (c.name = this.i18nService.t(c.name)));
|
||||
|
||||
this.customColor$
|
||||
.pipe(debounceTime(200), takeUntil(this.destroy$))
|
||||
.subscribe((color: string | null) => {
|
||||
if (color == null) {
|
||||
return;
|
||||
}
|
||||
this.customTextColor$.next(Utils.pickTextColorBasedOnBgColor(color));
|
||||
this.customColorSelected = true;
|
||||
this.currentSelection = color;
|
||||
});
|
||||
|
||||
this.setSelection(await this.accountUpdateService.loadColorFromState());
|
||||
}
|
||||
|
||||
async showCustomPicker() {
|
||||
this.customColorSelected = true;
|
||||
this.colorPickerElement.nativeElement.click();
|
||||
this.setSelection(this.customColor$.value);
|
||||
}
|
||||
|
||||
async generateAvatarColor() {
|
||||
Utils.stringToColor(this.profile.name.toString());
|
||||
}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
if (Utils.validateHexColor(this.currentSelection) || this.currentSelection == null) {
|
||||
await this.accountUpdateService.pushUpdate(this.currentSelection);
|
||||
this.changeColor.emit(this.currentSelection);
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("avatarUpdated"));
|
||||
} else {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async setSelection(color: string | null) {
|
||||
this.defaultColorPalette.filter((x) => x.selected).forEach((c) => (c.selected = false));
|
||||
|
||||
if (color == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
color = color.toLowerCase();
|
||||
|
||||
this.customColorSelected = false;
|
||||
//Allow for toggle
|
||||
if (this.currentSelection === color) {
|
||||
this.currentSelection = null;
|
||||
} else {
|
||||
const selectedColorIndex = this.defaultColorPalette.findIndex((c) => c.color === color);
|
||||
if (selectedColorIndex !== -1) {
|
||||
this.defaultColorPalette[selectedColorIndex].selected = true;
|
||||
this.currentSelection = color;
|
||||
} else {
|
||||
this.customColor$.next(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NamedAvatarColor {
|
||||
name: string;
|
||||
color: string;
|
||||
selected? = false;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<app-callout type="warning" *ngIf="showTwoFactorEmailWarning">
|
||||
{{ "changeEmailTwoFactorWarning" | i18n }}
|
||||
</app-callout>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
|
||||
<input
|
||||
id="masterPassword"
|
||||
type="password"
|
||||
name="MasterPasswordHash"
|
||||
class="form-control"
|
||||
[(ngModel)]="masterPassword"
|
||||
required
|
||||
[readonly]="tokenSent"
|
||||
appInputVerbatim
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newEmail">{{ "newEmail" | i18n }}</label>
|
||||
<input
|
||||
id="newEmail"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="NewEmail"
|
||||
[(ngModel)]="newEmail"
|
||||
required
|
||||
[readonly]="tokenSent"
|
||||
inputmode="email"
|
||||
appInputVerbatim="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="tokenSent">
|
||||
<hr />
|
||||
<p>{{ "changeEmailDesc" | i18n: newEmail }}</p>
|
||||
<app-callout type="warning">{{ "loggedOutWarning" | i18n }}</app-callout>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="token">{{ "code" | i18n }}</label>
|
||||
<input
|
||||
id="token"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Token"
|
||||
[(ngModel)]="token"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span *ngIf="!tokenSent">{{ "continue" | i18n }}</span>
|
||||
<span *ngIf="tokenSent">{{ "changeEmail" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" *ngIf="tokenSent" (click)="reset()">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
102
apps/web/src/app/auth/settings/account/change-email.component.ts
Normal file
102
apps/web/src/app/auth/settings/account/change-email.component.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { EmailTokenRequest } from "@bitwarden/common/auth/models/request/email-token.request";
|
||||
import { EmailRequest } from "@bitwarden/common/auth/models/request/email.request";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-change-email",
|
||||
templateUrl: "change-email.component.html",
|
||||
})
|
||||
export class ChangeEmailComponent implements OnInit {
|
||||
masterPassword: string;
|
||||
newEmail: string;
|
||||
token: string;
|
||||
tokenSent = false;
|
||||
showTwoFactorEmailWarning = false;
|
||||
|
||||
formPromise: Promise<any>;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private cryptoService: CryptoService,
|
||||
private messagingService: MessagingService,
|
||||
private logService: LogService,
|
||||
private stateService: StateService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
const twoFactorProviders = await this.apiService.getTwoFactorProviders();
|
||||
this.showTwoFactorEmailWarning = twoFactorProviders.data.some(
|
||||
(p) => p.type === TwoFactorProviderType.Email && p.enabled,
|
||||
);
|
||||
}
|
||||
|
||||
async submit() {
|
||||
this.newEmail = this.newEmail.trim().toLowerCase();
|
||||
if (!this.tokenSent) {
|
||||
const request = new EmailTokenRequest();
|
||||
request.newEmail = this.newEmail;
|
||||
request.masterPasswordHash = await this.cryptoService.hashMasterKey(
|
||||
this.masterPassword,
|
||||
await this.cryptoService.getOrDeriveMasterKey(this.masterPassword),
|
||||
);
|
||||
try {
|
||||
this.formPromise = this.apiService.postEmailToken(request);
|
||||
await this.formPromise;
|
||||
this.tokenSent = true;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
} else {
|
||||
const request = new EmailRequest();
|
||||
request.token = this.token;
|
||||
request.newEmail = this.newEmail;
|
||||
request.masterPasswordHash = await this.cryptoService.hashMasterKey(
|
||||
this.masterPassword,
|
||||
await this.cryptoService.getOrDeriveMasterKey(this.masterPassword),
|
||||
);
|
||||
const kdf = await this.stateService.getKdfType();
|
||||
const kdfConfig = await this.stateService.getKdfConfig();
|
||||
const newMasterKey = await this.cryptoService.makeMasterKey(
|
||||
this.masterPassword,
|
||||
this.newEmail,
|
||||
kdf,
|
||||
kdfConfig,
|
||||
);
|
||||
request.newMasterPasswordHash = await this.cryptoService.hashMasterKey(
|
||||
this.masterPassword,
|
||||
newMasterKey,
|
||||
);
|
||||
const newUserKey = await this.cryptoService.encryptUserKeyWithMasterKey(newMasterKey);
|
||||
request.key = newUserKey[1].encryptedString;
|
||||
try {
|
||||
this.formPromise = this.apiService.postEmail(request);
|
||||
await this.formPromise;
|
||||
this.reset();
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
this.i18nService.t("emailChanged"),
|
||||
this.i18nService.t("logBackIn"),
|
||||
);
|
||||
this.messagingService.send("logout");
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.token = this.newEmail = this.masterPassword = null;
|
||||
this.tokenSent = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="deleteAccountTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||
<form
|
||||
class="modal-content"
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
[formGroup]="deleteForm"
|
||||
>
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="deleteAccountTitle">{{ "deleteAccount" | i18n }}</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{ "deleteAccountDesc" | i18n }}</p>
|
||||
<app-callout type="warning">{{ "deleteAccountWarning" | i18n }}</app-callout>
|
||||
<app-user-verification ngDefaultControl formControlName="verification" name="verification">
|
||||
</app-user-verification>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger btn-submit" [disabled]="form.loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "deleteAccount" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
|
||||
import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
|
||||
import { Verification } from "@bitwarden/common/auth/types/verification";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-delete-account",
|
||||
templateUrl: "delete-account.component.html",
|
||||
})
|
||||
export class DeleteAccountComponent {
|
||||
formPromise: Promise<void>;
|
||||
|
||||
deleteForm = this.formBuilder.group({
|
||||
verification: undefined as Verification | undefined,
|
||||
});
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private formBuilder: FormBuilder,
|
||||
private accountApiService: AccountApiService,
|
||||
private logService: LogService,
|
||||
) {}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
const verification = this.deleteForm.get("verification").value;
|
||||
this.formPromise = this.accountApiService.deleteAccount(verification);
|
||||
await this.formPromise;
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
this.i18nService.t("accountDeleted"),
|
||||
this.i18nService.t("accountDeletedDesc"),
|
||||
);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<div *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
<form
|
||||
*ngIf="profile && !loading"
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="name">{{ "name" | i18n }}</label>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="profile.name" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">{{ "email" | i18n }}</label>
|
||||
<input
|
||||
id="email"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Email"
|
||||
[(ngModel)]="profile.email"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="mb-3">
|
||||
<dynamic-avatar text="{{ profile | userName }}" [id]="profile.id" [size]="'large'">
|
||||
</dynamic-avatar>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary tw-ml-3.5"
|
||||
appStopClick
|
||||
appStopProp
|
||||
(click)="openChangeAvatar()"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-pencil-square" aria-hidden="true"></i>
|
||||
Customize
|
||||
</button>
|
||||
</div>
|
||||
<app-account-fingerprint
|
||||
[fingerprintMaterial]="fingerprintMaterial"
|
||||
fingerprintLabel="{{ 'yourAccountsFingerprint' | i18n }}"
|
||||
>
|
||||
</app-account-fingerprint>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "save" | i18n }}</span>
|
||||
</button>
|
||||
</form>
|
||||
<ng-template #avatarModalTemplate></ng-template>
|
||||
72
apps/web/src/app/auth/settings/account/profile.component.ts
Normal file
72
apps/web/src/app/auth/settings/account/profile.component.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { ViewChild, ViewContainerRef, Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { UpdateProfileRequest } from "@bitwarden/common/auth/models/request/update-profile.request";
|
||||
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
|
||||
import { ChangeAvatarComponent } from "./change-avatar.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-profile",
|
||||
templateUrl: "profile.component.html",
|
||||
})
|
||||
export class ProfileComponent implements OnInit, OnDestroy {
|
||||
loading = true;
|
||||
profile: ProfileResponse;
|
||||
fingerprintMaterial: string;
|
||||
|
||||
formPromise: Promise<any>;
|
||||
@ViewChild("avatarModalTemplate", { read: ViewContainerRef, static: true })
|
||||
avatarModalRef: ViewContainerRef;
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService,
|
||||
private stateService: StateService,
|
||||
private modalService: ModalService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.profile = await this.apiService.getProfile();
|
||||
this.loading = false;
|
||||
this.fingerprintMaterial = await this.stateService.getUserId();
|
||||
}
|
||||
|
||||
async ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async openChangeAvatar() {
|
||||
const modalOpened = await this.modalService.openViewRef(
|
||||
ChangeAvatarComponent,
|
||||
this.avatarModalRef,
|
||||
(modal) => {
|
||||
modal.profile = this.profile;
|
||||
modal.changeColor.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
||||
modalOpened[0].close();
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
const request = new UpdateProfileRequest(this.profile.name, this.profile.masterPasswordHint);
|
||||
this.formPromise = this.apiService.putProfile(request);
|
||||
await this.formPromise;
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("accountUpdated"));
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="apiKeyTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||
<form
|
||||
class="modal-content"
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
>
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="apiKeyTitle">{{ apiKeyTitle | i18n }}</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{ apiKeyDescription | i18n }}</p>
|
||||
<app-user-verification
|
||||
[(ngModel)]="masterPassword"
|
||||
ngDefaultControl
|
||||
name="secret"
|
||||
*ngIf="!clientSecret"
|
||||
>
|
||||
</app-user-verification>
|
||||
|
||||
<app-callout type="warning" *ngIf="clientSecret">{{ apiKeyWarning | i18n }}</app-callout>
|
||||
<app-callout
|
||||
type="info"
|
||||
title="{{ 'oauth2ClientCredentials' | i18n }}"
|
||||
icon="bwi bwi-key"
|
||||
*ngIf="clientSecret"
|
||||
>
|
||||
<p class="mb-1">
|
||||
<strong>client_id:</strong><br />
|
||||
<code>{{ clientId }}</code>
|
||||
</p>
|
||||
<p class="mb-1">
|
||||
<strong>client_secret:</strong><br />
|
||||
<code>{{ clientSecret }}</code>
|
||||
</p>
|
||||
<p class="mb-1">
|
||||
<strong>scope:</strong><br />
|
||||
<code>{{ scope }}</code>
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
<strong>grant_type:</strong><br />
|
||||
<code>{{ grantType }}</code>
|
||||
</p>
|
||||
</app-callout>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-submit"
|
||||
[disabled]="form.loading"
|
||||
*ngIf="!clientSecret"
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ (isRotation ? "rotateApiKey" : "viewApiKey") | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
46
apps/web/src/app/auth/settings/security/api-key.component.ts
Normal file
46
apps/web/src/app/auth/settings/security/api-key.component.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
|
||||
import { ApiKeyResponse } from "@bitwarden/common/auth/models/response/api-key.response";
|
||||
import { Verification } from "@bitwarden/common/auth/types/verification";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-api-key",
|
||||
templateUrl: "api-key.component.html",
|
||||
})
|
||||
export class ApiKeyComponent {
|
||||
keyType: string;
|
||||
isRotation: boolean;
|
||||
postKey: (entityId: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
|
||||
entityId: string;
|
||||
scope: string;
|
||||
grantType: string;
|
||||
apiKeyTitle: string;
|
||||
apiKeyWarning: string;
|
||||
apiKeyDescription: string;
|
||||
|
||||
masterPassword: Verification;
|
||||
formPromise: Promise<ApiKeyResponse>;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
|
||||
constructor(
|
||||
private userVerificationService: UserVerificationService,
|
||||
private logService: LogService,
|
||||
) {}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
this.formPromise = this.userVerificationService
|
||||
.buildRequest(this.masterPassword)
|
||||
.then((request) => this.postKey(this.entityId, request));
|
||||
const response = await this.formPromise;
|
||||
this.clientSecret = response.apiKey;
|
||||
this.clientId = `${this.keyType}.${this.entityId}`;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<bit-dialog>
|
||||
<span bitDialogTitle>
|
||||
{{ "changeKdf" | i18n }}
|
||||
</span>
|
||||
|
||||
<span bitDialogContent>
|
||||
<bit-callout type="warning">{{ "changeKdfLoggedOutWarning" | i18n }}</bit-callout>
|
||||
<form
|
||||
id="form"
|
||||
[formGroup]="form"
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
autocomplete="off"
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<bit-form-field class="tw-mb-1"
|
||||
><bit-label>{{ "masterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="password"
|
||||
required
|
||||
formControlName="masterPassword"
|
||||
appAutofocus
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
bitSuffix
|
||||
bitIconButton
|
||||
bitPasswordInputToggle
|
||||
[(toggled)]="showPassword"
|
||||
></button
|
||||
><bit-hint>
|
||||
{{ "confirmIdentity" | i18n }}
|
||||
</bit-hint></bit-form-field
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</span>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton buttonType="primary" type="submit" [loading]="loading" form="form">
|
||||
<span>{{ "changeKdf" | i18n }}</span>
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" type="button" bitDialogClose>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
@@ -0,0 +1,91 @@
|
||||
import { DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { FormGroup, FormControl, Validators } from "@angular/forms";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||
import { KdfRequest } from "@bitwarden/common/models/request/kdf.request";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { KdfType } from "@bitwarden/common/platform/enums";
|
||||
|
||||
@Component({
|
||||
selector: "app-change-kdf-confirmation",
|
||||
templateUrl: "change-kdf-confirmation.component.html",
|
||||
})
|
||||
export class ChangeKdfConfirmationComponent {
|
||||
kdf: KdfType;
|
||||
kdfConfig: KdfConfig;
|
||||
|
||||
form = new FormGroup({
|
||||
masterPassword: new FormControl(null, Validators.required),
|
||||
});
|
||||
showPassword = false;
|
||||
masterPassword: string;
|
||||
formPromise: Promise<any>;
|
||||
loading = false;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private cryptoService: CryptoService,
|
||||
private messagingService: MessagingService,
|
||||
private stateService: StateService,
|
||||
private logService: LogService,
|
||||
@Inject(DIALOG_DATA) params: { kdf: KdfType; kdfConfig: KdfConfig },
|
||||
) {
|
||||
this.kdf = params.kdf;
|
||||
this.kdfConfig = params.kdfConfig;
|
||||
this.masterPassword = null;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
this.formPromise = this.makeKeyAndSaveAsync();
|
||||
await this.formPromise;
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
this.i18nService.t("encKeySettingsChanged"),
|
||||
this.i18nService.t("logBackIn"),
|
||||
);
|
||||
this.messagingService.send("logout");
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async makeKeyAndSaveAsync() {
|
||||
const masterPassword = this.form.value.masterPassword;
|
||||
const request = new KdfRequest();
|
||||
request.kdf = this.kdf;
|
||||
request.kdfIterations = this.kdfConfig.iterations;
|
||||
request.kdfMemory = this.kdfConfig.memory;
|
||||
request.kdfParallelism = this.kdfConfig.parallelism;
|
||||
const masterKey = await this.cryptoService.getOrDeriveMasterKey(masterPassword);
|
||||
request.masterPasswordHash = await this.cryptoService.hashMasterKey(masterPassword, masterKey);
|
||||
const email = await this.stateService.getEmail();
|
||||
const newMasterKey = await this.cryptoService.makeMasterKey(
|
||||
masterPassword,
|
||||
email,
|
||||
this.kdf,
|
||||
this.kdfConfig,
|
||||
);
|
||||
request.newMasterPasswordHash = await this.cryptoService.hashMasterKey(
|
||||
masterPassword,
|
||||
newMasterKey,
|
||||
);
|
||||
const newUserKey = await this.cryptoService.encryptUserKeyWithMasterKey(newMasterKey);
|
||||
request.key = newUserKey[1].encryptedString;
|
||||
|
||||
await this.apiService.postAccountKdf(request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<div class="tabbed-header">
|
||||
<h1>{{ "encKeySettings" | i18n }}</h1>
|
||||
</div>
|
||||
<bit-callout type="warning">{{ "changeKdfLoggedOutWarning" | i18n }}</bit-callout>
|
||||
<form #form ngNativeValidate autocomplete="off">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group mb-0">
|
||||
<label for="kdf">{{ "kdfAlgorithm" | i18n }}</label>
|
||||
<a
|
||||
class="ml-auto"
|
||||
href="https://bitwarden.com/help/kdf-algorithms"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
<select
|
||||
id="kdf"
|
||||
name="Kdf"
|
||||
[(ngModel)]="kdf"
|
||||
(ngModelChange)="onChangeKdf($event)"
|
||||
class="form-control mb-3"
|
||||
required
|
||||
>
|
||||
<option *ngFor="let o of kdfOptions" [ngValue]="o.value">{{ o.name }}</option>
|
||||
</select>
|
||||
<ng-container *ngIf="kdf == kdfType.Argon2id">
|
||||
<label for="kdfMemory">{{ "kdfMemory" | i18n }}</label>
|
||||
<input
|
||||
id="kdfMemory"
|
||||
type="number"
|
||||
min="16"
|
||||
max="1024"
|
||||
name="Memory"
|
||||
class="form-control mb-3"
|
||||
[(ngModel)]="kdfConfig.memory"
|
||||
required
|
||||
/>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group mb-0">
|
||||
<ng-container *ngIf="kdf == kdfType.PBKDF2_SHA256">
|
||||
<label for="kdfIterations">{{ "kdfIterations" | i18n }}</label>
|
||||
<a
|
||||
class="ml-auto"
|
||||
href="https://bitwarden.com/help/what-encryption-is-used/#changing-kdf-iterations"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
<input
|
||||
id="kdfIterations"
|
||||
type="number"
|
||||
min="100000"
|
||||
max="2000000"
|
||||
name="KdfIterations"
|
||||
class="form-control"
|
||||
[(ngModel)]="kdfConfig.iterations"
|
||||
required
|
||||
/>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="kdf == kdfType.Argon2id">
|
||||
<label for="kdfIterations">{{ "kdfIterations" | i18n }}</label>
|
||||
<input
|
||||
id="iterations"
|
||||
type="number"
|
||||
min="2"
|
||||
max="10"
|
||||
name="Iterations"
|
||||
class="form-control mb-3"
|
||||
[(ngModel)]="kdfConfig.iterations"
|
||||
required
|
||||
/>
|
||||
<label for="kdfParallelism">{{ "kdfParallelism" | i18n }}</label>
|
||||
<input
|
||||
id="kdfParallelism"
|
||||
type="number"
|
||||
min="1"
|
||||
max="16"
|
||||
name="Parallelism"
|
||||
class="form-control"
|
||||
[(ngModel)]="kdfConfig.parallelism"
|
||||
required
|
||||
/>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<ng-container *ngIf="kdf == kdfType.PBKDF2_SHA256">
|
||||
<p class="small form-text text-muted">
|
||||
{{ "kdfIterationsDesc" | i18n: (recommendedPbkdf2Iterations | number) }}
|
||||
</p>
|
||||
<bit-callout type="warning">
|
||||
{{ "kdfIterationsWarning" | i18n: (100000 | number) }}
|
||||
</bit-callout>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="kdf == kdfType.Argon2id">
|
||||
<p class="small form-text text-muted">{{ "argon2Desc" | i18n }}</p>
|
||||
<bit-callout type="warning"> {{ "argon2Warning" | i18n }}</bit-callout>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
(click)="openConfirmationModal()"
|
||||
type="button"
|
||||
buttonType="primary"
|
||||
bitButton
|
||||
[loading]="form.loading"
|
||||
>
|
||||
{{ "changeKdf" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
|
||||
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import {
|
||||
DEFAULT_KDF_CONFIG,
|
||||
DEFAULT_PBKDF2_ITERATIONS,
|
||||
DEFAULT_ARGON2_ITERATIONS,
|
||||
DEFAULT_ARGON2_MEMORY,
|
||||
DEFAULT_ARGON2_PARALLELISM,
|
||||
KdfType,
|
||||
} from "@bitwarden/common/platform/enums";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { ChangeKdfConfirmationComponent } from "./change-kdf-confirmation.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-change-kdf",
|
||||
templateUrl: "change-kdf.component.html",
|
||||
})
|
||||
export class ChangeKdfComponent implements OnInit {
|
||||
kdf = KdfType.PBKDF2_SHA256;
|
||||
kdfConfig: KdfConfig = DEFAULT_KDF_CONFIG;
|
||||
kdfType = KdfType;
|
||||
kdfOptions: any[] = [];
|
||||
recommendedPbkdf2Iterations = DEFAULT_PBKDF2_ITERATIONS;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private dialogService: DialogService,
|
||||
) {
|
||||
this.kdfOptions = [
|
||||
{ name: "PBKDF2 SHA-256", value: KdfType.PBKDF2_SHA256 },
|
||||
{ name: "Argon2id", value: KdfType.Argon2id },
|
||||
];
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.kdf = await this.stateService.getKdfType();
|
||||
this.kdfConfig = await this.stateService.getKdfConfig();
|
||||
}
|
||||
|
||||
async onChangeKdf(newValue: KdfType) {
|
||||
if (newValue === KdfType.PBKDF2_SHA256) {
|
||||
this.kdfConfig = new KdfConfig(DEFAULT_PBKDF2_ITERATIONS);
|
||||
} else if (newValue === KdfType.Argon2id) {
|
||||
this.kdfConfig = new KdfConfig(
|
||||
DEFAULT_ARGON2_ITERATIONS,
|
||||
DEFAULT_ARGON2_MEMORY,
|
||||
DEFAULT_ARGON2_PARALLELISM,
|
||||
);
|
||||
} else {
|
||||
throw new Error("Unknown KDF type.");
|
||||
}
|
||||
}
|
||||
|
||||
async openConfirmationModal() {
|
||||
this.dialogService.open(ChangeKdfConfirmationComponent, {
|
||||
data: {
|
||||
kdf: this.kdf,
|
||||
kdfConfig: this.kdfConfig,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SharedModule } from "../../../../shared";
|
||||
|
||||
import { ChangeKdfConfirmationComponent } from "./change-kdf-confirmation.component";
|
||||
import { ChangeKdfComponent } from "./change-kdf.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, SharedModule],
|
||||
declarations: [ChangeKdfComponent, ChangeKdfConfirmationComponent],
|
||||
exports: [ChangeKdfComponent, ChangeKdfConfirmationComponent],
|
||||
})
|
||||
export class ChangeKdfModule {}
|
||||
@@ -0,0 +1,18 @@
|
||||
<app-change-kdf *ngIf="showChangeKdf"></app-change-kdf>
|
||||
<div
|
||||
[ngClass]="{ 'tabbed-header': !showChangeKdf, 'secondary-header': showChangeKdf }"
|
||||
class="border-0 mb-0"
|
||||
>
|
||||
<h1>{{ "apiKey" | i18n }}</h1>
|
||||
</div>
|
||||
<p>
|
||||
{{ "userApiKeyDesc" | i18n }}
|
||||
</p>
|
||||
<button type="button" bitButton buttonType="secondary" (click)="viewUserApiKey()">
|
||||
{{ "viewApiKey" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="secondary" (click)="rotateUserApiKey()">
|
||||
{{ "rotateApiKey" | i18n }}
|
||||
</button>
|
||||
<ng-template #viewUserApiKeyTemplate></ng-template>
|
||||
<ng-template #rotateUserApiKeyTemplate></ng-template>
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
|
||||
import { ApiKeyComponent } from "./api-key.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-security-keys",
|
||||
templateUrl: "security-keys.component.html",
|
||||
})
|
||||
export class SecurityKeysComponent implements OnInit {
|
||||
@ViewChild("viewUserApiKeyTemplate", { read: ViewContainerRef, static: true })
|
||||
viewUserApiKeyModalRef: ViewContainerRef;
|
||||
@ViewChild("rotateUserApiKeyTemplate", { read: ViewContainerRef, static: true })
|
||||
rotateUserApiKeyModalRef: ViewContainerRef;
|
||||
|
||||
showChangeKdf = true;
|
||||
|
||||
constructor(
|
||||
private userVerificationService: UserVerificationService,
|
||||
private stateService: StateService,
|
||||
private modalService: ModalService,
|
||||
private apiService: ApiService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.showChangeKdf = await this.userVerificationService.hasMasterPassword();
|
||||
}
|
||||
|
||||
async viewUserApiKey() {
|
||||
const entityId = await this.stateService.getUserId();
|
||||
await this.modalService.openViewRef(ApiKeyComponent, this.viewUserApiKeyModalRef, (comp) => {
|
||||
comp.keyType = "user";
|
||||
comp.entityId = entityId;
|
||||
comp.postKey = this.apiService.postUserApiKey.bind(this.apiService);
|
||||
comp.scope = "api";
|
||||
comp.grantType = "client_credentials";
|
||||
comp.apiKeyTitle = "apiKey";
|
||||
comp.apiKeyWarning = "userApiKeyWarning";
|
||||
comp.apiKeyDescription = "userApiKeyDesc";
|
||||
});
|
||||
}
|
||||
|
||||
async rotateUserApiKey() {
|
||||
const entityId = await this.stateService.getUserId();
|
||||
await this.modalService.openViewRef(ApiKeyComponent, this.rotateUserApiKeyModalRef, (comp) => {
|
||||
comp.keyType = "user";
|
||||
comp.isRotation = true;
|
||||
comp.entityId = entityId;
|
||||
comp.postKey = this.apiService.postUserRotateApiKey.bind(this.apiService);
|
||||
comp.scope = "api";
|
||||
comp.grantType = "client_credentials";
|
||||
comp.apiKeyTitle = "apiKey";
|
||||
comp.apiKeyWarning = "userApiKeyWarning";
|
||||
comp.apiKeyDescription = "apiKeyRotateDesc";
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { ChangePasswordComponent } from "../change-password.component";
|
||||
import { TwoFactorSetupComponent } from "../two-factor-setup.component";
|
||||
|
||||
import { SecurityKeysComponent } from "./security-keys.component";
|
||||
import { SecurityComponent } from "./security.component";
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: "",
|
||||
component: SecurityComponent,
|
||||
data: { titleId: "security" },
|
||||
children: [
|
||||
{ path: "", pathMatch: "full", redirectTo: "change-password" },
|
||||
{
|
||||
path: "change-password",
|
||||
component: ChangePasswordComponent,
|
||||
data: { titleId: "masterPassword" },
|
||||
},
|
||||
{
|
||||
path: "two-factor",
|
||||
component: TwoFactorSetupComponent,
|
||||
data: { titleId: "twoStepLogin" },
|
||||
},
|
||||
{
|
||||
path: "security-keys",
|
||||
component: SecurityKeysComponent,
|
||||
data: { titleId: "keys" },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class SecurityRoutingModule {}
|
||||
@@ -0,0 +1,22 @@
|
||||
<div class="tabbed-nav d-flex flex-column">
|
||||
<ul class="nav nav-tabs">
|
||||
<ng-container *ngIf="showChangePassword">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="change-password" routerLinkActive="active">
|
||||
{{ "masterPassword" | i18n }}
|
||||
</a>
|
||||
</li>
|
||||
</ng-container>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="two-factor" routerLinkActive="active">
|
||||
{{ "twoStepLogin" | i18n }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="security-keys" routerLinkActive="active">
|
||||
{{ "keys" | i18n }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<router-outlet></router-outlet>
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
|
||||
@Component({
|
||||
selector: "app-security",
|
||||
templateUrl: "security.component.html",
|
||||
})
|
||||
export class SecurityComponent {
|
||||
showChangePassword = true;
|
||||
|
||||
constructor(private userVerificationService: UserVerificationService) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.showChangePassword = await this.userVerificationService.hasMasterPassword();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user