mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-08 00:01:28 +01:00
[PM-3000] Add Environment URLs to Account Switcher (#5978)
* add server url to account switcher tab * add serverUrl to SwitcherAccount(s) * refactor serverUrl getter * cleanup urls * adjust styling * remove SwitcherAccount class * remove authenticationStatus from AccountProfile * rename to inactiveAccounts for clarity * move business logic to environmentService * use tokenService instead of stateService * cleanup type and comments * remove unused property * replace magic strings * remove unused function * minor refactoring * refactor to use environmentService insead of getServerConfig * use Utils.getHost() instead of Utils.getDomain() * create getHost() method * remove comment * get base url as fallback * resolve eslint error * Update apps/desktop/src/app/layout/account-switcher.component.html Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com> --------- Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>
This commit is contained in:
parent
cd19fc5133
commit
90bad00cb5
@ -20,10 +20,11 @@
|
|||||||
*ngIf="activeAccount.email != null"
|
*ngIf="activeAccount.email != null"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></app-avatar>
|
></app-avatar>
|
||||||
<span
|
<div class="active-account">
|
||||||
>{{ activeAccount.email
|
<div>{{ activeAccount.email }}</div>
|
||||||
}}<span class="sr-only"> ({{ "switchAccount" | i18n }})</span></span
|
<span>{{ activeAccount.server }}</span>
|
||||||
>
|
<span class="sr-only"> ({{ "switchAccount" | i18n }})</span>
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #noActiveAccount>
|
<ng-template #noActiveAccount>
|
||||||
<span>{{ "switchAccount" | i18n }}</span>
|
<span>{{ "switchAccount" | i18n }}</span>
|
||||||
@ -55,38 +56,40 @@
|
|||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
>
|
>
|
||||||
<div class="accounts" *ngIf="numberOfAccounts > 0">
|
<div class="accounts" *ngIf="numberOfAccounts > 0">
|
||||||
<button *ngFor="let a of accounts | keyvalue" class="account" (click)="switch(a.key)">
|
<button
|
||||||
|
*ngFor="let account of inactiveAccounts | keyvalue"
|
||||||
|
class="account"
|
||||||
|
(click)="switch(account.key)"
|
||||||
|
>
|
||||||
<app-avatar
|
<app-avatar
|
||||||
[text]="a.value.profile.name ?? a.value.profile.email"
|
[text]="account.value.name ?? account.value.email"
|
||||||
[id]="a.value.profile.userId"
|
[id]="account.value.id"
|
||||||
[size]="25"
|
[size]="25"
|
||||||
[circle]="true"
|
[circle]="true"
|
||||||
[fontSize]="14"
|
[fontSize]="14"
|
||||||
[dynamic]="true"
|
[dynamic]="true"
|
||||||
[color]="a.value.avatarColor"
|
[color]="account.value.avatarColor"
|
||||||
*ngIf="a.value.profile.email != null"
|
*ngIf="account.value.email != null"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></app-avatar>
|
></app-avatar>
|
||||||
<div class="accountInfo">
|
<div class="accountInfo">
|
||||||
<span class="sr-only">{{ "switchAccount" | i18n }}: </span>
|
<span class="sr-only">{{ "switchAccount" | i18n }}: </span>
|
||||||
<span class="email">{{ a.value.profile.email }}</span>
|
<span class="email" aria-hidden="true">{{ account.value.email }}</span>
|
||||||
<span class="server" *ngIf="a.value.serverUrl != 'bitwarden.com'">
|
<span class="server" aria-hidden="true">
|
||||||
<span class="sr-only"> / </span>
|
<span class="sr-only"> / </span>{{ account.value.server }}
|
||||||
{{ a.value.serverUrl }}
|
|
||||||
</span>
|
</span>
|
||||||
<span class="status">
|
<span class="status" aria-hidden="true"
|
||||||
<span class="sr-only"> (</span>
|
><span class="sr-only"> (</span
|
||||||
{{
|
>{{
|
||||||
(a.value.profile.authenticationStatus === authStatus.Unlocked ? "unlocked" : "locked")
|
(account.value.authenticationStatus === authStatus.Unlocked ? "unlocked" : "locked")
|
||||||
| i18n
|
| i18n
|
||||||
}}
|
}}<span class="sr-only">)</span></span
|
||||||
<span class="sr-only">)</span>
|
>
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<i
|
<i
|
||||||
class="bwi bwi-2x text-muted"
|
class="bwi bwi-2x text-muted"
|
||||||
[ngClass]="
|
[ngClass]="
|
||||||
a.value.profile.authenticationStatus == authStatus.Unlocked ? 'bwi-unlock' : 'bwi-lock'
|
account.value.authenticationStatus === authStatus.Unlocked ? 'bwi-unlock' : 'bwi-lock'
|
||||||
"
|
"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></i>
|
></i>
|
||||||
@ -99,7 +102,7 @@
|
|||||||
<i class="bwi bwi-plus" aria-hidden="true"></i> {{ "addAccount" | i18n }}
|
<i class="bwi bwi-plus" aria-hidden="true"></i> {{ "addAccount" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="numberOfAccounts == 4">
|
<ng-container *ngIf="numberOfAccounts === 4">
|
||||||
<span class="accountLimitReached">{{ "accountSwitcherLimitReached" | i18n }} </span>
|
<span class="accountLimitReached">{{ "accountSwitcherLimitReached" | i18n }} </span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -7,6 +7,7 @@ import { concatMap, Subject, takeUntil } from "rxjs";
|
|||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
@ -17,24 +18,12 @@ type ActiveAccount = {
|
|||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
avatarColor: string;
|
avatarColor: string;
|
||||||
|
server: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class SwitcherAccount extends Account {
|
type InactiveAccount = ActiveAccount & {
|
||||||
get serverUrl() {
|
authenticationStatus: AuthenticationStatus;
|
||||||
return this.removeWebProtocolFromString(
|
};
|
||||||
this.settings?.environmentUrls?.base ??
|
|
||||||
this.settings?.environmentUrls.api ??
|
|
||||||
"https://bitwarden.com"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
avatarColor: string;
|
|
||||||
|
|
||||||
private removeWebProtocolFromString(urlString: string) {
|
|
||||||
const regex = /http(s)?(:)?(\/\/)?|(\/\/)?(www\.)?/g;
|
|
||||||
return urlString.replace(regex, "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-account-switcher",
|
selector: "app-account-switcher",
|
||||||
@ -61,13 +50,12 @@ export class SwitcherAccount extends Account {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||||
private destroy$ = new Subject<void>();
|
activeAccount?: ActiveAccount;
|
||||||
|
inactiveAccounts: { [userId: string]: InactiveAccount } = {};
|
||||||
|
|
||||||
|
authStatus = AuthenticationStatus;
|
||||||
|
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
accounts: { [userId: string]: SwitcherAccount } = {};
|
|
||||||
activeAccount?: ActiveAccount;
|
|
||||||
serverUrl: string;
|
|
||||||
authStatus = AuthenticationStatus;
|
|
||||||
overlayPosition: ConnectedPosition[] = [
|
overlayPosition: ConnectedPosition[] = [
|
||||||
{
|
{
|
||||||
originX: "end",
|
originX: "end",
|
||||||
@ -77,18 +65,20 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
get showSwitcher() {
|
get showSwitcher() {
|
||||||
const userIsInAVault = !Utils.isNullOrWhitespace(this.activeAccount?.email);
|
const userIsInAVault = !Utils.isNullOrWhitespace(this.activeAccount?.email);
|
||||||
const userIsAddingAnAdditionalAccount = Object.keys(this.accounts).length > 0;
|
const userIsAddingAnAdditionalAccount = Object.keys(this.inactiveAccounts).length > 0;
|
||||||
return userIsInAVault || userIsAddingAnAdditionalAccount;
|
return userIsInAVault || userIsAddingAnAdditionalAccount;
|
||||||
}
|
}
|
||||||
|
|
||||||
get numberOfAccounts() {
|
get numberOfAccounts() {
|
||||||
if (this.accounts == null) {
|
if (this.inactiveAccounts == null) {
|
||||||
this.isOpen = false;
|
this.isOpen = false;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return Object.keys(this.accounts).length;
|
return Object.keys(this.inactiveAccounts).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -96,26 +86,23 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
|||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private tokenService: TokenService
|
private tokenService: TokenService,
|
||||||
|
private environmentService: EnvironmentService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
this.stateService.accounts$
|
this.stateService.accounts$
|
||||||
.pipe(
|
.pipe(
|
||||||
concatMap(async (accounts: { [userId: string]: Account }) => {
|
concatMap(async (accounts: { [userId: string]: Account }) => {
|
||||||
for (const userId in accounts) {
|
this.inactiveAccounts = await this.createInactiveAccounts(accounts);
|
||||||
accounts[userId].profile.authenticationStatus = await this.authService.getAuthStatus(
|
|
||||||
userId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.accounts = await this.createSwitcherAccounts(accounts);
|
|
||||||
try {
|
try {
|
||||||
this.activeAccount = {
|
this.activeAccount = {
|
||||||
id: await this.tokenService.getUserId(),
|
id: await this.tokenService.getUserId(),
|
||||||
name: (await this.tokenService.getName()) ?? (await this.tokenService.getEmail()),
|
name: (await this.tokenService.getName()) ?? (await this.tokenService.getEmail()),
|
||||||
email: await this.tokenService.getEmail(),
|
email: await this.tokenService.getEmail(),
|
||||||
avatarColor: await this.stateService.getAvatarColor(),
|
avatarColor: await this.stateService.getAvatarColor(),
|
||||||
|
server: await this.environmentService.getHost(),
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
this.activeAccount = undefined;
|
this.activeAccount = undefined;
|
||||||
@ -152,24 +139,26 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
|||||||
this.router.navigate(["/login"]);
|
this.router.navigate(["/login"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createSwitcherAccounts(baseAccounts: {
|
private async createInactiveAccounts(baseAccounts: {
|
||||||
[userId: string]: Account;
|
[userId: string]: Account;
|
||||||
}): Promise<{ [userId: string]: SwitcherAccount }> {
|
}): Promise<{ [userId: string]: InactiveAccount }> {
|
||||||
const switcherAccounts: { [userId: string]: SwitcherAccount } = {};
|
const inactiveAccounts: { [userId: string]: InactiveAccount } = {};
|
||||||
|
|
||||||
for (const userId in baseAccounts) {
|
for (const userId in baseAccounts) {
|
||||||
if (userId == null || userId === (await this.stateService.getUserId())) {
|
if (userId == null || userId === (await this.stateService.getUserId())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// environmentUrls are stored on disk and must be retrieved separately from the in memory state offered from subscribing to accounts
|
inactiveAccounts[userId] = {
|
||||||
baseAccounts[userId].settings.environmentUrls = await this.stateService.getEnvironmentUrls({
|
id: userId,
|
||||||
userId: userId,
|
name: baseAccounts[userId].profile.name,
|
||||||
});
|
email: baseAccounts[userId].profile.email,
|
||||||
switcherAccounts[userId] = new SwitcherAccount(baseAccounts[userId]);
|
authenticationStatus: await this.authService.getAuthStatus(userId),
|
||||||
switcherAccounts[userId].avatarColor = await this.stateService.getAvatarColor({
|
avatarColor: await this.stateService.getAvatarColor({ userId: userId }),
|
||||||
userId: userId,
|
server: await this.environmentService.getHost(userId),
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
return switcherAccounts;
|
|
||||||
|
return inactiveAccounts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
.header {
|
.header {
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
min-height: 44px;
|
min-height: 54px;
|
||||||
max-height: 44px;
|
max-height: 54px;
|
||||||
border-bottom: 1px solid #000000;
|
border-bottom: 1px solid #000000;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 25% 1fr 25%;
|
grid-template-columns: 25% 1fr 25%;
|
||||||
@ -102,7 +102,7 @@
|
|||||||
.account-switcher {
|
.account-switcher {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr auto;
|
grid-template-columns: auto 1fr auto;
|
||||||
grid-column-gap: 5px;
|
grid-column-gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
@ -121,11 +121,22 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
.active-account {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
div {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: $font-size-small;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -23,7 +23,6 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
|||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
|
||||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
import { UserKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { UserKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import { PinLockType } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
import { PinLockType } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
||||||
@ -377,10 +376,7 @@ export class LockComponent implements OnInit, OnDestroy {
|
|||||||
this.biometricText = await this.stateService.getBiometricText();
|
this.biometricText = await this.stateService.getBiometricText();
|
||||||
this.email = await this.stateService.getEmail();
|
this.email = await this.stateService.getEmail();
|
||||||
|
|
||||||
const webVaultUrl = this.environmentService.getWebVaultUrl();
|
this.webVaultHostname = await this.environmentService.getHost();
|
||||||
const vaultUrl =
|
|
||||||
webVaultUrl === "https://vault.bitwarden.com" ? "https://bitwarden.com" : webVaultUrl;
|
|
||||||
this.webVaultHostname = Utils.getHostname(vaultUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -61,6 +61,7 @@ export abstract class EnvironmentService {
|
|||||||
getScimUrl: () => string;
|
getScimUrl: () => string;
|
||||||
setUrlsFromStorage: () => Promise<void>;
|
setUrlsFromStorage: () => Promise<void>;
|
||||||
setUrls: (urls: Urls) => Promise<Urls>;
|
setUrls: (urls: Urls) => Promise<Urls>;
|
||||||
|
getHost: (userId?: string) => Promise<string>;
|
||||||
setRegion: (region: Region) => Promise<void>;
|
setRegion: (region: Region) => Promise<void>;
|
||||||
getUrls: () => Urls;
|
getUrls: () => Urls;
|
||||||
isCloud: () => boolean;
|
isCloud: () => boolean;
|
||||||
|
@ -5,7 +5,6 @@ import { OrganizationData } from "../../../admin-console/models/data/organizatio
|
|||||||
import { PolicyData } from "../../../admin-console/models/data/policy.data";
|
import { PolicyData } from "../../../admin-console/models/data/policy.data";
|
||||||
import { ProviderData } from "../../../admin-console/models/data/provider.data";
|
import { ProviderData } from "../../../admin-console/models/data/provider.data";
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
|
||||||
import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable";
|
import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable";
|
||||||
import { EnvironmentUrls } from "../../../auth/models/domain/environment-urls";
|
import { EnvironmentUrls } from "../../../auth/models/domain/environment-urls";
|
||||||
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
|
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
|
||||||
@ -187,7 +186,6 @@ export class AccountKeys {
|
|||||||
|
|
||||||
export class AccountProfile {
|
export class AccountProfile {
|
||||||
apiKeyClientId?: string;
|
apiKeyClientId?: string;
|
||||||
authenticationStatus?: AuthenticationStatus;
|
|
||||||
convertAccountToKeyConnector?: boolean;
|
convertAccountToKeyConnector?: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
|
@ -4,9 +4,11 @@ import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
|
|||||||
import {
|
import {
|
||||||
EnvironmentService as EnvironmentServiceAbstraction,
|
EnvironmentService as EnvironmentServiceAbstraction,
|
||||||
Region,
|
Region,
|
||||||
|
RegionDomain,
|
||||||
Urls,
|
Urls,
|
||||||
} from "../abstractions/environment.service";
|
} from "../abstractions/environment.service";
|
||||||
import { StateService } from "../abstractions/state.service";
|
import { StateService } from "../abstractions/state.service";
|
||||||
|
import { Utils } from "../misc/utils";
|
||||||
|
|
||||||
export class EnvironmentService implements EnvironmentServiceAbstraction {
|
export class EnvironmentService implements EnvironmentServiceAbstraction {
|
||||||
private readonly urlsSubject = new ReplaySubject<void>(1);
|
private readonly urlsSubject = new ReplaySubject<void>(1);
|
||||||
@ -283,6 +285,28 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getHost(userId?: string) {
|
||||||
|
const region = await this.getRegion(userId ? userId : null);
|
||||||
|
|
||||||
|
switch (region) {
|
||||||
|
case Region.US:
|
||||||
|
return RegionDomain.US;
|
||||||
|
case Region.EU:
|
||||||
|
return RegionDomain.EU;
|
||||||
|
default: {
|
||||||
|
// Environment is self-hosted
|
||||||
|
const envUrls = await this.stateService.getEnvironmentUrls(
|
||||||
|
userId ? { userId: userId } : null
|
||||||
|
);
|
||||||
|
return Utils.getHost(envUrls.webVault || envUrls.base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getRegion(userId?: string) {
|
||||||
|
return this.stateService.getRegion(userId ? { userId: userId } : null);
|
||||||
|
}
|
||||||
|
|
||||||
async setRegion(region: Region) {
|
async setRegion(region: Region) {
|
||||||
this.selectedRegion = region;
|
this.selectedRegion = region;
|
||||||
await this.stateService.setRegion(region);
|
await this.stateService.setRegion(region);
|
||||||
|
Loading…
Reference in New Issue
Block a user