1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 08:13:42 +00:00

Merge branch 'main' into auth/pm-8111/browser-refresh-login-component

This commit is contained in:
rr-bw
2024-09-09 15:28:03 -07:00
218 changed files with 3051 additions and 2413 deletions

View File

@@ -127,9 +127,13 @@ import {
BillingApiServiceAbstraction,
OrganizationBillingServiceAbstraction,
} from "@bitwarden/common/billing/abstractions";
import { AccountBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/account/account-billing-api.service.abstraction";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { OrganizationBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-billing-api.service.abstraction";
import { AccountBillingApiService } from "@bitwarden/common/billing/services/account/account-billing-api.service";
import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service";
import { BillingApiService } from "@bitwarden/common/billing/services/billing-api.service";
import { OrganizationBillingApiService } from "@bitwarden/common/billing/services/organization/organization-billing-api.service";
import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
@@ -245,6 +249,10 @@ import { FolderService } from "@bitwarden/common/vault/services/folder/folder.se
import { TotpService } from "@bitwarden/common/vault/services/totp.service";
import { VaultSettingsService } from "@bitwarden/common/vault/services/vault-settings/vault-settings.service";
import { ToastService } from "@bitwarden/components";
import {
GeneratorHistoryService,
LocalGeneratorHistoryService,
} from "@bitwarden/generator-history";
import {
legacyPasswordGenerationServiceFactory,
legacyUsernameGenerationServiceFactory,
@@ -594,6 +602,11 @@ const safeProviders: SafeProvider[] = [
StateProvider,
],
}),
safeProvider({
provide: GeneratorHistoryService,
useClass: LocalGeneratorHistoryService,
deps: [EncryptService, CryptoServiceAbstraction, StateProvider],
}),
safeProvider({
provide: UsernameGenerationServiceAbstraction,
useFactory: legacyUsernameGenerationServiceFactory,
@@ -978,6 +991,16 @@ const safeProviders: SafeProvider[] = [
// subscribes to sync notifications and will update itself based on that.
deps: [ApiServiceAbstraction, SyncService],
}),
safeProvider({
provide: OrganizationBillingApiServiceAbstraction,
useClass: OrganizationBillingApiService,
deps: [ApiServiceAbstraction],
}),
safeProvider({
provide: AccountBillingApiServiceAbstraction,
useClass: AccountBillingApiService,
deps: [ApiServiceAbstraction],
}),
safeProvider({
provide: DefaultConfigService,
useClass: DefaultConfigService,

View File

@@ -13,9 +13,16 @@
<bit-icon [icon]="icon"></bit-icon>
</div>
<h1 *ngIf="title" bitTypography="h3" class="tw-mt-2 sm:tw-text-2xl">
{{ title }}
</h1>
<ng-container *ngIf="title">
<!-- Small screens -->
<h1 bitTypography="h3" class="tw-mt-2 sm:tw-hidden">
{{ title }}
</h1>
<!-- Medium to Larger screens -->
<h1 bitTypography="h2" class="tw-mt-2 tw-hidden sm:tw-block">
{{ title }}
</h1>
</ng-container>
<div *ngIf="subtitle" class="tw-text-sm sm:tw-text-base">{{ subtitle }}</div>
</div>

View File

@@ -0,0 +1,81 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { BehaviorSubject } from "rxjs";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
import { VaultTimeoutInputComponent } from "./vault-timeout-input.component";
describe("VaultTimeoutInputComponent", () => {
let component: VaultTimeoutInputComponent;
let fixture: ComponentFixture<VaultTimeoutInputComponent>;
const get$ = jest.fn().mockReturnValue(new BehaviorSubject({}));
const availableVaultTimeoutActions$ = jest.fn().mockReturnValue(new BehaviorSubject([]));
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [VaultTimeoutInputComponent],
providers: [
{ provide: PolicyService, useValue: { get$ } },
{ provide: VaultTimeoutSettingsService, useValue: { availableVaultTimeoutActions$ } },
{ provide: I18nService, useValue: { t: (key: string) => key } },
],
}).compileComponents();
fixture = TestBed.createComponent(VaultTimeoutInputComponent);
component = fixture.componentInstance;
component.vaultTimeoutOptions = [
{ name: "oneMinute", value: 1 },
{ name: "fiveMinutes", value: 5 },
{ name: "fifteenMinutes", value: 15 },
{ name: "thirtyMinutes", value: 30 },
{ name: "oneHour", value: 60 },
{ name: "fourHours", value: 240 },
{ name: "onRefresh", value: VaultTimeoutStringType.OnRestart },
];
fixture.detectChanges();
});
describe("form", () => {
beforeEach(async () => {
await component.ngOnInit();
});
it("invokes the onChange associated with `ControlValueAccessor`", () => {
const onChange = jest.fn();
component.registerOnChange(onChange);
component.form.controls.vaultTimeout.setValue(VaultTimeoutStringType.OnRestart);
expect(onChange).toHaveBeenCalledWith(VaultTimeoutStringType.OnRestart);
});
it("updates custom value to match preset option", () => {
// 1 hour
component.form.controls.vaultTimeout.setValue(60);
expect(component.form.value.custom).toEqual({ hours: 1, minutes: 0 });
// 17 minutes
component.form.controls.vaultTimeout.setValue(17);
expect(component.form.value.custom).toEqual({ hours: 0, minutes: 17 });
// 2.25 hours
component.form.controls.vaultTimeout.setValue(135);
expect(component.form.value.custom).toEqual({ hours: 2, minutes: 15 });
});
it("sets custom timeout to 0 when a preset string option is selected", () => {
// Set custom value to random values
component.form.controls.custom.setValue({ hours: 1, minutes: 1 });
component.form.controls.vaultTimeout.setValue(VaultTimeoutStringType.OnLocked);
expect(component.form.value.custom).toEqual({ hours: 0, minutes: 0 });
});
});
});

View File

@@ -4,6 +4,8 @@ import {
AbstractControl,
ControlValueAccessor,
FormBuilder,
FormControl,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
@@ -22,13 +24,15 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { VaultTimeout, VaultTimeoutOption } from "@bitwarden/common/types/vault-timeout.type";
import { FormFieldModule, SelectModule } from "@bitwarden/components";
interface VaultTimeoutFormValue {
vaultTimeout: VaultTimeout | null;
custom: {
hours: number | null;
minutes: number | null;
};
}
type VaultTimeoutForm = FormGroup<{
vaultTimeout: FormControl<VaultTimeout | null>;
custom: FormGroup<{
hours: FormControl<number | null>;
minutes: FormControl<number | null>;
}>;
}>;
type VaultTimeoutFormValue = VaultTimeoutForm["value"];
@Component({
selector: "auth-vault-timeout-input",
@@ -64,7 +68,7 @@ export class VaultTimeoutInputComponent
static CUSTOM_VALUE = -100;
static MIN_CUSTOM_MINUTES = 0;
form = this.formBuilder.group({
form: VaultTimeoutForm = this.formBuilder.group({
vaultTimeout: [null],
custom: this.formBuilder.group({
hours: [null],
@@ -120,7 +124,7 @@ export class VaultTimeoutInputComponent
takeUntil(this.destroy$),
)
.subscribe((value) => {
const current = Math.max(value, 0);
const current = typeof value === "string" ? 0 : Math.max(value, 0);
// This cannot emit an event b/c it would cause form.valueChanges to fire again
// and we are already handling that above so just silently update

View File

@@ -0,0 +1,12 @@
import {
BillingInvoiceResponse,
BillingTransactionResponse,
} from "@bitwarden/common/billing/models/response/billing.response";
export class AccountBillingApiServiceAbstraction {
getBillingInvoices: (id: string, startAfter?: string) => Promise<BillingInvoiceResponse[]>;
getBillingTransactions: (
id: string,
startAfter?: string,
) => Promise<BillingTransactionResponse[]>;
}

View File

@@ -0,0 +1,12 @@
import {
BillingInvoiceResponse,
BillingTransactionResponse,
} from "@bitwarden/common/billing/models/response/billing.response";
export class OrganizationBillingApiServiceAbstraction {
getBillingInvoices: (id: string, startAfter?: string) => Promise<BillingInvoiceResponse[]>;
getBillingTransactions: (
id: string,
startAfter?: string,
) => Promise<BillingTransactionResponse[]>;
}

View File

@@ -29,6 +29,7 @@ export class BillingSourceResponse extends BaseResponse {
}
export class BillingInvoiceResponse extends BaseResponse {
id: string;
url: string;
pdfUrl: string;
number: string;
@@ -38,6 +39,7 @@ export class BillingInvoiceResponse extends BaseResponse {
constructor(response: any) {
super(response);
this.id = this.getResponseProperty("Id");
this.url = this.getResponseProperty("Url");
this.pdfUrl = this.getResponseProperty("PdfUrl");
this.number = this.getResponseProperty("Number");

View File

@@ -0,0 +1,34 @@
import { ApiService } from "../../../abstractions/api.service";
import { AccountBillingApiServiceAbstraction } from "../../abstractions/account/account-billing-api.service.abstraction";
import {
BillingInvoiceResponse,
BillingTransactionResponse,
} from "../../models/response/billing.response";
export class AccountBillingApiService implements AccountBillingApiServiceAbstraction {
constructor(private apiService: ApiService) {}
async getBillingInvoices(startAfter?: string): Promise<BillingInvoiceResponse[]> {
const queryParams = startAfter ? `?startAfter=${startAfter}` : "";
const r = await this.apiService.send(
"GET",
`/accounts/billing/invoices${queryParams}`,
null,
true,
true,
);
return r?.map((i: any) => new BillingInvoiceResponse(i)) || [];
}
async getBillingTransactions(startAfter?: string): Promise<BillingTransactionResponse[]> {
const queryParams = startAfter ? `?startAfter=${startAfter}` : "";
const r = await this.apiService.send(
"GET",
`/accounts/billing/transactions${queryParams}`,
null,
true,
true,
);
return r?.map((i: any) => new BillingTransactionResponse(i)) || [];
}
}

View File

@@ -0,0 +1,37 @@
import { ApiService } from "../../../abstractions/api.service";
import { OrganizationBillingApiServiceAbstraction } from "../../abstractions/organizations/organization-billing-api.service.abstraction";
import {
BillingInvoiceResponse,
BillingTransactionResponse,
} from "../../models/response/billing.response";
export class OrganizationBillingApiService implements OrganizationBillingApiServiceAbstraction {
constructor(private apiService: ApiService) {}
async getBillingInvoices(id: string, startAfter?: string): Promise<BillingInvoiceResponse[]> {
const queryParams = startAfter ? `?startAfter=${startAfter}` : "";
const r = await this.apiService.send(
"GET",
`/organizations/${id}/billing/invoices${queryParams}`,
null,
true,
true,
);
return r?.map((i: any) => new BillingInvoiceResponse(i)) || [];
}
async getBillingTransactions(
id: string,
startAfter?: string,
): Promise<BillingTransactionResponse[]> {
const queryParams = startAfter ? `?startAfter=${startAfter}` : "";
const r = await this.apiService.send(
"GET",
`/organizations/${id}/billing/transactions${queryParams}`,
null,
true,
true,
);
return r?.map((i: any) => new BillingTransactionResponse(i)) || [];
}
}

View File

@@ -2,9 +2,22 @@ import { Observable, Subject } from "rxjs";
import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view";
export const Fido2ActiveRequestEvents = {
Refresh: "refresh-fido2-active-request",
Abort: "abort-fido2-active-request",
Continue: "continue-fido2-active-request",
} as const;
type Fido2ActiveRequestEvent = typeof Fido2ActiveRequestEvents;
export type RequestResult =
| { type: Fido2ActiveRequestEvent["Refresh"] }
| { type: Fido2ActiveRequestEvent["Abort"] }
| { type: Fido2ActiveRequestEvent["Continue"]; credentialId: string };
export interface ActiveRequest {
credentials: Fido2CredentialView[];
subject: Subject<string>;
subject: Subject<RequestResult>;
}
export type RequestCollection = Readonly<{ [tabId: number]: ActiveRequest }>;
@@ -16,6 +29,7 @@ export abstract class Fido2ActiveRequestManager {
tabId: number,
credentials: Fido2CredentialView[],
abortController: AbortController,
) => Promise<string>;
) => Promise<RequestResult>;
removeActiveRequest: (tabId: number) => void;
removeAllActiveRequests: () => void;
}

View File

@@ -1,7 +1,3 @@
import { Observable } from "rxjs";
import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view";
export const UserRequestedFallbackAbortReason = "UserRequestedFallback";
export type UserVerification = "discouraged" | "preferred" | "required";
@@ -20,10 +16,6 @@ export type UserVerification = "discouraged" | "preferred" | "required";
export abstract class Fido2ClientService {
isFido2FeatureEnabled: (hostname: string, origin: string) => Promise<boolean>;
availableAutofillCredentials$: (tabId: number) => Observable<Fido2CredentialView[]>;
autofillCredential: (tabId: number, credentialId: string) => Promise<void>;
/**
* Allows WebAuthn Relying Party scripts to request the creation of a new public key credential source.
* For more information please see: https://www.w3.org/TR/webauthn-3/#sctn-createCredential

View File

@@ -45,6 +45,11 @@ export interface PickCredentialParams {
* Bypass the UI and assume that the user has already interacted with the authenticator.
*/
assumeUserPresence?: boolean;
/**
* Identifies whether a cipher requires a master password reprompt when getting a credential.
*/
masterPasswordRepromptRequired?: boolean;
}
/**

View File

@@ -14,6 +14,8 @@ import {
ActiveRequest,
RequestCollection,
Fido2ActiveRequestManager as Fido2ActiveRequestManagerAbstraction,
Fido2ActiveRequestEvents,
RequestResult,
} from "../../abstractions/fido2/fido2-active-request-manager.abstraction";
export class Fido2ActiveRequestManager implements Fido2ActiveRequestManagerAbstraction {
@@ -53,7 +55,7 @@ export class Fido2ActiveRequestManager implements Fido2ActiveRequestManagerAbstr
tabId: number,
credentials: Fido2CredentialView[],
abortController: AbortController,
): Promise<string> {
): Promise<RequestResult> {
const newRequest: ActiveRequest = {
credentials,
subject: new Subject(),
@@ -65,10 +67,10 @@ export class Fido2ActiveRequestManager implements Fido2ActiveRequestManagerAbstr
const abortListener = () => this.abortActiveRequest(tabId);
abortController.signal.addEventListener("abort", abortListener);
const credentialId = firstValueFrom(newRequest.subject);
const requestResult = firstValueFrom(newRequest.subject);
abortController.signal.removeEventListener("abort", abortListener);
return credentialId;
return requestResult;
}
/**
@@ -85,12 +87,23 @@ export class Fido2ActiveRequestManager implements Fido2ActiveRequestManagerAbstr
});
}
/**
* Removes and aborts all active requests.
*/
removeAllActiveRequests() {
Object.keys(this.activeRequests$.value).forEach((tabId) => {
this.abortActiveRequest(Number(tabId));
});
this.updateRequests(() => ({}));
}
/**
* Aborts the active request associated with a given tab id.
*
* @param tabId - The tab id to abort the active request for.
*/
private abortActiveRequest(tabId: number): void {
this.activeRequests$.value[tabId]?.subject.next({ type: Fido2ActiveRequestEvents.Abort });
this.activeRequests$.value[tabId]?.subject.error(
new DOMException("The operation either timed out or was not allowed.", "AbortError"),
);

View File

@@ -576,6 +576,7 @@ describe("FidoAuthenticatorService", () => {
expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({
cipherIds: ciphers.map((c) => c.id),
userVerification: false,
masterPasswordRepromptRequired: false,
});
});
@@ -592,6 +593,7 @@ describe("FidoAuthenticatorService", () => {
expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({
cipherIds: [discoverableCiphers[0].id],
userVerification: false,
masterPasswordRepromptRequired: false,
});
});
@@ -609,6 +611,7 @@ describe("FidoAuthenticatorService", () => {
expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({
cipherIds: ciphers.map((c) => c.id),
userVerification,
masterPasswordRepromptRequired: false,
});
});
}

View File

@@ -160,6 +160,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
}
const reencrypted = await this.cipherService.encrypt(cipher, activeUserId);
await this.cipherService.updateWithServer(reencrypted);
await this.cipherService.clearCache(activeUserId);
credentialId = fido2Credential.credentialId;
} catch (error) {
this.logService?.error(
@@ -243,12 +244,18 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
}
let response = { cipherId: cipherOptions[0].id, userVerified: false };
const masterPasswordRepromptRequired = cipherOptions.some(
(cipher) => cipher.reprompt !== CipherRepromptType.None,
);
if (this.requiresUserVerificationPrompt(params, cipherOptions)) {
if (
this.requiresUserVerificationPrompt(params, cipherOptions, masterPasswordRepromptRequired)
) {
response = await userInterfaceSession.pickCredential({
cipherIds: cipherOptions.map((cipher) => cipher.id),
userVerification: params.requireUserVerification,
assumeUserPresence: params.assumeUserPresence,
masterPasswordRepromptRequired,
});
}
@@ -292,6 +299,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
);
const encrypted = await this.cipherService.encrypt(selectedCipher, activeUserId);
await this.cipherService.updateWithServer(encrypted);
await this.cipherService.clearCache(activeUserId);
}
const authenticatorData = await generateAuthData({
@@ -330,13 +338,14 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
private requiresUserVerificationPrompt(
params: Fido2AuthenticatorGetAssertionParams,
cipherOptions: CipherView[],
masterPasswordRepromptRequired: boolean,
): boolean {
return (
params.requireUserVerification ||
!params.assumeUserPresence ||
cipherOptions.length > 1 ||
cipherOptions.length === 0 ||
cipherOptions.some((cipher) => cipher.reprompt !== CipherRepromptType.None)
masterPasswordRepromptRequired
);
}

View File

@@ -6,11 +6,12 @@ import { AuthenticationStatus } from "../../../auth/enums/authentication-status"
import { DomainSettingsService } from "../../../autofill/services/domain-settings.service";
import { Utils } from "../../../platform/misc/utils";
import { VaultSettingsService } from "../../../vault/abstractions/vault-settings/vault-settings.service";
import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view";
import { ConfigService } from "../../abstractions/config/config.service";
import {
ActiveRequest,
Fido2ActiveRequestEvents,
Fido2ActiveRequestManager,
RequestResult,
} from "../../abstractions/fido2/fido2-active-request-manager.abstraction";
import {
Fido2AuthenticatorError,
@@ -56,7 +57,10 @@ describe("FidoAuthenticatorService", () => {
domainSettingsService = mock<DomainSettingsService>();
taskSchedulerService = mock<TaskSchedulerService>();
activeRequest = mock<ActiveRequest>({
subject: new BehaviorSubject<string>(""),
subject: new BehaviorSubject<RequestResult>({
type: Fido2ActiveRequestEvents.Continue,
credentialId: "",
}),
});
requestManager = mock<Fido2ActiveRequestManager>({
getActiveRequest$: (tabId: number) => new BehaviorSubject(activeRequest),
@@ -615,7 +619,10 @@ describe("FidoAuthenticatorService", () => {
});
beforeEach(() => {
requestManager.newActiveRequest.mockResolvedValue(crypto.randomUUID());
requestManager.newActiveRequest.mockResolvedValue({
type: Fido2ActiveRequestEvents.Continue,
credentialId: crypto.randomUUID(),
});
authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult());
});
@@ -676,28 +683,6 @@ describe("FidoAuthenticatorService", () => {
};
}
});
describe("autofill of credentials through the active request manager", () => {
it("returns an observable that updates with an array of the credentials for active Fido2 requests", async () => {
const activeRequestCredentials = mock<Fido2CredentialView>();
activeRequest.credentials = [activeRequestCredentials];
const observable = client.availableAutofillCredentials$(tab.id);
observable.subscribe((credentials) => {
expect(credentials).toEqual([activeRequestCredentials]);
});
});
it("triggers the logic of the next behavior subject of an active request", async () => {
const activeRequestCredentials = mock<Fido2CredentialView>();
activeRequest.credentials = [activeRequestCredentials];
jest.spyOn(activeRequest.subject, "next");
await client.autofillCredential(tab.id, activeRequestCredentials.credentialId);
expect(activeRequest.subject.next).toHaveBeenCalled();
});
});
});
/** This is a fake function that always returns the same byte sequence */

View File

@@ -1,13 +1,15 @@
import { firstValueFrom, map, Observable, Subscription } from "rxjs";
import { firstValueFrom, Subscription } from "rxjs";
import { parse } from "tldts";
import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { DomainSettingsService } from "../../../autofill/services/domain-settings.service";
import { VaultSettingsService } from "../../../vault/abstractions/vault-settings/vault-settings.service";
import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view";
import { ConfigService } from "../../abstractions/config/config.service";
import { Fido2ActiveRequestManager } from "../../abstractions/fido2/fido2-active-request-manager.abstraction";
import {
Fido2ActiveRequestEvents,
Fido2ActiveRequestManager,
} from "../../abstractions/fido2/fido2-active-request-manager.abstraction";
import {
Fido2AuthenticatorError,
Fido2AuthenticatorErrorCode,
@@ -73,17 +75,6 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
);
}
availableAutofillCredentials$(tabId: number): Observable<Fido2CredentialView[]> {
return this.requestManager
.getActiveRequest$(tabId)
.pipe(map((request) => request?.credentials ?? []));
}
async autofillCredential(tabId: number, credentialId: string) {
const request = this.requestManager.getActiveRequest(tabId);
request.subject.next(credentialId);
}
async isFido2FeatureEnabled(hostname: string, origin: string): Promise<boolean> {
const isUserLoggedIn =
(await this.authService.getAuthStatus()) !== AuthenticationStatus.LoggedOut;
@@ -385,12 +376,23 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
this.logService?.info(
`[Fido2Client] started mediated request, available credentials: ${availableCredentials.length}`,
);
const credentialId = await this.requestManager.newActiveRequest(
const requestResult = await this.requestManager.newActiveRequest(
tab.id,
availableCredentials,
abortController,
);
params.allowedCredentialIds = [Fido2Utils.bufferToString(guidToRawFormat(credentialId))];
if (requestResult.type === Fido2ActiveRequestEvents.Refresh) {
continue;
}
if (requestResult.type === Fido2ActiveRequestEvents.Abort) {
break;
}
params.allowedCredentialIds = [
Fido2Utils.bufferToString(guidToRawFormat(requestResult.credentialId)),
];
assumeUserPresence = true;
const clientDataHash = await crypto.subtle.digest({ name: "SHA-256" }, clientDataJSONBytes);

View File

@@ -16,10 +16,11 @@ export class UserAutoUnlockKeyService {
* However, for users that have the auto unlock user key set, we need to set the user key in memory
* on application bootstrap and on active account changes so that the user's vault loads unlocked.
* @param userId - The user id to check for an auto user key.
* @returns True if the auto user key is set successfully, false otherwise.
*/
async setUserKeyInMemoryIfAutoUserKeySet(userId: UserId): Promise<void> {
async setUserKeyInMemoryIfAutoUserKeySet(userId: UserId): Promise<boolean> {
if (userId == null) {
return;
return false;
}
const autoUserKey = await this.cryptoService.getUserKeyFromStorage(
@@ -28,9 +29,10 @@ export class UserAutoUnlockKeyService {
);
if (autoUserKey == null) {
return;
return false;
}
await this.cryptoService.setUserKey(autoUserKey, userId);
return true;
}
}

View File

@@ -1,4 +1,4 @@
<div class="tw-flex tw-gap-2 tw-items-center tw-w-full">
<div class="tw-flex tw-gap-2 tw-items-center tw-w-full tw-min-w-0">
<ng-content select="[slot=start]"></ng-content>
<div class="tw-flex tw-flex-col tw-items-start tw-text-start tw-w-full tw-truncate [&_p]:tw-mb-0">

View File

@@ -0,0 +1,20 @@
<bit-card *ngFor="let credential of credentials$ | async" class="tw-mb-2">
<div class="tw-flex tw-justify-between tw-items-center">
<div class="tw-flex tw-flex-col">
<h2 bitTypography="h6">{{ credential.credential }}</h2>
<span class="tw-text-muted" bitTypography="body1">{{
credential.generationDate | date: "medium"
}}</span>
</div>
<button
bitIconButton="bwi-clone"
bitSuffix
size="small"
type="button"
[appCopyClick]="credential.credential"
[ariaLabel]="'copyPassword' | i18n"
[appA11yTitle]="'copyPassword' | i18n"
showToast
></button>
</div>
</bit-card>

View File

@@ -0,0 +1,58 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { RouterLink } from "@angular/router";
import { BehaviorSubject, distinctUntilChanged, map, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserId } from "@bitwarden/common/types/guid";
import {
CardComponent,
IconButtonModule,
NoItemsModule,
SectionComponent,
SectionHeaderComponent,
} from "@bitwarden/components";
import { GeneratedCredential, GeneratorHistoryService } from "@bitwarden/generator-history";
@Component({
standalone: true,
selector: "bit-credential-generator-history",
templateUrl: "credential-generator-history.component.html",
imports: [
CommonModule,
IconButtonModule,
NoItemsModule,
JslibModule,
RouterLink,
CardComponent,
SectionComponent,
SectionHeaderComponent,
],
})
export class CredentialGeneratorHistoryComponent {
protected readonly userId$ = new BehaviorSubject<UserId>(null);
protected readonly credentials$ = new BehaviorSubject<GeneratedCredential[]>([]);
constructor(
private accountService: AccountService,
private history: GeneratorHistoryService,
) {
this.accountService.activeAccount$
.pipe(
takeUntilDestroyed(),
map(({ id }) => id),
distinctUntilChanged(),
)
.subscribe(this.userId$);
this.userId$
.pipe(
takeUntilDestroyed(),
switchMap((id) => id && this.history.credentials$(id)),
map((credentials) => credentials),
)
.subscribe(this.credentials$);
}
}

View File

@@ -0,0 +1,7 @@
<div class="tw-flex tw-flex-col tw-h-full tw-justify-center">
<div class="tw-text-center">
<bit-icon [icon]="noCredentialsIcon" aria-hidden="true"></bit-icon>
<h2 bitTypography="h4" class="tw-mt-3">{{ "noPasswordsToShow" | i18n }}</h2>
<div>{{ "noRecentlyGeneratedPassword" | i18n }}</div>
</div>
</div>

View File

@@ -0,0 +1,18 @@
import { Component } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { IconModule, TypographyModule } from "@bitwarden/components";
import { NoCredentialsIcon } from "./icons/no-credentials.icon";
@Component({
standalone: true,
selector: "bit-empty-credential-history",
templateUrl: "empty-credential-history.component.html",
imports: [JslibModule, IconModule, TypographyModule],
})
export class EmptyCredentialHistoryComponent {
noCredentialsIcon = NoCredentialsIcon;
constructor() {}
}

View File

@@ -0,0 +1,27 @@
import { svgIcon } from "@bitwarden/components";
export const NoCredentialsIcon = svgIcon`
<svg xmlns="http://www.w3.org/2000/svg" width="119" height="114" viewBox="0 0 119 114" fill="none">
<g clip-path="url(#clip0_201_7924)">
<path fill="#020F66" fill-rule="evenodd" clip-rule="evenodd" d="M35.2098 52.2486C35.9068 52.2486 36.4719 52.8137 36.4719 53.5107V58.2685C36.4719 58.9655 35.9068 59.5306 35.2098 59.5306C34.5128 59.5306 33.9478 58.9655 33.9478 58.2685V53.5107C33.9478 52.8137 34.5128 52.2486 35.2098 52.2486Z" />
<path fill="#020F66" fill-rule="evenodd" clip-rule="evenodd" d="M40.9963 56.4125C41.2091 57.0762 40.8437 57.7868 40.18 57.9997L35.5951 59.4703C34.9314 59.6832 34.2208 59.3177 34.0079 58.654C33.795 57.9903 34.1605 57.2797 34.8242 57.0668L39.409 55.5962C40.0727 55.3833 40.7834 55.7487 40.9963 56.4125Z" />
<path fill="#020F66" fill-rule="evenodd" clip-rule="evenodd" d="M34.471 57.2455C35.036 56.8374 35.8249 56.9647 36.233 57.5297L39.0445 61.4225C39.4526 61.9876 39.3254 62.7765 38.7603 63.1846C38.1952 63.5927 37.4063 63.4654 36.9982 62.9004L34.1868 59.0076C33.7787 58.4425 33.9059 57.6536 34.471 57.2455Z" />
<path fill="#020F66" fill-rule="evenodd" clip-rule="evenodd" d="M35.94 57.2401C36.508 57.6441 36.6411 58.432 36.2371 59.0001L33.4689 62.8928C33.065 63.4609 32.277 63.5939 31.709 63.19C31.141 62.786 31.0079 61.9981 31.4119 61.43L34.1801 57.5373C34.584 56.9692 35.3719 56.8362 35.94 57.2401Z" />
<path fill="#020F66" fill-rule="evenodd" clip-rule="evenodd" d="M29.4665 56.4091C29.6812 55.746 30.3929 55.3825 31.056 55.5972L35.5976 57.0679C36.2607 57.2826 36.6242 57.9942 36.4095 58.6573C36.1947 59.3205 35.4831 59.684 34.82 59.4692L30.2784 57.9986C29.6153 57.7839 29.2518 57.0723 29.4665 56.4091Z" />
<path fill="#020F66" fill-rule="evenodd" clip-rule="evenodd" d="M50.6932 52.2487C51.3902 52.2487 51.9553 52.8137 51.9553 53.5107V58.2686C51.9553 58.9656 51.3902 59.5306 50.6932 59.5306C49.9962 59.5306 49.4312 58.9656 49.4312 58.2686V53.5107C49.4312 52.8137 49.9962 52.2487 50.6932 52.2487Z" />
<path fill="#020F66" fill-rule="evenodd" clip-rule="evenodd" d="M56.4353 56.4088C56.6501 57.072 56.2866 57.7836 55.6234 57.9983L51.0819 59.4689C50.4187 59.6837 49.7071 59.3202 49.4924 58.657C49.2777 57.9939 49.6412 57.2823 50.3043 57.0676L54.8458 55.5969C55.509 55.3822 56.2206 55.7457 56.4353 56.4088Z" />
<path fill="#020F66" fill-rule="evenodd" clip-rule="evenodd" d="M49.9544 57.2452C50.5194 56.8371 51.3083 56.9643 51.7164 57.5294L54.5279 61.4221C54.936 61.9872 54.8087 62.7761 54.2437 63.1842C53.6786 63.5923 52.8897 63.4651 52.4816 62.9L49.6702 59.0072C49.2621 58.4422 49.3893 57.6533 49.9544 57.2452Z" />
<path fill="#020F66" fill-rule="evenodd" clip-rule="evenodd" d="M51.4331 57.2452C51.9982 57.6533 52.1254 58.4422 51.7173 59.0072L48.9059 62.9C48.4978 63.4651 47.7089 63.5923 47.1438 63.1842C46.5788 62.7761 46.4515 61.9872 46.8596 61.4221L49.6711 57.5294C50.0792 56.9643 50.8681 56.8371 51.4331 57.2452Z" />
<path fill="#020F66" fill-rule="evenodd" clip-rule="evenodd" d="M44.9514 56.4088C45.1661 55.7457 45.8777 55.3822 46.5409 55.5969L51.0824 57.0676C51.7455 57.2823 52.109 57.9939 51.8943 58.657C51.6796 59.3202 50.968 59.6837 50.3048 59.4689L45.7633 57.9983C45.1001 57.7836 44.7366 57.072 44.9514 56.4088Z" />
<path fill="#020F66" fill-rule="evenodd" clip-rule="evenodd" d="M60.5229 62.3772C60.5229 61.6802 61.088 61.1151 61.785 61.1151H70.7935C71.4905 61.1151 72.0556 61.6802 72.0556 62.3772C72.0556 63.0742 71.4905 63.6392 70.7935 63.6392H61.785C61.088 63.6392 60.5229 63.0742 60.5229 62.3772Z" />
<path fill="#020F66" fill-rule="evenodd" clip-rule="evenodd" d="M75.9663 62.3772C75.9663 61.6802 76.5314 61.1151 77.2284 61.1151H86.2369C86.9339 61.1151 87.4989 61.6802 87.4989 62.3772C87.4989 63.0742 86.9339 63.6392 86.2369 63.6392H77.2284C76.5314 63.6392 75.9663 63.0742 75.9663 62.3772Z" />
<path fill="#15C0CB" fill-rule="evenodd" clip-rule="evenodd" d="M20.1396 57.9313C20.1396 50.6126 26.0726 44.6796 33.3914 44.6796H86.3982C93.7169 44.6796 99.6499 50.6126 99.6499 57.9313C99.6499 65.25 93.7169 71.183 86.3982 71.183H33.3914C26.0726 71.183 20.1396 65.25 20.1396 57.9313ZM33.3914 47.2037C27.4667 47.2037 22.6638 52.0066 22.6638 57.9313C22.6638 63.856 27.4667 68.6589 33.3914 68.6589H86.3982C92.3229 68.6589 97.1258 63.856 97.1258 57.9313C97.1258 52.0066 92.3229 47.2037 86.3982 47.2037H33.3914Z"/>
<path fill="#020F66" fill-rule="evenodd" clip-rule="evenodd" d="M40.8279 11.8469C41.4764 12.1023 41.7952 12.835 41.5398 13.4836L37.3784 24.0525C37.123 24.701 36.3902 25.0198 35.7417 24.7644C35.0931 24.509 34.7744 23.7762 35.0298 23.1277L38.0204 15.5323C35.2016 16.9889 32.4865 18.7508 29.92 20.8232C9.44808 37.3546 6.25361 67.3517 22.785 87.8236C27.3496 93.4763 32.9382 97.8098 39.0683 100.775C39.6957 101.079 39.9583 101.834 39.6547 102.461C39.3512 103.089 38.5964 103.351 37.969 103.048C31.5107 99.9231 25.6247 95.3579 20.8212 89.4094C3.414 67.8529 6.77771 36.2666 28.3342 18.8594C31.1318 16.6003 34.0994 14.6905 37.1838 13.1248L29.3343 10.0341C28.6857 9.77875 28.367 9.04598 28.6223 8.39742C28.8777 7.74886 29.6105 7.43012 30.259 7.68548L40.8279 11.8469ZM84.1129 15.392C84.4739 14.7958 85.2499 14.6051 85.8462 14.9661C90.6935 17.901 95.1212 21.7125 98.8842 26.3725C116.291 47.929 112.928 79.5153 91.3711 96.9224C90.3117 97.7779 89.2278 98.5834 88.1224 99.339L96.3064 101.382C96.9827 101.551 97.394 102.236 97.2252 102.912C97.0564 103.588 96.3713 104 95.6951 103.831L84.6746 101.08C83.9984 100.911 83.587 100.226 83.7558 99.5498L86.5067 88.5294C86.6755 87.8531 87.3606 87.4417 88.0368 87.6105C88.7131 87.7794 89.1245 88.4644 88.9557 89.1407L86.9784 97.0621C87.9316 96.4005 88.8679 95.6994 89.7853 94.9586C110.257 78.4273 113.452 48.4302 96.9203 27.9583C93.3439 23.5293 89.1393 19.9108 84.5388 17.1253C83.9426 16.7643 83.7519 15.9883 84.1129 15.392Z" />
</g>
<defs>
<clipPath id="clip0_201_7924">
<rect width="119" height="114" fill="white"/>
</clipPath>
</defs>
</svg>
`;

View File

@@ -1,2 +1,4 @@
export { PassphraseSettingsComponent } from "./passphrase-settings.component";
export { CredentialGeneratorHistoryComponent } from "./credential-generator-history.component";
export { EmptyCredentialHistoryComponent } from "./empty-credential-history.component";
export { PasswordSettingsComponent } from "./password-settings.component";

View File

@@ -46,7 +46,7 @@
<bit-label> {{ field.name }} </bit-label>
</bit-form-control>
<bit-form-field *ngIf="field.type === fieldType.Linked" [disableReadOnlyBorder]="last">
<bit-label> {{ "linked" | i18n }}: {{ field.name }} </bit-label>
<bit-label> {{ "cfTypeLinked" | i18n }}: {{ field.name }} </bit-label>
<input
readonly
bitInput