mirror of
https://github.com/bitwarden/browser
synced 2025-12-21 02:33:46 +00:00
Merge branch 'master' of https://github.com/bitwarden/bitwarden into PS-1854-split-split-services-between-backgrond-and-visualizations
This commit is contained in:
@@ -281,12 +281,12 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
||||
return (
|
||||
this.authService.authingWithPassword() ||
|
||||
this.authService.authingWithSso() ||
|
||||
this.authService.authingWithApiKey() ||
|
||||
this.authService.authingWithUserApiKey() ||
|
||||
this.authService.authingWithPasswordless()
|
||||
);
|
||||
}
|
||||
|
||||
get needsLock(): boolean {
|
||||
return this.authService.authingWithSso() || this.authService.authingWithApiKey();
|
||||
return this.authService.authingWithSso() || this.authService.authingWithUserApiKey();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,10 @@ import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/abstr
|
||||
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/abstractions/messaging.service";
|
||||
import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationService as OrganizationServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||
import {
|
||||
InternalOrganizationService,
|
||||
OrganizationService as OrganizationServiceAbstraction,
|
||||
} from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||
import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "@bitwarden/common/abstractions/passwordGeneration.service";
|
||||
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@bitwarden/common/abstractions/passwordReprompt.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
@@ -356,7 +359,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
StateServiceAbstraction,
|
||||
ProviderServiceAbstraction,
|
||||
FolderApiServiceAbstraction,
|
||||
SyncNotifierServiceAbstraction,
|
||||
OrganizationServiceAbstraction,
|
||||
LOGOUT_CALLBACK,
|
||||
],
|
||||
},
|
||||
@@ -506,6 +509,8 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
LogService,
|
||||
OrganizationServiceAbstraction,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
SyncNotifierServiceAbstraction,
|
||||
MessagingServiceAbstraction,
|
||||
LOGOUT_CALLBACK,
|
||||
],
|
||||
},
|
||||
@@ -522,7 +527,11 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
{
|
||||
provide: OrganizationServiceAbstraction,
|
||||
useClass: OrganizationService,
|
||||
deps: [StateServiceAbstraction, SyncNotifierServiceAbstraction],
|
||||
deps: [StateServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: InternalOrganizationService,
|
||||
useExisting: OrganizationServiceAbstraction,
|
||||
},
|
||||
{
|
||||
provide: ProviderServiceAbstraction,
|
||||
|
||||
@@ -12,13 +12,13 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { TokenService } from "@bitwarden/common/abstractions/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/abstractions/twoFactor.service";
|
||||
import { ApiLogInStrategy } from "@bitwarden/common/misc/logInStrategies/apiLogin.strategy";
|
||||
import { UserApiLogInStrategy } from "@bitwarden/common/misc/logInStrategies/user-api-login.strategy";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { ApiLogInCredentials } from "@bitwarden/common/models/domain/log-in-credentials";
|
||||
import { UserApiLogInCredentials } from "@bitwarden/common/models/domain/log-in-credentials";
|
||||
|
||||
import { identityTokenResponseFactory } from "./logIn.strategy.spec";
|
||||
|
||||
describe("ApiLogInStrategy", () => {
|
||||
describe("UserApiLogInStrategy", () => {
|
||||
let cryptoService: SubstituteOf<CryptoService>;
|
||||
let apiService: SubstituteOf<ApiService>;
|
||||
let tokenService: SubstituteOf<TokenService>;
|
||||
@@ -31,8 +31,8 @@ describe("ApiLogInStrategy", () => {
|
||||
let stateService: SubstituteOf<StateService>;
|
||||
let twoFactorService: SubstituteOf<TwoFactorService>;
|
||||
|
||||
let apiLogInStrategy: ApiLogInStrategy;
|
||||
let credentials: ApiLogInCredentials;
|
||||
let apiLogInStrategy: UserApiLogInStrategy;
|
||||
let credentials: UserApiLogInCredentials;
|
||||
|
||||
const deviceId = Utils.newGuid();
|
||||
const keyConnectorUrl = "KEY_CONNECTOR_URL";
|
||||
@@ -55,7 +55,7 @@ describe("ApiLogInStrategy", () => {
|
||||
appIdService.getAppId().resolves(deviceId);
|
||||
tokenService.getTwoFactorToken().resolves(null);
|
||||
|
||||
apiLogInStrategy = new ApiLogInStrategy(
|
||||
apiLogInStrategy = new UserApiLogInStrategy(
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
@@ -69,7 +69,7 @@ describe("ApiLogInStrategy", () => {
|
||||
keyConnectorService
|
||||
);
|
||||
|
||||
credentials = new ApiLogInCredentials(apiClientId, apiClientSecret);
|
||||
credentials = new UserApiLogInCredentials(apiClientId, apiClientSecret);
|
||||
});
|
||||
|
||||
it("sends api key credentials to the server", async () => {
|
||||
@@ -1,12 +1,9 @@
|
||||
import { MockProxy, mock, any, mockClear, matches } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom, Subject } from "rxjs";
|
||||
import { MockProxy, mock, any, mockClear } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
||||
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { SyncNotifierService } from "@bitwarden/common/abstractions/sync/syncNotifier.service.abstraction";
|
||||
import { OrganizationData } from "@bitwarden/common/models/data/organization.data";
|
||||
import { SyncResponse } from "@bitwarden/common/models/response/sync.response";
|
||||
import { OrganizationService } from "@bitwarden/common/services/organization/organization.service";
|
||||
import { SyncEventArgs } from "@bitwarden/common/types/syncEventArgs";
|
||||
|
||||
describe("Organization Service", () => {
|
||||
let organizationService: OrganizationService;
|
||||
@@ -14,8 +11,6 @@ describe("Organization Service", () => {
|
||||
let stateService: MockProxy<StateService>;
|
||||
let activeAccount: BehaviorSubject<string>;
|
||||
let activeAccountUnlocked: BehaviorSubject<boolean>;
|
||||
let syncNotifierService: MockProxy<SyncNotifierService>;
|
||||
let sync: Subject<SyncEventArgs>;
|
||||
|
||||
const resetStateService = async (
|
||||
customizeStateService: (stateService: MockProxy<StateService>) => void
|
||||
@@ -25,7 +20,7 @@ describe("Organization Service", () => {
|
||||
stateService.activeAccount$ = activeAccount;
|
||||
stateService.activeAccountUnlocked$ = activeAccountUnlocked;
|
||||
customizeStateService(stateService);
|
||||
organizationService = new OrganizationService(stateService, syncNotifierService);
|
||||
organizationService = new OrganizationService(stateService);
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
};
|
||||
|
||||
@@ -41,12 +36,7 @@ describe("Organization Service", () => {
|
||||
"1": organizationData("1", "Test Org"),
|
||||
});
|
||||
|
||||
sync = new Subject<SyncEventArgs>();
|
||||
|
||||
syncNotifierService = mock<SyncNotifierService>();
|
||||
syncNotifierService.sync$ = sync;
|
||||
|
||||
organizationService = new OrganizationService(stateService, syncNotifierService);
|
||||
organizationService = new OrganizationService(stateService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -169,36 +159,6 @@ describe("Organization Service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("syncEvent works", () => {
|
||||
it("Complete event updates data", async () => {
|
||||
sync.next({
|
||||
status: "Completed",
|
||||
successfully: true,
|
||||
data: new SyncResponse({
|
||||
profile: {
|
||||
organizations: [
|
||||
{
|
||||
id: "1",
|
||||
name: "Updated Name",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
|
||||
expect(stateService.setOrganizations).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(stateService.setOrganizations).toHaveBeenLastCalledWith(
|
||||
matches((organizationData: { [id: string]: OrganizationData }) => {
|
||||
const organization = organizationData["1"];
|
||||
return organization?.name === "Updated Name";
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function organizationData(id: string, name: string) {
|
||||
const data = new OrganizationData({} as any);
|
||||
data.id = id;
|
||||
|
||||
@@ -24,9 +24,9 @@ import { EmergencyAccessUpdateRequest } from "../models/request/emergency-access
|
||||
import { EventRequest } from "../models/request/event.request";
|
||||
import { GroupRequest } from "../models/request/group.request";
|
||||
import { IapCheckRequest } from "../models/request/iap-check.request";
|
||||
import { ApiTokenRequest } from "../models/request/identity-token/api-token.request";
|
||||
import { PasswordTokenRequest } from "../models/request/identity-token/password-token.request";
|
||||
import { SsoTokenRequest } from "../models/request/identity-token/sso-token.request";
|
||||
import { UserApiTokenRequest } from "../models/request/identity-token/user-api-token.request";
|
||||
import { ImportCiphersRequest } from "../models/request/import-ciphers.request";
|
||||
import { ImportOrganizationCiphersRequest } from "../models/request/import-organization-ciphers.request";
|
||||
import { KdfRequest } from "../models/request/kdf.request";
|
||||
@@ -175,7 +175,7 @@ export abstract class ApiService {
|
||||
) => Promise<any>;
|
||||
|
||||
postIdentityToken: (
|
||||
request: PasswordTokenRequest | SsoTokenRequest | ApiTokenRequest
|
||||
request: PasswordTokenRequest | SsoTokenRequest | UserApiTokenRequest
|
||||
) => Promise<IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse>;
|
||||
refreshIdentityToken: () => Promise<any>;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Observable } from "rxjs";
|
||||
import { AuthenticationStatus } from "../enums/authenticationStatus";
|
||||
import { AuthResult } from "../models/domain/auth-result";
|
||||
import {
|
||||
ApiLogInCredentials,
|
||||
UserApiLogInCredentials,
|
||||
PasswordLogInCredentials,
|
||||
SsoLogInCredentials,
|
||||
PasswordlessLogInCredentials,
|
||||
@@ -20,7 +20,7 @@ export abstract class AuthService {
|
||||
|
||||
logIn: (
|
||||
credentials:
|
||||
| ApiLogInCredentials
|
||||
| UserApiLogInCredentials
|
||||
| PasswordLogInCredentials
|
||||
| SsoLogInCredentials
|
||||
| PasswordlessLogInCredentials
|
||||
@@ -31,7 +31,7 @@ export abstract class AuthService {
|
||||
) => Promise<AuthResult>;
|
||||
logOut: (callback: () => void) => void;
|
||||
makePreloginKey: (masterPassword: string, email: string) => Promise<SymmetricCryptoKey>;
|
||||
authingWithApiKey: () => boolean;
|
||||
authingWithUserApiKey: () => boolean;
|
||||
authingWithSso: () => boolean;
|
||||
authingWithPassword: () => boolean;
|
||||
authingWithPasswordless: () => boolean;
|
||||
|
||||
@@ -12,6 +12,10 @@ export abstract class FolderService {
|
||||
clearCache: () => Promise<void>;
|
||||
encrypt: (model: FolderView, key?: SymmetricCryptoKey) => Promise<Folder>;
|
||||
get: (id: string) => Promise<Folder>;
|
||||
/**
|
||||
* @deprecated Only use in CLI!
|
||||
*/
|
||||
getFromState: (id: string) => Promise<Folder>;
|
||||
/**
|
||||
* @deprecated Only use in CLI!
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { map, Observable } from "rxjs";
|
||||
|
||||
import { Utils } from "../../misc/utils";
|
||||
import { OrganizationData } from "../../models/data/organization.data";
|
||||
import { Organization } from "../../models/domain/organization";
|
||||
import { I18nService } from "../i18n.service";
|
||||
|
||||
@@ -83,3 +84,7 @@ export abstract class OrganizationService {
|
||||
canManageSponsorships: () => Promise<boolean>;
|
||||
hasOrganizations: () => boolean;
|
||||
}
|
||||
|
||||
export abstract class InternalOrganizationService extends OrganizationService {
|
||||
replace: (organizations: { [id: string]: OrganizationData }) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export enum AuthenticationType {
|
||||
Password = 0,
|
||||
Sso = 1,
|
||||
Api = 2,
|
||||
UserApi = 2,
|
||||
Passwordless = 3,
|
||||
}
|
||||
|
||||
@@ -11,23 +11,23 @@ import { TwoFactorProviderType } from "../../enums/twoFactorProviderType";
|
||||
import { Account, AccountProfile, AccountTokens } from "../../models/domain/account";
|
||||
import { AuthResult } from "../../models/domain/auth-result";
|
||||
import {
|
||||
ApiLogInCredentials,
|
||||
UserApiLogInCredentials,
|
||||
PasswordLogInCredentials,
|
||||
SsoLogInCredentials,
|
||||
PasswordlessLogInCredentials,
|
||||
} from "../../models/domain/log-in-credentials";
|
||||
import { DeviceRequest } from "../../models/request/device.request";
|
||||
import { ApiTokenRequest } from "../../models/request/identity-token/api-token.request";
|
||||
import { PasswordTokenRequest } from "../../models/request/identity-token/password-token.request";
|
||||
import { SsoTokenRequest } from "../../models/request/identity-token/sso-token.request";
|
||||
import { TokenTwoFactorRequest } from "../../models/request/identity-token/token-two-factor.request";
|
||||
import { UserApiTokenRequest } from "../../models/request/identity-token/user-api-token.request";
|
||||
import { KeysRequest } from "../../models/request/keys.request";
|
||||
import { IdentityCaptchaResponse } from "../../models/response/identity-captcha.response";
|
||||
import { IdentityTokenResponse } from "../../models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "../../models/response/identity-two-factor.response";
|
||||
|
||||
export abstract class LogInStrategy {
|
||||
protected abstract tokenRequest: ApiTokenRequest | PasswordTokenRequest | SsoTokenRequest;
|
||||
protected abstract tokenRequest: UserApiTokenRequest | PasswordTokenRequest | SsoTokenRequest;
|
||||
protected captchaBypassToken: string = null;
|
||||
|
||||
constructor(
|
||||
@@ -44,7 +44,7 @@ export abstract class LogInStrategy {
|
||||
|
||||
abstract logIn(
|
||||
credentials:
|
||||
| ApiLogInCredentials
|
||||
| UserApiLogInCredentials
|
||||
| PasswordLogInCredentials
|
||||
| SsoLogInCredentials
|
||||
| PasswordlessLogInCredentials
|
||||
|
||||
@@ -9,14 +9,14 @@ import { PlatformUtilsService } from "../../abstractions/platformUtils.service";
|
||||
import { StateService } from "../../abstractions/state.service";
|
||||
import { TokenService } from "../../abstractions/token.service";
|
||||
import { TwoFactorService } from "../../abstractions/twoFactor.service";
|
||||
import { ApiLogInCredentials } from "../../models/domain/log-in-credentials";
|
||||
import { ApiTokenRequest } from "../../models/request/identity-token/api-token.request";
|
||||
import { UserApiLogInCredentials } from "../../models/domain/log-in-credentials";
|
||||
import { UserApiTokenRequest } from "../../models/request/identity-token/user-api-token.request";
|
||||
import { IdentityTokenResponse } from "../../models/response/identity-token.response";
|
||||
|
||||
import { LogInStrategy } from "./logIn.strategy";
|
||||
|
||||
export class ApiLogInStrategy extends LogInStrategy {
|
||||
tokenRequest: ApiTokenRequest;
|
||||
export class UserApiLogInStrategy extends LogInStrategy {
|
||||
tokenRequest: UserApiTokenRequest;
|
||||
|
||||
constructor(
|
||||
cryptoService: CryptoService,
|
||||
@@ -51,8 +51,8 @@ export class ApiLogInStrategy extends LogInStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
async logIn(credentials: ApiLogInCredentials) {
|
||||
this.tokenRequest = new ApiTokenRequest(
|
||||
async logIn(credentials: UserApiLogInCredentials) {
|
||||
this.tokenRequest = new UserApiTokenRequest(
|
||||
credentials.clientId,
|
||||
credentials.clientSecret,
|
||||
await this.buildTwoFactor(),
|
||||
@@ -26,8 +26,8 @@ export class SsoLogInCredentials {
|
||||
) {}
|
||||
}
|
||||
|
||||
export class ApiLogInCredentials {
|
||||
readonly type = AuthenticationType.Api;
|
||||
export class UserApiLogInCredentials {
|
||||
readonly type = AuthenticationType.UserApi;
|
||||
|
||||
constructor(public clientId: string, public clientSecret: string) {}
|
||||
}
|
||||
|
||||
@@ -42,10 +42,12 @@ export abstract class TokenRequest {
|
||||
obj.authRequest = this.passwordlessAuthRequest;
|
||||
}
|
||||
|
||||
if (this.twoFactor.token && this.twoFactor.provider != null) {
|
||||
obj.twoFactorToken = this.twoFactor.token;
|
||||
obj.twoFactorProvider = this.twoFactor.provider;
|
||||
obj.twoFactorRemember = this.twoFactor.remember ? "1" : "0";
|
||||
if (this.twoFactor) {
|
||||
if (this.twoFactor.token && this.twoFactor.provider != null) {
|
||||
obj.twoFactorToken = this.twoFactor.token;
|
||||
obj.twoFactorProvider = this.twoFactor.provider;
|
||||
obj.twoFactorRemember = this.twoFactor.remember ? "1" : "0";
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DeviceRequest } from "../device.request";
|
||||
import { TokenTwoFactorRequest } from "./token-two-factor.request";
|
||||
import { TokenRequest } from "./token.request";
|
||||
|
||||
export class ApiTokenRequest extends TokenRequest {
|
||||
export class UserApiTokenRequest extends TokenRequest {
|
||||
constructor(
|
||||
public clientId: string,
|
||||
public clientSecret: string,
|
||||
@@ -31,10 +31,10 @@ import { EmergencyAccessUpdateRequest } from "../models/request/emergency-access
|
||||
import { EventRequest } from "../models/request/event.request";
|
||||
import { GroupRequest } from "../models/request/group.request";
|
||||
import { IapCheckRequest } from "../models/request/iap-check.request";
|
||||
import { ApiTokenRequest } from "../models/request/identity-token/api-token.request";
|
||||
import { PasswordTokenRequest } from "../models/request/identity-token/password-token.request";
|
||||
import { SsoTokenRequest } from "../models/request/identity-token/sso-token.request";
|
||||
import { TokenTwoFactorRequest } from "../models/request/identity-token/token-two-factor.request";
|
||||
import { UserApiTokenRequest } from "../models/request/identity-token/user-api-token.request";
|
||||
import { ImportCiphersRequest } from "../models/request/import-ciphers.request";
|
||||
import { ImportOrganizationCiphersRequest } from "../models/request/import-organization-ciphers.request";
|
||||
import { KdfRequest } from "../models/request/kdf.request";
|
||||
@@ -206,7 +206,7 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
// Auth APIs
|
||||
|
||||
async postIdentityToken(
|
||||
request: ApiTokenRequest | PasswordTokenRequest | SsoTokenRequest
|
||||
request: UserApiTokenRequest | PasswordTokenRequest | SsoTokenRequest
|
||||
): Promise<IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse> {
|
||||
const headers = new Headers({
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
@@ -219,7 +219,7 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
request.alterIdentityTokenHeaders(headers);
|
||||
|
||||
const identityToken =
|
||||
request instanceof ApiTokenRequest
|
||||
request instanceof UserApiTokenRequest
|
||||
? request.toIdentityToken()
|
||||
: request.toIdentityToken(this.platformUtilsService.getClientType());
|
||||
|
||||
@@ -2271,8 +2271,7 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
|
||||
const appId = await this.appIdService.getAppId();
|
||||
const deviceRequest = new DeviceRequest(appId, this.platformUtilsService);
|
||||
|
||||
const tokenRequest = new ApiTokenRequest(
|
||||
const tokenRequest = new UserApiTokenRequest(
|
||||
clientId,
|
||||
clientSecret,
|
||||
new TokenTwoFactorRequest(),
|
||||
|
||||
@@ -17,13 +17,13 @@ import { AuthenticationStatus } from "../enums/authenticationStatus";
|
||||
import { AuthenticationType } from "../enums/authenticationType";
|
||||
import { KdfType } from "../enums/kdfType";
|
||||
import { KeySuffixOptions } from "../enums/keySuffixOptions";
|
||||
import { ApiLogInStrategy } from "../misc/logInStrategies/apiLogin.strategy";
|
||||
import { PasswordLogInStrategy } from "../misc/logInStrategies/passwordLogin.strategy";
|
||||
import { PasswordlessLogInStrategy } from "../misc/logInStrategies/passwordlessLogin.strategy";
|
||||
import { SsoLogInStrategy } from "../misc/logInStrategies/ssoLogin.strategy";
|
||||
import { UserApiLogInStrategy } from "../misc/logInStrategies/user-api-login.strategy";
|
||||
import { AuthResult } from "../models/domain/auth-result";
|
||||
import {
|
||||
ApiLogInCredentials,
|
||||
UserApiLogInCredentials,
|
||||
PasswordLogInCredentials,
|
||||
SsoLogInCredentials,
|
||||
PasswordlessLogInCredentials,
|
||||
@@ -67,7 +67,7 @@ export class AuthService implements AuthServiceAbstraction {
|
||||
}
|
||||
|
||||
private logInStrategy:
|
||||
| ApiLogInStrategy
|
||||
| UserApiLogInStrategy
|
||||
| PasswordLogInStrategy
|
||||
| SsoLogInStrategy
|
||||
| PasswordlessLogInStrategy;
|
||||
@@ -92,7 +92,7 @@ export class AuthService implements AuthServiceAbstraction {
|
||||
|
||||
async logIn(
|
||||
credentials:
|
||||
| ApiLogInCredentials
|
||||
| UserApiLogInCredentials
|
||||
| PasswordLogInCredentials
|
||||
| SsoLogInCredentials
|
||||
| PasswordlessLogInCredentials
|
||||
@@ -100,7 +100,7 @@ export class AuthService implements AuthServiceAbstraction {
|
||||
this.clearState();
|
||||
|
||||
let strategy:
|
||||
| ApiLogInStrategy
|
||||
| UserApiLogInStrategy
|
||||
| PasswordLogInStrategy
|
||||
| SsoLogInStrategy
|
||||
| PasswordlessLogInStrategy;
|
||||
@@ -134,8 +134,8 @@ export class AuthService implements AuthServiceAbstraction {
|
||||
this.keyConnectorService
|
||||
);
|
||||
break;
|
||||
case AuthenticationType.Api:
|
||||
strategy = new ApiLogInStrategy(
|
||||
case AuthenticationType.UserApi:
|
||||
strategy = new UserApiLogInStrategy(
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
@@ -203,8 +203,8 @@ export class AuthService implements AuthServiceAbstraction {
|
||||
this.messagingService.send("loggedOut");
|
||||
}
|
||||
|
||||
authingWithApiKey(): boolean {
|
||||
return this.logInStrategy instanceof ApiLogInStrategy;
|
||||
authingWithUserApiKey(): boolean {
|
||||
return this.logInStrategy instanceof UserApiLogInStrategy;
|
||||
}
|
||||
|
||||
authingWithSso(): boolean {
|
||||
@@ -272,7 +272,7 @@ export class AuthService implements AuthServiceAbstraction {
|
||||
|
||||
private saveState(
|
||||
strategy:
|
||||
| ApiLogInStrategy
|
||||
| UserApiLogInStrategy
|
||||
| PasswordLogInStrategy
|
||||
| SsoLogInStrategy
|
||||
| PasswordlessLogInStrategy
|
||||
|
||||
@@ -64,6 +64,20 @@ export class FolderService implements InternalFolderServiceAbstraction {
|
||||
return folders.find((folder) => folder.id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated For the CLI only
|
||||
* @param id id of the folder
|
||||
*/
|
||||
async getFromState(id: string): Promise<Folder> {
|
||||
const foldersMap = await this.stateService.getEncryptedFolders();
|
||||
const folder = foldersMap[id];
|
||||
if (folder == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Folder(folder);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Only use in CLI!
|
||||
*/
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
import { BehaviorSubject, concatMap, filter } from "rxjs";
|
||||
import { BehaviorSubject, concatMap } from "rxjs";
|
||||
|
||||
import { OrganizationService as OrganizationServiceAbstraction } from "../../abstractions/organization/organization.service.abstraction";
|
||||
import { InternalOrganizationService as InternalOrganizationServiceAbstraction } from "../../abstractions/organization/organization.service.abstraction";
|
||||
import { StateService } from "../../abstractions/state.service";
|
||||
import { SyncNotifierService } from "../../abstractions/sync/syncNotifier.service.abstraction";
|
||||
import { OrganizationData } from "../../models/data/organization.data";
|
||||
import { Organization } from "../../models/domain/organization";
|
||||
import { isSuccessfullyCompleted } from "../../types/syncEventArgs";
|
||||
|
||||
export class OrganizationService implements OrganizationServiceAbstraction {
|
||||
export class OrganizationService implements InternalOrganizationServiceAbstraction {
|
||||
protected _organizations = new BehaviorSubject<Organization[]>([]);
|
||||
|
||||
organizations$ = this._organizations.asObservable();
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private syncNotifierService: SyncNotifierService
|
||||
) {
|
||||
constructor(private stateService: StateService) {
|
||||
this.stateService.activeAccountUnlocked$
|
||||
.pipe(
|
||||
concatMap(async (unlocked) => {
|
||||
@@ -29,28 +24,6 @@ export class OrganizationService implements OrganizationServiceAbstraction {
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.syncNotifierService.sync$
|
||||
.pipe(
|
||||
filter(isSuccessfullyCompleted),
|
||||
concatMap(async ({ data }) => {
|
||||
const { profile } = data;
|
||||
const organizations: { [id: string]: OrganizationData } = {};
|
||||
profile.organizations.forEach((o) => {
|
||||
organizations[o.id] = new OrganizationData(o);
|
||||
});
|
||||
|
||||
profile.providerOrganizations.forEach((o) => {
|
||||
if (organizations[o.id] == null) {
|
||||
organizations[o.id] = new OrganizationData(o);
|
||||
organizations[o.id].isProviderUser = true;
|
||||
}
|
||||
});
|
||||
|
||||
await this.updateStateAndObservables(organizations);
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async getAll(userId?: string): Promise<Organization[]> {
|
||||
@@ -78,7 +51,7 @@ export class OrganizationService implements OrganizationServiceAbstraction {
|
||||
|
||||
organizations[organization.id] = organization;
|
||||
|
||||
await this.updateStateAndObservables(organizations);
|
||||
await this.replace(organizations);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
@@ -92,7 +65,7 @@ export class OrganizationService implements OrganizationServiceAbstraction {
|
||||
}
|
||||
|
||||
delete organizations[id];
|
||||
await this.updateStateAndObservables(organizations);
|
||||
await this.replace(organizations);
|
||||
}
|
||||
|
||||
get(id: string): Organization {
|
||||
@@ -121,9 +94,9 @@ export class OrganizationService implements OrganizationServiceAbstraction {
|
||||
return organizations.find((organization) => organization.identifier === identifier);
|
||||
}
|
||||
|
||||
private async updateStateAndObservables(organizationsMap: { [id: string]: OrganizationData }) {
|
||||
await this.stateService.setOrganizations(organizationsMap);
|
||||
this.updateObservables(organizationsMap);
|
||||
async replace(organizations: { [id: string]: OrganizationData }) {
|
||||
await this.stateService.setOrganizations(organizations);
|
||||
this.updateObservables(organizations);
|
||||
}
|
||||
|
||||
private updateObservables(organizationsMap: { [id: string]: OrganizationData }) {
|
||||
|
||||
@@ -7,17 +7,18 @@ import { InternalFolderService } from "../../abstractions/folder/folder.service.
|
||||
import { KeyConnectorService } from "../../abstractions/keyConnector.service";
|
||||
import { LogService } from "../../abstractions/log.service";
|
||||
import { MessagingService } from "../../abstractions/messaging.service";
|
||||
import { InternalOrganizationService } from "../../abstractions/organization/organization.service.abstraction";
|
||||
import { InternalPolicyService } from "../../abstractions/policy/policy.service.abstraction";
|
||||
import { ProviderService } from "../../abstractions/provider.service";
|
||||
import { SendService } from "../../abstractions/send.service";
|
||||
import { SettingsService } from "../../abstractions/settings.service";
|
||||
import { StateService } from "../../abstractions/state.service";
|
||||
import { SyncService as SyncServiceAbstraction } from "../../abstractions/sync/sync.service.abstraction";
|
||||
import { SyncNotifierService } from "../../abstractions/sync/syncNotifier.service.abstraction";
|
||||
import { sequentialize } from "../../misc/sequentialize";
|
||||
import { CipherData } from "../../models/data/cipher.data";
|
||||
import { CollectionData } from "../../models/data/collection.data";
|
||||
import { FolderData } from "../../models/data/folder.data";
|
||||
import { OrganizationData } from "../../models/data/organization.data";
|
||||
import { PolicyData } from "../../models/data/policy.data";
|
||||
import { ProviderData } from "../../models/data/provider.data";
|
||||
import { SendData } from "../../models/data/send.data";
|
||||
@@ -52,7 +53,7 @@ export class SyncService implements SyncServiceAbstraction {
|
||||
private stateService: StateService,
|
||||
private providerService: ProviderService,
|
||||
private folderApiService: FolderApiServiceAbstraction,
|
||||
private syncNotifierService: SyncNotifierService,
|
||||
private organizationService: InternalOrganizationService,
|
||||
private logoutCallback: (expired: boolean) => Promise<void>
|
||||
) {}
|
||||
|
||||
@@ -76,10 +77,8 @@ export class SyncService implements SyncServiceAbstraction {
|
||||
@sequentialize(() => "fullSync")
|
||||
async fullSync(forceSync: boolean, allowThrowOnError = false): Promise<boolean> {
|
||||
this.syncStarted();
|
||||
this.syncNotifierService.next({ status: "Started" });
|
||||
const isAuthenticated = await this.stateService.getIsAuthenticated();
|
||||
if (!isAuthenticated) {
|
||||
this.syncNotifierService.next({ status: "Completed", successfully: false });
|
||||
return this.syncCompleted(false);
|
||||
}
|
||||
|
||||
@@ -95,7 +94,6 @@ export class SyncService implements SyncServiceAbstraction {
|
||||
|
||||
if (!needsSync) {
|
||||
await this.setLastSync(now);
|
||||
this.syncNotifierService.next({ status: "Completed", successfully: false });
|
||||
return this.syncCompleted(false);
|
||||
}
|
||||
|
||||
@@ -112,13 +110,11 @@ export class SyncService implements SyncServiceAbstraction {
|
||||
await this.syncPolicies(response.policies);
|
||||
|
||||
await this.setLastSync(now);
|
||||
this.syncNotifierService.next({ status: "Completed", successfully: true, data: response });
|
||||
return this.syncCompleted(true);
|
||||
} catch (e) {
|
||||
if (allowThrowOnError) {
|
||||
throw e;
|
||||
} else {
|
||||
this.syncNotifierService.next({ status: "Completed", successfully: false });
|
||||
return this.syncCompleted(false);
|
||||
}
|
||||
}
|
||||
@@ -315,11 +311,24 @@ export class SyncService implements SyncServiceAbstraction {
|
||||
await this.stateService.setForcePasswordReset(response.forcePasswordReset);
|
||||
await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector);
|
||||
|
||||
const organizations: { [id: string]: OrganizationData } = {};
|
||||
response.organizations.forEach((o) => {
|
||||
organizations[o.id] = new OrganizationData(o);
|
||||
});
|
||||
|
||||
const providers: { [id: string]: ProviderData } = {};
|
||||
response.providers.forEach((p) => {
|
||||
providers[p.id] = new ProviderData(p);
|
||||
});
|
||||
|
||||
response.providerOrganizations.forEach((o) => {
|
||||
if (organizations[o.id] == null) {
|
||||
organizations[o.id] = new OrganizationData(o);
|
||||
organizations[o.id].isProviderUser = true;
|
||||
}
|
||||
});
|
||||
|
||||
await this.organizationService.replace(organizations);
|
||||
await this.providerService.save(providers);
|
||||
|
||||
if (await this.keyConnectorService.userNeedsMigration()) {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Component, HostBinding, Input } from "@angular/core";
|
||||
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
|
||||
enum CharacterType {
|
||||
Letter,
|
||||
Emoji,
|
||||
Special,
|
||||
Number,
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "bit-color-password",
|
||||
template: `<div
|
||||
*ngFor="let character of passwordArray; index as i"
|
||||
[class]="getCharacterClass(character)"
|
||||
>
|
||||
<span>{{ character }}</span>
|
||||
<span *ngIf="showCount" class="tw-whitespace-nowrap tw-text-xs tw-leading-5 tw-text-main">{{
|
||||
i + 1
|
||||
}}</span>
|
||||
</div>`,
|
||||
})
|
||||
export class ColorPasswordComponent {
|
||||
@Input() private password: string = null;
|
||||
@Input() showCount = false;
|
||||
|
||||
characterStyles: Record<CharacterType, string[]> = {
|
||||
[CharacterType.Emoji]: [],
|
||||
[CharacterType.Letter]: ["tw-text-main"],
|
||||
[CharacterType.Special]: ["tw-text-danger"],
|
||||
[CharacterType.Number]: ["tw-text-primary-500"],
|
||||
};
|
||||
|
||||
@HostBinding("class")
|
||||
get classList() {
|
||||
return ["tw-min-w-0", "tw-whitespace-pre-wrap", "tw-break-all"];
|
||||
}
|
||||
|
||||
get passwordArray() {
|
||||
// Convert to an array to handle cases that strings have special characters, i.e.: emoji.
|
||||
return Array.from(this.password);
|
||||
}
|
||||
|
||||
getCharacterClass(character: string) {
|
||||
const charType = this.getCharacterType(character);
|
||||
const charClass = this.characterStyles[charType].concat("tw-inline-flex");
|
||||
|
||||
if (this.showCount) {
|
||||
return charClass.concat([
|
||||
"tw-inline-flex",
|
||||
"tw-flex-col",
|
||||
"tw-items-center",
|
||||
"tw-w-7",
|
||||
"tw-py-1",
|
||||
"odd:tw-bg-secondary-100",
|
||||
]);
|
||||
}
|
||||
|
||||
return charClass;
|
||||
}
|
||||
|
||||
private getCharacterType(character: string): CharacterType {
|
||||
if (character.match(Utils.regexpEmojiPresentation)) {
|
||||
return CharacterType.Emoji;
|
||||
}
|
||||
|
||||
if (character.match(/\d/)) {
|
||||
return CharacterType.Number;
|
||||
}
|
||||
|
||||
const specials = ["&", "<", ">", " "];
|
||||
if (specials.includes(character) || character.match(/[^\w ]/)) {
|
||||
return CharacterType.Special;
|
||||
}
|
||||
|
||||
return CharacterType.Letter;
|
||||
}
|
||||
}
|
||||
11
libs/components/src/color-password/color-password.module.ts
Normal file
11
libs/components/src/color-password/color-password.module.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { ColorPasswordComponent } from "./color-password.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
exports: [ColorPasswordComponent],
|
||||
declarations: [ColorPasswordComponent],
|
||||
})
|
||||
export class ColorPasswordModule {}
|
||||
52
libs/components/src/color-password/color-password.stories.ts
Normal file
52
libs/components/src/color-password/color-password.stories.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Meta, Story } from "@storybook/angular";
|
||||
|
||||
import { ColorPasswordComponent } from "./color-password.component";
|
||||
|
||||
const examplePassword = "Wq$Jk😀7jDX#rS5Sdi!z";
|
||||
|
||||
export default {
|
||||
title: "Component Library/Color Password",
|
||||
component: ColorPasswordComponent,
|
||||
args: {
|
||||
password: examplePassword,
|
||||
showCount: false,
|
||||
},
|
||||
parameters: {
|
||||
design: {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/file/6fvTDa3zfvgWdizLQ7nSTP/Numbered-Password",
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<ColorPasswordComponent> = (args: ColorPasswordComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-color-password class="tw-text-base" [password]="password" [showCount]="showCount"></bit-color-password>
|
||||
`,
|
||||
});
|
||||
|
||||
const WrappedTemplate: Story<ColorPasswordComponent> = (args: ColorPasswordComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<div class="tw-max-w-32">
|
||||
<bit-color-password class="tw-text-base" [password]="password" [showCount]="showCount"></bit-color-password>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
export const ColorPassword = Template.bind({});
|
||||
|
||||
export const WrappedColorPassword = WrappedTemplate.bind({});
|
||||
|
||||
export const ColorPasswordCount = Template.bind({});
|
||||
ColorPasswordCount.args = {
|
||||
password: examplePassword,
|
||||
showCount: true,
|
||||
};
|
||||
|
||||
export const WrappedColorPasswordCount = WrappedTemplate.bind({});
|
||||
WrappedColorPasswordCount.args = {
|
||||
password: examplePassword,
|
||||
showCount: true,
|
||||
};
|
||||
1
libs/components/src/color-password/index.ts
Normal file
1
libs/components/src/color-password/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./color-password.module";
|
||||
@@ -1,10 +1,12 @@
|
||||
import { DialogModule, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { DIALOG_DATA, DialogModule, DialogRef } 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 { IconButtonModule } from "../icon-button";
|
||||
import { SharedModule } from "../shared";
|
||||
import { I18nMockService } from "../utils/i18n-mock.service";
|
||||
|
||||
import { DialogService } from "./dialog.service";
|
||||
@@ -35,7 +37,7 @@ class StoryDialogComponent {
|
||||
@Component({
|
||||
selector: "story-dialog-content",
|
||||
template: `
|
||||
<bit-dialog [dialogSize]="large">
|
||||
<bit-dialog dialogSize="large">
|
||||
<span bitDialogTitle>Dialog Title</span>
|
||||
<span bitDialogContent>
|
||||
Dialog body text goes here.
|
||||
@@ -68,7 +70,7 @@ export default {
|
||||
DialogTitleContainerDirective,
|
||||
StoryDialogContentComponent,
|
||||
],
|
||||
imports: [ButtonModule, DialogModule],
|
||||
imports: [SharedModule, ButtonModule, DialogModule, IconButtonModule],
|
||||
providers: [
|
||||
DialogService,
|
||||
{
|
||||
|
||||
@@ -14,4 +14,5 @@ export * from "./multi-select";
|
||||
export * from "./tabs";
|
||||
export * from "./table";
|
||||
export * from "./toggle-group";
|
||||
export * from "./color-password";
|
||||
export * from "./utils/i18n-mock.service";
|
||||
|
||||
@@ -6,49 +6,85 @@ const linkStyles: Record<LinkType, string[]> = {
|
||||
primary: [
|
||||
"!tw-text-primary-500",
|
||||
"hover:!tw-text-primary-500",
|
||||
"focus-visible:tw-ring-primary-700",
|
||||
"focus-visible:before:tw-ring-primary-700",
|
||||
"disabled:!tw-text-primary-500/60",
|
||||
],
|
||||
secondary: [
|
||||
"!tw-text-main",
|
||||
"hover:!tw-text-main",
|
||||
"focus-visible:tw-ring-primary-700",
|
||||
"focus-visible:before:tw-ring-primary-700",
|
||||
"disabled:!tw-text-muted/60",
|
||||
],
|
||||
contrast: [
|
||||
"!tw-text-contrast",
|
||||
"hover:!tw-text-contrast",
|
||||
"focus-visible:tw-ring-text-contrast",
|
||||
"focus-visible:before:tw-ring-text-contrast",
|
||||
"disabled:!tw-text-contrast/60",
|
||||
],
|
||||
};
|
||||
|
||||
@Directive({
|
||||
selector: "button[bitLink], a[bitLink]",
|
||||
})
|
||||
export class LinkDirective {
|
||||
@HostBinding("class") get classList() {
|
||||
return [
|
||||
"tw-font-semibold",
|
||||
"tw-py-0.5",
|
||||
"tw-px-0",
|
||||
"tw-bg-transparent",
|
||||
"tw-border-0",
|
||||
"tw-border-none",
|
||||
"tw-rounded",
|
||||
"tw-transition",
|
||||
"hover:tw-underline",
|
||||
"hover:tw-decoration-1",
|
||||
"focus-visible:tw-outline-none",
|
||||
"focus-visible:tw-underline",
|
||||
"focus-visible:tw-decoration-1",
|
||||
"focus-visible:tw-ring-2",
|
||||
"focus-visible:tw-z-10",
|
||||
"disabled:tw-no-underline",
|
||||
"disabled:tw-cursor-not-allowed",
|
||||
].concat(linkStyles[this.linkType] ?? []);
|
||||
}
|
||||
const commonStyles = [
|
||||
"tw-leading-none",
|
||||
"tw-p-0",
|
||||
"tw-font-semibold",
|
||||
"tw-bg-transparent",
|
||||
"tw-border-0",
|
||||
"tw-border-none",
|
||||
"tw-rounded",
|
||||
"tw-transition",
|
||||
"hover:tw-underline",
|
||||
"hover:tw-decoration-1",
|
||||
"disabled:tw-no-underline",
|
||||
"disabled:tw-cursor-not-allowed",
|
||||
"focus-visible:tw-outline-none",
|
||||
"focus-visible:tw-underline",
|
||||
"focus-visible:tw-decoration-1",
|
||||
|
||||
// Workaround for html button tag not being able to be set to `display: inline`
|
||||
// and at the same time not being able to use `tw-ring-offset` because of box-shadow issue.
|
||||
// https://github.com/w3c/csswg-drafts/issues/3226
|
||||
// Add `tw-inline`, add `tw-py-0.5` and use regular `tw-ring` if issue is fixed.
|
||||
//
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/3595
|
||||
// Remove `before:` and use regular `tw-ring` when browser no longer has bug, or better:
|
||||
// switch to `outline` with `outline-offset` when Safari supports border radius on outline.
|
||||
// Using `box-shadow` to create outlines is a hack and as such `outline` should be preferred.
|
||||
"tw-relative",
|
||||
"before:tw-content-['']",
|
||||
"before:tw-block",
|
||||
"before:tw-absolute",
|
||||
"before:-tw-inset-x-[0.1em]",
|
||||
"before:tw-rounded-md",
|
||||
"before:tw-transition",
|
||||
"before:tw-ring-2",
|
||||
"focus-visible:before:tw-ring-text-contrast",
|
||||
"focus-visible:tw-z-10",
|
||||
];
|
||||
|
||||
@Directive()
|
||||
abstract class LinkDirective {
|
||||
@Input()
|
||||
linkType: LinkType = "primary";
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: "a[bitLink]",
|
||||
})
|
||||
export class AnchorLinkDirective extends LinkDirective {
|
||||
@HostBinding("class") get classList() {
|
||||
return ["before:-tw-inset-y-[0.125rem]"]
|
||||
.concat(commonStyles)
|
||||
.concat(linkStyles[this.linkType] ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: "button[bitLink]",
|
||||
})
|
||||
export class ButtonLinkDirective extends LinkDirective {
|
||||
@HostBinding("class") get classList() {
|
||||
return ["before:-tw-inset-y-[0.25rem]"]
|
||||
.concat(commonStyles)
|
||||
.concat(linkStyles[this.linkType] ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { LinkDirective } from "./link.directive";
|
||||
import { AnchorLinkDirective, ButtonLinkDirective } from "./link.directive";
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
exports: [LinkDirective],
|
||||
declarations: [LinkDirective],
|
||||
exports: [AnchorLinkDirective, ButtonLinkDirective],
|
||||
declarations: [AnchorLinkDirective, ButtonLinkDirective],
|
||||
})
|
||||
export class LinkModule {}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { Meta, Story } from "@storybook/angular";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
|
||||
import { LinkDirective } from "./link.directive";
|
||||
import { AnchorLinkDirective, ButtonLinkDirective } from "./link.directive";
|
||||
import { LinkModule } from "./link.module";
|
||||
|
||||
export default {
|
||||
title: "Component Library/Link",
|
||||
component: LinkDirective,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [LinkModule],
|
||||
}),
|
||||
],
|
||||
argTypes: {
|
||||
linkType: {
|
||||
options: ["primary", "secondary", "contrast"],
|
||||
@@ -19,25 +24,33 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const ButtonTemplate: Story<LinkDirective> = (args: LinkDirective) => ({
|
||||
const ButtonTemplate: Story<ButtonLinkDirective> = (args: ButtonLinkDirective) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<div class="tw-p-2" [ngClass]="{ 'tw-bg-transparent': linkType != 'contrast', 'tw-bg-primary-500': linkType === 'contrast' }">
|
||||
<button bitLink [linkType]="linkType" class="tw-mb-2 tw-block">Button</button>
|
||||
<button bitLink [linkType]="linkType" class="tw-mb-2 tw-block">
|
||||
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
|
||||
Add Icon Button
|
||||
</button>
|
||||
<button bitLink [linkType]="linkType" class="tw-mb-2 tw-block">
|
||||
Chevron Icon Button
|
||||
<i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button bitLink [linkType]="linkType" class="tw-text-sm tw-block">Small Button</button>
|
||||
<div class="tw-block tw-p-2">
|
||||
<button bitLink [linkType]="linkType">Button</button>
|
||||
</div>
|
||||
<div class="tw-block tw-p-2">
|
||||
<button bitLink [linkType]="linkType">
|
||||
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
|
||||
Add Icon Button
|
||||
</button>
|
||||
</div>
|
||||
<div class="tw-block tw-p-2">
|
||||
<button bitLink [linkType]="linkType">
|
||||
Chevron Icon Button
|
||||
<i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="tw-block tw-p-2">
|
||||
<button bitLink [linkType]="linkType" class="tw-text-sm">Small Button</button>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
const AnchorTemplate: Story<LinkDirective> = (args: LinkDirective) => ({
|
||||
const AnchorTemplate: Story<AnchorLinkDirective> = (args: AnchorLinkDirective) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<div class="tw-p-2" [ngClass]="{ 'tw-bg-transparent': linkType != 'contrast', 'tw-bg-primary-500': linkType === 'contrast' }">
|
||||
@@ -73,6 +86,20 @@ Anchors.args = {
|
||||
linkType: "primary",
|
||||
};
|
||||
|
||||
const InlineTemplate: Story = (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<span class="tw-text-main">
|
||||
On the internet pargraphs often contain <a bitLink href="#">inline links</a>, but few know that <button bitLink>buttons</button> can be used for similar purposes.
|
||||
</span>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Inline = InlineTemplate.bind({});
|
||||
Inline.args = {
|
||||
linkType: "primary",
|
||||
};
|
||||
|
||||
const DisabledTemplate: Story = (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
role="tabpanel"
|
||||
*ngFor="let tab of tabs; let i = index"
|
||||
[id]="getTabContentId(i)"
|
||||
[attr.tabindex]="selectedIndex === i ? 0 : -1"
|
||||
[attr.tabindex]="tab.contentTabIndex"
|
||||
[attr.labeledby]="getTabLabelId(i)"
|
||||
[active]="tab.isActive"
|
||||
[content]="tab.content"
|
||||
|
||||
@@ -20,9 +20,18 @@ import { TabLabelDirective } from "./tab-label.directive";
|
||||
})
|
||||
export class TabComponent implements OnInit {
|
||||
@Input() disabled = false;
|
||||
|
||||
@Input("label") textLabel = "";
|
||||
|
||||
/**
|
||||
* Optional tabIndex for the tabPanel that contains this tab's content.
|
||||
*
|
||||
* If the tabpanel does not contain any focusable elements or the first element with content is not focusable,
|
||||
* this should be set to 0 to include it in the tab sequence of the page.
|
||||
*
|
||||
* @remarks See note 4 of https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/
|
||||
*/
|
||||
@Input() contentTabIndex: number | undefined;
|
||||
|
||||
@ViewChild(TemplateRef, { static: true }) implicitContent: TemplateRef<unknown>;
|
||||
@ContentChild(TabLabelDirective) templateLabel: TabLabelDirective;
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ import { Component } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
|
||||
import { ButtonModule } from "../button";
|
||||
import { FormFieldModule } from "../form-field";
|
||||
|
||||
import { TabGroupComponent } from "./tab-group/tab-group.component";
|
||||
import { TabsModule } from "./tabs.module";
|
||||
|
||||
@@ -44,6 +47,8 @@ export default {
|
||||
imports: [
|
||||
CommonModule,
|
||||
TabsModule,
|
||||
ButtonModule,
|
||||
FormFieldModule,
|
||||
RouterModule.forRoot(
|
||||
[
|
||||
{ path: "", redirectTo: "active", pathMatch: "full" },
|
||||
@@ -125,3 +130,32 @@ const PreserveContentTabGroupTemplate: Story<TabGroupComponent> = (args: any) =>
|
||||
});
|
||||
|
||||
export const PreserveContentTabs = PreserveContentTabGroupTemplate.bind({});
|
||||
|
||||
const KeyboardNavTabGroupTemplate: Story<TabGroupComponent> = (args: any) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-tab-group label="Keyboard Navigation Tabs" class="tw-text-main">
|
||||
<bit-tab label="Form Tab">
|
||||
<p>
|
||||
You can navigate through all tab labels, form inputs, and the button that is outside the tab group via
|
||||
the keyboard.
|
||||
</p>
|
||||
<bit-form-field>
|
||||
<bit-label>First Input</bit-label>
|
||||
<input type="text" bitInput />
|
||||
</bit-form-field>
|
||||
<bit-form-field>
|
||||
<bit-label>Second Input</bit-label>
|
||||
<input type="text" bitInput />
|
||||
</bit-form-field>
|
||||
</bit-tab>
|
||||
|
||||
<bit-tab label="No Focusable Content Tab" [contentTabIndex]="0">
|
||||
<p>This tab has no focusable content, but the panel should still be focusable</p>
|
||||
</bit-tab>
|
||||
</bit-tab-group>
|
||||
<button bitButton buttonType="primary" class="tw-mt-5">External Button</button>
|
||||
`,
|
||||
});
|
||||
|
||||
export const KeyboardNavigation = KeyboardNavTabGroupTemplate.bind({});
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
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", () => {
|
||||
let logService: ConsoleLogService;
|
||||
beforeEach(() => {
|
||||
caughtMessage = {};
|
||||
interceptConsole(caughtMessage);
|
||||
logService = new ConsoleLogService(true);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
restoreConsole();
|
||||
});
|
||||
|
||||
it("should redirect all console to error if BW_RESPONSE env is true", () => {
|
||||
process.env.BW_RESPONSE = "true";
|
||||
|
||||
logService.debug("this is a debug message");
|
||||
expect(caughtMessage).toMatchObject({
|
||||
error: { 0: "this is a debug message" },
|
||||
});
|
||||
});
|
||||
|
||||
it("should not redirect console to error if BW_RESPONSE != true", () => {
|
||||
process.env.BW_RESPONSE = "false";
|
||||
|
||||
logService.debug("debug");
|
||||
logService.info("info");
|
||||
logService.warning("warning");
|
||||
logService.error("error");
|
||||
|
||||
expect(caughtMessage).toMatchObject({
|
||||
log: { 0: "info" },
|
||||
warn: { 0: "warning" },
|
||||
error: { 0: "error" },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
|
||||
import { NodeCryptoFunctionService } from "@bitwarden/node/services/nodeCryptoFunction.service";
|
||||
import { NodeCryptoFunctionService } from "@bitwarden/node/services/node-crypto-function.service";
|
||||
|
||||
const RsaPublicKey =
|
||||
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl0Vawl/toXzkEvB82FEtqHP" +
|
||||
@@ -1,113 +0,0 @@
|
||||
import * as chalk from "chalk";
|
||||
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
|
||||
import { Response } from "./models/response";
|
||||
import { ListResponse } from "./models/response/listResponse";
|
||||
import { MessageResponse } from "./models/response/messageResponse";
|
||||
import { StringResponse } from "./models/response/stringResponse";
|
||||
|
||||
export abstract class BaseProgram {
|
||||
constructor(
|
||||
protected stateService: StateService,
|
||||
private writeLn: (s: string, finalLine: boolean, error: boolean) => void
|
||||
) {}
|
||||
|
||||
protected processResponse(
|
||||
response: Response,
|
||||
exitImmediately = false,
|
||||
dataProcessor: () => string = null
|
||||
) {
|
||||
if (!response.success) {
|
||||
if (process.env.BW_QUIET !== "true") {
|
||||
if (process.env.BW_RESPONSE === "true") {
|
||||
this.writeLn(this.getJson(response), true, false);
|
||||
} else {
|
||||
this.writeLn(chalk.redBright(response.message), true, true);
|
||||
}
|
||||
}
|
||||
const exitCode = process.env.BW_CLEANEXIT ? 0 : 1;
|
||||
if (exitImmediately) {
|
||||
process.exit(exitCode);
|
||||
} else {
|
||||
process.exitCode = exitCode;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.BW_RESPONSE === "true") {
|
||||
this.writeLn(this.getJson(response), true, false);
|
||||
} else if (response.data != null) {
|
||||
let out: string = dataProcessor != null ? dataProcessor() : null;
|
||||
if (out == null) {
|
||||
if (response.data.object === "string") {
|
||||
const data = (response.data as StringResponse).data;
|
||||
if (data != null) {
|
||||
out = data;
|
||||
}
|
||||
} else if (response.data.object === "list") {
|
||||
out = this.getJson((response.data as ListResponse).data);
|
||||
} else if (response.data.object === "message") {
|
||||
out = this.getMessage(response);
|
||||
} else {
|
||||
out = this.getJson(response.data);
|
||||
}
|
||||
}
|
||||
|
||||
if (out != null && process.env.BW_QUIET !== "true") {
|
||||
this.writeLn(out, true, false);
|
||||
}
|
||||
}
|
||||
if (exitImmediately) {
|
||||
process.exit(0);
|
||||
} else {
|
||||
process.exitCode = 0;
|
||||
}
|
||||
}
|
||||
|
||||
protected getJson(obj: any): string {
|
||||
if (process.env.BW_PRETTY === "true") {
|
||||
return JSON.stringify(obj, null, " ");
|
||||
} else {
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
}
|
||||
|
||||
protected getMessage(response: Response): string {
|
||||
const message = response.data as MessageResponse;
|
||||
if (process.env.BW_RAW === "true") {
|
||||
return message.raw;
|
||||
}
|
||||
|
||||
let out = "";
|
||||
if (message.title != null) {
|
||||
if (message.noColor) {
|
||||
out = message.title;
|
||||
} else {
|
||||
out = chalk.greenBright(message.title);
|
||||
}
|
||||
}
|
||||
if (message.message != null) {
|
||||
if (message.title != null) {
|
||||
out += "\n";
|
||||
}
|
||||
out += message.message;
|
||||
}
|
||||
return out.trim() === "" ? null : out;
|
||||
}
|
||||
|
||||
protected async exitIfAuthed() {
|
||||
const authed = await this.stateService.getIsAuthenticated();
|
||||
if (authed) {
|
||||
const email = await this.stateService.getEmail();
|
||||
this.processResponse(Response.error("You are already logged in as " + email + "."), true);
|
||||
}
|
||||
}
|
||||
|
||||
protected async exitIfNotAuthed() {
|
||||
const authed = await this.stateService.getIsAuthenticated();
|
||||
if (!authed) {
|
||||
this.processResponse(Response.error("You are not logged in."), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,627 +0,0 @@
|
||||
import * as http from "http";
|
||||
|
||||
import * as program from "commander";
|
||||
import * as inquirer from "inquirer";
|
||||
import Separator from "inquirer/lib/objects/separator";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/abstractions/twoFactor.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/enums/twoFactorProviderType";
|
||||
import { NodeUtils } from "@bitwarden/common/misc/nodeUtils";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { AuthResult } from "@bitwarden/common/models/domain/auth-result";
|
||||
import {
|
||||
ApiLogInCredentials,
|
||||
PasswordLogInCredentials,
|
||||
SsoLogInCredentials,
|
||||
} from "@bitwarden/common/models/domain/log-in-credentials";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/models/request/identity-token/token-two-factor.request";
|
||||
import { TwoFactorEmailRequest } from "@bitwarden/common/models/request/two-factor-email.request";
|
||||
import { UpdateTempPasswordRequest } from "@bitwarden/common/models/request/update-temp-password.request";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
|
||||
import { Response } from "../models/response";
|
||||
import { MessageResponse } from "../models/response/messageResponse";
|
||||
|
||||
export class LoginCommand {
|
||||
protected validatedParams: () => Promise<any>;
|
||||
protected success: () => Promise<MessageResponse>;
|
||||
protected logout: () => Promise<void>;
|
||||
protected canInteract: boolean;
|
||||
protected clientId: string;
|
||||
protected clientSecret: string;
|
||||
protected email: string;
|
||||
|
||||
private ssoRedirectUri: string = null;
|
||||
|
||||
constructor(
|
||||
protected authService: AuthService,
|
||||
protected apiService: ApiService,
|
||||
protected i18nService: I18nService,
|
||||
protected environmentService: EnvironmentService,
|
||||
protected passwordGenerationService: PasswordGenerationService,
|
||||
protected cryptoFunctionService: CryptoFunctionService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected stateService: StateService,
|
||||
protected cryptoService: CryptoService,
|
||||
protected policyService: PolicyService,
|
||||
protected twoFactorService: TwoFactorService,
|
||||
clientId: string
|
||||
) {
|
||||
this.clientId = clientId;
|
||||
}
|
||||
|
||||
async run(email: string, password: string, options: program.OptionValues) {
|
||||
this.canInteract = process.env.BW_NOINTERACTION !== "true";
|
||||
|
||||
let ssoCodeVerifier: string = null;
|
||||
let ssoCode: string = null;
|
||||
let orgIdentifier: string = null;
|
||||
|
||||
let clientId: string = null;
|
||||
let clientSecret: string = null;
|
||||
|
||||
let selectedProvider: any = null;
|
||||
|
||||
if (options.apikey != null) {
|
||||
const apiIdentifiers = await this.apiIdentifiers();
|
||||
clientId = apiIdentifiers.clientId;
|
||||
clientSecret = apiIdentifiers.clientSecret;
|
||||
} else if (options.sso != null && this.canInteract) {
|
||||
const passwordOptions: any = {
|
||||
type: "password",
|
||||
length: 64,
|
||||
uppercase: true,
|
||||
lowercase: true,
|
||||
numbers: true,
|
||||
special: false,
|
||||
};
|
||||
const state = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, "sha256");
|
||||
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
|
||||
try {
|
||||
const ssoParams = await this.openSsoPrompt(codeChallenge, state);
|
||||
ssoCode = ssoParams.ssoCode;
|
||||
orgIdentifier = ssoParams.orgIdentifier;
|
||||
} catch {
|
||||
return Response.badRequest("Something went wrong. Try again.");
|
||||
}
|
||||
} else {
|
||||
if ((email == null || email === "") && this.canInteract) {
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||
output: process.stderr,
|
||||
})({
|
||||
type: "input",
|
||||
name: "email",
|
||||
message: "Email address:",
|
||||
});
|
||||
email = answer.email;
|
||||
}
|
||||
if (email == null || email.trim() === "") {
|
||||
return Response.badRequest("Email address is required.");
|
||||
}
|
||||
if (email.indexOf("@") === -1) {
|
||||
return Response.badRequest("Email address is invalid.");
|
||||
}
|
||||
this.email = email;
|
||||
|
||||
if (password == null || password === "") {
|
||||
if (options.passwordfile) {
|
||||
password = await NodeUtils.readFirstLine(options.passwordfile);
|
||||
} else if (options.passwordenv && process.env[options.passwordenv]) {
|
||||
password = process.env[options.passwordenv];
|
||||
} else if (this.canInteract) {
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||
output: process.stderr,
|
||||
})({
|
||||
type: "password",
|
||||
name: "password",
|
||||
message: "Master password:",
|
||||
});
|
||||
password = answer.password;
|
||||
}
|
||||
}
|
||||
|
||||
if (password == null || password === "") {
|
||||
return Response.badRequest("Master password is required.");
|
||||
}
|
||||
}
|
||||
|
||||
let twoFactorToken: string = options.code;
|
||||
let twoFactorMethod: TwoFactorProviderType = null;
|
||||
try {
|
||||
if (options.method != null) {
|
||||
twoFactorMethod = parseInt(options.method, null);
|
||||
}
|
||||
} catch (e) {
|
||||
return Response.error("Invalid two-step login method.");
|
||||
}
|
||||
|
||||
const twoFactor =
|
||||
twoFactorToken == null
|
||||
? null
|
||||
: new TokenTwoFactorRequest(twoFactorMethod, twoFactorToken, false);
|
||||
|
||||
try {
|
||||
if (this.validatedParams != null) {
|
||||
await this.validatedParams();
|
||||
}
|
||||
|
||||
let response: AuthResult = null;
|
||||
if (clientId != null && clientSecret != null) {
|
||||
response = await this.authService.logIn(new ApiLogInCredentials(clientId, clientSecret));
|
||||
} else if (ssoCode != null && ssoCodeVerifier != null) {
|
||||
response = await this.authService.logIn(
|
||||
new SsoLogInCredentials(
|
||||
ssoCode,
|
||||
ssoCodeVerifier,
|
||||
this.ssoRedirectUri,
|
||||
orgIdentifier,
|
||||
twoFactor
|
||||
)
|
||||
);
|
||||
} else {
|
||||
response = await this.authService.logIn(
|
||||
new PasswordLogInCredentials(email, password, null, twoFactor)
|
||||
);
|
||||
}
|
||||
if (response.captchaSiteKey) {
|
||||
const credentials = new PasswordLogInCredentials(email, password);
|
||||
const handledResponse = await this.handleCaptchaRequired(twoFactor, credentials);
|
||||
|
||||
// Error Response
|
||||
if (handledResponse instanceof Response) {
|
||||
return handledResponse;
|
||||
} else {
|
||||
response = handledResponse;
|
||||
}
|
||||
}
|
||||
if (response.requiresTwoFactor) {
|
||||
const twoFactorProviders = this.twoFactorService.getSupportedProviders(null);
|
||||
if (twoFactorProviders.length === 0) {
|
||||
return Response.badRequest("No providers available for this client.");
|
||||
}
|
||||
|
||||
if (twoFactorMethod != null) {
|
||||
try {
|
||||
selectedProvider = twoFactorProviders.filter((p) => p.type === twoFactorMethod)[0];
|
||||
} catch (e) {
|
||||
return Response.error("Invalid two-step login method.");
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedProvider == null) {
|
||||
if (twoFactorProviders.length === 1) {
|
||||
selectedProvider = twoFactorProviders[0];
|
||||
} else if (this.canInteract) {
|
||||
const twoFactorOptions: (string | Separator)[] = twoFactorProviders.map((p) => p.name);
|
||||
twoFactorOptions.push(new inquirer.Separator());
|
||||
twoFactorOptions.push("Cancel");
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||
output: process.stderr,
|
||||
})({
|
||||
type: "list",
|
||||
name: "method",
|
||||
message: "Two-step login method:",
|
||||
choices: twoFactorOptions,
|
||||
});
|
||||
const i = twoFactorOptions.indexOf(answer.method);
|
||||
if (i === twoFactorOptions.length - 1) {
|
||||
return Response.error("Login failed.");
|
||||
}
|
||||
selectedProvider = twoFactorProviders[i];
|
||||
}
|
||||
if (selectedProvider == null) {
|
||||
return Response.error("Login failed. No provider selected.");
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
twoFactorToken == null &&
|
||||
response.twoFactorProviders.size > 1 &&
|
||||
selectedProvider.type === TwoFactorProviderType.Email
|
||||
) {
|
||||
const emailReq = new TwoFactorEmailRequest();
|
||||
emailReq.email = this.authService.email;
|
||||
emailReq.masterPasswordHash = this.authService.masterPasswordHash;
|
||||
await this.apiService.postTwoFactorEmail(emailReq);
|
||||
}
|
||||
|
||||
if (twoFactorToken == null) {
|
||||
if (this.canInteract) {
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||
output: process.stderr,
|
||||
})({
|
||||
type: "input",
|
||||
name: "token",
|
||||
message: "Two-step login code:",
|
||||
});
|
||||
twoFactorToken = answer.token;
|
||||
}
|
||||
if (twoFactorToken == null || twoFactorToken === "") {
|
||||
return Response.badRequest("Code is required.");
|
||||
}
|
||||
}
|
||||
|
||||
response = await this.authService.logInTwoFactor(
|
||||
new TokenTwoFactorRequest(selectedProvider.type, twoFactorToken),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
if (response.captchaSiteKey) {
|
||||
const twoFactorRequest = new TokenTwoFactorRequest(selectedProvider.type, twoFactorToken);
|
||||
const handledResponse = await this.handleCaptchaRequired(twoFactorRequest);
|
||||
|
||||
// Error Response
|
||||
if (handledResponse instanceof Response) {
|
||||
return handledResponse;
|
||||
} else {
|
||||
response = handledResponse;
|
||||
}
|
||||
}
|
||||
|
||||
if (response.requiresTwoFactor) {
|
||||
return Response.error("Login failed.");
|
||||
}
|
||||
|
||||
if (response.resetMasterPassword) {
|
||||
return Response.error(
|
||||
"In order to log in with SSO from the CLI, you must first log in" +
|
||||
" through the web vault to set your master password."
|
||||
);
|
||||
}
|
||||
|
||||
// Handle Updating Temp Password if NOT using an API Key for authentication
|
||||
if (response.forcePasswordReset && clientId == null && clientSecret == null) {
|
||||
return await this.updateTempPassword();
|
||||
}
|
||||
|
||||
return await this.handleSuccessResponse();
|
||||
} catch (e) {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleSuccessResponse(): Promise<Response> {
|
||||
if (this.success != null) {
|
||||
const res = await this.success();
|
||||
return Response.success(res);
|
||||
} else {
|
||||
const res = new MessageResponse("You are logged in!", null);
|
||||
return Response.success(res);
|
||||
}
|
||||
}
|
||||
|
||||
private async updateTempPassword(error?: string): Promise<Response> {
|
||||
// If no interaction available, alert user to use web vault
|
||||
if (!this.canInteract) {
|
||||
await this.logout();
|
||||
this.authService.logOut(() => {
|
||||
/* Do nothing */
|
||||
});
|
||||
return Response.error(
|
||||
new MessageResponse(
|
||||
"An organization administrator recently changed your master password. In order to access the vault, you must update your master password now via the web vault. You have been logged out.",
|
||||
null
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (this.email == null || this.email === "undefined") {
|
||||
this.email = await this.stateService.getEmail();
|
||||
}
|
||||
|
||||
// Get New Master Password
|
||||
const baseMessage =
|
||||
"An organization administrator recently changed your master password. In order to access the vault, you must update your master password now.\n" +
|
||||
"Master password: ";
|
||||
const firstMessage = error != null ? error + baseMessage : baseMessage;
|
||||
const mp: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
|
||||
type: "password",
|
||||
name: "password",
|
||||
message: firstMessage,
|
||||
});
|
||||
const masterPassword = mp.password;
|
||||
|
||||
// Master Password Validation
|
||||
if (masterPassword == null || masterPassword === "") {
|
||||
return this.updateTempPassword("Master password is required.\n");
|
||||
}
|
||||
|
||||
if (masterPassword.length < 8) {
|
||||
return this.updateTempPassword("Master password must be at least 8 characters long.\n");
|
||||
}
|
||||
|
||||
// Strength & Policy Validation
|
||||
const strengthResult = this.passwordGenerationService.passwordStrength(
|
||||
masterPassword,
|
||||
this.getPasswordStrengthUserInput()
|
||||
);
|
||||
|
||||
// Get New Master Password Re-type
|
||||
const reTypeMessage = "Re-type New Master password (Strength: " + strengthResult.score + ")";
|
||||
const retype: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
|
||||
type: "password",
|
||||
name: "password",
|
||||
message: reTypeMessage,
|
||||
});
|
||||
const masterPasswordRetype = retype.password;
|
||||
|
||||
// Re-type Validation
|
||||
if (masterPassword !== masterPasswordRetype) {
|
||||
return this.updateTempPassword("Master password confirmation does not match.\n");
|
||||
}
|
||||
|
||||
// Get Hint (optional)
|
||||
const hint: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
|
||||
type: "input",
|
||||
name: "input",
|
||||
message: "Master Password Hint (optional):",
|
||||
});
|
||||
const masterPasswordHint = hint.input;
|
||||
|
||||
// Retrieve details for key generation
|
||||
const enforcedPolicyOptions = await firstValueFrom(
|
||||
this.policyService.masterPasswordPolicyOptions$()
|
||||
);
|
||||
const kdf = await this.stateService.getKdfType();
|
||||
const kdfIterations = await this.stateService.getKdfIterations();
|
||||
|
||||
if (
|
||||
enforcedPolicyOptions != null &&
|
||||
!this.policyService.evaluateMasterPassword(
|
||||
strengthResult.score,
|
||||
masterPassword,
|
||||
enforcedPolicyOptions
|
||||
)
|
||||
) {
|
||||
return this.updateTempPassword(
|
||||
"Your new master password does not meet the policy requirements.\n"
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Create new key and hash new password
|
||||
const newKey = await this.cryptoService.makeKey(
|
||||
masterPassword,
|
||||
this.email.trim().toLowerCase(),
|
||||
kdf,
|
||||
kdfIterations
|
||||
);
|
||||
const newPasswordHash = await this.cryptoService.hashPassword(masterPassword, newKey);
|
||||
|
||||
// Grab user's current enc key
|
||||
const userEncKey = await this.cryptoService.getEncKey();
|
||||
|
||||
// Create new encKey for the User
|
||||
const newEncKey = await this.cryptoService.remakeEncKey(newKey, userEncKey);
|
||||
|
||||
// Create request
|
||||
const request = new UpdateTempPasswordRequest();
|
||||
request.key = newEncKey[1].encryptedString;
|
||||
request.newMasterPasswordHash = newPasswordHash;
|
||||
request.masterPasswordHint = masterPasswordHint;
|
||||
|
||||
// Update user's password
|
||||
await this.apiService.putUpdateTempPassword(request);
|
||||
return this.handleSuccessResponse();
|
||||
} catch (e) {
|
||||
await this.logout();
|
||||
this.authService.logOut(() => {
|
||||
/* Do nothing */
|
||||
});
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleCaptchaRequired(
|
||||
twoFactorRequest: TokenTwoFactorRequest,
|
||||
credentials: PasswordLogInCredentials = null
|
||||
): Promise<AuthResult | Response> {
|
||||
const badCaptcha = Response.badRequest(
|
||||
"Your authentication request has been flagged and will require user interaction to proceed.\n" +
|
||||
"Please use your API key to validate this request and ensure BW_CLIENTSECRET is correct, if set.\n" +
|
||||
"(https://bitwarden.com/help/cli-auth-challenges)"
|
||||
);
|
||||
|
||||
try {
|
||||
const captchaClientSecret = await this.apiClientSecret(true);
|
||||
if (Utils.isNullOrWhitespace(captchaClientSecret)) {
|
||||
return badCaptcha;
|
||||
}
|
||||
|
||||
let authResultResponse: AuthResult = null;
|
||||
if (credentials != null) {
|
||||
credentials.captchaToken = captchaClientSecret;
|
||||
credentials.twoFactor = twoFactorRequest;
|
||||
authResultResponse = await this.authService.logIn(credentials);
|
||||
} else {
|
||||
authResultResponse = await this.authService.logInTwoFactor(
|
||||
twoFactorRequest,
|
||||
captchaClientSecret
|
||||
);
|
||||
}
|
||||
|
||||
return authResultResponse;
|
||||
} catch (e) {
|
||||
if (
|
||||
e instanceof ErrorResponse ||
|
||||
(e.constructor.name === ErrorResponse.name &&
|
||||
(e as ErrorResponse).message.includes("Captcha is invalid"))
|
||||
) {
|
||||
return badCaptcha;
|
||||
} else {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getPasswordStrengthUserInput() {
|
||||
let userInput: string[] = [];
|
||||
const atPosition = this.email.indexOf("@");
|
||||
if (atPosition > -1) {
|
||||
userInput = userInput.concat(
|
||||
this.email
|
||||
.substr(0, atPosition)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.split(/[^A-Za-z0-9]/)
|
||||
);
|
||||
}
|
||||
return userInput;
|
||||
}
|
||||
|
||||
private async apiClientId(): Promise<string> {
|
||||
let clientId: string = null;
|
||||
|
||||
const storedClientId: string = process.env.BW_CLIENTID;
|
||||
if (storedClientId == null) {
|
||||
if (this.canInteract) {
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||
output: process.stderr,
|
||||
})({
|
||||
type: "input",
|
||||
name: "clientId",
|
||||
message: "client_id:",
|
||||
});
|
||||
clientId = answer.clientId;
|
||||
} else {
|
||||
clientId = null;
|
||||
}
|
||||
} else {
|
||||
clientId = storedClientId;
|
||||
}
|
||||
|
||||
return clientId;
|
||||
}
|
||||
|
||||
private async apiClientSecret(isAdditionalAuthentication = false): Promise<string> {
|
||||
const additionalAuthenticationMessage = "Additional authentication required.\nAPI key ";
|
||||
let clientSecret: string = null;
|
||||
|
||||
const storedClientSecret: string = this.clientSecret || process.env.BW_CLIENTSECRET;
|
||||
if (this.canInteract && storedClientSecret == null) {
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||
output: process.stderr,
|
||||
})({
|
||||
type: "input",
|
||||
name: "clientSecret",
|
||||
message:
|
||||
(isAdditionalAuthentication ? additionalAuthenticationMessage : "") + "client_secret:",
|
||||
});
|
||||
clientSecret = answer.clientSecret;
|
||||
} else {
|
||||
clientSecret = storedClientSecret;
|
||||
}
|
||||
|
||||
return clientSecret;
|
||||
}
|
||||
|
||||
private async apiIdentifiers(): Promise<{ clientId: string; clientSecret: string }> {
|
||||
return {
|
||||
clientId: await this.apiClientId(),
|
||||
clientSecret: await this.apiClientSecret(),
|
||||
};
|
||||
}
|
||||
|
||||
private async openSsoPrompt(
|
||||
codeChallenge: string,
|
||||
state: string
|
||||
): Promise<{ ssoCode: string; orgIdentifier: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const callbackServer = http.createServer((req, res) => {
|
||||
const urlString = "http://localhost" + req.url;
|
||||
const url = new URL(urlString);
|
||||
const code = url.searchParams.get("code");
|
||||
const receivedState = url.searchParams.get("state");
|
||||
const orgIdentifier = this.getOrgIdentifierFromState(receivedState);
|
||||
res.setHeader("Content-Type", "text/html");
|
||||
if (code != null && receivedState != null && this.checkState(receivedState, state)) {
|
||||
res.writeHead(200);
|
||||
res.end(
|
||||
"<html><head><title>Success | Bitwarden CLI</title></head><body>" +
|
||||
"<h1>Successfully authenticated with the Bitwarden CLI</h1>" +
|
||||
"<p>You may now close this tab and return to the terminal.</p>" +
|
||||
"</body></html>"
|
||||
);
|
||||
callbackServer.close(() =>
|
||||
resolve({
|
||||
ssoCode: code,
|
||||
orgIdentifier: orgIdentifier,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
res.writeHead(400);
|
||||
res.end(
|
||||
"<html><head><title>Failed | Bitwarden CLI</title></head><body>" +
|
||||
"<h1>Something went wrong logging into the Bitwarden CLI</h1>" +
|
||||
"<p>You may now close this tab and return to the terminal.</p>" +
|
||||
"</body></html>"
|
||||
);
|
||||
callbackServer.close(() => reject());
|
||||
}
|
||||
});
|
||||
let foundPort = false;
|
||||
const webUrl = this.environmentService.getWebVaultUrl();
|
||||
for (let port = 8065; port <= 8070; port++) {
|
||||
try {
|
||||
this.ssoRedirectUri = "http://localhost:" + port;
|
||||
callbackServer.listen(port, () => {
|
||||
this.platformUtilsService.launchUri(
|
||||
webUrl +
|
||||
"/#/sso?clientId=" +
|
||||
this.clientId +
|
||||
"&redirectUri=" +
|
||||
encodeURIComponent(this.ssoRedirectUri) +
|
||||
"&state=" +
|
||||
state +
|
||||
"&codeChallenge=" +
|
||||
codeChallenge
|
||||
);
|
||||
});
|
||||
foundPort = true;
|
||||
break;
|
||||
} catch {
|
||||
// Ignore error since we run the same command up to 5 times.
|
||||
}
|
||||
}
|
||||
if (!foundPort) {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getOrgIdentifierFromState(state: string): string {
|
||||
if (state === null || state === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stateSplit = state.split("_identifier=");
|
||||
return stateSplit.length > 1 ? stateSplit[1] : null;
|
||||
}
|
||||
|
||||
private checkState(state: string, checkState: string): boolean {
|
||||
if (state === null || state === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (checkState === null || checkState === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const stateSplit = state.split("_identifier=");
|
||||
const checkStateSplit = checkState.split("_identifier=");
|
||||
return stateSplit[0] === checkStateSplit[0];
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
import { Response } from "../models/response";
|
||||
import { MessageResponse } from "../models/response/messageResponse";
|
||||
|
||||
export class LogoutCommand {
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private i18nService: I18nService,
|
||||
private logoutCallback: () => Promise<void>
|
||||
) {}
|
||||
|
||||
async run() {
|
||||
await this.logoutCallback();
|
||||
this.authService.logOut(() => {
|
||||
/* Do nothing */
|
||||
});
|
||||
const res = new MessageResponse("You have logged out.", null);
|
||||
return Response.success(res);
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import * as fetch from "node-fetch";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
|
||||
import { Response } from "../models/response";
|
||||
import { MessageResponse } from "../models/response/messageResponse";
|
||||
|
||||
export class UpdateCommand {
|
||||
inPkg = false;
|
||||
|
||||
constructor(
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private repoName: string,
|
||||
private executableName: string,
|
||||
private showExtendedMessage: boolean
|
||||
) {
|
||||
this.inPkg = !!(process as any).pkg;
|
||||
}
|
||||
|
||||
async run(): Promise<Response> {
|
||||
const currentVersion = await this.platformUtilsService.getApplicationVersion();
|
||||
|
||||
const response = await fetch.default(
|
||||
"https://api.github.com/repos/bitwarden/" + this.repoName + "/releases/latest"
|
||||
);
|
||||
if (response.status === 200) {
|
||||
const responseJson = await response.json();
|
||||
const res = new MessageResponse(null, null);
|
||||
|
||||
const tagName: string = responseJson.tag_name;
|
||||
if (tagName === "v" + currentVersion) {
|
||||
res.title = "No update available.";
|
||||
res.noColor = true;
|
||||
return Response.success(res);
|
||||
}
|
||||
|
||||
let downloadUrl: string = null;
|
||||
if (responseJson.assets != null) {
|
||||
for (const a of responseJson.assets) {
|
||||
const download: string = a.browser_download_url;
|
||||
if (download == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (download.indexOf(".zip") === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
process.platform === "win32" &&
|
||||
download.indexOf(this.executableName + "-windows") > -1
|
||||
) {
|
||||
downloadUrl = download;
|
||||
break;
|
||||
} else if (
|
||||
process.platform === "darwin" &&
|
||||
download.indexOf(this.executableName + "-macos") > -1
|
||||
) {
|
||||
downloadUrl = download;
|
||||
break;
|
||||
} else if (
|
||||
process.platform === "linux" &&
|
||||
download.indexOf(this.executableName + "-linux") > -1
|
||||
) {
|
||||
downloadUrl = download;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.title = "A new version is available: " + tagName;
|
||||
if (downloadUrl == null) {
|
||||
downloadUrl = "https://github.com/bitwarden/" + this.repoName + "/releases";
|
||||
} else {
|
||||
res.raw = downloadUrl;
|
||||
}
|
||||
res.message = "";
|
||||
if (responseJson.body != null && responseJson.body !== "") {
|
||||
res.message = responseJson.body + "\n\n";
|
||||
}
|
||||
|
||||
res.message += "You can download this update at " + downloadUrl;
|
||||
|
||||
if (this.showExtendedMessage) {
|
||||
if (this.inPkg) {
|
||||
res.message +=
|
||||
"\n\nIf you installed this CLI through a package manager " +
|
||||
"you should probably update using its update command instead.";
|
||||
} else {
|
||||
res.message +=
|
||||
"\n\nIf you installed this CLI through NPM " +
|
||||
"you should update using `npm install -g @bitwarden/" +
|
||||
this.repoName +
|
||||
"`";
|
||||
}
|
||||
}
|
||||
return Response.success(res);
|
||||
} else {
|
||||
return Response.error("Error contacting update API: " + response.status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { BaseResponse } from "./response/baseResponse";
|
||||
|
||||
export class Response {
|
||||
static error(error: any, data?: any): Response {
|
||||
const res = new Response();
|
||||
res.success = false;
|
||||
if (typeof error === "string") {
|
||||
res.message = error;
|
||||
} else {
|
||||
res.message =
|
||||
error.message != null
|
||||
? error.message
|
||||
: error.toString() === "[object Object]"
|
||||
? JSON.stringify(error)
|
||||
: error.toString();
|
||||
}
|
||||
res.data = data;
|
||||
return res;
|
||||
}
|
||||
|
||||
static notFound(): Response {
|
||||
return Response.error("Not found.");
|
||||
}
|
||||
|
||||
static badRequest(message: string): Response {
|
||||
return Response.error(message);
|
||||
}
|
||||
|
||||
static multipleResults(ids: string[]): Response {
|
||||
let msg =
|
||||
"More than one result was found. Try getting a specific object by `id` instead. " +
|
||||
"The following objects were found:";
|
||||
ids.forEach((id) => {
|
||||
msg += "\n" + id;
|
||||
});
|
||||
return Response.error(msg, ids);
|
||||
}
|
||||
|
||||
static success(data?: BaseResponse): Response {
|
||||
const res = new Response();
|
||||
res.success = true;
|
||||
res.data = data;
|
||||
return res;
|
||||
}
|
||||
|
||||
success: boolean;
|
||||
message: string;
|
||||
errorCode: number;
|
||||
data: BaseResponse;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export interface BaseResponse {
|
||||
object: string;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { BaseResponse } from "./baseResponse";
|
||||
|
||||
export class FileResponse implements BaseResponse {
|
||||
object: string;
|
||||
data: Buffer;
|
||||
fileName: string;
|
||||
|
||||
constructor(data: Buffer, fileName: string) {
|
||||
this.object = "file";
|
||||
this.data = data;
|
||||
this.fileName = fileName;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { BaseResponse } from "./baseResponse";
|
||||
|
||||
export class ListResponse implements BaseResponse {
|
||||
object: string;
|
||||
data: BaseResponse[];
|
||||
|
||||
constructor(data: BaseResponse[]) {
|
||||
this.object = "list";
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { BaseResponse } from "./baseResponse";
|
||||
|
||||
export class MessageResponse implements BaseResponse {
|
||||
object: string;
|
||||
title: string;
|
||||
message: string;
|
||||
raw: string;
|
||||
noColor = false;
|
||||
|
||||
constructor(title: string, message: string) {
|
||||
this.object = "message";
|
||||
this.title = title;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { BaseResponse } from "./baseResponse";
|
||||
|
||||
export class StringResponse implements BaseResponse {
|
||||
object: string;
|
||||
data: string;
|
||||
|
||||
constructor(data: string) {
|
||||
this.object = "string";
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
import * as child_process from "child_process";
|
||||
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { ClientType } from "@bitwarden/common/enums/clientType";
|
||||
import { DeviceType } from "@bitwarden/common/enums/deviceType";
|
||||
|
||||
// eslint-disable-next-line
|
||||
const open = require("open");
|
||||
|
||||
export class CliPlatformUtilsService implements PlatformUtilsService {
|
||||
clientType: ClientType;
|
||||
|
||||
private deviceCache: DeviceType = null;
|
||||
|
||||
constructor(clientType: ClientType, private packageJson: any) {
|
||||
this.clientType = clientType;
|
||||
}
|
||||
|
||||
getDevice(): DeviceType {
|
||||
if (!this.deviceCache) {
|
||||
switch (process.platform) {
|
||||
case "win32":
|
||||
this.deviceCache = DeviceType.WindowsDesktop;
|
||||
break;
|
||||
case "darwin":
|
||||
this.deviceCache = DeviceType.MacOsDesktop;
|
||||
break;
|
||||
case "linux":
|
||||
default:
|
||||
this.deviceCache = DeviceType.LinuxDesktop;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this.deviceCache;
|
||||
}
|
||||
|
||||
getDeviceString(): string {
|
||||
const device = DeviceType[this.getDevice()].toLowerCase();
|
||||
return device.replace("desktop", "");
|
||||
}
|
||||
|
||||
getClientType() {
|
||||
return this.clientType;
|
||||
}
|
||||
|
||||
isFirefox() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isChrome() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isEdge() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isOpera() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isVivaldi() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isSafari() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isMacAppStore() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isViewOpen() {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
launchUri(uri: string, options?: any): void {
|
||||
if (process.platform === "linux") {
|
||||
child_process.spawnSync("xdg-open", [uri]);
|
||||
} else {
|
||||
open(uri);
|
||||
}
|
||||
}
|
||||
|
||||
getApplicationVersion(): Promise<string> {
|
||||
return Promise.resolve(this.packageJson.version);
|
||||
}
|
||||
|
||||
getApplicationVersionSync(): string {
|
||||
return this.packageJson.version;
|
||||
}
|
||||
|
||||
supportsWebAuthn(win: Window) {
|
||||
return false;
|
||||
}
|
||||
|
||||
supportsDuo(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
showToast(
|
||||
type: "error" | "success" | "warning" | "info",
|
||||
title: string,
|
||||
text: string | string[],
|
||||
options?: any
|
||||
): void {
|
||||
throw new Error("Not implemented.");
|
||||
}
|
||||
|
||||
showDialog(
|
||||
text: string,
|
||||
title?: string,
|
||||
confirmText?: string,
|
||||
cancelText?: string,
|
||||
type?: string
|
||||
): Promise<boolean> {
|
||||
throw new Error("Not implemented.");
|
||||
}
|
||||
|
||||
isDev(): boolean {
|
||||
return process.env.BWCLI_ENV === "development";
|
||||
}
|
||||
|
||||
isSelfHost(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
copyToClipboard(text: string, options?: any): void {
|
||||
throw new Error("Not implemented.");
|
||||
}
|
||||
|
||||
readFromClipboard(options?: any): Promise<string> {
|
||||
throw new Error("Not implemented.");
|
||||
}
|
||||
|
||||
supportsBiometric(): Promise<boolean> {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
authenticateBiometric(): Promise<boolean> {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
supportsSecureStorage(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { LogLevelType } from "@bitwarden/common/enums/logLevelType";
|
||||
import { ConsoleLogService as BaseConsoleLogService } from "@bitwarden/common/services/consoleLog.service";
|
||||
|
||||
export class ConsoleLogService extends BaseConsoleLogService {
|
||||
constructor(isDev: boolean, filter: (level: LogLevelType) => boolean = null) {
|
||||
super(isDev, filter);
|
||||
}
|
||||
|
||||
write(level: LogLevelType, message: string) {
|
||||
if (this.filter != null && this.filter(level)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.BW_RESPONSE === "true") {
|
||||
// eslint-disable-next-line
|
||||
console.error(message);
|
||||
return;
|
||||
}
|
||||
|
||||
super.write(level, message);
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
import * as lowdb from "lowdb";
|
||||
import * as FileSync from "lowdb/adapters/FileSync";
|
||||
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
|
||||
import { NodeUtils } from "@bitwarden/common/misc/nodeUtils";
|
||||
import { sequentialize } from "@bitwarden/common/misc/sequentialize";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
|
||||
export class LowdbStorageService implements AbstractStorageService {
|
||||
protected dataFilePath: string;
|
||||
private db: lowdb.LowdbSync<any>;
|
||||
private defaults: any;
|
||||
private ready = false;
|
||||
|
||||
constructor(
|
||||
protected logService: LogService,
|
||||
defaults?: any,
|
||||
private dir?: string,
|
||||
private allowCache = false
|
||||
) {
|
||||
this.defaults = defaults;
|
||||
}
|
||||
|
||||
@sequentialize(() => "lowdbStorageInit")
|
||||
async init() {
|
||||
if (this.ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.info("Initializing lowdb storage service.");
|
||||
let adapter: lowdb.AdapterSync<any>;
|
||||
if (Utils.isNode && this.dir != null) {
|
||||
if (!fs.existsSync(this.dir)) {
|
||||
this.logService.warning(`Could not find dir, "${this.dir}"; creating it instead.`);
|
||||
NodeUtils.mkdirpSync(this.dir, "700");
|
||||
this.logService.info(`Created dir "${this.dir}".`);
|
||||
}
|
||||
this.dataFilePath = path.join(this.dir, "data.json");
|
||||
if (!fs.existsSync(this.dataFilePath)) {
|
||||
this.logService.warning(
|
||||
`Could not find data file, "${this.dataFilePath}"; creating it instead.`
|
||||
);
|
||||
fs.writeFileSync(this.dataFilePath, "", { mode: 0o600 });
|
||||
fs.chmodSync(this.dataFilePath, 0o600);
|
||||
this.logService.info(`Created data file "${this.dataFilePath}" with chmod 600.`);
|
||||
} else {
|
||||
this.logService.info(`db file "${this.dataFilePath} already exists"; using existing db`);
|
||||
}
|
||||
await this.lockDbFile(() => {
|
||||
adapter = new FileSync(this.dataFilePath);
|
||||
});
|
||||
}
|
||||
try {
|
||||
this.logService.info("Attempting to create lowdb storage adapter.");
|
||||
this.db = lowdb(adapter);
|
||||
this.logService.info("Successfully created lowdb storage adapter.");
|
||||
} catch (e) {
|
||||
if (e instanceof SyntaxError) {
|
||||
this.logService.warning(
|
||||
`Error creating lowdb storage adapter, "${e.message}"; emptying data file.`
|
||||
);
|
||||
if (fs.existsSync(this.dataFilePath)) {
|
||||
const backupPath = this.dataFilePath + ".bak";
|
||||
this.logService.warning(`Writing backup of data file to ${backupPath}`);
|
||||
await fs.copyFile(this.dataFilePath, backupPath, () => {
|
||||
this.logService.warning(
|
||||
`Error while creating data file backup, "${e.message}". No backup may have been created.`
|
||||
);
|
||||
});
|
||||
}
|
||||
adapter.write({});
|
||||
this.db = lowdb(adapter);
|
||||
} else {
|
||||
this.logService.error(`Error creating lowdb storage adapter, "${e.message}".`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.defaults != null) {
|
||||
this.lockDbFile(() => {
|
||||
this.logService.info("Writing defaults.");
|
||||
this.readForNoCache();
|
||||
this.db.defaults(this.defaults).write();
|
||||
this.logService.info("Successfully wrote defaults to db.");
|
||||
});
|
||||
}
|
||||
|
||||
this.ready = true;
|
||||
}
|
||||
|
||||
async get<T>(key: string): Promise<T> {
|
||||
await this.waitForReady();
|
||||
return this.lockDbFile(() => {
|
||||
this.readForNoCache();
|
||||
const val = this.db.get(key).value();
|
||||
this.logService.debug(`Successfully read ${key} from db`);
|
||||
if (val == null) {
|
||||
return null;
|
||||
}
|
||||
return val as T;
|
||||
});
|
||||
}
|
||||
|
||||
has(key: string): Promise<boolean> {
|
||||
return this.get(key).then((v) => v != null);
|
||||
}
|
||||
|
||||
async save(key: string, obj: any): Promise<any> {
|
||||
await this.waitForReady();
|
||||
return this.lockDbFile(() => {
|
||||
this.readForNoCache();
|
||||
this.db.set(key, obj).write();
|
||||
this.logService.debug(`Successfully wrote ${key} to db`);
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
async remove(key: string): Promise<any> {
|
||||
await this.waitForReady();
|
||||
return this.lockDbFile(() => {
|
||||
this.readForNoCache();
|
||||
this.db.unset(key).write();
|
||||
this.logService.debug(`Successfully removed ${key} from db`);
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
protected async lockDbFile<T>(action: () => T): Promise<T> {
|
||||
// Lock methods implemented in clients
|
||||
return Promise.resolve(action());
|
||||
}
|
||||
|
||||
private readForNoCache() {
|
||||
if (!this.allowCache) {
|
||||
this.db.read();
|
||||
}
|
||||
}
|
||||
|
||||
private async waitForReady() {
|
||||
if (!this.ready) {
|
||||
await this.init();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import * as FormData from "form-data";
|
||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||
import * as fe from "node-fetch";
|
||||
|
||||
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { TokenService } from "@bitwarden/common/abstractions/token.service";
|
||||
import { ApiService } from "@bitwarden/common/services/api.service";
|
||||
|
||||
(global as any).fetch = fe.default;
|
||||
(global as any).Request = fe.Request;
|
||||
(global as any).Response = fe.Response;
|
||||
(global as any).Headers = fe.Headers;
|
||||
(global as any).FormData = FormData;
|
||||
|
||||
export class NodeApiService extends ApiService {
|
||||
constructor(
|
||||
tokenService: TokenService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
environmentService: EnvironmentService,
|
||||
appIdService: AppIdService,
|
||||
logoutCallback: (expired: boolean) => Promise<void>,
|
||||
customUserAgent: string = null
|
||||
) {
|
||||
super(
|
||||
tokenService,
|
||||
platformUtilsService,
|
||||
environmentService,
|
||||
appIdService,
|
||||
logoutCallback,
|
||||
customUserAgent
|
||||
);
|
||||
}
|
||||
|
||||
nativeFetch(request: Request): Promise<Response> {
|
||||
const proxy = process.env.http_proxy || process.env.https_proxy;
|
||||
if (proxy) {
|
||||
(request as any).agent = new HttpsProxyAgent(proxy);
|
||||
}
|
||||
return fetch(request);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user