1
0
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:
rr-bw 2023-11-15 11:02:11 -08:00 committed by GitHub
parent cd19fc5133
commit 90bad00cb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 98 additions and 76 deletions

View File

@ -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">&nbsp;({{ "switchAccount" | i18n }})</span></span <span>{{ activeAccount.server }}</span>
> <span class="sr-only">&nbsp;({{ "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 }}:&nbsp;</span> <span class="sr-only">{{ "switchAccount" | i18n }}:&nbsp;</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">&nbsp;(</span> ><span class="sr-only">&nbsp;(</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>

View File

@ -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;
} }
} }

View File

@ -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 {

View File

@ -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);
} }
/** /**

View File

@ -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;

View File

@ -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;

View File

@ -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);