1
0
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:
Jake Fink
2023-11-30 17:15:06 -05:00
committed by GitHub
parent cf6ed0d8a6
commit 8a0fa574c7
27 changed files with 18 additions and 18 deletions

View File

@@ -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>

View 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);
}
}

View File

@@ -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">&times;</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>

View File

@@ -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;
}

View File

@@ -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>

View 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;
}
}

View File

@@ -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">&times;</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>

View File

@@ -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);
}
}
}

View File

@@ -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>

View 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);
}
}
}

View File

@@ -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">&times;</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>

View 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);
}
}
}

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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,
},
});
}
}

View File

@@ -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 {}

View File

@@ -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>

View File

@@ -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";
});
}
}

View File

@@ -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 {}

View File

@@ -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>

View File

@@ -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();
}
}