Merge branch 'main' into autofill/pm-12316-implement-inline-menu-passkeys-loading-state
This commit is contained in:
commit
090daeeb48
|
@ -5,7 +5,7 @@
|
|||
</ng-container>
|
||||
</popup-header>
|
||||
|
||||
<div class="tw-bg-background-alt tw-p-2">
|
||||
<div class="tw-bg-background-alt">
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">{{ "autofillSuggestionsSectionTitle" | i18n }}</h2>
|
||||
|
@ -107,7 +107,7 @@
|
|||
</bit-section-header>
|
||||
<bit-item>
|
||||
<button bit-item-content type="button" (click)="openURI($event, browserShortcutsURI)">
|
||||
<h3 bitTypography="h5">{{ "autofillKeyboardManagerShortcutsLabel" | i18n }}</h3>
|
||||
<h3 bitTypography="body2">{{ "autofillKeyboardManagerShortcutsLabel" | i18n }}</h3>
|
||||
<bit-hint slot="secondary" class="tw-text-sm tw-whitespace-normal">
|
||||
{{ autofillKeyboardHelperText }}
|
||||
</bit-hint>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</ng-container>
|
||||
</popup-header>
|
||||
|
||||
<div class="tw-bg-background-alt tw-p-2">
|
||||
<div class="tw-bg-background-alt">
|
||||
<p>
|
||||
{{
|
||||
accountSwitcherEnabled ? ("excludedDomainsDescAlt" | i18n) : ("excludedDomainsDesc" | i18n)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</ng-container>
|
||||
</popup-header>
|
||||
|
||||
<div class="tw-bg-background-alt tw-p-2">
|
||||
<div class="tw-bg-background-alt">
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">{{ "vaultSaveOptionsTitle" | i18n }}</h2>
|
||||
|
|
|
@ -1484,14 +1484,7 @@ export default class MainBackground {
|
|||
});
|
||||
|
||||
if (needStorageReseed) {
|
||||
await this.reseedStorage(
|
||||
await firstValueFrom(
|
||||
this.configService.userCachedFeatureFlag$(
|
||||
FeatureFlag.StorageReseedRefactor,
|
||||
userBeingLoggedOut,
|
||||
),
|
||||
),
|
||||
);
|
||||
await this.reseedStorage();
|
||||
}
|
||||
|
||||
if (BrowserApi.isManifestVersion(3)) {
|
||||
|
@ -1546,7 +1539,7 @@ export default class MainBackground {
|
|||
await SafariApp.sendMessageToApp("showPopover", null, true);
|
||||
}
|
||||
|
||||
async reseedStorage(doFillBuffer: boolean) {
|
||||
async reseedStorage() {
|
||||
if (
|
||||
!this.platformUtilsService.isChrome() &&
|
||||
!this.platformUtilsService.isVivaldi() &&
|
||||
|
@ -1555,11 +1548,7 @@ export default class MainBackground {
|
|||
return;
|
||||
}
|
||||
|
||||
if (doFillBuffer) {
|
||||
await this.storageService.fillBuffer();
|
||||
} else {
|
||||
await this.storageService.reseed();
|
||||
}
|
||||
await this.storageService.fillBuffer();
|
||||
}
|
||||
|
||||
async clearClipboard(clipboardValue: string, clearMs: number) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { firstValueFrom, map, mergeMap, of, switchMap } from "rxjs";
|
||||
import { firstValueFrom, map, mergeMap } from "rxjs";
|
||||
|
||||
import { LockService } from "@bitwarden/auth/common";
|
||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||
|
@ -281,22 +281,7 @@ export default class RuntimeBackground {
|
|||
await this.main.refreshMenu();
|
||||
break;
|
||||
case "bgReseedStorage": {
|
||||
const doFillBuffer = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) => {
|
||||
if (account == null) {
|
||||
return of(false);
|
||||
}
|
||||
|
||||
return this.configService.userCachedFeatureFlag$(
|
||||
FeatureFlag.StorageReseedRefactor,
|
||||
account.id,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await this.main.reseedStorage(doFillBuffer);
|
||||
await this.main.reseedStorage();
|
||||
break;
|
||||
}
|
||||
case "authResult": {
|
||||
|
|
|
@ -1,192 +0,0 @@
|
|||
import { objToStore } from "./abstractions/abstract-chrome-storage-api.service";
|
||||
import BrowserLocalStorageService, {
|
||||
RESEED_IN_PROGRESS_KEY,
|
||||
} from "./browser-local-storage.service";
|
||||
|
||||
const apiGetLike =
|
||||
(store: Record<any, any>) => (key: string, callback: (items: { [key: string]: any }) => void) => {
|
||||
if (key == null) {
|
||||
callback(store);
|
||||
} else {
|
||||
callback({ [key]: store[key] });
|
||||
}
|
||||
};
|
||||
|
||||
describe("BrowserLocalStorageService", () => {
|
||||
let service: BrowserLocalStorageService;
|
||||
let store: Record<any, any>;
|
||||
let changeListener: (changes: { [key: string]: chrome.storage.StorageChange }) => void;
|
||||
|
||||
let saveMock: jest.Mock;
|
||||
let getMock: jest.Mock;
|
||||
let clearMock: jest.Mock;
|
||||
let removeMock: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
store = {};
|
||||
|
||||
// Record change listener
|
||||
chrome.storage.local.onChanged.addListener = jest.fn((listener) => {
|
||||
changeListener = listener;
|
||||
});
|
||||
|
||||
service = new BrowserLocalStorageService();
|
||||
|
||||
// setup mocks
|
||||
getMock = chrome.storage.local.get as jest.Mock;
|
||||
getMock.mockImplementation(apiGetLike(store));
|
||||
saveMock = chrome.storage.local.set as jest.Mock;
|
||||
saveMock.mockImplementation((update, callback) => {
|
||||
Object.entries(update).forEach(([key, value]) => {
|
||||
store[key] = value;
|
||||
});
|
||||
callback();
|
||||
});
|
||||
clearMock = chrome.storage.local.clear as jest.Mock;
|
||||
clearMock.mockImplementation((callback) => {
|
||||
store = {};
|
||||
callback?.();
|
||||
});
|
||||
removeMock = chrome.storage.local.remove as jest.Mock;
|
||||
removeMock.mockImplementation((keys, callback) => {
|
||||
if (Array.isArray(keys)) {
|
||||
keys.forEach((key) => {
|
||||
delete store[key];
|
||||
});
|
||||
} else {
|
||||
delete store[keys];
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
chrome.runtime.lastError = undefined;
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("reseed", () => {
|
||||
it.each([
|
||||
{
|
||||
key1: objToStore("value1"),
|
||||
key2: objToStore("value2"),
|
||||
key3: null,
|
||||
},
|
||||
{},
|
||||
])("saves all data in storage %s", async (testStore) => {
|
||||
for (const key of Object.keys(testStore) as Array<keyof typeof testStore>) {
|
||||
store[key] = testStore[key];
|
||||
}
|
||||
await service.reseed();
|
||||
|
||||
expect(saveMock).toHaveBeenLastCalledWith(
|
||||
{ ...testStore, [RESEED_IN_PROGRESS_KEY]: objToStore(true) },
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
key1: objToStore("value1"),
|
||||
key2: objToStore("value2"),
|
||||
key3: null,
|
||||
},
|
||||
{},
|
||||
])("results in the same store %s", async (testStore) => {
|
||||
for (const key of Object.keys(testStore) as Array<keyof typeof testStore>) {
|
||||
store[key] = testStore[key];
|
||||
}
|
||||
await service.reseed();
|
||||
|
||||
expect(store).toEqual(testStore);
|
||||
});
|
||||
|
||||
it("converts non-serialized values to serialized", async () => {
|
||||
store.key1 = "value1";
|
||||
store.key2 = "value2";
|
||||
|
||||
const expectedStore = {
|
||||
key1: objToStore("value1"),
|
||||
key2: objToStore("value2"),
|
||||
reseedInProgress: objToStore(true),
|
||||
};
|
||||
|
||||
await service.reseed();
|
||||
|
||||
expect(saveMock).toHaveBeenLastCalledWith(expectedStore, expect.any(Function));
|
||||
});
|
||||
|
||||
it("clears data", async () => {
|
||||
await service.reseed();
|
||||
|
||||
expect(clearMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("throws if get has chrome.runtime.lastError", async () => {
|
||||
getMock.mockImplementation((key, callback) => {
|
||||
chrome.runtime.lastError = new Error("Get Test Error");
|
||||
callback();
|
||||
});
|
||||
|
||||
await expect(async () => await service.reseed()).rejects.toThrow("Get Test Error");
|
||||
});
|
||||
|
||||
it("throws if save has chrome.runtime.lastError", async () => {
|
||||
saveMock.mockImplementation((obj, callback) => {
|
||||
chrome.runtime.lastError = new Error("Save Test Error");
|
||||
callback();
|
||||
});
|
||||
|
||||
await expect(async () => await service.reseed()).rejects.toThrow("Save Test Error");
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(["get", "has", "save", "remove"] as const)("%s", (method) => {
|
||||
let interval: string | number | NodeJS.Timeout;
|
||||
|
||||
afterEach(() => {
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
});
|
||||
|
||||
function startReseed() {
|
||||
store[RESEED_IN_PROGRESS_KEY] = objToStore(true);
|
||||
}
|
||||
|
||||
function endReseed() {
|
||||
delete store[RESEED_IN_PROGRESS_KEY];
|
||||
changeListener({ reseedInProgress: { oldValue: true } });
|
||||
}
|
||||
|
||||
it("waits for reseed prior to operation", async () => {
|
||||
startReseed();
|
||||
|
||||
const promise = service[method]("key", "value"); // note "value" is only used in save, but ignored in other methods
|
||||
|
||||
await expect(promise).not.toBeFulfilled(10);
|
||||
|
||||
endReseed();
|
||||
|
||||
await expect(promise).toBeResolved();
|
||||
});
|
||||
|
||||
it("does not wait if reseed is not in progress", async () => {
|
||||
const promise = service[method]("key", "value");
|
||||
await expect(promise).toBeResolved(1);
|
||||
});
|
||||
|
||||
it("awaits prior reseed operations before starting a new one", async () => {
|
||||
startReseed();
|
||||
|
||||
const promise = service.reseed();
|
||||
|
||||
await expect(promise).not.toBeFulfilled(10);
|
||||
|
||||
endReseed();
|
||||
|
||||
await expect(promise).toBeResolved();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,35 +1,8 @@
|
|||
import { defer, filter, firstValueFrom, map, merge, throwError, timeout } from "rxjs";
|
||||
|
||||
import AbstractChromeStorageService, {
|
||||
SerializedValue,
|
||||
objToStore,
|
||||
} from "./abstractions/abstract-chrome-storage-api.service";
|
||||
|
||||
export const RESEED_IN_PROGRESS_KEY = "reseedInProgress";
|
||||
import AbstractChromeStorageService from "./abstractions/abstract-chrome-storage-api.service";
|
||||
|
||||
export default class BrowserLocalStorageService extends AbstractChromeStorageService {
|
||||
constructor() {
|
||||
super(chrome.storage.local);
|
||||
this.chromeStorageApi.remove(RESEED_IN_PROGRESS_KEY, () => {
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads, clears, and re-saves all data in local storage. This is a hack to remove previously stored sensitive data from
|
||||
* local storage logs.
|
||||
*
|
||||
* @see https://github.com/bitwarden/clients/issues/485
|
||||
*/
|
||||
async reseed(): Promise<void> {
|
||||
try {
|
||||
await this.save(RESEED_IN_PROGRESS_KEY, true);
|
||||
const data = await this.getAll();
|
||||
await this.clear();
|
||||
await this.saveAll(data);
|
||||
} finally {
|
||||
await super.remove(RESEED_IN_PROGRESS_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
async fillBuffer() {
|
||||
|
@ -71,107 +44,4 @@ export default class BrowserLocalStorageService extends AbstractChromeStorageSer
|
|||
);
|
||||
});
|
||||
}
|
||||
|
||||
override async get<T>(key: string): Promise<T> {
|
||||
await this.awaitReseed();
|
||||
return super.get(key);
|
||||
}
|
||||
|
||||
override async has(key: string): Promise<boolean> {
|
||||
await this.awaitReseed();
|
||||
return super.has(key);
|
||||
}
|
||||
|
||||
override async save(key: string, obj: any): Promise<void> {
|
||||
await this.awaitReseed();
|
||||
return super.save(key, obj);
|
||||
}
|
||||
|
||||
override async remove(key: string): Promise<void> {
|
||||
await this.awaitReseed();
|
||||
return super.remove(key);
|
||||
}
|
||||
|
||||
private async awaitReseed(): Promise<void> {
|
||||
const notReseeding = async () => {
|
||||
return !(await super.get(RESEED_IN_PROGRESS_KEY));
|
||||
};
|
||||
|
||||
const finishedReseeding = this.updates$.pipe(
|
||||
filter(({ key, updateType }) => key === RESEED_IN_PROGRESS_KEY && updateType === "remove"),
|
||||
map(() => true),
|
||||
);
|
||||
|
||||
await firstValueFrom(
|
||||
merge(defer(notReseeding), finishedReseeding).pipe(
|
||||
filter((v) => v),
|
||||
timeout({
|
||||
// We eventually need to give up and throw an error
|
||||
first: 5_000,
|
||||
with: () =>
|
||||
throwError(
|
||||
() => new Error("Reseeding local storage did not complete in a timely manner."),
|
||||
),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears local storage
|
||||
*/
|
||||
private async clear() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.chromeStorageApi.clear(() => {
|
||||
if (chrome.runtime.lastError) {
|
||||
return reject(chrome.runtime.lastError);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all objects stored in local storage.
|
||||
*
|
||||
* @remarks This method processes values prior to resolving, do not use `chrome.storage.local` directly
|
||||
* @returns Promise resolving to keyed object of all stored data
|
||||
*/
|
||||
private async getAll(): Promise<Record<string, unknown>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.chromeStorageApi.get(null, (allStorage) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
return reject(chrome.runtime.lastError);
|
||||
}
|
||||
|
||||
const resolved = Object.entries(allStorage).reduce(
|
||||
(agg, [key, value]) => {
|
||||
agg[key] = this.processGetObject(value);
|
||||
return agg;
|
||||
},
|
||||
{} as Record<string, unknown>,
|
||||
);
|
||||
resolve(resolved);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async saveAll(data: Record<string, unknown>): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const keyedData = Object.entries(data).reduce(
|
||||
(agg, [key, value]) => {
|
||||
agg[key] = objToStore(value);
|
||||
return agg;
|
||||
},
|
||||
{} as Record<string, SerializedValue>,
|
||||
);
|
||||
this.chromeStorageApi.set(keyedData, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
return reject(chrome.runtime.lastError);
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ import { PopOutComponent } from "../../../../../platform/popup/components/pop-ou
|
|||
import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component";
|
||||
import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component";
|
||||
import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service";
|
||||
import { BrowserPremiumUpgradePromptService } from "../../../services/browser-premium-upgrade-prompt.service";
|
||||
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
|
||||
|
||||
|
@ -77,6 +78,7 @@ export class ViewV2Component {
|
|||
private vaultPopupAutofillService: VaultPopupAutofillService,
|
||||
private accountService: AccountService,
|
||||
private eventCollectionService: EventCollectionService,
|
||||
private popupRouterCacheService: PopupRouterCacheService,
|
||||
) {
|
||||
this.subscribeToParams();
|
||||
}
|
||||
|
@ -163,8 +165,8 @@ export class ViewV2Component {
|
|||
return false;
|
||||
}
|
||||
|
||||
const successRoute = this.cipher.isDeleted ? "/trash" : "/vault";
|
||||
await this.router.navigate([successRoute]);
|
||||
await this.popupRouterCacheService.back();
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
|
@ -181,7 +183,7 @@ export class ViewV2Component {
|
|||
this.logService.error(e);
|
||||
}
|
||||
|
||||
await this.router.navigate(["/trash"]);
|
||||
await this.popupRouterCacheService.back();
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router";
|
||||
|
||||
import { ProductTierType, ProductType } from "@bitwarden/common/billing/enums";
|
||||
import { ProductType } from "@bitwarden/common/billing/enums";
|
||||
|
||||
import { freeTrialTextResolver } from "./free-trial-text.resolver";
|
||||
|
||||
|
@ -11,48 +11,25 @@ const route = {
|
|||
const routerStateSnapshot = {} as RouterStateSnapshot;
|
||||
|
||||
describe("freeTrialTextResolver", () => {
|
||||
[
|
||||
{
|
||||
param: ProductType.PasswordManager,
|
||||
keyBase: "startYour7DayFreeTrialOfBitwardenPasswordManager",
|
||||
},
|
||||
{
|
||||
param: ProductType.SecretsManager,
|
||||
keyBase: "startYour7DayFreeTrialOfBitwardenSecretsManager",
|
||||
},
|
||||
{
|
||||
param: `${ProductType.PasswordManager},${ProductType.SecretsManager}`,
|
||||
keyBase: "startYour7DayFreeTrialOfBitwarden",
|
||||
},
|
||||
].forEach(({ param, keyBase }) => {
|
||||
describe(`when product is ${param}`, () => {
|
||||
beforeEach(() => {
|
||||
route.queryParams.product = `${param}`;
|
||||
});
|
||||
it("shows password manager text", () => {
|
||||
route.queryParams.product = `${ProductType.PasswordManager}`;
|
||||
|
||||
it("returns teams trial text", () => {
|
||||
route.queryParams.productTier = ProductTierType.Teams;
|
||||
expect(freeTrialTextResolver(route, routerStateSnapshot)).toBe(
|
||||
"continueSettingUpFreeTrialPasswordManager",
|
||||
);
|
||||
});
|
||||
|
||||
expect(freeTrialTextResolver(route, routerStateSnapshot)).toBe(`${keyBase}ForTeams`);
|
||||
});
|
||||
it("shows secret manager text", () => {
|
||||
route.queryParams.product = `${ProductType.SecretsManager}`;
|
||||
|
||||
it("returns enterprise trial text", () => {
|
||||
route.queryParams.productTier = ProductTierType.Enterprise;
|
||||
expect(freeTrialTextResolver(route, routerStateSnapshot)).toBe(
|
||||
"continueSettingUpFreeTrialSecretsManager",
|
||||
);
|
||||
});
|
||||
|
||||
expect(freeTrialTextResolver(route, routerStateSnapshot)).toBe(`${keyBase}ForEnterprise`);
|
||||
});
|
||||
it("shows default text", () => {
|
||||
route.queryParams.product = `${ProductType.PasswordManager},${ProductType.SecretsManager}`;
|
||||
|
||||
it("returns families trial text", () => {
|
||||
route.queryParams.productTier = ProductTierType.Families;
|
||||
|
||||
expect(freeTrialTextResolver(route, routerStateSnapshot)).toBe(`${keyBase}ForFamilies`);
|
||||
});
|
||||
|
||||
it("returns default trial text", () => {
|
||||
route.queryParams.productTier = "";
|
||||
|
||||
expect(freeTrialTextResolver(route, routerStateSnapshot)).toBe(keyBase);
|
||||
});
|
||||
});
|
||||
expect(freeTrialTextResolver(route, routerStateSnapshot)).toBe("continueSettingUpFreeTrial");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,43 +1,22 @@
|
|||
import { ActivatedRouteSnapshot, ResolveFn } from "@angular/router";
|
||||
|
||||
import { ProductType, ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { ProductType } from "@bitwarden/common/billing/enums";
|
||||
|
||||
export const freeTrialTextResolver: ResolveFn<string | null> = (
|
||||
route: ActivatedRouteSnapshot,
|
||||
): string | null => {
|
||||
const { product, productTier } = route.queryParams;
|
||||
const { product } = route.queryParams;
|
||||
const products: ProductType[] = (product ?? "").split(",").map((p: string) => parseInt(p));
|
||||
|
||||
const onlyPasswordManager = products.length === 1 && products[0] === ProductType.PasswordManager;
|
||||
const onlySecretsManager = products.length === 1 && products[0] === ProductType.SecretsManager;
|
||||
const forTeams = parseInt(productTier) === ProductTierType.Teams;
|
||||
const forEnterprise = parseInt(productTier) === ProductTierType.Enterprise;
|
||||
const forFamilies = parseInt(productTier) === ProductTierType.Families;
|
||||
|
||||
switch (true) {
|
||||
case onlyPasswordManager && forTeams:
|
||||
return "startYour7DayFreeTrialOfBitwardenPasswordManagerForTeams";
|
||||
case onlyPasswordManager && forEnterprise:
|
||||
return "startYour7DayFreeTrialOfBitwardenPasswordManagerForEnterprise";
|
||||
case onlyPasswordManager && forFamilies:
|
||||
return "startYour7DayFreeTrialOfBitwardenPasswordManagerForFamilies";
|
||||
case onlyPasswordManager:
|
||||
return "startYour7DayFreeTrialOfBitwardenPasswordManager";
|
||||
case onlySecretsManager && forTeams:
|
||||
return "startYour7DayFreeTrialOfBitwardenSecretsManagerForTeams";
|
||||
case onlySecretsManager && forEnterprise:
|
||||
return "startYour7DayFreeTrialOfBitwardenSecretsManagerForEnterprise";
|
||||
case onlySecretsManager && forFamilies:
|
||||
return "startYour7DayFreeTrialOfBitwardenSecretsManagerForFamilies";
|
||||
return "continueSettingUpFreeTrialPasswordManager";
|
||||
case onlySecretsManager:
|
||||
return "startYour7DayFreeTrialOfBitwardenSecretsManager";
|
||||
case forTeams:
|
||||
return "startYour7DayFreeTrialOfBitwardenForTeams";
|
||||
case forEnterprise:
|
||||
return "startYour7DayFreeTrialOfBitwardenForEnterprise";
|
||||
case forFamilies:
|
||||
return "startYour7DayFreeTrialOfBitwardenForFamilies";
|
||||
return "continueSettingUpFreeTrialSecretsManager";
|
||||
default:
|
||||
return "startYour7DayFreeTrialOfBitwarden";
|
||||
return "continueSettingUpFreeTrial";
|
||||
}
|
||||
};
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<form [formGroup]="selfHostedForm" [bitSubmit]="submit">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
|
||||
<div>
|
||||
<div class="tw-pt-2 tw-pb-1">
|
||||
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
|
||||
{{ "chooseFile" | i18n }}
|
||||
</button>
|
||||
|
|
|
@ -8683,41 +8683,14 @@
|
|||
"manageBillingFromProviderPortalMessage": {
|
||||
"message": "Manage billing from the Provider Portal"
|
||||
},
|
||||
"startYour7DayFreeTrialOfBitwarden": {
|
||||
"message": "Start your 7-Day free trial of Bitwarden"
|
||||
"continueSettingUpFreeTrial": {
|
||||
"message": "Continue setting up your free trial of Bitwarden"
|
||||
},
|
||||
"startYour7DayFreeTrialOfBitwardenForTeams": {
|
||||
"message": "Start your 7-Day free trial of Bitwarden for Teams"
|
||||
"continueSettingUpFreeTrialPasswordManager": {
|
||||
"message": "Continue setting up your free trial of Bitwarden Password Manager"
|
||||
},
|
||||
"startYour7DayFreeTrialOfBitwardenForFamilies": {
|
||||
"message": "Start your 7-Day free trial of Bitwarden for Families"
|
||||
},
|
||||
"startYour7DayFreeTrialOfBitwardenForEnterprise": {
|
||||
"message": "Start your 7-Day free trial of Bitwarden for Enterprise"
|
||||
},
|
||||
"startYour7DayFreeTrialOfBitwardenSecretsManager": {
|
||||
"message": "Start your 7-Day free trial of Bitwarden Secrets Manager"
|
||||
},
|
||||
"startYour7DayFreeTrialOfBitwardenSecretsManagerForTeams": {
|
||||
"message": "Start your 7-Day free trial of Bitwarden Secrets Manager for Teams"
|
||||
},
|
||||
"startYour7DayFreeTrialOfBitwardenSecretsManagerForFamilies": {
|
||||
"message": "Start your 7-Day free trial of Bitwarden Secrets Manager for Families"
|
||||
},
|
||||
"startYour7DayFreeTrialOfBitwardenSecretsManagerForEnterprise": {
|
||||
"message": "Start your 7-Day free trial of Bitwarden Secrets Manager for Enterprise"
|
||||
},
|
||||
"startYour7DayFreeTrialOfBitwardenPasswordManager": {
|
||||
"message": "Start your 7-Day free trial of Bitwarden Password Manager"
|
||||
},
|
||||
"startYour7DayFreeTrialOfBitwardenPasswordManagerForTeams": {
|
||||
"message": "Start your 7-Day free trial of Bitwarden Password Manager for Teams"
|
||||
},
|
||||
"startYour7DayFreeTrialOfBitwardenPasswordManagerForFamilies": {
|
||||
"message": "Start your 7-Day free trial of Bitwarden Password Manager for Families"
|
||||
},
|
||||
"startYour7DayFreeTrialOfBitwardenPasswordManagerForEnterprise": {
|
||||
"message": "Start your 7-Day free trial of Bitwarden Password Manager for Enterprise"
|
||||
"continueSettingUpFreeTrialSecretsManager": {
|
||||
"message": "Continue setting up your free trial of Bitwarden Secrets Manager"
|
||||
},
|
||||
"enterTeamsOrgInfo": {
|
||||
"message": "Enter your Teams organization information"
|
||||
|
@ -9029,6 +9002,24 @@
|
|||
"purchasedSeatsRemoved": {
|
||||
"message": "purchased seats removed"
|
||||
},
|
||||
"environmentVariables": {
|
||||
"message": "Environment variables"
|
||||
},
|
||||
"organizationId": {
|
||||
"message": "Organization ID"
|
||||
},
|
||||
"projectIds": {
|
||||
"message": "Project IDs"
|
||||
},
|
||||
"projectId": {
|
||||
"message": "Project ID"
|
||||
},
|
||||
"projectsAccessedByMachineAccount": {
|
||||
"message": "The following projects can be accessed by this machine account."
|
||||
},
|
||||
"config": {
|
||||
"message": "Config"
|
||||
},
|
||||
"learnMoreAboutEmergencyAccess": {
|
||||
"message":"Learn more about emergency access"
|
||||
},
|
||||
|
|
|
@ -6,4 +6,5 @@ export class ProjectListView {
|
|||
revisionDate: string;
|
||||
read: boolean;
|
||||
write: boolean;
|
||||
linkable: boolean;
|
||||
}
|
||||
|
|
|
@ -131,6 +131,7 @@ export class ProjectService {
|
|||
);
|
||||
projectListView.creationDate = s.creationDate;
|
||||
projectListView.revisionDate = s.revisionDate;
|
||||
projectListView.linkable = true;
|
||||
return projectListView;
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<div *ngIf="!loading">
|
||||
<div class="tw-p-6 tw-border tw-border-solid tw-border-secondary-600 tw-rounded">
|
||||
<h2 bitTypography="h2">{{ "environmentVariables" | i18n }}</h2>
|
||||
<div class="tw-flex tw-gap-6 tw-pt-4">
|
||||
<bit-form-field class="tw-w-2/5 tw-min-w-80" tw>
|
||||
<bit-label>{{ "identityUrl" | i18n }}</bit-label>
|
||||
<input bitInput type="text" [(ngModel)]="identityUrl" [disabled]="true" />
|
||||
<button
|
||||
bitSuffix
|
||||
type="button"
|
||||
type="button"
|
||||
bitIconButton="bwi-clone"
|
||||
[bitAction]="copyIdentityUrl"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
<bit-form-field class="tw-w-2/5 tw-min-w-80">
|
||||
<bit-label>{{ "apiUrl" | i18n }}</bit-label>
|
||||
<input bitInput type="text" [(ngModel)]="apiUrl" [disabled]="true" />
|
||||
<button bitSuffix type="button" bitIconButton="bwi-clone" [bitAction]="copyApiUrl"></button>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<bit-form-field class="tw-w-2/5 tw-min-w-80">
|
||||
<bit-label>{{ "organizationId" | i18n }}</bit-label>
|
||||
<input bitInput type="text" [(ngModel)]="organizationId" [disabled]="true" />
|
||||
<button
|
||||
bitSuffix
|
||||
type="button"
|
||||
bitIconButton="bwi-clone"
|
||||
[bitAction]="copyOrganizationId"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-pt-12">
|
||||
<h2 slot="summary" class="tw-mb-0" bitTypography="h2" noMargin>{{ "projectIds" | i18n }}</h2>
|
||||
<p *ngIf="!hasProjects" class="tw-mt-6">{{ "projectsNoItemsTitle" | i18n }}</p>
|
||||
<p *ngIf="hasProjects" class="tw-mt-4">{{ "projectsAccessedByMachineAccount" | i18n }}</p>
|
||||
<sm-projects-list
|
||||
class="tw-mt-8"
|
||||
*ngIf="hasProjects"
|
||||
[showMenus]="false"
|
||||
[projects]="projects"
|
||||
></sm-projects-list>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="loading" class="tw-items-center tw-justify-center tw-pt-64 tw-text-center">
|
||||
<i class="bwi bwi-spinner bwi-spin bwi-3x"></i>
|
||||
</div>
|
|
@ -0,0 +1,127 @@
|
|||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Params } from "@angular/router";
|
||||
import { Subject, concatMap, takeUntil } from "rxjs";
|
||||
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { ProjectListView } from "../../models/view/project-list.view";
|
||||
import { ProjectService } from "../../projects/project.service";
|
||||
import { AccessPolicyService } from "../../shared/access-policies/access-policy.service";
|
||||
|
||||
class ServiceAccountConfig {
|
||||
organizationId: string;
|
||||
serviceAccountId: string;
|
||||
identityUrl: string;
|
||||
apiUrl: string;
|
||||
projects: ProjectListView[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "sm-service-account-config",
|
||||
templateUrl: "./config.component.html",
|
||||
})
|
||||
export class ServiceAccountConfigComponent implements OnInit, OnDestroy {
|
||||
identityUrl: string;
|
||||
apiUrl: string;
|
||||
organizationId: string;
|
||||
serviceAccountId: string;
|
||||
projects: ProjectListView[];
|
||||
hasProjects = false;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
loading = true;
|
||||
|
||||
constructor(
|
||||
private environmentService: EnvironmentService,
|
||||
private route: ActivatedRoute,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private toastService: ToastService,
|
||||
private i18nService: I18nService,
|
||||
private projectService: ProjectService,
|
||||
private accessPolicyService: AccessPolicyService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.params
|
||||
.pipe(
|
||||
concatMap(async (params: Params) => {
|
||||
return await this.load(params.organizationId, params.serviceAccountId);
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((smConfig) => {
|
||||
this.identityUrl = smConfig.identityUrl;
|
||||
this.apiUrl = smConfig.apiUrl;
|
||||
this.organizationId = smConfig.organizationId;
|
||||
this.serviceAccountId = smConfig.serviceAccountId;
|
||||
this.projects = smConfig.projects;
|
||||
|
||||
this.hasProjects = smConfig.projects.length > 0;
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
async load(organizationId: string, serviceAccountId: string): Promise<ServiceAccountConfig> {
|
||||
const environment = await this.environmentService.getEnvironment();
|
||||
|
||||
const allProjects = await this.projectService.getProjects(organizationId);
|
||||
const policies = await this.accessPolicyService.getServiceAccountGrantedPolicies(
|
||||
organizationId,
|
||||
serviceAccountId,
|
||||
);
|
||||
|
||||
const projects = policies.grantedProjectPolicies.map((policy) => {
|
||||
return {
|
||||
id: policy.accessPolicy.grantedProjectId,
|
||||
name: policy.accessPolicy.grantedProjectName,
|
||||
organizationId: organizationId,
|
||||
linkable: allProjects.some(
|
||||
(project) => project.id === policy.accessPolicy.grantedProjectId,
|
||||
),
|
||||
} as ProjectListView;
|
||||
});
|
||||
|
||||
return {
|
||||
organizationId: organizationId,
|
||||
serviceAccountId: serviceAccountId,
|
||||
identityUrl: environment.getIdentityUrl(),
|
||||
apiUrl: environment.getApiUrl(),
|
||||
projects: projects,
|
||||
} as ServiceAccountConfig;
|
||||
}
|
||||
|
||||
copyIdentityUrl = () => {
|
||||
this.platformUtilsService.copyToClipboard(this.identityUrl);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("valueCopied", this.i18nService.t("identityUrl")),
|
||||
});
|
||||
};
|
||||
|
||||
copyApiUrl = () => {
|
||||
this.platformUtilsService.copyToClipboard(this.apiUrl);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("valueCopied", this.i18nService.t("apiUrl")),
|
||||
});
|
||||
};
|
||||
|
||||
copyOrganizationId = () => {
|
||||
this.platformUtilsService.copyToClipboard(this.organizationId);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("valueCopied", this.i18nService.t("organizationId")),
|
||||
});
|
||||
};
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
|
@ -29,6 +29,7 @@
|
|||
</div>
|
||||
</bit-tab-link>
|
||||
<bit-tab-link [route]="['events']">{{ "eventLogs" | i18n }}</bit-tab-link>
|
||||
<bit-tab-link [route]="['config']">{{ "config" | i18n }}</bit-tab-link>
|
||||
</bit-tab-nav-bar>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
@ -2,6 +2,7 @@ import { NgModule } from "@angular/core";
|
|||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { AccessTokenComponent } from "./access/access-tokens.component";
|
||||
import { ServiceAccountConfigComponent } from "./config/config.component";
|
||||
import { ServiceAccountEventsComponent } from "./event-logs/service-accounts-events.component";
|
||||
import { serviceAccountAccessGuard } from "./guards/service-account-access.guard";
|
||||
import { ServiceAccountPeopleComponent } from "./people/service-account-people.component";
|
||||
|
@ -40,6 +41,10 @@ const routes: Routes = [
|
|||
path: "events",
|
||||
component: ServiceAccountEventsComponent,
|
||||
},
|
||||
{
|
||||
path: "config",
|
||||
component: ServiceAccountConfigComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -9,6 +9,7 @@ import { AccessTokenComponent } from "./access/access-tokens.component";
|
|||
import { AccessTokenCreateDialogComponent } from "./access/dialogs/access-token-create-dialog.component";
|
||||
import { AccessTokenDialogComponent } from "./access/dialogs/access-token-dialog.component";
|
||||
import { ExpirationOptionsComponent } from "./access/dialogs/expiration-options.component";
|
||||
import { ServiceAccountConfigComponent } from "./config/config.component";
|
||||
import { ServiceAccountDeleteDialogComponent } from "./dialog/service-account-delete-dialog.component";
|
||||
import { ServiceAccountDialogComponent } from "./dialog/service-account-dialog.component";
|
||||
import { ServiceAccountEventsComponent } from "./event-logs/service-accounts-events.component";
|
||||
|
@ -28,6 +29,7 @@ import { ServiceAccountsComponent } from "./service-accounts.component";
|
|||
AccessTokenDialogComponent,
|
||||
ExpirationOptionsComponent,
|
||||
ServiceAccountComponent,
|
||||
ServiceAccountConfigComponent,
|
||||
ServiceAccountDeleteDialogComponent,
|
||||
ServiceAccountDialogComponent,
|
||||
ServiceAccountEventsComponent,
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
<bit-table *ngIf="projects?.length >= 1" [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell class="tw-w-0">
|
||||
<th bitCell class="tw-w-0" *ngIf="showMenus">
|
||||
<label class="!tw-mb-0 tw-flex tw-w-fit tw-gap-2 !tw-font-bold !tw-text-muted">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
@ -32,7 +32,7 @@
|
|||
</label>
|
||||
</th>
|
||||
<th bitCell bitSortable="name" default>{{ "name" | i18n }}</th>
|
||||
<th bitCell bitSortable="revisionDate">{{ "lastEdited" | i18n }}</th>
|
||||
<th bitCell bitSortable="revisionDate" *ngIf="showMenus">{{ "lastEdited" | i18n }}</th>
|
||||
<th
|
||||
bitCell
|
||||
class="tw-w-0"
|
||||
|
@ -45,13 +45,14 @@
|
|||
[bitMenuTriggerFor]="tableMenu"
|
||||
[title]="'options' | i18n"
|
||||
[attr.aria-label]="'options' | i18n"
|
||||
*ngIf="showMenus"
|
||||
></button>
|
||||
</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *ngFor="let project of rows$ | async">
|
||||
<td bitCell>
|
||||
<td bitCell *ngIf="showMenus">
|
||||
<input
|
||||
type="checkbox"
|
||||
(change)="$event ? selection.toggle(project.id) : null"
|
||||
|
@ -61,12 +62,32 @@
|
|||
<td bitCell>
|
||||
<div class="tw-flex tw-items-center tw-gap-4 tw-break-all">
|
||||
<i class="bwi bwi-collection tw-text-muted" aria-hidden="true"></i>
|
||||
<a bitLink [routerLink]="['/sm', project.organizationId, 'projects', project.id]">{{
|
||||
project.name
|
||||
}}</a>
|
||||
<div>
|
||||
<a
|
||||
*ngIf="project.linkable"
|
||||
bitLink
|
||||
[routerLink]="['/sm', project.organizationId, 'projects', project.id]"
|
||||
>{{ project.name }}</a
|
||||
>
|
||||
<span *ngIf="!project.linkable">{{ project.name }}</span>
|
||||
<div class="tw-text-sm tw-text-muted tw-block">
|
||||
{{ project.id }}
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-clone"
|
||||
buttonType="main"
|
||||
size="small"
|
||||
[title]="'copyUuid' | i18n"
|
||||
[attr.aria-label]="'copyUuid' | i18n"
|
||||
(click)="copyProjectUuidToClipboard(project.id)"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td bitCell class="tw-whitespace-nowrap">{{ project.revisionDate | date: "medium" }}</td>
|
||||
<td bitCell class="tw-whitespace-nowrap" *ngIf="showMenus">
|
||||
{{ project.revisionDate | date: "medium" }}
|
||||
</td>
|
||||
<td bitCell>
|
||||
<button
|
||||
type="button"
|
||||
|
@ -75,6 +96,7 @@
|
|||
[bitMenuTriggerFor]="projectMenu"
|
||||
[title]="'options' | i18n"
|
||||
[attr.aria-label]="'options' | i18n"
|
||||
*ngIf="showMenus"
|
||||
></button>
|
||||
</td>
|
||||
<bit-menu #projectMenu>
|
||||
|
|
|
@ -24,6 +24,8 @@ export class ProjectsListComponent {
|
|||
}
|
||||
private _projects: ProjectListView[];
|
||||
|
||||
@Input() showMenus?: boolean = true;
|
||||
|
||||
@Input()
|
||||
set search(search: string) {
|
||||
this.selection.clear();
|
||||
|
@ -33,6 +35,7 @@ export class ProjectsListComponent {
|
|||
@Output() editProjectEvent = new EventEmitter<string>();
|
||||
@Output() deleteProjectEvent = new EventEmitter<ProjectListView[]>();
|
||||
@Output() newProjectEvent = new EventEmitter();
|
||||
@Output() copiedProjectUUIdEvent = new EventEmitter<string>();
|
||||
|
||||
selection = new SelectionModel<string>(true, []);
|
||||
protected dataSource = new TableDataSource<ProjectListView>();
|
||||
|
@ -90,4 +93,13 @@ export class ProjectsListComponent {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
copyProjectUuidToClipboard(id: string) {
|
||||
this.platformUtilsService.copyToClipboard(id);
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("valueCopied", this.i18nService.t("projectId")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { NgModule } from "@angular/core";
|
||||
|
||||
import {
|
||||
CardComponent,
|
||||
MultiSelectModule,
|
||||
SearchModule,
|
||||
SelectModule,
|
||||
NoItemsModule,
|
||||
FormFieldModule,
|
||||
} from "@bitwarden/components";
|
||||
import { CoreOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/core";
|
||||
import { DynamicAvatarComponent } from "@bitwarden/web-vault/app/components/dynamic-avatar.component";
|
||||
|
@ -31,17 +33,21 @@ import { SecretsListComponent } from "./secrets-list.component";
|
|||
DynamicAvatarComponent,
|
||||
SearchModule,
|
||||
HeaderModule,
|
||||
CardComponent,
|
||||
FormFieldModule,
|
||||
],
|
||||
exports: [
|
||||
AccessPolicySelectorComponent,
|
||||
BulkConfirmationDialogComponent,
|
||||
BulkStatusDialogComponent,
|
||||
FormFieldModule,
|
||||
HeaderModule,
|
||||
NewMenuComponent,
|
||||
NoItemsModule,
|
||||
ProjectsListComponent,
|
||||
SearchModule,
|
||||
SecretsListComponent,
|
||||
CardComponent,
|
||||
SelectModule,
|
||||
SharedModule,
|
||||
],
|
||||
|
|
|
@ -34,7 +34,6 @@ export enum FeatureFlag {
|
|||
AccountDeprovisioning = "pm-10308-account-deprovisioning",
|
||||
NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements",
|
||||
AC2476_DeprecateStripeSourcesAPI = "AC-2476-deprecate-stripe-sources-api",
|
||||
StorageReseedRefactor = "storage-reseed-refactor",
|
||||
CipherKeyEncryption = "cipher-key-encryption",
|
||||
}
|
||||
|
||||
|
@ -77,7 +76,6 @@ export const DefaultFeatureFlagValue = {
|
|||
[FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE,
|
||||
[FeatureFlag.EnableNewCardCombinedExpiryAutofill]: FALSE,
|
||||
[FeatureFlag.DelayFido2PageScriptInitWithinMv2]: FALSE,
|
||||
[FeatureFlag.StorageReseedRefactor]: FALSE,
|
||||
[FeatureFlag.AccountDeprovisioning]: FALSE,
|
||||
[FeatureFlag.NotificationBarAddLoginImprovements]: FALSE,
|
||||
[FeatureFlag.AC2476_DeprecateStripeSourcesAPI]: FALSE,
|
||||
|
|
Loading…
Reference in New Issue