[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"
|
||||
aria-hidden="true"
|
||||
></app-avatar>
|
||||
<span
|
||||
>{{ activeAccount.email
|
||||
}}<span class="sr-only"> ({{ "switchAccount" | i18n }})</span></span
|
||||
>
|
||||
<div class="active-account">
|
||||
<div>{{ activeAccount.email }}</div>
|
||||
<span>{{ activeAccount.server }}</span>
|
||||
<span class="sr-only"> ({{ "switchAccount" | i18n }})</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #noActiveAccount>
|
||||
<span>{{ "switchAccount" | i18n }}</span>
|
||||
|
@ -55,38 +56,40 @@
|
|||
aria-modal="true"
|
||||
>
|
||||
<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
|
||||
[text]="a.value.profile.name ?? a.value.profile.email"
|
||||
[id]="a.value.profile.userId"
|
||||
[text]="account.value.name ?? account.value.email"
|
||||
[id]="account.value.id"
|
||||
[size]="25"
|
||||
[circle]="true"
|
||||
[fontSize]="14"
|
||||
[dynamic]="true"
|
||||
[color]="a.value.avatarColor"
|
||||
*ngIf="a.value.profile.email != null"
|
||||
[color]="account.value.avatarColor"
|
||||
*ngIf="account.value.email != null"
|
||||
aria-hidden="true"
|
||||
></app-avatar>
|
||||
<div class="accountInfo">
|
||||
<span class="sr-only">{{ "switchAccount" | i18n }}: </span>
|
||||
<span class="email">{{ a.value.profile.email }}</span>
|
||||
<span class="server" *ngIf="a.value.serverUrl != 'bitwarden.com'">
|
||||
<span class="sr-only"> / </span>
|
||||
{{ a.value.serverUrl }}
|
||||
<span class="email" aria-hidden="true">{{ account.value.email }}</span>
|
||||
<span class="server" aria-hidden="true">
|
||||
<span class="sr-only"> / </span>{{ account.value.server }}
|
||||
</span>
|
||||
<span class="status">
|
||||
<span class="sr-only"> (</span>
|
||||
{{
|
||||
(a.value.profile.authenticationStatus === authStatus.Unlocked ? "unlocked" : "locked")
|
||||
<span class="status" aria-hidden="true"
|
||||
><span class="sr-only"> (</span
|
||||
>{{
|
||||
(account.value.authenticationStatus === authStatus.Unlocked ? "unlocked" : "locked")
|
||||
| i18n
|
||||
}}
|
||||
<span class="sr-only">)</span>
|
||||
</span>
|
||||
}}<span class="sr-only">)</span></span
|
||||
>
|
||||
</div>
|
||||
<i
|
||||
class="bwi bwi-2x text-muted"
|
||||
[ngClass]="
|
||||
a.value.profile.authenticationStatus == authStatus.Unlocked ? 'bwi-unlock' : 'bwi-lock'
|
||||
account.value.authenticationStatus === authStatus.Unlocked ? 'bwi-unlock' : 'bwi-lock'
|
||||
"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
|
@ -99,7 +102,7 @@
|
|||
<i class="bwi bwi-plus" aria-hidden="true"></i> {{ "addAccount" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="numberOfAccounts == 4">
|
||||
<ng-container *ngIf="numberOfAccounts === 4">
|
||||
<span class="accountLimitReached">{{ "accountSwitcherLimitReached" | i18n }} </span>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
|
|
@ -7,6 +7,7 @@ import { concatMap, Subject, takeUntil } from "rxjs";
|
|||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
@ -17,24 +18,12 @@ type ActiveAccount = {
|
|||
name: string;
|
||||
email: string;
|
||||
avatarColor: string;
|
||||
server: string;
|
||||
};
|
||||
|
||||
export class SwitcherAccount extends Account {
|
||||
get serverUrl() {
|
||||
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, "");
|
||||
}
|
||||
}
|
||||
type InactiveAccount = ActiveAccount & {
|
||||
authenticationStatus: AuthenticationStatus;
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "app-account-switcher",
|
||||
|
@ -61,13 +50,12 @@ export class SwitcherAccount extends Account {
|
|||
],
|
||||
})
|
||||
export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
activeAccount?: ActiveAccount;
|
||||
inactiveAccounts: { [userId: string]: InactiveAccount } = {};
|
||||
|
||||
authStatus = AuthenticationStatus;
|
||||
|
||||
isOpen = false;
|
||||
accounts: { [userId: string]: SwitcherAccount } = {};
|
||||
activeAccount?: ActiveAccount;
|
||||
serverUrl: string;
|
||||
authStatus = AuthenticationStatus;
|
||||
overlayPosition: ConnectedPosition[] = [
|
||||
{
|
||||
originX: "end",
|
||||
|
@ -77,18 +65,20 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
|||
},
|
||||
];
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
get showSwitcher() {
|
||||
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;
|
||||
}
|
||||
|
||||
get numberOfAccounts() {
|
||||
if (this.accounts == null) {
|
||||
if (this.inactiveAccounts == null) {
|
||||
this.isOpen = false;
|
||||
return 0;
|
||||
}
|
||||
return Object.keys(this.accounts).length;
|
||||
return Object.keys(this.inactiveAccounts).length;
|
||||
}
|
||||
|
||||
constructor(
|
||||
|
@ -96,26 +86,23 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
|||
private authService: AuthService,
|
||||
private messagingService: MessagingService,
|
||||
private router: Router,
|
||||
private tokenService: TokenService
|
||||
private tokenService: TokenService,
|
||||
private environmentService: EnvironmentService
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.stateService.accounts$
|
||||
.pipe(
|
||||
concatMap(async (accounts: { [userId: string]: Account }) => {
|
||||
for (const userId in accounts) {
|
||||
accounts[userId].profile.authenticationStatus = await this.authService.getAuthStatus(
|
||||
userId
|
||||
);
|
||||
}
|
||||
this.inactiveAccounts = await this.createInactiveAccounts(accounts);
|
||||
|
||||
this.accounts = await this.createSwitcherAccounts(accounts);
|
||||
try {
|
||||
this.activeAccount = {
|
||||
id: await this.tokenService.getUserId(),
|
||||
name: (await this.tokenService.getName()) ?? (await this.tokenService.getEmail()),
|
||||
email: await this.tokenService.getEmail(),
|
||||
avatarColor: await this.stateService.getAvatarColor(),
|
||||
server: await this.environmentService.getHost(),
|
||||
};
|
||||
} catch {
|
||||
this.activeAccount = undefined;
|
||||
|
@ -152,24 +139,26 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
|||
this.router.navigate(["/login"]);
|
||||
}
|
||||
|
||||
private async createSwitcherAccounts(baseAccounts: {
|
||||
private async createInactiveAccounts(baseAccounts: {
|
||||
[userId: string]: Account;
|
||||
}): Promise<{ [userId: string]: SwitcherAccount }> {
|
||||
const switcherAccounts: { [userId: string]: SwitcherAccount } = {};
|
||||
}): Promise<{ [userId: string]: InactiveAccount }> {
|
||||
const inactiveAccounts: { [userId: string]: InactiveAccount } = {};
|
||||
|
||||
for (const userId in baseAccounts) {
|
||||
if (userId == null || userId === (await this.stateService.getUserId())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// environmentUrls are stored on disk and must be retrieved separately from the in memory state offered from subscribing to accounts
|
||||
baseAccounts[userId].settings.environmentUrls = await this.stateService.getEnvironmentUrls({
|
||||
userId: userId,
|
||||
});
|
||||
switcherAccounts[userId] = new SwitcherAccount(baseAccounts[userId]);
|
||||
switcherAccounts[userId].avatarColor = await this.stateService.getAvatarColor({
|
||||
userId: userId,
|
||||
});
|
||||
inactiveAccounts[userId] = {
|
||||
id: userId,
|
||||
name: baseAccounts[userId].profile.name,
|
||||
email: baseAccounts[userId].profile.email,
|
||||
authenticationStatus: await this.authService.getAuthStatus(userId),
|
||||
avatarColor: await this.stateService.getAvatarColor({ userId: userId }),
|
||||
server: await this.environmentService.getHost(userId),
|
||||
};
|
||||
}
|
||||
return switcherAccounts;
|
||||
|
||||
return inactiveAccounts;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.header {
|
||||
-webkit-app-region: drag;
|
||||
min-height: 44px;
|
||||
max-height: 44px;
|
||||
min-height: 54px;
|
||||
max-height: 54px;
|
||||
border-bottom: 1px solid #000000;
|
||||
display: grid;
|
||||
grid-template-columns: 25% 1fr 25%;
|
||||
|
@ -102,7 +102,7 @@
|
|||
.account-switcher {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
grid-column-gap: 5px;
|
||||
grid-column-gap: 10px;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
padding: 0 10px;
|
||||
|
@ -121,11 +121,22 @@
|
|||
display: block;
|
||||
}
|
||||
|
||||
span {
|
||||
.active-account {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
|
||||
div {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
|
|
@ -23,7 +23,6 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
|||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.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 { UserKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
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.email = await this.stateService.getEmail();
|
||||
|
||||
const webVaultUrl = this.environmentService.getWebVaultUrl();
|
||||
const vaultUrl =
|
||||
webVaultUrl === "https://vault.bitwarden.com" ? "https://bitwarden.com" : webVaultUrl;
|
||||
this.webVaultHostname = Utils.getHostname(vaultUrl);
|
||||
this.webVaultHostname = await this.environmentService.getHost();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -61,6 +61,7 @@ export abstract class EnvironmentService {
|
|||
getScimUrl: () => string;
|
||||
setUrlsFromStorage: () => Promise<void>;
|
||||
setUrls: (urls: Urls) => Promise<Urls>;
|
||||
getHost: (userId?: string) => Promise<string>;
|
||||
setRegion: (region: Region) => Promise<void>;
|
||||
getUrls: () => Urls;
|
||||
isCloud: () => boolean;
|
||||
|
|
|
@ -5,7 +5,6 @@ import { OrganizationData } from "../../../admin-console/models/data/organizatio
|
|||
import { PolicyData } from "../../../admin-console/models/data/policy.data";
|
||||
import { ProviderData } from "../../../admin-console/models/data/provider.data";
|
||||
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 { EnvironmentUrls } from "../../../auth/models/domain/environment-urls";
|
||||
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
|
||||
|
@ -187,7 +186,6 @@ export class AccountKeys {
|
|||
|
||||
export class AccountProfile {
|
||||
apiKeyClientId?: string;
|
||||
authenticationStatus?: AuthenticationStatus;
|
||||
convertAccountToKeyConnector?: boolean;
|
||||
name?: string;
|
||||
email?: string;
|
||||
|
|
|
@ -4,9 +4,11 @@ import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
|
|||
import {
|
||||
EnvironmentService as EnvironmentServiceAbstraction,
|
||||
Region,
|
||||
RegionDomain,
|
||||
Urls,
|
||||
} from "../abstractions/environment.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { Utils } from "../misc/utils";
|
||||
|
||||
export class EnvironmentService implements EnvironmentServiceAbstraction {
|
||||
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) {
|
||||
this.selectedRegion = region;
|
||||
await this.stateService.setRegion(region);
|
||||
|
|
Loading…
Reference in New Issue