1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 16:53:34 +00:00

Merge branch 'master' into feature/org-admin-refresh

This commit is contained in:
Vincent Salucci
2022-09-12 12:41:11 -05:00
152 changed files with 2973 additions and 961 deletions

View File

@@ -68,6 +68,8 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
protected successRoute = "login";
protected accountCreated = false;
constructor(
protected formValidationErrorService: FormValidationErrorsService,
protected formBuilder: UntypedFormBuilder,
@@ -92,100 +94,33 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
async submit(showToast = true) {
let email = this.formGroup.get("email")?.value;
let name = this.formGroup.get("name")?.value;
const masterPassword = this.formGroup.get("masterPassword")?.value;
const hint = this.formGroup.get("hint")?.value;
this.formGroup.markAllAsTouched();
this.showErrorSummary = true;
if (this.formGroup.get("acceptPolicies").hasError("required")) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("acceptPoliciesRequired")
);
return;
}
//web
if (this.formGroup.invalid && !showToast) {
return;
}
//desktop, browser
if (this.formGroup.invalid && showToast) {
const errorText = this.getErrorToastMessage();
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), errorText);
return;
}
if (this.passwordStrengthResult != null && this.passwordStrengthResult.score < 3) {
const result = await this.platformUtilsService.showDialog(
this.i18nService.t("weakMasterPasswordDesc"),
this.i18nService.t("weakMasterPassword"),
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!result) {
return;
}
}
name = name === "" ? null : name;
email = email.trim().toLowerCase();
const kdf = DEFAULT_KDF_TYPE;
const kdfIterations = DEFAULT_KDF_ITERATIONS;
const key = await this.cryptoService.makeKey(masterPassword, email, kdf, kdfIterations);
const encKey = await this.cryptoService.makeEncKey(key);
const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key);
const keys = await this.cryptoService.makeKeyPair(encKey[0]);
const request = new RegisterRequest(
email,
name,
hashedPassword,
hint,
encKey[1].encryptedString,
kdf,
kdfIterations,
this.referenceData,
this.captchaToken
);
request.keys = new KeysRequest(keys[0], keys[1].encryptedString);
const orgInvite = await this.stateService.getOrganizationInvitation();
if (orgInvite != null && orgInvite.token != null && orgInvite.organizationUserId != null) {
request.token = orgInvite.token;
request.organizationUserId = orgInvite.organizationUserId;
}
let name = this.formGroup.get("name")?.value;
name = name === "" ? null : name; // Why do we do this?
const masterPassword = this.formGroup.get("masterPassword")?.value;
try {
this.formPromise = this.apiService.postRegister(request);
try {
await this.formPromise;
} catch (e) {
if (this.handleCaptchaRequired(e)) {
if (!this.accountCreated) {
const registerResponse = await this.registerAccount(
await this.buildRegisterRequest(email, masterPassword, name),
showToast
);
if (!registerResponse.successful) {
return;
} else {
throw e;
}
this.accountCreated = true;
}
if (this.isInTrialFlow) {
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("trialAccountCreated")
);
//login user here
const credentials = new PasswordLogInCredentials(
email,
masterPassword,
this.captchaToken,
null
);
await this.authService.logIn(credentials);
if (!this.accountCreated) {
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("trialAccountCreated")
);
}
const loginResponse = await this.logIn(email, masterPassword, this.captchaToken);
if (loginResponse.captchaRequired) {
return;
}
this.createdAccount.emit(this.formGroup.get("email")?.value);
} else {
this.platformUtilsService.showToast(
@@ -247,4 +182,114 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
return !ctrlValue && this.showTerms ? { required: true } : null;
};
}
private async validateRegistration(showToast: boolean): Promise<{ isValid: boolean }> {
this.formGroup.markAllAsTouched();
this.showErrorSummary = true;
if (this.formGroup.get("acceptPolicies").hasError("required")) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("acceptPoliciesRequired")
);
return { isValid: false };
}
//web
if (this.formGroup.invalid && !showToast) {
return { isValid: false };
}
//desktop, browser
if (this.formGroup.invalid && showToast) {
const errorText = this.getErrorToastMessage();
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), errorText);
return { isValid: false };
}
if (this.passwordStrengthResult != null && this.passwordStrengthResult.score < 3) {
const result = await this.platformUtilsService.showDialog(
this.i18nService.t("weakMasterPasswordDesc"),
this.i18nService.t("weakMasterPassword"),
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!result) {
return { isValid: false };
}
}
return { isValid: true };
}
private async buildRegisterRequest(
email: string,
masterPassword: string,
name: string
): Promise<RegisterRequest> {
const hint = this.formGroup.get("hint")?.value;
const kdf = DEFAULT_KDF_TYPE;
const kdfIterations = DEFAULT_KDF_ITERATIONS;
const key = await this.cryptoService.makeKey(masterPassword, email, kdf, kdfIterations);
const encKey = await this.cryptoService.makeEncKey(key);
const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key);
const keys = await this.cryptoService.makeKeyPair(encKey[0]);
const request = new RegisterRequest(
email,
name,
hashedPassword,
hint,
encKey[1].encryptedString,
kdf,
kdfIterations,
this.referenceData,
this.captchaToken
);
request.keys = new KeysRequest(keys[0], keys[1].encryptedString);
const orgInvite = await this.stateService.getOrganizationInvitation();
if (orgInvite != null && orgInvite.token != null && orgInvite.organizationUserId != null) {
request.token = orgInvite.token;
request.organizationUserId = orgInvite.organizationUserId;
}
return request;
}
private async registerAccount(
request: RegisterRequest,
showToast: boolean
): Promise<{ successful: boolean }> {
if (!(await this.validateRegistration(showToast)).isValid) {
return { successful: false };
}
this.formPromise = this.apiService.postRegister(request);
try {
await this.formPromise;
return { successful: true };
} catch (e) {
if (this.handleCaptchaRequired(e)) {
return { successful: false };
} else {
throw e;
}
}
}
private async logIn(
email: string,
masterPassword: string,
captchaBypassToken: string
): Promise<{ captchaRequired: boolean }> {
const credentials = new PasswordLogInCredentials(
email,
masterPassword,
captchaBypassToken,
null
);
const loginResponse = await this.authService.logIn(credentials);
if (this.handleCaptchaRequired(loginResponse)) {
return { captchaRequired: true };
}
return { captchaRequired: false };
}
}

View File

@@ -0,0 +1,18 @@
import { InjectionToken } from "@angular/core";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
export const WINDOW = new InjectionToken<Window>("WINDOW");
export const MEMORY_STORAGE = new InjectionToken<AbstractStorageService>("MEMORY_STORAGE");
export const SECURE_STORAGE = new InjectionToken<AbstractStorageService>("SECURE_STORAGE");
export const STATE_FACTORY = new InjectionToken<StateFactory>("STATE_FACTORY");
export const STATE_SERVICE_USE_CACHE = new InjectionToken<boolean>("STATE_SERVICE_USE_CACHE");
export const LOGOUT_CALLBACK = new InjectionToken<(expired: boolean, userId?: string) => void>(
"LOGOUT_CALLBACK"
);
export const LOCKED_CALLBACK = new InjectionToken<() => void>("LOCKED_CALLBACK");
export const CLIENT_TYPE = new InjectionToken<boolean>("CLIENT_TYPE");
export const LOCALES_DIRECTORY = new InjectionToken<string>("LOCALES_DIRECTORY");
export const SYSTEM_LANGUAGE = new InjectionToken<string>("SYSTEM_LANGUAGE");
export const LOG_MAC_FAILURES = new InjectionToken<string>("LOG_MAC_FAILURES");

View File

@@ -1,4 +1,4 @@
import { InjectionToken, Injector, LOCALE_ID, NgModule } from "@angular/core";
import { Injector, LOCALE_ID, NgModule } from "@angular/core";
import { ThemingService } from "@bitwarden/angular/services/theming/theming.service";
import { AbstractThemingService } from "@bitwarden/angular/services/theming/theming.service.abstraction";
@@ -12,6 +12,8 @@ import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/abstrac
import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/abstractions/broadcaster.service";
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService as CollectionServiceAbstraction } from "@bitwarden/common/abstractions/collection.service";
import { ConfigApiServiceAbstraction } from "@bitwarden/common/abstractions/config/config-api.service.abstraction";
import { ConfigServiceAbstraction } from "@bitwarden/common/abstractions/config/config.service.abstraction";
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/abstractions/crypto.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { EnvironmentService as EnvironmentServiceAbstraction } from "@bitwarden/common/abstractions/environment.service";
@@ -66,6 +68,8 @@ import { AuditService } from "@bitwarden/common/services/audit.service";
import { AuthService } from "@bitwarden/common/services/auth.service";
import { CipherService } from "@bitwarden/common/services/cipher.service";
import { CollectionService } from "@bitwarden/common/services/collection.service";
import { ConfigApiService } from "@bitwarden/common/services/config/config-api.service";
import { ConfigService } from "@bitwarden/common/services/config/config.service";
import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service";
import { CryptoService } from "@bitwarden/common/services/crypto.service";
import { EncryptService } from "@bitwarden/common/services/encrypt.service";
@@ -105,24 +109,22 @@ import { LockGuard } from "../guards/lock.guard";
import { UnauthGuard } from "../guards/unauth.guard";
import { BroadcasterService } from "./broadcaster.service";
import {
WINDOW,
MEMORY_STORAGE,
SECURE_STORAGE,
STATE_FACTORY,
STATE_SERVICE_USE_CACHE,
LOGOUT_CALLBACK,
LOCKED_CALLBACK,
LOCALES_DIRECTORY,
SYSTEM_LANGUAGE,
LOG_MAC_FAILURES,
} from "./injection-tokens";
import { ModalService } from "./modal.service";
import { PasswordRepromptService } from "./passwordReprompt.service";
import { ValidationService } from "./validation.service";
export const WINDOW = new InjectionToken<Window>("WINDOW");
export const MEMORY_STORAGE = new InjectionToken<AbstractStorageService>("MEMORY_STORAGE");
export const SECURE_STORAGE = new InjectionToken<AbstractStorageService>("SECURE_STORAGE");
export const STATE_FACTORY = new InjectionToken<StateFactory>("STATE_FACTORY");
export const STATE_SERVICE_USE_CACHE = new InjectionToken<boolean>("STATE_SERVICE_USE_CACHE");
export const LOGOUT_CALLBACK = new InjectionToken<(expired: boolean, userId?: string) => void>(
"LOGOUT_CALLBACK"
);
export const LOCKED_CALLBACK = new InjectionToken<() => void>("LOCKED_CALLBACK");
export const CLIENT_TYPE = new InjectionToken<boolean>("CLIENT_TYPE");
export const LOCALES_DIRECTORY = new InjectionToken<string>("LOCALES_DIRECTORY");
export const SYSTEM_LANGUAGE = new InjectionToken<string>("SYSTEM_LANGUAGE");
export const LOG_MAC_FAILURES = new InjectionToken<string>("LOG_MAC_FAILURES");
@NgModule({
declarations: [],
providers: [
@@ -532,6 +534,16 @@ export const LOG_MAC_FAILURES = new InjectionToken<string>("LOG_MAC_FAILURES");
useClass: OrganizationApiService,
deps: [ApiServiceAbstraction],
},
{
provide: ConfigServiceAbstraction,
useClass: ConfigService,
deps: [StateServiceAbstraction, ConfigApiServiceAbstraction],
},
{
provide: ConfigApiServiceAbstraction,
useClass: ConfigApiService,
deps: [ApiServiceAbstraction],
},
],
})
export class JslibServicesModule {}

View File

@@ -5,7 +5,7 @@ import { BehaviorSubject, filter, fromEvent, Observable } from "rxjs";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { ThemeType } from "@bitwarden/common/enums/themeType";
import { WINDOW } from "../jslib-services.module";
import { WINDOW } from "../injection-tokens";
import { Theme } from "./theme";
import { ThemeBuilder } from "./themeBuilder";

View File

@@ -1,32 +1,9 @@
import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service";
const originalConsole = console;
import { interceptConsole, restoreConsole } from "../shared/interceptConsole";
let caughtMessage: any;
declare let console: any;
export function interceptConsole(interceptions: any): object {
console = {
log: function () {
// eslint-disable-next-line
interceptions.log = arguments;
},
warn: function () {
// eslint-disable-next-line
interceptions.warn = arguments;
},
error: function () {
// eslint-disable-next-line
interceptions.error = arguments;
},
};
return interceptions;
}
export function restoreConsole() {
console = originalConsole;
}
describe("ConsoleLogService", () => {
let logService: ConsoleLogService;
beforeEach(() => {

View File

@@ -0,0 +1,25 @@
const originalConsole = console;
declare let console: any;
export function interceptConsole(interceptions: any): object {
console = {
log: function () {
// eslint-disable-next-line
interceptions.log = arguments;
},
warn: function () {
// eslint-disable-next-line
interceptions.warn = arguments;
},
error: function () {
// eslint-disable-next-line
interceptions.error = arguments;
},
};
return interceptions;
}
export function restoreConsole() {
console = originalConsole;
}

View File

@@ -0,0 +1,5 @@
import { ServerConfigResponse } from "@bitwarden/common/models/response/server-config-response";
export abstract class ConfigApiServiceAbstraction {
get: () => Promise<ServerConfigResponse>;
}

View File

@@ -0,0 +1,7 @@
import { Observable } from "rxjs";
import { ServerConfig } from "./server-config";
export abstract class ConfigServiceAbstraction {
serverConfig$: Observable<ServerConfig | null>;
}

View File

@@ -0,0 +1,40 @@
import {
ServerConfigData,
ThirdPartyServerConfigData,
EnvironmentServerConfigData,
} from "@bitwarden/common/models/data/server-config.data";
const dayInMilliseconds = 24 * 3600 * 1000;
const eighteenHoursInMilliseconds = 18 * 3600 * 1000;
export class ServerConfig {
version: string;
gitHash: string;
server?: ThirdPartyServerConfigData;
environment?: EnvironmentServerConfigData;
utcDate: Date;
constructor(serverConfigData: ServerConfigData) {
this.version = serverConfigData.version;
this.gitHash = serverConfigData.gitHash;
this.server = serverConfigData.server;
this.utcDate = new Date(serverConfigData.utcDate);
this.environment = serverConfigData.environment;
if (this.server?.name == null && this.server?.url == null) {
this.server = null;
}
}
private getAgeInMilliseconds(): number {
return new Date().getTime() - this.utcDate?.getTime();
}
isValid(): boolean {
return this.getAgeInMilliseconds() <= dayInMilliseconds;
}
expiresSoon(): boolean {
return this.getAgeInMilliseconds() >= eighteenHoursInMilliseconds;
}
}

View File

@@ -33,4 +33,5 @@ export abstract class EnvironmentService {
setUrlsFromStorage: () => Promise<void>;
setUrls: (urls: Urls) => Promise<Urls>;
getUrls: () => Urls;
isCloud: () => boolean;
}

View File

@@ -13,6 +13,7 @@ import { OrganizationData } from "../models/data/organizationData";
import { PolicyData } from "../models/data/policyData";
import { ProviderData } from "../models/data/providerData";
import { SendData } from "../models/data/sendData";
import { ServerConfigData } from "../models/data/server-config.data";
import { Account, AccountSettingsSettings } from "../models/domain/account";
import { EncString } from "../models/domain/encString";
import { EnvironmentUrls } from "../models/domain/environmentUrls";
@@ -319,4 +320,12 @@ export abstract class StateService<T extends Account = Account> {
setStateVersion: (value: number) => Promise<void>;
getWindow: () => Promise<WindowState>;
setWindow: (value: WindowState) => Promise<void>;
/**
* @deprecated Do not call this directly, use ConfigService
*/
getServerConfig: (options?: StorageOptions) => Promise<ServerConfigData>;
/**
* @deprecated Do not call this directly, use ConfigService
*/
setServerConfig: (value: ServerConfigData, options?: StorageOptions) => Promise<void>;
}

View File

@@ -0,0 +1,51 @@
import {
ServerConfigResponse,
ThirdPartyServerConfigResponse,
EnvironmentServerConfigResponse,
} from "../response/server-config-response";
export class ServerConfigData {
version: string;
gitHash: string;
server?: ThirdPartyServerConfigData;
environment?: EnvironmentServerConfigData;
utcDate: string;
constructor(serverConfigReponse: ServerConfigResponse) {
this.version = serverConfigReponse?.version;
this.gitHash = serverConfigReponse?.gitHash;
this.server = serverConfigReponse?.server
? new ThirdPartyServerConfigData(serverConfigReponse.server)
: null;
this.utcDate = new Date().toISOString();
this.environment = serverConfigReponse?.environment
? new EnvironmentServerConfigData(serverConfigReponse.environment)
: null;
}
}
export class ThirdPartyServerConfigData {
name: string;
url: string;
constructor(response: ThirdPartyServerConfigResponse) {
this.name = response.name;
this.url = response.url;
}
}
export class EnvironmentServerConfigData {
vault: string;
api: string;
identity: string;
notifications: string;
sso: string;
constructor(response: EnvironmentServerConfigResponse) {
this.vault = response.vault;
this.api = response.api;
this.identity = response.identity;
this.notifications = response.notifications;
this.sso = response.sso;
}
}

View File

@@ -10,6 +10,7 @@ import { OrganizationData } from "../data/organizationData";
import { PolicyData } from "../data/policyData";
import { ProviderData } from "../data/providerData";
import { SendData } from "../data/sendData";
import { ServerConfigData } from "../data/server-config.data";
import { CipherView } from "../view/cipherView";
import { CollectionView } from "../view/collectionView";
import { SendView } from "../view/sendView";
@@ -140,6 +141,7 @@ export class AccountSettings {
settings?: AccountSettingsSettings; // TODO: Merge whatever is going on here into the AccountSettings model properly
vaultTimeout?: number;
vaultTimeoutAction?: string = "lock";
serverConfig?: ServerConfigData;
}
export type AccountSettingsSettings = {

View File

@@ -0,0 +1,59 @@
import { BaseResponse } from "./baseResponse";
export class ServerConfigResponse extends BaseResponse {
version: string;
gitHash: string;
server: ThirdPartyServerConfigResponse;
environment: EnvironmentServerConfigResponse;
constructor(response: any) {
super(response);
if (response == null) {
return;
}
this.version = this.getResponseProperty("Version");
this.gitHash = this.getResponseProperty("GitHash");
this.server = new ThirdPartyServerConfigResponse(this.getResponseProperty("Server"));
this.environment = new EnvironmentServerConfigResponse(this.getResponseProperty("Environment"));
}
}
export class EnvironmentServerConfigResponse extends BaseResponse {
vault: string;
api: string;
identity: string;
notifications: string;
sso: string;
constructor(data: any = null) {
super(data);
if (data == null) {
return;
}
this.vault = this.getResponseProperty("Vault");
this.api = this.getResponseProperty("Api");
this.identity = this.getResponseProperty("Identity");
this.notifications = this.getResponseProperty("Notifications");
this.sso = this.getResponseProperty("Sso");
}
}
export class ThirdPartyServerConfigResponse extends BaseResponse {
name: string;
url: string;
constructor(data: any = null) {
super(data);
if (data == null) {
return;
}
this.name = this.getResponseProperty("Name");
this.url = this.getResponseProperty("Url");
}
}

View File

@@ -0,0 +1,12 @@
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { ConfigApiServiceAbstraction as ConfigApiServiceAbstraction } from "@bitwarden/common/abstractions/config/config-api.service.abstraction";
import { ServerConfigResponse } from "@bitwarden/common/models/response/server-config-response";
export class ConfigApiService implements ConfigApiServiceAbstraction {
constructor(private apiService: ApiService) {}
async get(): Promise<ServerConfigResponse> {
const r = await this.apiService.send("GET", "/config", null, true, true);
return new ServerConfigResponse(r);
}
}

View File

@@ -0,0 +1,61 @@
import { BehaviorSubject, concatMap, map, switchMap, timer, EMPTY } from "rxjs";
import { ServerConfigData } from "@bitwarden/common/models/data/server-config.data";
import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction";
import { ConfigServiceAbstraction } from "../../abstractions/config/config.service.abstraction";
import { ServerConfig } from "../../abstractions/config/server-config";
import { StateService } from "../../abstractions/state.service";
export class ConfigService implements ConfigServiceAbstraction {
private _serverConfig = new BehaviorSubject<ServerConfig | null>(null);
serverConfig$ = this._serverConfig.asObservable();
constructor(
private stateService: StateService,
private configApiService: ConfigApiServiceAbstraction
) {
this.stateService.activeAccountUnlocked$
.pipe(
switchMap((unlocked) => {
if (!unlocked) {
this._serverConfig.next(null);
return EMPTY;
}
// Re-fetch the server config every hour
return timer(0, 3600 * 1000).pipe(map(() => unlocked));
}),
concatMap(async (unlocked) => {
return unlocked ? await this.buildServerConfig() : null;
})
)
.subscribe((serverConfig) => {
this._serverConfig.next(serverConfig);
});
}
private async buildServerConfig(): Promise<ServerConfig> {
const data = await this.stateService.getServerConfig();
const domain = data ? new ServerConfig(data) : null;
if (domain == null || !domain.isValid() || domain.expiresSoon()) {
const value = await this.fetchServerConfig();
return value ?? domain;
}
return domain;
}
private async fetchServerConfig(): Promise<ServerConfig> {
const response = await this.configApiService.get();
const data = new ServerConfigData(response);
if (data != null) {
await this.stateService.setServerConfig(data);
return new ServerConfig(data);
}
return null;
}
}

View File

@@ -207,4 +207,10 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
return url.trim();
}
isCloud(): boolean {
return ["https://api.bitwarden.com", "https://vault.bitwarden.com/api"].includes(
this.getApiUrl()
);
}
}

View File

@@ -5,7 +5,7 @@ import { EventType } from "@bitwarden/common/enums/eventType";
* If you want to use this, don't.
* If you think you should use that after the warning, don't.
*/
export default class NoOpEventService implements EventService {
export class NoopEventService implements EventService {
constructor() {
if (chrome.runtime.getManifest().manifest_version !== 3) {
throw new Error("You are not allowed to use this when not in manifest_version 3");

View File

@@ -21,6 +21,7 @@ import { OrganizationData } from "../models/data/organizationData";
import { PolicyData } from "../models/data/policyData";
import { ProviderData } from "../models/data/providerData";
import { SendData } from "../models/data/sendData";
import { ServerConfigData } from "../models/data/server-config.data";
import {
Account,
AccountData,
@@ -2277,6 +2278,23 @@ export class StateService<
);
}
async setServerConfig(value: ServerConfigData, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
);
account.settings.serverConfig = value;
return await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
);
}
async getServerConfig(options: StorageOptions): Promise<ServerConfigData> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
)?.settings?.serverConfig;
}
protected async getGlobals(options: StorageOptions): Promise<TGlobalState> {
let globals: TGlobalState;
if (this.useMemory(options.storageLocation)) {

View File

@@ -1,15 +1,22 @@
import { DialogModule as CdkDialogModule } from "@angular/cdk/dialog";
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { DialogCloseDirective } from "./dialog-close.directive";
import { SharedModule } from "../shared";
import { DialogService } from "./dialog.service";
import { DialogComponent } from "./dialog/dialog.component";
import { DialogCloseDirective } from "./directives/dialog-close.directive";
import { DialogTitleContainerDirective } from "./directives/dialog-title-container.directive";
import { SimpleDialogComponent } from "./simple-dialog/simple-dialog.component";
@NgModule({
imports: [CommonModule, CdkDialogModule],
declarations: [DialogCloseDirective, DialogComponent, SimpleDialogComponent],
imports: [SharedModule, CdkDialogModule],
declarations: [
DialogCloseDirective,
DialogComponent,
DialogTitleContainerDirective,
SimpleDialogComponent,
],
exports: [CdkDialogModule, DialogComponent, SimpleDialogComponent],
providers: [DialogService],
})

View File

@@ -0,0 +1,97 @@
import { DialogModule, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { ButtonModule } from "../button";
import { I18nMockService } from "../utils/i18n-mock.service";
import { DialogService } from "./dialog.service";
import { DialogComponent } from "./dialog/dialog.component";
import { DialogCloseDirective } from "./directives/dialog-close.directive";
import { DialogTitleContainerDirective } from "./directives/dialog-title-container.directive";
interface Animal {
animal: string;
}
@Component({
selector: "app-story-dialog",
template: `<button bitButton (click)="openDialog()">Open Dialog</button>`,
})
class StoryDialogComponent {
constructor(public dialogService: DialogService) {}
openDialog() {
this.dialogService.open(StoryDialogContentComponent, {
data: {
animal: "panda",
},
});
}
}
@Component({
selector: "story-dialog-content",
template: `
<bit-dialog [dialogSize]="large">
<span bitDialogTitle>Dialog Title</span>
<span bitDialogContent>
Dialog body text goes here.
<br />
Animal: {{ animal }}
</span>
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
<button bitButton buttonType="primary" (click)="dialogRef.close()">Save</button>
<button bitButton buttonType="secondary" bitDialogClose>Cancel</button>
</div>
</bit-dialog>
`,
})
class StoryDialogContentComponent {
constructor(public dialogRef: DialogRef, @Inject(DIALOG_DATA) private data: Animal) {}
get animal() {
return this.data?.animal;
}
}
export default {
title: "Component Library/Dialogs/Service",
component: StoryDialogComponent,
decorators: [
moduleMetadata({
declarations: [
DialogCloseDirective,
DialogComponent,
DialogTitleContainerDirective,
StoryDialogContentComponent,
],
imports: [ButtonModule, DialogModule],
providers: [
DialogService,
{
provide: I18nService,
useFactory: () => {
return new I18nMockService({
close: "Close",
});
},
},
],
}),
],
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library",
},
},
} as Meta;
const Template: Story<StoryDialogComponent> = (args: StoryDialogComponent) => ({
props: args,
});
export const Default = Template.bind({});

View File

@@ -5,21 +5,26 @@
<div
class="tw-flex tw-gap-4 tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300 tw-p-4"
>
<h2 class="tw-mb-0 tw-grow tw-text-lg tw-uppercase">
<ng-content select="[bit-dialog-title]"></ng-content>
</h2>
<button class="tw-border-0 tw-bg-transparent tw-p-0" bitDialogClose>
<h1 bitDialogTitleContainer class="tw-mb-0 tw-grow tw-text-lg tw-uppercase">
<ng-content select="[bitDialogTitle]"></ng-content>
</h1>
<button
bitDialogClose
class="tw-border-0 tw-bg-transparent tw-p-0"
title="{{ 'close' | i18n }}"
attr.aria-label="{{ 'close' | i18n }}"
>
<i class="bwi bwi-close tw-text-xs tw-font-bold tw-text-main" aria-hidden="true"></i>
</button>
</div>
<div class="tw-overflow-y-auto tw-p-4 tw-pb-8">
<ng-content select="[bit-dialog-content]"></ng-content>
<ng-content select="[bitDialogContent]"></ng-content>
</div>
<div
class="tw-border-0 tw-border-t tw-border-solid tw-border-secondary-300 tw-bg-background-alt tw-p-4"
>
<ng-content select="[bit-dialog-footer]"></ng-content>
<ng-content select="[bitDialogFooter]"></ng-content>
</div>
</div>

View File

@@ -1,6 +1,12 @@
import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { ButtonModule } from "../../button";
import { SharedModule } from "../../shared";
import { I18nMockService } from "../../utils/i18n-mock.service";
import { DialogCloseDirective } from "../directives/dialog-close.directive";
import { DialogTitleContainerDirective } from "../directives/dialog-title-container.directive";
import { DialogComponent } from "./dialog.component";
@@ -9,7 +15,18 @@ export default {
component: DialogComponent,
decorators: [
moduleMetadata({
imports: [ButtonModule],
imports: [SharedModule, ButtonModule],
declarations: [DialogTitleContainerDirective, DialogCloseDirective],
providers: [
{
provide: I18nService,
useFactory: () => {
return new I18nMockService({
close: "Close",
});
},
},
],
}),
],
args: {
@@ -27,9 +44,9 @@ const Template: Story<DialogComponent> = (args: DialogComponent) => ({
props: args,
template: `
<bit-dialog [dialogSize]="dialogSize">
<span bit-dialog-title>{{title}}</span>
<span bit-dialog-content>Dialog body text goes here.</span>
<div bit-dialog-footer class="tw-flex tw-flex-row tw-gap-2">
<span bitDialogTitle>{{title}}</span>
<span bitDialogContent>Dialog body text goes here.</span>
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
<button bitButton buttonType="primary">Save</button>
<button bitButton buttonType="secondary">Cancel</button>
</div>
@@ -59,15 +76,15 @@ const TemplateScrolling: Story<DialogComponent> = (args: DialogComponent) => ({
props: args,
template: `
<bit-dialog [dialogSize]="dialogSize">
<span bit-dialog-title>Scrolling Example</span>
<span bit-dialog-content>
<span bitDialogTitle>Scrolling Example</span>
<span bitDialogContent>
Dialog body text goes here.<br>
<ng-container *ngFor="let _ of [].constructor(100)">
repeating lines of characters <br>
</ng-container>
end of sequence!
</span>
<div bit-dialog-footer class="tw-flex tw-flex-row tw-gap-2">
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
<button bitButton buttonType="primary">Save</button>
<button bitButton buttonType="secondary">Cancel</button>
</div>

View File

@@ -1,18 +1,15 @@
import { DialogRef } from "@angular/cdk/dialog";
import { Directive, Input, Optional } from "@angular/core";
import { Directive, HostListener, Input, Optional } from "@angular/core";
@Directive({
selector: "[bitDialogClose]",
host: {
"(click)": "close()",
},
})
export class DialogCloseDirective {
@Input("bit-dialog-close") dialogResult: any;
constructor(@Optional() public dialogRef: DialogRef<any>) {}
close() {
@HostListener("click") close(): void {
this.dialogRef.close(this.dialogResult);
}
}

View File

@@ -0,0 +1,30 @@
import { CdkDialogContainer, DialogRef } from "@angular/cdk/dialog";
import { Directive, HostBinding, Input, OnInit, Optional } from "@angular/core";
// Increments for each instance of this component
let nextId = 0;
@Directive({
selector: "[bitDialogTitleContainer]",
})
export class DialogTitleContainerDirective implements OnInit {
@HostBinding("id") id = `bit-dialog-title-${nextId++}`;
@Input() simple = false;
constructor(@Optional() private dialogRef: DialogRef<any>) {}
ngOnInit(): void {
// Based on angular/components, licensed under MIT
// https://github.com/angular/components/blob/14.2.0/src/material/dialog/dialog-content-directives.ts#L121-L128
if (this.dialogRef) {
Promise.resolve().then(() => {
const container = this.dialogRef.containerInstance as CdkDialogContainer;
if (container && !container._ariaLabelledBy) {
container._ariaLabelledBy = this.id;
}
});
}
}
}

View File

@@ -4,9 +4,10 @@ import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { ButtonModule } from "../button";
import { DialogCloseDirective } from "./dialog-close.directive";
import { DialogService } from "./dialog.service";
import { DialogComponent } from "./dialog/dialog.component";
import { DialogCloseDirective } from "./directives/dialog-close.directive";
import { DialogTitleContainerDirective } from "./directives/dialog-title-container.directive";
import { SimpleDialogComponent } from "./simple-dialog/simple-dialog.component";
interface Animal {
animal: string;
@@ -14,7 +15,7 @@ interface Animal {
@Component({
selector: "app-story-dialog",
template: `<button bitButton (click)="openDialog()">Open Dialog</button>`,
template: `<button bitButton (click)="openDialog()">Open Simple Dialog</button>`,
})
class StoryDialogComponent {
constructor(public dialogService: DialogService) {}
@@ -31,18 +32,18 @@ class StoryDialogComponent {
@Component({
selector: "story-dialog-content",
template: `
<bit-dialog [dialogSize]="large">
<span bit-dialog-title>Dialog Title</span>
<span bit-dialog-content>
<bit-simple-dialog>
<span bitDialogTitle>Dialog Title</span>
<span bitDialogContent>
Dialog body text goes here.
<br />
Animal: {{ animal }}
</span>
<div bit-dialog-footer class="tw-flex tw-flex-row tw-gap-2">
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
<button bitButton buttonType="primary" (click)="dialogRef.close()">Save</button>
<button bitButton buttonType="secondary" bitDialogClose>Cancel</button>
</div>
</bit-dialog>
</bit-simple-dialog>
`,
})
class StoryDialogContentComponent {
@@ -54,11 +55,16 @@ class StoryDialogContentComponent {
}
export default {
title: "Component Library/Dialogs/Service",
title: "Component Library/Dialogs/Service/Simple",
component: StoryDialogComponent,
decorators: [
moduleMetadata({
declarations: [DialogComponent, StoryDialogContentComponent, DialogCloseDirective],
declarations: [
DialogCloseDirective,
SimpleDialogComponent,
DialogTitleContainerDirective,
StoryDialogContentComponent,
],
imports: [ButtonModule, DialogModule],
providers: [DialogService],
}),

View File

@@ -1,19 +1,19 @@
<div
class="tw-my-4 tw-flex tw-max-h-screen tw-max-w-sm tw-flex-col tw-overflow-hidden tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-text-contrast tw-text-main"
class="tw-my-4 tw-flex tw-max-h-screen tw-w-96 tw-max-w-90vw tw-flex-col tw-overflow-hidden tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-text-contrast tw-text-main"
>
<div class="tw-flex tw-flex-col tw-items-center tw-gap-2 tw-px-4 tw-pt-4 tw-text-center">
<ng-content *ngIf="hasIcon; else elseBlock" select="[bit-dialog-icon]"></ng-content>
<ng-template #elseBlock>
<i class="bwi bwi-exclamation-triangle tw-text-3xl tw-text-warning" aria-hidden="true"></i>
</ng-template>
<h2 class="tw-mb-0 tw-text-base tw-font-semibold">
<ng-content select="[bit-dialog-title]"></ng-content>
</h2>
<h1 bitDialogTitleContainer class="tw-mb-0 tw-text-base tw-font-semibold">
<ng-content select="[bitDialogTitle]"></ng-content>
</h1>
</div>
<div class="tw-overflow-y-auto tw-px-4 tw-pt-2 tw-pb-4 tw-text-center tw-text-base">
<ng-content select="[bit-dialog-content]"></ng-content>
<ng-content select="[bitDialogContent]"></ng-content>
</div>
<div class="tw-border-0 tw-border-t tw-border-solid tw-border-secondary-300 tw-p-4">
<ng-content select="[bit-dialog-footer]"></ng-content>
<ng-content select="[bitDialogFooter]"></ng-content>
</div>
</div>

View File

@@ -1,6 +1,7 @@
import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { ButtonModule } from "../../button";
import { DialogTitleContainerDirective } from "../directives/dialog-title-container.directive";
import { IconDirective, SimpleDialogComponent } from "./simple-dialog.component";
@@ -10,7 +11,7 @@ export default {
decorators: [
moduleMetadata({
imports: [ButtonModule],
declarations: [IconDirective],
declarations: [IconDirective, DialogTitleContainerDirective],
}),
],
parameters: {
@@ -25,9 +26,9 @@ const Template: Story<SimpleDialogComponent> = (args: SimpleDialogComponent) =>
props: args,
template: `
<bit-simple-dialog>
<span bit-dialog-title>Alert Dialog</span>
<span bit-dialog-content>Message Content</span>
<div bit-dialog-footer class="tw-flex tw-flex-row tw-gap-2">
<span bitDialogTitle>Alert Dialog</span>
<span bitDialogContent>Message Content</span>
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
<button bitButton buttonType="primary">Yes</button>
<button bitButton buttonType="secondary">No</button>
</div>
@@ -42,9 +43,9 @@ const TemplateWithIcon: Story<SimpleDialogComponent> = (args: SimpleDialogCompon
template: `
<bit-simple-dialog>
<i bit-dialog-icon class="bwi bwi-star tw-text-3xl tw-text-success" aria-hidden="true"></i>
<span bit-dialog-title>Premium Subscription Available</span>
<span bit-dialog-content> Message Content</span>
<div bit-dialog-footer class="tw-flex tw-flex-row tw-gap-2">
<span bitDialogTitle>Premium Subscription Available</span>
<span bitDialogContent> Message Content</span>
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
<button bitButton buttonType="primary">Yes</button>
<button bitButton buttonType="secondary">No</button>
</div>
@@ -58,8 +59,8 @@ const TemplateScroll: Story<SimpleDialogComponent> = (args: SimpleDialogComponen
props: args,
template: `
<bit-simple-dialog>
<span bit-dialog-title>Alert Dialog</span>
<span bit-dialog-content>
<span bitDialogTitle>Alert Dialog</span>
<span bitDialogContent>
Message Content
Message text goes here.<br>
<ng-container *ngFor="let _ of [].constructor(100)">
@@ -67,7 +68,7 @@ const TemplateScroll: Story<SimpleDialogComponent> = (args: SimpleDialogComponen
</ng-container>
end of sequence!
</span>
<div bit-dialog-footer class="tw-flex tw-flex-row tw-gap-2">
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
<button bitButton buttonType="primary">Yes</button>
<button bitButton buttonType="secondary">No</button>
</div>

View File

@@ -1,10 +1,8 @@
import { CommonModule } from "@angular/common";
import { NgModule, Pipe, PipeTransform } from "@angular/core";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { NgModule } from "@angular/core";
import { BitInputDirective } from "../input/input.directive";
import { InputModule } from "../input/input.module";
import { SharedModule } from "../shared";
import { BitErrorSummary } from "./error-summary.component";
import { BitErrorComponent } from "./error.component";
@@ -14,22 +12,8 @@ import { BitLabel } from "./label.directive";
import { BitPrefixDirective } from "./prefix.directive";
import { BitSuffixDirective } from "./suffix.directive";
/**
* Temporarily duplicate this pipe
*/
@Pipe({
name: "i18n",
})
export class I18nPipe implements PipeTransform {
constructor(private i18nService: I18nService) {}
transform(id: string, p1?: string, p2?: string, p3?: string): string {
return this.i18nService.t(id, p1, p2, p3);
}
}
@NgModule({
imports: [CommonModule, InputModule],
imports: [SharedModule, InputModule],
exports: [
BitErrorComponent,
BitErrorSummary,
@@ -48,7 +32,6 @@ export class I18nPipe implements PipeTransform {
BitLabel,
BitPrefixDirective,
BitSuffixDirective,
I18nPipe,
],
})
export class FormFieldModule {}

View File

@@ -0,0 +1,17 @@
import { Pipe, PipeTransform } from "@angular/core";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
/**
* Temporarily duplicate this pipe
*/
@Pipe({
name: "i18n",
})
export class I18nPipe implements PipeTransform {
constructor(private i18nService: I18nService) {}
transform(id: string, p1?: string, p2?: string, p3?: string): string {
return this.i18nService.t(id, p1, p2, p3);
}
}

View File

@@ -0,0 +1 @@
export * from "./shared.module";

View File

@@ -0,0 +1,11 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { I18nPipe } from "./i18n.pipe";
@NgModule({
imports: [CommonModule],
declarations: [I18nPipe],
exports: [CommonModule, I18nPipe],
})
export class SharedModule {}

View File

@@ -80,6 +80,7 @@ module.exports = {
},
maxWidth: ({ theme }) => ({
...theme("width"),
"90vw": "90vw",
}),
},
},

View File

@@ -1,10 +1,7 @@
import {
interceptConsole,
restoreConsole,
} from "@bitwarden/common/../spec/services/consolelog.service.spec";
import { ConsoleLogService } from "@bitwarden/node/cli/services/consoleLog.service";
import { interceptConsole, restoreConsole } from "../../../common/spec/shared/interceptConsole";
let caughtMessage: any = {};
describe("CLI Console log service", () => {

View File

@@ -10,10 +10,15 @@ module.exports = {
// Also anecdotally improves performance when run locally
maxWorkers: 3,
// Jest does not use tsconfig.spec.json by default
globals: {
"ts-jest": {
// Jest does not use tsconfig.spec.json by default
tsconfig: "<rootDir>/tsconfig.spec.json",
// Further workaround for memory leak, recommended here:
// https://github.com/kulshekhar/ts-jest/issues/1967#issuecomment-697494014
// Makes tests run faster and reduces size/rate of leak, but loses typechecking on test code
// See https://bitwarden.atlassian.net/browse/EC-497 for more info
isolatedModules: true,
},
},
};