1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-27 01:53:23 +00:00

Merge remote-tracking branch 'origin/PM-14892-Sales-Tax-Estimation-For-Clients' into PM-14892-Sales-Tax-Estimation-For-Clients

This commit is contained in:
Jonas Hendrickx
2024-11-20 19:37:19 +01:00
560 changed files with 23021 additions and 8681 deletions

View File

@@ -24,6 +24,7 @@ import {
} from "../admin-console/models/response/organization-connection.response";
import { OrganizationExportResponse } from "../admin-console/models/response/organization-export.response";
import { OrganizationSponsorshipSyncStatusResponse } from "../admin-console/models/response/organization-sponsorship-sync-status.response";
import { PreValidateSponsorshipResponse } from "../admin-console/models/response/pre-validate-sponsorship.response";
import {
ProviderOrganizationOrganizationDetailsResponse,
ProviderOrganizationResponse,
@@ -35,7 +36,7 @@ import {
ProviderUserUserDetailsResponse,
} from "../admin-console/models/response/provider/provider-user.response";
import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response";
import { CreateAuthRequest } from "../auth/models/request/create-auth.request";
import { AuthRequest } from "../auth/models/request/auth.request";
import { DeviceVerificationRequest } from "../auth/models/request/device-verification.request";
import { DisableTwoFactorAuthenticatorRequest } from "../auth/models/request/disable-two-factor-authenticator.request";
import { EmailTokenRequest } from "../auth/models/request/email-token.request";
@@ -185,8 +186,8 @@ export abstract class ApiService {
putUpdateTdeOffboardingPassword: (request: UpdateTdeOffboardingPasswordRequest) => Promise<any>;
postConvertToKeyConnector: () => Promise<void>;
//passwordless
postAuthRequest: (request: CreateAuthRequest) => Promise<AuthRequestResponse>;
postAdminAuthRequest: (request: CreateAuthRequest) => Promise<AuthRequestResponse>;
postAuthRequest: (request: AuthRequest) => Promise<AuthRequestResponse>;
postAdminAuthRequest: (request: AuthRequest) => Promise<AuthRequestResponse>;
getAuthResponse: (id: string, accessCode: string) => Promise<AuthRequestResponse>;
getAuthRequest: (id: string) => Promise<AuthRequestResponse>;
putAuthRequest: (id: string, request: PasswordlessAuthRequest) => Promise<AuthRequestResponse>;
@@ -490,7 +491,9 @@ export abstract class ApiService {
) => Promise<OrganizationSponsorshipSyncStatusResponse>;
deleteRevokeSponsorship: (sponsoringOrganizationId: string) => Promise<void>;
deleteRemoveSponsorship: (sponsoringOrgId: string) => Promise<void>;
postPreValidateSponsorshipToken: (sponsorshipToken: string) => Promise<boolean>;
postPreValidateSponsorshipToken: (
sponsorshipToken: string,
) => Promise<PreValidateSponsorshipResponse>;
postRedeemSponsorship: (
sponsorshipToken: string,
request: OrganizationSponsorshipRedeemRequest,

View File

@@ -0,0 +1,124 @@
import { map, Observable } from "rxjs";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { Utils } from "../../../platform/misc/utils";
import { UserId } from "../../../types/guid";
import { OrganizationData } from "../../models/data/organization.data";
import { Organization } from "../../models/domain/organization";
export function canAccessVaultTab(org: Organization): boolean {
return org.canViewAllCollections;
}
export function canAccessSettingsTab(org: Organization): boolean {
return (
org.isOwner ||
org.canManagePolicies ||
org.canManageSso ||
org.canManageScim ||
org.canAccessImportExport ||
org.canManageDeviceApprovals
);
}
export function canAccessMembersTab(org: Organization): boolean {
return org.canManageUsers || org.canManageUsersPassword;
}
export function canAccessGroupsTab(org: Organization): boolean {
return org.canManageGroups;
}
export function canAccessReportingTab(org: Organization): boolean {
return org.canAccessReports || org.canAccessEventLogs;
}
export function canAccessBillingTab(org: Organization): boolean {
return org.isOwner;
}
export function canAccessOrgAdmin(org: Organization): boolean {
// Admin console can only be accessed by Owners for disabled organizations
if (!org.enabled && !org.isOwner) {
return false;
}
return (
canAccessMembersTab(org) ||
canAccessGroupsTab(org) ||
canAccessReportingTab(org) ||
canAccessBillingTab(org) ||
canAccessSettingsTab(org) ||
canAccessVaultTab(org)
);
}
export function getOrganizationById(id: string) {
return map<Organization[], Organization | undefined>((orgs) => orgs.find((o) => o.id === id));
}
export function canAccessAdmin(i18nService: I18nService) {
return map<Organization[], Organization[]>((orgs) =>
orgs.filter(canAccessOrgAdmin).sort(Utils.getSortFunction(i18nService, "name")),
);
}
export function canAccessImport(i18nService: I18nService) {
return map<Organization[], Organization[]>((orgs) =>
orgs
.filter((org) => org.canAccessImportExport || org.canCreateNewCollections)
.sort(Utils.getSortFunction(i18nService, "name")),
);
}
/**
* Publishes an observable stream of organizations. This service is meant to
* be used widely across Bitwarden as the primary way of fetching organizations.
* Risky operations like updates are isolated to the
* internal extension `InternalOrganizationServiceAbstraction`.
*/
export abstract class vNextOrganizationService {
/**
* Publishes state for all organizations under the specified user.
* @returns An observable list of organizations
*/
organizations$: (userId: UserId) => Observable<Organization[]>;
// @todo Clean these up. Continuing to expand them is not recommended.
// @see https://bitwarden.atlassian.net/browse/AC-2252
memberOrganizations$: (userId: UserId) => Observable<Organization[]>;
/**
* Emits true if the user can create or manage a Free Bitwarden Families sponsorship.
*/
canManageSponsorships$: (userId: UserId) => Observable<boolean>;
/**
* Emits true if any of the user's organizations have a Free Bitwarden Families sponsorship available.
*/
familySponsorshipAvailable$: (userId: UserId) => Observable<boolean>;
hasOrganizations: (userId: UserId) => Observable<boolean>;
}
/**
* Big scary buttons that **update** organization state. These should only be
* called from within admin-console scoped code. Extends the base
* `OrganizationService` for easy access to `get` calls.
* @internal
*/
export abstract class vNextInternalOrganizationServiceAbstraction extends vNextOrganizationService {
/**
* Replaces state for the provided organization, or creates it if not found.
* @param organization The organization state being saved.
* @param userId The userId to replace state for.
*/
upsert: (OrganizationData: OrganizationData, userId: UserId) => Promise<void>;
/**
* Replaces state for the entire registered organization list for the specified user.
* You probably don't want this unless you're calling from a full sync
* operation or a logout. See `upsert` for creating & updating a single
* organization in the state.
* @param organizations A complete list of all organization state for the provided
* user.
* @param userId The userId to replace state for.
*/
replace: (organizations: { [id: string]: OrganizationData }, userId: UserId) => Promise<void>;
}

View File

@@ -12,4 +12,5 @@ export enum PolicyType {
DisablePersonalVaultExport = 10, // Disable personal vault export
ActivateAutofill = 11, // Activates autofill with page load on the browser extension
AutomaticAppLogIn = 12, // Enables automatic log in of apps from configured identity provider
FreeFamiliesSponsorshipPolicy = 13, // Disables free families plan for organization
}

View File

@@ -283,9 +283,7 @@ export class Organization {
return true;
}
return this.hasProvider && this.providerType === ProviderType.Msp
? this.isProviderUser
: this.isOwner;
return this.hasBillableProvider ? this.isProviderUser : this.isOwner;
}
get canEditSubscription() {
@@ -304,6 +302,14 @@ export class Organization {
return this.providerId != null || this.providerName != null;
}
get hasBillableProvider() {
return (
this.hasProvider &&
(this.providerType === ProviderType.Msp ||
this.providerType === ProviderType.MultiOrganizationEnterprise)
);
}
get hasReseller() {
return this.hasProvider && this.providerType === ProviderType.Reseller;
}

View File

@@ -0,0 +1,10 @@
import { BaseResponse } from "../../../models/response/base.response";
export class OrganizationSponsorshipResponse extends BaseResponse {
isPolicyEnabled: string;
constructor(response: any) {
super(response);
this.isPolicyEnabled = this.getResponseProperty("IsPolicyEnabled");
}
}

View File

@@ -0,0 +1,12 @@
import { BaseResponse } from "../../../models/response/base.response";
export class PreValidateSponsorshipResponse extends BaseResponse {
isTokenValid: boolean;
isFreeFamilyPolicyEnabled: boolean;
constructor(response: any) {
super(response);
this.isTokenValid = this.getResponseProperty("IsTokenValid");
this.isFreeFamilyPolicyEnabled = this.getResponseProperty("IsFreeFamilyPolicyEnabled");
}
}

View File

@@ -0,0 +1,204 @@
import { firstValueFrom } from "rxjs";
import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec";
import { Utils } from "../../../platform/misc/utils";
import { OrganizationId, UserId } from "../../../types/guid";
import { OrganizationData } from "../../models/data/organization.data";
import { Organization } from "../../models/domain/organization";
import { DefaultvNextOrganizationService } from "./default-vnext-organization.service";
import { ORGANIZATIONS } from "./vnext-organization.state";
describe("OrganizationService", () => {
let organizationService: DefaultvNextOrganizationService;
const fakeUserId = Utils.newGuid() as UserId;
let fakeStateProvider: FakeStateProvider;
/**
* It is easier to read arrays than records in code, but we store a record
* in state. This helper methods lets us build organization arrays in tests
* and easily map them to records before storing them in state.
*/
function arrayToRecord(input: OrganizationData[]): Record<OrganizationId, OrganizationData> {
if (input == null) {
return undefined;
}
return Object.fromEntries(input?.map((i) => [i.id, i]));
}
/**
* There are a few assertions in this spec that check for array equality
* but want to ignore a specific index that _should_ be different. This
* function takes two arrays, and an index. It checks for equality of the
* arrays, but splices out the specified index from both arrays first.
*/
function expectIsEqualExceptForIndex(x: any[], y: any[], indexToExclude: number) {
// Clone the arrays to avoid modifying the reference values
const a = [...x];
const b = [...y];
delete a[indexToExclude];
delete b[indexToExclude];
expect(a).toEqual(b);
}
/**
* Builds a simple mock `OrganizationData[]` array that can be used in tests
* to populate state.
* @param count The number of organizations to populate the list with. The
* function returns undefined if this is less than 1. The default value is 1.
* @param suffix A string to append to data fields on each organization.
* This defaults to the index of the organization in the list.
* @returns an `OrganizationData[]` array that can be used to populate
* stateProvider.
*/
function buildMockOrganizations(count = 1, suffix?: string): OrganizationData[] {
if (count < 1) {
return undefined;
}
function buildMockOrganization(id: OrganizationId, name: string, identifier: string) {
const data = new OrganizationData({} as any, {} as any);
data.id = id;
data.name = name;
data.identifier = identifier;
return data;
}
const mockOrganizations = [];
for (let i = 0; i < count; i++) {
const s = suffix ? suffix + i.toString() : i.toString();
mockOrganizations.push(
buildMockOrganization(("org" + s) as OrganizationId, "org" + s, "orgIdentifier" + s),
);
}
return mockOrganizations;
}
const setOrganizationsState = (organizationData: OrganizationData[] | null) =>
fakeStateProvider.setUserState(
ORGANIZATIONS,
organizationData == null ? null : arrayToRecord(organizationData),
fakeUserId,
);
beforeEach(async () => {
fakeStateProvider = new FakeStateProvider(mockAccountServiceWith(fakeUserId));
organizationService = new DefaultvNextOrganizationService(fakeStateProvider);
});
describe("canManageSponsorships", () => {
it("can because one is available", async () => {
const mockData: OrganizationData[] = buildMockOrganizations(1);
mockData[0].familySponsorshipAvailable = true;
await setOrganizationsState(mockData);
const result = await firstValueFrom(organizationService.canManageSponsorships$(fakeUserId));
expect(result).toBe(true);
});
it("can because one is used", async () => {
const mockData: OrganizationData[] = buildMockOrganizations(1);
mockData[0].familySponsorshipFriendlyName = "Something";
await setOrganizationsState(mockData);
const result = await firstValueFrom(organizationService.canManageSponsorships$(fakeUserId));
expect(result).toBe(true);
});
it("can not because one isn't available or taken", async () => {
const mockData: OrganizationData[] = buildMockOrganizations(1);
mockData[0].familySponsorshipFriendlyName = null;
await setOrganizationsState(mockData);
const result = await firstValueFrom(organizationService.canManageSponsorships$(fakeUserId));
expect(result).toBe(false);
});
});
describe("organizations$", () => {
describe("null checking behavior", () => {
it("publishes an empty array if organizations in state = undefined", async () => {
const mockData: OrganizationData[] = undefined;
await setOrganizationsState(mockData);
const result = await firstValueFrom(organizationService.organizations$(fakeUserId));
expect(result).toEqual([]);
});
it("publishes an empty array if organizations in state = null", async () => {
const mockData: OrganizationData[] = null;
await setOrganizationsState(mockData);
const result = await firstValueFrom(organizationService.organizations$(fakeUserId));
expect(result).toEqual([]);
});
it("publishes an empty array if organizations in state = []", async () => {
const mockData: OrganizationData[] = [];
await setOrganizationsState(mockData);
const result = await firstValueFrom(organizationService.organizations$(fakeUserId));
expect(result).toEqual([]);
});
it("returns state for a user", async () => {
const mockData = buildMockOrganizations(10);
await setOrganizationsState(mockData);
const result = await firstValueFrom(organizationService.organizations$(fakeUserId));
expect(result).toEqual(mockData);
});
});
});
describe("upsert()", () => {
it("can create the organization list if necassary", async () => {
// Notice that no default state is provided in this test, so the list in
// `stateProvider` will be null when the `upsert` method is called.
const mockData = buildMockOrganizations();
await organizationService.upsert(mockData[0], fakeUserId);
const result = await firstValueFrom(organizationService.organizations$(fakeUserId));
expect(result).toEqual(mockData.map((x) => new Organization(x)));
});
it("updates an organization that already exists in state", async () => {
const mockData = buildMockOrganizations(10);
await setOrganizationsState(mockData);
const indexToUpdate = 5;
const anUpdatedOrganization = {
...buildMockOrganizations(1, "UPDATED").pop(),
id: mockData[indexToUpdate].id,
};
await organizationService.upsert(anUpdatedOrganization, fakeUserId);
const result = await firstValueFrom(organizationService.organizations$(fakeUserId));
expect(result[indexToUpdate]).not.toEqual(new Organization(mockData[indexToUpdate]));
expect(result[indexToUpdate].id).toEqual(new Organization(mockData[indexToUpdate]).id);
expectIsEqualExceptForIndex(
result,
mockData.map((x) => new Organization(x)),
indexToUpdate,
);
});
});
describe("replace()", () => {
it("replaces the entire organization list in state", async () => {
const originalData = buildMockOrganizations(10);
await setOrganizationsState(originalData);
const newData = buildMockOrganizations(10, "newData");
await organizationService.replace(arrayToRecord(newData), fakeUserId);
const result = await firstValueFrom(organizationService.organizations$(fakeUserId));
expect(result).toEqual(newData);
expect(result).not.toEqual(originalData);
});
// This is more or less a test for logouts
it("can replace state with null", async () => {
const originalData = buildMockOrganizations(2);
await setOrganizationsState(originalData);
await organizationService.replace(null, fakeUserId);
const result = await firstValueFrom(organizationService.organizations$(fakeUserId));
expect(result).toEqual([]);
expect(result).not.toEqual(originalData);
});
});
});

View File

@@ -0,0 +1,101 @@
import { map, Observable } from "rxjs";
import { StateProvider } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { vNextInternalOrganizationServiceAbstraction } from "../../abstractions/organization/vnext.organization.service";
import { OrganizationData } from "../../models/data/organization.data";
import { Organization } from "../../models/domain/organization";
import { ORGANIZATIONS } from "./vnext-organization.state";
/**
* Filter out organizations from an observable that __do not__ offer a
* families-for-enterprise sponsorship to members.
* @returns a function that can be used in `Observable<Organization[]>` pipes,
* like `organizationService.organizations$`
*/
function mapToExcludeOrganizationsWithoutFamilySponsorshipSupport() {
return map<Organization[], Organization[]>((orgs) => orgs.filter((o) => o.canManageSponsorships));
}
/**
* Filter out organizations from an observable that the organization user
* __is not__ a direct member of. This will exclude organizations only
* accessible as a provider.
* @returns a function that can be used in `Observable<Organization[]>` pipes,
* like `organizationService.organizations$`
*/
function mapToExcludeProviderOrganizations() {
return map<Organization[], Organization[]>((orgs) => orgs.filter((o) => o.isMember));
}
/**
* Map an observable stream of organizations down to a boolean indicating
* if any organizations exist (`orgs.length > 0`).
* @returns a function that can be used in `Observable<Organization[]>` pipes,
* like `organizationService.organizations$`
*/
function mapToBooleanHasAnyOrganizations() {
return map<Organization[], boolean>((orgs) => orgs.length > 0);
}
export class DefaultvNextOrganizationService
implements vNextInternalOrganizationServiceAbstraction
{
memberOrganizations$(userId: UserId): Observable<Organization[]> {
return this.organizations$(userId).pipe(mapToExcludeProviderOrganizations());
}
constructor(private stateProvider: StateProvider) {}
canManageSponsorships$(userId: UserId) {
return this.organizations$(userId).pipe(
mapToExcludeOrganizationsWithoutFamilySponsorshipSupport(),
mapToBooleanHasAnyOrganizations(),
);
}
familySponsorshipAvailable$(userId: UserId) {
return this.organizations$(userId).pipe(
map((orgs) => orgs.some((o) => o.familySponsorshipAvailable)),
);
}
hasOrganizations(userId: UserId): Observable<boolean> {
return this.organizations$(userId).pipe(mapToBooleanHasAnyOrganizations());
}
async upsert(organization: OrganizationData, userId: UserId): Promise<void> {
await this.organizationState(userId).update((existingOrganizations) => {
const organizations = existingOrganizations ?? {};
organizations[organization.id] = organization;
return organizations;
});
}
async replace(organizations: { [id: string]: OrganizationData }, userId: UserId): Promise<void> {
await this.organizationState(userId).update(() => organizations);
}
organizations$(userId: UserId): Observable<Organization[] | undefined> {
return this.organizationState(userId).state$.pipe(this.mapOrganizationRecordToArray());
}
private organizationState(userId: UserId) {
return this.stateProvider.getUser(userId, ORGANIZATIONS);
}
/**
* Accepts a record of `OrganizationData`, which is how we store the
* organization list as a JSON object on disk, to an array of
* `Organization`, which is how the data is published to callers of the
* service.
* @returns a function that can be used to pipe organization data from
* stored state to an exposed object easily consumable by others.
*/
private mapOrganizationRecordToArray() {
return map<Record<string, OrganizationData>, Organization[]>((orgs) =>
Object.values(orgs ?? {})?.map((o) => new Organization(o)),
);
}
}

View File

@@ -0,0 +1,20 @@
import { Jsonify } from "type-fest";
import { ORGANIZATIONS_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state";
import { OrganizationData } from "../../models/data/organization.data";
/**
* The `KeyDefinition` for accessing organization lists in application state.
* @todo Ideally this wouldn't require a `fromJSON()` call, but `OrganizationData`
* has some properties that contain functions. This should probably get
* cleaned up.
*/
export const ORGANIZATIONS = UserKeyDefinition.record<OrganizationData>(
ORGANIZATIONS_DISK,
"organizations",
{
deserializer: (obj: Jsonify<OrganizationData>) => OrganizationData.fromJSON(obj),
clearOn: ["logout"],
},
);

View File

@@ -1,6 +1,6 @@
import { AuthRequestType } from "../../enums/auth-request-type";
export class CreateAuthRequest {
export class AuthRequest {
constructor(
readonly email: string,
readonly deviceIdentifier: string,

View File

@@ -8,8 +8,8 @@ export class AuthRequestResponse extends BaseResponse {
publicKey: string;
requestDeviceType: DeviceType;
requestIpAddress: string;
key: string;
masterPasswordHash: string;
key: string; // could be either an encrypted MasterKey or an encrypted UserKey
masterPasswordHash: string; // if hash is present, the `key` above is an encrypted MasterKey (else `key` is an encrypted UserKey)
creationDate: string;
requestApproved?: boolean;
responseDate?: string;

View File

@@ -14,14 +14,12 @@ export enum FeatureFlag {
UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection",
EmailVerification = "email-verification",
InlineMenuFieldQualification = "inline-menu-field-qualification",
MemberAccessReport = "ac-2059-member-access-report",
TwoFactorComponentRefactor = "two-factor-component-refactor",
InlineMenuPositioningImprovements = "inline-menu-positioning-improvements",
ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner",
VaultBulkManagementAction = "vault-bulk-management-action",
IdpAutoSubmitLogin = "idp-auto-submit-login",
UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh",
EnableUpgradePasswordManagerSub = "AC-2708-upgrade-password-manager-sub",
GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor",
EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill",
DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2",
@@ -41,6 +39,7 @@ export enum FeatureFlag {
SecurityTasks = "security-tasks",
NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss",
NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss",
DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship",
}
export type AllowedFeatureFlagTypes = boolean | number | string;
@@ -65,14 +64,12 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE,
[FeatureFlag.EmailVerification]: FALSE,
[FeatureFlag.InlineMenuFieldQualification]: FALSE,
[FeatureFlag.MemberAccessReport]: FALSE,
[FeatureFlag.TwoFactorComponentRefactor]: FALSE,
[FeatureFlag.InlineMenuPositioningImprovements]: FALSE,
[FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE,
[FeatureFlag.VaultBulkManagementAction]: FALSE,
[FeatureFlag.IdpAutoSubmitLogin]: FALSE,
[FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE,
[FeatureFlag.EnableUpgradePasswordManagerSub]: FALSE,
[FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE,
[FeatureFlag.EnableNewCardCombinedExpiryAutofill]: FALSE,
[FeatureFlag.DelayFido2PageScriptInitWithinMv2]: FALSE,
@@ -92,6 +89,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.SecurityTasks]: FALSE,
[FeatureFlag.NewDeviceVerificationTemporaryDismiss]: FALSE,
[FeatureFlag.NewDeviceVerificationPermanentDismiss]: FALSE,
[FeatureFlag.DisableFreeFamiliesSponsorship]: FALSE,
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;

View File

@@ -10,6 +10,11 @@ export abstract class SdkService {
*/
supported$: Observable<boolean>;
/**
* Retrieve the version of the SDK.
*/
version$: Observable<string>;
/**
* Retrieve a client initialized without a user.
* This client can only be used for operations that don't require a user context.

View File

@@ -1,7 +1,6 @@
// required to avoid linting errors when there are no flags
// eslint-disable-next-line @typescript-eslint/ban-types
export type SharedFlags = {
showPasswordless?: boolean;
sdk?: boolean;
prereleaseBuild?: boolean;
};

View File

@@ -8,6 +8,7 @@ import {
distinctUntilChanged,
tap,
switchMap,
catchError,
} from "rxjs";
import { KeyService } from "@bitwarden/key-management";
@@ -51,6 +52,11 @@ export class DefaultSdkService implements SdkService {
}),
);
version$ = this.client$.pipe(
map((client) => client.version()),
catchError(() => "Unsupported"),
);
constructor(
private sdkClientFactory: SdkClientFactory,
private environmentService: EnvironmentService,

View File

@@ -173,3 +173,7 @@ export const PREMIUM_BANNER_DISK_LOCAL = new StateDefinition("premiumBannerRepro
});
export const BANNERS_DISMISSED_DISK = new StateDefinition("bannersDismissed", "disk");
export const VAULT_BROWSER_UI_ONBOARDING = new StateDefinition("vaultBrowserUiOnboarding", "disk");
export const NEW_DEVICE_VERIFICATION_NOTICE = new StateDefinition(
"newDeviceVerificationNotice",
"disk",
);

View File

@@ -29,6 +29,7 @@ import {
} from "../admin-console/models/response/organization-connection.response";
import { OrganizationExportResponse } from "../admin-console/models/response/organization-export.response";
import { OrganizationSponsorshipSyncStatusResponse } from "../admin-console/models/response/organization-sponsorship-sync-status.response";
import { PreValidateSponsorshipResponse } from "../admin-console/models/response/pre-validate-sponsorship.response";
import {
ProviderOrganizationOrganizationDetailsResponse,
ProviderOrganizationResponse,
@@ -41,7 +42,7 @@ import {
} from "../admin-console/models/response/provider/provider-user.response";
import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response";
import { TokenService } from "../auth/abstractions/token.service";
import { CreateAuthRequest } from "../auth/models/request/create-auth.request";
import { AuthRequest } from "../auth/models/request/auth.request";
import { DeviceVerificationRequest } from "../auth/models/request/device-verification.request";
import { DisableTwoFactorAuthenticatorRequest } from "../auth/models/request/disable-two-factor-authenticator.request";
import { EmailTokenRequest } from "../auth/models/request/email-token.request";
@@ -259,11 +260,12 @@ export class ApiService implements ApiServiceAbstraction {
}
// TODO: PM-3519: Create and move to AuthRequest Api service
async postAuthRequest(request: CreateAuthRequest): Promise<AuthRequestResponse> {
// TODO: PM-9724: Remove legacy auth request methods when we remove legacy LoginViaAuthRequestV1Components
async postAuthRequest(request: AuthRequest): Promise<AuthRequestResponse> {
const r = await this.send("POST", "/auth-requests/", request, false, true);
return new AuthRequestResponse(r);
}
async postAdminAuthRequest(request: CreateAuthRequest): Promise<AuthRequestResponse> {
async postAdminAuthRequest(request: AuthRequest): Promise<AuthRequestResponse> {
const r = await this.send("POST", "/auth-requests/admin-request", request, true, true);
return new AuthRequestResponse(r);
}
@@ -1680,8 +1682,10 @@ export class ApiService implements ApiServiceAbstraction {
);
}
async postPreValidateSponsorshipToken(sponsorshipToken: string): Promise<boolean> {
const r = await this.send(
async postPreValidateSponsorshipToken(
sponsorshipToken: string,
): Promise<PreValidateSponsorshipResponse> {
const response = await this.send(
"POST",
"/organization/sponsorship/validate-token?sponsorshipToken=" +
encodeURIComponent(sponsorshipToken),
@@ -1689,7 +1693,8 @@ export class ApiService implements ApiServiceAbstraction {
true,
true,
);
return r as boolean;
return new PreValidateSponsorshipResponse(response);
}
async postRedeemSponsorship(

View File

@@ -28,6 +28,11 @@ type NumberConstraints = {
/** maximum number value. When absent, min value is unbounded. */
max?: number;
/** recommended value. This is the value bitwarden recommends
* to the user as an appropriate value.
*/
recommendation?: number;
/** requires the number be a multiple of the step value;
* this field must be a positive number. +0 and Infinity are
* prohibited. When absent, any number is accepted.

View File

@@ -17,6 +17,10 @@ export class SshKeyView extends ItemView {
}
get maskedPrivateKey(): string {
if (!this.privateKey || this.privateKey.length === 0) {
return "";
}
let lines = this.privateKey.split("\n").filter((l) => l.trim() !== "");
lines = lines.map((l, i) => {
if (i === 0 || i === lines.length - 1) {