Merge branch 'master' into refactor/authService
1
.git-blame-ignore-revs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
193434461dbd9c48fe5dcbad95693470aec422ac
|
||||||
20
README.md
@@ -16,3 +16,23 @@ Common code referenced across Bitwarden JavaScript projects.
|
|||||||
- _Microsoft Build Tools 2015_ in Visual Studio Installer
|
- _Microsoft Build Tools 2015_ in Visual Studio Installer
|
||||||
- [Windows 10 SDK 17134](https://developer.microsoft.com/en-us/windows/downloads/sdk-archive/)
|
- [Windows 10 SDK 17134](https://developer.microsoft.com/en-us/windows/downloads/sdk-archive/)
|
||||||
either by downloading it seperately or through the Visual Studio Installer.
|
either by downloading it seperately or through the Visual Studio Installer.
|
||||||
|
|
||||||
|
## Prettier
|
||||||
|
|
||||||
|
We recently migrated to using Prettier as code formatter. All previous branches will need to updated to avoid large merge conflicts using the following steps:
|
||||||
|
|
||||||
|
1. Check out your local Branch
|
||||||
|
2. Run `git merge 8b2dfc6cdcb8ff5b604364c2ea6d343473aee7cd`
|
||||||
|
3. Resolve any merge conflicts, commit.
|
||||||
|
4. Run `npm run prettier`
|
||||||
|
5. Commit
|
||||||
|
6. Run `git merge -Xours 193434461dbd9c48fe5dcbad95693470aec422ac`
|
||||||
|
7. Push
|
||||||
|
|
||||||
|
### Git blame
|
||||||
|
|
||||||
|
We also recommend that you configure git to ignore the prettier revision using:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config blame.ignoreRevsFile .git-blame-ignore-revs
|
||||||
|
```
|
||||||
|
|||||||
@@ -18,6 +18,21 @@ const IconMap: any = {
|
|||||||
"fa-apple": String.fromCharCode(0xf179),
|
"fa-apple": String.fromCharCode(0xf179),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a mapping from supported card brands to
|
||||||
|
* the filenames of icon that should be present in images/cards folder of clients.
|
||||||
|
*/
|
||||||
|
const cardIcons: Record<string, string> = {
|
||||||
|
Visa: "card-visa",
|
||||||
|
Mastercard: "card-mastercard",
|
||||||
|
Amex: "card-amex",
|
||||||
|
Discover: "card-discover",
|
||||||
|
"Diners Club": "card-diners-club",
|
||||||
|
JCB: "card-jcb",
|
||||||
|
Maestro: "card-maestro",
|
||||||
|
UnionPay: "card-union-pay",
|
||||||
|
};
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-vault-icon",
|
selector: "app-vault-icon",
|
||||||
templateUrl: "icon.component.html",
|
templateUrl: "icon.component.html",
|
||||||
@@ -59,6 +74,7 @@ export class IconComponent implements OnChanges {
|
|||||||
break;
|
break;
|
||||||
case CipherType.Card:
|
case CipherType.Card:
|
||||||
this.icon = "fa-credit-card";
|
this.icon = "fa-credit-card";
|
||||||
|
this.setCardIcon();
|
||||||
break;
|
break;
|
||||||
case CipherType.Identity:
|
case CipherType.Identity:
|
||||||
this.icon = "fa-id-card-o";
|
this.icon = "fa-id-card-o";
|
||||||
@@ -102,4 +118,11 @@ export class IconComponent implements OnChanges {
|
|||||||
this.image = null;
|
this.image = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setCardIcon() {
|
||||||
|
const brand = this.cipher.card.brand;
|
||||||
|
if (this.imageEnabled && brand in cardIcons) {
|
||||||
|
this.icon = "credit-card-icon " + cardIcons[brand];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
angular/src/images/cards/amex-dark.png
Normal file
|
After Width: | Height: | Size: 773 B |
BIN
angular/src/images/cards/amex-light.png
Normal file
|
After Width: | Height: | Size: 773 B |
BIN
angular/src/images/cards/diners_club-dark.png
Normal file
|
After Width: | Height: | Size: 783 B |
BIN
angular/src/images/cards/diners_club-light.png
Normal file
|
After Width: | Height: | Size: 713 B |
BIN
angular/src/images/cards/discover-dark.png
Normal file
|
After Width: | Height: | Size: 808 B |
BIN
angular/src/images/cards/discover-light.png
Normal file
|
After Width: | Height: | Size: 830 B |
BIN
angular/src/images/cards/jcb-dark.png
Normal file
|
After Width: | Height: | Size: 836 B |
BIN
angular/src/images/cards/jcb-light.png
Normal file
|
After Width: | Height: | Size: 798 B |
BIN
angular/src/images/cards/maestro-dark.png
Normal file
|
After Width: | Height: | Size: 752 B |
BIN
angular/src/images/cards/maestro-light.png
Normal file
|
After Width: | Height: | Size: 820 B |
BIN
angular/src/images/cards/mastercard-dark.png
Normal file
|
After Width: | Height: | Size: 737 B |
BIN
angular/src/images/cards/mastercard-light.png
Normal file
|
After Width: | Height: | Size: 757 B |
BIN
angular/src/images/cards/union_pay-dark.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
angular/src/images/cards/union_pay-light.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
angular/src/images/cards/visa-dark.png
Normal file
|
After Width: | Height: | Size: 548 B |
BIN
angular/src/images/cards/visa-light.png
Normal file
|
After Width: | Height: | Size: 590 B |
44
angular/src/scss/icons.scss
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
$card-icons-base: "~@bitwarden/jslib-angular/src/images/cards/";
|
||||||
|
$card-icons: (
|
||||||
|
"visa": $card-icons-base + "visa-light.png",
|
||||||
|
"amex": $card-icons-base + "amex-light.png",
|
||||||
|
"diners-club": $card-icons-base + "diners_club-light.png",
|
||||||
|
"discover": $card-icons-base + "discover-light.png",
|
||||||
|
"jcb": $card-icons-base + "jcb-light.png",
|
||||||
|
"maestro": $card-icons-base + "maestro-light.png",
|
||||||
|
"mastercard": $card-icons-base + "mastercard-light.png",
|
||||||
|
"union-pay": $card-icons-base + "union_pay-light.png",
|
||||||
|
);
|
||||||
|
|
||||||
|
$card-icons-dark: (
|
||||||
|
"visa": $card-icons-base + "visa-dark.png",
|
||||||
|
"amex": $card-icons-base + "amex-dark.png",
|
||||||
|
"diners-club": $card-icons-base + "diners_club-dark.png",
|
||||||
|
"discover": $card-icons-base + "discover-dark.png",
|
||||||
|
"jcb": $card-icons-base + "jcb-dark.png",
|
||||||
|
"maestro": $card-icons-base + "maestro-dark.png",
|
||||||
|
"mastercard": $card-icons-base + "mastercard-dark.png",
|
||||||
|
"union-pay": $card-icons-base + "union_pay-dark.png",
|
||||||
|
);
|
||||||
|
|
||||||
|
.credit-card-icon {
|
||||||
|
display: block; // Resolves the parent container being slighly to big
|
||||||
|
height: 19px;
|
||||||
|
width: 24px;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
@each $name, $url in $card-icons {
|
||||||
|
.card-#{$name} {
|
||||||
|
background-image: url("#{$url}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@each $theme in $dark-icon-themes {
|
||||||
|
@each $name, $url in $card-icons-dark {
|
||||||
|
.#{$theme} .card-#{$name} {
|
||||||
|
background-image: url("#{$url}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,11 +24,11 @@ import { CollectionView } from "../models/view/collectionView";
|
|||||||
import { FolderView } from "../models/view/folderView";
|
import { FolderView } from "../models/view/folderView";
|
||||||
import { SendView } from "../models/view/sendView";
|
import { SendView } from "../models/view/sendView";
|
||||||
|
|
||||||
export abstract class StateService {
|
export abstract class StateService<T extends Account = Account> {
|
||||||
accounts: BehaviorSubject<{ [userId: string]: Account }>;
|
accounts: BehaviorSubject<{ [userId: string]: T }>;
|
||||||
activeAccount: BehaviorSubject<string>;
|
activeAccount: BehaviorSubject<string>;
|
||||||
|
|
||||||
addAccount: (account: Account) => Promise<void>;
|
addAccount: (account: T) => Promise<void>;
|
||||||
setActiveUser: (userId: string) => Promise<void>;
|
setActiveUser: (userId: string) => Promise<void>;
|
||||||
clean: (options?: StorageOptions) => Promise<void>;
|
clean: (options?: StorageOptions) => Promise<void>;
|
||||||
init: () => Promise<void>;
|
init: () => Promise<void>;
|
||||||
|
|||||||
@@ -172,7 +172,11 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldValue = field[valueKey].toString();
|
// TODO: when date FieldType exists, store this as a date field type instead of formatted Text if k is 'date'
|
||||||
|
const fieldValue =
|
||||||
|
field.k === "date"
|
||||||
|
? new Date(field[valueKey] * 1000).toUTCString()
|
||||||
|
: field[valueKey].toString();
|
||||||
const fieldDesignation =
|
const fieldDesignation =
|
||||||
field[designationKey] != null ? field[designationKey].toString() : null;
|
field[designationKey] != null ? field[designationKey].toString() : null;
|
||||||
|
|
||||||
|
|||||||
@@ -95,9 +95,6 @@ export class AccountProfile {
|
|||||||
hasPremiumPersonally?: boolean;
|
hasPremiumPersonally?: boolean;
|
||||||
lastActive?: number;
|
lastActive?: number;
|
||||||
lastSync?: string;
|
lastSync?: string;
|
||||||
ssoCodeVerifier?: string;
|
|
||||||
ssoOrganizationIdentifier?: string;
|
|
||||||
ssoState?: string;
|
|
||||||
userId?: string;
|
userId?: string;
|
||||||
usesKeyConnector?: boolean;
|
usesKeyConnector?: boolean;
|
||||||
keyHash?: string;
|
keyHash?: string;
|
||||||
@@ -133,9 +130,6 @@ export class AccountSettings {
|
|||||||
enableMinimizeToTray?: boolean;
|
enableMinimizeToTray?: boolean;
|
||||||
enableStartToTray?: boolean;
|
enableStartToTray?: boolean;
|
||||||
enableTray?: boolean;
|
enableTray?: boolean;
|
||||||
environmentUrls?: any = {
|
|
||||||
server: "bitwarden.com",
|
|
||||||
};
|
|
||||||
equivalentDomains?: any;
|
equivalentDomains?: any;
|
||||||
minimizeOnCopyToClipboard?: boolean;
|
minimizeOnCopyToClipboard?: boolean;
|
||||||
neverDomains?: { [id: string]: any };
|
neverDomains?: { [id: string]: any };
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ export class GlobalState {
|
|||||||
locale?: string;
|
locale?: string;
|
||||||
openAtLogin?: boolean;
|
openAtLogin?: boolean;
|
||||||
organizationInvitation?: any;
|
organizationInvitation?: any;
|
||||||
|
ssoCodeVerifier?: string;
|
||||||
|
ssoOrganizationIdentifier?: string;
|
||||||
|
ssoState?: string;
|
||||||
rememberedEmail?: string;
|
rememberedEmail?: string;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
window?: Map<string, any> = new Map<string, any>();
|
window?: Map<string, any> = new Map<string, any>();
|
||||||
@@ -21,4 +24,7 @@ export class GlobalState {
|
|||||||
noAutoPromptBiometrics?: boolean;
|
noAutoPromptBiometrics?: boolean;
|
||||||
noAutoPromptBiometricsText?: string;
|
noAutoPromptBiometricsText?: string;
|
||||||
stateVersion: number;
|
stateVersion: number;
|
||||||
|
environmentUrls?: any = {
|
||||||
|
server: "bitwarden.com",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Account } from "./account";
|
import { Account } from "./account";
|
||||||
import { GlobalState } from "./globalState";
|
import { GlobalState } from "./globalState";
|
||||||
|
|
||||||
export class State {
|
export class State<TAccount extends Account = Account> {
|
||||||
accounts: { [userId: string]: Account } = {};
|
accounts: { [userId: string]: TAccount } = {};
|
||||||
globals: GlobalState = new GlobalState();
|
globals: GlobalState = new GlobalState();
|
||||||
activeUserId: string;
|
activeUserId: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { HashPurpose } from "../enums/hashPurpose";
|
|||||||
import { KdfType } from "../enums/kdfType";
|
import { KdfType } from "../enums/kdfType";
|
||||||
import { TwoFactorProviderType } from "../enums/twoFactorProviderType";
|
import { TwoFactorProviderType } from "../enums/twoFactorProviderType";
|
||||||
|
|
||||||
import { AccountProfile, AccountTokens } from "../models/domain/account";
|
import { Account, AccountProfile, AccountTokens } from "../models/domain/account";
|
||||||
import { AuthResult } from "../models/domain/authResult";
|
import { AuthResult } from "../models/domain/authResult";
|
||||||
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ export class AuthService implements AuthServiceAbstraction {
|
|||||||
await this.cryptoService.setKey(key);
|
await this.cryptoService.setKey(key);
|
||||||
await this.cryptoService.setKeyHash(localHashedPassword);
|
await this.cryptoService.setKeyHash(localHashedPassword);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
const result = await this.processTokenResponse(response, false, onSuccess);
|
const result = await this.processTokenResponse(response, false, onSuccess);
|
||||||
|
|
||||||
if (result.requiresTwoFactor) {
|
if (result.requiresTwoFactor) {
|
||||||
@@ -112,13 +112,15 @@ export class AuthService implements AuthServiceAbstraction {
|
|||||||
): Promise<AuthResult> {
|
): Promise<AuthResult> {
|
||||||
this.twoFactorService.clearSelectedProvider();
|
this.twoFactorService.clearSelectedProvider();
|
||||||
|
|
||||||
const tokenRequest = this.savedTokenRequest ?? new SsoTokenRequest(
|
const tokenRequest =
|
||||||
code,
|
this.savedTokenRequest ??
|
||||||
codeVerifier,
|
new SsoTokenRequest(
|
||||||
redirectUrl,
|
code,
|
||||||
await this.buildTwoFactor(twoFactor),
|
codeVerifier,
|
||||||
await this.buildDeviceRequest()
|
redirectUrl,
|
||||||
);
|
await this.buildTwoFactor(twoFactor),
|
||||||
|
await this.buildDeviceRequest()
|
||||||
|
);
|
||||||
|
|
||||||
const response = await this.apiService.postIdentityToken(tokenRequest);
|
const response = await this.apiService.postIdentityToken(tokenRequest);
|
||||||
const tokenResponse = response as IdentityTokenResponse;
|
const tokenResponse = response as IdentityTokenResponse;
|
||||||
@@ -137,7 +139,7 @@ export class AuthService implements AuthServiceAbstraction {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const result = await this.processTokenResponse(response, newSsoUser, onSuccess);
|
const result = await this.processTokenResponse(response, newSsoUser, onSuccess);
|
||||||
|
|
||||||
@@ -155,12 +157,14 @@ export class AuthService implements AuthServiceAbstraction {
|
|||||||
): Promise<AuthResult> {
|
): Promise<AuthResult> {
|
||||||
this.twoFactorService.clearSelectedProvider();
|
this.twoFactorService.clearSelectedProvider();
|
||||||
|
|
||||||
const tokenRequest = this.savedTokenRequest ?? new ApiTokenRequest(
|
const tokenRequest =
|
||||||
clientId,
|
this.savedTokenRequest ??
|
||||||
clientSecret,
|
new ApiTokenRequest(
|
||||||
await this.buildTwoFactor(twoFactor),
|
clientId,
|
||||||
await this.buildDeviceRequest()
|
clientSecret,
|
||||||
);
|
await this.buildTwoFactor(twoFactor),
|
||||||
|
await this.buildDeviceRequest()
|
||||||
|
);
|
||||||
|
|
||||||
const response = await this.apiService.postIdentityToken(tokenRequest);
|
const response = await this.apiService.postIdentityToken(tokenRequest);
|
||||||
|
|
||||||
@@ -173,7 +177,7 @@ export class AuthService implements AuthServiceAbstraction {
|
|||||||
const keyConnectorUrl = this.environmentService.getKeyConnectorUrl();
|
const keyConnectorUrl = this.environmentService.getKeyConnectorUrl();
|
||||||
await this.keyConnectorService.getAndSetKey(keyConnectorUrl);
|
await this.keyConnectorService.getAndSetKey(keyConnectorUrl);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const result = await this.processTokenResponse(response, false, onSuccess);
|
const result = await this.processTokenResponse(response, false, onSuccess);
|
||||||
|
|
||||||
@@ -219,13 +223,13 @@ export class AuthService implements AuthServiceAbstraction {
|
|||||||
return this.savedTokenRequest instanceof PasswordTokenRequest;
|
return this.savedTokenRequest instanceof PasswordTokenRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
get email(): string {
|
get email(): string {
|
||||||
return (this.savedTokenRequest as PasswordTokenRequest).email;
|
return (this.savedTokenRequest as PasswordTokenRequest).email;
|
||||||
}
|
}
|
||||||
|
|
||||||
get masterPasswordHash(): string {
|
get masterPasswordHash(): string {
|
||||||
return (this.savedTokenRequest as PasswordTokenRequest).masterPasswordHash;
|
return (this.savedTokenRequest as PasswordTokenRequest).masterPasswordHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
async makePreloginKey(masterPassword: string, email: string): Promise<SymmetricCryptoKey> {
|
async makePreloginKey(masterPassword: string, email: string): Promise<SymmetricCryptoKey> {
|
||||||
email = email.trim().toLowerCase();
|
email = email.trim().toLowerCase();
|
||||||
@@ -275,7 +279,9 @@ export class AuthService implements AuthServiceAbstraction {
|
|||||||
|
|
||||||
if (this.setCryptoKeys && !newSsoUser) {
|
if (this.setCryptoKeys && !newSsoUser) {
|
||||||
await this.cryptoService.setEncKey(tokenResponse.key);
|
await this.cryptoService.setEncKey(tokenResponse.key);
|
||||||
await this.cryptoService.setEncPrivateKey(tokenResponse.privateKey ?? await this.createKeyPairForOldAccount());
|
await this.cryptoService.setEncPrivateKey(
|
||||||
|
tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await onSuccess();
|
await onSuccess();
|
||||||
@@ -314,25 +320,27 @@ export class AuthService implements AuthServiceAbstraction {
|
|||||||
|
|
||||||
private async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
|
private async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
|
||||||
const accountInformation = await this.tokenService.decodeToken(tokenResponse.accessToken);
|
const accountInformation = await this.tokenService.decodeToken(tokenResponse.accessToken);
|
||||||
await this.stateService.addAccount({
|
await this.stateService.addAccount(
|
||||||
profile: {
|
new Account({
|
||||||
...new AccountProfile(),
|
profile: {
|
||||||
...{
|
...new AccountProfile(),
|
||||||
userId: accountInformation.sub,
|
...{
|
||||||
email: accountInformation.email,
|
userId: accountInformation.sub,
|
||||||
hasPremiumPersonally: accountInformation.premium,
|
email: accountInformation.email,
|
||||||
kdfIterations: tokenResponse.kdfIterations,
|
hasPremiumPersonally: accountInformation.premium,
|
||||||
kdfType: tokenResponse.kdf,
|
kdfIterations: tokenResponse.kdfIterations,
|
||||||
|
kdfType: tokenResponse.kdf,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
tokens: {
|
||||||
tokens: {
|
...new AccountTokens(),
|
||||||
...new AccountTokens(),
|
...{
|
||||||
...{
|
accessToken: tokenResponse.accessToken,
|
||||||
accessToken: tokenResponse.accessToken,
|
refreshToken: tokenResponse.refreshToken,
|
||||||
refreshToken: tokenResponse.refreshToken,
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createKeyPairForOldAccount() {
|
private async createKeyPairForOldAccount() {
|
||||||
|
|||||||
@@ -761,13 +761,13 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
protected async storeKey(key: SymmetricCryptoKey, userId?: string) {
|
protected async storeKey(key: SymmetricCryptoKey, userId?: string) {
|
||||||
if (
|
if (await this.shouldStoreKey(KeySuffixOptions.Auto, userId)) {
|
||||||
(await this.shouldStoreKey(KeySuffixOptions.Auto, userId)) ||
|
await this.stateService.setCryptoMasterKeyAuto(key.keyB64, { userId: userId });
|
||||||
(await this.shouldStoreKey(KeySuffixOptions.Biometric, userId))
|
} else if (await this.shouldStoreKey(KeySuffixOptions.Biometric, userId)) {
|
||||||
) {
|
await this.stateService.setCryptoMasterKeyBiometric(key.keyB64, { userId: userId });
|
||||||
await this.stateService.setCryptoMasterKeyB64(key.keyB64, { userId: userId });
|
|
||||||
} else {
|
} else {
|
||||||
await this.stateService.setCryptoMasterKeyB64(null, { userId: userId });
|
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
|
||||||
|
await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,18 +109,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setUrlsFromStorage(): Promise<void> {
|
async setUrlsFromStorage(): Promise<void> {
|
||||||
const urlsObj: any = await this.stateService.getEnvironmentUrls();
|
const urls: any = await this.stateService.getEnvironmentUrls();
|
||||||
const urls = urlsObj || {
|
|
||||||
base: null,
|
|
||||||
api: null,
|
|
||||||
identity: null,
|
|
||||||
icons: null,
|
|
||||||
notifications: null,
|
|
||||||
events: null,
|
|
||||||
webVault: null,
|
|
||||||
keyConnector: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const envUrls = new EnvironmentUrls();
|
const envUrls = new EnvironmentUrls();
|
||||||
|
|
||||||
if (urls.base) {
|
if (urls.base) {
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { StateService as StateServiceAbstraction } from "../abstractions/state.service";
|
import { StateService as StateServiceAbstraction } from "../abstractions/state.service";
|
||||||
|
|
||||||
import { Account } from "../models/domain/account";
|
import {
|
||||||
|
Account,
|
||||||
|
AccountData,
|
||||||
|
AccountKeys,
|
||||||
|
AccountProfile,
|
||||||
|
AccountTokens,
|
||||||
|
} from "../models/domain/account";
|
||||||
|
|
||||||
import { LogService } from "../abstractions/log.service";
|
import { LogService } from "../abstractions/log.service";
|
||||||
import { StorageService } from "../abstractions/storage.service";
|
import { StorageService } from "../abstractions/storage.service";
|
||||||
@@ -34,19 +40,21 @@ import { SendData } from "../models/data/sendData";
|
|||||||
|
|
||||||
import { BehaviorSubject } from "rxjs";
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
import { StateMigrationService } from "./stateMigration.service";
|
import { StateMigrationService } from "../abstractions/stateMigration.service";
|
||||||
|
|
||||||
export class StateService implements StateServiceAbstraction {
|
export class StateService<TAccount extends Account = Account>
|
||||||
accounts = new BehaviorSubject<{ [userId: string]: Account }>({});
|
implements StateServiceAbstraction<TAccount>
|
||||||
|
{
|
||||||
|
accounts = new BehaviorSubject<{ [userId: string]: TAccount }>({});
|
||||||
activeAccount = new BehaviorSubject<string>(null);
|
activeAccount = new BehaviorSubject<string>(null);
|
||||||
|
|
||||||
private state: State = new State();
|
protected state: State<TAccount> = new State<TAccount>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private storageService: StorageService,
|
protected storageService: StorageService,
|
||||||
private secureStorageService: StorageService,
|
protected secureStorageService: StorageService,
|
||||||
private logService: LogService,
|
protected logService: LogService,
|
||||||
private stateMigrationService: StateMigrationService
|
protected stateMigrationService: StateMigrationService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
@@ -60,7 +68,7 @@ export class StateService implements StateServiceAbstraction {
|
|||||||
|
|
||||||
async loadStateFromDisk() {
|
async loadStateFromDisk() {
|
||||||
if ((await this.getActiveUserIdFromStorage()) != null) {
|
if ((await this.getActiveUserIdFromStorage()) != null) {
|
||||||
const diskState = await this.storageService.get<State>(
|
const diskState = await this.storageService.get<State<TAccount>>(
|
||||||
"state",
|
"state",
|
||||||
await this.defaultOnDiskOptions()
|
await this.defaultOnDiskOptions()
|
||||||
);
|
);
|
||||||
@@ -71,10 +79,7 @@ export class StateService implements StateServiceAbstraction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async addAccount(account: Account) {
|
async addAccount(account: TAccount) {
|
||||||
if (account?.profile?.userId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.state.accounts[account.profile.userId] = account;
|
this.state.accounts[account.profile.userId] = account;
|
||||||
await this.scaffoldNewAccountStorage(account);
|
await this.scaffoldNewAccountStorage(account);
|
||||||
await this.setActiveUser(account.profile.userId);
|
await this.setActiveUser(account.profile.userId);
|
||||||
@@ -83,7 +88,7 @@ export class StateService implements StateServiceAbstraction {
|
|||||||
|
|
||||||
async setActiveUser(userId: string): Promise<void> {
|
async setActiveUser(userId: string): Promise<void> {
|
||||||
this.state.activeUserId = userId;
|
this.state.activeUserId = userId;
|
||||||
const storedState = await this.storageService.get<State>(
|
const storedState = await this.storageService.get<State<TAccount>>(
|
||||||
"state",
|
"state",
|
||||||
await this.defaultOnDiskOptions()
|
await this.defaultOnDiskOptions()
|
||||||
);
|
);
|
||||||
@@ -1321,47 +1326,64 @@ export class StateService implements StateServiceAbstraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getEntityId(options?: StorageOptions): Promise<string> {
|
async getEntityId(options?: StorageOptions): Promise<string> {
|
||||||
return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))
|
return (
|
||||||
?.profile?.entityId;
|
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
||||||
|
)?.profile?.entityId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setEntityId(value: string, options?: StorageOptions): Promise<void> {
|
async setEntityId(value: string, options?: StorageOptions): Promise<void> {
|
||||||
const account = await this.getAccount(
|
const account = await this.getAccount(
|
||||||
this.reconcileOptions(options, this.defaultInMemoryOptions)
|
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
|
||||||
);
|
);
|
||||||
account.profile.entityId = value;
|
account.profile.entityId = value;
|
||||||
await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions));
|
await this.saveAccount(
|
||||||
|
account,
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEntityType(options?: StorageOptions): Promise<any> {
|
async getEntityType(options?: StorageOptions): Promise<any> {
|
||||||
return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))
|
return (
|
||||||
?.profile?.entityType;
|
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
||||||
|
)?.profile?.entityType;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setEntityType(value: string, options?: StorageOptions): Promise<void> {
|
async setEntityType(value: string, options?: StorageOptions): Promise<void> {
|
||||||
const account = await this.getAccount(
|
const account = await this.getAccount(
|
||||||
this.reconcileOptions(options, this.defaultInMemoryOptions)
|
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
|
||||||
);
|
);
|
||||||
account.profile.entityType = value;
|
account.profile.entityType = value;
|
||||||
await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions));
|
await this.saveAccount(
|
||||||
|
account,
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEnvironmentUrls(options?: StorageOptions): Promise<any> {
|
async getEnvironmentUrls(options?: StorageOptions): Promise<any> {
|
||||||
return (
|
return (
|
||||||
(await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
|
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
|
||||||
?.settings?.environmentUrls ?? {
|
?.environmentUrls ?? {
|
||||||
|
base: null,
|
||||||
|
api: null,
|
||||||
|
identity: null,
|
||||||
|
icons: null,
|
||||||
|
notifications: null,
|
||||||
|
events: null,
|
||||||
|
webVault: null,
|
||||||
|
keyConnector: null,
|
||||||
|
// TODO: this is a bug and we should use base instead for the server detail in the account switcher, otherwise self hosted urls will not show correctly
|
||||||
server: "bitwarden.com",
|
server: "bitwarden.com",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setEnvironmentUrls(value: any, options?: StorageOptions): Promise<void> {
|
async setEnvironmentUrls(value: any, options?: StorageOptions): Promise<void> {
|
||||||
const account = await this.getAccount(
|
const globals = await this.getGlobals(
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
);
|
);
|
||||||
account.settings.environmentUrls = value;
|
globals.environmentUrls = value;
|
||||||
await this.saveAccount(
|
await this.saveGlobals(
|
||||||
account,
|
globals,
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1402,17 +1424,20 @@ export class StateService implements StateServiceAbstraction {
|
|||||||
|
|
||||||
async getEverBeenUnlocked(options?: StorageOptions): Promise<boolean> {
|
async getEverBeenUnlocked(options?: StorageOptions): Promise<boolean> {
|
||||||
return (
|
return (
|
||||||
(await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.profile
|
(await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
|
||||||
?.everBeenUnlocked ?? false
|
?.profile?.everBeenUnlocked ?? false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setEverBeenUnlocked(value: boolean, options?: StorageOptions): Promise<void> {
|
async setEverBeenUnlocked(value: boolean, options?: StorageOptions): Promise<void> {
|
||||||
const account = await this.getAccount(
|
const account = await this.getAccount(
|
||||||
this.reconcileOptions(options, this.defaultInMemoryOptions)
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
);
|
);
|
||||||
account.profile.everBeenUnlocked = value;
|
account.profile.everBeenUnlocked = value;
|
||||||
await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions));
|
await this.saveAccount(
|
||||||
|
account,
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getForcePasswordReset(options?: StorageOptions): Promise<boolean> {
|
async getForcePasswordReset(options?: StorageOptions): Promise<boolean> {
|
||||||
@@ -1874,46 +1899,54 @@ export class StateService implements StateServiceAbstraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getSsoCodeVerifier(options?: StorageOptions): Promise<string> {
|
async getSsoCodeVerifier(options?: StorageOptions): Promise<string> {
|
||||||
return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))
|
return (
|
||||||
?.profile?.ssoCodeVerifier;
|
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||||
|
)?.ssoCodeVerifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setSsoCodeVerifier(value: string, options?: StorageOptions): Promise<void> {
|
async setSsoCodeVerifier(value: string, options?: StorageOptions): Promise<void> {
|
||||||
const account = await this.getAccount(
|
const globals = await this.getGlobals(
|
||||||
this.reconcileOptions(options, this.defaultInMemoryOptions)
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
globals.ssoCodeVerifier = value;
|
||||||
|
await this.saveGlobals(
|
||||||
|
globals,
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
);
|
);
|
||||||
account.profile.ssoCodeVerifier = value;
|
|
||||||
await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSsoOrgIdentifier(options?: StorageOptions): Promise<string> {
|
async getSsoOrgIdentifier(options?: StorageOptions): Promise<string> {
|
||||||
return (
|
return (
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
||||||
)?.profile?.ssoOrganizationIdentifier;
|
)?.ssoOrganizationIdentifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setSsoOrganizationIdentifier(value: string, options?: StorageOptions): Promise<void> {
|
async setSsoOrganizationIdentifier(value: string, options?: StorageOptions): Promise<void> {
|
||||||
const account = await this.getAccount(
|
const globals = await this.getGlobals(
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
|
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
|
||||||
);
|
);
|
||||||
account.profile.ssoOrganizationIdentifier = value;
|
globals.ssoOrganizationIdentifier = value;
|
||||||
await this.saveAccount(
|
await this.saveGlobals(
|
||||||
account,
|
globals,
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
|
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSsoState(options?: StorageOptions): Promise<string> {
|
async getSsoState(options?: StorageOptions): Promise<string> {
|
||||||
return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))
|
return (
|
||||||
?.profile?.ssoState;
|
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||||
|
)?.ssoState;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setSsoState(value: string, options?: StorageOptions): Promise<void> {
|
async setSsoState(value: string, options?: StorageOptions): Promise<void> {
|
||||||
const account = await this.getAccount(
|
const globals = await this.getGlobals(
|
||||||
this.reconcileOptions(options, this.defaultInMemoryOptions)
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
globals.ssoState = value;
|
||||||
|
await this.saveGlobals(
|
||||||
|
globals,
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
);
|
);
|
||||||
account.profile.ssoState = value;
|
|
||||||
await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTheme(options?: StorageOptions): Promise<string> {
|
async getTheme(options?: StorageOptions): Promise<string> {
|
||||||
@@ -1973,10 +2006,7 @@ export class StateService implements StateServiceAbstraction {
|
|||||||
const accountVaultTimeout = (
|
const accountVaultTimeout = (
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
||||||
)?.settings?.vaultTimeout;
|
)?.settings?.vaultTimeout;
|
||||||
const globalVaultTimeout = (
|
return accountVaultTimeout;
|
||||||
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
|
||||||
)?.vaultTimeout;
|
|
||||||
return accountVaultTimeout ?? globalVaultTimeout ?? 15;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async setVaultTimeout(value: number, options?: StorageOptions): Promise<void> {
|
async setVaultTimeout(value: number, options?: StorageOptions): Promise<void> {
|
||||||
@@ -2039,7 +2069,7 @@ export class StateService implements StateServiceAbstraction {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getGlobals(options: StorageOptions): Promise<GlobalState> {
|
protected async getGlobals(options: StorageOptions): Promise<GlobalState> {
|
||||||
let globals: GlobalState;
|
let globals: GlobalState;
|
||||||
if (this.useMemory(options.storageLocation)) {
|
if (this.useMemory(options.storageLocation)) {
|
||||||
globals = this.getGlobalsFromMemory();
|
globals = this.getGlobalsFromMemory();
|
||||||
@@ -2052,39 +2082,41 @@ export class StateService implements StateServiceAbstraction {
|
|||||||
return globals ?? new GlobalState();
|
return globals ?? new GlobalState();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async saveGlobals(globals: GlobalState, options: StorageOptions) {
|
protected async saveGlobals(globals: GlobalState, options: StorageOptions) {
|
||||||
return this.useMemory(options.storageLocation)
|
return this.useMemory(options.storageLocation)
|
||||||
? this.saveGlobalsToMemory(globals)
|
? this.saveGlobalsToMemory(globals)
|
||||||
: await this.saveGlobalsToDisk(globals, options);
|
: await this.saveGlobalsToDisk(globals, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getGlobalsFromMemory(): GlobalState {
|
protected getGlobalsFromMemory(): GlobalState {
|
||||||
return this.state.globals;
|
return this.state.globals;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getGlobalsFromDisk(options: StorageOptions): Promise<GlobalState> {
|
protected async getGlobalsFromDisk(options: StorageOptions): Promise<GlobalState> {
|
||||||
return (await this.storageService.get<State>("state", options))?.globals;
|
return (await this.storageService.get<State<TAccount>>("state", options))?.globals;
|
||||||
}
|
}
|
||||||
|
|
||||||
private saveGlobalsToMemory(globals: GlobalState): void {
|
protected saveGlobalsToMemory(globals: GlobalState): void {
|
||||||
this.state.globals = globals;
|
this.state.globals = globals;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async saveGlobalsToDisk(globals: GlobalState, options: StorageOptions): Promise<void> {
|
protected async saveGlobalsToDisk(globals: GlobalState, options: StorageOptions): Promise<void> {
|
||||||
if (options.useSecureStorage) {
|
if (options.useSecureStorage) {
|
||||||
const state = (await this.secureStorageService.get<State>("state", options)) ?? new State();
|
const state =
|
||||||
|
(await this.secureStorageService.get<State<TAccount>>("state", options)) ?? new State();
|
||||||
state.globals = globals;
|
state.globals = globals;
|
||||||
await this.secureStorageService.save("state", state, options);
|
await this.secureStorageService.save("state", state, options);
|
||||||
} else {
|
} else {
|
||||||
const state = (await this.storageService.get<State>("state", options)) ?? new State();
|
const state =
|
||||||
|
(await this.storageService.get<State<TAccount>>("state", options)) ?? new State();
|
||||||
state.globals = globals;
|
state.globals = globals;
|
||||||
await this.saveStateToStorage(state, options);
|
await this.saveStateToStorage(state, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAccount(options: StorageOptions): Promise<Account> {
|
protected async getAccount(options: StorageOptions): Promise<TAccount> {
|
||||||
try {
|
try {
|
||||||
let account: Account;
|
let account: TAccount;
|
||||||
if (this.useMemory(options.storageLocation)) {
|
if (this.useMemory(options.storageLocation)) {
|
||||||
account = this.getAccountFromMemory(options);
|
account = this.getAccountFromMemory(options);
|
||||||
}
|
}
|
||||||
@@ -2093,51 +2125,51 @@ export class StateService implements StateServiceAbstraction {
|
|||||||
account = await this.getAccountFromDisk(options);
|
account = await this.getAccountFromDisk(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
return account != null ? new Account(account) : null;
|
return account;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logService.error(e);
|
this.logService.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAccountFromMemory(options: StorageOptions): Account {
|
protected getAccountFromMemory(options: StorageOptions): TAccount {
|
||||||
if (this.state.accounts == null) {
|
if (this.state.accounts == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return this.state.accounts[this.getUserIdFromMemory(options)];
|
return this.state.accounts[this.getUserIdFromMemory(options)];
|
||||||
}
|
}
|
||||||
|
|
||||||
private getUserIdFromMemory(options: StorageOptions): string {
|
protected getUserIdFromMemory(options: StorageOptions): string {
|
||||||
return options?.userId != null
|
return options?.userId != null
|
||||||
? this.state.accounts[options.userId]?.profile?.userId
|
? this.state.accounts[options.userId]?.profile?.userId
|
||||||
: this.state.activeUserId;
|
: this.state.activeUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAccountFromDisk(options: StorageOptions): Promise<Account> {
|
protected async getAccountFromDisk(options: StorageOptions): Promise<TAccount> {
|
||||||
if (options?.userId == null && this.state.activeUserId == null) {
|
if (options?.userId == null && this.state.activeUserId == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = options?.useSecureStorage
|
const state = options?.useSecureStorage
|
||||||
? (await this.secureStorageService.get<State>("state", options)) ??
|
? (await this.secureStorageService.get<State<TAccount>>("state", options)) ??
|
||||||
(await this.storageService.get<State>(
|
(await this.storageService.get<State<TAccount>>(
|
||||||
"state",
|
"state",
|
||||||
this.reconcileOptions(options, { htmlStorageLocation: HtmlStorageLocation.Local })
|
this.reconcileOptions(options, { htmlStorageLocation: HtmlStorageLocation.Local })
|
||||||
))
|
))
|
||||||
: await this.storageService.get<State>("state", options);
|
: await this.storageService.get<State<TAccount>>("state", options);
|
||||||
|
|
||||||
return state?.accounts[options?.userId ?? this.state.activeUserId];
|
return state?.accounts[options?.userId ?? this.state.activeUserId];
|
||||||
}
|
}
|
||||||
|
|
||||||
private useMemory(storageLocation: StorageLocation) {
|
protected useMemory(storageLocation: StorageLocation) {
|
||||||
return storageLocation === StorageLocation.Memory || storageLocation === StorageLocation.Both;
|
return storageLocation === StorageLocation.Memory || storageLocation === StorageLocation.Both;
|
||||||
}
|
}
|
||||||
|
|
||||||
private useDisk(storageLocation: StorageLocation) {
|
protected useDisk(storageLocation: StorageLocation) {
|
||||||
return storageLocation === StorageLocation.Disk || storageLocation === StorageLocation.Both;
|
return storageLocation === StorageLocation.Disk || storageLocation === StorageLocation.Both;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async saveAccount(
|
protected async saveAccount(
|
||||||
account: Account,
|
account: TAccount,
|
||||||
options: StorageOptions = {
|
options: StorageOptions = {
|
||||||
storageLocation: StorageLocation.Both,
|
storageLocation: StorageLocation.Both,
|
||||||
useSecureStorage: false,
|
useSecureStorage: false,
|
||||||
@@ -2148,86 +2180,75 @@ export class StateService implements StateServiceAbstraction {
|
|||||||
: await this.saveAccountToDisk(account, options);
|
: await this.saveAccountToDisk(account, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async saveAccountToDisk(account: Account, options: StorageOptions): Promise<void> {
|
protected async saveAccountToDisk(account: TAccount, options: StorageOptions): Promise<void> {
|
||||||
const storageLocation = options.useSecureStorage
|
const storageLocation = options.useSecureStorage
|
||||||
? this.secureStorageService
|
? this.secureStorageService
|
||||||
: this.storageService;
|
: this.storageService;
|
||||||
|
|
||||||
const state = (await storageLocation.get<State>("state", options)) ?? new State();
|
const state =
|
||||||
|
(await storageLocation.get<State<TAccount>>("state", options)) ?? new State<TAccount>();
|
||||||
state.accounts[account.profile.userId] = account;
|
state.accounts[account.profile.userId] = account;
|
||||||
|
|
||||||
await storageLocation.save("state", state, options);
|
await storageLocation.save("state", state, options);
|
||||||
await this.pushAccounts();
|
await this.pushAccounts();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async saveAccountToMemory(account: Account): Promise<void> {
|
protected async saveAccountToMemory(account: TAccount): Promise<void> {
|
||||||
if (this.getAccountFromMemory({ userId: account.profile.userId }) !== null) {
|
if (this.getAccountFromMemory({ userId: account.profile.userId }) !== null) {
|
||||||
this.state.accounts[account.profile.userId] = account;
|
this.state.accounts[account.profile.userId] = account;
|
||||||
}
|
}
|
||||||
await this.pushAccounts();
|
await this.pushAccounts();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async scaffoldNewAccountStorage(account: Account): Promise<void> {
|
protected async scaffoldNewAccountStorage(account: TAccount): Promise<void> {
|
||||||
await this.scaffoldNewAccountLocalStorage(account);
|
await this.scaffoldNewAccountLocalStorage(account);
|
||||||
await this.scaffoldNewAccountSessionStorage(account);
|
await this.scaffoldNewAccountSessionStorage(account);
|
||||||
await this.scaffoldNewAccountMemoryStorage(account);
|
await this.scaffoldNewAccountMemoryStorage(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async scaffoldNewAccountLocalStorage(account: Account): Promise<void> {
|
protected async scaffoldNewAccountLocalStorage(account: TAccount): Promise<void> {
|
||||||
const storedState =
|
const storedState =
|
||||||
(await this.storageService.get<State>("state", await this.defaultOnDiskLocalOptions())) ??
|
(await this.storageService.get<State<TAccount>>(
|
||||||
new State();
|
"state",
|
||||||
|
await this.defaultOnDiskLocalOptions()
|
||||||
|
)) ?? new State<TAccount>();
|
||||||
const storedAccount = storedState.accounts[account.profile.userId];
|
const storedAccount = storedState.accounts[account.profile.userId];
|
||||||
if (storedAccount != null) {
|
if (storedAccount != null) {
|
||||||
account = {
|
account.settings = storedAccount.settings;
|
||||||
settings: storedAccount.settings,
|
|
||||||
profile: account.profile,
|
|
||||||
tokens: account.tokens,
|
|
||||||
keys: account.keys,
|
|
||||||
data: account.data,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
storedState.accounts[account.profile.userId] = account;
|
storedState.accounts[account.profile.userId] = account;
|
||||||
await this.saveStateToStorage(storedState, await this.defaultOnDiskLocalOptions());
|
await this.saveStateToStorage(storedState, await this.defaultOnDiskLocalOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async scaffoldNewAccountMemoryStorage(account: Account): Promise<void> {
|
protected async scaffoldNewAccountMemoryStorage(account: TAccount): Promise<void> {
|
||||||
const storedState =
|
const storedState =
|
||||||
(await this.storageService.get<State>("state", await this.defaultOnDiskMemoryOptions())) ??
|
(await this.storageService.get<State<TAccount>>(
|
||||||
new State();
|
"state",
|
||||||
|
await this.defaultOnDiskMemoryOptions()
|
||||||
|
)) ?? new State<TAccount>();
|
||||||
const storedAccount = storedState.accounts[account.profile.userId];
|
const storedAccount = storedState.accounts[account.profile.userId];
|
||||||
if (storedAccount != null) {
|
if (storedAccount != null) {
|
||||||
account = {
|
account.settings = storedAccount.settings;
|
||||||
settings: storedAccount.settings,
|
|
||||||
profile: account.profile,
|
|
||||||
tokens: account.tokens,
|
|
||||||
keys: account.keys,
|
|
||||||
data: account.data,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
storedState.accounts[account.profile.userId] = account;
|
storedState.accounts[account.profile.userId] = account;
|
||||||
await this.saveStateToStorage(storedState, await this.defaultOnDiskMemoryOptions());
|
await this.saveStateToStorage(storedState, await this.defaultOnDiskMemoryOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async scaffoldNewAccountSessionStorage(account: Account): Promise<void> {
|
protected async scaffoldNewAccountSessionStorage(account: TAccount): Promise<void> {
|
||||||
const storedState =
|
const storedState =
|
||||||
(await this.storageService.get<State>("state", await this.defaultOnDiskOptions())) ??
|
(await this.storageService.get<State<TAccount>>(
|
||||||
new State();
|
"state",
|
||||||
|
await this.defaultOnDiskOptions()
|
||||||
|
)) ?? new State<TAccount>();
|
||||||
const storedAccount = storedState.accounts[account.profile.userId];
|
const storedAccount = storedState.accounts[account.profile.userId];
|
||||||
if (storedAccount != null) {
|
if (storedAccount != null) {
|
||||||
account = {
|
account.settings = storedAccount.settings;
|
||||||
settings: storedAccount.settings,
|
|
||||||
profile: account.profile,
|
|
||||||
tokens: account.tokens,
|
|
||||||
keys: account.keys,
|
|
||||||
data: account.data,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
storedState.accounts[account.profile.userId] = account;
|
storedState.accounts[account.profile.userId] = account;
|
||||||
await this.saveStateToStorage(storedState, await this.defaultOnDiskOptions());
|
await this.saveStateToStorage(storedState, await this.defaultOnDiskOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async pushAccounts(): Promise<void> {
|
protected async pushAccounts(): Promise<void> {
|
||||||
await this.pruneInMemoryAccounts();
|
await this.pruneInMemoryAccounts();
|
||||||
if (this.state?.accounts == null || Object.keys(this.state.accounts).length < 1) {
|
if (this.state?.accounts == null || Object.keys(this.state.accounts).length < 1) {
|
||||||
this.accounts.next(null);
|
this.accounts.next(null);
|
||||||
@@ -2237,7 +2258,7 @@ export class StateService implements StateServiceAbstraction {
|
|||||||
this.accounts.next(this.state.accounts);
|
this.accounts.next(this.state.accounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
private reconcileOptions(
|
protected reconcileOptions(
|
||||||
requestedOptions: StorageOptions,
|
requestedOptions: StorageOptions,
|
||||||
defaultOptions: StorageOptions
|
defaultOptions: StorageOptions
|
||||||
): StorageOptions {
|
): StorageOptions {
|
||||||
@@ -2255,11 +2276,11 @@ export class StateService implements StateServiceAbstraction {
|
|||||||
return requestedOptions;
|
return requestedOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get defaultInMemoryOptions(): StorageOptions {
|
protected get defaultInMemoryOptions(): StorageOptions {
|
||||||
return { storageLocation: StorageLocation.Memory, userId: this.state.activeUserId };
|
return { storageLocation: StorageLocation.Memory, userId: this.state.activeUserId };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async defaultOnDiskOptions(): Promise<StorageOptions> {
|
protected async defaultOnDiskOptions(): Promise<StorageOptions> {
|
||||||
return {
|
return {
|
||||||
storageLocation: StorageLocation.Disk,
|
storageLocation: StorageLocation.Disk,
|
||||||
htmlStorageLocation: HtmlStorageLocation.Session,
|
htmlStorageLocation: HtmlStorageLocation.Session,
|
||||||
@@ -2268,7 +2289,7 @@ export class StateService implements StateServiceAbstraction {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async defaultOnDiskLocalOptions(): Promise<StorageOptions> {
|
protected async defaultOnDiskLocalOptions(): Promise<StorageOptions> {
|
||||||
return {
|
return {
|
||||||
storageLocation: StorageLocation.Disk,
|
storageLocation: StorageLocation.Disk,
|
||||||
htmlStorageLocation: HtmlStorageLocation.Local,
|
htmlStorageLocation: HtmlStorageLocation.Local,
|
||||||
@@ -2277,7 +2298,7 @@ export class StateService implements StateServiceAbstraction {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async defaultOnDiskMemoryOptions(): Promise<StorageOptions> {
|
protected async defaultOnDiskMemoryOptions(): Promise<StorageOptions> {
|
||||||
return {
|
return {
|
||||||
storageLocation: StorageLocation.Disk,
|
storageLocation: StorageLocation.Disk,
|
||||||
htmlStorageLocation: HtmlStorageLocation.Memory,
|
htmlStorageLocation: HtmlStorageLocation.Memory,
|
||||||
@@ -2286,7 +2307,7 @@ export class StateService implements StateServiceAbstraction {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async defaultSecureStorageOptions(): Promise<StorageOptions> {
|
protected async defaultSecureStorageOptions(): Promise<StorageOptions> {
|
||||||
return {
|
return {
|
||||||
storageLocation: StorageLocation.Disk,
|
storageLocation: StorageLocation.Disk,
|
||||||
useSecureStorage: true,
|
useSecureStorage: true,
|
||||||
@@ -2294,46 +2315,38 @@ export class StateService implements StateServiceAbstraction {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getActiveUserIdFromStorage(): Promise<string> {
|
protected async getActiveUserIdFromStorage(): Promise<string> {
|
||||||
const state = await this.storageService.get<State>("state");
|
const state = await this.storageService.get<State<TAccount>>("state");
|
||||||
return state?.activeUserId;
|
return state?.activeUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async removeAccountFromLocalStorage(
|
protected async removeAccountFromLocalStorage(
|
||||||
userId: string = this.state.activeUserId
|
userId: string = this.state.activeUserId
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const state = await this.storageService.get<State>("state", {
|
const state = await this.storageService.get<State<TAccount>>("state", {
|
||||||
htmlStorageLocation: HtmlStorageLocation.Local,
|
htmlStorageLocation: HtmlStorageLocation.Local,
|
||||||
});
|
});
|
||||||
if (state?.accounts[userId] == null) {
|
if (state?.accounts[userId] == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
state.accounts[userId] = this.resetAccount(state.accounts[userId]);
|
||||||
state.accounts[userId] = new Account({
|
|
||||||
settings: state.accounts[userId].settings,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.saveStateToStorage(state, await this.defaultOnDiskLocalOptions());
|
await this.saveStateToStorage(state, await this.defaultOnDiskLocalOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async removeAccountFromSessionStorage(
|
protected async removeAccountFromSessionStorage(
|
||||||
userId: string = this.state.activeUserId
|
userId: string = this.state.activeUserId
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const state = await this.storageService.get<State>("state", {
|
const state = await this.storageService.get<State<TAccount>>("state", {
|
||||||
htmlStorageLocation: HtmlStorageLocation.Session,
|
htmlStorageLocation: HtmlStorageLocation.Session,
|
||||||
});
|
});
|
||||||
if (state?.accounts[userId] == null) {
|
if (state?.accounts[userId] == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
state.accounts[userId] = this.resetAccount(state.accounts[userId]);
|
||||||
state.accounts[userId] = new Account({
|
|
||||||
settings: state.accounts[userId].settings,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.saveStateToStorage(state, await this.defaultOnDiskOptions());
|
await this.saveStateToStorage(state, await this.defaultOnDiskOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async removeAccountFromSecureStorage(
|
protected async removeAccountFromSecureStorage(
|
||||||
userId: string = this.state.activeUserId
|
userId: string = this.state.activeUserId
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.setCryptoMasterKeyAuto(null, { userId: userId });
|
await this.setCryptoMasterKeyAuto(null, { userId: userId });
|
||||||
@@ -2341,15 +2354,18 @@ export class StateService implements StateServiceAbstraction {
|
|||||||
await this.setCryptoMasterKeyB64(null, { userId: userId });
|
await this.setCryptoMasterKeyB64(null, { userId: userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeAccountFromMemory(userId: string = this.state.activeUserId): void {
|
protected removeAccountFromMemory(userId: string = this.state.activeUserId): void {
|
||||||
delete this.state.accounts[userId];
|
delete this.state.accounts[userId];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async saveStateToStorage(state: State, options: StorageOptions): Promise<void> {
|
protected async saveStateToStorage(
|
||||||
|
state: State<TAccount>,
|
||||||
|
options: StorageOptions
|
||||||
|
): Promise<void> {
|
||||||
await this.storageService.save("state", state, options);
|
await this.storageService.save("state", state, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async pruneInMemoryAccounts() {
|
protected async pruneInMemoryAccounts() {
|
||||||
// We preserve settings for logged out accounts, but we don't want to consider them when thinking about active account state
|
// We preserve settings for logged out accounts, but we don't want to consider them when thinking about active account state
|
||||||
for (const userId in this.state.accounts) {
|
for (const userId in this.state.accounts) {
|
||||||
if (!(await this.getIsAuthenticated({ userId: userId }))) {
|
if (!(await this.getIsAuthenticated({ userId: userId }))) {
|
||||||
@@ -2357,4 +2373,13 @@ export class StateService implements StateServiceAbstraction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// settings persist even on reset
|
||||||
|
protected resetAccount(account: TAccount) {
|
||||||
|
account.data = new AccountData();
|
||||||
|
account.keys = new AccountKeys();
|
||||||
|
account.profile = new AccountProfile();
|
||||||
|
account.tokens = new AccountTokens();
|
||||||
|
return account;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,19 +114,22 @@ export class StateMigrationService {
|
|||||||
readonly latestVersion: number = 2;
|
readonly latestVersion: number = 2;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private storageService: StorageService,
|
protected storageService: StorageService,
|
||||||
private secureStorageService: StorageService
|
protected secureStorageService: StorageService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async needsMigration(): Promise<boolean> {
|
async needsMigration(): Promise<boolean> {
|
||||||
const currentStateVersion = (await this.storageService.get<State>("state"))?.globals
|
const currentStateVersion = (
|
||||||
?.stateVersion;
|
await this.storageService.get<State<Account>>("state", {
|
||||||
|
htmlStorageLocation: HtmlStorageLocation.Local,
|
||||||
|
})
|
||||||
|
)?.globals?.stateVersion;
|
||||||
return currentStateVersion == null || currentStateVersion < this.latestVersion;
|
return currentStateVersion == null || currentStateVersion < this.latestVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
async migrate(): Promise<void> {
|
async migrate(): Promise<void> {
|
||||||
let currentStateVersion =
|
let currentStateVersion =
|
||||||
(await this.storageService.get<State>("state"))?.globals?.stateVersion ?? 1;
|
(await this.storageService.get<State<Account>>("state"))?.globals?.stateVersion ?? 1;
|
||||||
while (currentStateVersion < this.latestVersion) {
|
while (currentStateVersion < this.latestVersion) {
|
||||||
switch (currentStateVersion) {
|
switch (currentStateVersion) {
|
||||||
case 1:
|
case 1:
|
||||||
@@ -138,10 +141,10 @@ export class StateMigrationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async migrateStateFrom1To2(): Promise<void> {
|
protected async migrateStateFrom1To2(): Promise<void> {
|
||||||
const options: StorageOptions = { htmlStorageLocation: HtmlStorageLocation.Local };
|
const options: StorageOptions = { htmlStorageLocation: HtmlStorageLocation.Local };
|
||||||
const userId = await this.storageService.get<string>("userId");
|
const userId = await this.storageService.get<string>("userId");
|
||||||
const initialState: State =
|
const initialState: State<Account> =
|
||||||
userId == null
|
userId == null
|
||||||
? {
|
? {
|
||||||
globals: {
|
globals: {
|
||||||
@@ -174,6 +177,7 @@ export class StateMigrationService {
|
|||||||
v1Keys.enableBiometric,
|
v1Keys.enableBiometric,
|
||||||
options
|
options
|
||||||
),
|
),
|
||||||
|
environmentUrls: await this.storageService.get<any>(v1Keys.environmentUrls, options),
|
||||||
installedVersion: await this.storageService.get<string>(
|
installedVersion: await this.storageService.get<string>(
|
||||||
v1Keys.installedVersion,
|
v1Keys.installedVersion,
|
||||||
options
|
options
|
||||||
@@ -192,6 +196,15 @@ export class StateMigrationService {
|
|||||||
),
|
),
|
||||||
openAtLogin: await this.storageService.get<boolean>(v1Keys.openAtLogin, options),
|
openAtLogin: await this.storageService.get<boolean>(v1Keys.openAtLogin, options),
|
||||||
organizationInvitation: await this.storageService.get<string>("", options),
|
organizationInvitation: await this.storageService.get<string>("", options),
|
||||||
|
ssoCodeVerifier: await this.storageService.get<string>(
|
||||||
|
v1Keys.ssoCodeVerifier,
|
||||||
|
options
|
||||||
|
),
|
||||||
|
ssoOrganizationIdentifier: await this.storageService.get<string>(
|
||||||
|
v1Keys.ssoIdentifier,
|
||||||
|
options
|
||||||
|
),
|
||||||
|
ssoState: null,
|
||||||
rememberedEmail: await this.storageService.get<string>(
|
rememberedEmail: await this.storageService.get<string>(
|
||||||
v1Keys.rememberedEmail,
|
v1Keys.rememberedEmail,
|
||||||
options
|
options
|
||||||
@@ -327,15 +340,6 @@ export class StateMigrationService {
|
|||||||
keyHash: await this.storageService.get<string>(v1Keys.keyHash, options),
|
keyHash: await this.storageService.get<string>(v1Keys.keyHash, options),
|
||||||
lastActive: await this.storageService.get<number>(v1Keys.lastActive, options),
|
lastActive: await this.storageService.get<number>(v1Keys.lastActive, options),
|
||||||
lastSync: null,
|
lastSync: null,
|
||||||
ssoCodeVerifier: await this.storageService.get<string>(
|
|
||||||
v1Keys.ssoCodeVerifier,
|
|
||||||
options
|
|
||||||
),
|
|
||||||
ssoOrganizationIdentifier: await this.storageService.get<string>(
|
|
||||||
v1Keys.ssoIdentifier,
|
|
||||||
options
|
|
||||||
),
|
|
||||||
ssoState: null,
|
|
||||||
userId: userId,
|
userId: userId,
|
||||||
usesKeyConnector: null,
|
usesKeyConnector: null,
|
||||||
},
|
},
|
||||||
@@ -439,10 +443,6 @@ export class StateMigrationService {
|
|||||||
options
|
options
|
||||||
),
|
),
|
||||||
enableTray: await this.storageService.get<boolean>(v1Keys.enableTray, options),
|
enableTray: await this.storageService.get<boolean>(v1Keys.enableTray, options),
|
||||||
environmentUrls: await this.storageService.get<any>(
|
|
||||||
v1Keys.environmentUrls,
|
|
||||||
options
|
|
||||||
),
|
|
||||||
equivalentDomains: await this.storageService.get<any>(
|
equivalentDomains: await this.storageService.get<any>(
|
||||||
v1Keys.equivalentDomains,
|
v1Keys.equivalentDomains,
|
||||||
options
|
options
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
|||||||
async isLocked(userId?: string): Promise<boolean> {
|
async isLocked(userId?: string): Promise<boolean> {
|
||||||
const neverLock =
|
const neverLock =
|
||||||
(await this.cryptoService.hasKeyStored(KeySuffixOptions.Auto, userId)) &&
|
(await this.cryptoService.hasKeyStored(KeySuffixOptions.Auto, userId)) &&
|
||||||
!(await this.stateService.getEverBeenUnlocked({ userId: userId }));
|
(await this.stateService.getEverBeenUnlocked({ userId: userId }));
|
||||||
if (neverLock) {
|
if (neverLock) {
|
||||||
// TODO: This also _sets_ the key so when we check memory in the next line it finds a key.
|
// TODO: This also _sets_ the key so when we check memory in the next line it finds a key.
|
||||||
// We should refactor here.
|
// We should refactor here.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { StringResponse } from "./models/response/stringResponse";
|
|||||||
|
|
||||||
export abstract class BaseProgram {
|
export abstract class BaseProgram {
|
||||||
constructor(
|
constructor(
|
||||||
private stateService: StateService,
|
protected stateService: StateService,
|
||||||
private writeLn: (s: string, finalLine: boolean, error: boolean) => void
|
private writeLn: (s: string, finalLine: boolean, error: boolean) => void
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|||||||
@@ -476,6 +476,19 @@ describe("1Password 1Pif Importer", () => {
|
|||||||
|
|
||||||
// remaining fields as custom fields
|
// remaining fields as custom fields
|
||||||
expect(cipher.fields.length).toEqual(6);
|
expect(cipher.fields.length).toEqual(6);
|
||||||
|
const fields = cipher.fields;
|
||||||
|
expect(fields[0].name).toEqual("sex");
|
||||||
|
expect(fields[0].value).toEqual("male");
|
||||||
|
expect(fields[1].name).toEqual("birth date");
|
||||||
|
expect(fields[1].value).toEqual("Mon, 11 Mar 2019 12:01:00 GMT");
|
||||||
|
expect(fields[2].name).toEqual("occupation");
|
||||||
|
expect(fields[2].value).toEqual("Engineer");
|
||||||
|
expect(fields[3].name).toEqual("department");
|
||||||
|
expect(fields[3].value).toEqual("IT");
|
||||||
|
expect(fields[4].name).toEqual("job title");
|
||||||
|
expect(fields[4].value).toEqual("Developer");
|
||||||
|
expect(fields[5].name).toEqual("home");
|
||||||
|
expect(fields[5].value).toEqual("+49 333 222 111");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should create password history", async () => {
|
it("should create password history", async () => {
|
||||||
|
|||||||