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:
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
18
libs/angular/src/services/injection-tokens.ts
Normal file
18
libs/angular/src/services/injection-tokens.ts
Normal 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");
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
25
libs/common/spec/shared/interceptConsole.ts
Normal file
25
libs/common/spec/shared/interceptConsole.ts
Normal 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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ServerConfigResponse } from "@bitwarden/common/models/response/server-config-response";
|
||||
|
||||
export abstract class ConfigApiServiceAbstraction {
|
||||
get: () => Promise<ServerConfigResponse>;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { ServerConfig } from "./server-config";
|
||||
|
||||
export abstract class ConfigServiceAbstraction {
|
||||
serverConfig$: Observable<ServerConfig | null>;
|
||||
}
|
||||
40
libs/common/src/abstractions/config/server-config.ts
Normal file
40
libs/common/src/abstractions/config/server-config.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -33,4 +33,5 @@ export abstract class EnvironmentService {
|
||||
setUrlsFromStorage: () => Promise<void>;
|
||||
setUrls: (urls: Urls) => Promise<Urls>;
|
||||
getUrls: () => Urls;
|
||||
isCloud: () => boolean;
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
51
libs/common/src/models/data/server-config.data.ts
Normal file
51
libs/common/src/models/data/server-config.data.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
59
libs/common/src/models/response/server-config-response.ts
Normal file
59
libs/common/src/models/response/server-config-response.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
12
libs/common/src/services/config/config-api.service.ts
Normal file
12
libs/common/src/services/config/config-api.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
61
libs/common/src/services/config/config.service.ts
Normal file
61
libs/common/src/services/config/config.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
97
libs/components/src/dialog/dialog.service.stories.ts
Normal file
97
libs/components/src/dialog/dialog.service.stories.ts
Normal 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({});
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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],
|
||||
}),
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
17
libs/components/src/shared/i18n.pipe.ts
Normal file
17
libs/components/src/shared/i18n.pipe.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
1
libs/components/src/shared/index.ts
Normal file
1
libs/components/src/shared/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./shared.module";
|
||||
11
libs/components/src/shared/shared.module.ts
Normal file
11
libs/components/src/shared/shared.module.ts
Normal 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 {}
|
||||
@@ -80,6 +80,7 @@ module.exports = {
|
||||
},
|
||||
maxWidth: ({ theme }) => ({
|
||||
...theme("width"),
|
||||
"90vw": "90vw",
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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", () => {
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user