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-08-26 08:50:15 -07:00
289 changed files with 8268 additions and 1473 deletions

View File

@@ -81,6 +81,11 @@ export class SsoComponent implements OnInit {
const state = await this.ssoLoginService.getSsoState();
await this.ssoLoginService.setCodeVerifier(null);
await this.ssoLoginService.setSsoState(null);
if (qParams.redirectUri != null) {
this.redirectUri = qParams.redirectUri;
}
if (
qParams.code != null &&
codeVerifier != null &&

View File

@@ -8,8 +8,7 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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 { I18nMockService } from "@bitwarden/components/src";
import { I18nMockService, ToastService } from "@bitwarden/components/src";
import { canAccessFeature } from "./feature-flag.guard";
@@ -22,11 +21,11 @@ describe("canAccessFeature", () => {
const redirectRoute = "redirect";
let mockConfigService: MockProxy<ConfigService>;
let mockPlatformUtilsService: MockProxy<PlatformUtilsService>;
let mockToastService: MockProxy<ToastService>;
const setup = (featureGuard: CanActivateFn, flagValue: any) => {
mockConfigService = mock<ConfigService>();
mockPlatformUtilsService = mock<PlatformUtilsService>();
mockToastService = mock<ToastService>();
// Mock the correct getter based on the type of flagValue; also mock default values if one is not provided
if (typeof flagValue === "boolean") {
@@ -57,7 +56,7 @@ describe("canAccessFeature", () => {
],
providers: [
{ provide: ConfigService, useValue: mockConfigService },
{ provide: PlatformUtilsService, useValue: mockPlatformUtilsService },
{ provide: ToastService, useValue: mockToastService },
{ provide: LogService, useValue: mock<LogService>() },
{
provide: I18nService,
@@ -117,11 +116,11 @@ describe("canAccessFeature", () => {
await router.navigate([featureRoute]);
expect(mockPlatformUtilsService.showToast).toHaveBeenCalledWith(
"error",
null,
"Access Denied!",
);
expect(mockToastService.showToast).toHaveBeenCalledWith({
variant: "error",
title: null,
message: "Access Denied!",
});
});
it("does not show an error toast when the feature flag is enabled", async () => {
@@ -129,7 +128,7 @@ describe("canAccessFeature", () => {
await router.navigate([featureRoute]);
expect(mockPlatformUtilsService.showToast).not.toHaveBeenCalled();
expect(mockToastService.showToast).not.toHaveBeenCalled();
});
it("redirects to the specified redirect url when the feature flag is disabled", async () => {

View File

@@ -5,7 +5,7 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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 { ToastService } from "@bitwarden/components";
// Replace this with a type safe lookup of the feature flag values in PM-2282
type FlagValue = boolean | number | string;
@@ -24,7 +24,7 @@ export const canAccessFeature = (
): CanActivateFn => {
return async () => {
const configService = inject(ConfigService);
const platformUtilsService = inject(PlatformUtilsService);
const toastService = inject(ToastService);
const router = inject(Router);
const i18nService = inject(I18nService);
const logService = inject(LogService);
@@ -36,7 +36,11 @@ export const canAccessFeature = (
return true;
}
platformUtilsService.showToast("error", null, i18nService.t("accessDenied"));
toastService.showToast({
variant: "error",
title: null,
message: i18nService.t("accessDenied"),
});
if (redirectUrlOnDisabled != null) {
return router.createUrlTree([redirectUrlOnDisabled]);

View File

@@ -1,10 +1,8 @@
export class UpdateProfileRequest {
name: string;
masterPasswordHint: string;
culture = "en-US"; // deprecated
constructor(name: string, masterPasswordHint: string) {
constructor(name: string) {
this.name = name;
this.masterPasswordHint = masterPasswordHint ? masterPasswordHint : null;
}
}

View File

@@ -10,7 +10,6 @@ export class ProfileResponse extends BaseResponse {
name: string;
email: string;
emailVerified: boolean;
masterPasswordHint: string;
premiumPersonally: boolean;
premiumFromOrganization: boolean;
culture: string;
@@ -32,7 +31,6 @@ export class ProfileResponse extends BaseResponse {
this.name = this.getResponseProperty("Name");
this.email = this.getResponseProperty("Email");
this.emailVerified = this.getResponseProperty("EmailVerified");
this.masterPasswordHint = this.getResponseProperty("MasterPasswordHint");
this.premiumPersonally = this.getResponseProperty("Premium");
this.premiumFromOrganization = this.getResponseProperty("PremiumFromOrganization");
this.culture = this.getResponseProperty("Culture");

View File

@@ -0,0 +1,39 @@
import { Observable, map } from "rxjs";
import { GlobalStateProvider, KeyDefinition, ANIMATION_DISK } from "../state";
export abstract class AnimationControlService {
/**
* The routing animation toggle.
*/
abstract enableRoutingAnimation$: Observable<boolean>;
/**
* A method for updating the state of the animation toggle.
* @param theme The new state.
*/
abstract setEnableRoutingAnimation(state: boolean): Promise<void>;
}
const ROUTING_ANIMATION = new KeyDefinition<boolean>(ANIMATION_DISK, "routing", {
deserializer: (s) => s,
});
export class DefaultAnimationControlService implements AnimationControlService {
private readonly enableRoutingAnimationState = this.globalStateProvider.get(ROUTING_ANIMATION);
enableRoutingAnimation$ = this.enableRoutingAnimationState.state$.pipe(
map((state) => state ?? this.defaultEnableRoutingAnimation),
);
constructor(
private globalStateProvider: GlobalStateProvider,
private defaultEnableRoutingAnimation: boolean = true,
) {}
async setEnableRoutingAnimation(state: boolean): Promise<void> {
await this.enableRoutingAnimationState.update(() => state, {
shouldUpdate: (currentState) => currentState !== state,
});
}
}

View File

@@ -40,6 +40,11 @@ export interface PickCredentialParams {
* Whether or not the user must be verified before completing the operation.
*/
userVerification: boolean;
/**
* Bypass the UI and assume that the user has already interacted with the authenticator.
*/
assumeUserPresence?: boolean;
}
/**

View File

@@ -243,10 +243,12 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
}
let response = { cipherId: cipherOptions[0].id, userVerified: false };
if (this.requiresUserVerificationPrompt(params, cipherOptions)) {
response = await userInterfaceSession.pickCredential({
cipherIds: cipherOptions.map((cipher) => cipher.id),
userVerification: params.requireUserVerification,
assumeUserPresence: params.assumeUserPresence,
});
}

View File

@@ -117,6 +117,7 @@ export const POPUP_VIEW_MEMORY = new StateDefinition("popupView", "memory", {
export const SYNC_DISK = new StateDefinition("sync", "disk", { web: "memory" });
export const THEMING_DISK = new StateDefinition("theming", "disk", { web: "disk-local" });
export const TRANSLATION_DISK = new StateDefinition("translation", "disk", { web: "disk-local" });
export const ANIMATION_DISK = new StateDefinition("animation", "disk");
export const TASK_SCHEDULER_DISK = new StateDefinition("taskScheduler", "disk");
// Secrets Manager

View File

@@ -1,4 +1,4 @@
import { firstValueFrom, map, of, switchMap } from "rxjs";
import { firstValueFrom, map, Observable, of, switchMap } from "rxjs";
import { ApiService } from "../../abstractions/api.service";
import { AccountService } from "../../auth/abstractions/account.service";
@@ -67,6 +67,17 @@ export abstract class CoreSyncService implements SyncService {
return this.stateProvider.getUser(userId, LAST_SYNC_DATE).state$;
}
activeUserLastSync$(): Observable<Date | null> {
return this.accountService.activeAccount$.pipe(
switchMap((a) => {
if (a == null) {
return of(null);
}
return this.lastSync$(a.id);
}),
);
}
async setLastSync(date: Date, userId: UserId): Promise<void> {
await this.stateProvider.getUser(userId, LAST_SYNC_DATE).update(() => date);
}

View File

@@ -34,6 +34,12 @@ export abstract class SyncService {
*/
abstract lastSync$(userId: UserId): Observable<Date | null>;
/**
* Retrieves a stream of the currently active user's last sync date.
* Or null if there is no current active user or the active user has not synced before.
*/
abstract activeUserLastSync$(): Observable<Date | null>;
/**
* Optionally does a full sync operation including going to the server to gather the source
* of truth and set that data to state.

View File

@@ -1 +1,3 @@
export * from "./item.module";
export { BitItemHeight, BitItemHeightClass } from "./item.component";

View File

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

View File

@@ -5,6 +5,20 @@ import { A11yRowDirective } from "../a11y/a11y-row.directive";
import { ItemActionComponent } from "./item-action.component";
/**
* The class used to set the height of a bit item's inner content.
*/
export const BitItemHeightClass = `tw-h-[52px]`;
/**
* The height of a bit item in pixels. Includes any margin, padding, or border. Used by the virtual scroll
* to estimate how many items can be displayed at once and how large the virtual container should be.
* Needs to be updated if the item height or spacing changes.
*
* 52px + 5.25px bottom margin + 1px border = 58.25px
*/
export const BitItemHeight = 58.25; //
@Component({
selector: "bit-item",
standalone: true,

View File

@@ -115,6 +115,7 @@ export const TextOverflow: Story = {
template: /*html*/ `
<bit-item>
<bit-item-content>
<i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i>
Helloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo!
<ng-container slot="secondary">Worlddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd!</ng-container>
</bit-item-content>

View File

@@ -8,12 +8,20 @@
<bit-label>
{{ "website" | i18n }}
</bit-label>
<input readonly bitInput type="text" [value]="login.launchUri" aria-readonly="true" />
<input
readonly
bitInput
type="text"
[value]="login.hostOrUri"
aria-readonly="true"
data-testid="login-website"
/>
<button
bitIconButton="bwi-external-link"
bitSuffix
type="button"
(click)="openWebsite(login.launchUri)"
data-testid="launch-website"
></button>
<button
bitIconButton="bwi-clone"
@@ -23,6 +31,7 @@
[valueLabel]="'website' | i18n"
showToast
[appA11yTitle]="'copyValue' | i18n"
data-testid="copy-website"
></button>
</bit-form-field>
</ng-container>

View File

@@ -2,8 +2,8 @@
<bit-section-header>
<h2 bitTypography="h6">{{ "loginCredentials" | i18n }}</h2>
</bit-section-header>
<bit-card>
<bit-form-field [disableMargin]="!cipher.login.password && !cipher.login.totp">
<bit-card class="[&_bit-form-field:last-of-type]:tw-mb-0">
<bit-form-field *ngIf="cipher.login.username">
<bit-label>
{{ "username" | i18n }}
</bit-label>
@@ -23,10 +23,10 @@
[valueLabel]="'username' | i18n"
showToast
[appA11yTitle]="'copyValue' | i18n"
data-testid="toggle-username"
data-testid="copy-username"
></button>
</bit-form-field>
<bit-form-field [disableMargin]="!cipher.login.totp">
<bit-form-field *ngIf="cipher.login.password">
<bit-label>{{ "password" | i18n }}</bit-label>
<input
readonly
@@ -65,13 +65,27 @@
data-testid="copy-password"
></button>
</bit-form-field>
<ng-container *ngIf="showPasswordCount && passwordRevealed">
<div
*ngIf="showPasswordCount && passwordRevealed"
[ngClass]="{ 'tw-mt-3': !cipher.login.totp }"
>
<bit-color-password
[password]="cipher.login.password"
[showCount]="true"
></bit-color-password>
</ng-container>
<bit-form-field disableMargin *ngIf="cipher.login.totp">
</div>
<bit-form-field *ngIf="cipher.login?.fido2Credentials?.length > 0">
<bit-label>{{ "typePasskey" | i18n }} </bit-label>
<input
readonly
bitInput
type="text"
[value]="fido2CredentialCreationDateValue"
aria-readonly="true"
data-testid="login-passkey"
/>
</bit-form-field>
<bit-form-field *ngIf="cipher.login.totp">
<bit-label
>{{ "verificationCodeTotp" | i18n }}
<span

View File

@@ -1,10 +1,11 @@
import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core";
import { CommonModule, DatePipe } from "@angular/common";
import { Component, inject, Input } from "@angular/core";
import { Router } from "@angular/router";
import { Observable, shareReplay } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
CardComponent,
@@ -47,12 +48,23 @@ export class LoginCredentialsViewComponent {
showPasswordCount: boolean = false;
passwordRevealed: boolean = false;
totpCopyCode: string;
private datePipe = inject(DatePipe);
constructor(
private billingAccountProfileStateService: BillingAccountProfileStateService,
private router: Router,
private i18nService: I18nService,
) {}
get fido2CredentialCreationDateValue(): string {
const dateCreated = this.i18nService.t("dateCreated");
const creationDate = this.datePipe.transform(
this.cipher.login.fido2Credentials[0]?.creationDate,
"short",
);
return `${dateCreated} ${creationDate}`;
}
async getPremium() {
await this.router.navigate(["/premium"]);
}