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:
1
.github/whitelist-capital-letters.txt
vendored
1
.github/whitelist-capital-letters.txt
vendored
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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">!@#$%^&*</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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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"],
|
||||
},
|
||||
);
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
@@ -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()],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
3
libs/components/src/badge/badge.component.html
Normal file
3
libs/components/src/badge/badge.component.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<span [ngClass]="{ 'tw-truncate tw-block': truncate }">
|
||||
<ng-content></ng-content>
|
||||
</span>
|
||||
@@ -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) {
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { BadgeDirective, BadgeVariant } from "./badge.directive";
|
||||
export { BadgeComponent, BadgeVariant } from "./badge.component";
|
||||
export * from "./badge.module";
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user