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:
@@ -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 &&
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export * from "./item.module";
|
||||
|
||||
export { BitItemHeight, BitItemHeightClass } from "./item.component";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user