1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-11 22:13:32 +00:00

Merge branch 'tools/PM-10938/1pux-ssh-key-import' into update-1pux-ssh-pr

This commit is contained in:
Bernd Schoolmann
2024-12-24 12:06:46 +01:00
committed by GitHub
46 changed files with 133 additions and 4070 deletions

View File

@@ -37,7 +37,6 @@
./apps/browser/store/windows/AppxManifest.xml
./apps/browser/src/background/nativeMessaging.background.ts
./apps/browser/src/models/browserComponentState.ts
./apps/browser/src/models/browserSendComponentState.ts
./apps/browser/src/models/browserGroupingsComponentState.ts
./apps/browser/src/models/biometricErrors.ts
./apps/browser/src/browser/safariApp.ts

View File

@@ -454,9 +454,6 @@
"length": {
"message": "Length"
},
"passwordMinLength": {
"message": "Minimum password length"
},
"uppercase": {
"message": "Uppercase (A-Z)",
"description": "deprecated. Use uppercaseLabel instead."
@@ -528,10 +525,6 @@
"minSpecial": {
"message": "Minimum special"
},
"avoidAmbChar": {
"message": "Avoid ambiguous characters",
"description": "deprecated. Use avoidAmbiguous instead."
},
"avoidAmbiguous": {
"message": "Avoid ambiguous characters",
"description": "Label for the avoid ambiguous characters checkbox."
@@ -1157,9 +1150,6 @@
"moveToOrganization": {
"message": "Move to organization"
},
"share": {
"message": "Share"
},
"movedItemToOrg": {
"message": "$ITEMNAME$ moved to $ORGNAME$",
"placeholders": {
@@ -2047,9 +2037,6 @@
"clone": {
"message": "Clone"
},
"passwordGeneratorPolicyInEffect": {
"message": "One or more organization policies are affecting your generator settings."
},
"passwordGenerator": {
"message": "Password generator"
},
@@ -2389,14 +2376,6 @@
"message": "Send details",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"searchSends": {
"message": "Search Sends",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"addSend": {
"message": "Add Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendTypeText": {
"message": "Text"
},
@@ -2413,16 +2392,9 @@
"hideTextByDefault": {
"message": "Hide text by default"
},
"maxAccessCountReached": {
"message": "Max access count reached",
"description": "This text will be displayed after a Send has been accessed the maximum amount of times."
},
"expired": {
"message": "Expired"
},
"pendingDeletion": {
"message": "Pending deletion"
},
"passwordProtected": {
"message": "Password protected"
},
@@ -2472,24 +2444,9 @@
"message": "Edit Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendTypeHeader": {
"message": "What type of Send is this?",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendNameDesc": {
"message": "A friendly name to describe this Send.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendFileDesc": {
"message": "The file you want to send."
},
"deletionDate": {
"message": "Deletion date"
},
"deletionDateDesc": {
"message": "The Send will be permanently deleted on the specified date and time.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deletionDateDescV2": {
"message": "The Send will be permanently deleted on this date.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
@@ -2497,10 +2454,6 @@
"expirationDate": {
"message": "Expiration date"
},
"expirationDateDesc": {
"message": "If set, access to this Send will expire on the specified date and time.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"oneDay": {
"message": "1 day"
},
@@ -2516,43 +2469,10 @@
"custom": {
"message": "Custom"
},
"maximumAccessCount": {
"message": "Maximum Access Count"
},
"maximumAccessCountDesc": {
"message": "If set, users will no longer be able to access this Send once the maximum access count is reached.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendPasswordDesc": {
"message": "Optionally require a password for users to access this Send.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendPasswordDescV3": {
"message": "Add an optional password for recipients to access this Send.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendNotesDesc": {
"message": "Private notes about this Send.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendDisableDesc": {
"message": "Deactivate this Send so that no one can access it.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendShareDesc": {
"message": "Copy this Send's link to clipboard upon save.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendTextDesc": {
"message": "The text you want to send."
},
"sendHideText": {
"message": "Hide this Send's text by default.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"currentAccessCount": {
"message": "Current access count"
},
"createSend": {
"message": "New Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
@@ -2635,18 +2555,6 @@
"sendFileCalloutHeader": {
"message": "Before you start"
},
"sendFirefoxCustomDatePopoutMessage1": {
"message": "To use a calendar style date picker",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'"
},
"sendFirefoxCustomDatePopoutMessage2": {
"message": "click here",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'"
},
"sendFirefoxCustomDatePopoutMessage3": {
"message": "to pop out your window.",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'"
},
"expirationDateIsInvalid": {
"message": "The expiration date provided is not valid."
},
@@ -2662,15 +2570,9 @@
"dateParsingError": {
"message": "There was an error saving your deletion and expiration dates."
},
"hideEmail": {
"message": "Hide my email address from recipients."
},
"hideYourEmail": {
"message": "Hide your email address from viewers."
},
"sendOptionsPolicyInEffect": {
"message": "One or more organization policies are affecting your Send options."
},
"passwordPrompt": {
"message": "Master password re-prompt"
},
@@ -2884,9 +2786,6 @@
"error": {
"message": "Error"
},
"regenerateUsername": {
"message": "Regenerate username"
},
"generateUsername": {
"message": "Generate username"
},
@@ -2927,9 +2826,6 @@
}
}
},
"usernameType": {
"message": "Username type"
},
"plusAddressedEmail": {
"message": "Plus addressed email",
"description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com"
@@ -2952,12 +2848,6 @@
"websiteName": {
"message": "Website name"
},
"whatWouldYouLikeToGenerate": {
"message": "What would you like to generate?"
},
"passwordType": {
"message": "Password type"
},
"service": {
"message": "Service"
},

View File

@@ -1,100 +0,0 @@
<form (ngSubmit)="submit()">
<app-header>
<div class="left"></div>
<h1 class="center">
<span class="title">{{ "verifyIdentity" | i18n }}</span>
</h1>
<div class="right">
<button type="submit" *ngIf="pinEnabled || masterPasswordEnabled">
{{ "unlock" | i18n }}
</button>
</div>
</app-header>
<main tabindex="-1">
<ng-container *ngIf="fido2PopoutSessionData$ | async as fido2Data">
<div class="box">
<div class="box-content">
<div
class="box-content-row box-content-row-flex"
appBoxRow
*ngIf="pinEnabled || masterPasswordEnabled"
>
<div class="row-main" *ngIf="pinEnabled">
<label for="pin">{{ "pin" | i18n }}</label>
<input
id="pin"
type="{{ showPassword ? 'text' : 'password' }}"
name="PIN"
class="monospaced"
[(ngModel)]="pin"
required
appInputVerbatim
/>
</div>
<div class="row-main" *ngIf="masterPasswordEnabled && !pinEnabled">
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
aria-describedby="masterPasswordHelp"
class="monospaced"
[(ngModel)]="masterPassword"
required
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword()"
[attr.aria-pressed]="showPassword"
>
<i
class="bwi bwi-lg"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
aria-hidden="true"
></i>
</button>
</div>
</div>
</div>
<div id="masterPasswordHelp" class="box-footer">
<p>
{{
fido2Data.isFido2Session
? ("yourPasskeyIsLocked" | i18n)
: ("yourVaultIsLocked" | i18n)
}}
</p>
{{ "loggedInAsOn" | i18n: email : webVaultHostname }}
</div>
</div>
<div class="box" *ngIf="biometricLock">
<div class="box-footer no-pad">
<button
type="button"
class="btn primary block"
(click)="unlockBiometric()"
appStopClick
[disabled]="pendingBiometric"
>
{{ "unlockWithBiometrics" | i18n }}
</button>
</div>
</div>
<p class="text-center" *ngIf="!fido2Data.isFido2Session">
<button type="button" appStopClick (click)="logOut()">{{ "logOut" | i18n }}</button>
</p>
<app-callout *ngIf="biometricError" type="danger">{{ biometricError }}</app-callout>
<p class="text-center text-muted" *ngIf="pendingBiometric">
<i class="bwi bwi-spinner bwi-spin" aria-hidden="true"></i> {{ "awaitDesktop" | i18n }}
</p>
<app-fido2-use-browser-link-v1></app-fido2-use-browser-link-v1>
</ng-container>
</main>
</form>

View File

@@ -1,185 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, NgZone, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService, ToastService } from "@bitwarden/components";
import {
KdfConfigService,
KeyService,
BiometricsService,
BiometricStateService,
} from "@bitwarden/key-management";
import { BiometricErrors, BiometricErrorTypes } from "../../models/biometricErrors";
import { BrowserRouterService } from "../../platform/popup/services/browser-router.service";
import { fido2PopoutSessionData$ } from "../../vault/popup/utils/fido2-popout-session-data";
@Component({
selector: "app-lock",
templateUrl: "lock.component.html",
})
export class LockComponent extends BaseLockComponent implements OnInit {
private isInitialLockScreen: boolean;
biometricError: string;
pendingBiometric = false;
fido2PopoutSessionData$ = fido2PopoutSessionData$();
constructor(
masterPasswordService: InternalMasterPasswordServiceAbstraction,
router: Router,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
messagingService: MessagingService,
keyService: KeyService,
vaultTimeoutService: VaultTimeoutService,
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
environmentService: EnvironmentService,
stateService: StateService,
apiService: ApiService,
logService: LogService,
ngZone: NgZone,
policyApiService: PolicyApiServiceAbstraction,
policyService: InternalPolicyService,
passwordStrengthService: PasswordStrengthServiceAbstraction,
authService: AuthService,
dialogService: DialogService,
deviceTrustService: DeviceTrustServiceAbstraction,
userVerificationService: UserVerificationService,
pinService: PinServiceAbstraction,
private routerService: BrowserRouterService,
biometricStateService: BiometricStateService,
biometricsService: BiometricsService,
accountService: AccountService,
kdfConfigService: KdfConfigService,
syncService: SyncService,
toastService: ToastService,
) {
super(
masterPasswordService,
router,
i18nService,
platformUtilsService,
messagingService,
keyService,
vaultTimeoutService,
vaultTimeoutSettingsService,
environmentService,
stateService,
apiService,
logService,
ngZone,
policyApiService,
policyService,
passwordStrengthService,
dialogService,
deviceTrustService,
userVerificationService,
pinService,
biometricStateService,
biometricsService,
accountService,
authService,
kdfConfigService,
syncService,
toastService,
);
this.successRoute = "/tabs/current";
this.isInitialLockScreen = (window as any).previousPopupUrl == null;
this.onSuccessfulSubmit = async () => {
const previousUrl = this.routerService.getPreviousUrl();
if (previousUrl) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigateByUrl(previousUrl);
} else {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate([this.successRoute]);
}
};
}
async ngOnInit() {
await super.ngOnInit();
const autoBiometricsPrompt = await firstValueFrom(
this.biometricStateService.promptAutomatically$,
);
window.setTimeout(async () => {
document.getElementById(this.pinEnabled ? "pin" : "masterPassword")?.focus();
if (
this.biometricLock &&
autoBiometricsPrompt &&
this.isInitialLockScreen &&
(await this.authService.getAuthStatus()) === AuthenticationStatus.Locked
) {
await this.unlockBiometric(true);
}
}, 100);
}
override async unlockBiometric(automaticPrompt: boolean = false): Promise<boolean> {
if (!this.biometricLock) {
return;
}
this.biometricError = null;
let success;
try {
const available = await super.isBiometricUnlockAvailable();
if (!available) {
if (!automaticPrompt) {
await this.dialogService.openSimpleDialog({
type: "warning",
title: { key: "biometricsNotAvailableTitle" },
content: { key: "biometricsNotAvailableDesc" },
acceptButtonText: { key: "ok" },
cancelButtonText: null,
});
}
} else {
this.pendingBiometric = true;
success = await super.unlockBiometric();
}
} catch (e) {
const error = BiometricErrors[e?.message as BiometricErrorTypes];
if (error == null) {
this.logService.error("Unknown error: " + e);
return false;
}
this.biometricError = this.i18nService.t(error.description);
} finally {
this.pendingBiometric = false;
}
return success;
}
}

View File

@@ -1,20 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
import { DeepJsonify } from "@bitwarden/common/types/deep-jsonify";
import { BrowserComponentState } from "./browserComponentState";
export class BrowserSendComponentState extends BrowserComponentState {
sends: SendView[];
static fromJSON(json: DeepJsonify<BrowserSendComponentState>) {
if (json == null) {
return null;
}
return Object.assign(new BrowserSendComponentState(), json, {
sends: json.sends?.map((s) => SendView.fromJSON(s)),
});
}
}

View File

@@ -17,7 +17,6 @@ import {
unauthGuardFn,
} from "@bitwarden/angular/auth/guards";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import { extensionRefreshRedirect } from "@bitwarden/angular/utils/extension-refresh-redirect";
import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap";
import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards";
import {
@@ -26,7 +25,7 @@ import {
LoginComponent,
LoginSecondaryContentComponent,
LockIcon,
LockV2Component,
LockComponent,
LoginViaAuthRequestComponent,
PasswordHintComponent,
RegistrationFinishComponent,
@@ -60,7 +59,6 @@ import {
} from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component";
import { HintComponent } from "../auth/popup/hint.component";
import { HomeComponent } from "../auth/popup/home.component";
import { LockComponent } from "../auth/popup/lock.component";
import { LoginDecryptionOptionsComponentV1 } from "../auth/popup/login-decryption-options/login-decryption-options-v1.component";
import { LoginComponentV1 } from "../auth/popup/login-v1.component";
import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-request-v1.component";
@@ -88,11 +86,6 @@ import BrowserPopupUtils from "../platform/popup/browser-popup-utils";
import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service";
import { CredentialGeneratorHistoryComponent } from "../tools/popup/generator/credential-generator-history.component";
import { CredentialGeneratorComponent } from "../tools/popup/generator/credential-generator.component";
import { GeneratorComponent } from "../tools/popup/generator/generator.component";
import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component";
import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.component";
import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.component";
import { SendTypeComponent } from "../tools/popup/send/send-type.component";
import { SendAddEditComponent as SendAddEditV2Component } from "../tools/popup/send-v2/add-edit/send-add-edit.component";
import { SendCreatedComponent } from "../tools/popup/send-v2/send-created/send-created.component";
import { SendV2Component } from "../tools/popup/send-v2/send-v2.component";
@@ -173,13 +166,6 @@ const routes: Routes = [
canActivate: [fido2AuthGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
}),
{
path: "lock",
component: LockComponent,
canActivate: [lockGuard()],
canMatch: [extensionRefreshRedirect("/lockV2")],
data: { elevation: 1, doNotSaveUrl: true } satisfies RouteDataProperties,
},
...twofactorRefactorSwap(
TwoFactorComponent,
AnonLayoutWrapperComponent,
@@ -339,15 +325,16 @@ const routes: Routes = [
}),
{
path: "generator",
component: GeneratorComponent,
component: CredentialGeneratorComponent,
canActivate: [authGuard],
data: { elevation: 0 } satisfies RouteDataProperties,
},
...extensionRefreshSwap(PasswordGeneratorHistoryComponent, CredentialGeneratorHistoryComponent, {
{
path: "generator-history",
component: CredentialGeneratorHistoryComponent,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
}),
},
{
path: "import",
component: ImportBrowserV2Component,
@@ -425,21 +412,17 @@ const routes: Routes = [
data: { elevation: 1 } satisfies RouteDataProperties,
}),
{
path: "send-type",
component: SendTypeComponent,
path: "add-send",
component: SendAddEditV2Component,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
},
...extensionRefreshSwap(SendAddEditComponent, SendAddEditV2Component, {
path: "add-send",
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
}),
...extensionRefreshSwap(SendAddEditComponent, SendAddEditV2Component, {
{
path: "edit-send",
component: SendAddEditV2Component,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
}),
},
{
path: "send-created",
component: SendCreatedComponent,
@@ -650,8 +633,8 @@ const routes: Routes = [
],
},
{
path: "lockV2",
canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh), lockGuard()],
path: "lock",
canActivate: [lockGuard()],
data: {
pageIcon: LockIcon,
pageTitle: {
@@ -662,19 +645,19 @@ const routes: Routes = [
elevation: 1,
/**
* This ensures that in a passkey flow the `/fido2?<queryParams>` URL does not get
* overwritten in the `BrowserRouterService` by the `/lockV2` route. This way, after
* overwritten in the `BrowserRouterService` by the `/lock` route. This way, after
* unlocking, the user can be redirected back to the `/fido2?<queryParams>` URL.
*
* Also, this prevents a routing loop when using biometrics to unlock the vault in MV2 (Firefox),
* locking up the browser (https://bitwarden.atlassian.net/browse/PM-16116). This involves the
* `popup-router-cache.service` pushing the `lockV2` route to the history.
* `popup-router-cache.service` pushing the `lock` route to the history.
*/
doNotSaveUrl: true,
} satisfies ExtensionAnonLayoutWrapperData & RouteDataProperties,
children: [
{
path: "",
component: LockV2Component,
component: LockComponent,
},
],
},
@@ -766,22 +749,24 @@ const routes: Routes = [
canDeactivate: [clearVaultStateGuard],
data: { elevation: 0 } satisfies RouteDataProperties,
},
...extensionRefreshSwap(GeneratorComponent, CredentialGeneratorComponent, {
{
path: "generator",
component: CredentialGeneratorComponent,
canActivate: [authGuard],
data: { elevation: 0 } satisfies RouteDataProperties,
}),
},
{
path: "settings",
component: SettingsV2Component,
canActivate: [authGuard],
data: { elevation: 0 } satisfies RouteDataProperties,
},
...extensionRefreshSwap(SendGroupingsComponent, SendV2Component, {
{
path: "send",
component: SendV2Component,
canActivate: [authGuard],
data: { elevation: 0 } satisfies RouteDataProperties,
}),
},
],
},
{

View File

@@ -26,7 +26,6 @@ import { PopupCompactModeService } from "../platform/popup/layout/popup-compact-
import { PopupWidthService } from "../platform/popup/layout/popup-width.service";
import { PopupViewCacheService } from "../platform/popup/view-cache/popup-view-cache.service";
import { initPopupClosedListener } from "../platform/services/popup-view-cache-background.service";
import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service";
import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service";
import { routerTransition } from "./app-routing.animations";
@@ -57,7 +56,6 @@ export class AppComponent implements OnInit, OnDestroy {
private i18nService: I18nService,
private router: Router,
private stateService: StateService,
private browserSendStateService: BrowserSendStateService,
private vaultBrowserStateService: VaultBrowserStateService,
private cipherService: CipherService,
private changeDetectorRef: ChangeDetectorRef,
@@ -243,8 +241,6 @@ export class AppComponent implements OnInit, OnDestroy {
await Promise.all([
this.vaultBrowserStateService.setBrowserGroupingsComponentState(null),
this.vaultBrowserStateService.setBrowserVaultItemsComponentState(null),
this.browserSendStateService.setBrowserSendComponentState(null),
this.browserSendStateService.setBrowserSendTypeComponentState(null),
]);
}

View File

@@ -23,7 +23,6 @@ import { EnvironmentComponent } from "../auth/popup/environment.component";
import { ExtensionAnonLayoutWrapperComponent } from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component";
import { HintComponent } from "../auth/popup/hint.component";
import { HomeComponent } from "../auth/popup/home.component";
import { LockComponent } from "../auth/popup/lock.component";
import { LoginDecryptionOptionsComponentV1 } from "../auth/popup/login-decryption-options/login-decryption-options-v1.component";
import { LoginComponentV1 } from "../auth/popup/login-v1.component";
import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-request-v1.component";
@@ -57,12 +56,6 @@ import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.comp
import { PopupPageComponent } from "../platform/popup/layout/popup-page.component";
import { PopupTabNavigationComponent } from "../platform/popup/layout/popup-tab-navigation.component";
import { FilePopoutCalloutComponent } from "../tools/popup/components/file-popout-callout.component";
import { GeneratorComponent } from "../tools/popup/generator/generator.component";
import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component";
import { SendListComponent } from "../tools/popup/send/components/send-list.component";
import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.component";
import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.component";
import { SendTypeComponent } from "../tools/popup/send/send-type.component";
import { ActionButtonsComponent } from "../vault/popup/components/action-buttons.component";
import { CipherRowComponent } from "../vault/popup/components/cipher-row.component";
import { AddEditCustomFieldsComponent } from "../vault/popup/components/vault/add-edit-custom-fields.component";
@@ -156,21 +149,14 @@ import "../platform/popup/locales";
VaultFilterComponent,
HintComponent,
HomeComponent,
LockComponent,
LoginViaAuthRequestComponentV1,
LoginComponentV1,
LoginDecryptionOptionsComponentV1,
NotificationsSettingsV1Component,
AppearanceComponent,
GeneratorComponent,
PasswordGeneratorHistoryComponent,
PasswordHistoryComponent,
PremiumComponent,
RegisterComponent,
SendAddEditComponent,
SendGroupingsComponent,
SendListComponent,
SendTypeComponent,
SetPasswordComponent,
VaultSettingsComponent,
ShareComponent,

View File

@@ -152,7 +152,6 @@ import { ForegroundSyncService } from "../../platform/sync/foreground-sync.servi
import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging";
import { ExtensionLockComponentService } from "../../services/extension-lock-component.service";
import { ForegroundVaultTimeoutService } from "../../services/vault-timeout/foreground-vault-timeout.service";
import { BrowserSendStateService } from "../../tools/popup/services/browser-send-state.service";
import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service";
import { Fido2UserVerificationService } from "../../vault/services/fido2-user-verification.service";
import { VaultBrowserStateService } from "../../vault/services/vault-browser-state.service";
@@ -473,11 +472,6 @@ const safeProviders: SafeProvider[] = [
useClass: UserNotificationSettingsService,
deps: [StateProvider],
}),
safeProvider({
provide: BrowserSendStateService,
useClass: BrowserSendStateService,
deps: [StateProvider],
}),
safeProvider({
provide: MessageListener,
useFactory: (subject: Subject<Message<Record<string, unknown>>>, ngZone: NgZone) =>

View File

@@ -1,588 +0,0 @@
<app-header [hideAccountSwitcher]="comingFromAddEdit">
<div class="left">
<app-pop-out [show]="!comingFromAddEdit"></app-pop-out>
<button type="button" (click)="close()" *ngIf="comingFromAddEdit">
{{ "cancel" | i18n }}
</button>
</div>
<h1 class="center">
<span class="title">{{ "generator" | i18n }}</span>
</h1>
<div class="right">
<button type="button" (click)="select()" *ngIf="comingFromAddEdit">
{{ "select" | i18n }}
</button>
</div>
</app-header>
<main tabindex="-1">
<app-callout type="info" *ngIf="enforcedPasswordPolicyOptions?.inEffect() && type === 'password'">
{{ "passwordGeneratorPolicyInEffect" | i18n }}
</app-callout>
<div class="generated-block" *ngIf="type === 'password'">
<div
class="generated-wrapper"
[innerHTML]="password | colorPassword"
[appCopyText]="password"
></div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'copyPassword' | i18n }}"
(click)="copy()"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
<button
type="button"
appStopClick
appA11yTitle="{{ 'regeneratePassword' | i18n }}"
(click)="regenerate()"
>
<i class="bwi bwi-lg bwi-generate" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="generated-block" *ngIf="type === 'username'">
<div
class="generated-wrapper"
[innerHTML]="username | colorPassword"
[appCopyText]="username"
></div>
<div class="action-buttons" #form [appApiAction]="usernameGeneratingPromise">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'copyUsername' | i18n }}"
(click)="copy()"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
<button
type="button"
appStopClick
appA11yTitle="{{ 'regenerateUsername' | i18n }}"
(click)="$any(form).loading ? false : regenerate()"
[attr.aria-disabled]="$any(form).loading ? 'true' : null"
>
<i
class="bwi bwi-lg bwi-generate"
[ngClass]="$any(form).loading ? 'bwi-spin' : ''"
aria-hidden="true"
></i>
</button>
</div>
</div>
<div class="box" *ngIf="!comingFromAddEdit">
<div class="box-content">
<div class="box-content-row" role="radiogroup" aria-labelledby="typeHeading">
<label id="typeHeading" class="radio-header">{{
"whatWouldYouLikeToGenerate" | i18n
}}</label>
<div class="radio-group text-default" appBoxRow *ngFor="let o of typeOptions">
<input
type="radio"
[(ngModel)]="type"
name="Type"
id="type_{{ o.value }}"
[value]="o.value"
(change)="typeChanged()"
[checked]="type === o.value"
/>
<label for="type_{{ o.value }}">
{{ o.name }}
</label>
</div>
</div>
</div>
</div>
<ng-container *ngIf="type === 'password'">
<div class="box">
<h2 class="box-header">
{{ "options" | i18n }}
</h2>
<div class="box-content">
<div class="box-content-row" role="radiogroup" aria-labelledby="passwordTypeHeading">
<label id="passwordTypeHeading" class="radio-header">{{ "passwordType" | i18n }}</label>
<div class="radio-group text-default" appBoxRow *ngFor="let o of passTypeOptions">
<input
type="radio"
[(ngModel)]="passwordOptions.type"
name="PasswordType"
id="passwordtype_{{ o.value }}"
[value]="o.value"
(change)="savePasswordOptions()"
[checked]="passwordOptions.type === o.value"
/>
<label for="passwordtype_{{ o.value }}">
{{ o.name }}
</label>
</div>
</div>
</div>
</div>
<div class="box" *ngIf="passwordOptions.type === 'passphrase'">
<div class="box-content">
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="num-words">{{ "numWords" | i18n }}</label>
<input
id="num-words"
type="number"
min="3"
max="20"
(change)="savePasswordOptions()"
[(ngModel)]="passwordOptions.numWords"
/>
</div>
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="word-separator">{{ "wordSeparator" | i18n }}</label>
<input
id="word-separator"
type="text"
maxlength="1"
(input)="savePasswordOptions()"
[(ngModel)]="passwordOptions.wordSeparator"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="capitalize">{{ "capitalize" | i18n }}</label>
<input
id="capitalize"
type="checkbox"
(change)="savePasswordOptions()"
[(ngModel)]="passwordOptions.capitalize"
[disabled]="enforcedPasswordPolicyOptions?.capitalize"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="include-number">{{ "includeNumber" | i18n }}</label>
<input
id="include-number"
type="checkbox"
(change)="savePasswordOptions()"
[(ngModel)]="passwordOptions.includeNumber"
[disabled]="enforcedPasswordPolicyOptions?.includeNumber"
/>
</div>
</div>
</div>
<ng-container *ngIf="passwordOptions.type === 'password'">
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-slider" appBoxRow>
<label for="length">{{ "length" | i18n }}</label>
<input
id="length"
type="number"
[min]="passwordOptions.minLength"
max="128"
[(ngModel)]="passwordOptions.length"
(change)="savePasswordOptions()"
/>
<input
id="lengthRange"
type="range"
[min]="passwordOptions.minLength"
max="128"
step="1"
[(ngModel)]="passwordOptions.length"
(change)="sliderChanged()"
(input)="sliderInput()"
attr.aria-label="{{ 'length' | i18n }}"
tabindex="-1"
/>
</div>
<div class="box-content-row" appBoxRow>
<span>{{ "passwordMinLength" | i18n }}</span>
<span
class="sr-only"
attr.aria-label="{{ 'passwordMinLength' | i18n }}"
role="status"
aria-live="polite"
>
{{ passwordOptionsMinLengthForReader$ | async }}
</span>
<span class="txt-right">{{ passwordOptions.minLength }}</span>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="uppercase">A-Z</label>
<input
id="uppercase"
type="checkbox"
(change)="savePasswordOptions()"
attr.aria-label="{{ 'uppercase' | i18n }}"
[disabled]="enforcedPasswordPolicyOptions.useUppercase"
[(ngModel)]="passwordOptions.uppercase"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="lowercase">a-z</label>
<input
id="lowercase"
type="checkbox"
(change)="savePasswordOptions()"
attr.aria-label="{{ 'lowercase' | i18n }}"
[disabled]="enforcedPasswordPolicyOptions.useLowercase"
[(ngModel)]="passwordOptions.lowercase"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="numbers">0-9</label>
<input
id="numbers"
type="checkbox"
attr.aria-label="{{ 'numbers' | i18n }}"
[disabled]="enforcedPasswordPolicyOptions.useNumbers"
[ngModel]="passwordOptions.number"
(ngModelChange)="setPasswordOptionsNumber($event)"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="special">!&#64;#$%^&*</label>
<input
id="special"
type="checkbox"
attr.aria-label="{{ 'specialCharacters' | i18n }}"
[disabled]="enforcedPasswordPolicyOptions.useSpecial"
[ngModel]="passwordOptions.special"
(ngModelChange)="setPasswordOptionsSpecial($event)"
/>
</div>
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="min-number">{{ "minNumbers" | i18n }}</label>
<input
id="min-number"
type="number"
min="0"
max="9"
[(ngModel)]="passwordOptions.minNumber"
(input)="onPasswordOptionsMinNumberInput($event)"
/>
</div>
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="min-special">{{ "minSpecial" | i18n }}</label>
<input
id="min-special"
type="number"
min="0"
max="9"
[(ngModel)]="passwordOptions.minSpecial"
(input)="onPasswordOptionsMinSpecialInput($event)"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="ambiguous">{{ "avoidAmbChar" | i18n }}</label>
<input
id="ambiguous"
type="checkbox"
(change)="savePasswordOptions()"
[(ngModel)]="avoidAmbiguous"
/>
</div>
</div>
</div>
</ng-container>
<div class="box list">
<div class="box-content single-line">
<a class="box-content-row box-content-row-flex" routerLink="/generator-history">
<div class="row-main">{{ "passwordHistory" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</a>
</div>
</div>
</ng-container>
<ng-container *ngIf="type === 'username'">
<div class="box">
<h2 class="box-header">
{{ "options" | i18n }}
</h2>
<div class="box-content">
<div class="box-content-row" role="radiogroup" aria-labelledby="usernameTypeHeading">
<label id="usernameTypeHeading" class="radio-header">
{{ "usernameType" | i18n }}
<a
href="https://bitwarden.com/help/generator/#username-types"
target="_blank"
rel="noreferrer"
appA11yTitle="{{ 'learnMore' | i18n }}"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
</label>
<div
class="radio-group align-start text-default"
appBoxRow
*ngFor="let o of usernameTypeOptions"
>
<input
type="radio"
[(ngModel)]="usernameOptions.type"
name="UsernameType"
id="type_{{ o.value }}"
[value]="o.value"
(change)="saveUsernameOptions()"
[checked]="usernameOptions.type === o.value"
/>
<label for="type_{{ o.value }}">
{{ o.name }}
<div class="small text-muted" *ngIf="o.desc">
{{ o.desc }}
</div>
</label>
</div>
</div>
</div>
</div>
<div class="box" *ngIf="usernameOptions.type === 'forwarded'">
<div class="box-content">
<div class="box-content-row" role="listbox" aria-labelledby="forwardTypeHeading">
<label id="forwardTypeHeading">{{ "service" | i18n }}</label>
<select
id="ForwardTypeDropdown"
name="ForwardType"
[(ngModel)]="usernameOptions.forwardedService"
(change)="saveUsernameOptions()"
>
<option *ngFor="let o of forwardOptions" [ngValue]="o.value" role="option">
{{ o.name }}
</option>
</select>
</div>
<ng-container *ngIf="usernameOptions.forwardedService === 'simplelogin'">
<div class="box-content-row" appBoxRow>
<label for="simplelogin-apikey">{{ "apiKey" | i18n }}</label>
<input
id="simplelogin-apikey"
type="password"
name="SimpleLoginApiKey"
[(ngModel)]="usernameOptions.forwardedSimpleLoginApiKey"
(blur)="saveUsernameOptions()"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="simplelogin-baseUrl">{{ "baseUrl" | i18n }}</label>
<input
id="simplelogin-baseUrl"
type="text"
name="SimpleLoginDomain"
[(ngModel)]="usernameOptions.forwardedSimpleLoginBaseUrl"
(blur)="saveUsernameOptions()"
/>
</div>
</ng-container>
<ng-container *ngIf="usernameOptions.forwardedService === 'duckduckgo'">
<div class="box-content-row" appBoxRow>
<label for="duckduckgo-apikey">{{ "apiKey" | i18n }}</label>
<input
id="duckduckgo-apikey"
type="password"
name="DuckDuckGoApiKey"
[(ngModel)]="usernameOptions.forwardedDuckDuckGoToken"
(blur)="saveUsernameOptions()"
/>
</div>
</ng-container>
<ng-container *ngIf="usernameOptions.forwardedService === 'anonaddy'">
<div class="box-content-row" appBoxRow>
<label for="anonaddy-accessToken">{{ "apiAccessToken" | i18n }}</label>
<input
id="anonaddy-accessToken"
type="password"
name="AnonAddyAccessToken"
[(ngModel)]="usernameOptions.forwardedAnonAddyApiToken"
(blur)="saveUsernameOptions()"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="anonaddy-domain">{{ "aliasDomain" | i18n }}</label>
<input
id="anonaddy-domain"
type="text"
name="AnonAddyDomain"
[(ngModel)]="usernameOptions.forwardedAnonAddyDomain"
(blur)="saveUsernameOptions()"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="anonaddy-baseUrl">{{ "baseUrl" | i18n }}</label>
<input
id="anonaddy-baseUrl"
type="text"
name="AnonAddyDomain"
[(ngModel)]="usernameOptions.forwardedAnonAddyBaseUrl"
(blur)="saveUsernameOptions()"
/>
</div>
</ng-container>
<ng-container *ngIf="usernameOptions.forwardedService === 'firefoxrelay'">
<div class="box-content-row" appBoxRow>
<label for="firefox-apikey">{{ "apiAccessToken" | i18n }}</label>
<input
id="firefox-apikey"
type="password"
name="FirefoxApiKey"
[(ngModel)]="usernameOptions.forwardedFirefoxApiToken"
(blur)="saveUsernameOptions()"
/>
</div>
</ng-container>
<ng-container *ngIf="usernameOptions.forwardedService === 'fastmail'">
<div class="box-content-row" appBoxRow>
<label for="fastmail-apiToken">{{ "apiAccessToken" | i18n }}</label>
<input
id="fastmail-apiToken"
type="password"
name="FastmailApiToken"
[(ngModel)]="usernameOptions.forwardedFastmailApiToken"
(blur)="saveUsernameOptions()"
/>
</div>
</ng-container>
<ng-container *ngIf="usernameOptions.forwardedService === 'forwardemail'">
<div class="box-content-row" appBoxRow>
<label for="forwardemail-accessToken">{{ "apiAccessToken" | i18n }}</label>
<input
id="forwardemail-accessToken"
type="password"
name="ForwardEmailAccessToken"
[(ngModel)]="usernameOptions.forwardedForwardEmailApiToken"
(blur)="saveUsernameOptions()"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="forwardemail-domain">{{ "aliasDomain" | i18n }}</label>
<input
id="forwardemail-domain"
type="text"
name="ForwardEmailDomain"
[(ngModel)]="usernameOptions.forwardedForwardEmailDomain"
(blur)="saveUsernameOptions()"
/>
</div>
</ng-container>
</div>
</div>
<div class="box" *ngIf="usernameOptions.type === 'subaddress'">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="subaddress-email">{{ "emailAddress" | i18n }}</label>
<input
id="subaddress-email"
type="text"
name="SubaddressEmail"
[(ngModel)]="usernameOptions.subaddressEmail"
(blur)="saveUsernameOptions()"
/>
</div>
<div
class="box-content-row"
role="radiogroup"
aria-labelledby="subaddressTypeHeading"
*ngIf="subaddressOptions.length > 1"
>
<label id="subaddressTypeHeading" class="radio-header">{{ "type" | i18n }}</label>
<div class="radio-group text-default" appBoxRow *ngFor="let o of subaddressOptions">
<input
type="radio"
[(ngModel)]="usernameOptions.subaddressType"
name="SubaddressType"
id="subaddresstype_{{ o.value }}"
[value]="o.value"
(change)="saveUsernameOptions()"
[checked]="usernameOptions.subaddressType === o.value"
/>
<label for="subaddresstype_{{ o.value }}">
{{ o.name }}
</label>
</div>
</div>
<div class="box-content-row" appBoxRow *ngIf="usernameWebsite">
<label for="subaddress-website">{{ "website" | i18n }}</label>
<input
id="subaddress-website"
type="text"
name="SubaddressWebsite"
[value]="usernameOptions.website"
disabled
readonly
/>
</div>
</div>
</div>
<div class="box" *ngIf="usernameOptions.type === 'catchall'">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="catchall-domain">{{ "domainName" | i18n }}</label>
<input
id="catchall-domain"
type="text"
name="CatchallDomain"
[(ngModel)]="usernameOptions.catchallDomain"
(blur)="saveUsernameOptions()"
/>
</div>
<div
class="box-content-row"
role="radiogroup"
aria-labelledby="catchallTypeHeading"
*ngIf="catchallOptions.length > 1"
>
<label id="catchallTypeHeading" class="radio-header">{{ "type" | i18n }}</label>
<div class="radio-group text-default" appBoxRow *ngFor="let o of catchallOptions">
<input
type="radio"
[(ngModel)]="usernameOptions.catchallType"
name="CatchallType"
id="catchalltype_{{ o.value }}"
[value]="o.value"
(change)="saveUsernameOptions()"
[checked]="usernameOptions.catchallType === o.value"
/>
<label for="catchalltype_{{ o.value }}">
{{ o.name }}
</label>
</div>
</div>
<div class="box-content-row" appBoxRow *ngIf="usernameWebsite">
<label for="catchall-website">{{ "website" | i18n }}</label>
<input
id="catchall-website"
type="text"
name="CatchallWebsite"
[value]="usernameOptions.website"
disabled
readonly
/>
</div>
</div>
</div>
<div class="box" *ngIf="usernameOptions.type === 'word'">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="capitalize">{{ "capitalize" | i18n }}</label>
<input
id="capitalize"
type="checkbox"
(change)="saveUsernameOptions()"
[(ngModel)]="usernameOptions.wordCapitalize"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="include-number">{{ "includeNumber" | i18n }}</label>
<input
id="include-number"
type="checkbox"
(change)="saveUsernameOptions()"
[(ngModel)]="usernameOptions.wordIncludeNumber"
/>
</div>
</div>
</div>
</ng-container>
</main>

View File

@@ -1,88 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Location } from "@angular/common";
import { Component, NgZone, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { GeneratorComponent as BaseGeneratorComponent } from "@bitwarden/angular/tools/generator/components/generator.component";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher-info";
import { ToastService } from "@bitwarden/components";
import {
PasswordGenerationServiceAbstraction,
UsernameGenerationServiceAbstraction,
} from "@bitwarden/generator-legacy";
@Component({
selector: "app-generator",
templateUrl: "generator.component.html",
})
export class GeneratorComponent extends BaseGeneratorComponent implements OnInit {
private addEditCipherInfo: AddEditCipherInfo;
private cipherState: CipherView;
private cipherService: CipherService;
constructor(
passwordGenerationService: PasswordGenerationServiceAbstraction,
usernameGenerationService: UsernameGenerationServiceAbstraction,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
accountService: AccountService,
cipherService: CipherService,
route: ActivatedRoute,
logService: LogService,
ngZone: NgZone,
private location: Location,
toastService: ToastService,
) {
super(
passwordGenerationService,
usernameGenerationService,
platformUtilsService,
accountService,
i18nService,
logService,
route,
ngZone,
window,
toastService,
);
this.cipherService = cipherService;
}
async ngOnInit() {
this.addEditCipherInfo = await firstValueFrom(this.cipherService.addEditCipherInfo$);
if (this.addEditCipherInfo != null) {
this.cipherState = this.addEditCipherInfo.cipher;
}
this.comingFromAddEdit = this.cipherState != null;
if (this.cipherState?.login?.hasUris) {
this.usernameWebsite = this.cipherState.login.uris[0].hostname;
}
await super.ngOnInit();
}
select() {
super.select();
if (this.type === "password") {
this.cipherState.login.password = this.password;
} else if (this.type === "username") {
this.cipherState.login.username = this.username;
}
this.addEditCipherInfo.cipher = this.cipherState;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.cipherService.setAddEditCipherInfo(this.addEditCipherInfo);
this.close();
}
close() {
this.location.back();
}
}

View File

@@ -1,48 +0,0 @@
<header>
<div class="left">
<button type="button" type="button" (click)="close()">
<span class="header-icon" aria-hidden="true"><i class="bwi bwi-angle-left"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="center">
<span class="title">{{ "passwordHistory" | i18n }}</span>
</h1>
<div class="right">
<button type="button" type="button" (click)="clear()">
{{ "clear" | i18n }}
</button>
</div>
</header>
<main tabindex="-1">
<div class="box list full-list" *ngIf="history && history.length">
<div class="box-content">
<div class="box-content-row box-content-row-flex" *ngFor="let h of history">
<div class="row-main">
<div class="row-main-content">
<div
class="monospaced password-wrapper"
[appCopyText]="h.password"
[innerHTML]="h.password | colorPassword"
></div>
<span class="detail">{{ h.date | date: "medium" }}</span>
</div>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'copyPassword' | i18n }}"
(click)="copy(h.password)"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</div>
<div class="no-items" *ngIf="!history || !history.length">
<p>{{ "noPasswordsInList" | i18n }}</p>
</div>
</main>

View File

@@ -1,28 +0,0 @@
import { Location } from "@angular/common";
import { Component } from "@angular/core";
import { PasswordGeneratorHistoryComponent as BasePasswordGeneratorHistoryComponent } from "@bitwarden/angular/tools/generator/components/password-generator-history.component";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
@Component({
selector: "app-password-generator-history",
templateUrl: "password-generator-history.component.html",
})
export class PasswordGeneratorHistoryComponent extends BasePasswordGeneratorHistoryComponent {
constructor(
passwordGenerationService: PasswordGenerationServiceAbstraction,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
private location: Location,
toastService: ToastService,
) {
super(passwordGenerationService, platformUtilsService, i18nService, window, toastService);
}
close() {
this.location.back();
}
}

View File

@@ -1,98 +0,0 @@
<div
role="group"
*ngFor="let s of sends"
appA11yTitle="{{ s.name }}"
class="box-content-row box-content-row-flex"
>
<button
type="button"
class="row-main"
(click)="selectSend(s)"
appStopClick
title="{{ title }} - {{ s.name }}"
>
<div class="app-vault-icon">
<div class="icon" aria-hidden="true">
<i class="bwi bwi-fw bwi-lg bwi-file-text" *ngIf="s.type === sendType.Text"></i>
<i class="bwi bwi-fw bwi-lg bwi-file" *ngIf="s.type === sendType.File"></i>
</div>
</div>
<div class="row-main-content">
<span class="text">
{{ s.name }}
<ng-container *ngIf="s.disabled">
<i
class="bwi bwi-exclamation-triangle text-muted"
title="{{ 'disabled' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "disabled" | i18n }}</span>
</ng-container>
<ng-container *ngIf="s.password">
<i
class="bwi bwi-key text-muted"
title="{{ 'passwordProtected' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "passwordProtected" | i18n }}</span>
</ng-container>
<ng-container *ngIf="s.maxAccessCountReached">
<i
class="bwi bwi-ban text-muted"
title="{{ 'maxAccessCountReached' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "maxAccessCountReached" | i18n }}</span>
</ng-container>
<ng-container *ngIf="s.expired">
<i class="bwi bwi-clock text-muted" title="{{ 'expired' | i18n }}" aria-hidden="true"></i>
<span class="sr-only">{{ "expired" | i18n }}</span>
</ng-container>
<ng-container *ngIf="s.pendingDelete">
<i
class="bwi bwi-trash text-muted"
title="{{ 'pendingDeletion' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "pendingDeletion" | i18n }}</span>
</ng-container>
</span>
<span class="detail">{{ s.deletionDate | date: "medium" }}</span>
</div>
</button>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appStopProp
appA11yTitle="{{ 'copySendLink' | i18n }}"
(click)="copySendLink(s)"
>
<i class="bwi bwi-lg bwi-files" aria-hidden="true"></i>
</button>
<button
type="button"
class="row-btn"
[ngClass]="{ disabled: disabledByPolicy }"
[attr.disabled]="disabledByPolicy ? '' : null"
appStopClick
appStopProp
appA11yTitle="{{ 'removePassword' | i18n }}"
(click)="removePassword(s)"
*ngIf="s.password"
>
<i class="bwi bwi-lg bwi-undo" aria-hidden="true"></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appStopProp
appA11yTitle="{{ 'delete' | i18n }}"
(click)="delete(s)"
>
<i class="bwi bwi-lg bwi-trash" aria-hidden="true"></i>
</button>
</div>
</div>

View File

@@ -1,38 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
@Component({
selector: "app-send-list",
templateUrl: "send-list.component.html",
})
export class SendListComponent {
@Input() sends: SendView[];
@Input() title: string;
@Input() disabledByPolicy = false;
@Output() onSelected = new EventEmitter<SendView>();
@Output() onCopySendLink = new EventEmitter<SendView>();
@Output() onRemovePassword = new EventEmitter<SendView>();
@Output() onDeleteSend = new EventEmitter<SendView>();
sendType = SendType;
selectSend(s: SendView) {
this.onSelected.emit(s);
}
copySendLink(s: SendView) {
this.onCopySendLink.emit(s);
}
removePassword(s: SendView) {
this.onRemovePassword.emit(s);
}
delete(s: SendView) {
this.onDeleteSend.emit(s);
}
}

View File

@@ -1,411 +0,0 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" [formGroup]="formGroup">
<header>
<div class="left">
<button type="button" (click)="cancel()">{{ "cancel" | i18n }}</button>
</div>
<h1 class="center">
<span class="title">{{ title }}</span>
</h1>
<div class="right">
<button type="submit">
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
</div>
</header>
<main tabindex="-1" *ngIf="send">
<!-- Policy Banner -->
<app-callout type="warning" title="{{ 'sendDisabled' | i18n }}" *ngIf="disableSend">
{{ "sendDisabledWarning" | i18n }}
</app-callout>
<app-callout type="info" *ngIf="disableHideEmail && !disableSend">
{{ "sendOptionsPolicyInEffect" | i18n }}
</app-callout>
<!-- File Warning -->
<tools-file-popout-callout *ngIf="type === sendType.File && !disableSend" />
<!-- Name -->
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="name">{{ "name" | i18n }}</label>
<input
id="name"
type="text"
formControlName="name"
aria-describedby="nameHelp"
[readonly]="disableSend"
/>
</div>
</div>
<div id="nameHelp" class="box-footer">
{{ "sendNameDesc" | i18n }}
</div>
</div>
<!-- Type Options -->
<div class="box" *ngIf="!editMode">
<div class="box-content no-hover">
<div class="box-content-row">
<label for="sendTypeOptions">{{ "sendTypeHeader" | i18n }}</label>
<div
class="radio-group text-default"
appBoxRow
name="SendTypeOptions"
*ngFor="let o of typeOptions"
>
<input
type="radio"
formControlName="type"
id="type_{{ o.value }}"
[value]="o.value"
[readonly]="disableSend"
/>
<label for="type_{{ o.value }}">
{{ o.name }}
</label>
</div>
</div>
</div>
</div>
<!-- File -->
<div class="box" *ngIf="type === sendType.File && (editMode || showFileSelector)">
<div class="box-content no-hover">
<div class="box-content-row" *ngIf="editMode">
<label for="file">{{ "file" | i18n }}</label>
<div class="row-main">{{ send.file.fileName }} ({{ send.file.sizeName }})</div>
</div>
<div class="box-content-row" *ngIf="showFileSelector">
<label for="file">{{ "file" | i18n }}</label>
<input
type="file"
id="file"
formControlName="file"
aria-describedby="fileHelp"
[readonly]="disableSend"
/>
</div>
</div>
<div id="fileHelp" class="box-footer" *ngIf="showFileSelector">
{{ "sendFileDesc" | i18n }} {{ "maxFileSize" | i18n }}
</div>
</div>
<!-- Text -->
<div class="box" *ngIf="type === sendType.Text">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="text">{{ "sendTypeText" | i18n }}</label>
<textarea
id="text"
formControlName="text"
aria-describedby="textHelp"
rows="6"
></textarea>
</div>
</div>
<div id="textHelp" class="box-footer">
{{ "sendTextDesc" | i18n }}
</div>
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="hideText">{{ "sendHideText" | i18n }}</label>
<input id="hideText" type="checkbox" name="HideText" formControlName="textHidden" />
</div>
</div>
</div>
<!-- Share -->
<div class="box">
<h2 class="box-header">
{{ "share" | i18n }}
</h2>
<div class="box-content">
<!-- Copy Link on Save -->
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="copyOnSave">{{ "sendShareDesc" | i18n }}</label>
<input id="copyOnSave" type="checkbox" name="CopyOnSave" formControlName="copyLink" />
</div>
</div>
</div>
<!-- Options -->
<div class="box">
<h2>
<button
type="button"
class="box-header-expandable"
(click)="showOptions = !showOptions"
[attr.aria-expanded]="showOptions"
>
<i *ngIf="!showOptions" class="bwi bwi-angle-right bwi-sm icon" aria-hidden="true"></i>
<i *ngIf="showOptions" class="bwi bwi-angle-down bwi-sm icon" aria-hidden="true"></i>
{{ "options" | i18n }}
</button>
</h2>
</div>
<div [hidden]="!showOptions">
<!-- Deletion Date -->
<div class="box">
<div class="box-content">
<ng-container *ngIf="!editMode">
<div class="box-content-row" appBoxRow>
<label for="deletionDate">{{ "deletionDate" | i18n }}</label>
<select
id="deletionDate"
name="DeletionDateSelect"
aria-describedby="deletionDateHelp"
formControlName="selectedDeletionDatePreset"
required
>
<option *ngFor="let o of deletionDatePresets" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
</div>
<div
class="box-content-row"
appBoxRow
*ngIf="formGroup.controls['selectedDeletionDatePreset'].value === 0"
>
<input
id="deletionDateCustom"
type="datetime-local"
name="DeletionDate"
aria-describedby="deletionDateHelp"
formControlName="defaultDeletionDateTime"
required
placeholder="MM/DD/YYYY HH:MM AM/PM"
/>
</div>
</ng-container>
<div class="box-content-row" appBoxRow *ngIf="editMode">
<label for="deletionDate">{{ "deletionDate" | i18n }}</label>
<input
id="deletionDateCustom"
type="datetime-local"
name="DeletionDate"
aria-describedby="deletionDateHelp"
formControlName="defaultDeletionDateTime"
required
placeholder="MM/DD/YYYY HH:MM AM/PM"
/>
</div>
</div>
<div id="deletionDateHelp" class="box-footer">
{{ "deletionDateDesc" | i18n }}
<ng-container
*ngIf="
!inPopout &&
isFirefox &&
(editMode ||
(formGroup.controls['selectedDeletionDatePreset'].value === 0 && !editMode))
"
>
<br />{{ "sendFirefoxCustomDatePopoutMessage1" | i18n }}
<a (click)="popOutWindow()">{{ "sendFirefoxCustomDatePopoutMessage2" | i18n }}</a>
{{ "sendFirefoxCustomDatePopoutMessage3" | i18n }}
</ng-container>
</div>
</div>
<!-- Expiration Date -->
<div class="box">
<div class="box-content">
<ng-container *ngIf="!editMode">
<div class="box-content-row" *ngIf="!editMode" appBoxRow>
<label for="editExpirationDate">{{ "expirationDate" | i18n }}</label>
<select
id="expirationDate"
name="ExpirationDateSelect"
aria-describedby="expirationDateHelp"
formControlName="selectedExpirationDatePreset"
required
>
<option *ngFor="let o of expirationDatePresets" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
</div>
<div
class="box-content-row"
*ngIf="formGroup.controls['selectedExpirationDatePreset'].value === 0"
appBoxRow
>
<input
id="expirationDateCustom"
type="datetime-local"
name="ExpirationDate"
aria-describedby="expirationDateHelp"
formControlName="defaultExpirationDateTime"
required
placeholder="MM/DD/YYYY HH:MM AM/PM"
[readOnly]="disableSend"
/>
</div>
</ng-container>
<div class="box-content-row" *ngIf="editMode" appBoxRow>
<div class="flex-label">
<label>{{ "expirationDate" | i18n }}</label>
<button type="button" *ngIf="!disableSend" appStopClick (click)="clearExpiration()">
{{ "clear" | i18n }}
</button>
</div>
<input
id="expirationDateCustom"
type="datetime-local"
name="ExpirationDate"
aria-describedby="expirationDateHelp"
formControlName="defaultExpirationDateTime"
required
placeholder="MM/DD/YYYY HH:MM AM/PM"
[readOnly]="disableSend"
/>
</div>
</div>
<div id="expirationDateHelp" class="box-footer">
{{ "expirationDateDesc" | i18n }}
<ng-container
*ngIf="
!inPopout &&
isFirefox &&
(editMode ||
(formGroup.controls['selectedExpirationDatePreset'].value === 0 && !editMode))
"
>
<br />{{ "sendFirefoxCustomDatePopoutMessage1" | i18n }}
<a (click)="popOutWindow()">{{ "sendFirefoxCustomDatePopoutMessage2" | i18n }}</a>
{{ "sendFirefoxCustomDatePopoutMessage3" | i18n }}
</ng-container>
</div>
</div>
<!-- Maximum Access Count -->
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="maximumAccessCount">{{ "maximumAccessCount" | i18n }}</label>
<input
id="maximumAccessCount"
min="1"
type="number"
name="MaximumAccessCount"
aria-describedby="maximumAccessCountHelp"
formControlName="maxAccessCount"
/>
</div>
</div>
<div id="maximumAccessCountHelp" class="box-footer">
{{ "maximumAccessCountDesc" | i18n }}
</div>
</div>
<!-- Current Access Count -->
<div class="box" *ngIf="editMode">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="currentAccessCount">{{ "currentAccessCount" | i18n }}</label>
<input
id="currentAccessCount"
type="text"
name="CurrentAccessCount"
formControlName="accessCount"
/>
</div>
</div>
</div>
<!-- Password -->
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="password" *ngIf="hasPassword">{{ "newPassword" | i18n }}</label>
<label for="password" *ngIf="!hasPassword">{{ "password" | i18n }}</label>
<input
id="password"
type="{{ showPassword ? 'text' : 'password' }}"
name="Password"
aria-describedby="passwordHelp"
class="monospaced"
formControlName="password"
appInputVerbatim
/>
</div>
<div class="action-buttons" *ngIf="!disableSend">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePasswordVisible()"
[attr.aria-pressed]="showPassword"
>
<i
class="bwi bwi-lg"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
aria-hidden="true"
></i>
</button>
</div>
</div>
</div>
<div id="passwordHelp" class="box-footer">
{{ "sendPasswordDesc" | i18n }}
</div>
</div>
<!-- Notes -->
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="notes">{{ "notes" | i18n }}</label>
<textarea
id="notes"
name="Notes"
aria-describedby="notesHelp"
rows="6"
formControlName="notes"
></textarea>
</div>
</div>
<div id="notesHelp" class="box-footer">
{{ "sendNotesDesc" | i18n }}
</div>
</div>
<!-- Hide Email -->
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="hideEmail">{{ "hideEmail" | i18n }}</label>
<input id="hideEmail" type="checkbox" name="HideEmail" formControlName="hideEmail" />
</div>
</div>
</div>
<!-- Disable Send -->
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="disableSend">{{ "sendDisableDesc" | i18n }}</label>
<input id="disableSend" type="checkbox" name="DisableSend" formControlName="disabled" />
</div>
</div>
</div>
</div>
<!-- Delete -->
<div class="box list" *ngIf="editMode">
<div class="box-content single-line">
<button
type="button"
class="box-content-row"
appStopClick
(click)="delete()"
[appApiAction]="deletePromise"
#deleteBtn
>
<div class="row-main text-danger">
<div class="icon text-danger" aria-hidden="true">
<i class="bwi bwi-trash bwi-lg bwi-fw" [hidden]="$any(deleteBtn).loading"></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!$any(deleteBtn).loading"
></i>
</div>
<span>{{ "deleteSend" | i18n }}</span>
</div>
</button>
</div>
</div>
</main>
</form>

View File

@@ -1,140 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DatePipe, Location } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { DialogService, ToastService } from "@bitwarden/components";
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
import { FilePopoutUtilsService } from "../services/file-popout-utils.service";
@Component({
selector: "app-send-add-edit",
templateUrl: "send-add-edit.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class SendAddEditComponent extends BaseAddEditComponent implements OnInit {
// Options header
showOptions = false;
// File visibility
isFirefox = false;
inPopout = false;
showFileSelector = false;
constructor(
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
stateService: StateService,
messagingService: MessagingService,
policyService: PolicyService,
environmentService: EnvironmentService,
datePipe: DatePipe,
sendService: SendService,
private route: ActivatedRoute,
private router: Router,
private location: Location,
logService: LogService,
sendApiService: SendApiService,
dialogService: DialogService,
formBuilder: FormBuilder,
private filePopoutUtilsService: FilePopoutUtilsService,
billingAccountProfileStateService: BillingAccountProfileStateService,
accountService: AccountService,
toastService: ToastService,
) {
super(
i18nService,
platformUtilsService,
environmentService,
datePipe,
sendService,
messagingService,
policyService,
logService,
stateService,
sendApiService,
dialogService,
formBuilder,
billingAccountProfileStateService,
accountService,
toastService,
);
}
popOutWindow() {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserPopupUtils.openCurrentPagePopout(window);
}
async ngOnInit() {
// File visibility
this.showFileSelector =
!this.editMode && !this.filePopoutUtilsService.showFilePopoutMessage(window);
this.inPopout = BrowserPopupUtils.inPopout(window);
this.isFirefox = this.platformUtilsService.isFirefox();
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (params) => {
if (params.sendId) {
this.sendId = params.sendId;
}
if (params.type) {
const type = parseInt(params.type, null);
this.type = type;
}
await super.ngOnInit();
});
window.setTimeout(() => {
if (!this.editMode) {
document.getElementById("name").focus();
}
}, 200);
}
async submit(): Promise<boolean> {
if (await super.submit()) {
this.cancel();
return true;
}
return false;
}
async delete(): Promise<boolean> {
if (await super.delete()) {
this.cancel();
return true;
}
return false;
}
cancel() {
// If true, the window was pop'd out on the add-send page. location.back will not work
const isPopup = (window as any)?.previousPopupUrl?.startsWith("/add-send") ?? false;
if (!isPopup) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["tabs/send"]);
} else {
this.location.back();
}
}
}

View File

@@ -1,119 +0,0 @@
<app-header>
<div class="left" *ngIf="showLeftHeader">
<app-pop-out></app-pop-out>
</div>
<h1 class="sr-only">{{ "send" | i18n }}</h1>
<div class="search center">
<input
type="search"
placeholder="{{ 'searchSends' | i18n }}"
id="search"
[(ngModel)]="searchText"
(input)="search(200)"
autocomplete="off"
appAutofocus
/>
<i class="bwi bwi-search"></i>
</div>
<div class="right">
<button
type="button"
(click)="addSend()"
appA11yTitle="{{ 'addSend' | i18n }}"
[disabled]="disableSend"
>
<i class="bwi bwi-plus bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
</div>
</app-header>
<main tabindex="-1" [ngClass]="{ flex: disableSend, 'tab-page': disableSend }">
<app-callout type="warning" title="{{ 'sendDisabled' | i18n }}" *ngIf="disableSend">
{{ "sendDisabledWarning" | i18n }}
</app-callout>
<div class="no-items" *ngIf="(!sends || !sends.length) && !showSearching() && !disableSend">
<i class="bwi bwi-spinner bwi-spin bwi-3x" *ngIf="!loaded"></i>
<ng-container *ngIf="loaded">
<img class="no-items-image" aria-hidden="true" />
<p>{{ "noItemsInList" | i18n }}</p>
<button
type="button"
(click)="addSend()"
class="btn block primary link"
[disabled]="disableSend"
>
{{ "addSend" | i18n }}
</button>
</ng-container>
</div>
<ng-container *ngIf="sends && sends.length && !showSearching()">
<div class="box list">
<h2 class="box-header">
{{ "types" | i18n }}
</h2>
<div class="box-content single-line">
<button
type="button"
class="box-content-row"
appStopClick
(click)="selectType(sendType.Text)"
>
<div class="row-main">
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-file-text"></i></div>
<span class="text">{{ "sendTypeText" | i18n }}</span>
</div>
<span class="row-sub-label">{{ getSendCount(sends, sendType.Text) }}</span>
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
</button>
<button
type="button"
class="box-content-row"
appStopClick
(click)="selectType(sendType.File)"
>
<div class="row-main">
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-file"></i></div>
<span class="text">{{ "sendTypeFile" | i18n }}</span>
</div>
<span class="row-sub-label">{{ getSendCount(sends, sendType.File) }}</span>
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
</button>
</div>
</div>
<div class="box list">
<h2 class="box-header">
{{ "allSends" | i18n }}
<div class="flex-right">{{ sends.length }}</div>
</h2>
<div class="box-content">
<app-send-list
[sends]="sends"
title="{{ 'editItem' | i18n }}"
[disabledByPolicy]="disableSend"
(onSelected)="selectSend($event)"
(onCopySendLink)="copy($event)"
(onRemovePassword)="removePassword($event)"
(onDeleteSend)="delete($event)"
></app-send-list>
</div>
</div>
</ng-container>
<ng-container *ngIf="showSearching()">
<div class="no-items" *ngIf="!filteredSends || !filteredSends.length">
<p>{{ "noItemsInList" | i18n }}</p>
</div>
<div class="box list full-list" *ngIf="filteredSends && filteredSends.length > 0">
<div class="box-content">
<app-send-list
[sends]="filteredSends"
title="{{ 'editItem' | i18n }}"
[disabledByPolicy]="disableSend"
(onSelected)="selectSend($event)"
(onCopySendLink)="copy($event)"
(onRemovePassword)="removePassword($event)"
(onDeleteSend)="delete($event)"
>
</app-send-list>
</div>
</div>
</ng-container>
</main>

View File

@@ -1,203 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { SendComponent as BaseSendComponent } from "@bitwarden/angular/tools/send/send.component";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService, ToastService } from "@bitwarden/components";
import { BrowserSendComponentState } from "../../../models/browserSendComponentState";
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
import { BrowserSendStateService } from "../services/browser-send-state.service";
const ComponentId = "SendComponent";
@Component({
selector: "app-send-groupings",
templateUrl: "send-groupings.component.html",
})
export class SendGroupingsComponent extends BaseSendComponent implements OnInit, OnDestroy {
// Header
showLeftHeader = true;
// State Handling
state: BrowserSendComponentState;
private loadedTimeout: number;
constructor(
sendService: SendService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService,
ngZone: NgZone,
policyService: PolicyService,
searchService: SearchService,
private stateService: BrowserSendStateService,
private router: Router,
private syncService: SyncService,
private changeDetectorRef: ChangeDetectorRef,
private broadcasterService: BroadcasterService,
logService: LogService,
sendApiService: SendApiService,
dialogService: DialogService,
toastService: ToastService,
) {
super(
sendService,
i18nService,
platformUtilsService,
environmentService,
ngZone,
searchService,
policyService,
logService,
sendApiService,
dialogService,
toastService,
);
this.onSuccessfulLoad = async () => {
this.selectAll();
};
}
async ngOnInit() {
// Determine Header details
this.showLeftHeader = !(
BrowserPopupUtils.inSidebar(window) && this.platformUtilsService.isFirefox()
);
// Clear state of Send Type Component
await this.stateService.setBrowserSendTypeComponentState(null);
// Let super class finish
await super.ngOnInit();
// Handle State Restore if necessary
const restoredScopeState = await this.restoreState();
if (this.state?.searchText != null) {
this.searchText = this.state.searchText;
}
if (!this.syncService.syncInProgress) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.load();
} else {
this.loadedTimeout = window.setTimeout(() => {
if (!this.loaded) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.load();
}
}, 5000);
}
if (!this.syncService.syncInProgress || restoredScopeState) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserPopupUtils.setContentScrollY(window, this.state?.scrollY);
}
// Load all sends if sync completed in background
this.broadcasterService.subscribe(ComponentId, (message: any) => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.ngZone.run(async () => {
switch (message.command) {
case "syncCompleted":
window.setTimeout(() => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.load();
}, 500);
break;
default:
break;
}
this.changeDetectorRef.detectChanges();
});
});
}
ngOnDestroy() {
// Remove timeout
if (this.loadedTimeout != null) {
window.clearTimeout(this.loadedTimeout);
}
// Save state
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.saveState();
// Unsubscribe
this.broadcasterService.unsubscribe(ComponentId);
}
async selectType(type: SendType) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/send-type"], { queryParams: { type: type } });
}
async selectSend(s: SendView) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/edit-send"], { queryParams: { sendId: s.id } });
}
async addSend() {
if (this.disableSend) {
return;
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/add-send"]);
}
async removePassword(s: SendView): Promise<boolean> {
if (this.disableSend) {
return;
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
super.removePassword(s);
}
showSearching() {
return this.hasSearched || (!this.searchPending && this.isSearchable);
}
getSendCount(sends: SendView[], type: SendType): number {
return sends.filter((s) => s.type === type).length;
}
private async saveState() {
this.state = Object.assign(new BrowserSendComponentState(), {
scrollY: BrowserPopupUtils.getContentScrollY(window),
searchText: this.searchText,
sends: this.sends,
});
await this.stateService.setBrowserSendComponentState(this.state);
}
private async restoreState(): Promise<boolean> {
this.state = await this.stateService.getBrowserSendComponentState();
if (this.state == null) {
return false;
}
if (this.state.sends != null) {
this.sends = this.state.sends;
}
return true;
}
}

View File

@@ -1,68 +0,0 @@
<header>
<div class="left">
<button type="button" (click)="back()">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="sr-only">{{ "send" | i18n }}</h1>
<div class="search">
<input
type="search"
placeholder="{{ 'searchType' | i18n }}"
id="search"
[(ngModel)]="searchText"
(input)="search(200)"
autocomplete="off"
appAutofocus
/>
<i class="bwi bwi-search"></i>
</div>
<div class="right">
<button
type="button"
(click)="addSend()"
appA11yTitle="{{ 'addSend' | i18n }}"
[disabled]="disableSend"
>
<i class="bwi bwi-plus bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
</div>
</header>
<main tabindex="-1" [ngClass]="{ flex: disableSend }">
<app-callout type="warning" title="{{ 'sendDisabled' | i18n }}" *ngIf="disableSend">
{{ "sendDisabledWarning" | i18n }}
</app-callout>
<div class="no-items" *ngIf="!filteredSends.length">
<i class="bwi bwi-spinner bwi-spin bwi-3x" *ngIf="!loaded" aria-hidden="true"></i>
<ng-container *ngIf="loaded">
<p>{{ "noItemsInList" | i18n }}</p>
<button
type="button"
(click)="addSend()"
class="btn block primary link"
[disabled]="disableSend"
>
{{ "addSend" | i18n }}
</button>
</ng-container>
</div>
<div class="box list only-list" *ngIf="filteredSends.length">
<h2 class="box-header">
{{ groupingTitle }}
<span class="flex-right">{{ filteredSends.length }}</span>
</h2>
<div class="box-content">
<app-send-list
[sends]="filteredSends"
title="{{ 'editItem' | i18n }}"
[disabledByPolicy]="disableSend"
(onSelected)="selectSend($event)"
(onCopySendLink)="copy($event)"
(onRemovePassword)="removePassword($event)"
(onDeleteSend)="delete($event)"
>
</app-send-list>
</div>
</div>
</main>

View File

@@ -1,190 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Location } from "@angular/common";
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
import { SendComponent as BaseSendComponent } from "@bitwarden/angular/tools/send/send.component";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { DialogService, ToastService } from "@bitwarden/components";
import { BrowserComponentState } from "../../../models/browserComponentState";
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
import { BrowserSendStateService } from "../services/browser-send-state.service";
const ComponentId = "SendTypeComponent";
@Component({
selector: "app-send-type",
templateUrl: "send-type.component.html",
})
export class SendTypeComponent extends BaseSendComponent implements OnInit, OnDestroy {
groupingTitle: string;
// State Handling
state: BrowserComponentState;
private refreshTimeout: number;
private applySavedState = true;
constructor(
sendService: SendService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService,
ngZone: NgZone,
policyService: PolicyService,
searchService: SearchService,
private stateService: BrowserSendStateService,
private route: ActivatedRoute,
private location: Location,
private changeDetectorRef: ChangeDetectorRef,
private broadcasterService: BroadcasterService,
private router: Router,
logService: LogService,
sendApiService: SendApiService,
dialogService: DialogService,
toastService: ToastService,
) {
super(
sendService,
i18nService,
platformUtilsService,
environmentService,
ngZone,
searchService,
policyService,
logService,
sendApiService,
dialogService,
toastService,
);
this.onSuccessfulLoad = async () => {
this.selectType(this.type);
};
this.applySavedState =
(window as any).previousPopupUrl != null &&
!(window as any).previousPopupUrl.startsWith("/send-type");
}
async ngOnInit() {
// Let super class finish
await super.ngOnInit();
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (params) => {
if (this.applySavedState) {
this.state = await this.stateService.getBrowserSendTypeComponentState();
if (this.state?.searchText != null) {
this.searchText = this.state.searchText;
}
}
if (params.type != null) {
this.type = parseInt(params.type, null);
switch (this.type) {
case SendType.Text:
this.groupingTitle = this.i18nService.t("sendTypeText");
break;
case SendType.File:
this.groupingTitle = this.i18nService.t("sendTypeFile");
break;
default:
break;
}
await this.load((s) => s.type === this.type);
}
// Restore state and remove reference
if (this.applySavedState && this.state != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserPopupUtils.setContentScrollY(window, this.state?.scrollY);
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.stateService.setBrowserSendTypeComponentState(null);
});
// Refresh Send list if sync completed in background
this.broadcasterService.subscribe(ComponentId, (message: any) => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.ngZone.run(async () => {
switch (message.command) {
case "syncCompleted":
if (message.successfully) {
this.refreshTimeout = window.setTimeout(() => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.refresh();
}, 500);
}
break;
default:
break;
}
this.changeDetectorRef.detectChanges();
});
});
}
ngOnDestroy() {
// Remove timeout
if (this.refreshTimeout != null) {
window.clearTimeout(this.refreshTimeout);
}
// Save state
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.saveState();
// Unsubscribe
this.broadcasterService.unsubscribe(ComponentId);
}
async selectSend(s: SendView) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/edit-send"], { queryParams: { sendId: s.id } });
}
async addSend() {
if (this.disableSend) {
return;
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/add-send"], { queryParams: { type: this.type } });
}
async removePassword(s: SendView): Promise<boolean> {
if (this.disableSend) {
return;
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
super.removePassword(s);
}
back() {
(window as any).routeDirection = "b";
this.location.back();
}
private async saveState() {
this.state = {
scrollY: BrowserPopupUtils.getContentScrollY(window),
searchText: this.searchText,
};
await this.stateService.setBrowserSendTypeComponentState(this.state);
}
}

View File

@@ -1,59 +0,0 @@
import {
FakeAccountService,
mockAccountServiceWith,
} from "@bitwarden/common/../spec/fake-account-service";
import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider";
import { awaitAsync } from "@bitwarden/common/../spec/utils";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserId } from "@bitwarden/common/types/guid";
import { BrowserComponentState } from "../../../models/browserComponentState";
import { BrowserSendComponentState } from "../../../models/browserSendComponentState";
import { BrowserSendStateService } from "./browser-send-state.service";
describe("Browser Send State Service", () => {
let stateProvider: FakeStateProvider;
let accountService: FakeAccountService;
let stateService: BrowserSendStateService;
const mockUserId = Utils.newGuid() as UserId;
beforeEach(() => {
accountService = mockAccountServiceWith(mockUserId);
stateProvider = new FakeStateProvider(accountService);
stateService = new BrowserSendStateService(stateProvider);
});
describe("getBrowserSendComponentState", () => {
it("should return BrowserSendComponentState", async () => {
const state = new BrowserSendComponentState();
state.scrollY = 0;
state.searchText = "test";
await stateService.setBrowserSendComponentState(state);
await awaitAsync();
const actual = await stateService.getBrowserSendComponentState();
expect(actual).toStrictEqual(state);
});
});
describe("getBrowserSendTypeComponentState", () => {
it("should return BrowserComponentState", async () => {
const state = new BrowserComponentState();
state.scrollY = 0;
state.searchText = "test";
await stateService.setBrowserSendTypeComponentState(state);
await awaitAsync();
const actual = await stateService.getBrowserSendTypeComponentState();
expect(actual).toStrictEqual(state);
});
});
});

View File

@@ -1,71 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable, firstValueFrom } from "rxjs";
import { ActiveUserState, StateProvider } from "@bitwarden/common/platform/state";
import { BrowserComponentState } from "../../../models/browserComponentState";
import { BrowserSendComponentState } from "../../../models/browserSendComponentState";
import { BROWSER_SEND_COMPONENT, BROWSER_SEND_TYPE_COMPONENT } from "./key-definitions";
/** Get or set the active user's component state for the Send browser component
*/
export class BrowserSendStateService {
/** Observable that contains the current state for active user Sends including the send data and type counts
* along with the search text and scroll position
*/
browserSendComponentState$: Observable<BrowserSendComponentState>;
/** Observable that contains the current state for active user Sends that only includes the search text
* and scroll position
*/
browserSendTypeComponentState$: Observable<BrowserComponentState>;
private activeUserBrowserSendComponentState: ActiveUserState<BrowserSendComponentState>;
private activeUserBrowserSendTypeComponentState: ActiveUserState<BrowserComponentState>;
constructor(protected stateProvider: StateProvider) {
this.activeUserBrowserSendComponentState = this.stateProvider.getActive(BROWSER_SEND_COMPONENT);
this.browserSendComponentState$ = this.activeUserBrowserSendComponentState.state$;
this.activeUserBrowserSendTypeComponentState = this.stateProvider.getActive(
BROWSER_SEND_TYPE_COMPONENT,
);
this.browserSendTypeComponentState$ = this.activeUserBrowserSendTypeComponentState.state$;
}
/** Get the active user's browser send component state
* @returns { BrowserSendComponentState } contains the sends and type counts along with the scroll position and search text for the
* send component on the browser
*/
async getBrowserSendComponentState(): Promise<BrowserSendComponentState> {
return await firstValueFrom(this.browserSendComponentState$);
}
/** Set the active user's browser send component state
* @param { BrowserSendComponentState } value sets the sends along with the scroll position and search text for
* the send component on the browser
*/
async setBrowserSendComponentState(value: BrowserSendComponentState): Promise<void> {
await this.activeUserBrowserSendComponentState.update(() => value, {
shouldUpdate: (current) => !(current == null && value == null),
});
}
/** Get the active user's browser component state
* @returns { BrowserComponentState } contains the scroll position and search text for the sends menu on the browser
*/
async getBrowserSendTypeComponentState(): Promise<BrowserComponentState> {
return await firstValueFrom(this.browserSendTypeComponentState$);
}
/** Set the active user's browser component state
* @param { BrowserComponentState } value set the scroll position and search text for the send component on the browser
*/
async setBrowserSendTypeComponentState(value: BrowserComponentState): Promise<void> {
await this.activeUserBrowserSendTypeComponentState.update(() => value, {
shouldUpdate: (current) => !(current == null && value == null),
});
}
}

View File

@@ -1,39 +0,0 @@
import { Jsonify } from "type-fest";
import { BrowserSendComponentState } from "../../../models/browserSendComponentState";
import { BROWSER_SEND_COMPONENT, BROWSER_SEND_TYPE_COMPONENT } from "./key-definitions";
describe("Key definitions", () => {
describe("BROWSER_SEND_COMPONENT", () => {
it("should deserialize BrowserSendComponentState", () => {
const keyDef = BROWSER_SEND_COMPONENT;
const expectedState = {
scrollY: 0,
searchText: "test",
};
const result = keyDef.deserializer(
JSON.parse(JSON.stringify(expectedState)) as Jsonify<BrowserSendComponentState>,
);
expect(result).toEqual(expectedState);
});
});
describe("BROWSER_SEND_TYPE_COMPONENT", () => {
it("should deserialize BrowserComponentState", () => {
const keyDef = BROWSER_SEND_TYPE_COMPONENT;
const expectedState = {
scrollY: 0,
searchText: "test",
};
const result = keyDef.deserializer(JSON.parse(JSON.stringify(expectedState)));
expect(result).toEqual(expectedState);
});
});
});

View File

@@ -1,27 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { BROWSER_SEND_MEMORY, UserKeyDefinition } from "@bitwarden/common/platform/state";
import { BrowserComponentState } from "../../../models/browserComponentState";
import { BrowserSendComponentState } from "../../../models/browserSendComponentState";
export const BROWSER_SEND_COMPONENT = new UserKeyDefinition<BrowserSendComponentState>(
BROWSER_SEND_MEMORY,
"browser_send_component",
{
deserializer: (obj: Jsonify<BrowserSendComponentState>) =>
BrowserSendComponentState.fromJSON(obj),
clearOn: ["logout", "lock"],
},
);
export const BROWSER_SEND_TYPE_COMPONENT = new UserKeyDefinition<BrowserComponentState>(
BROWSER_SEND_MEMORY,
"browser_send_type_component",
{
deserializer: (obj: Jsonify<BrowserComponentState>) => BrowserComponentState.fromJSON(obj),
clearOn: ["logout", "lock"],
},
);

View File

@@ -55,7 +55,7 @@
bitBadge
variant="primary"
(click)="doAutofill(cipher)"
[title]="'autofillTitle' | i18n: cipher.name"
[title]="autofillShortcutTooltip() ?? ('autofillTitle' | i18n: cipher.name)"
[attr.aria-label]="'autofillTitle' | i18n: cipher.name"
>
{{ "fill" | i18n }}

View File

@@ -2,12 +2,22 @@
// @ts-strict-ignore
import { ScrollingModule } from "@angular/cdk/scrolling";
import { CommonModule } from "@angular/common";
import { booleanAttribute, Component, EventEmitter, inject, Input, Output } from "@angular/core";
import {
AfterViewInit,
booleanAttribute,
Component,
EventEmitter,
inject,
Input,
Output,
signal,
} from "@angular/core";
import { Router, RouterLink } from "@angular/router";
import { map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
@@ -50,7 +60,7 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options
templateUrl: "vault-list-items-container.component.html",
standalone: true,
})
export class VaultListItemsContainerComponent {
export class VaultListItemsContainerComponent implements AfterViewInit {
private compactModeService = inject(CompactModeService);
/**
@@ -133,14 +143,29 @@ export class VaultListItemsContainerComponent {
return cipher.collections[0]?.name;
}
protected autofillShortcutTooltip = signal<string | undefined>(undefined);
constructor(
private i18nService: I18nService,
private vaultPopupAutofillService: VaultPopupAutofillService,
private passwordRepromptService: PasswordRepromptService,
private cipherService: CipherService,
private router: Router,
private platformUtilsService: PlatformUtilsService,
) {}
async ngAfterViewInit() {
const autofillShortcut = await this.platformUtilsService.getAutofillKeyboardShortcut();
if (autofillShortcut === "") {
this.autofillShortcutTooltip.set(undefined);
} else {
const autofillTitle = this.i18nService.t("autoFill");
this.autofillShortcutTooltip.set(`${autofillTitle} ${autofillShortcut}`);
}
}
/**
* Launches the login cipher in a new browser tab.
*/

View File

@@ -15,7 +15,6 @@ import {
unauthGuardFn,
} from "@bitwarden/angular/auth/guards";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import { extensionRefreshRedirect } from "@bitwarden/angular/utils/extension-refresh-redirect";
import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards";
import {
AnonLayoutWrapperComponent,
@@ -23,7 +22,7 @@ import {
LoginComponent,
LoginSecondaryContentComponent,
LockIcon,
LockV2Component,
LockComponent,
LoginViaAuthRequestComponent,
PasswordHintComponent,
RegistrationFinishComponent,
@@ -51,7 +50,6 @@ import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-fa
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
import { HintComponent } from "../auth/hint.component";
import { LockComponent } from "../auth/lock.component";
import { LoginDecryptionOptionsComponentV1 } from "../auth/login/login-decryption-options/login-decryption-options-v1.component";
import { LoginComponentV1 } from "../auth/login/login-v1.component";
import { LoginViaAuthRequestComponentV1 } from "../auth/login/login-via-auth-request-v1.component";
@@ -81,12 +79,6 @@ const routes: Routes = [
children: [], // Children lets us have an empty component.
canActivate: [redirectGuard({ loggedIn: "/vault", loggedOut: "/login", locked: "/lock" })],
},
{
path: "lock",
component: LockComponent,
canActivate: [lockGuard()],
canMatch: [extensionRefreshRedirect("/lockV2")],
},
...twofactorRefactorSwap(
TwoFactorComponent,
AnonLayoutWrapperComponent,
@@ -373,8 +365,8 @@ const routes: Routes = [
],
},
{
path: "lockV2",
canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh), lockGuard()],
path: "lock",
canActivate: [lockGuard()],
data: {
pageIcon: LockIcon,
pageTitle: {
@@ -385,7 +377,7 @@ const routes: Routes = [
children: [
{
path: "",
component: LockV2Component,
component: LockComponent,
},
],
},

View File

@@ -13,7 +13,6 @@ import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.compo
import { DeleteAccountComponent } from "../auth/delete-account.component";
import { EnvironmentComponent } from "../auth/environment.component";
import { HintComponent } from "../auth/hint.component";
import { LockComponent } from "../auth/lock.component";
import { LoginModule } from "../auth/login/login.module";
import { RegisterComponent } from "../auth/register.component";
import { RemovePasswordComponent } from "../auth/remove-password.component";
@@ -78,7 +77,6 @@ import { SendComponent } from "./tools/send/send.component";
FolderAddEditComponent,
HeaderComponent,
HintComponent,
LockComponent,
NavComponent,
GeneratorComponent,
PasswordGeneratorHistoryComponent,

View File

@@ -1,80 +0,0 @@
<form id="lock-page" (ngSubmit)="submit()">
<div class="content">
<p aria-hidden="true"><i class="bwi bwi-lock bwi-4x text-muted"></i></p>
<p>{{ "yourVaultIsLocked" | i18n }}</p>
<div class="box last">
<div class="box-content">
<div
class="box-content-row box-content-row-flex"
appBoxRow
*ngIf="pinEnabled || masterPasswordEnabled"
>
<div class="row-main" *ngIf="pinEnabled">
<label for="pin">{{ "pin" | i18n }}</label>
<input
id="pin"
type="{{ showPassword ? 'text' : 'password' }}"
name="PIN"
class="monospaced"
[(ngModel)]="pin"
required
appInputVerbatim
/>
</div>
<div class="row-main" *ngIf="masterPasswordEnabled && !pinEnabled">
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
aria-describedby="masterPasswordHelp"
class="monospaced"
[(ngModel)]="masterPassword"
required
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
</div>
<div id="masterPasswordHelp" class="box-footer">
{{ "loggedInAsOn" | i18n: email : webVaultHostname }}
</div>
</div>
<div class="buttons with-rows">
<div class="buttons-row" *ngIf="supportsBiometric && biometricLock && biometricReady">
<button
type="button"
class="btn block"
[ngClass]="{ 'primary font-weight-bold': !pinEnabled && !masterPasswordEnabled }"
(click)="unlockBiometric()"
>
{{ biometricText | i18n }}
</button>
</div>
<div class="buttons-row">
<button type="submit" class="btn primary block" *ngIf="pinEnabled || masterPasswordEnabled">
<i class="bwi bwi-unlock" aria-hidden="true"></i> <b>{{ "unlock" | i18n }}</b>
</button>
<button type="button" class="btn block" (click)="logOut()">
{{ "logOut" | i18n }}
</button>
</div>
</div>
</div>
</form>

View File

@@ -1,478 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { NO_ERRORS_SCHEMA } from "@angular/core";
import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing";
import { ActivatedRoute } from "@angular/router";
import { MockProxy, mock } from "jest-mock-extended";
import { of } from "rxjs";
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { UserId } from "@bitwarden/common/types/guid";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService, ToastService } from "@bitwarden/components";
import {
KdfConfigService,
KeyService,
BiometricsService as AbstractBiometricService,
BiometricStateService,
} from "@bitwarden/key-management";
import { BiometricsService } from "../key-management/biometrics/biometrics.service";
import { LockComponent } from "./lock.component";
// ipc mock global
const isWindowVisibleMock = jest.fn();
(global as any).ipc = {
platform: {
isWindowVisible: isWindowVisibleMock,
},
keyManagement: {
biometric: {
enabled: jest.fn(),
},
},
};
describe("LockComponent", () => {
let component: LockComponent;
let fixture: ComponentFixture<LockComponent>;
let stateServiceMock: MockProxy<StateService>;
let biometricStateService: MockProxy<BiometricStateService>;
let biometricsService: MockProxy<BiometricsService>;
let messagingServiceMock: MockProxy<MessagingService>;
let broadcasterServiceMock: MockProxy<BroadcasterService>;
let platformUtilsServiceMock: MockProxy<PlatformUtilsService>;
let activatedRouteMock: MockProxy<ActivatedRoute>;
let mockMasterPasswordService: FakeMasterPasswordService;
let mockToastService: MockProxy<ToastService>;
const mockUserId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
beforeEach(async () => {
stateServiceMock = mock<StateService>();
messagingServiceMock = mock<MessagingService>();
broadcasterServiceMock = mock<BroadcasterService>();
platformUtilsServiceMock = mock<PlatformUtilsService>();
mockToastService = mock<ToastService>();
activatedRouteMock = mock<ActivatedRoute>();
activatedRouteMock.queryParams = mock<ActivatedRoute["queryParams"]>();
mockMasterPasswordService = new FakeMasterPasswordService();
biometricStateService = mock();
biometricStateService.dismissedRequirePasswordOnStartCallout$ = of(false);
biometricStateService.promptAutomatically$ = of(false);
biometricStateService.promptCancelled$ = of(false);
await TestBed.configureTestingModule({
declarations: [LockComponent, I18nPipe],
providers: [
{ provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService },
{
provide: I18nService,
useValue: mock<I18nService>(),
},
{
provide: PlatformUtilsService,
useValue: platformUtilsServiceMock,
},
{
provide: MessagingService,
useValue: messagingServiceMock,
},
{
provide: KeyService,
useValue: mock<KeyService>(),
},
{
provide: VaultTimeoutService,
useValue: mock<VaultTimeoutService>(),
},
{
provide: VaultTimeoutSettingsService,
useValue: mock<VaultTimeoutSettingsService>(),
},
{
provide: EnvironmentService,
useValue: mock<EnvironmentService>(),
},
{
provide: StateService,
useValue: stateServiceMock,
},
{
provide: ApiService,
useValue: mock<ApiService>(),
},
{
provide: ActivatedRoute,
useValue: activatedRouteMock,
},
{
provide: BroadcasterService,
useValue: broadcasterServiceMock,
},
{
provide: PolicyApiServiceAbstraction,
useValue: mock<PolicyApiServiceAbstraction>(),
},
{
provide: InternalPolicyService,
useValue: mock<InternalPolicyService>(),
},
{
provide: PasswordStrengthServiceAbstraction,
useValue: mock<PasswordStrengthServiceAbstraction>(),
},
{
provide: LogService,
useValue: mock<LogService>(),
},
{
provide: DialogService,
useValue: mock<DialogService>(),
},
{
provide: DeviceTrustServiceAbstraction,
useValue: mock<DeviceTrustServiceAbstraction>(),
},
{
provide: UserVerificationService,
useValue: mock<UserVerificationService>(),
},
{
provide: PinServiceAbstraction,
useValue: mock<PinServiceAbstraction>(),
},
{
provide: BiometricStateService,
useValue: biometricStateService,
},
{
provide: AbstractBiometricService,
useValue: biometricsService,
},
{
provide: AccountService,
useValue: accountService,
},
{
provide: AuthService,
useValue: mock(),
},
{
provide: KdfConfigService,
useValue: mock<KdfConfigService>(),
},
{
provide: SyncService,
useValue: mock<SyncService>(),
},
{
provide: ToastService,
useValue: mockToastService,
},
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
fixture = TestBed.createComponent(LockComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
afterEach(() => {
jest.resetAllMocks();
});
describe("ngOnInit", () => {
it("should call super.ngOnInit() once", async () => {
const superNgOnInitSpy = jest.spyOn(BaseLockComponent.prototype, "ngOnInit");
await component.ngOnInit();
expect(superNgOnInitSpy).toHaveBeenCalledTimes(1);
});
it('should set "autoPromptBiometric" to true if "biometricState.promptAutomatically$" resolves to true', async () => {
biometricStateService.promptAutomatically$ = of(true);
await component.ngOnInit();
expect(component["autoPromptBiometric"]).toBe(true);
});
it('should set "autoPromptBiometric" to false if "biometricState.promptAutomatically$" resolves to false', async () => {
biometricStateService.promptAutomatically$ = of(false);
await component.ngOnInit();
expect(component["autoPromptBiometric"]).toBe(false);
});
it('should set "biometricReady" to true if "stateService.getBiometricReady()" resolves to true', async () => {
component["canUseBiometric"] = jest.fn().mockResolvedValue(true);
await component.ngOnInit();
expect(component["biometricReady"]).toBe(true);
});
it('should set "biometricReady" to false if "stateService.getBiometricReady()" resolves to false', async () => {
component["canUseBiometric"] = jest.fn().mockResolvedValue(false);
await component.ngOnInit();
expect(component["biometricReady"]).toBe(false);
});
it("should call displayBiometricUpdateWarning", async () => {
component["displayBiometricUpdateWarning"] = jest.fn();
await component.ngOnInit();
expect(component["displayBiometricUpdateWarning"]).toHaveBeenCalledTimes(1);
});
it("should call delayedAskForBiometric", async () => {
component["delayedAskForBiometric"] = jest.fn();
await component.ngOnInit();
expect(component["delayedAskForBiometric"]).toHaveBeenCalledTimes(1);
expect(component["delayedAskForBiometric"]).toHaveBeenCalledWith(500);
});
it("should call delayedAskForBiometric when queryParams change", async () => {
activatedRouteMock.queryParams = of({ promptBiometric: true });
component["delayedAskForBiometric"] = jest.fn();
await component.ngOnInit();
expect(component["delayedAskForBiometric"]).toHaveBeenCalledTimes(1);
expect(component["delayedAskForBiometric"]).toHaveBeenCalledWith(500);
});
it("should call messagingService.send", async () => {
await component.ngOnInit();
expect(messagingServiceMock.send).toHaveBeenCalledWith("getWindowIsFocused");
});
describe("broadcasterService.subscribe", () => {
it('should call onWindowHidden() when "broadcasterService.subscribe" is called with "windowHidden"', async () => {
component["onWindowHidden"] = jest.fn();
await component.ngOnInit();
broadcasterServiceMock.subscribe.mock.calls[0][1]({ command: "windowHidden" });
expect(component["onWindowHidden"]).toHaveBeenCalledTimes(1);
});
it('should call focusInput() when "broadcasterService.subscribe" is called with "windowIsFocused" is true and deferFocus is false', async () => {
component["focusInput"] = jest.fn();
component["deferFocus"] = null;
await component.ngOnInit();
broadcasterServiceMock.subscribe.mock.calls[0][1]({
command: "windowIsFocused",
windowIsFocused: true,
} as any);
expect(component["deferFocus"]).toBe(false);
expect(component["focusInput"]).toHaveBeenCalledTimes(1);
});
it('should not call focusInput() when "broadcasterService.subscribe" is called with "windowIsFocused" is true and deferFocus is true', async () => {
component["focusInput"] = jest.fn();
component["deferFocus"] = null;
await component.ngOnInit();
broadcasterServiceMock.subscribe.mock.calls[0][1]({
command: "windowIsFocused",
windowIsFocused: false,
} as any);
expect(component["deferFocus"]).toBe(true);
expect(component["focusInput"]).toHaveBeenCalledTimes(0);
});
it('should call focusInput() when "broadcasterService.subscribe" is called with "windowIsFocused" is true and deferFocus is true', async () => {
component["focusInput"] = jest.fn();
component["deferFocus"] = true;
await component.ngOnInit();
broadcasterServiceMock.subscribe.mock.calls[0][1]({
command: "windowIsFocused",
windowIsFocused: true,
} as any);
expect(component["deferFocus"]).toBe(false);
expect(component["focusInput"]).toHaveBeenCalledTimes(1);
});
it('should not call focusInput() when "broadcasterService.subscribe" is called with "windowIsFocused" is false and deferFocus is true', async () => {
component["focusInput"] = jest.fn();
component["deferFocus"] = true;
await component.ngOnInit();
broadcasterServiceMock.subscribe.mock.calls[0][1]({
command: "windowIsFocused",
windowIsFocused: false,
} as any);
expect(component["deferFocus"]).toBe(true);
expect(component["focusInput"]).toHaveBeenCalledTimes(0);
});
});
});
describe("ngOnDestroy", () => {
it("should call super.ngOnDestroy()", () => {
const superNgOnDestroySpy = jest.spyOn(BaseLockComponent.prototype, "ngOnDestroy");
component.ngOnDestroy();
expect(superNgOnDestroySpy).toHaveBeenCalledTimes(1);
});
it("should call broadcasterService.unsubscribe()", () => {
component.ngOnDestroy();
expect(broadcasterServiceMock.unsubscribe).toHaveBeenCalledTimes(1);
});
});
describe("focusInput", () => {
it('should call "focus" on #pin input if pinEnabled is true', () => {
component["pinEnabled"] = true;
global.document.getElementById = jest.fn().mockReturnValue({ focus: jest.fn() });
component["focusInput"]();
expect(global.document.getElementById).toHaveBeenCalledWith("pin");
});
it('should call "focus" on #masterPassword input if pinEnabled is false', () => {
component["pinEnabled"] = false;
global.document.getElementById = jest.fn().mockReturnValue({ focus: jest.fn() });
component["focusInput"]();
expect(global.document.getElementById).toHaveBeenCalledWith("masterPassword");
});
});
describe("delayedAskForBiometric", () => {
beforeEach(() => {
component["supportsBiometric"] = true;
component["autoPromptBiometric"] = true;
});
it('should wait for "delay" milliseconds', fakeAsync(async () => {
const delaySpy = jest.spyOn(global, "setTimeout");
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
component["delayedAskForBiometric"](5000);
tick(4000);
component["biometricAsked"] = false;
tick(1000);
component["biometricAsked"] = true;
expect(delaySpy).toHaveBeenCalledWith(expect.any(Function), 5000);
}));
it('should return; if "params" is defined and "params.promptBiometric" is false', fakeAsync(async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
component["delayedAskForBiometric"](5000, { promptBiometric: false });
tick(5000);
expect(component["biometricAsked"]).toBe(false);
}));
it('should not return; if "params" is defined and "params.promptBiometric" is true', fakeAsync(async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
component["delayedAskForBiometric"](5000, { promptBiometric: true });
tick(5000);
expect(component["biometricAsked"]).toBe(true);
}));
it('should not return; if "params" is undefined', fakeAsync(async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
component["delayedAskForBiometric"](5000);
tick(5000);
expect(component["biometricAsked"]).toBe(true);
}));
it('should return; if "supportsBiometric" is false', fakeAsync(async () => {
component["supportsBiometric"] = false;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
component["delayedAskForBiometric"](5000);
tick(5000);
expect(component["biometricAsked"]).toBe(false);
}));
it('should return; if "autoPromptBiometric" is false', fakeAsync(async () => {
component["autoPromptBiometric"] = false;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
component["delayedAskForBiometric"](5000);
tick(5000);
expect(component["biometricAsked"]).toBe(false);
}));
it("should call unlockBiometric() if biometricAsked is false and window is visible", fakeAsync(async () => {
isWindowVisibleMock.mockResolvedValue(true);
component["unlockBiometric"] = jest.fn();
component["biometricAsked"] = false;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
component["delayedAskForBiometric"](5000);
tick(5000);
expect(component["unlockBiometric"]).toHaveBeenCalledTimes(1);
}));
it("should not call unlockBiometric() if biometricAsked is false and window is not visible", fakeAsync(async () => {
isWindowVisibleMock.mockResolvedValue(false);
component["unlockBiometric"] = jest.fn();
component["biometricAsked"] = false;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
component["delayedAskForBiometric"](5000);
tick(5000);
expect(component["unlockBiometric"]).toHaveBeenCalledTimes(0);
}));
it("should not call unlockBiometric() if biometricAsked is true", fakeAsync(async () => {
isWindowVisibleMock.mockResolvedValue(true);
component["unlockBiometric"] = jest.fn();
component["biometricAsked"] = true;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
component["delayedAskForBiometric"](5000);
tick(5000);
expect(component["unlockBiometric"]).toHaveBeenCalledTimes(0);
}));
});
describe("canUseBiometric", () => {
it("should call biometric.enabled with current active user", async () => {
await component["canUseBiometric"]();
expect(ipc.keyManagement.biometric.enabled).toHaveBeenCalledWith(mockUserId);
});
});
it('onWindowHidden() should set "showPassword" to false', () => {
component["showPassword"] = true;
component["onWindowHidden"]();
expect(component["showPassword"]).toBe(false);
});
});

View File

@@ -1,235 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom, map, switchMap } from "rxjs";
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { DeviceType } from "@bitwarden/common/enums";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService, ToastService } from "@bitwarden/components";
import {
KdfConfigService,
KeyService,
BiometricsService,
BiometricStateService,
} from "@bitwarden/key-management";
const BroadcasterSubscriptionId = "LockComponent";
@Component({
selector: "app-lock",
templateUrl: "lock.component.html",
})
export class LockComponent extends BaseLockComponent implements OnInit, OnDestroy {
private deferFocus: boolean = null;
protected biometricReady = false;
private biometricAsked = false;
private autoPromptBiometric = false;
private timerId: any;
constructor(
masterPasswordService: InternalMasterPasswordServiceAbstraction,
router: Router,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
messagingService: MessagingService,
keyService: KeyService,
vaultTimeoutService: VaultTimeoutService,
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
environmentService: EnvironmentService,
protected override stateService: StateService,
apiService: ApiService,
private route: ActivatedRoute,
private broadcasterService: BroadcasterService,
ngZone: NgZone,
policyApiService: PolicyApiServiceAbstraction,
policyService: InternalPolicyService,
passwordStrengthService: PasswordStrengthServiceAbstraction,
logService: LogService,
dialogService: DialogService,
deviceTrustService: DeviceTrustServiceAbstraction,
userVerificationService: UserVerificationService,
pinService: PinServiceAbstraction,
biometricStateService: BiometricStateService,
biometricsService: BiometricsService,
accountService: AccountService,
authService: AuthService,
kdfConfigService: KdfConfigService,
syncService: SyncService,
toastService: ToastService,
) {
super(
masterPasswordService,
router,
i18nService,
platformUtilsService,
messagingService,
keyService,
vaultTimeoutService,
vaultTimeoutSettingsService,
environmentService,
stateService,
apiService,
logService,
ngZone,
policyApiService,
policyService,
passwordStrengthService,
dialogService,
deviceTrustService,
userVerificationService,
pinService,
biometricStateService,
biometricsService,
accountService,
authService,
kdfConfigService,
syncService,
toastService,
);
}
async ngOnInit() {
await super.ngOnInit();
this.autoPromptBiometric = await firstValueFrom(
this.biometricStateService.promptAutomatically$,
);
this.biometricReady = await this.canUseBiometric();
await this.displayBiometricUpdateWarning();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.delayedAskForBiometric(500);
this.route.queryParams.pipe(switchMap((params) => this.delayedAskForBiometric(500, params)));
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(() => {
switch (message.command) {
case "windowHidden":
this.onWindowHidden();
break;
case "windowIsFocused":
if (this.deferFocus === null) {
this.deferFocus = !message.windowIsFocused;
if (!this.deferFocus) {
this.focusInput();
}
} else if (this.deferFocus && message.windowIsFocused) {
this.focusInput();
this.deferFocus = false;
}
break;
default:
}
});
});
this.messagingService.send("getWindowIsFocused");
// start background listener until destroyed on interval
this.timerId = setInterval(async () => {
this.supportsBiometric = await this.biometricsService.supportsBiometric();
this.biometricReady = await this.canUseBiometric();
}, 1000);
}
ngOnDestroy() {
super.ngOnDestroy();
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
clearInterval(this.timerId);
}
onWindowHidden() {
this.showPassword = false;
}
private async delayedAskForBiometric(delay: number, params?: any) {
await new Promise((resolve) => setTimeout(resolve, delay));
if (params && !params.promptBiometric) {
return;
}
if (!this.supportsBiometric || !this.autoPromptBiometric || this.biometricAsked) {
return;
}
if (await firstValueFrom(this.biometricStateService.promptCancelled$)) {
return;
}
this.biometricAsked = true;
if (await ipc.platform.isWindowVisible()) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.unlockBiometric();
}
}
private async canUseBiometric() {
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id)));
return await ipc.keyManagement.biometric.enabled(userId);
}
private focusInput() {
document.getElementById(this.pinEnabled ? "pin" : "masterPassword")?.focus();
}
private async displayBiometricUpdateWarning(): Promise<void> {
if (await firstValueFrom(this.biometricStateService.dismissedRequirePasswordOnStartCallout$)) {
return;
}
if (this.platformUtilsService.getDevice() !== DeviceType.WindowsDesktop) {
return;
}
if (await firstValueFrom(this.biometricStateService.biometricUnlockEnabled$)) {
const response = await this.dialogService.openSimpleDialog({
title: { key: "windowsBiometricUpdateWarningTitle" },
content: { key: "windowsBiometricUpdateWarning" },
type: "warning",
});
await this.biometricStateService.setRequirePasswordOnStart(response);
if (response) {
await this.biometricStateService.setPromptAutomatically(false);
}
this.supportsBiometric = await this.canUseBiometric();
await this.biometricStateService.setDismissedRequirePasswordOnStartCallout();
}
}
get biometricText() {
switch (this.platformUtilsService.getDevice()) {
case DeviceType.MacOsDesktop:
return "unlockWithTouchId";
case DeviceType.WindowsDesktop:
return "unlockWithWindowsHello";
case DeviceType.LinuxDesktop:
return "unlockWithPolkit";
default:
throw new Error("Unsupported platform");
}
}
}

View File

@@ -1,27 +0,0 @@
<form [bitSubmit]="submit" [formGroup]="formGroup">
<bit-form-field>
<bit-label>{{ "masterPass" | i18n }}</bit-label>
<input
type="password"
formControlName="masterPassword"
bitInput
appAutofocus
name="masterPassword"
required
/>
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
<bit-hint>{{ "loggedInAsEmailOn" | i18n: email : webVaultHostname }}</bit-hint>
</bit-form-field>
<hr />
<div class="tw-flex tw-gap-2">
<button type="submit" bitButton bitFormButton buttonType="primary" block>
<i class="bwi bwi-unlock" aria-hidden="true"></i>
{{ "unlock" | i18n }}
</button>
<button type="button" bitButton bitFormButton block (click)="logOut()">
{{ "logOut" | i18n }}
</button>
</div>
</form>

View File

@@ -1,51 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit, inject } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
import { SharedModule } from "../shared";
@Component({
selector: "app-lock",
templateUrl: "lock.component.html",
standalone: true,
imports: [SharedModule],
})
export class LockComponent extends BaseLockComponent implements OnInit {
formBuilder = inject(FormBuilder);
formGroup = this.formBuilder.group({
masterPassword: ["", { validators: Validators.required, updateOn: "submit" }],
});
get masterPasswordFormControl() {
return this.formGroup.controls.masterPassword;
}
async ngOnInit() {
await super.ngOnInit();
this.masterPasswordFormControl.setValue(this.masterPassword);
this.onSuccessfulSubmit = async () => {
await this.router.navigateByUrl(this.successRoute);
};
}
async superSubmit() {
await super.submit();
}
submit = async () => {
this.formGroup.markAllAsTouched();
if (this.formGroup.invalid) {
return;
}
this.masterPassword = this.masterPasswordFormControl.value;
await this.superSubmit();
};
}

View File

@@ -12,7 +12,6 @@ import {
} from "@bitwarden/angular/auth/guards";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import { generatorSwap } from "@bitwarden/angular/tools/generator/generator-swap";
import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap";
import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards";
import {
AnonLayoutWrapperComponent,
@@ -26,7 +25,7 @@ import {
RegistrationLinkExpiredComponent,
LoginComponent,
LoginSecondaryContentComponent,
LockV2Component,
LockComponent,
LockIcon,
TwoFactorTimeoutIcon,
UserLockIcon,
@@ -56,7 +55,6 @@ import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizatio
import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component";
import { deepLinkGuard } from "./auth/guards/deep-link.guard";
import { HintComponent } from "./auth/hint.component";
import { LockComponent } from "./auth/lock.component";
import { LoginDecryptionOptionsComponentV1 } from "./auth/login/login-decryption-options/login-decryption-options-v1.component";
import { LoginComponentV1 } from "./auth/login/login-v1.component";
import { LoginViaAuthRequestComponentV1 } from "./auth/login/login-via-auth-request-v1.component";
@@ -509,44 +507,23 @@ const routes: Routes = [
},
},
},
...extensionRefreshSwap(
LockComponent,
LockV2Component,
{
path: "lock",
canActivate: [deepLinkGuard(), lockGuard()],
children: [
{
path: "",
component: LockComponent,
},
],
data: {
pageTitle: {
key: "yourVaultIsLockedV2",
},
pageIcon: LockIcon,
showReadonlyHostname: true,
} satisfies AnonLayoutWrapperData,
},
{
path: "lock",
canActivate: [deepLinkGuard(), lockGuard()],
children: [
{
path: "",
component: LockV2Component,
},
],
data: {
pageTitle: {
key: "yourVaultIsLockedV2",
},
pageIcon: LockIcon,
showReadonlyHostname: true,
} satisfies AnonLayoutWrapperData,
},
),
{
path: "lock",
canActivate: [deepLinkGuard(), lockGuard()],
children: [
{
path: "",
component: LockComponent,
},
],
data: {
pageTitle: {
key: "yourVaultIsLockedV2",
},
pageIcon: LockIcon,
showReadonlyHostname: true,
} satisfies AnonLayoutWrapperData,
},
{
path: "2fa",
canActivate: [unauthGuardFn()],

View File

@@ -104,6 +104,10 @@ import {
} from "../components/vault-item-dialog/vault-item-dialog.component";
import { VaultItemEvent } from "../components/vault-items/vault-item-event";
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
import {
AttachmentDialogResult,
AttachmentsV2Component,
} from "../individual-vault/attachments-v2.component";
import {
BulkDeleteDialogResult,
openBulkDeleteDialog,
@@ -119,7 +123,6 @@ import { VaultHeaderComponent } from "../org-vault/vault-header/vault-header.com
import { getNestedCollectionTree } from "../utils/collection-utils";
import { AddEditComponent } from "./add-edit.component";
import { AttachmentsComponent } from "./attachments.component";
import {
BulkCollectionsDialogComponent,
BulkCollectionsDialogResult,
@@ -761,29 +764,18 @@ export class VaultComponent implements OnInit, OnDestroy {
return;
}
let madeAttachmentChanges = false;
const [modal] = await this.modalService.openViewRef(
AttachmentsComponent,
this.attachmentsModalRef,
(comp) => {
comp.organization = this.organization;
comp.cipherId = cipher.id;
comp.onUploadedAttachment
.pipe(takeUntil(this.destroy$))
.subscribe(() => (madeAttachmentChanges = true));
comp.onDeletedAttachment
.pipe(takeUntil(this.destroy$))
.subscribe(() => (madeAttachmentChanges = true));
},
);
modal.onClosed.pipe(takeUntil(this.destroy$)).subscribe(() => {
if (madeAttachmentChanges) {
this.refresh();
}
madeAttachmentChanges = false;
const dialogRef = AttachmentsV2Component.open(this.dialogService, {
cipherId: cipher.id as CipherId,
});
const result = await firstValueFrom(dialogRef.closed);
if (
result.action === AttachmentDialogResult.Removed ||
result.action === AttachmentDialogResult.Uploaded
) {
this.refresh();
}
}
async addCipher(cipherType?: CipherType) {

View File

@@ -1,398 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom, Subject } from "rxjs";
import { concatMap, map, take, takeUntil } from "rxjs/operators";
import { PinServiceAbstraction, PinLockType } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.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 { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import {
MasterPasswordVerification,
MasterPasswordVerificationResponse,
} from "@bitwarden/common/auth/types/verification";
import { ClientType } from "@bitwarden/common/enums";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService, ToastService } from "@bitwarden/components";
import {
KdfConfigService,
KeyService,
BiometricStateService,
BiometricsService,
} from "@bitwarden/key-management";
@Directive()
export class LockComponent implements OnInit, OnDestroy {
masterPassword = "";
pin = "";
showPassword = false;
email: string;
pinEnabled = false;
masterPasswordEnabled = false;
webVaultHostname = "";
formPromise: Promise<MasterPasswordVerificationResponse>;
supportsBiometric: boolean;
biometricLock: boolean;
private activeUserId: UserId;
protected successRoute = "vault";
protected forcePasswordResetRoute = "update-temp-password";
protected onSuccessfulSubmit: () => Promise<void>;
private invalidPinAttempts = 0;
private pinLockType: PinLockType;
private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions = undefined;
private destroy$ = new Subject<void>();
constructor(
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
protected router: Router,
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
protected messagingService: MessagingService,
protected keyService: KeyService,
protected vaultTimeoutService: VaultTimeoutService,
protected vaultTimeoutSettingsService: VaultTimeoutSettingsService,
protected environmentService: EnvironmentService,
protected stateService: StateService,
protected apiService: ApiService,
protected logService: LogService,
protected ngZone: NgZone,
protected policyApiService: PolicyApiServiceAbstraction,
protected policyService: InternalPolicyService,
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
protected dialogService: DialogService,
protected deviceTrustService: DeviceTrustServiceAbstraction,
protected userVerificationService: UserVerificationService,
protected pinService: PinServiceAbstraction,
protected biometricStateService: BiometricStateService,
protected biometricsService: BiometricsService,
protected accountService: AccountService,
protected authService: AuthService,
protected kdfConfigService: KdfConfigService,
protected syncService: SyncService,
protected toastService: ToastService,
) {}
async ngOnInit() {
this.accountService.activeAccount$
.pipe(
concatMap(async (account) => {
this.activeUserId = account?.id;
await this.load(account?.id);
}),
takeUntil(this.destroy$),
)
.subscribe();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
async submit() {
if (this.pinEnabled) {
return await this.handlePinRequiredUnlock();
}
await this.handleMasterPasswordRequiredUnlock();
}
async logOut() {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "logOut" },
content: { key: "logOutConfirmation" },
acceptButtonText: { key: "logOut" },
type: "warning",
});
if (confirmed) {
this.messagingService.send("logout", { userId: this.activeUserId });
}
}
async unlockBiometric(): Promise<boolean> {
if (!this.biometricLock) {
return;
}
await this.biometricStateService.setUserPromptCancelled();
const userKey = await this.keyService.getUserKeyFromStorage(
KeySuffixOptions.Biometric,
this.activeUserId,
);
if (userKey) {
await this.setUserKeyAndContinue(userKey, this.activeUserId, false);
}
return !!userKey;
}
async isBiometricUnlockAvailable(): Promise<boolean> {
if (!(await this.biometricsService.supportsBiometric())) {
return false;
}
return this.biometricsService.isBiometricUnlockAvailable();
}
togglePassword() {
this.showPassword = !this.showPassword;
const input = document.getElementById(this.pinEnabled ? "pin" : "masterPassword");
if (this.ngZone.isStable) {
input.focus();
} else {
this.ngZone.onStable.pipe(take(1)).subscribe(() => input.focus());
}
}
private async handlePinRequiredUnlock() {
if (this.pin == null || this.pin === "") {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("pinRequired"),
});
return;
}
return await this.doUnlockWithPin();
}
private async doUnlockWithPin() {
const MAX_INVALID_PIN_ENTRY_ATTEMPTS = 5;
try {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
const userKey = await this.pinService.decryptUserKeyWithPin(this.pin, userId);
if (userKey) {
await this.setUserKeyAndContinue(userKey, userId);
return; // successfully unlocked
}
// Failure state: invalid PIN or failed decryption
this.invalidPinAttempts++;
// Log user out if they have entered an invalid PIN too many times
if (this.invalidPinAttempts >= MAX_INVALID_PIN_ENTRY_ATTEMPTS) {
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("tooManyInvalidPinEntryAttemptsLoggingOut"),
});
this.messagingService.send("logout");
return;
}
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("invalidPin"),
});
} catch {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("unexpectedError"),
});
}
}
private async handleMasterPasswordRequiredUnlock() {
if (this.masterPassword == null || this.masterPassword === "") {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPasswordRequired"),
});
return;
}
await this.doUnlockWithMasterPassword();
}
private async doUnlockWithMasterPassword() {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
const verification = {
type: VerificationType.MasterPassword,
secret: this.masterPassword,
} as MasterPasswordVerification;
let passwordValid = false;
let response: MasterPasswordVerificationResponse;
try {
this.formPromise = this.userVerificationService.verifyUserByMasterPassword(
verification,
userId,
this.email,
);
response = await this.formPromise;
this.enforcedMasterPasswordOptions = MasterPasswordPolicyOptions.fromResponse(
response.policyOptions,
);
passwordValid = true;
} catch (e) {
this.logService.error(e);
} finally {
this.formPromise = null;
}
if (!passwordValid) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("invalidMasterPassword"),
});
return;
}
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
response.masterKey,
userId,
);
await this.setUserKeyAndContinue(userKey, userId, true);
}
private async setUserKeyAndContinue(
key: UserKey,
userId: UserId,
evaluatePasswordAfterUnlock = false,
) {
await this.keyService.setUserKey(key, userId);
// Now that we have a decrypted user key in memory, we can check if we
// need to establish trust on the current device
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
await this.deviceTrustService.trustDeviceIfRequired(activeAccount.id);
await this.doContinue(evaluatePasswordAfterUnlock);
}
private async doContinue(evaluatePasswordAfterUnlock: boolean) {
await this.biometricStateService.resetUserPromptCancelled();
this.messagingService.send("unlocked");
if (evaluatePasswordAfterUnlock) {
try {
// If we do not have any saved policies, attempt to load them from the service
if (this.enforcedMasterPasswordOptions == undefined) {
this.enforcedMasterPasswordOptions = await firstValueFrom(
this.policyService.masterPasswordPolicyOptions$(),
);
}
if (this.requirePasswordChange()) {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.WeakMasterPassword,
userId,
);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate([this.forcePasswordResetRoute]);
return;
}
} catch (e) {
// Do not prevent unlock if there is an error evaluating policies
this.logService.error(e);
}
}
// Vault can be de-synced since notifications get ignored while locked. Need to check whether sync is required using the sync service.
const clientType = this.platformUtilsService.getClientType();
if (clientType === ClientType.Browser || clientType === ClientType.Desktop) {
// Desktop and Browser have better offline support and to facilitate this we don't make the user wait for what
// could be an HTTP Timeout because their server is unreachable.
await Promise.race([
this.syncService
.fullSync(false)
.catch((err) => this.logService.error("Error during unlock sync", err)),
new Promise<void>((resolve) =>
setTimeout(() => {
this.logService.warning("Skipping sync wait, continuing to unlock.");
resolve();
}, 5_000),
),
]);
} else {
await this.syncService.fullSync(false);
}
if (this.onSuccessfulSubmit != null) {
await this.onSuccessfulSubmit();
} else if (this.router != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate([this.successRoute]);
}
}
private async load(userId: UserId) {
this.pinLockType = await this.pinService.getPinLockType(userId);
this.pinEnabled = await this.pinService.isPinDecryptionAvailable(userId);
this.masterPasswordEnabled = await this.userVerificationService.hasMasterPassword();
this.supportsBiometric = await this.biometricsService.supportsBiometric();
this.biometricLock =
(await this.vaultTimeoutSettingsService.isBiometricLockSet()) &&
((await this.keyService.hasUserKeyStored(KeySuffixOptions.Biometric)) ||
!this.platformUtilsService.supportsSecureStorage());
this.email = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
);
this.webVaultHostname = (await this.environmentService.getEnvironment()).getHostname();
}
/**
* Checks if the master password meets the enforced policy requirements
* If not, returns false
*/
private requirePasswordChange(): boolean {
if (
this.enforcedMasterPasswordOptions == undefined ||
!this.enforcedMasterPasswordOptions.enforceOnLogin
) {
return false;
}
const passwordStrength = this.passwordStrengthService.getPasswordStrength(
this.masterPassword,
this.email,
)?.score;
return !this.policyService.evaluateMasterPassword(
passwordStrength,
this.masterPassword,
this.enforcedMasterPasswordOptions,
);
}
}

View File

@@ -75,7 +75,7 @@ const clientTypeToSuccessRouteRecord: Partial<Record<ClientType, string>> = {
IconButtonModule,
],
})
export class LockV2Component implements OnInit, OnDestroy {
export class LockComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
activeAccount: Account | null;
@@ -543,8 +543,8 @@ export class LockV2Component implements OnInit, OnDestroy {
const previousUrl = this.lockComponentService.getPreviousUrl();
/**
* In a passkey flow, the `previousUrl` will still be `/fido2?<queryParams>` at this point
* because the `/lockV2` route doesn't save the URL in the `BrowserRouterService`. This is
* handled by the `doNotSaveUrl` property on the `lockV2` route in `app-routing.module.ts`.
* because the `/lock` route doesn't save the URL in the `BrowserRouterService`. This is
* handled by the `doNotSaveUrl` property on the `/lock` route in `app-routing.module.ts`.
*/
if (previousUrl) {
await this.router.navigateByUrl(previousUrl);

View File

@@ -0,0 +1,3 @@
<span [ngClass]="{ 'tw-truncate tw-block': truncate }">
<ng-content></ng-content>
</span>

View File

@@ -1,6 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, ElementRef, HostBinding, Input } from "@angular/core";
import { CommonModule } from "@angular/common";
import { Component, ElementRef, HostBinding, Input } from "@angular/core";
import { FocusableElement } from "../shared/focusable-element";
@@ -28,12 +29,14 @@ const hoverStyles: Record<BadgeVariant, string[]> = {
info: ["hover:tw-bg-info-600", "hover:tw-border-info-600", "hover:!tw-text-black"],
};
@Directive({
@Component({
selector: "span[bitBadge], a[bitBadge], button[bitBadge]",
providers: [{ provide: FocusableElement, useExisting: BadgeDirective }],
providers: [{ provide: FocusableElement, useExisting: BadgeComponent }],
imports: [CommonModule],
templateUrl: "badge.component.html",
standalone: true,
})
export class BadgeDirective implements FocusableElement {
export class BadgeComponent implements FocusableElement {
@HostBinding("class") get classList() {
return [
"tw-inline-block",
@@ -62,8 +65,8 @@ export class BadgeDirective implements FocusableElement {
"disabled:tw-cursor-not-allowed",
]
.concat(styles[this.variant])
.concat(this.hasHoverEffects ? hoverStyles[this.variant] : [])
.concat(this.truncate ? ["tw-truncate", this.maxWidthClass] : []);
.concat(this.hasHoverEffects ? [...hoverStyles[this.variant], "tw-min-w-10"] : [])
.concat(this.truncate ? this.maxWidthClass : []);
}
@HostBinding("attr.title") get titleAttr() {
if (this.title !== undefined) {

View File

@@ -1,9 +1,9 @@
import { NgModule } from "@angular/core";
import { BadgeDirective } from "./badge.directive";
import { BadgeComponent } from "./badge.component";
@NgModule({
imports: [BadgeDirective],
exports: [BadgeDirective],
imports: [BadgeComponent],
exports: [BadgeComponent],
})
export class BadgeModule {}

View File

@@ -1,14 +1,14 @@
import { CommonModule } from "@angular/common";
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
import { BadgeDirective } from "./badge.directive";
import { BadgeComponent } from "./badge.component";
export default {
title: "Component Library/Badge",
component: BadgeDirective,
component: BadgeComponent,
decorators: [
moduleMetadata({
imports: [CommonModule, BadgeDirective],
imports: [CommonModule, BadgeComponent],
}),
],
args: {
@@ -21,9 +21,9 @@ export default {
url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A16956",
},
},
} as Meta<BadgeDirective>;
} as Meta<BadgeComponent>;
type Story = StoryObj<BadgeDirective>;
type Story = StoryObj<BadgeComponent>;
export const Variants: Story = {
render: (args) => ({

View File

@@ -1,2 +1,2 @@
export { BadgeDirective, BadgeVariant } from "./badge.directive";
export { BadgeComponent, BadgeVariant } from "./badge.component";
export * from "./badge.module";

View File

@@ -8,5 +8,13 @@ import { A11yCellDirective } from "../a11y/a11y-cell.directive";
imports: [],
template: `<ng-content></ng-content>`,
providers: [{ provide: A11yCellDirective, useExisting: ItemActionComponent }],
host: {
class:
/**
* `top` and `bottom` units should be kept in sync with `item-content.component.ts`'s y-axis padding.
* we want this `:after` element to be the same height as the `item-content`
*/
"[&>button]:tw-relative [&>button:not([bit-item-content])]:after:tw-content-[''] [&>button]:after:tw-absolute [&>button]:after:tw-block bit-compact:[&>button]:after:tw-top-[-0.8rem] bit-compact:[&>button]:after:tw-bottom-[-0.8rem] [&>button]:after:tw-top-[-0.85rem] [&>button]:after:tw-bottom-[-0.85rem] [&>button]:after:tw-right-[-0.25rem] [&>button]:after:tw-left-[-0.25rem]",
},
})
export class ItemActionComponent extends A11yCellDirective {}

View File

@@ -19,6 +19,10 @@ import { TypographyModule } from "../typography";
templateUrl: `item-content.component.html`,
host: {
class:
/**
* y-axis padding should be kept in sync with `item-action.component.ts`'s `top` and `bottom` units.
* we want this to be the same height as the `item-action`'s `:after` element
*/
"fvw-target tw-outline-none tw-text-main hover:tw-text-main tw-no-underline hover:tw-no-underline tw-text-base tw-py-2 tw-px-4 bit-compact:tw-py-1.5 bit-compact:tw-px-2 tw-bg-transparent tw-w-full tw-border-none tw-flex tw-gap-4 tw-items-center tw-justify-between",
},
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -70,7 +70,7 @@ export const Default: Story = {
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
<button type="button" bitBadge variant="primary">Fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
@@ -163,7 +163,7 @@ const multipleActionListTemplate = /*html*/ `
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
<button type="button" bitBadge variant="primary">Fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
@@ -182,7 +182,7 @@ const multipleActionListTemplate = /*html*/ `
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
<button type="button" bitBadge variant="primary">Fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
@@ -201,7 +201,7 @@ const multipleActionListTemplate = /*html*/ `
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
<button type="button" bitBadge variant="primary">Fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
@@ -220,7 +220,7 @@ const multipleActionListTemplate = /*html*/ `
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
<button type="button" bitBadge variant="primary">Fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
@@ -239,7 +239,7 @@ const multipleActionListTemplate = /*html*/ `
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
<button type="button" bitBadge variant="primary">Fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
@@ -258,7 +258,7 @@ const multipleActionListTemplate = /*html*/ `
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
<button type="button" bitBadge variant="primary">Fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
@@ -332,14 +332,14 @@ export const SingleActionWithBadge: Story = {
<bit-item>
<a bit-item-content href="#">
Foobar
<span bitBadge variant="primary" slot="default-trailing">Auto-fill</span>
<span bitBadge variant="primary" slot="default-trailing">Fill</span>
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a>
</bit-item>
<bit-item>
<a bit-item-content href="#">
Helloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo!
<span bitBadge variant="primary" slot="default-trailing">Auto-fill</span>
<span bitBadge variant="primary" slot="default-trailing">Fill</span>
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a>
</bit-item>
@@ -375,7 +375,7 @@ export const VirtualScrolling: Story = {
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
<button type="button" bitBadge variant="primary">Fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
@@ -405,7 +405,7 @@ export const WithoutBorderRadius: Story = {
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
<button type="button" bitBadge variant="primary">Fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone"></button>