mirror of
https://github.com/bitwarden/browser
synced 2025-12-23 11:43:46 +00:00
Move auth to app folder (#5336)
* move auth folder into app folder * fix auth folder imports * reorder imports in login component
This commit is contained in:
45
apps/web/src/app/auth/accept-emergency.component.html
Normal file
45
apps/web/src/app/auth/accept-emergency.component.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<div class="mt-5 d-flex justify-content-center" *ngIf="loading">
|
||||
<div>
|
||||
<img class="mb-4 logo logo-themed" alt="Bitwarden" />
|
||||
<p class="text-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container" *ngIf="!loading && !authed">
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="lead text-center mb-4">{{ "emergencyAccess" | i18n }}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body">
|
||||
<p class="text-center">
|
||||
{{ name }}
|
||||
</p>
|
||||
<p>{{ "acceptEmergencyAccess" | i18n }}</p>
|
||||
<hr />
|
||||
<div class="d-flex">
|
||||
<a
|
||||
routerLink="/login"
|
||||
[queryParams]="{ email: email }"
|
||||
class="btn btn-primary btn-block"
|
||||
>
|
||||
{{ "logIn" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
routerLink="/register"
|
||||
[queryParams]="{ email: email }"
|
||||
class="btn btn-primary btn-block ml-2 mt-0"
|
||||
>
|
||||
{{ "createAccount" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
59
apps/web/src/app/auth/accept-emergency.component.ts
Normal file
59
apps/web/src/app/auth/accept-emergency.component.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { ActivatedRoute, Params, Router } from "@angular/router";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { EmergencyAccessAcceptRequest } from "@bitwarden/common/auth/models/request/emergency-access-accept.request";
|
||||
|
||||
import { BaseAcceptComponent } from "../common/base.accept.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-accept-emergency",
|
||||
templateUrl: "accept-emergency.component.html",
|
||||
})
|
||||
export class AcceptEmergencyComponent extends BaseAcceptComponent {
|
||||
name: string;
|
||||
|
||||
protected requiredParameters: string[] = ["id", "name", "email", "token"];
|
||||
protected failedShortMessage = "emergencyInviteAcceptFailedShort";
|
||||
protected failedMessage = "emergencyInviteAcceptFailed";
|
||||
|
||||
constructor(
|
||||
router: Router,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
route: ActivatedRoute,
|
||||
private apiService: ApiService,
|
||||
stateService: StateService
|
||||
) {
|
||||
super(router, platformUtilsService, i18nService, route, stateService);
|
||||
}
|
||||
|
||||
async authedHandler(qParams: Params): Promise<void> {
|
||||
const request = new EmergencyAccessAcceptRequest();
|
||||
request.token = qParams.token;
|
||||
this.actionPromise = this.apiService.postEmergencyAccessAccept(qParams.id, request);
|
||||
await this.actionPromise;
|
||||
await this.stateService.setEmergencyAccessInvitation(null);
|
||||
this.platformUtilService.showToast(
|
||||
"success",
|
||||
this.i18nService.t("inviteAccepted"),
|
||||
this.i18nService.t("emergencyInviteAcceptedDesc"),
|
||||
{ timeout: 10000 }
|
||||
);
|
||||
this.router.navigate(["/vault"]);
|
||||
}
|
||||
|
||||
async unauthedHandler(qParams: Params): Promise<void> {
|
||||
this.name = qParams.name;
|
||||
if (this.name != null) {
|
||||
// Fix URL encoding of space issue with Angular
|
||||
this.name = this.name.replace(/\+/g, " ");
|
||||
}
|
||||
|
||||
// save the invitation to state so sso logins can find it later
|
||||
await this.stateService.setEmergencyAccessInvitation(qParams);
|
||||
}
|
||||
}
|
||||
46
apps/web/src/app/auth/accept-organization.component.html
Normal file
46
apps/web/src/app/auth/accept-organization.component.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<div class="mt-5 d-flex justify-content-center" *ngIf="loading">
|
||||
<div>
|
||||
<img src="../../images/logo-dark@2x.png" class="mb-4 logo" alt="Bitwarden" />
|
||||
<p class="text-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container" *ngIf="!loading">
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="lead text-center mb-4">{{ "joinOrganization" | i18n }}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body">
|
||||
<p class="text-center">
|
||||
{{ orgName }}
|
||||
<strong class="d-block mt-2">{{ email }}</strong>
|
||||
</p>
|
||||
<p>{{ "joinOrganizationDesc" | i18n }}</p>
|
||||
<hr />
|
||||
<div class="d-flex">
|
||||
<a
|
||||
routerLink="/login"
|
||||
[queryParams]="{ email: email }"
|
||||
class="btn btn-primary btn-block"
|
||||
>
|
||||
{{ "logIn" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
routerLink="/register"
|
||||
[queryParams]="{ email: email }"
|
||||
class="btn btn-primary btn-block ml-2 mt-0"
|
||||
>
|
||||
{{ "createAccount" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
187
apps/web/src/app/auth/accept-organization.component.ts
Normal file
187
apps/web/src/app/auth/accept-organization.component.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { ActivatedRoute, Params, Router } from "@angular/router";
|
||||
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
|
||||
import {
|
||||
OrganizationUserAcceptInitRequest,
|
||||
OrganizationUserAcceptRequest,
|
||||
} from "@bitwarden/common/abstractions/organization-user/requests";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
|
||||
import { BaseAcceptComponent } from "../common/base.accept.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-accept-organization",
|
||||
templateUrl: "accept-organization.component.html",
|
||||
})
|
||||
export class AcceptOrganizationComponent extends BaseAcceptComponent {
|
||||
orgName: string;
|
||||
|
||||
protected requiredParameters: string[] = ["organizationId", "organizationUserId", "token"];
|
||||
|
||||
constructor(
|
||||
router: Router,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
route: ActivatedRoute,
|
||||
stateService: StateService,
|
||||
private cryptoService: CryptoService,
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
private policyService: PolicyService,
|
||||
private logService: LogService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private organizationUserService: OrganizationUserService,
|
||||
private messagingService: MessagingService
|
||||
) {
|
||||
super(router, platformUtilsService, i18nService, route, stateService);
|
||||
}
|
||||
|
||||
async authedHandler(qParams: Params): Promise<void> {
|
||||
const initOrganization =
|
||||
qParams.initOrganization != null && qParams.initOrganization.toLocaleLowerCase() === "true";
|
||||
if (initOrganization) {
|
||||
this.actionPromise = this.acceptInitOrganizationFlow(qParams);
|
||||
} else {
|
||||
const needsReAuth = (await this.stateService.getOrganizationInvitation()) == null;
|
||||
if (needsReAuth) {
|
||||
// Accepting an org invite requires authentication from a logged out state
|
||||
this.messagingService.send("logout", { redirect: false });
|
||||
await this.prepareOrganizationInvitation(qParams);
|
||||
return;
|
||||
}
|
||||
|
||||
// User has already logged in and passed the Master Password policy check
|
||||
this.actionPromise = this.acceptFlow(qParams);
|
||||
}
|
||||
|
||||
await this.actionPromise;
|
||||
await this.stateService.setOrganizationInvitation(null);
|
||||
this.platformUtilService.showToast(
|
||||
"success",
|
||||
this.i18nService.t("inviteAccepted"),
|
||||
initOrganization
|
||||
? this.i18nService.t("inviteInitAcceptedDesc")
|
||||
: this.i18nService.t("inviteAcceptedDesc"),
|
||||
{ timeout: 10000 }
|
||||
);
|
||||
this.router.navigate(["/vault"]);
|
||||
}
|
||||
|
||||
async unauthedHandler(qParams: Params): Promise<void> {
|
||||
await this.prepareOrganizationInvitation(qParams);
|
||||
}
|
||||
|
||||
private async acceptInitOrganizationFlow(qParams: Params): Promise<any> {
|
||||
return this.prepareAcceptInitRequest(qParams).then((request) =>
|
||||
this.organizationUserService.postOrganizationUserAcceptInit(
|
||||
qParams.organizationId,
|
||||
qParams.organizationUserId,
|
||||
request
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private async acceptFlow(qParams: Params): Promise<any> {
|
||||
return this.prepareAcceptRequest(qParams).then((request) =>
|
||||
this.organizationUserService.postOrganizationUserAccept(
|
||||
qParams.organizationId,
|
||||
qParams.organizationUserId,
|
||||
request
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private async prepareAcceptInitRequest(
|
||||
qParams: Params
|
||||
): Promise<OrganizationUserAcceptInitRequest> {
|
||||
const request = new OrganizationUserAcceptInitRequest();
|
||||
request.token = qParams.token;
|
||||
|
||||
const [encryptedOrgShareKey, orgShareKey] = await this.cryptoService.makeShareKey();
|
||||
const [orgPublicKey, encryptedOrgPrivateKey] = await this.cryptoService.makeKeyPair(
|
||||
orgShareKey
|
||||
);
|
||||
const collection = await this.cryptoService.encrypt(
|
||||
this.i18nService.t("defaultCollection"),
|
||||
orgShareKey
|
||||
);
|
||||
|
||||
request.key = encryptedOrgShareKey.encryptedString;
|
||||
request.keys = new OrganizationKeysRequest(
|
||||
orgPublicKey,
|
||||
encryptedOrgPrivateKey.encryptedString
|
||||
);
|
||||
request.collectionName = collection.encryptedString;
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private async prepareAcceptRequest(qParams: Params): Promise<OrganizationUserAcceptRequest> {
|
||||
const request = new OrganizationUserAcceptRequest();
|
||||
request.token = qParams.token;
|
||||
|
||||
if (await this.performResetPasswordAutoEnroll(qParams)) {
|
||||
const response = await this.organizationApiService.getKeys(qParams.organizationId);
|
||||
|
||||
if (response == null) {
|
||||
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
|
||||
}
|
||||
|
||||
const publicKey = Utils.fromB64ToArray(response.publicKey);
|
||||
|
||||
// RSA Encrypt user's encKey.key with organization public key
|
||||
const encKey = await this.cryptoService.getEncKey();
|
||||
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey.buffer);
|
||||
|
||||
// Add reset password key to accept request
|
||||
request.resetPasswordKey = encryptedKey.encryptedString;
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
private async performResetPasswordAutoEnroll(qParams: Params): Promise<boolean> {
|
||||
let policyList: Policy[] = null;
|
||||
try {
|
||||
const policies = await this.policyApiService.getPoliciesByToken(
|
||||
qParams.organizationId,
|
||||
qParams.token,
|
||||
qParams.email,
|
||||
qParams.organizationUserId
|
||||
);
|
||||
policyList = this.policyService.mapPoliciesFromToken(policies);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
if (policyList != null) {
|
||||
const result = this.policyService.getResetPasswordPolicyOptions(
|
||||
policyList,
|
||||
qParams.organizationId
|
||||
);
|
||||
// Return true if policy enabled and auto-enroll enabled
|
||||
return result[1] && result[0].autoEnrollEnabled;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async prepareOrganizationInvitation(qParams: Params): Promise<void> {
|
||||
this.orgName = qParams.organizationName;
|
||||
if (this.orgName != null) {
|
||||
// Fix URL encoding of space issue with Angular
|
||||
this.orgName = this.orgName.replace(/\+/g, " ");
|
||||
}
|
||||
await this.stateService.setOrganizationInvitation(qParams);
|
||||
}
|
||||
}
|
||||
44
apps/web/src/app/auth/hint.component.html
Normal file
44
apps/web/src/app/auth/hint.component.html
Normal file
@@ -0,0 +1,44 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="lead text-center mb-4">{{ "passwordHint" | i18n }}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="email">{{ "emailAddress" | i18n }}</label>
|
||||
<input
|
||||
id="email"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Email"
|
||||
[(ngModel)]="email"
|
||||
required
|
||||
appAutofocus
|
||||
inputmode="email"
|
||||
appInputVerbatim="false"
|
||||
/>
|
||||
<small class="form-text text-muted">{{ "enterEmailToGetHint" | i18n }}</small>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="d-flex">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-block btn-submit"
|
||||
[disabled]="form.loading"
|
||||
>
|
||||
<span [hidden]="form.loading">{{ "submit" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
||||
{{ "cancel" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
26
apps/web/src/app/auth/hint.component.ts
Normal file
26
apps/web/src/app/auth/hint.component.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { HintComponent as BaseHintComponent } from "@bitwarden/angular/auth/components/hint.component";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-hint",
|
||||
templateUrl: "hint.component.html",
|
||||
})
|
||||
export class HintComponent extends BaseHintComponent {
|
||||
constructor(
|
||||
router: Router,
|
||||
i18nService: I18nService,
|
||||
apiService: ApiService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
logService: LogService,
|
||||
loginService: LoginService
|
||||
) {
|
||||
super(router, i18nService, apiService, platformUtilsService, logService, loginService);
|
||||
}
|
||||
}
|
||||
66
apps/web/src/app/auth/lock.component.html
Normal file
66
apps/web/src/app/auth/lock.component.html
Normal file
@@ -0,0 +1,66 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="text-center mb-4">
|
||||
<i class="bwi bwi-lock bwi-4x text-muted" aria-hidden="true"></i>
|
||||
</p>
|
||||
<p class="lead text-center mx-4 mb-4">{{ "yourVaultIsLocked" | i18n }}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
|
||||
<div class="d-flex">
|
||||
<input
|
||||
id="masterPassword"
|
||||
type="{{ showPassword ? 'text' : 'password' }}"
|
||||
name="MasterPassword"
|
||||
class="text-monospace form-control"
|
||||
[(ngModel)]="masterPassword"
|
||||
required
|
||||
appAutofocus
|
||||
appInputVerbatim
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-1 btn btn-link"
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="togglePassword()"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted form-text">
|
||||
{{ "loggedInAsEmailOn" | i18n : email : webVaultHostname }}
|
||||
</small>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="d-flex">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-block btn-submit"
|
||||
[disabled]="form.loading"
|
||||
>
|
||||
<span> <i class="bwi bwi-unlock" aria-hidden="true"></i> {{ "unlock" | i18n }} </span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary btn-block ml-2 mt-0"
|
||||
(click)="logOut()"
|
||||
>
|
||||
{{ "logOut" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
79
apps/web/src/app/auth/lock.component.ts
Normal file
79
apps/web/src/app/auth/lock.component.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Component, NgZone } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
|
||||
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
|
||||
import { RouterService } from "../core";
|
||||
|
||||
@Component({
|
||||
selector: "app-lock",
|
||||
templateUrl: "lock.component.html",
|
||||
})
|
||||
export class LockComponent extends BaseLockComponent {
|
||||
constructor(
|
||||
router: Router,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
messagingService: MessagingService,
|
||||
cryptoService: CryptoService,
|
||||
vaultTimeoutService: VaultTimeoutService,
|
||||
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
environmentService: EnvironmentService,
|
||||
private routerService: RouterService,
|
||||
stateService: StateService,
|
||||
apiService: ApiService,
|
||||
logService: LogService,
|
||||
keyConnectorService: KeyConnectorService,
|
||||
ngZone: NgZone,
|
||||
policyApiService: PolicyApiServiceAbstraction,
|
||||
policyService: InternalPolicyService,
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
dialogService: DialogServiceAbstraction
|
||||
) {
|
||||
super(
|
||||
router,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
messagingService,
|
||||
cryptoService,
|
||||
vaultTimeoutService,
|
||||
vaultTimeoutSettingsService,
|
||||
environmentService,
|
||||
stateService,
|
||||
apiService,
|
||||
logService,
|
||||
keyConnectorService,
|
||||
ngZone,
|
||||
policyApiService,
|
||||
policyService,
|
||||
passwordGenerationService,
|
||||
dialogService
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await super.ngOnInit();
|
||||
this.onSuccessfulSubmit = async () => {
|
||||
const previousUrl = this.routerService.getPreviousUrl();
|
||||
if (previousUrl && previousUrl !== "/" && previousUrl.indexOf("lock") === -1) {
|
||||
this.successRoute = previousUrl;
|
||||
}
|
||||
this.router.navigateByUrl(this.successRoute);
|
||||
};
|
||||
}
|
||||
}
|
||||
46
apps/web/src/app/auth/login/login-with-device.component.html
Normal file
46
apps/web/src/app/auth/login/login-with-device.component.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<!-- Please remove this disable statement when editing this file! -->
|
||||
<!-- eslint-disable tailwindcss/no-custom-classname -->
|
||||
<div
|
||||
class="tw-mx-auto tw-mt-5 tw-flex tw-max-w-lg tw-flex-col tw-items-center tw-justify-center tw-p-8"
|
||||
>
|
||||
<div>
|
||||
<img class="logo logo-themed" alt="Bitwarden" />
|
||||
<p class="tw-mx-4 tw-mt-3 tw-mb-4 tw-text-center tw-text-xl">
|
||||
{{ "loginOrCreateNewAccount" | i18n }}
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="tw-mt-3 tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
|
||||
>
|
||||
<h2 class="tw-mb-6 tw-text-xl tw-font-semibold">{{ "logInInitiated" | i18n }}</h2>
|
||||
|
||||
<div class="tw-text-light">
|
||||
<p class="tw-mb-6">{{ "notificationSentDevice" | i18n }}</p>
|
||||
|
||||
<p class="tw-mb-6">
|
||||
{{ "fingerprintMatchInfo" | i18n }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-6">
|
||||
<h4 class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</h4>
|
||||
<p>
|
||||
<code>{{ fingerprintPhrase }}</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="tw-my-10" *ngIf="showResendNotification">
|
||||
<a [routerLink]="[]" disabled="true" (click)="startPasswordlessLogin()">{{
|
||||
"resendNotification" | i18n
|
||||
}}</a>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="tw-text-light tw-mt-3">
|
||||
{{ "loginWithDeviceEnabledInfo" | i18n }}
|
||||
<a routerLink="/login">{{ "viewAllLoginOptions" | i18n }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
64
apps/web/src/app/auth/login/login-with-device.component.ts
Normal file
64
apps/web/src/app/auth/login/login-with-device.component.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { LoginWithDeviceComponent as BaseLoginWithDeviceComponent } from "@bitwarden/angular/auth/components/login-with-device.component";
|
||||
import { AnonymousHubService } from "@bitwarden/common/abstractions/anonymousHub.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { ValidationService } from "@bitwarden/common/abstractions/validation.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
|
||||
import { StateService } from "../../core";
|
||||
|
||||
@Component({
|
||||
selector: "app-login-with-device",
|
||||
templateUrl: "login-with-device.component.html",
|
||||
})
|
||||
export class LoginWithDeviceComponent
|
||||
extends BaseLoginWithDeviceComponent
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
constructor(
|
||||
router: Router,
|
||||
cryptoService: CryptoService,
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
appIdService: AppIdService,
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
apiService: ApiService,
|
||||
authService: AuthService,
|
||||
logService: LogService,
|
||||
environmentService: EnvironmentService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
anonymousHubService: AnonymousHubService,
|
||||
validationService: ValidationService,
|
||||
stateService: StateService,
|
||||
loginService: LoginService
|
||||
) {
|
||||
super(
|
||||
router,
|
||||
cryptoService,
|
||||
cryptoFunctionService,
|
||||
appIdService,
|
||||
passwordGenerationService,
|
||||
apiService,
|
||||
authService,
|
||||
logService,
|
||||
environmentService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
anonymousHubService,
|
||||
validationService,
|
||||
stateService,
|
||||
loginService
|
||||
);
|
||||
}
|
||||
}
|
||||
137
apps/web/src/app/auth/login/login.component.html
Normal file
137
apps/web/src/app/auth/login/login.component.html
Normal file
@@ -0,0 +1,137 @@
|
||||
<form
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
class="tw-container tw-mx-auto"
|
||||
[formGroup]="formGroup"
|
||||
>
|
||||
<div
|
||||
class="tw-mx-auto tw-mt-5 tw-flex tw-max-w-lg tw-flex-col tw-items-center tw-justify-center tw-p-8"
|
||||
>
|
||||
<div>
|
||||
<img class="logo logo-themed" alt="Bitwarden" />
|
||||
<p class="tw-mx-4 tw-mt-3 tw-mb-4 tw-text-center tw-text-xl">
|
||||
{{ "loginOrCreateNewAccount" | i18n }}
|
||||
</p>
|
||||
<div
|
||||
class="tw-mt-3 tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
|
||||
>
|
||||
<ng-container *ngIf="!validatedEmail">
|
||||
<div class="tw-mb-3">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "emailAddress" | i18n }}</bit-label>
|
||||
<input
|
||||
id="login_input_email"
|
||||
bitInput
|
||||
type="email"
|
||||
formControlName="email"
|
||||
appAutofocus
|
||||
(keyup.enter)="validateEmail()"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3 tw-flex tw-items-start">
|
||||
<bit-form-control class="tw-mb-0">
|
||||
<input type="checkbox" bitCheckbox formControlName="rememberEmail" />
|
||||
<bit-label>{{ "rememberEmail" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3">
|
||||
<button
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="primary"
|
||||
class="tw-w-full"
|
||||
[disabled]="form.loading"
|
||||
(click)="validateEmail()"
|
||||
>
|
||||
<span> {{ "continue" | i18n }} </span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<p class="tw-m-0 tw-text-sm">
|
||||
{{ "newAroundHere" | i18n }}
|
||||
<!--mousedown event is used over click because it prevents the validation from firing -->
|
||||
<a routerLink="/register" (mousedown)="goToRegister()">{{ "createAccount" | i18n }}</a>
|
||||
</p>
|
||||
</ng-container>
|
||||
|
||||
<div [ngClass]="{ 'tw-hidden': !validatedEmail }">
|
||||
<div class="tw-mb-6 tw-h-28">
|
||||
<bit-form-field class="!tw-mb-1">
|
||||
<bit-label>{{ "masterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
id="login_input_master-password"
|
||||
type="password"
|
||||
bitInput
|
||||
#masterPasswordInput
|
||||
formControlName="masterPassword"
|
||||
/>
|
||||
<button type="button" bitSuffix bitIconButton bitPasswordInputToggle></button>
|
||||
</bit-form-field>
|
||||
<a
|
||||
class="-tw-mt-2"
|
||||
routerLink="/hint"
|
||||
(mousedown)="goToHint()"
|
||||
(click)="setFormValues()"
|
||||
>{{ "getMasterPasswordHint" | i18n }}</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<div [hidden]="!showCaptcha()">
|
||||
<iframe id="hcaptcha_iframe" height="80"></iframe>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3 tw-flex tw-space-x-4">
|
||||
<button
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
type="submit"
|
||||
[block]="true"
|
||||
[loading]="form.loading"
|
||||
>
|
||||
<span> {{ "loginWithMasterPassword" | i18n }} </span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3" *ngIf="showLoginWithDevice && showPasswordless">
|
||||
<button
|
||||
bitButton
|
||||
type="button"
|
||||
[block]="true"
|
||||
buttonType="secondary"
|
||||
(click)="startPasswordlessLogin()"
|
||||
>
|
||||
<span> <i class="bwi bwi-mobile"></i> {{ "loginWithDevice" | i18n }} </span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3">
|
||||
<a
|
||||
routerLink="/sso"
|
||||
[queryParams]="{ email: formGroup.value.email }"
|
||||
(click)="saveEmailSettings()"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
class="tw-w-full"
|
||||
>
|
||||
<i class="bwi bwi-provider tw-mr-2"></i>
|
||||
{{ "enterpriseSingleSignOn" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="tw-m-0 tw-text-sm">
|
||||
<p class="tw-mb-1">{{ "loggingInAs" | i18n }} {{ loggedEmail }}</p>
|
||||
<a [routerLink]="[]" (click)="toggleValidateEmail(false)">{{ "notYou" | i18n }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
211
apps/web/src/app/auth/login/login.component.ts
Normal file
211
apps/web/src/app/auth/login/login.component.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||
import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
|
||||
import { flagEnabled } from "../../../utils/flags";
|
||||
import { RouterService, StateService } from "../../core";
|
||||
|
||||
@Component({
|
||||
selector: "app-login",
|
||||
templateUrl: "login.component.html",
|
||||
})
|
||||
export class LoginComponent extends BaseLoginComponent implements OnInit, OnDestroy {
|
||||
showResetPasswordAutoEnrollWarning = false;
|
||||
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions;
|
||||
policies: ListResponse<PolicyResponse>;
|
||||
showPasswordless = false;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
apiService: ApiService,
|
||||
appIdService: AppIdService,
|
||||
authService: AuthService,
|
||||
router: Router,
|
||||
i18nService: I18nService,
|
||||
route: ActivatedRoute,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
environmentService: EnvironmentService,
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
private policyService: InternalPolicyService,
|
||||
logService: LogService,
|
||||
ngZone: NgZone,
|
||||
protected stateService: StateService,
|
||||
private messagingService: MessagingService,
|
||||
private routerService: RouterService,
|
||||
formBuilder: FormBuilder,
|
||||
formValidationErrorService: FormValidationErrorsService,
|
||||
loginService: LoginService
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
appIdService,
|
||||
authService,
|
||||
router,
|
||||
platformUtilsService,
|
||||
i18nService,
|
||||
stateService,
|
||||
environmentService,
|
||||
passwordGenerationService,
|
||||
cryptoFunctionService,
|
||||
logService,
|
||||
ngZone,
|
||||
formBuilder,
|
||||
formValidationErrorService,
|
||||
route,
|
||||
loginService
|
||||
);
|
||||
this.onSuccessfulLogin = async () => {
|
||||
this.messagingService.send("setFullWidth");
|
||||
};
|
||||
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||
this.showPasswordless = flagEnabled("showPasswordless");
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||
if (qParams.premium != null) {
|
||||
this.routerService.setPreviousUrl("/settings/premium");
|
||||
} else if (qParams.org != null) {
|
||||
const route = this.router.createUrlTree(["create-organization"], {
|
||||
queryParams: { plan: qParams.org },
|
||||
});
|
||||
this.routerService.setPreviousUrl(route.toString());
|
||||
}
|
||||
|
||||
// Are they coming from an email for sponsoring a families organization
|
||||
if (qParams.sponsorshipToken != null) {
|
||||
const route = this.router.createUrlTree(["setup/families-for-enterprise"], {
|
||||
queryParams: { token: qParams.sponsorshipToken },
|
||||
});
|
||||
this.routerService.setPreviousUrl(route.toString());
|
||||
}
|
||||
await super.ngOnInit();
|
||||
});
|
||||
|
||||
const invite = await this.stateService.getOrganizationInvitation();
|
||||
if (invite != null) {
|
||||
let policyList: Policy[] = null;
|
||||
try {
|
||||
this.policies = await this.policyApiService.getPoliciesByToken(
|
||||
invite.organizationId,
|
||||
invite.token,
|
||||
invite.email,
|
||||
invite.organizationUserId
|
||||
);
|
||||
policyList = this.policyService.mapPoliciesFromToken(this.policies);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
if (policyList != null) {
|
||||
const resetPasswordPolicy = this.policyService.getResetPasswordPolicyOptions(
|
||||
policyList,
|
||||
invite.organizationId
|
||||
);
|
||||
// Set to true if policy enabled and auto-enroll enabled
|
||||
this.showResetPasswordAutoEnrollWarning =
|
||||
resetPasswordPolicy[1] && resetPasswordPolicy[0].autoEnrollEnabled;
|
||||
|
||||
this.policyService
|
||||
.masterPasswordPolicyOptions$(policyList)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((enforcedPasswordPolicyOptions) => {
|
||||
this.enforcedPasswordPolicyOptions = enforcedPasswordPolicyOptions;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async goAfterLogIn() {
|
||||
const masterPassword = this.formGroup.value.masterPassword;
|
||||
|
||||
// Check master password against policy
|
||||
if (this.enforcedPasswordPolicyOptions != null) {
|
||||
const strengthResult = this.passwordGenerationService.passwordStrength(
|
||||
masterPassword,
|
||||
this.formGroup.value.email
|
||||
);
|
||||
const masterPasswordScore = strengthResult == null ? null : strengthResult.score;
|
||||
|
||||
// If invalid, save policies and require update
|
||||
if (
|
||||
!this.policyService.evaluateMasterPassword(
|
||||
masterPasswordScore,
|
||||
masterPassword,
|
||||
this.enforcedPasswordPolicyOptions
|
||||
)
|
||||
) {
|
||||
const policiesData: { [id: string]: PolicyData } = {};
|
||||
this.policies.data.map((p) => (policiesData[p.id] = new PolicyData(p)));
|
||||
await this.policyService.replace(policiesData);
|
||||
this.router.navigate(["update-password"]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const previousUrl = this.routerService.getPreviousUrl();
|
||||
if (previousUrl) {
|
||||
this.router.navigateByUrl(previousUrl);
|
||||
} else {
|
||||
this.loginService.clearValues();
|
||||
this.router.navigate([this.successRoute]);
|
||||
}
|
||||
}
|
||||
|
||||
goToHint() {
|
||||
this.setFormValues();
|
||||
this.router.navigateByUrl("/hint");
|
||||
}
|
||||
|
||||
goToRegister() {
|
||||
const email = this.formGroup.value.email;
|
||||
|
||||
if (email) {
|
||||
this.router.navigate(["/register"], { queryParams: { email: email } });
|
||||
return;
|
||||
}
|
||||
|
||||
this.router.navigate(["/register"]);
|
||||
}
|
||||
|
||||
async submit() {
|
||||
const rememberEmail = this.formGroup.value.rememberEmail;
|
||||
|
||||
if (!rememberEmail) {
|
||||
await this.stateService.setRememberedEmail(null);
|
||||
}
|
||||
await super.submit(false);
|
||||
}
|
||||
}
|
||||
15
apps/web/src/app/auth/login/login.module.ts
Normal file
15
apps/web/src/app/auth/login/login.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { CheckboxModule } from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../../app/shared";
|
||||
|
||||
import { LoginWithDeviceComponent } from "./login-with-device.component";
|
||||
import { LoginComponent } from "./login.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, CheckboxModule],
|
||||
declarations: [LoginComponent, LoginWithDeviceComponent],
|
||||
exports: [LoginComponent, LoginWithDeviceComponent],
|
||||
})
|
||||
export class LoginModule {}
|
||||
44
apps/web/src/app/auth/recover-delete.component.html
Normal file
44
apps/web/src/app/auth/recover-delete.component.html
Normal file
@@ -0,0 +1,44 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="lead text-center mb-4">{{ "deleteAccount" | i18n }}</p>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p>{{ "deleteRecoverDesc" | i18n }}</p>
|
||||
<div class="form-group">
|
||||
<label for="email">{{ "emailAddress" | i18n }}</label>
|
||||
<input
|
||||
id="email"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Email"
|
||||
[(ngModel)]="email"
|
||||
required
|
||||
appAutofocus
|
||||
inputmode="email"
|
||||
appInputVerbatim="false"
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="d-flex">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-block btn-submit"
|
||||
[disabled]="form.loading"
|
||||
>
|
||||
<span>{{ "submit" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
||||
{{ "cancel" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
42
apps/web/src/app/auth/recover-delete.component.ts
Normal file
42
apps/web/src/app/auth/recover-delete.component.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { DeleteRecoverRequest } from "@bitwarden/common/models/request/delete-recover.request";
|
||||
|
||||
@Component({
|
||||
selector: "app-recover-delete",
|
||||
templateUrl: "recover-delete.component.html",
|
||||
})
|
||||
export class RecoverDeleteComponent {
|
||||
email: string;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private apiService: ApiService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private logService: LogService
|
||||
) {}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
const request = new DeleteRecoverRequest();
|
||||
request.email = this.email.trim().toLowerCase();
|
||||
this.formPromise = this.apiService.postAccountRecoverDelete(request);
|
||||
await this.formPromise;
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("deleteRecoverEmailSent")
|
||||
);
|
||||
this.router.navigate(["/"]);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
76
apps/web/src/app/auth/recover-two-factor.component.html
Normal file
76
apps/web/src/app/auth/recover-two-factor.component.html
Normal file
@@ -0,0 +1,76 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="lead text-center mb-4">{{ "recoverAccountTwoStep" | i18n }}</p>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p>
|
||||
{{ "recoverAccountTwoStepDesc" | i18n }}
|
||||
<a
|
||||
href="https://bitwarden.com/help/lost-two-step-device/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>{{ "learnMore" | i18n }}</a
|
||||
>
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label for="email">{{ "emailAddress" | i18n }}</label>
|
||||
<input
|
||||
id="email"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Email"
|
||||
[(ngModel)]="email"
|
||||
required
|
||||
appAutofocus
|
||||
inputmode="email"
|
||||
appInputVerbatim="false"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
|
||||
<input
|
||||
id="masterPassword"
|
||||
type="password"
|
||||
name="MasterPassword"
|
||||
class="form-control"
|
||||
[(ngModel)]="masterPassword"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="recoveryCode">{{ "recoveryCodeTitle" | i18n }}</label>
|
||||
<input
|
||||
id="recoveryCode"
|
||||
class="text-monospace form-control"
|
||||
type="text"
|
||||
name="RecoveryCode"
|
||||
[(ngModel)]="recoveryCode"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="d-flex">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-block btn-submit"
|
||||
[disabled]="form.loading"
|
||||
>
|
||||
<span>{{ "submit" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
||||
{{ "cancel" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
51
apps/web/src/app/auth/recover-two-factor.component.ts
Normal file
51
apps/web/src/app/auth/recover-two-factor.component.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { TwoFactorRecoveryRequest } from "@bitwarden/common/auth/models/request/two-factor-recovery.request";
|
||||
|
||||
@Component({
|
||||
selector: "app-recover-two-factor",
|
||||
templateUrl: "recover-two-factor.component.html",
|
||||
})
|
||||
export class RecoverTwoFactorComponent {
|
||||
email: string;
|
||||
masterPassword: string;
|
||||
recoveryCode: string;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private apiService: ApiService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private cryptoService: CryptoService,
|
||||
private authService: AuthService,
|
||||
private logService: LogService
|
||||
) {}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
const request = new TwoFactorRecoveryRequest();
|
||||
request.recoveryCode = this.recoveryCode.replace(/\s/g, "").toLowerCase();
|
||||
request.email = this.email.trim().toLowerCase();
|
||||
const key = await this.authService.makePreloginKey(this.masterPassword, request.email);
|
||||
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, key);
|
||||
this.formPromise = this.apiService.postTwoFactorRecover(request);
|
||||
await this.formPromise;
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("twoStepRecoverDisabled")
|
||||
);
|
||||
this.router.navigate(["/"]);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
157
apps/web/src/app/auth/register-form/register-form.component.html
Normal file
157
apps/web/src/app/auth/register-form/register-form.component.html
Normal file
@@ -0,0 +1,157 @@
|
||||
<!-- Please remove this disable statement when editing this file! -->
|
||||
<!-- eslint-disable tailwindcss/no-custom-classname -->
|
||||
<form
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
class="tw-container tw-mx-auto"
|
||||
[formGroup]="formGroup"
|
||||
>
|
||||
<div>
|
||||
<div class="tw-mb-3">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "emailAddress" | i18n }}</bit-label>
|
||||
<input id="register-form_input_email" bitInput type="email" formControlName="email" />
|
||||
<bit-hint>{{ "emailAddressDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "name" | i18n }}</bit-label>
|
||||
<input id="register-form_input_name" bitInput type="text" formControlName="name" />
|
||||
<bit-hint>{{ "yourNameDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3">
|
||||
<app-callout
|
||||
type="info"
|
||||
[enforcedPolicyOptions]="enforcedPolicyOptions"
|
||||
*ngIf="enforcedPolicyOptions"
|
||||
>
|
||||
</app-callout>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "masterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
id="register-form_input_master-password"
|
||||
bitInput
|
||||
type="password"
|
||||
formControlName="masterPassword"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
bitSuffix
|
||||
bitIconButton
|
||||
bitPasswordInputToggle
|
||||
[(toggled)]="showPassword"
|
||||
></button>
|
||||
<bit-hint>
|
||||
<span class="tw-font-semibold">{{ "important" | i18n }}</span>
|
||||
{{ "masterPassImportant" | i18n }} {{ characterMinimumMessage }}
|
||||
</bit-hint>
|
||||
</bit-form-field>
|
||||
<app-password-strength
|
||||
[password]="formGroup.get('masterPassword')?.value"
|
||||
[email]="formGroup.get('email')?.value"
|
||||
[name]="formGroup.get('name')?.value"
|
||||
[showText]="true"
|
||||
(passwordStrengthResult)="getStrengthResult($event)"
|
||||
>
|
||||
</app-password-strength>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "reTypeMasterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
id="register-form_input_confirm-master-password"
|
||||
bitInput
|
||||
type="password"
|
||||
formControlName="confirmMasterPassword"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
bitSuffix
|
||||
bitIconButton
|
||||
bitPasswordInputToggle
|
||||
[(toggled)]="showPassword"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "masterPassHintLabel" | i18n }}</bit-label>
|
||||
<input id="register-form_input_hint" bitInput type="text" formControlName="hint" />
|
||||
<bit-hint>{{ "masterPassHintDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
|
||||
<div [hidden]="!showCaptcha()">
|
||||
<iframe id="hcaptcha_iframe" height="80"></iframe>
|
||||
</div>
|
||||
<div class="tw-mb-4 tw-flex tw-items-start">
|
||||
<input
|
||||
class="mt-1"
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
id="checkForBreaches"
|
||||
name="CheckBreach"
|
||||
formControlName="checkForBreaches"
|
||||
/>
|
||||
<bit-label for="checkForBreaches"> {{ "checkForBreaches" | i18n }}</bit-label>
|
||||
</div>
|
||||
<div class="tw-mb-3 tw-flex tw-items-start" *ngIf="showTerms">
|
||||
<input
|
||||
class="mt-1"
|
||||
id="register-form-input-accept-policies"
|
||||
bitCheckbox
|
||||
type="checkbox"
|
||||
formControlName="acceptPolicies"
|
||||
/>
|
||||
|
||||
<bit-label for="register-form-input-accept-policies">
|
||||
{{ "acceptPolicies" | i18n }}<br />
|
||||
<a href="https://bitwarden.com/terms/" target="_blank" rel="noopener">{{
|
||||
"termsOfService" | i18n
|
||||
}}</a
|
||||
>,
|
||||
<a href="https://bitwarden.com/privacy/" target="_blank" rel="noopener">{{
|
||||
"privacyPolicy" | i18n
|
||||
}}</a>
|
||||
</bit-label>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<div class="tw-flex tw-space-x-2 tw-pt-2">
|
||||
<ng-container *ngIf="!accountCreated">
|
||||
<button
|
||||
[block]="true"
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
bitButton
|
||||
[loading]="form.loading"
|
||||
>
|
||||
{{ "createAccount" | i18n }}
|
||||
</button>
|
||||
<a bitButton [block]="true" buttonType="secondary" routerLink="/login">
|
||||
<i class="bwi bwi-sign-in tw-mr-2"></i>
|
||||
{{ "logIn" | i18n }}
|
||||
</a>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="accountCreated">
|
||||
<button
|
||||
[block]="true"
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
bitButton
|
||||
[loading]="form.loading"
|
||||
>
|
||||
{{ "logIn" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
<bit-error-summary *ngIf="showErrorSummary" [formGroup]="formGroup"></bit-error-summary>
|
||||
</div>
|
||||
</form>
|
||||
103
apps/web/src/app/auth/register-form/register-form.component.ts
Normal file
103
apps/web/src/app/auth/register-form/register-form.component.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { UntypedFormBuilder } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { RegisterComponent as BaseRegisterComponent } from "@bitwarden/angular/components/register.component";
|
||||
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||
import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
|
||||
@Component({
|
||||
selector: "app-register-form",
|
||||
templateUrl: "./register-form.component.html",
|
||||
})
|
||||
export class RegisterFormComponent extends BaseRegisterComponent {
|
||||
@Input() queryParamEmail: string;
|
||||
@Input() enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
@Input() referenceDataValue: ReferenceEventRequest;
|
||||
|
||||
showErrorSummary = false;
|
||||
characterMinimumMessage: string;
|
||||
|
||||
constructor(
|
||||
formValidationErrorService: FormValidationErrorsService,
|
||||
formBuilder: UntypedFormBuilder,
|
||||
authService: AuthService,
|
||||
router: Router,
|
||||
i18nService: I18nService,
|
||||
cryptoService: CryptoService,
|
||||
apiService: ApiService,
|
||||
stateService: StateService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
private policyService: PolicyService,
|
||||
environmentService: EnvironmentService,
|
||||
logService: LogService,
|
||||
auditService: AuditService,
|
||||
dialogService: DialogServiceAbstraction
|
||||
) {
|
||||
super(
|
||||
formValidationErrorService,
|
||||
formBuilder,
|
||||
authService,
|
||||
router,
|
||||
i18nService,
|
||||
cryptoService,
|
||||
apiService,
|
||||
stateService,
|
||||
platformUtilsService,
|
||||
passwordGenerationService,
|
||||
environmentService,
|
||||
logService,
|
||||
auditService,
|
||||
dialogService
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await super.ngOnInit();
|
||||
this.referenceData = this.referenceDataValue;
|
||||
|
||||
if (this.queryParamEmail) {
|
||||
this.formGroup.get("email")?.setValue(this.queryParamEmail);
|
||||
}
|
||||
|
||||
if (this.enforcedPolicyOptions != null && this.enforcedPolicyOptions.minLength > 0) {
|
||||
this.characterMinimumMessage = "";
|
||||
} else {
|
||||
this.characterMinimumMessage = this.i18nService.t("characterMinimum", this.minimumLength);
|
||||
}
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (
|
||||
this.enforcedPolicyOptions != null &&
|
||||
!this.policyService.evaluateMasterPassword(
|
||||
this.passwordStrengthResult.score,
|
||||
this.formGroup.value.masterPassword,
|
||||
this.enforcedPolicyOptions
|
||||
)
|
||||
) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("masterPasswordPolicyRequirementsNotMet")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await super.submit(false);
|
||||
}
|
||||
}
|
||||
12
apps/web/src/app/auth/register-form/register-form.module.ts
Normal file
12
apps/web/src/app/auth/register-form/register-form.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
import { RegisterFormComponent } from "./register-form.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule],
|
||||
declarations: [RegisterFormComponent],
|
||||
exports: [RegisterFormComponent],
|
||||
})
|
||||
export class RegisterFormModule {}
|
||||
55
apps/web/src/app/auth/remove-password.component.html
Normal file
55
apps/web/src/app/auth/remove-password.component.html
Normal file
@@ -0,0 +1,55 @@
|
||||
<div class="mt-5 d-flex justify-content-center" *ngIf="loading">
|
||||
<div>
|
||||
<img class="mb-4 logo logo-themed" alt="Bitwarden" />
|
||||
<p class="text-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container" *ngIf="!loading">
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="lead text-center mb-4">{{ "removeMasterPassword" | i18n }}</p>
|
||||
<hr />
|
||||
<div class="card d-block">
|
||||
<div class="card-body">
|
||||
<p>{{ "convertOrganizationEncryptionDesc" | i18n : organization.name }}</p>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-block"
|
||||
(click)="convert()"
|
||||
[disabled]="actionPromise"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
*ngIf="continuing"
|
||||
></i>
|
||||
{{ "removeMasterPassword" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary btn-block"
|
||||
(click)="leave()"
|
||||
[disabled]="actionPromise"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
*ngIf="leaving"
|
||||
></i>
|
||||
{{ "leaveOrganization" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
9
apps/web/src/app/auth/remove-password.component.ts
Normal file
9
apps/web/src/app/auth/remove-password.component.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/angular/auth/components/remove-password.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-remove-password",
|
||||
templateUrl: "remove-password.component.html",
|
||||
})
|
||||
export class RemovePasswordComponent extends BaseRemovePasswordComponent {}
|
||||
121
apps/web/src/app/auth/set-password.component.html
Normal file
121
apps/web/src/app/auth/set-password.component.html
Normal file
@@ -0,0 +1,121 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate autocomplete="off">
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="lead text-center mb-4">{{ "setMasterPassword" | i18n }}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body text-center" *ngIf="syncLoading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
{{ "loading" | i18n }}
|
||||
</div>
|
||||
<div class="card-body" *ngIf="!syncLoading">
|
||||
<app-callout type="info">{{ "ssoCompleteRegistration" | i18n }}</app-callout>
|
||||
<app-callout
|
||||
type="warning"
|
||||
title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}"
|
||||
*ngIf="resetPasswordAutoEnroll"
|
||||
>
|
||||
{{ "resetPasswordAutoEnrollInviteWarning" | i18n }}
|
||||
</app-callout>
|
||||
<div class="form-group">
|
||||
<app-callout
|
||||
type="info"
|
||||
[enforcedPolicyOptions]="enforcedPolicyOptions"
|
||||
*ngIf="enforcedPolicyOptions"
|
||||
>
|
||||
</app-callout>
|
||||
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
|
||||
<div class="d-flex">
|
||||
<div class="w-100">
|
||||
<input
|
||||
id="masterPassword"
|
||||
type="{{ showPassword ? 'text' : 'password' }}"
|
||||
name="MasterPasswordHash"
|
||||
class="text-monospace form-control mb-1"
|
||||
[(ngModel)]="masterPassword"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
<app-password-strength
|
||||
[password]="masterPassword"
|
||||
[email]="email"
|
||||
[showText]="true"
|
||||
(passwordStrengthResult)="getStrengthResult($event)"
|
||||
>
|
||||
</app-password-strength>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-1 btn btn-link"
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="togglePassword(false)"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
|
||||
></i>
|
||||
</button>
|
||||
<div class="progress-bar invisible"></div>
|
||||
</div>
|
||||
</div>
|
||||
<small class="form-text text-muted">{{ "masterPassDesc" | i18n }}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="masterPasswordRetype">{{ "reTypeMasterPass" | i18n }}</label>
|
||||
<div class="d-flex">
|
||||
<input
|
||||
id="masterPasswordRetype"
|
||||
type="{{ showPassword ? 'text' : 'password' }}"
|
||||
name="MasterPasswordRetype"
|
||||
class="text-monospace form-control"
|
||||
[(ngModel)]="masterPasswordRetype"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-1 btn btn-link"
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="togglePassword(true)"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="hint">{{ "masterPassHint" | i18n }}</label>
|
||||
<input id="hint" class="form-control" type="text" name="Hint" [(ngModel)]="hint" />
|
||||
<small class="form-text text-muted">{{ "masterPassHintDesc" | i18n }}</small>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="d-flex">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-block btn-submit"
|
||||
[disabled]="form.loading"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span>{{ "submit" | i18n }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary btn-block ml-2 mt-0"
|
||||
(click)="logOut()"
|
||||
>
|
||||
{{ "logOut" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
59
apps/web/src/app/auth/set-password.component.ts
Normal file
59
apps/web/src/app/auth/set-password.component.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/components/set-password.component";
|
||||
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
|
||||
@Component({
|
||||
selector: "app-set-password",
|
||||
templateUrl: "set-password.component.html",
|
||||
})
|
||||
export class SetPasswordComponent extends BaseSetPasswordComponent {
|
||||
constructor(
|
||||
apiService: ApiService,
|
||||
i18nService: I18nService,
|
||||
cryptoService: CryptoService,
|
||||
messagingService: MessagingService,
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
policyApiService: PolicyApiServiceAbstraction,
|
||||
policyService: PolicyService,
|
||||
router: Router,
|
||||
syncService: SyncService,
|
||||
route: ActivatedRoute,
|
||||
stateService: StateService,
|
||||
organizationApiService: OrganizationApiServiceAbstraction,
|
||||
organizationUserService: OrganizationUserService,
|
||||
dialogService: DialogServiceAbstraction
|
||||
) {
|
||||
super(
|
||||
i18nService,
|
||||
cryptoService,
|
||||
messagingService,
|
||||
passwordGenerationService,
|
||||
platformUtilsService,
|
||||
policyApiService,
|
||||
policyService,
|
||||
router,
|
||||
apiService,
|
||||
syncService,
|
||||
route,
|
||||
stateService,
|
||||
organizationApiService,
|
||||
organizationUserService,
|
||||
dialogService
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="deAuthTitle">
|
||||
<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="deAuthTitle">{{ "deauthorizeSessions" | 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>{{ "deauthorizeSessionsDesc" | i18n }}</p>
|
||||
<app-callout type="warning">{{ "deauthorizeSessionsWarning" | i18n }}</app-callout>
|
||||
<app-user-verification [(ngModel)]="masterPassword" ngDefaultControl name="secret">
|
||||
</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>{{ "deauthorizeSessions" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { Verification } from "@bitwarden/common/types/verification";
|
||||
|
||||
@Component({
|
||||
selector: "app-deauthorize-sessions",
|
||||
templateUrl: "deauthorize-sessions.component.html",
|
||||
})
|
||||
export class DeauthorizeSessionsComponent {
|
||||
masterPassword: Verification;
|
||||
formPromise: Promise<unknown>;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private messagingService: MessagingService,
|
||||
private logService: LogService
|
||||
) {}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
this.formPromise = this.userVerificationService
|
||||
.buildRequest(this.masterPassword)
|
||||
.then((request) => this.apiService.postSecurityStamp(request));
|
||||
await this.formPromise;
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
this.i18nService.t("sessionsDeauthorized"),
|
||||
this.i18nService.t("logBackIn")
|
||||
);
|
||||
this.messagingService.send("logout");
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
||||
<form
|
||||
class="modal-content"
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
>
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="userAddEditTitle">
|
||||
<app-premium-badge *ngIf="readOnly"></app-premium-badge>
|
||||
{{ title }}
|
||||
<small class="text-muted" *ngIf="name">{{ name }}</small>
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" *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>
|
||||
<div class="modal-body" *ngIf="!loading">
|
||||
<ng-container *ngIf="!editMode">
|
||||
<p>{{ "inviteEmergencyContactDesc" | i18n }}</p>
|
||||
<div class="form-group mb-4">
|
||||
<label for="email">{{ "email" | i18n }}</label>
|
||||
<input
|
||||
id="email"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Email"
|
||||
[(ngModel)]="email"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</ng-container>
|
||||
<h3>
|
||||
{{ "userAccess" | i18n }}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
href="https://bitwarden.com/help/emergency-access/#user-access"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</h3>
|
||||
<div class="form-check mt-2 form-check-block">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="userType"
|
||||
id="emergencyTypeView"
|
||||
[value]="emergencyAccessType.View"
|
||||
[(ngModel)]="type"
|
||||
/>
|
||||
<label class="form-check-label" for="emergencyTypeView">
|
||||
{{ "view" | i18n }}
|
||||
<small>{{ "viewDesc" | i18n }}</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mt-2 form-check-block">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="userType"
|
||||
id="emergencyTypeTakeover"
|
||||
[value]="emergencyAccessType.Takeover"
|
||||
[(ngModel)]="type"
|
||||
[disabled]="readOnly"
|
||||
/>
|
||||
<label class="form-check-label" for="emergencyTypeTakeover">
|
||||
{{ "takeover" | i18n }}
|
||||
<small>{{ "takeoverDesc" | i18n }}</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group col-6 mt-4">
|
||||
<label for="waitTime">{{ "waitTime" | i18n }}</label>
|
||||
<select
|
||||
id="waitTime"
|
||||
name="waitTime"
|
||||
[(ngModel)]="waitTime"
|
||||
class="form-control"
|
||||
[disabled]="readOnly"
|
||||
>
|
||||
<option *ngFor="let o of waitTimes" [ngValue]="o.value">{{ o.name }}</option>
|
||||
</select>
|
||||
<small class="text-muted">{{ "waitTimeDesc" | i18n }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
bitButton
|
||||
[loading]="loading || form.loading"
|
||||
[disabled]="readOnly"
|
||||
>
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" type="button" data-dismiss="modal">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
<div class="ml-auto">
|
||||
<button
|
||||
#deleteBtn
|
||||
bitButton
|
||||
buttonType="danger"
|
||||
type="button"
|
||||
(click)="delete()"
|
||||
appA11yTitle="{{ 'delete' | i18n }}"
|
||||
*ngIf="editMode"
|
||||
[disabled]="$any(deleteBtn).loading"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-trash bwi-lg bwi-fw"
|
||||
[hidden]="$any(deleteBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!$any(deleteBtn).loading"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,103 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { EmergencyAccessType } from "@bitwarden/common/auth/enums/emergency-access-type";
|
||||
import { EmergencyAccessInviteRequest } from "@bitwarden/common/auth/models/request/emergency-access-invite.request";
|
||||
import { EmergencyAccessUpdateRequest } from "@bitwarden/common/auth/models/request/emergency-access-update.request";
|
||||
|
||||
@Component({
|
||||
selector: "emergency-access-add-edit",
|
||||
templateUrl: "emergency-access-add-edit.component.html",
|
||||
})
|
||||
export class EmergencyAccessAddEditComponent implements OnInit {
|
||||
@Input() name: string;
|
||||
@Input() emergencyAccessId: string;
|
||||
@Output() onSaved = new EventEmitter();
|
||||
@Output() onDeleted = new EventEmitter();
|
||||
|
||||
loading = true;
|
||||
readOnly = false;
|
||||
editMode = false;
|
||||
title: string;
|
||||
email: string;
|
||||
type: EmergencyAccessType = EmergencyAccessType.View;
|
||||
|
||||
formPromise: Promise<any>;
|
||||
|
||||
emergencyAccessType = EmergencyAccessType;
|
||||
waitTimes: { name: string; value: number }[];
|
||||
waitTime: number;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.editMode = this.loading = this.emergencyAccessId != null;
|
||||
|
||||
this.waitTimes = [
|
||||
{ name: this.i18nService.t("oneDay"), value: 1 },
|
||||
{ name: this.i18nService.t("days", "2"), value: 2 },
|
||||
{ name: this.i18nService.t("days", "7"), value: 7 },
|
||||
{ name: this.i18nService.t("days", "14"), value: 14 },
|
||||
{ name: this.i18nService.t("days", "30"), value: 30 },
|
||||
{ name: this.i18nService.t("days", "90"), value: 90 },
|
||||
];
|
||||
|
||||
if (this.editMode) {
|
||||
this.editMode = true;
|
||||
this.title = this.i18nService.t("editEmergencyContact");
|
||||
try {
|
||||
const emergencyAccess = await this.apiService.getEmergencyAccess(this.emergencyAccessId);
|
||||
this.type = emergencyAccess.type;
|
||||
this.waitTime = emergencyAccess.waitTimeDays;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
} else {
|
||||
this.title = this.i18nService.t("inviteEmergencyContact");
|
||||
this.waitTime = this.waitTimes[2].value;
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
if (this.editMode) {
|
||||
const request = new EmergencyAccessUpdateRequest();
|
||||
request.type = this.type;
|
||||
request.waitTimeDays = this.waitTime;
|
||||
|
||||
this.formPromise = this.apiService.putEmergencyAccess(this.emergencyAccessId, request);
|
||||
} else {
|
||||
const request = new EmergencyAccessInviteRequest();
|
||||
request.email = this.email.trim();
|
||||
request.type = this.type;
|
||||
request.waitTimeDays = this.waitTime;
|
||||
|
||||
this.formPromise = this.apiService.postEmergencyAccessInvite(request);
|
||||
}
|
||||
|
||||
await this.formPromise;
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.name)
|
||||
);
|
||||
this.onSaved.emit();
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async delete() {
|
||||
this.onDeleted.emit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
|
||||
import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
||||
|
||||
@Component({
|
||||
selector: "emergency-access-attachments",
|
||||
templateUrl: "../../../vault/individual-vault/attachments.component.html",
|
||||
})
|
||||
export class EmergencyAccessAttachmentsComponent extends BaseAttachmentsComponent {
|
||||
viewOnly = true;
|
||||
canAccessAttachments = true;
|
||||
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
i18nService: I18nService,
|
||||
cryptoService: CryptoService,
|
||||
stateService: StateService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
apiService: ApiService,
|
||||
logService: LogService,
|
||||
fileDownloadService: FileDownloadService,
|
||||
dialogService: DialogServiceAbstraction
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
i18nService,
|
||||
cryptoService,
|
||||
platformUtilsService,
|
||||
apiService,
|
||||
window,
|
||||
logService,
|
||||
stateService,
|
||||
fileDownloadService,
|
||||
dialogService
|
||||
);
|
||||
}
|
||||
|
||||
protected async init() {
|
||||
// Do nothing since cipher is already decoded
|
||||
}
|
||||
|
||||
protected showFixOldAttachments(attachment: AttachmentView) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="confirmUserTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="confirmUserTitle">
|
||||
{{ "confirmUser" | i18n }}
|
||||
<small class="text-muted" *ngIf="name">{{ name }}</small>
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
{{ "fingerprintEnsureIntegrityVerify" | i18n }}
|
||||
<a href="https://bitwarden.com/help/fingerprint-phrase/" target="_blank" rel="noopener">
|
||||
{{ "learnMore" | i18n }}</a
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<code>{{ fingerprint }}</code>
|
||||
</p>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="dontAskAgain"
|
||||
name="DontAskAgain"
|
||||
[(ngModel)]="dontAskAgain"
|
||||
/>
|
||||
<label class="form-check-label" for="dontAskAgain">
|
||||
{{ "dontAskFingerprintAgain" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<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>{{ "confirm" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
|
||||
@Component({
|
||||
selector: "emergency-access-confirm",
|
||||
templateUrl: "emergency-access-confirm.component.html",
|
||||
})
|
||||
export class EmergencyAccessConfirmComponent implements OnInit {
|
||||
@Input() name: string;
|
||||
@Input() userId: string;
|
||||
@Input() emergencyAccessId: string;
|
||||
@Input() formPromise: Promise<any>;
|
||||
@Output() onConfirmed = new EventEmitter();
|
||||
|
||||
dontAskAgain = false;
|
||||
loading = true;
|
||||
fingerprint: string;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private cryptoService: CryptoService,
|
||||
private stateService: StateService,
|
||||
private logService: LogService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
try {
|
||||
const publicKeyResponse = await this.apiService.getUserPublicKey(this.userId);
|
||||
if (publicKeyResponse != null) {
|
||||
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
|
||||
const fingerprint = await this.cryptoService.getFingerprint(this.userId, publicKey.buffer);
|
||||
if (fingerprint != null) {
|
||||
this.fingerprint = fingerprint.join("-");
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.dontAskAgain) {
|
||||
await this.stateService.setAutoConfirmFingerprints(true);
|
||||
}
|
||||
|
||||
try {
|
||||
this.onConfirmed.emit();
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
||||
<form
|
||||
class="modal-content"
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
>
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="userAddEditTitle">
|
||||
{{ "takeover" | i18n }}
|
||||
<small class="text-muted" *ngIf="name">{{ name }}</small>
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<app-callout type="warning">{{ "loggedOutWarning" | i18n }}</app-callout>
|
||||
<app-callout
|
||||
type="info"
|
||||
[enforcedPolicyOptions]="enforcedPolicyOptions"
|
||||
*ngIf="enforcedPolicyOptions"
|
||||
>
|
||||
</app-callout>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="masterPassword">{{ "newMasterPass" | i18n }}</label>
|
||||
<input
|
||||
id="masterPassword"
|
||||
type="password"
|
||||
name="NewMasterPasswordHash"
|
||||
class="form-control mb-1"
|
||||
[(ngModel)]="masterPassword"
|
||||
required
|
||||
appInputVerbatim
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<app-password-strength
|
||||
[password]="masterPassword"
|
||||
[email]="email"
|
||||
[showText]="true"
|
||||
(passwordStrengthResult)="getStrengthResult($event)"
|
||||
>
|
||||
</app-password-strength>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="masterPasswordRetype">{{ "confirmNewMasterPass" | i18n }}</label>
|
||||
<input
|
||||
id="masterPasswordRetype"
|
||||
type="password"
|
||||
name="MasterPasswordRetype"
|
||||
class="form-control"
|
||||
[(ngModel)]="masterPasswordRetype"
|
||||
required
|
||||
appInputVerbatim
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<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>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,131 @@
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { takeUntil } from "rxjs";
|
||||
|
||||
import { ChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component";
|
||||
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
|
||||
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||
import { EmergencyAccessPasswordRequest } from "@bitwarden/common/auth/models/request/emergency-access-password.request";
|
||||
import { KdfType } from "@bitwarden/common/enums";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
|
||||
@Component({
|
||||
selector: "emergency-access-takeover",
|
||||
templateUrl: "emergency-access-takeover.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class EmergencyAccessTakeoverComponent
|
||||
extends ChangePasswordComponent
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
@Output() onDone = new EventEmitter();
|
||||
@Input() emergencyAccessId: string;
|
||||
@Input() name: string;
|
||||
@Input() email: string;
|
||||
@Input() kdf: KdfType;
|
||||
@Input() kdfIterations: number;
|
||||
|
||||
formPromise: Promise<any>;
|
||||
|
||||
constructor(
|
||||
i18nService: I18nService,
|
||||
cryptoService: CryptoService,
|
||||
messagingService: MessagingService,
|
||||
stateService: StateService,
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
policyService: PolicyService,
|
||||
private apiService: ApiService,
|
||||
private logService: LogService,
|
||||
dialogService: DialogServiceAbstraction
|
||||
) {
|
||||
super(
|
||||
i18nService,
|
||||
cryptoService,
|
||||
messagingService,
|
||||
passwordGenerationService,
|
||||
platformUtilsService,
|
||||
policyService,
|
||||
stateService,
|
||||
dialogService
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
const response = await this.apiService.getEmergencyGrantorPolicies(this.emergencyAccessId);
|
||||
if (response.data != null && response.data.length > 0) {
|
||||
const policies = response.data.map(
|
||||
(policyResponse: PolicyResponse) => new Policy(new PolicyData(policyResponse))
|
||||
);
|
||||
|
||||
this.policyService
|
||||
.masterPasswordPolicyOptions$(policies)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((enforcedPolicyOptions) => (this.enforcedPolicyOptions = enforcedPolicyOptions));
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
ngOnDestroy(): void {
|
||||
super.ngOnDestroy();
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (!(await this.strongPassword())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const takeoverResponse = await this.apiService.postEmergencyAccessTakeover(
|
||||
this.emergencyAccessId
|
||||
);
|
||||
|
||||
const oldKeyBuffer = await this.cryptoService.rsaDecrypt(takeoverResponse.keyEncrypted);
|
||||
const oldEncKey = new SymmetricCryptoKey(oldKeyBuffer);
|
||||
|
||||
if (oldEncKey == null) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("unexpectedError")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const key = await this.cryptoService.makeKey(
|
||||
this.masterPassword,
|
||||
this.email,
|
||||
takeoverResponse.kdf,
|
||||
new KdfConfig(
|
||||
takeoverResponse.kdfIterations,
|
||||
takeoverResponse.kdfMemory,
|
||||
takeoverResponse.kdfParallelism
|
||||
)
|
||||
);
|
||||
const masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, key);
|
||||
|
||||
const encKey = await this.cryptoService.remakeEncKey(key, oldEncKey);
|
||||
|
||||
const request = new EmergencyAccessPasswordRequest();
|
||||
request.newMasterPasswordHash = masterPasswordHash;
|
||||
request.key = encKey[1].encryptedString;
|
||||
|
||||
this.apiService.postEmergencyAccessPassword(this.emergencyAccessId, request);
|
||||
|
||||
try {
|
||||
this.onDone.emit();
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<div class="page-header">
|
||||
<h1>{{ "vault" | i18n }}</h1>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<ng-container *ngIf="ciphers.length">
|
||||
<table class="table table-hover table-list table-ciphers">
|
||||
<tbody>
|
||||
<tr *ngFor="let c of ciphers">
|
||||
<td class="table-list-icon">
|
||||
<app-vault-icon [cipher]="c"></app-vault-icon>
|
||||
</td>
|
||||
<td class="reduced-lh wrap">
|
||||
<a href="#" appStopClick (click)="selectCipher(c)" title="{{ 'editItem' | i18n }}">{{
|
||||
c.name
|
||||
}}</a>
|
||||
<ng-container *ngIf="c.organizationId">
|
||||
<i
|
||||
class="bwi bwi-collection"
|
||||
appStopProp
|
||||
title="{{ 'shared' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "shared" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="c.hasAttachments">
|
||||
<i
|
||||
class="bwi bwi-paperclip"
|
||||
appStopProp
|
||||
title="{{ 'attachments' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "attachments" | i18n }}</span>
|
||||
</ng-container>
|
||||
<br />
|
||||
<small>{{ c.subTitle }}</small>
|
||||
</td>
|
||||
<td class="table-list-options">
|
||||
<div class="dropdown" appListDropdown *ngIf="c.hasAttachments">
|
||||
<button
|
||||
class="btn btn-outline-secondary dropdown-toggle"
|
||||
type="button"
|
||||
id="dropdownMenuButton"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="viewAttachments(c)">
|
||||
<i class="bwi bwi-fw bwi-paperclip" aria-hidden="true"></i>
|
||||
{{ "attachments" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!loaded">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ng-template #cipherAddEdit></ng-template>
|
||||
<ng-template #attachments></ng-template>
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { EmergencyAccessViewResponse } from "@bitwarden/common/auth/models/response/emergency-access.response";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { EmergencyAccessAttachmentsComponent } from "./emergency-access-attachments.component";
|
||||
import { EmergencyAddEditComponent } from "./emergency-add-edit.component";
|
||||
|
||||
@Component({
|
||||
selector: "emergency-access-view",
|
||||
templateUrl: "emergency-access-view.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class EmergencyAccessViewComponent implements OnInit {
|
||||
@ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true })
|
||||
cipherAddEditModalRef: ViewContainerRef;
|
||||
@ViewChild("attachments", { read: ViewContainerRef, static: true })
|
||||
attachmentsModalRef: ViewContainerRef;
|
||||
|
||||
id: string;
|
||||
ciphers: CipherView[] = [];
|
||||
loaded = false;
|
||||
|
||||
constructor(
|
||||
private cipherService: CipherService,
|
||||
private cryptoService: CryptoService,
|
||||
private modalService: ModalService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private apiService: ApiService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
this.route.params.subscribe((qParams) => {
|
||||
if (qParams.id == null) {
|
||||
return this.router.navigate(["settings/emergency-access"]);
|
||||
}
|
||||
|
||||
this.id = qParams.id;
|
||||
|
||||
this.load();
|
||||
});
|
||||
}
|
||||
|
||||
async selectCipher(cipher: CipherView) {
|
||||
// eslint-disable-next-line
|
||||
const [_, childComponent] = await this.modalService.openViewRef(
|
||||
EmergencyAddEditComponent,
|
||||
this.cipherAddEditModalRef,
|
||||
(comp) => {
|
||||
comp.cipherId = cipher == null ? null : cipher.id;
|
||||
comp.cipher = cipher;
|
||||
}
|
||||
);
|
||||
|
||||
return childComponent;
|
||||
}
|
||||
|
||||
async load() {
|
||||
const response = await this.apiService.postEmergencyAccessView(this.id);
|
||||
this.ciphers = await this.getAllCiphers(response);
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
async viewAttachments(cipher: CipherView) {
|
||||
await this.modalService.openViewRef(
|
||||
EmergencyAccessAttachmentsComponent,
|
||||
this.attachmentsModalRef,
|
||||
(comp) => {
|
||||
comp.cipher = cipher;
|
||||
comp.emergencyAccessId = this.id;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected async getAllCiphers(response: EmergencyAccessViewResponse): Promise<CipherView[]> {
|
||||
const ciphers = response.ciphers;
|
||||
|
||||
const decCiphers: CipherView[] = [];
|
||||
const oldKeyBuffer = await this.cryptoService.rsaDecrypt(response.keyEncrypted);
|
||||
const oldEncKey = new SymmetricCryptoKey(oldKeyBuffer);
|
||||
|
||||
const promises: any[] = [];
|
||||
ciphers.forEach((cipherResponse) => {
|
||||
const cipherData = new CipherData(cipherResponse);
|
||||
const cipher = new Cipher(cipherData);
|
||||
promises.push(cipher.decrypt(oldEncKey).then((c) => decCiphers.push(c)));
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
decCiphers.sort(this.cipherService.getLocaleSortingFunction());
|
||||
|
||||
return decCiphers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
<!-- Please remove this disable statement when editing this file! -->
|
||||
<!-- eslint-disable @angular-eslint/template/button-has-type -->
|
||||
<div class="page-header">
|
||||
<h1>{{ "emergencyAccess" | i18n }}</h1>
|
||||
</div>
|
||||
<p>
|
||||
{{ "emergencyAccessDesc" | i18n }}
|
||||
<a href="https://bitwarden.com/help/emergency-access/" target="_blank" rel="noopener">
|
||||
{{ "learnMore" | i18n }}.
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p *ngIf="isOrganizationOwner">
|
||||
<b>{{ "warning" | i18n }}:</b> {{ "emergencyAccessOwnerWarning" | i18n }}
|
||||
</p>
|
||||
|
||||
<div class="page-header d-flex">
|
||||
<h2>
|
||||
{{ "trustedEmergencyContacts" | i18n }}
|
||||
<app-premium-badge></app-premium-badge>
|
||||
</h2>
|
||||
<div class="ml-auto d-flex">
|
||||
<button
|
||||
class="btn btn-sm btn-outline-primary ml-3"
|
||||
type="button"
|
||||
(click)="invite()"
|
||||
[disabled]="!canAccessPremium"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-plus bwi-fw"></i>
|
||||
{{ "addEmergencyContact" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-hover table-list mb-0" *ngIf="trustedContacts && trustedContacts.length">
|
||||
<tbody>
|
||||
<tr *ngFor="let c of trustedContacts; let i = index">
|
||||
<td width="30">
|
||||
<bit-avatar
|
||||
[text]="c | userName"
|
||||
[id]="c.granteeId"
|
||||
[color]="c.avatarColor"
|
||||
size="small"
|
||||
></bit-avatar>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" appStopClick (click)="edit(c)">{{ c.email }}</a>
|
||||
<span
|
||||
bitBadge
|
||||
badgeType="secondary"
|
||||
*ngIf="c.status === emergencyAccessStatusType.Invited"
|
||||
>{{ "invited" | i18n }}</span
|
||||
>
|
||||
<span
|
||||
bitBadge
|
||||
badgeType="warning"
|
||||
*ngIf="c.status === emergencyAccessStatusType.Accepted"
|
||||
>{{ "accepted" | i18n }}</span
|
||||
>
|
||||
<span
|
||||
bitBadge
|
||||
badgeType="warning"
|
||||
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated"
|
||||
>{{ "emergencyAccessRecoveryInitiated" | i18n }}</span
|
||||
>
|
||||
<span bitBadge *ngIf="c.status === emergencyAccessStatusType.RecoveryApproved">{{
|
||||
"emergencyAccessRecoveryApproved" | i18n
|
||||
}}</span>
|
||||
|
||||
<span bitBadge *ngIf="c.type === emergencyAccessType.View">{{ "view" | i18n }}</span>
|
||||
<span bitBadge *ngIf="c.type === emergencyAccessType.Takeover">{{
|
||||
"takeover" | i18n
|
||||
}}</span>
|
||||
|
||||
<small class="text-muted d-block" *ngIf="c.name">{{ c.name }}</small>
|
||||
</td>
|
||||
<td class="table-list-options">
|
||||
<button
|
||||
[bitMenuTriggerFor]="trustedContactOptions"
|
||||
class="tw-border-none tw-bg-transparent tw-text-main"
|
||||
type="button"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-ellipsis-v bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<bit-menu #trustedContactOptions>
|
||||
<button
|
||||
bitMenuItem
|
||||
*ngIf="c.status === emergencyAccessStatusType.Invited"
|
||||
(click)="reinvite(c)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
|
||||
{{ "resendInvitation" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
bitMenuItem
|
||||
*ngIf="c.status === emergencyAccessStatusType.Accepted"
|
||||
(click)="confirm(c)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
||||
{{ "confirm" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
bitMenuItem
|
||||
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated"
|
||||
(click)="approve(c)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
||||
{{ "approve" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
bitMenuItem
|
||||
*ngIf="
|
||||
c.status === emergencyAccessStatusType.RecoveryInitiated ||
|
||||
c.status === emergencyAccessStatusType.RecoveryApproved
|
||||
"
|
||||
(click)="reject(c)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
|
||||
{{ "reject" | i18n }}
|
||||
</button>
|
||||
<button bitMenuItem (click)="remove(c)">
|
||||
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
|
||||
{{ "remove" | i18n }}
|
||||
</button>
|
||||
</bit-menu>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<ng-container *ngIf="!trustedContacts || !trustedContacts.length">
|
||||
<p *ngIf="loaded">{{ "noTrustedContacts" | i18n }}</p>
|
||||
<ng-container *ngIf="!loaded">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<div class="page-header spaced-header">
|
||||
<h2>{{ "designatedEmergencyContacts" | i18n }}</h2>
|
||||
</div>
|
||||
|
||||
<table class="table table-hover table-list mb-0" *ngIf="grantedContacts && grantedContacts.length">
|
||||
<tbody>
|
||||
<tr *ngFor="let c of grantedContacts; let i = index">
|
||||
<td width="30">
|
||||
<bit-avatar
|
||||
[text]="c | userName"
|
||||
[id]="c.grantorId"
|
||||
[color]="c.avatarColor"
|
||||
size="small"
|
||||
></bit-avatar>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ c.email }}</span>
|
||||
<span bitBadge *ngIf="c.status === emergencyAccessStatusType.Invited">{{
|
||||
"invited" | i18n
|
||||
}}</span>
|
||||
<span
|
||||
bitBadge
|
||||
badgeType="warning"
|
||||
*ngIf="c.status === emergencyAccessStatusType.Accepted"
|
||||
>{{ "accepted" | i18n }}</span
|
||||
>
|
||||
<span
|
||||
bitBadge
|
||||
badgeType="warning"
|
||||
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated"
|
||||
>{{ "emergencyAccessRecoveryInitiated" | i18n }}</span
|
||||
>
|
||||
<span
|
||||
bitBadge
|
||||
badgeType="success"
|
||||
*ngIf="c.status === emergencyAccessStatusType.RecoveryApproved"
|
||||
>{{ "emergencyAccessRecoveryApproved" | i18n }}</span
|
||||
>
|
||||
|
||||
<span bitBadge *ngIf="c.type === emergencyAccessType.View">{{ "view" | i18n }}</span>
|
||||
<span bitBadge *ngIf="c.type === emergencyAccessType.Takeover">{{
|
||||
"takeover" | i18n
|
||||
}}</span>
|
||||
|
||||
<small class="text-muted d-block" *ngIf="c.name">{{ c.name }}</small>
|
||||
</td>
|
||||
<td class="table-list-options">
|
||||
<button
|
||||
[bitMenuTriggerFor]="grantedContactOptions"
|
||||
class="tw-border-none tw-bg-transparent tw-text-main"
|
||||
type="button"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-ellipsis-v bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<bit-menu #grantedContactOptions>
|
||||
<button
|
||||
bitMenuItem
|
||||
*ngIf="c.status === emergencyAccessStatusType.Confirmed"
|
||||
(click)="requestAccess(c)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
|
||||
{{ "requestAccess" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
bitMenuItem
|
||||
*ngIf="
|
||||
c.status === emergencyAccessStatusType.RecoveryApproved &&
|
||||
c.type === emergencyAccessType.Takeover
|
||||
"
|
||||
(click)="takeover(c)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-key" aria-hidden="true"></i>
|
||||
{{ "takeover" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
bitMenuItem
|
||||
*ngIf="
|
||||
c.status === emergencyAccessStatusType.RecoveryApproved &&
|
||||
c.type === emergencyAccessType.View
|
||||
"
|
||||
[routerLink]="c.id"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-eye" aria-hidden="true"></i>
|
||||
{{ "view" | i18n }}
|
||||
</button>
|
||||
<button bitMenuItem (click)="remove(c)">
|
||||
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
|
||||
{{ "remove" | i18n }}
|
||||
</button>
|
||||
</bit-menu>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<ng-container *ngIf="!grantedContacts || !grantedContacts.length">
|
||||
<p *ngIf="loaded">{{ "noGrantedAccess" | i18n }}</p>
|
||||
<ng-container *ngIf="!loaded">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #addEdit></ng-template>
|
||||
<ng-template #takeoverTemplate></ng-template>
|
||||
<ng-template #confirmTemplate></ng-template>
|
||||
@@ -0,0 +1,323 @@
|
||||
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
|
||||
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
|
||||
import { DialogServiceAbstraction, SimpleDialogType } from "@bitwarden/angular/services/dialog";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { EmergencyAccessStatusType } from "@bitwarden/common/auth/enums/emergency-access-status-type";
|
||||
import { EmergencyAccessType } from "@bitwarden/common/auth/enums/emergency-access-type";
|
||||
import { EmergencyAccessConfirmRequest } from "@bitwarden/common/auth/models/request/emergency-access-confirm.request";
|
||||
import {
|
||||
EmergencyAccessGranteeDetailsResponse,
|
||||
EmergencyAccessGrantorDetailsResponse,
|
||||
} from "@bitwarden/common/auth/models/response/emergency-access.response";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
|
||||
import { EmergencyAccessAddEditComponent } from "./emergency-access-add-edit.component";
|
||||
import { EmergencyAccessConfirmComponent } from "./emergency-access-confirm.component";
|
||||
import { EmergencyAccessTakeoverComponent } from "./emergency-access-takeover.component";
|
||||
|
||||
@Component({
|
||||
selector: "emergency-access",
|
||||
templateUrl: "emergency-access.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class EmergencyAccessComponent implements OnInit {
|
||||
@ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
||||
@ViewChild("takeoverTemplate", { read: ViewContainerRef, static: true })
|
||||
takeoverModalRef: ViewContainerRef;
|
||||
@ViewChild("confirmTemplate", { read: ViewContainerRef, static: true })
|
||||
confirmModalRef: ViewContainerRef;
|
||||
|
||||
loaded = false;
|
||||
canAccessPremium: boolean;
|
||||
trustedContacts: EmergencyAccessGranteeDetailsResponse[];
|
||||
grantedContacts: EmergencyAccessGrantorDetailsResponse[];
|
||||
emergencyAccessType = EmergencyAccessType;
|
||||
emergencyAccessStatusType = EmergencyAccessStatusType;
|
||||
actionPromise: Promise<any>;
|
||||
isOrganizationOwner: boolean;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private modalService: ModalService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private cryptoService: CryptoService,
|
||||
private messagingService: MessagingService,
|
||||
private userNamePipe: UserNamePipe,
|
||||
private logService: LogService,
|
||||
private stateService: StateService,
|
||||
private organizationService: OrganizationService,
|
||||
protected dialogService: DialogServiceAbstraction
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.canAccessPremium = await this.stateService.getCanAccessPremium();
|
||||
const orgs = await this.organizationService.getAll();
|
||||
this.isOrganizationOwner = orgs.some((o) => o.isOwner);
|
||||
this.load();
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.trustedContacts = (await this.apiService.getEmergencyAccessTrusted()).data;
|
||||
this.grantedContacts = (await this.apiService.getEmergencyAccessGranted()).data;
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
async premiumRequired() {
|
||||
if (!this.canAccessPremium) {
|
||||
this.messagingService.send("premiumRequired");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async edit(details: EmergencyAccessGranteeDetailsResponse) {
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
EmergencyAccessAddEditComponent,
|
||||
this.addEditModalRef,
|
||||
(comp) => {
|
||||
comp.name = this.userNamePipe.transform(details);
|
||||
comp.emergencyAccessId = details?.id;
|
||||
comp.readOnly = !this.canAccessPremium;
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
comp.onSaved.subscribe(() => {
|
||||
modal.close();
|
||||
this.load();
|
||||
});
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
comp.onDeleted.subscribe(() => {
|
||||
modal.close();
|
||||
this.remove(details);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
invite() {
|
||||
this.edit(null);
|
||||
}
|
||||
|
||||
async reinvite(contact: EmergencyAccessGranteeDetailsResponse) {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
this.actionPromise = this.apiService.postEmergencyAccessReinvite(contact.id);
|
||||
await this.actionPromise;
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("hasBeenReinvited", contact.email)
|
||||
);
|
||||
this.actionPromise = null;
|
||||
}
|
||||
|
||||
async confirm(contact: EmergencyAccessGranteeDetailsResponse) {
|
||||
function updateUser() {
|
||||
contact.status = EmergencyAccessStatusType.Confirmed;
|
||||
}
|
||||
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const autoConfirm = await this.stateService.getAutoConfirmFingerPrints();
|
||||
if (autoConfirm == null || !autoConfirm) {
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
EmergencyAccessConfirmComponent,
|
||||
this.confirmModalRef,
|
||||
(comp) => {
|
||||
comp.name = this.userNamePipe.transform(contact);
|
||||
comp.emergencyAccessId = contact.id;
|
||||
comp.userId = contact?.granteeId;
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
comp.onConfirmed.subscribe(async () => {
|
||||
modal.close();
|
||||
|
||||
comp.formPromise = this.doConfirmation(contact);
|
||||
await comp.formPromise;
|
||||
|
||||
updateUser();
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(contact))
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionPromise = this.doConfirmation(contact);
|
||||
await this.actionPromise;
|
||||
updateUser();
|
||||
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(contact))
|
||||
);
|
||||
this.actionPromise = null;
|
||||
}
|
||||
|
||||
async remove(
|
||||
details: EmergencyAccessGranteeDetailsResponse | EmergencyAccessGrantorDetailsResponse
|
||||
) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: this.userNamePipe.transform(details),
|
||||
content: { key: "removeUserConfirmation" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.apiService.deleteEmergencyAccess(details.id);
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("removedUserId", this.userNamePipe.transform(details))
|
||||
);
|
||||
|
||||
if (details instanceof EmergencyAccessGranteeDetailsResponse) {
|
||||
this.removeGrantee(details);
|
||||
} else {
|
||||
this.removeGrantor(details);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async requestAccess(details: EmergencyAccessGrantorDetailsResponse) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: this.userNamePipe.transform(details),
|
||||
content: {
|
||||
key: "requestAccessConfirmation",
|
||||
placeholders: [details.waitTimeDays.toString()],
|
||||
},
|
||||
acceptButtonText: { key: "requestAccess" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.apiService.postEmergencyAccessInitiate(details.id);
|
||||
|
||||
details.status = EmergencyAccessStatusType.RecoveryInitiated;
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("requestSent", this.userNamePipe.transform(details))
|
||||
);
|
||||
}
|
||||
|
||||
async approve(details: EmergencyAccessGranteeDetailsResponse) {
|
||||
const type = this.i18nService.t(
|
||||
details.type === EmergencyAccessType.View ? "view" : "takeover"
|
||||
);
|
||||
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: this.userNamePipe.transform(details),
|
||||
content: {
|
||||
key: "approveAccessConfirmation",
|
||||
placeholders: [this.userNamePipe.transform(details), type],
|
||||
},
|
||||
acceptButtonText: { key: "approve" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.apiService.postEmergencyAccessApprove(details.id);
|
||||
details.status = EmergencyAccessStatusType.RecoveryApproved;
|
||||
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("emergencyApproved", this.userNamePipe.transform(details))
|
||||
);
|
||||
}
|
||||
|
||||
async reject(details: EmergencyAccessGranteeDetailsResponse) {
|
||||
await this.apiService.postEmergencyAccessReject(details.id);
|
||||
details.status = EmergencyAccessStatusType.Confirmed;
|
||||
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("emergencyRejected", this.userNamePipe.transform(details))
|
||||
);
|
||||
}
|
||||
|
||||
async takeover(details: EmergencyAccessGrantorDetailsResponse) {
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
EmergencyAccessTakeoverComponent,
|
||||
this.takeoverModalRef,
|
||||
(comp) => {
|
||||
comp.name = this.userNamePipe.transform(details);
|
||||
comp.email = details.email;
|
||||
comp.emergencyAccessId = details != null ? details.id : null;
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
comp.onDone.subscribe(() => {
|
||||
modal.close();
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("passwordResetFor", this.userNamePipe.transform(details))
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private removeGrantee(details: EmergencyAccessGranteeDetailsResponse) {
|
||||
const index = this.trustedContacts.indexOf(details);
|
||||
if (index > -1) {
|
||||
this.trustedContacts.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private removeGrantor(details: EmergencyAccessGrantorDetailsResponse) {
|
||||
const index = this.grantedContacts.indexOf(details);
|
||||
if (index > -1) {
|
||||
this.grantedContacts.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypt the master password hash using the grantees public key, and send it to bitwarden for escrow.
|
||||
private async doConfirmation(details: EmergencyAccessGranteeDetailsResponse) {
|
||||
const encKey = await this.cryptoService.getEncKey();
|
||||
const publicKeyResponse = await this.apiService.getUserPublicKey(details.granteeId);
|
||||
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
|
||||
|
||||
try {
|
||||
this.logService.debug(
|
||||
"User's fingerprint: " +
|
||||
(await this.cryptoService.getFingerprint(details.granteeId, publicKey.buffer)).join("-")
|
||||
);
|
||||
} catch {
|
||||
// Ignore errors since it's just a debug message
|
||||
}
|
||||
|
||||
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey.buffer);
|
||||
const request = new EmergencyAccessConfirmRequest();
|
||||
request.key = encryptedKey.encryptedString;
|
||||
await this.apiService.postEmergencyAccessConfirm(details.id, request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
|
||||
import { CollectionService } from "@bitwarden/common/admin-console/abstractions/collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password/";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { PasswordRepromptService } from "@bitwarden/common/vault/abstractions/password-reprompt.service";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
|
||||
import { AddEditComponent as BaseAddEditComponent } from "../../../vault/individual-vault/add-edit.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-org-vault-add-edit",
|
||||
templateUrl: "../../../vault/individual-vault/add-edit.component.html",
|
||||
})
|
||||
export class EmergencyAddEditComponent extends BaseAddEditComponent {
|
||||
originalCipher: Cipher = null;
|
||||
viewOnly = true;
|
||||
protected override componentName = "app-org-vault-add-edit";
|
||||
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
folderService: FolderService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
auditService: AuditService,
|
||||
stateService: StateService,
|
||||
collectionService: CollectionService,
|
||||
totpService: TotpService,
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
messagingService: MessagingService,
|
||||
eventCollectionService: EventCollectionService,
|
||||
policyService: PolicyService,
|
||||
passwordRepromptService: PasswordRepromptService,
|
||||
organizationService: OrganizationService,
|
||||
logService: LogService,
|
||||
sendApiService: SendApiService,
|
||||
dialogService: DialogServiceAbstraction
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
folderService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
auditService,
|
||||
stateService,
|
||||
collectionService,
|
||||
totpService,
|
||||
passwordGenerationService,
|
||||
messagingService,
|
||||
eventCollectionService,
|
||||
policyService,
|
||||
organizationService,
|
||||
logService,
|
||||
passwordRepromptService,
|
||||
sendApiService,
|
||||
dialogService
|
||||
);
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.title = this.i18nService.t("viewItem");
|
||||
}
|
||||
|
||||
protected async loadCipher() {
|
||||
return Promise.resolve(this.originalCipher);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faAuthenticatorTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="2faAuthenticatorTitle">
|
||||
{{ "twoStepLogin" | i18n }}
|
||||
<small>{{ "authenticatorAppTitle" | i18n }}</small>
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<app-two-factor-verify
|
||||
[organizationId]="organizationId"
|
||||
[type]="type"
|
||||
(onAuthed)="auth($any($event))"
|
||||
*ngIf="!authed"
|
||||
>
|
||||
</app-two-factor-verify>
|
||||
<form
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
*ngIf="authed"
|
||||
>
|
||||
<div class="modal-body">
|
||||
<ng-container *ngIf="!enabled">
|
||||
<img class="float-right mfaType0" alt="Authenticator app logo" />
|
||||
<p>{{ "twoStepAuthenticatorDesc" | i18n }}</p>
|
||||
<p>
|
||||
<strong>1. {{ "twoStepAuthenticatorDownloadApp" | i18n }}</strong>
|
||||
</p>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="enabled">
|
||||
<app-callout type="success" title="{{ 'enabled' | i18n }}" icon="bwi bwi-check-circle">
|
||||
<p>{{ "twoStepLoginProviderEnabled" | i18n }}</p>
|
||||
{{ "twoStepAuthenticatorReaddDesc" | i18n }}
|
||||
</app-callout>
|
||||
<img class="float-right mfaType0" alt="Authenticator app logo" />
|
||||
<p>{{ "twoStepAuthenticatorNeedApp" | i18n }}</p>
|
||||
</ng-container>
|
||||
<ul class="bwi-ul">
|
||||
<li>
|
||||
<i class="bwi bwi-li bwi-apple"></i>{{ "iosDevices" | i18n }}:
|
||||
<a
|
||||
href="https://itunes.apple.com/us/app/authy/id494168017?mt=8"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>Authy</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-li bwi-android"></i>{{ "androidDevices" | i18n }}:
|
||||
<a
|
||||
href="https://play.google.com/store/apps/details?id=com.authy.authy"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>Authy</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-li bwi-windows"></i>{{ "windowsDevices" | i18n }}:
|
||||
<a
|
||||
href="https://www.microsoft.com/p/authenticator/9wzdncrfj3rj"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>Microsoft Authenticator</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<p>{{ "twoStepAuthenticatorAppsRecommended" | i18n }}</p>
|
||||
<p *ngIf="!enabled">
|
||||
<strong>2. {{ "twoStepAuthenticatorScanCode" | i18n }}</strong>
|
||||
</p>
|
||||
<hr *ngIf="enabled" />
|
||||
<p class="text-center" [ngClass]="{ 'mb-0': enabled }">
|
||||
<canvas id="qr"></canvas><br />
|
||||
<code appA11yTitle="{{ 'key' | i18n }}">{{ key }}</code>
|
||||
</p>
|
||||
<ng-container *ngIf="!enabled">
|
||||
<label for="token">3. {{ "twoStepAuthenticatorEnterCode" | i18n }}</label>
|
||||
<input
|
||||
id="token"
|
||||
type="text"
|
||||
name="Token"
|
||||
class="form-control"
|
||||
[(ngModel)]="token"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<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="!enabled">{{ "enable" | i18n }}</span>
|
||||
<span *ngIf="enabled">{{ "disable" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
|
||||
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { UpdateTwoFactorAuthenticatorRequest } from "@bitwarden/common/auth/models/request/update-two-factor-authenticator.request";
|
||||
import { TwoFactorAuthenticatorResponse } from "@bitwarden/common/auth/models/response/two-factor-authenticator.response";
|
||||
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
|
||||
import { TwoFactorBaseComponent } from "./two-factor-base.component";
|
||||
|
||||
// NOTE: There are additional options available but these are just the ones we are current using.
|
||||
// See: https://github.com/neocotic/qrious#examples
|
||||
interface QRiousOptions {
|
||||
element: HTMLElement;
|
||||
value: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
QRious: new (options: QRiousOptions) => unknown;
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-two-factor-authenticator",
|
||||
templateUrl: "two-factor-authenticator.component.html",
|
||||
})
|
||||
export class TwoFactorAuthenticatorComponent
|
||||
extends TwoFactorBaseComponent
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
type = TwoFactorProviderType.Authenticator;
|
||||
key: string;
|
||||
token: string;
|
||||
formPromise: Promise<TwoFactorAuthenticatorResponse>;
|
||||
|
||||
override componentName = "app-two-factor-authenticator";
|
||||
private qrScript: HTMLScriptElement;
|
||||
|
||||
constructor(
|
||||
apiService: ApiService,
|
||||
i18nService: I18nService,
|
||||
userVerificationService: UserVerificationService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
logService: LogService,
|
||||
private stateService: StateService,
|
||||
dialogService: DialogServiceAbstraction
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
logService,
|
||||
userVerificationService,
|
||||
dialogService
|
||||
);
|
||||
this.qrScript = window.document.createElement("script");
|
||||
this.qrScript.src = "scripts/qrious.min.js";
|
||||
this.qrScript.async = true;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
window.document.body.appendChild(this.qrScript);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
window.document.body.removeChild(this.qrScript);
|
||||
}
|
||||
|
||||
auth(authResponse: AuthResponse<TwoFactorAuthenticatorResponse>) {
|
||||
super.auth(authResponse);
|
||||
return this.processResponse(authResponse.response);
|
||||
}
|
||||
|
||||
submit() {
|
||||
if (this.enabled) {
|
||||
return super.disable(this.formPromise);
|
||||
} else {
|
||||
return this.enable();
|
||||
}
|
||||
}
|
||||
|
||||
protected async enable() {
|
||||
const request = await this.buildRequestModel(UpdateTwoFactorAuthenticatorRequest);
|
||||
request.token = this.token;
|
||||
request.key = this.key;
|
||||
|
||||
return super.enable(async () => {
|
||||
this.formPromise = this.apiService.putTwoFactorAuthenticator(request);
|
||||
const response = await this.formPromise;
|
||||
await this.processResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
private async processResponse(response: TwoFactorAuthenticatorResponse) {
|
||||
this.token = null;
|
||||
this.enabled = response.enabled;
|
||||
this.key = response.key;
|
||||
const email = await this.stateService.getEmail();
|
||||
window.setTimeout(() => {
|
||||
new window.QRious({
|
||||
element: document.getElementById("qr"),
|
||||
value:
|
||||
"otpauth://totp/Bitwarden:" +
|
||||
Utils.encodeRFC3986URIComponent(email) +
|
||||
"?secret=" +
|
||||
encodeURIComponent(this.key) +
|
||||
"&issuer=Bitwarden",
|
||||
size: 160,
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
93
apps/web/src/app/auth/settings/two-factor-base.component.ts
Normal file
93
apps/web/src/app/auth/settings/two-factor-base.component.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Directive, EventEmitter, Output } from "@angular/core";
|
||||
|
||||
import { DialogServiceAbstraction, SimpleDialogType } from "@bitwarden/angular/services/dialog";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
|
||||
import { TwoFactorProviderRequest } from "@bitwarden/common/auth/models/request/two-factor-provider.request";
|
||||
import { AuthResponseBase } from "@bitwarden/common/auth/types/auth-response";
|
||||
|
||||
@Directive()
|
||||
export abstract class TwoFactorBaseComponent {
|
||||
@Output() onUpdated = new EventEmitter<boolean>();
|
||||
|
||||
type: TwoFactorProviderType;
|
||||
organizationId: string;
|
||||
twoFactorProviderType = TwoFactorProviderType;
|
||||
enabled = false;
|
||||
authed = false;
|
||||
|
||||
protected hashedSecret: string;
|
||||
protected verificationType: VerificationType;
|
||||
protected componentName = "";
|
||||
|
||||
constructor(
|
||||
protected apiService: ApiService,
|
||||
protected i18nService: I18nService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected logService: LogService,
|
||||
protected userVerificationService: UserVerificationService,
|
||||
protected dialogService: DialogServiceAbstraction
|
||||
) {}
|
||||
|
||||
protected auth(authResponse: AuthResponseBase) {
|
||||
this.hashedSecret = authResponse.secret;
|
||||
this.verificationType = authResponse.verificationType;
|
||||
this.authed = true;
|
||||
}
|
||||
|
||||
protected async enable(enableFunction: () => Promise<void>) {
|
||||
try {
|
||||
await enableFunction();
|
||||
this.onUpdated.emit(true);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected async disable(promise: Promise<unknown>) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "disable" },
|
||||
content: { key: "twoStepDisableDesc" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const request = await this.buildRequestModel(TwoFactorProviderRequest);
|
||||
request.type = this.type;
|
||||
if (this.organizationId != null) {
|
||||
promise = this.apiService.putTwoFactorOrganizationDisable(this.organizationId, request);
|
||||
} else {
|
||||
promise = this.apiService.putTwoFactorDisable(request);
|
||||
}
|
||||
await promise;
|
||||
this.enabled = false;
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("twoStepDisabled"));
|
||||
this.onUpdated.emit(false);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected async buildRequestModel<T extends SecretVerificationRequest>(
|
||||
requestClass: new () => T
|
||||
) {
|
||||
return this.userVerificationService.buildRequest(
|
||||
{
|
||||
secret: this.hashedSecret,
|
||||
type: this.verificationType,
|
||||
},
|
||||
requestClass,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
105
apps/web/src/app/auth/settings/two-factor-duo.component.html
Normal file
105
apps/web/src/app/auth/settings/two-factor-duo.component.html
Normal file
@@ -0,0 +1,105 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faDuoTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="2faDuoTitle">
|
||||
{{ "twoStepLogin" | i18n }}
|
||||
<small>Duo</small>
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<app-two-factor-verify
|
||||
[organizationId]="organizationId"
|
||||
[type]="type"
|
||||
(onAuthed)="auth($any($event))"
|
||||
*ngIf="!authed"
|
||||
>
|
||||
</app-two-factor-verify>
|
||||
<form
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
*ngIf="authed"
|
||||
autocomplete="off"
|
||||
>
|
||||
<div class="modal-body">
|
||||
<ng-container *ngIf="enabled">
|
||||
<app-callout type="success" title="{{ 'enabled' | i18n }}" icon="bwi bwi-check-circle">
|
||||
{{ "twoStepLoginProviderEnabled" | i18n }}
|
||||
</app-callout>
|
||||
<img class="float-right ml-3 mfaType2" alt="Duo logo" />
|
||||
<strong>{{ "twoFactorDuoIntegrationKey" | i18n }}:</strong> {{ ikey }}
|
||||
<br />
|
||||
<strong>{{ "twoFactorDuoSecretKey" | i18n }}:</strong> {{ skey }}
|
||||
<br />
|
||||
<strong>{{ "twoFactorDuoApiHostname" | i18n }}:</strong> {{ host }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!enabled">
|
||||
<img class="float-right ml-3 mfaType2" alt="Duo logo" />
|
||||
<p>{{ "twoFactorDuoDesc" | i18n }}</p>
|
||||
<div class="form-group">
|
||||
<label for="ikey">{{ "twoFactorDuoIntegrationKey" | i18n }}</label>
|
||||
<input
|
||||
id="ikey"
|
||||
type="text"
|
||||
name="IntegrationKey"
|
||||
class="form-control"
|
||||
[(ngModel)]="ikey"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="skey">{{ "twoFactorDuoSecretKey" | i18n }}</label>
|
||||
<input
|
||||
id="skey"
|
||||
type="password"
|
||||
name="SecretKey"
|
||||
class="form-control"
|
||||
[(ngModel)]="skey"
|
||||
required
|
||||
appInputVerbatim
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="host">{{ "twoFactorDuoApiHostname" | i18n }}</label>
|
||||
<input
|
||||
id="host"
|
||||
type="text"
|
||||
name="Host"
|
||||
class="form-control"
|
||||
[(ngModel)]="host"
|
||||
placeholder="{{ 'ex' | i18n }} api-xxxxxxxx.duosecurity.com"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<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="!enabled">{{ "enable" | i18n }}</span>
|
||||
<span *ngIf="enabled">{{ "disable" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
86
apps/web/src/app/auth/settings/two-factor-duo.component.ts
Normal file
86
apps/web/src/app/auth/settings/two-factor-duo.component.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { UpdateTwoFactorDuoRequest } from "@bitwarden/common/auth/models/request/update-two-factor-duo.request";
|
||||
import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two-factor-duo.response";
|
||||
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
||||
|
||||
import { TwoFactorBaseComponent } from "./two-factor-base.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-two-factor-duo",
|
||||
templateUrl: "two-factor-duo.component.html",
|
||||
})
|
||||
export class TwoFactorDuoComponent extends TwoFactorBaseComponent {
|
||||
type = TwoFactorProviderType.Duo;
|
||||
ikey: string;
|
||||
skey: string;
|
||||
host: string;
|
||||
formPromise: Promise<TwoFactorDuoResponse>;
|
||||
|
||||
override componentName = "app-two-factor-duo";
|
||||
|
||||
constructor(
|
||||
apiService: ApiService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
logService: LogService,
|
||||
userVerificationService: UserVerificationService,
|
||||
dialogService: DialogServiceAbstraction
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
logService,
|
||||
userVerificationService,
|
||||
dialogService
|
||||
);
|
||||
}
|
||||
|
||||
auth(authResponse: AuthResponse<TwoFactorDuoResponse>) {
|
||||
super.auth(authResponse);
|
||||
this.processResponse(authResponse.response);
|
||||
}
|
||||
|
||||
submit() {
|
||||
if (this.enabled) {
|
||||
return super.disable(this.formPromise);
|
||||
} else {
|
||||
return this.enable();
|
||||
}
|
||||
}
|
||||
|
||||
protected async enable() {
|
||||
const request = await this.buildRequestModel(UpdateTwoFactorDuoRequest);
|
||||
request.integrationKey = this.ikey;
|
||||
request.secretKey = this.skey;
|
||||
request.host = this.host;
|
||||
|
||||
return super.enable(async () => {
|
||||
if (this.organizationId != null) {
|
||||
this.formPromise = this.apiService.putTwoFactorOrganizationDuo(
|
||||
this.organizationId,
|
||||
request
|
||||
);
|
||||
} else {
|
||||
this.formPromise = this.apiService.putTwoFactorDuo(request);
|
||||
}
|
||||
const response = await this.formPromise;
|
||||
await this.processResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
private processResponse(response: TwoFactorDuoResponse) {
|
||||
this.ikey = response.integrationKey;
|
||||
this.skey = response.secretKey;
|
||||
this.host = response.host;
|
||||
this.enabled = response.enabled;
|
||||
}
|
||||
}
|
||||
108
apps/web/src/app/auth/settings/two-factor-email.component.html
Normal file
108
apps/web/src/app/auth/settings/two-factor-email.component.html
Normal file
@@ -0,0 +1,108 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faEmailTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="2faEmailTitle">
|
||||
{{ "twoStepLogin" | i18n }}
|
||||
<small>{{ "emailTitle" | i18n }}</small>
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<app-two-factor-verify
|
||||
[organizationId]="organizationId"
|
||||
[type]="type"
|
||||
(onAuthed)="auth($any($event))"
|
||||
*ngIf="!authed"
|
||||
>
|
||||
</app-two-factor-verify>
|
||||
<form
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
*ngIf="authed"
|
||||
>
|
||||
<div class="modal-body">
|
||||
<ng-container *ngIf="enabled">
|
||||
<app-callout type="success" title="{{ 'enabled' | i18n }}" icon="bwi bwi-check-circle">
|
||||
{{ "twoStepLoginProviderEnabled" | i18n }}
|
||||
</app-callout>
|
||||
<strong>{{ "email" | i18n }}:</strong> {{ email }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!enabled">
|
||||
<p class="d-flex">
|
||||
<span class="mr-3">{{ "twoFactorEmailDesc" | i18n }}</span>
|
||||
<img class="float-right ml-auto mfaType1" alt="Email logo" />
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label for="email">1. {{ "twoFactorEmailEnterEmail" | i18n }}</label>
|
||||
<input
|
||||
id="email"
|
||||
type="text"
|
||||
name="Email"
|
||||
class="form-control"
|
||||
[(ngModel)]="email"
|
||||
required
|
||||
inputmode="email"
|
||||
appInputVerbatim="false"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3 d-flex">
|
||||
<button
|
||||
#sendBtn
|
||||
type="button"
|
||||
class="btn btn-outline-primary btn-sm btn-submit align-self-start"
|
||||
(click)="sendEmail()"
|
||||
[appApiAction]="emailPromise"
|
||||
[disabled]="$any(sendBtn).loading"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span>{{ "sendEmail" | i18n }}</span>
|
||||
</button>
|
||||
<span class="text-success ml-3" *ngIf="sentEmail">
|
||||
{{ "verificationCodeEmailSent" | i18n : sentEmail }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="token">2. {{ "twoFactorEmailEnterCode" | i18n }}</label>
|
||||
<input
|
||||
id="token"
|
||||
type="text"
|
||||
name="Token"
|
||||
class="form-control"
|
||||
[(ngModel)]="token"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<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="!enabled">{{ "enable" | i18n }}</span>
|
||||
<span *ngIf="enabled">{{ "disable" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
96
apps/web/src/app/auth/settings/two-factor-email.component.ts
Normal file
96
apps/web/src/app/auth/settings/two-factor-email.component.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request";
|
||||
import { UpdateTwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/update-two-factor-email.request";
|
||||
import { TwoFactorEmailResponse } from "@bitwarden/common/auth/models/response/two-factor-email.response";
|
||||
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
||||
|
||||
import { TwoFactorBaseComponent } from "./two-factor-base.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-two-factor-email",
|
||||
templateUrl: "two-factor-email.component.html",
|
||||
})
|
||||
export class TwoFactorEmailComponent extends TwoFactorBaseComponent {
|
||||
type = TwoFactorProviderType.Email;
|
||||
email: string;
|
||||
token: string;
|
||||
sentEmail: string;
|
||||
formPromise: Promise<TwoFactorEmailResponse>;
|
||||
emailPromise: Promise<unknown>;
|
||||
|
||||
override componentName = "app-two-factor-email";
|
||||
|
||||
constructor(
|
||||
apiService: ApiService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
logService: LogService,
|
||||
userVerificationService: UserVerificationService,
|
||||
private stateService: StateService,
|
||||
dialogService: DialogServiceAbstraction
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
logService,
|
||||
userVerificationService,
|
||||
dialogService
|
||||
);
|
||||
}
|
||||
|
||||
auth(authResponse: AuthResponse<TwoFactorEmailResponse>) {
|
||||
super.auth(authResponse);
|
||||
return this.processResponse(authResponse.response);
|
||||
}
|
||||
|
||||
submit() {
|
||||
if (this.enabled) {
|
||||
return super.disable(this.formPromise);
|
||||
} else {
|
||||
return this.enable();
|
||||
}
|
||||
}
|
||||
|
||||
async sendEmail() {
|
||||
try {
|
||||
const request = await this.buildRequestModel(TwoFactorEmailRequest);
|
||||
request.email = this.email;
|
||||
this.emailPromise = this.apiService.postTwoFactorEmailSetup(request);
|
||||
await this.emailPromise;
|
||||
this.sentEmail = this.email;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected async enable() {
|
||||
const request = await this.buildRequestModel(UpdateTwoFactorEmailRequest);
|
||||
request.email = this.email;
|
||||
request.token = this.token;
|
||||
|
||||
return super.enable(async () => {
|
||||
this.formPromise = this.apiService.putTwoFactorEmail(request);
|
||||
const response = await this.formPromise;
|
||||
await this.processResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
private async processResponse(response: TwoFactorEmailResponse) {
|
||||
this.token = null;
|
||||
this.email = response.email;
|
||||
this.enabled = response.enabled;
|
||||
if (!this.enabled && (this.email == null || this.email === "")) {
|
||||
this.email = await this.stateService.getEmail();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faRecoveryTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="2faRecoveryTitle">
|
||||
{{ "twoStepLogin" | i18n }}
|
||||
<small>{{ "recoveryCodeTitle" | i18n }}</small>
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<app-two-factor-verify [type]="type" (onAuthed)="auth($event)" *ngIf="!authed">
|
||||
</app-two-factor-verify>
|
||||
<ng-container *ngIf="authed">
|
||||
<div class="modal-body text-center">
|
||||
<ng-container *ngIf="code">
|
||||
<p>{{ "twoFactorRecoveryYourCode" | i18n }}:</p>
|
||||
<code class="text-lg">{{ code }}</code>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!code">
|
||||
{{ "twoFactorRecoveryNoCode" | i18n }}
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" (click)="print()" *ngIf="code">
|
||||
{{ "printCode" | i18n }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { TwoFactorRecoverResponse } from "@bitwarden/common/auth/models/response/two-factor-recover.response";
|
||||
|
||||
@Component({
|
||||
selector: "app-two-factor-recovery",
|
||||
templateUrl: "two-factor-recovery.component.html",
|
||||
})
|
||||
export class TwoFactorRecoveryComponent {
|
||||
type = -1;
|
||||
code: string;
|
||||
authed: boolean;
|
||||
twoFactorProviderType = TwoFactorProviderType;
|
||||
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
auth(authResponse: any) {
|
||||
this.authed = true;
|
||||
this.processResponse(authResponse.response);
|
||||
}
|
||||
|
||||
print() {
|
||||
const w = window.open();
|
||||
w.document.write(
|
||||
'<div style="font-size: 18px; text-align: center;">' +
|
||||
"<p>" +
|
||||
this.i18nService.t("twoFactorRecoveryYourCode") +
|
||||
":</p>" +
|
||||
"<code style=\"font-family: Menlo, Monaco, Consolas, 'Courier New', monospace;\">" +
|
||||
this.code +
|
||||
"</code></div>" +
|
||||
'<p style="text-align: center;">' +
|
||||
new Date() +
|
||||
"</p>"
|
||||
);
|
||||
w.onafterprint = () => w.close();
|
||||
w.print();
|
||||
}
|
||||
|
||||
private formatString(s: string) {
|
||||
if (s == null) {
|
||||
return null;
|
||||
}
|
||||
return s
|
||||
.replace(/(.{4})/g, "$1 ")
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
private processResponse(response: TwoFactorRecoverResponse) {
|
||||
this.code = this.formatString(response.code);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<!-- Please remove this disable statement when editing this file! -->
|
||||
<!-- eslint-disable @angular-eslint/template/button-has-type -->
|
||||
<div [ngClass]="tabbedHeader ? 'tabbed-header' : 'page-header'">
|
||||
<h1 *ngIf="!organizationId">{{ "twoStepLogin" | i18n }}</h1>
|
||||
<h1 *ngIf="organizationId">{{ "twoStepLoginEnforcement" | i18n }}</h1>
|
||||
</div>
|
||||
<p *ngIf="!organizationId">{{ "twoStepLoginDesc" | i18n }}</p>
|
||||
<ng-container *ngIf="organizationId">
|
||||
<p>
|
||||
{{ "twoStepLoginOrganizationDescStart" | i18n }}
|
||||
<a routerLink="../policies">{{ "twoStepLoginPolicy" | i18n }}.</a>
|
||||
<br />
|
||||
{{ "twoStepLoginOrganizationDuoDesc" | i18n }}
|
||||
</p>
|
||||
<p>{{ "twoStepLoginOrganizationSsoDesc" | i18n }}</p>
|
||||
</ng-container>
|
||||
<bit-callout type="warning" *ngIf="!organizationId">
|
||||
<p>{{ "twoStepLoginRecoveryWarning" | i18n }}</p>
|
||||
<button bitButton buttonType="secondary" (click)="recoveryCode()">
|
||||
{{ "viewRecoveryCode" | i18n }}
|
||||
</button>
|
||||
</bit-callout>
|
||||
<h2 [ngClass]="{ 'mt-5': !organizationId }">
|
||||
{{ "providers" | i18n }}
|
||||
<small *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-fw text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</small>
|
||||
</h2>
|
||||
<bit-callout type="warning" *ngIf="showPolicyWarning">
|
||||
{{ "twoStepLoginPolicyUserWarning" | i18n }}
|
||||
</bit-callout>
|
||||
<ul class="list-group list-group-2fa">
|
||||
<li *ngFor="let p of providers" class="list-group-item d-flex align-items-center">
|
||||
<div class="logo-2fa d-flex justify-content-center">
|
||||
<img [class]="'mfaType' + p.type" [alt]="p.name + ' logo'" />
|
||||
</div>
|
||||
<div class="mx-4">
|
||||
<h3 class="mb-0">
|
||||
{{ p.name }}
|
||||
<ng-container *ngIf="p.enabled">
|
||||
<i
|
||||
class="bwi bwi-check text-success bwi-fw"
|
||||
title="{{ 'enabled' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "enabled" | i18n }}</span>
|
||||
</ng-container>
|
||||
<app-premium-badge *ngIf="p.premium"></app-premium-badge>
|
||||
</h3>
|
||||
{{ p.description }}
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<button
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
[disabled]="!canAccessPremium && p.premium"
|
||||
(click)="manage(p.type)"
|
||||
>
|
||||
{{ "manage" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ng-template #authenticatorTemplate></ng-template>
|
||||
<ng-template #recoveryTemplate></ng-template>
|
||||
<ng-template #duoTemplate></ng-template>
|
||||
<ng-template #emailTemplate></ng-template>
|
||||
<ng-template #yubikeyTemplate></ng-template>
|
||||
<ng-template #webAuthnTemplate></ng-template>
|
||||
212
apps/web/src/app/auth/settings/two-factor-setup.component.ts
Normal file
212
apps/web/src/app/auth/settings/two-factor-setup.component.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import { Component, OnDestroy, OnInit, Type, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service";
|
||||
|
||||
import { TwoFactorAuthenticatorComponent } from "./two-factor-authenticator.component";
|
||||
import { TwoFactorDuoComponent } from "./two-factor-duo.component";
|
||||
import { TwoFactorEmailComponent } from "./two-factor-email.component";
|
||||
import { TwoFactorRecoveryComponent } from "./two-factor-recovery.component";
|
||||
import { TwoFactorWebAuthnComponent } from "./two-factor-webauthn.component";
|
||||
import { TwoFactorYubiKeyComponent } from "./two-factor-yubikey.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-two-factor-setup",
|
||||
templateUrl: "two-factor-setup.component.html",
|
||||
})
|
||||
export class TwoFactorSetupComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("recoveryTemplate", { read: ViewContainerRef, static: true })
|
||||
recoveryModalRef: ViewContainerRef;
|
||||
@ViewChild("authenticatorTemplate", { read: ViewContainerRef, static: true })
|
||||
authenticatorModalRef: ViewContainerRef;
|
||||
@ViewChild("yubikeyTemplate", { read: ViewContainerRef, static: true })
|
||||
yubikeyModalRef: ViewContainerRef;
|
||||
@ViewChild("duoTemplate", { read: ViewContainerRef, static: true }) duoModalRef: ViewContainerRef;
|
||||
@ViewChild("emailTemplate", { read: ViewContainerRef, static: true })
|
||||
emailModalRef: ViewContainerRef;
|
||||
@ViewChild("webAuthnTemplate", { read: ViewContainerRef, static: true })
|
||||
webAuthnModalRef: ViewContainerRef;
|
||||
|
||||
organizationId: string;
|
||||
providers: any[] = [];
|
||||
canAccessPremium: boolean;
|
||||
showPolicyWarning = false;
|
||||
loading = true;
|
||||
modal: ModalRef;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
tabbedHeader = true;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
private twoFactorAuthPolicyAppliesToActiveUser: boolean;
|
||||
|
||||
constructor(
|
||||
protected apiService: ApiService,
|
||||
protected modalService: ModalService,
|
||||
protected messagingService: MessagingService,
|
||||
protected policyService: PolicyService,
|
||||
private stateService: StateService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.canAccessPremium = await this.stateService.getCanAccessPremium();
|
||||
|
||||
for (const key in TwoFactorProviders) {
|
||||
// eslint-disable-next-line
|
||||
if (!TwoFactorProviders.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const p = (TwoFactorProviders as any)[key];
|
||||
if (this.filterProvider(p.type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.providers.push({
|
||||
type: p.type,
|
||||
name: p.name,
|
||||
description: p.description,
|
||||
enabled: false,
|
||||
premium: p.premium,
|
||||
sort: p.sort,
|
||||
});
|
||||
}
|
||||
|
||||
this.providers.sort((a: any, b: any) => a.sort - b.sort);
|
||||
|
||||
this.policyService
|
||||
.policyAppliesToActiveUser$(PolicyType.TwoFactorAuthentication)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((policyAppliesToActiveUser) => {
|
||||
this.twoFactorAuthPolicyAppliesToActiveUser = policyAppliesToActiveUser;
|
||||
});
|
||||
|
||||
await this.load();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.loading = true;
|
||||
const providerList = await this.getTwoFactorProviders();
|
||||
providerList.data.forEach((p) => {
|
||||
this.providers.forEach((p2) => {
|
||||
if (p.type === p2.type) {
|
||||
p2.enabled = p.enabled;
|
||||
}
|
||||
});
|
||||
});
|
||||
this.evaluatePolicies();
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async manage(type: TwoFactorProviderType) {
|
||||
switch (type) {
|
||||
case TwoFactorProviderType.Authenticator: {
|
||||
const authComp = await this.openModal(
|
||||
this.authenticatorModalRef,
|
||||
TwoFactorAuthenticatorComponent
|
||||
);
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
authComp.onUpdated.subscribe((enabled: boolean) => {
|
||||
this.updateStatus(enabled, TwoFactorProviderType.Authenticator);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case TwoFactorProviderType.Yubikey: {
|
||||
const yubiComp = await this.openModal(this.yubikeyModalRef, TwoFactorYubiKeyComponent);
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
yubiComp.onUpdated.subscribe((enabled: boolean) => {
|
||||
this.updateStatus(enabled, TwoFactorProviderType.Yubikey);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case TwoFactorProviderType.Duo: {
|
||||
const duoComp = await this.openModal(this.duoModalRef, TwoFactorDuoComponent);
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
duoComp.onUpdated.subscribe((enabled: boolean) => {
|
||||
this.updateStatus(enabled, TwoFactorProviderType.Duo);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case TwoFactorProviderType.Email: {
|
||||
const emailComp = await this.openModal(this.emailModalRef, TwoFactorEmailComponent);
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
emailComp.onUpdated.subscribe((enabled: boolean) => {
|
||||
this.updateStatus(enabled, TwoFactorProviderType.Email);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case TwoFactorProviderType.WebAuthn: {
|
||||
const webAuthnComp = await this.openModal(
|
||||
this.webAuthnModalRef,
|
||||
TwoFactorWebAuthnComponent
|
||||
);
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
webAuthnComp.onUpdated.subscribe((enabled: boolean) => {
|
||||
this.updateStatus(enabled, TwoFactorProviderType.WebAuthn);
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
recoveryCode() {
|
||||
this.openModal(this.recoveryModalRef, TwoFactorRecoveryComponent);
|
||||
}
|
||||
|
||||
async premiumRequired() {
|
||||
if (!this.canAccessPremium) {
|
||||
this.messagingService.send("premiumRequired");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected getTwoFactorProviders() {
|
||||
return this.apiService.getTwoFactorProviders();
|
||||
}
|
||||
|
||||
protected filterProvider(type: TwoFactorProviderType) {
|
||||
return type === TwoFactorProviderType.OrganizationDuo;
|
||||
}
|
||||
|
||||
protected async openModal<T>(ref: ViewContainerRef, type: Type<T>): Promise<T> {
|
||||
const [modal, childComponent] = await this.modalService.openViewRef(type, ref);
|
||||
this.modal = modal;
|
||||
|
||||
return childComponent;
|
||||
}
|
||||
|
||||
protected updateStatus(enabled: boolean, type: TwoFactorProviderType) {
|
||||
if (!enabled && this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
this.providers.forEach((p) => {
|
||||
if (p.type === type) {
|
||||
p.enabled = enabled;
|
||||
}
|
||||
});
|
||||
this.evaluatePolicies();
|
||||
}
|
||||
|
||||
private async evaluatePolicies() {
|
||||
if (this.organizationId == null && this.providers.filter((p) => p.enabled).length === 1) {
|
||||
this.showPolicyWarning = this.twoFactorAuthPolicyAppliesToActiveUser;
|
||||
} else {
|
||||
this.showPolicyWarning = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-body">
|
||||
<p>{{ "twoStepLoginAuthDesc" | i18n }}</p>
|
||||
<app-user-verification [(ngModel)]="secret" ngDefaultControl name="secret">
|
||||
</app-user-verification>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<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>{{ "continue" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
|
||||
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
||||
import { TwoFactorResponse } from "@bitwarden/common/auth/types/two-factor-response";
|
||||
import { Verification } from "@bitwarden/common/types/verification";
|
||||
|
||||
@Component({
|
||||
selector: "app-two-factor-verify",
|
||||
templateUrl: "two-factor-verify.component.html",
|
||||
})
|
||||
export class TwoFactorVerifyComponent {
|
||||
@Input() type: TwoFactorProviderType;
|
||||
@Input() organizationId: string;
|
||||
@Output() onAuthed = new EventEmitter<AuthResponse<TwoFactorResponse>>();
|
||||
|
||||
secret: Verification;
|
||||
formPromise: Promise<TwoFactorResponse>;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private logService: LogService,
|
||||
private userVerificationService: UserVerificationService
|
||||
) {}
|
||||
|
||||
async submit() {
|
||||
let hashedSecret: string;
|
||||
|
||||
try {
|
||||
this.formPromise = this.userVerificationService.buildRequest(this.secret).then((request) => {
|
||||
hashedSecret =
|
||||
this.secret.type === VerificationType.MasterPassword
|
||||
? request.masterPasswordHash
|
||||
: request.otp;
|
||||
return this.apiCall(request);
|
||||
});
|
||||
|
||||
const response = await this.formPromise;
|
||||
this.onAuthed.emit({
|
||||
response: response,
|
||||
secret: hashedSecret,
|
||||
verificationType: this.secret.type,
|
||||
});
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private apiCall(request: SecretVerificationRequest): Promise<TwoFactorResponse> {
|
||||
switch (this.type) {
|
||||
case -1 as TwoFactorProviderType:
|
||||
return this.apiService.getTwoFactorRecover(request);
|
||||
case TwoFactorProviderType.Duo:
|
||||
case TwoFactorProviderType.OrganizationDuo:
|
||||
if (this.organizationId != null) {
|
||||
return this.apiService.getTwoFactorOrganizationDuo(this.organizationId, request);
|
||||
} else {
|
||||
return this.apiService.getTwoFactorDuo(request);
|
||||
}
|
||||
case TwoFactorProviderType.Email:
|
||||
return this.apiService.getTwoFactorEmail(request);
|
||||
case TwoFactorProviderType.WebAuthn:
|
||||
return this.apiService.getTwoFactorWebAuthn(request);
|
||||
case TwoFactorProviderType.Authenticator:
|
||||
return this.apiService.getTwoFactorAuthenticator(request);
|
||||
case TwoFactorProviderType.Yubikey:
|
||||
return this.apiService.getTwoFactorYubiKey(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faU2fTitle">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="2faU2fTitle">
|
||||
{{ "twoStepLogin" | i18n }}
|
||||
<small>{{ "webAuthnTitle" | i18n }}</small>
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<app-two-factor-verify
|
||||
[organizationId]="organizationId"
|
||||
[type]="type"
|
||||
(onAuthed)="auth($any($event))"
|
||||
*ngIf="!authed"
|
||||
>
|
||||
</app-two-factor-verify>
|
||||
<form
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
*ngIf="authed"
|
||||
>
|
||||
<div class="modal-body">
|
||||
<app-callout
|
||||
type="success"
|
||||
title="{{ 'enabled' | i18n }}"
|
||||
icon="bwi bwi-check-circle"
|
||||
*ngIf="enabled"
|
||||
>
|
||||
{{ "twoStepLoginProviderEnabled" | i18n }}
|
||||
</app-callout>
|
||||
<app-callout type="warning">
|
||||
<p>{{ "twoFactorWebAuthnWarning" | i18n }}</p>
|
||||
<ul class="mb-0">
|
||||
<li>{{ "twoFactorWebAuthnSupportWeb" | i18n }}</li>
|
||||
</ul>
|
||||
</app-callout>
|
||||
<img class="float-right ml-5 mfaType7" alt="FIDO2 WebAuthn logo'" />
|
||||
<ul class="bwi-ul">
|
||||
<li
|
||||
*ngFor="let k of keys; let i = index"
|
||||
#removeKeyBtn
|
||||
[appApiAction]="k.removePromise"
|
||||
>
|
||||
<i class="bwi bwi-li bwi-key"></i>
|
||||
<strong *ngIf="!k.configured || !k.name">{{ "webAuthnkeyX" | i18n : i + 1 }}</strong>
|
||||
<strong *ngIf="k.configured && k.name">{{ k.name }}</strong>
|
||||
<ng-container *ngIf="k.configured && !$any(removeKeyBtn).loading">
|
||||
<ng-container *ngIf="k.migrated">
|
||||
<span>{{ "webAuthnMigrated" | i18n }}</span>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="keysConfiguredCount > 1 && k.configured">
|
||||
<i
|
||||
class="bwi bwi-spin bwi-spinner text-muted bwi-fw"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
*ngIf="$any(removeKeyBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
-
|
||||
<a href="#" appStopClick (click)="remove(k)">{{ "remove" | i18n }}</a>
|
||||
</ng-container>
|
||||
</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<p>{{ "twoFactorWebAuthnAdd" | i18n }}:</p>
|
||||
<ol>
|
||||
<li>{{ "twoFactorU2fGiveName" | i18n }}</li>
|
||||
<li>{{ "twoFactorU2fPlugInReadKey" | i18n }}</li>
|
||||
<li>{{ "twoFactorU2fTouchButton" | i18n }}</li>
|
||||
<li>{{ "twoFactorU2fSaveForm" | i18n }}</li>
|
||||
</ol>
|
||||
<div class="row">
|
||||
<div class="form-group col-6">
|
||||
<label for="name">{{ "name" | i18n }}</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
name="Name"
|
||||
class="form-control"
|
||||
[(ngModel)]="name"
|
||||
[disabled]="!keyIdAvailable"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
(click)="readKey()"
|
||||
class="btn btn-outline-secondary mr-2"
|
||||
[disabled]="$any(readKeyBtn).loading || webAuthnListening || !keyIdAvailable"
|
||||
#readKeyBtn
|
||||
[appApiAction]="challengePromise"
|
||||
>
|
||||
{{ "readKey" | i18n }}
|
||||
</button>
|
||||
<ng-container *ngIf="$any(readKeyBtn).loading">
|
||||
<i class="bwi bwi-spinner bwi-spin text-muted" aria-hidden="true"></i>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!$any(readKeyBtn).loading">
|
||||
<ng-container *ngIf="webAuthnListening">
|
||||
<i class="bwi bwi-spinner bwi-spin text-muted" aria-hidden="true"></i>
|
||||
{{ "twoFactorU2fWaiting" | i18n }}...
|
||||
</ng-container>
|
||||
<ng-container *ngIf="webAuthnResponse">
|
||||
<i class="bwi bwi-check-circle text-success" aria-hidden="true"></i>
|
||||
{{ "twoFactorU2fClickSave" | i18n }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="webAuthnError">
|
||||
<i class="bwi bwi-exclamation-triangle text-danger" aria-hidden="true"></i>
|
||||
{{ "twoFactorU2fProblemReadingTryAgain" | i18n }}
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
[disabled]="form.loading || !webAuthnResponse"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
*ngIf="form.loading"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span *ngIf="!form.loading">{{ "save" | i18n }}</span>
|
||||
</button>
|
||||
<button
|
||||
#disableBtn
|
||||
type="button"
|
||||
class="btn btn-outline-secondary btn-submit"
|
||||
[disabled]="$any(disableBtn).loading"
|
||||
(click)="disable()"
|
||||
*ngIf="enabled"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span>{{ "disableAllKeys" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
191
apps/web/src/app/auth/settings/two-factor-webauthn.component.ts
Normal file
191
apps/web/src/app/auth/settings/two-factor-webauthn.component.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { Component, NgZone } from "@angular/core";
|
||||
|
||||
import { DialogServiceAbstraction, SimpleDialogType } from "@bitwarden/angular/services/dialog";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
|
||||
import { UpdateTwoFactorWebAuthnDeleteRequest } from "@bitwarden/common/auth/models/request/update-two-factor-web-authn-delete.request";
|
||||
import { UpdateTwoFactorWebAuthnRequest } from "@bitwarden/common/auth/models/request/update-two-factor-web-authn.request";
|
||||
import {
|
||||
ChallengeResponse,
|
||||
TwoFactorWebAuthnResponse,
|
||||
} from "@bitwarden/common/auth/models/response/two-factor-web-authn.response";
|
||||
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
||||
|
||||
import { TwoFactorBaseComponent } from "./two-factor-base.component";
|
||||
|
||||
interface Key {
|
||||
id: number;
|
||||
name: string;
|
||||
configured: boolean;
|
||||
migrated?: boolean;
|
||||
removePromise: Promise<TwoFactorWebAuthnResponse> | null;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-two-factor-webauthn",
|
||||
templateUrl: "two-factor-webauthn.component.html",
|
||||
})
|
||||
export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
|
||||
type = TwoFactorProviderType.WebAuthn;
|
||||
name: string;
|
||||
keys: Key[];
|
||||
keyIdAvailable: number = null;
|
||||
keysConfiguredCount = 0;
|
||||
webAuthnError: boolean;
|
||||
webAuthnListening: boolean;
|
||||
webAuthnResponse: PublicKeyCredential;
|
||||
challengePromise: Promise<ChallengeResponse>;
|
||||
formPromise: Promise<TwoFactorWebAuthnResponse>;
|
||||
|
||||
override componentName = "app-two-factor-webauthn";
|
||||
|
||||
constructor(
|
||||
apiService: ApiService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
private ngZone: NgZone,
|
||||
logService: LogService,
|
||||
userVerificationService: UserVerificationService,
|
||||
dialogService: DialogServiceAbstraction
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
logService,
|
||||
userVerificationService,
|
||||
dialogService
|
||||
);
|
||||
}
|
||||
|
||||
auth(authResponse: AuthResponse<TwoFactorWebAuthnResponse>) {
|
||||
super.auth(authResponse);
|
||||
this.processResponse(authResponse.response);
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (this.webAuthnResponse == null || this.keyIdAvailable == null) {
|
||||
// Should never happen.
|
||||
return Promise.reject();
|
||||
}
|
||||
const request = await this.buildRequestModel(UpdateTwoFactorWebAuthnRequest);
|
||||
request.deviceResponse = this.webAuthnResponse;
|
||||
request.id = this.keyIdAvailable;
|
||||
request.name = this.name;
|
||||
|
||||
return super.enable(async () => {
|
||||
this.formPromise = this.apiService.putTwoFactorWebAuthn(request);
|
||||
const response = await this.formPromise;
|
||||
await this.processResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
disable() {
|
||||
return super.disable(this.formPromise);
|
||||
}
|
||||
|
||||
async remove(key: Key) {
|
||||
if (this.keysConfiguredCount <= 1 || key.removePromise != null) {
|
||||
return;
|
||||
}
|
||||
const name = key.name != null ? key.name : this.i18nService.t("webAuthnkeyX", key.id as any);
|
||||
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: name,
|
||||
content: { key: "removeU2fConfirmation" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
const request = await this.buildRequestModel(UpdateTwoFactorWebAuthnDeleteRequest);
|
||||
request.id = key.id;
|
||||
try {
|
||||
key.removePromise = this.apiService.deleteTwoFactorWebAuthn(request);
|
||||
const response = await key.removePromise;
|
||||
key.removePromise = null;
|
||||
await this.processResponse(response);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async readKey() {
|
||||
if (this.keyIdAvailable == null) {
|
||||
return;
|
||||
}
|
||||
const request = await this.buildRequestModel(SecretVerificationRequest);
|
||||
try {
|
||||
this.challengePromise = this.apiService.getTwoFactorWebAuthnChallenge(request);
|
||||
const challenge = await this.challengePromise;
|
||||
this.readDevice(challenge);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private readDevice(webAuthnChallenge: ChallengeResponse) {
|
||||
// eslint-disable-next-line
|
||||
console.log("listening for key...");
|
||||
this.resetWebAuthn(true);
|
||||
|
||||
navigator.credentials
|
||||
.create({
|
||||
publicKey: webAuthnChallenge,
|
||||
})
|
||||
.then((data: PublicKeyCredential) => {
|
||||
this.ngZone.run(() => {
|
||||
this.webAuthnListening = false;
|
||||
this.webAuthnResponse = data;
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
// eslint-disable-next-line
|
||||
console.error(err);
|
||||
this.resetWebAuthn(false);
|
||||
// TODO: Should we display the actual error?
|
||||
this.webAuthnError = true;
|
||||
});
|
||||
}
|
||||
|
||||
private resetWebAuthn(listening = false) {
|
||||
this.webAuthnResponse = null;
|
||||
this.webAuthnError = false;
|
||||
this.webAuthnListening = listening;
|
||||
}
|
||||
|
||||
private processResponse(response: TwoFactorWebAuthnResponse) {
|
||||
this.resetWebAuthn();
|
||||
this.keys = [];
|
||||
this.keyIdAvailable = null;
|
||||
this.name = null;
|
||||
this.keysConfiguredCount = 0;
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
if (response.keys != null) {
|
||||
const key = response.keys.filter((k) => k.id === i);
|
||||
if (key.length > 0) {
|
||||
this.keysConfiguredCount++;
|
||||
this.keys.push({
|
||||
id: i,
|
||||
name: key[0].name,
|
||||
configured: true,
|
||||
migrated: key[0].migrated,
|
||||
removePromise: null,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
this.keys.push({ id: i, name: null, configured: false, removePromise: null });
|
||||
if (this.keyIdAvailable == null) {
|
||||
this.keyIdAvailable = i;
|
||||
}
|
||||
}
|
||||
this.enabled = response.enabled;
|
||||
}
|
||||
}
|
||||
125
apps/web/src/app/auth/settings/two-factor-yubikey.component.html
Normal file
125
apps/web/src/app/auth/settings/two-factor-yubikey.component.html
Normal file
@@ -0,0 +1,125 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faYubiKeyTitle">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="2faYubiKeyTitle">
|
||||
{{ "twoStepLogin" | i18n }}
|
||||
<small>YubiKey</small>
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<app-two-factor-verify
|
||||
[organizationId]="organizationId"
|
||||
[type]="type"
|
||||
(onAuthed)="auth($any($event))"
|
||||
*ngIf="!authed"
|
||||
>
|
||||
</app-two-factor-verify>
|
||||
<form
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
*ngIf="authed"
|
||||
autocomplete="off"
|
||||
>
|
||||
<div class="modal-body">
|
||||
<app-callout
|
||||
type="success"
|
||||
title="{{ 'enabled' | i18n }}"
|
||||
icon="bwi bwi-check-circle"
|
||||
*ngIf="enabled"
|
||||
>
|
||||
{{ "twoStepLoginProviderEnabled" | i18n }}
|
||||
</app-callout>
|
||||
<app-callout type="warning">
|
||||
<p>{{ "twoFactorYubikeyWarning" | i18n }}</p>
|
||||
<ul class="mb-0">
|
||||
<li>{{ "twoFactorYubikeySupportUsb" | i18n }}</li>
|
||||
<li>{{ "twoFactorYubikeySupportMobile" | i18n }}</li>
|
||||
</ul>
|
||||
</app-callout>
|
||||
<img class="float-right mfaType3" alt="YubiKey OTP security key logo" />
|
||||
<p>{{ "twoFactorYubikeyAdd" | i18n }}:</p>
|
||||
<ol>
|
||||
<li>{{ "twoFactorYubikeyPlugIn" | i18n }}</li>
|
||||
<li>{{ "twoFactorYubikeySelectKey" | i18n }}</li>
|
||||
<li>{{ "twoFactorYubikeyTouchButton" | i18n }}</li>
|
||||
<li>{{ "twoFactorYubikeySaveForm" | i18n }}</li>
|
||||
</ol>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="form-group col-6" *ngFor="let k of keys; let i = index">
|
||||
<label for="key{{ i + 1 }}">{{ "yubikeyX" | i18n : i + 1 }}</label>
|
||||
<input
|
||||
id="key{{ i + 1 }}"
|
||||
type="password"
|
||||
name="Key{{ i + 1 }}"
|
||||
class="form-control"
|
||||
[(ngModel)]="k.key"
|
||||
*ngIf="!k.existingKey"
|
||||
appInputVerbatim
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<div class="d-flex" *ngIf="k.existingKey">
|
||||
<span class="mr-2">{{ k.existingKey }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-link text-danger ml-auto"
|
||||
(click)="remove(k)"
|
||||
appA11yTitle="{{ 'remove' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-minus-circle bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<strong class="d-block mb-2">{{ "nfcSupport" | i18n }}</strong>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="nfc" name="Nfc" [(ngModel)]="nfc" />
|
||||
<label class="form-check-label" for="nfc">{{
|
||||
"twoFactorYubikeySupportsNfc" | i18n
|
||||
}}</label>
|
||||
</div>
|
||||
<small class="form-text text-muted">{{ "twoFactorYubikeySupportsNfcDesc" | i18n }}</small>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<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>
|
||||
<button
|
||||
#disableBtn
|
||||
type="button"
|
||||
class="btn btn-outline-secondary btn-submit"
|
||||
[appApiAction]="disablePromise"
|
||||
[disabled]="$any(disableBtn).loading"
|
||||
(click)="disable()"
|
||||
*ngIf="enabled"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span>{{ "disableAllKeys" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
106
apps/web/src/app/auth/settings/two-factor-yubikey.component.ts
Normal file
106
apps/web/src/app/auth/settings/two-factor-yubikey.component.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { UpdateTwoFactorYubioOtpRequest } from "@bitwarden/common/auth/models/request/update-two-factor-yubio-otp.request";
|
||||
import { TwoFactorYubiKeyResponse } from "@bitwarden/common/auth/models/response/two-factor-yubi-key.response";
|
||||
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
||||
|
||||
import { TwoFactorBaseComponent } from "./two-factor-base.component";
|
||||
|
||||
interface Key {
|
||||
key: string;
|
||||
existingKey: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-two-factor-yubikey",
|
||||
templateUrl: "two-factor-yubikey.component.html",
|
||||
})
|
||||
export class TwoFactorYubiKeyComponent extends TwoFactorBaseComponent {
|
||||
type = TwoFactorProviderType.Yubikey;
|
||||
keys: Key[];
|
||||
nfc = false;
|
||||
|
||||
formPromise: Promise<TwoFactorYubiKeyResponse>;
|
||||
disablePromise: Promise<unknown>;
|
||||
|
||||
override componentName = "app-two-factor-yubikey";
|
||||
|
||||
constructor(
|
||||
apiService: ApiService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
logService: LogService,
|
||||
userVerificationService: UserVerificationService,
|
||||
dialogService: DialogServiceAbstraction
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
logService,
|
||||
userVerificationService,
|
||||
dialogService
|
||||
);
|
||||
}
|
||||
|
||||
auth(authResponse: AuthResponse<TwoFactorYubiKeyResponse>) {
|
||||
super.auth(authResponse);
|
||||
this.processResponse(authResponse.response);
|
||||
}
|
||||
|
||||
async submit() {
|
||||
const request = await this.buildRequestModel(UpdateTwoFactorYubioOtpRequest);
|
||||
request.key1 = this.keys != null && this.keys.length > 0 ? this.keys[0].key : null;
|
||||
request.key2 = this.keys != null && this.keys.length > 1 ? this.keys[1].key : null;
|
||||
request.key3 = this.keys != null && this.keys.length > 2 ? this.keys[2].key : null;
|
||||
request.key4 = this.keys != null && this.keys.length > 3 ? this.keys[3].key : null;
|
||||
request.key5 = this.keys != null && this.keys.length > 4 ? this.keys[4].key : null;
|
||||
request.nfc = this.nfc;
|
||||
|
||||
return super.enable(async () => {
|
||||
this.formPromise = this.apiService.putTwoFactorYubiKey(request);
|
||||
const response = await this.formPromise;
|
||||
await this.processResponse(response);
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("yubikeysUpdated"));
|
||||
});
|
||||
}
|
||||
|
||||
disable() {
|
||||
return super.disable(this.disablePromise);
|
||||
}
|
||||
|
||||
remove(key: Key) {
|
||||
key.existingKey = null;
|
||||
key.key = null;
|
||||
}
|
||||
|
||||
private processResponse(response: TwoFactorYubiKeyResponse) {
|
||||
this.enabled = response.enabled;
|
||||
this.keys = [
|
||||
{ key: response.key1, existingKey: this.padRight(response.key1) },
|
||||
{ key: response.key2, existingKey: this.padRight(response.key2) },
|
||||
{ key: response.key3, existingKey: this.padRight(response.key3) },
|
||||
{ key: response.key4, existingKey: this.padRight(response.key4) },
|
||||
{ key: response.key5, existingKey: this.padRight(response.key5) },
|
||||
];
|
||||
this.nfc = response.nfc || !response.enabled;
|
||||
}
|
||||
|
||||
private padRight(str: string, character = "•", size = 44) {
|
||||
if (str == null || character == null || str.length >= size) {
|
||||
return str;
|
||||
}
|
||||
const max = (size - str.length) / character.length;
|
||||
for (let i = 0; i < max; i++) {
|
||||
str += character;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
}
|
||||
11
apps/web/src/app/auth/settings/verify-email.component.html
Normal file
11
apps/web/src/app/auth/settings/verify-email.component.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<div class="tw-rounded tw-border tw-border-solid tw-border-warning-500 tw-bg-background">
|
||||
<div class="tw-bg-warning-500 tw-px-5 tw-py-2.5 tw-font-bold tw-uppercase tw-text-contrast">
|
||||
<i class="bwi bwi-envelope bwi-fw" aria-hidden="true"></i> {{ "verifyEmail" | i18n }}
|
||||
</div>
|
||||
<div class="tw-p-5">
|
||||
<p>{{ "verifyEmailDesc" | i18n }}</p>
|
||||
<button id="sendBtn" bitButton type="button" block [bitAction]="send">
|
||||
{{ "sendEmail" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
45
apps/web/src/app/auth/settings/verify-email.component.ts
Normal file
45
apps/web/src/app/auth/settings/verify-email.component.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Component, EventEmitter, Output } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-verify-email",
|
||||
templateUrl: "verify-email.component.html",
|
||||
})
|
||||
export class VerifyEmailComponent {
|
||||
actionPromise: Promise<unknown>;
|
||||
|
||||
@Output() onVerified = new EventEmitter<boolean>();
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService,
|
||||
private tokenService: TokenService
|
||||
) {}
|
||||
|
||||
async verifyEmail(): Promise<void> {
|
||||
await this.apiService.refreshIdentityToken();
|
||||
if (await this.tokenService.getEmailVerified()) {
|
||||
this.onVerified.emit(true);
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("emailVerified"));
|
||||
return;
|
||||
}
|
||||
|
||||
await this.apiService.postAccountVerifyEmail();
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("checkInboxForVerification")
|
||||
);
|
||||
}
|
||||
|
||||
send = async () => {
|
||||
await this.verifyEmail();
|
||||
};
|
||||
}
|
||||
52
apps/web/src/app/auth/sso.component.html
Normal file
52
apps/web/src/app/auth/sso.component.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<form
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
class="container"
|
||||
[appApiAction]="initiateSsoFormPromise"
|
||||
ngNativeValidate
|
||||
>
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<img class="logo mb-2 logo-themed" alt="Bitwarden" />
|
||||
<div class="card d-block mt-4">
|
||||
<div class="card-body" *ngIf="loggingIn">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
{{ "loading" | i18n }}
|
||||
</div>
|
||||
<div class="card-body" *ngIf="!loggingIn">
|
||||
<p>{{ "ssoLogInWithOrgIdentifier" | i18n }}</p>
|
||||
<div class="form-group">
|
||||
<label for="identifier">{{ "ssoIdentifier" | i18n }}</label>
|
||||
<input
|
||||
id="identifier"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Identifier"
|
||||
[(ngModel)]="identifier"
|
||||
required
|
||||
appAutofocus
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="d-flex">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-block btn-submit"
|
||||
[disabled]="form.loading"
|
||||
>
|
||||
<span> <i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ "logIn" | i18n }} </span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
||||
{{ "cancel" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
139
apps/web/src/app/auth/sso.component.ts
Normal file
139
apps/web/src/app/auth/sso.component.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/abstractions/organization-domain/org-domain-api.service.abstraction";
|
||||
import { OrganizationDomainSsoDetailsResponse } from "@bitwarden/common/abstractions/organization-domain/responses/organization-domain-sso-details.response";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { ValidationService } from "@bitwarden/common/abstractions/validation.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
|
||||
import { HttpStatusCode } from "@bitwarden/common/enums";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
|
||||
@Component({
|
||||
selector: "app-sso",
|
||||
templateUrl: "sso.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class SsoComponent extends BaseSsoComponent {
|
||||
constructor(
|
||||
authService: AuthService,
|
||||
router: Router,
|
||||
i18nService: I18nService,
|
||||
route: ActivatedRoute,
|
||||
stateService: StateService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
apiService: ApiService,
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
environmentService: EnvironmentService,
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
logService: LogService,
|
||||
private orgDomainApiService: OrgDomainApiServiceAbstraction,
|
||||
private loginService: LoginService,
|
||||
private validationService: ValidationService
|
||||
) {
|
||||
super(
|
||||
authService,
|
||||
router,
|
||||
i18nService,
|
||||
route,
|
||||
stateService,
|
||||
platformUtilsService,
|
||||
apiService,
|
||||
cryptoFunctionService,
|
||||
environmentService,
|
||||
passwordGenerationService,
|
||||
logService
|
||||
);
|
||||
this.redirectUri = window.location.origin + "/sso-connector.html";
|
||||
this.clientId = "web";
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
super.ngOnInit();
|
||||
|
||||
// if we have an emergency access invite, redirect to emergency access
|
||||
const emergencyAccessInvite = await this.stateService.getEmergencyAccessInvitation();
|
||||
if (emergencyAccessInvite != null) {
|
||||
this.onSuccessfulLoginNavigate = async () => {
|
||||
this.router.navigate(["/accept-emergency"], {
|
||||
queryParams: {
|
||||
id: emergencyAccessInvite.id,
|
||||
name: emergencyAccessInvite.name,
|
||||
email: emergencyAccessInvite.email,
|
||||
token: emergencyAccessInvite.token,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||
if (qParams.identifier != null) {
|
||||
// SSO Org Identifier in query params takes precedence over claimed domains
|
||||
this.identifier = qParams.identifier;
|
||||
} else {
|
||||
// Note: this flow is written for web but both browser and desktop
|
||||
// redirect here on SSO button click.
|
||||
|
||||
// Check if email matches any claimed domains
|
||||
if (qParams.email) {
|
||||
// show loading spinner
|
||||
this.loggingIn = true;
|
||||
try {
|
||||
const response: OrganizationDomainSsoDetailsResponse =
|
||||
await this.orgDomainApiService.getClaimedOrgDomainByEmail(qParams.email);
|
||||
|
||||
if (response?.ssoAvailable) {
|
||||
this.identifier = response.organizationIdentifier;
|
||||
await this.submit();
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
this.handleGetClaimedDomainByEmailError(error);
|
||||
}
|
||||
|
||||
this.loggingIn = false;
|
||||
}
|
||||
|
||||
// Fallback to state svc if domain is unclaimed
|
||||
const storedIdentifier = await this.stateService.getSsoOrgIdentifier();
|
||||
if (storedIdentifier != null) {
|
||||
this.identifier = storedIdentifier;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private handleGetClaimedDomainByEmailError(error: any): void {
|
||||
if (error instanceof ErrorResponse) {
|
||||
const errorResponse: ErrorResponse = error as ErrorResponse;
|
||||
switch (errorResponse.statusCode) {
|
||||
case HttpStatusCode.NotFound:
|
||||
//this is a valid case for a domain not found
|
||||
return;
|
||||
|
||||
default:
|
||||
this.validationService.showError(errorResponse);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async submit() {
|
||||
await this.stateService.setSsoOrganizationIdentifier(this.identifier);
|
||||
if (this.clientId === "browser") {
|
||||
document.cookie = `ssoHandOffMessage=${this.i18nService.t("ssoHandOff")};SameSite=strict`;
|
||||
}
|
||||
super.submit();
|
||||
}
|
||||
}
|
||||
68
apps/web/src/app/auth/two-factor-options.component.html
Normal file
68
apps/web/src/app/auth/two-factor-options.component.html
Normal file
@@ -0,0 +1,68 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="twoStepOptionsTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="twoStepOptionsTitle">{{ "twoStepOptions" | i18n }}</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="list-group list-group-flush-2fa">
|
||||
<div *ngFor="let p of providers" class="list-group-item list-group-item-action">
|
||||
<div class="two-factor-content">
|
||||
<div class="logo-col">
|
||||
<img [class]="'mfaType' + p.type" [alt]="p.name + ' logo'" />
|
||||
</div>
|
||||
<div class="text-col">
|
||||
<h3>{{ p.name }}</h3>
|
||||
{{ p.description }}
|
||||
</div>
|
||||
<div class="btn-col">
|
||||
<button
|
||||
[attr.aria-describedby]="p.name"
|
||||
type="button"
|
||||
class="btn btn-outline-secondary btn-sm"
|
||||
(click)="choose(p)"
|
||||
>
|
||||
{{ "select" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group-item list-group-item-action" (click)="recover()">
|
||||
<div class="two-factor-content">
|
||||
<div class="logo-col">
|
||||
<img class="recovery-code-img" alt="rc logo" />
|
||||
</div>
|
||||
<div class="text-col">
|
||||
<h3>{{ "recoveryCodeTitle" | i18n }}</h3>
|
||||
{{ "recoveryCodeDesc" | i18n }}
|
||||
</div>
|
||||
<div class="btn-col">
|
||||
<button
|
||||
[attr.aria-describedby]="'recoveryCodeTitle' | i18n"
|
||||
type="button"
|
||||
class="btn btn-outline-secondary btn-sm"
|
||||
(click)="recover()"
|
||||
>
|
||||
{{ "select" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
22
apps/web/src/app/auth/two-factor-options.component.ts
Normal file
22
apps/web/src/app/auth/two-factor-options.component.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-options.component";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-two-factor-options",
|
||||
templateUrl: "two-factor-options.component.html",
|
||||
})
|
||||
export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent {
|
||||
constructor(
|
||||
twoFactorService: TwoFactorService,
|
||||
router: Router,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService
|
||||
) {
|
||||
super(twoFactorService, router, i18nService, platformUtilsService, window);
|
||||
}
|
||||
}
|
||||
155
apps/web/src/app/auth/two-factor.component.html
Normal file
155
apps/web/src/app/auth/two-factor.component.html
Normal file
@@ -0,0 +1,155 @@
|
||||
<form
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
class="container"
|
||||
ngNativeValidate
|
||||
autocomplete="off"
|
||||
>
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div
|
||||
class="col-5"
|
||||
[ngClass]="{
|
||||
'col-9':
|
||||
selectedProviderType === providerType.Duo ||
|
||||
selectedProviderType === providerType.OrganizationDuo
|
||||
}"
|
||||
>
|
||||
<p class="lead text-center mb-4">{{ title }}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body">
|
||||
<ng-container
|
||||
*ngIf="
|
||||
selectedProviderType === providerType.Email ||
|
||||
selectedProviderType === providerType.Authenticator
|
||||
"
|
||||
>
|
||||
<p *ngIf="selectedProviderType === providerType.Authenticator">
|
||||
{{ "enterVerificationCodeApp" | i18n }}
|
||||
</p>
|
||||
<p *ngIf="selectedProviderType === providerType.Email">
|
||||
{{ "enterVerificationCodeEmail" | i18n : twoFactorEmail }}
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label for="code" class="sr-only">{{ "verificationCode" | i18n }}</label>
|
||||
<input
|
||||
id="code"
|
||||
type="text"
|
||||
name="Code"
|
||||
class="form-control"
|
||||
[(ngModel)]="token"
|
||||
required
|
||||
appAutofocus
|
||||
inputmode="tel"
|
||||
appInputVerbatim
|
||||
/>
|
||||
<small class="form-text" *ngIf="selectedProviderType === providerType.Email">
|
||||
<a
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="sendEmail(true)"
|
||||
[appApiAction]="emailPromise"
|
||||
*ngIf="selectedProviderType === providerType.Email"
|
||||
>
|
||||
{{ "sendVerificationCodeEmailAgain" | i18n }}
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selectedProviderType === providerType.Yubikey">
|
||||
<p class="text-center">{{ "insertYubiKey" | i18n }}</p>
|
||||
<picture>
|
||||
<source srcset="../../images/yubikey.avif" type="image/avif" />
|
||||
<source srcset="../../images/yubikey.webp" type="image/webp" />
|
||||
<img src="../../images/yubikey.jpg" class="rounded img-fluid mb-3" alt="" />
|
||||
</picture>
|
||||
<div class="form-group">
|
||||
<label for="code" class="sr-only">{{ "verificationCode" | i18n }}</label>
|
||||
<input
|
||||
id="code"
|
||||
type="password"
|
||||
name="Code"
|
||||
class="form-control"
|
||||
[(ngModel)]="token"
|
||||
required
|
||||
appAutofocus
|
||||
appInputVerbatim
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selectedProviderType === providerType.WebAuthn">
|
||||
<div id="web-authn-frame" class="mb-3">
|
||||
<iframe id="webauthn_iframe"></iframe>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container
|
||||
*ngIf="
|
||||
selectedProviderType === providerType.Duo ||
|
||||
selectedProviderType === providerType.OrganizationDuo
|
||||
"
|
||||
>
|
||||
<div id="duo-frame" class="mb-3">
|
||||
<iframe id="duo_iframe"></iframe>
|
||||
</div>
|
||||
</ng-container>
|
||||
<i
|
||||
class="bwi bwi-spinner text-muted bwi-spin pull-right"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
*ngIf="form.loading && selectedProviderType === providerType.WebAuthn"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<div class="form-check" *ngIf="selectedProviderType != null">
|
||||
<input
|
||||
id="remember"
|
||||
type="checkbox"
|
||||
name="Remember"
|
||||
class="form-check-input"
|
||||
[(ngModel)]="remember"
|
||||
/>
|
||||
<label for="remember" class="form-check-label">{{ "rememberMe" | i18n }}</label>
|
||||
</div>
|
||||
<ng-container *ngIf="selectedProviderType == null">
|
||||
<p>{{ "noTwoStepProviders" | i18n }}</p>
|
||||
<p>{{ "noTwoStepProviders2" | i18n }}</p>
|
||||
</ng-container>
|
||||
<hr />
|
||||
<div [hidden]="!showCaptcha()">
|
||||
<iframe id="hcaptcha_iframe" height="80"></iframe>
|
||||
</div>
|
||||
<div class="d-flex mb-3">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-block btn-submit"
|
||||
[disabled]="form.loading"
|
||||
*ngIf="
|
||||
selectedProviderType != null &&
|
||||
selectedProviderType !== providerType.Duo &&
|
||||
selectedProviderType !== providerType.OrganizationDuo &&
|
||||
selectedProviderType !== providerType.WebAuthn
|
||||
"
|
||||
>
|
||||
<span>
|
||||
<i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ "continue" | i18n }}
|
||||
</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
||||
{{ "cancel" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<a href="#" appStopClick (click)="anotherMethod()">{{
|
||||
"useAnotherTwoStepMethod" | i18n
|
||||
}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<ng-template #twoFactorOptions></ng-template>
|
||||
111
apps/web/src/app/auth/two-factor.component.ts
Normal file
111
apps/web/src/app/auth/two-factor.component.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
|
||||
import { RouterService } from "../core";
|
||||
|
||||
import { TwoFactorOptionsComponent } from "./two-factor-options.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-two-factor",
|
||||
templateUrl: "two-factor.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class TwoFactorComponent extends BaseTwoFactorComponent {
|
||||
@ViewChild("twoFactorOptions", { read: ViewContainerRef, static: true })
|
||||
twoFactorOptionsModal: ViewContainerRef;
|
||||
|
||||
constructor(
|
||||
authService: AuthService,
|
||||
router: Router,
|
||||
i18nService: I18nService,
|
||||
apiService: ApiService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
stateService: StateService,
|
||||
environmentService: EnvironmentService,
|
||||
private modalService: ModalService,
|
||||
route: ActivatedRoute,
|
||||
logService: LogService,
|
||||
twoFactorService: TwoFactorService,
|
||||
appIdService: AppIdService,
|
||||
private routerService: RouterService,
|
||||
loginService: LoginService
|
||||
) {
|
||||
super(
|
||||
authService,
|
||||
router,
|
||||
i18nService,
|
||||
apiService,
|
||||
platformUtilsService,
|
||||
window,
|
||||
environmentService,
|
||||
stateService,
|
||||
route,
|
||||
logService,
|
||||
twoFactorService,
|
||||
appIdService,
|
||||
loginService
|
||||
);
|
||||
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||
}
|
||||
|
||||
async anotherMethod() {
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
TwoFactorOptionsComponent,
|
||||
this.twoFactorOptionsModal,
|
||||
(comp) => {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
comp.onProviderSelected.subscribe(async (provider: TwoFactorProviderType) => {
|
||||
modal.close();
|
||||
this.selectedProviderType = provider;
|
||||
await this.init();
|
||||
});
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
comp.onRecoverSelected.subscribe(() => {
|
||||
modal.close();
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async goAfterLogIn() {
|
||||
this.loginService.clearValues();
|
||||
const previousUrl = this.routerService.getPreviousUrl();
|
||||
if (previousUrl) {
|
||||
this.router.navigateByUrl(previousUrl);
|
||||
} else {
|
||||
// if we have an emergency access invite, redirect to emergency access
|
||||
const emergencyAccessInvite = await this.stateService.getEmergencyAccessInvitation();
|
||||
if (emergencyAccessInvite != null) {
|
||||
this.router.navigate(["/accept-emergency"], {
|
||||
queryParams: {
|
||||
id: emergencyAccessInvite.id,
|
||||
name: emergencyAccessInvite.name,
|
||||
email: emergencyAccessInvite.email,
|
||||
token: emergencyAccessInvite.token,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.router.navigate([this.successRoute], {
|
||||
queryParams: {
|
||||
identifier: this.identifier,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
91
apps/web/src/app/auth/update-password.component.html
Normal file
91
apps/web/src/app/auth/update-password.component.html
Normal file
@@ -0,0 +1,91 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate autocomplete="off">
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-4">
|
||||
<p class="lead text-center mb-4">{{ "updateMasterPassword" | i18n }}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body">
|
||||
<app-callout type="warning">{{ "masterPasswordInvalidWarning" | i18n }} </app-callout>
|
||||
<app-callout
|
||||
type="info"
|
||||
[enforcedPolicyOptions]="enforcedPolicyOptions"
|
||||
*ngIf="enforcedPolicyOptions"
|
||||
></app-callout>
|
||||
|
||||
<form
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
autocomplete="off"
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="currentMasterPassword">{{ "currentMasterPass" | i18n }}</label>
|
||||
<input
|
||||
id="currentMasterPassword"
|
||||
type="password"
|
||||
name="MasterPasswordHash"
|
||||
class="form-control"
|
||||
[(ngModel)]="currentMasterPassword"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="newMasterPassword">{{ "newMasterPass" | i18n }}</label>
|
||||
<input
|
||||
id="newMasterPassword"
|
||||
type="password"
|
||||
name="NewMasterPasswordHash"
|
||||
class="form-control mb-1"
|
||||
[(ngModel)]="masterPassword"
|
||||
required
|
||||
appInputVerbatim
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<app-password-strength
|
||||
[password]="masterPassword"
|
||||
[email]="email"
|
||||
[showText]="true"
|
||||
(passwordStrengthResult)="getStrengthResult($event)"
|
||||
></app-password-strength>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="masterPasswordRetype">{{ "confirmNewMasterPass" | i18n }}</label>
|
||||
<input
|
||||
id="masterPasswordRetype"
|
||||
type="password"
|
||||
name="MasterPasswordRetype"
|
||||
class="form-control"
|
||||
[(ngModel)]="masterPasswordRetype"
|
||||
required
|
||||
appInputVerbatim
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i
|
||||
class="fa fa-spinner fa-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span>{{ "changeMasterPassword" | i18n }}</span>
|
||||
</button>
|
||||
<button (click)="cancel()" type="button" class="btn btn-outline-secondary">
|
||||
<span>{{ "cancel" | i18n }}</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
51
apps/web/src/app/auth/update-password.component.ts
Normal file
51
apps/web/src/app/auth/update-password.component.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { UpdatePasswordComponent as BaseUpdatePasswordComponent } from "@bitwarden/angular/auth/components/update-password.component";
|
||||
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
|
||||
@Component({
|
||||
selector: "app-update-password",
|
||||
templateUrl: "update-password.component.html",
|
||||
})
|
||||
export class UpdatePasswordComponent extends BaseUpdatePasswordComponent {
|
||||
constructor(
|
||||
router: Router,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
policyService: PolicyService,
|
||||
cryptoService: CryptoService,
|
||||
messagingService: MessagingService,
|
||||
apiService: ApiService,
|
||||
logService: LogService,
|
||||
stateService: StateService,
|
||||
userVerificationService: UserVerificationService,
|
||||
dialogService: DialogServiceAbstraction
|
||||
) {
|
||||
super(
|
||||
router,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
passwordGenerationService,
|
||||
policyService,
|
||||
cryptoService,
|
||||
messagingService,
|
||||
apiService,
|
||||
stateService,
|
||||
userVerificationService,
|
||||
logService,
|
||||
dialogService
|
||||
);
|
||||
}
|
||||
}
|
||||
100
apps/web/src/app/auth/update-temp-password.component.html
Normal file
100
apps/web/src/app/auth/update-temp-password.component.html
Normal file
@@ -0,0 +1,100 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate autocomplete="off">
|
||||
<div class="tw-mt-12 tw-flex tw-justify-center">
|
||||
<div class="tw-w-1/3">
|
||||
<h1 bitTypography="h1" class="tw-mb-4 tw-text-center">{{ "updateMasterPassword" | i18n }}</h1>
|
||||
<div
|
||||
class="tw-block tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-8"
|
||||
>
|
||||
<app-callout type="warning">{{ masterPasswordWarningText }} </app-callout>
|
||||
<app-callout
|
||||
type="info"
|
||||
[enforcedPolicyOptions]="enforcedPolicyOptions"
|
||||
*ngIf="enforcedPolicyOptions"
|
||||
>
|
||||
</app-callout>
|
||||
<bit-form-field *ngIf="requireCurrentPassword">
|
||||
<bit-label>{{ "currentMasterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="password"
|
||||
appInputVerbatim
|
||||
required
|
||||
[(ngModel)]="verification.secret"
|
||||
name="currentMasterPassword"
|
||||
id="currentMasterPassword"
|
||||
[appAutofocus]="requireCurrentPassword"
|
||||
/>
|
||||
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
|
||||
</bit-form-field>
|
||||
<div class="tw-mb-4">
|
||||
<bit-form-field class="!tw-mb-1">
|
||||
<bit-label>{{ "newMasterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="password"
|
||||
appInputVerbatim
|
||||
required
|
||||
[(ngModel)]="masterPassword"
|
||||
name="masterPassword"
|
||||
id="masterPassword"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton
|
||||
bitSuffix
|
||||
bitPasswordInputToggle
|
||||
[(toggled)]="showPassword"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
<app-password-strength
|
||||
[password]="masterPassword"
|
||||
[email]="email"
|
||||
[showText]="true"
|
||||
(passwordStrengthResult)="getStrengthResult($event)"
|
||||
>
|
||||
</app-password-strength>
|
||||
</div>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "confirmNewMasterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="password"
|
||||
appInputVerbatim
|
||||
required
|
||||
[(ngModel)]="masterPasswordRetype"
|
||||
name="masterPasswordRetype"
|
||||
id="masterPasswordRetype"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton
|
||||
bitSuffix
|
||||
bitPasswordInputToggle
|
||||
[(toggled)]="showPassword"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "masterPassHint" | i18n }}</bit-label>
|
||||
<input bitInput type="text" [(ngModel)]="hint" name="hint" id="hint" />
|
||||
<bit-hint>{{ "masterPassHintDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<hr />
|
||||
<div class="tw-flex tw-space-x-2">
|
||||
<button
|
||||
type="submit"
|
||||
bitButton
|
||||
[block]="true"
|
||||
buttonType="primary"
|
||||
[loading]="form.loading"
|
||||
[disabled]="form.loading"
|
||||
>
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton [block]="true" buttonType="secondary" (click)="logOut()">
|
||||
{{ "logOut" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
9
apps/web/src/app/auth/update-temp-password.component.ts
Normal file
9
apps/web/src/app/auth/update-temp-password.component.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from "@bitwarden/angular/auth/components/update-temp-password.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-update-temp-password",
|
||||
templateUrl: "update-temp-password.component.html",
|
||||
})
|
||||
export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent {}
|
||||
13
apps/web/src/app/auth/verify-email-token.component.html
Normal file
13
apps/web/src/app/auth/verify-email-token.component.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<div class="mt-5 d-flex justify-content-center">
|
||||
<div>
|
||||
<img class="mb-4 logo logo-themed" alt="Bitwarden" />
|
||||
<p class="text-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
50
apps/web/src/app/auth/verify-email-token.component.ts
Normal file
50
apps/web/src/app/auth/verify-email-token.component.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { VerifyEmailRequest } from "@bitwarden/common/models/request/verify-email.request";
|
||||
|
||||
@Component({
|
||||
selector: "app-verify-email-token",
|
||||
templateUrl: "verify-email-token.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class VerifyEmailTokenComponent implements OnInit {
|
||||
constructor(
|
||||
private router: Router,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private route: ActivatedRoute,
|
||||
private apiService: ApiService,
|
||||
private logService: LogService,
|
||||
private stateService: StateService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||
if (qParams.userId != null && qParams.token != null) {
|
||||
try {
|
||||
await this.apiService.postAccountVerifyEmailToken(
|
||||
new VerifyEmailRequest(qParams.userId, qParams.token)
|
||||
);
|
||||
if (await this.stateService.getIsAuthenticated()) {
|
||||
await this.apiService.refreshIdentityToken();
|
||||
}
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("emailVerified"));
|
||||
this.router.navigate(["/"]);
|
||||
return;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("emailVerifiedFailed"));
|
||||
this.router.navigate(["/"]);
|
||||
});
|
||||
}
|
||||
}
|
||||
34
apps/web/src/app/auth/verify-recover-delete.component.html
Normal file
34
apps/web/src/app/auth/verify-recover-delete.component.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="lead text-center mb-4">{{ "deleteAccount" | i18n }}</p>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<app-callout type="warning">{{ "deleteAccountWarning" | i18n }}</app-callout>
|
||||
<p class="text-center">
|
||||
<strong>{{ email }}</strong>
|
||||
</p>
|
||||
<p>{{ "deleteRecoverConfirmDesc" | i18n }}</p>
|
||||
<hr />
|
||||
<div class="d-flex">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-danger btn-block btn-submit"
|
||||
[disabled]="form.loading"
|
||||
>
|
||||
<span>{{ "deleteAccount" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
||||
{{ "cancel" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
60
apps/web/src/app/auth/verify-recover-delete.component.ts
Normal file
60
apps/web/src/app/auth/verify-recover-delete.component.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { VerifyDeleteRecoverRequest } from "@bitwarden/common/models/request/verify-delete-recover.request";
|
||||
|
||||
@Component({
|
||||
selector: "app-verify-recover-delete",
|
||||
templateUrl: "verify-recover-delete.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class VerifyRecoverDeleteComponent implements OnInit {
|
||||
email: string;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
private userId: string;
|
||||
private token: string;
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private apiService: ApiService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private route: ActivatedRoute,
|
||||
private logService: LogService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||
if (qParams.userId != null && qParams.token != null && qParams.email != null) {
|
||||
this.userId = qParams.userId;
|
||||
this.token = qParams.token;
|
||||
this.email = qParams.email;
|
||||
} else {
|
||||
this.router.navigate(["/"]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
const request = new VerifyDeleteRecoverRequest(this.userId, this.token);
|
||||
this.formPromise = this.apiService.postAccountRecoverDeleteToken(request);
|
||||
await this.formPromise;
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
this.i18nService.t("accountDeleted"),
|
||||
this.i18nService.t("accountDeletedDesc")
|
||||
);
|
||||
this.router.navigate(["/"]);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user