mirror of
https://github.com/bitwarden/browser
synced 2026-01-09 20:13:42 +00:00
Merge branch 'master' into AC-1423-update-organization-subscription-cloud-page
This commit is contained in:
17
apps/web/src/app/admin-console/icons/devices.ts
Normal file
17
apps/web/src/app/admin-console/icons/devices.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const Devices = svgIcon`
|
||||
<svg width="201" height="201" viewBox="0 0 201 201" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity=".49">
|
||||
<path class="tw-fill-secondary-500" fill-rule="evenodd" clip-rule="evenodd" d="M34.3628 82.0889H10.3628C7.04908 82.0889 4.36279 84.7752 4.36279 88.0889V148.089C4.36279 151.403 7.04909 154.089 10.3628 154.089H34.3628C37.6765 154.089 40.3628 151.403 40.3628 148.089V88.0889C40.3628 84.7752 37.6765 82.0889 34.3628 82.0889ZM10.3628 78.0889C4.83995 78.0889 0.362793 82.566 0.362793 88.0889V148.089C0.362793 153.612 4.83995 158.089 10.3628 158.089H34.3628C39.8856 158.089 44.3628 153.612 44.3628 148.089V88.0889C44.3628 82.566 39.8856 78.0889 34.3628 78.0889H10.3628Z" />
|
||||
<path class="tw-fill-secondary-500" fill-rule="evenodd" clip-rule="evenodd" d="M20.7329 86.8979C20.7329 86.3457 21.1806 85.8979 21.7329 85.8979H22.975C23.5273 85.8979 23.975 86.3457 23.975 86.8979C23.975 87.4502 23.5273 87.8979 22.975 87.8979H21.7329C21.1806 87.8979 20.7329 87.4502 20.7329 86.8979Z" />
|
||||
<path class="tw-fill-secondary-500" fill-rule="evenodd" clip-rule="evenodd" d="M68.3628 159.089C68.3628 158.537 68.8105 158.089 69.3628 158.089H127.363C127.915 158.089 128.363 158.537 128.363 159.089C128.363 159.641 127.915 160.089 127.363 160.089H69.3628C68.8105 160.089 68.3628 159.641 68.3628 159.089Z" />
|
||||
<path class="tw-fill-secondary-500" fill-rule="evenodd" clip-rule="evenodd" d="M88.103 159.089V141.325H90.103V159.089H88.103Z" />
|
||||
<path class="tw-fill-secondary-500" fill-rule="evenodd" clip-rule="evenodd" d="M108.073 159.089V141.325H110.073V159.089H108.073Z" />
|
||||
<path class="tw-fill-secondary-500" fill-rule="evenodd" clip-rule="evenodd" d="M27.3628 64.0889C27.3628 56.3569 33.6308 50.0889 41.3628 50.0889H157.363C165.095 50.0889 171.363 56.3569 171.363 64.0889V70.0889H167.363V64.0889C167.363 58.566 162.886 54.0889 157.363 54.0889H41.3628C35.8399 54.0889 31.3628 58.566 31.3628 64.0889V80.0889H27.3628V64.0889ZM42.3628 138.089H127.363V142.089H42.3628V138.089Z" />
|
||||
<path class="tw-fill-secondary-500" fill-rule="evenodd" clip-rule="evenodd" d="M35.3628 65.0889C35.3628 61.2229 38.4968 58.0889 42.3628 58.0889H156.363C160.229 58.0889 163.363 61.2229 163.363 65.0889V70.0889H161.363V65.0889C161.363 62.3274 159.124 60.0889 156.363 60.0889H42.3628C39.6014 60.0889 37.3628 62.3274 37.3628 65.0889V80.0889H35.3628V65.0889ZM42.3628 132.089H127.363V134.089H42.3628V132.089Z" />
|
||||
<path class="tw-fill-secondary-500" fill-rule="evenodd" clip-rule="evenodd" d="M125.363 78.0889C125.363 72.566 129.84 68.0889 135.363 68.0889H188.363C193.886 68.0889 198.363 72.566 198.363 78.0889V158.089C198.363 163.612 193.886 168.089 188.363 168.089H135.363C129.84 168.089 125.363 163.612 125.363 158.089V78.0889ZM135.363 72.0889C132.049 72.0889 129.363 74.7752 129.363 78.0889V158.089C129.363 161.403 132.049 164.089 135.363 164.089H188.363C191.676 164.089 194.363 161.403 194.363 158.089V78.0889C194.363 74.7752 191.677 72.0889 188.363 72.0889H135.363Z" />
|
||||
<path class="tw-fill-secondary-500" d="M164.363 159.089C164.363 160.193 163.467 161.089 162.363 161.089C161.258 161.089 160.363 160.193 160.363 159.089C160.363 157.984 161.258 157.089 162.363 157.089C163.467 157.089 164.363 157.984 164.363 159.089Z" />
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
1
apps/web/src/app/admin-console/icons/index.ts
Normal file
1
apps/web/src/app/admin-console/icons/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./devices";
|
||||
@@ -83,6 +83,7 @@ export class UserAdminService {
|
||||
}));
|
||||
view.groups = u.groups;
|
||||
view.accessSecretsManager = u.accessSecretsManager;
|
||||
view.hasMasterPassword = u.hasMasterPassword;
|
||||
|
||||
return view;
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ export class OrganizationUserAdminView {
|
||||
accessAll: boolean;
|
||||
permissions: PermissionsApi;
|
||||
resetPasswordEnrolled: boolean;
|
||||
hasMasterPassword: boolean;
|
||||
|
||||
collections: CollectionAccessSelectionView[] = [];
|
||||
groups: string[] = [];
|
||||
|
||||
@@ -20,6 +20,7 @@ export class OrganizationUserView {
|
||||
avatarColor: string;
|
||||
twoFactorEnabled: boolean;
|
||||
usesKeyConnector: boolean;
|
||||
hasMasterPassword: boolean;
|
||||
|
||||
collections: CollectionAccessSelectionView[] = [];
|
||||
groups: string[] = [];
|
||||
|
||||
@@ -1,118 +1,89 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="eventLogsTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="eventLogsTitle">
|
||||
{{ "eventLogs" | i18n }}
|
||||
<small class="text-muted" *ngIf="name">{{ name }}</small>
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="!loaded">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="loaded">
|
||||
<div class="d-flex">
|
||||
<div class="form-inline">
|
||||
<label class="sr-only" for="start">{{ "startDate" | i18n }}</label>
|
||||
<bit-dialog dialogSize="large" [loading]="loading">
|
||||
<span bitDialogTitle>
|
||||
{{ "eventLogs" | i18n }}
|
||||
<small class="tw-text-muted" *ngIf="name">{{ name }}</small>
|
||||
</span>
|
||||
<div bitDialogContent>
|
||||
<form [formGroup]="filterFormGroup" [bitSubmit]="refreshEvents">
|
||||
<div class="tw-flex tw-items-center tw-space-x-2">
|
||||
<div>
|
||||
<label class="tw-sr-only" for="start">{{ "startDate" | i18n }}</label>
|
||||
<span>
|
||||
<input
|
||||
bitInput
|
||||
type="datetime-local"
|
||||
class="form-control form-control-sm"
|
||||
id="start"
|
||||
placeholder="{{ 'startDate' | i18n }}"
|
||||
[(ngModel)]="start"
|
||||
placeholder="YYYY-MM-DDTHH:MM"
|
||||
formControlName="start"
|
||||
/>
|
||||
<span class="mx-2">-</span>
|
||||
<label class="sr-only" for="end">{{ "endDate" | i18n }}</label>
|
||||
</span>
|
||||
</div>
|
||||
<span class="tw-mx-2">-</span>
|
||||
<div>
|
||||
<label class="tw-sr-only" for="end">{{ "endDate" | i18n }}</label>
|
||||
<span>
|
||||
<input
|
||||
bitInput
|
||||
type="datetime-local"
|
||||
class="form-control form-control-sm"
|
||||
id="end"
|
||||
placeholder="{{ 'endDate' | i18n }}"
|
||||
[(ngModel)]="end"
|
||||
placeholder="YYYY-MM-DDTHH:MM"
|
||||
formControlName="end"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
#refreshBtn
|
||||
[appApiAction]="refreshPromise"
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-primary ml-3"
|
||||
(click)="loadEvents(true)"
|
||||
[disabled]="loaded && $any(refreshBtn).loading"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<button type="submit" bitButton buttonType="primary" bitFormButton>
|
||||
<i class="bwi bwi-refresh bwi-fw" aria-hidden="true"></i>
|
||||
{{ "refresh" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<hr />
|
||||
<div *ngIf="!dataSource.data || !dataSource.data.length">
|
||||
{{ "noEventsInList" | i18n }}
|
||||
</div>
|
||||
<bit-table [dataSource]="dataSource" *ngIf="dataSource?.data?.length">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell>{{ "timestamp" | i18n }}</th>
|
||||
<th bitCell>
|
||||
<span class="tw-sr-only">{{ "client" | i18n }}</span>
|
||||
</th>
|
||||
<th bitCell *ngIf="showUser">{{ "member" | i18n }}</th>
|
||||
<th bitCell>{{ "event" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *ngFor="let r of rows$ | async">
|
||||
<td bitCell>{{ r.date | date : "medium" }}</td>
|
||||
<td bitCell>
|
||||
<i
|
||||
class="bwi bwi-refresh bwi-fw"
|
||||
[ngClass]="{ 'bwi-spin': loaded && $any(refreshBtn).loading }"
|
||||
class="tw-text-muted bwi bwi-lg {{ r.appIcon }}"
|
||||
title="{{ r.appName }}, {{ r.ip }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
{{ "refresh" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<hr />
|
||||
<div *ngIf="!events || !events.length">
|
||||
{{ "noEventsInList" | i18n }}
|
||||
</div>
|
||||
<table class="table table-hover mb-0" *ngIf="events && events.length">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border-top-0" width="210">{{ "timestamp" | i18n }}</th>
|
||||
<th class="border-top-0" width="40">
|
||||
<span class="sr-only">{{ "device" | i18n }}</span>
|
||||
</th>
|
||||
<th class="border-top-0" width="150" *ngIf="showUser">{{ "user" | i18n }}</th>
|
||||
<th class="border-top-0">{{ "event" | i18n }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let e of events">
|
||||
<td>{{ e.date | date : "medium" }}</td>
|
||||
<td>
|
||||
<i
|
||||
class="text-muted bwi bwi-lg {{ e.appIcon }}"
|
||||
title="{{ e.appName }}, {{ e.ip }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ e.appName }}, {{ e.ip }}</span>
|
||||
</td>
|
||||
<td *ngIf="showUser">
|
||||
<span appA11yTitle="{{ e.userEmail }}">{{ e.userName }}</span>
|
||||
</td>
|
||||
<td [innerHTML]="e.message"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button
|
||||
#moreBtn
|
||||
[appApiAction]="morePromise"
|
||||
type="button"
|
||||
class="btn btn-block btn-link btn-submit"
|
||||
(click)="loadEvents(false)"
|
||||
[disabled]="loaded && $any(moreBtn).loading"
|
||||
*ngIf="continuationToken"
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "loadMore" | i18n }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<span class="tw-sr-only">{{ r.appName }}, {{ r.ip }}</span>
|
||||
</td>
|
||||
<td bitCell *ngIf="showUser">
|
||||
<span appA11yTitle="{{ r.userEmail }}">{{ r.userName }}</span>
|
||||
</td>
|
||||
<td bitCell [innerHTML]="r.message"></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
<button
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
block
|
||||
[bitAction]="loadMoreEvents"
|
||||
type="button"
|
||||
*ngIf="continuationToken"
|
||||
>
|
||||
{{ "loadMore" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton buttonType="secondary" type="button" bitDialogClose>
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
|
||||
@@ -1,78 +1,114 @@
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnInit } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
|
||||
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
|
||||
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
|
||||
import { EventResponse } from "@bitwarden/common/models/response/event.response";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { EventView } from "@bitwarden/common/models/view/event.view";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { TableDataSource } from "@bitwarden/components";
|
||||
|
||||
import { EventService } from "../../../core";
|
||||
import { SharedModule } from "../../../shared";
|
||||
|
||||
export interface EntityEventsDialogParams {
|
||||
entity: "user" | "cipher";
|
||||
entityId: string;
|
||||
|
||||
organizationId?: string;
|
||||
providerId?: string;
|
||||
showUser?: boolean;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-entity-events",
|
||||
imports: [SharedModule],
|
||||
templateUrl: "entity-events.component.html",
|
||||
standalone: true,
|
||||
})
|
||||
export class EntityEventsComponent implements OnInit {
|
||||
@Input() name: string;
|
||||
@Input() entity: "user" | "cipher";
|
||||
@Input() entityId: string;
|
||||
@Input() organizationId: string;
|
||||
@Input() providerId: string;
|
||||
@Input() showUser = false;
|
||||
|
||||
loading = true;
|
||||
loaded = false;
|
||||
events: any[];
|
||||
start: string;
|
||||
end: string;
|
||||
continuationToken: string;
|
||||
refreshPromise: Promise<any>;
|
||||
morePromise: Promise<any>;
|
||||
protected dataSource = new TableDataSource<EventView>();
|
||||
protected filterFormGroup = this.formBuilder.group({
|
||||
start: [""],
|
||||
end: [""],
|
||||
});
|
||||
|
||||
private orgUsersUserIdMap = new Map<string, any>();
|
||||
private orgUsersIdMap = new Map<string, any>();
|
||||
|
||||
get name() {
|
||||
return this.params.name;
|
||||
}
|
||||
|
||||
get showUser() {
|
||||
return this.params.showUser ?? false;
|
||||
}
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) private params: EntityEventsDialogParams,
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private eventService: EventService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private userNamePipe: UserNamePipe,
|
||||
private logService: LogService,
|
||||
private organizationUserService: OrganizationUserService
|
||||
private organizationUserService: OrganizationUserService,
|
||||
private formBuilder: FormBuilder,
|
||||
private validationService: ValidationService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
const defaultDates = this.eventService.getDefaultDateFilters();
|
||||
this.start = defaultDates[0];
|
||||
this.end = defaultDates[1];
|
||||
this.filterFormGroup.setValue({
|
||||
start: defaultDates[0],
|
||||
end: defaultDates[1],
|
||||
});
|
||||
await this.load();
|
||||
}
|
||||
|
||||
async load() {
|
||||
if (this.showUser) {
|
||||
const response = await this.organizationUserService.getAllUsers(this.organizationId);
|
||||
response.data.forEach((u) => {
|
||||
const name = this.userNamePipe.transform(u);
|
||||
this.orgUsersIdMap.set(u.id, { name: name, email: u.email });
|
||||
this.orgUsersUserIdMap.set(u.userId, { name: name, email: u.email });
|
||||
});
|
||||
try {
|
||||
if (this.showUser) {
|
||||
const response = await this.organizationUserService.getAllUsers(this.params.organizationId);
|
||||
response.data.forEach((u) => {
|
||||
const name = this.userNamePipe.transform(u);
|
||||
this.orgUsersIdMap.set(u.id, { name: name, email: u.email });
|
||||
this.orgUsersUserIdMap.set(u.userId, { name: name, email: u.email });
|
||||
});
|
||||
}
|
||||
await this.loadEvents(true);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
await this.loadEvents(true);
|
||||
this.loaded = true;
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async loadEvents(clearExisting: boolean) {
|
||||
if (this.refreshPromise != null || this.morePromise != null) {
|
||||
return;
|
||||
}
|
||||
loadMoreEvents = async () => {
|
||||
await this.loadEvents(false);
|
||||
};
|
||||
|
||||
refreshEvents = async () => {
|
||||
await this.loadEvents(true);
|
||||
};
|
||||
|
||||
private async loadEvents(clearExisting: boolean) {
|
||||
let dates: string[] = null;
|
||||
try {
|
||||
dates = this.eventService.formatDateFilters(this.start, this.end);
|
||||
dates = this.eventService.formatDateFilters(
|
||||
this.filterFormGroup.value.start,
|
||||
this.filterFormGroup.value.end
|
||||
);
|
||||
} catch (e) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
@@ -82,46 +118,34 @@ export class EntityEventsComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
let response: ListResponse<EventResponse>;
|
||||
try {
|
||||
let promise: Promise<any>;
|
||||
if (this.entity === "user" && this.providerId) {
|
||||
promise = this.apiService.getEventsProviderUser(
|
||||
this.providerId,
|
||||
this.entityId,
|
||||
dates[0],
|
||||
dates[1],
|
||||
clearExisting ? null : this.continuationToken
|
||||
);
|
||||
} else if (this.entity === "user") {
|
||||
promise = this.apiService.getEventsOrganizationUser(
|
||||
this.organizationId,
|
||||
this.entityId,
|
||||
dates[0],
|
||||
dates[1],
|
||||
clearExisting ? null : this.continuationToken
|
||||
);
|
||||
} else {
|
||||
promise = this.apiService.getEventsCipher(
|
||||
this.entityId,
|
||||
dates[0],
|
||||
dates[1],
|
||||
clearExisting ? null : this.continuationToken
|
||||
);
|
||||
}
|
||||
if (clearExisting) {
|
||||
this.refreshPromise = promise;
|
||||
} else {
|
||||
this.morePromise = promise;
|
||||
}
|
||||
response = await promise;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
if (this.params.entity === "user" && this.params.providerId) {
|
||||
response = await this.apiService.getEventsProviderUser(
|
||||
this.params.providerId,
|
||||
this.params.entityId,
|
||||
dates[0],
|
||||
dates[1],
|
||||
clearExisting ? null : this.continuationToken
|
||||
);
|
||||
} else if (this.params.entity === "user") {
|
||||
response = await this.apiService.getEventsOrganizationUser(
|
||||
this.params.organizationId,
|
||||
this.params.entityId,
|
||||
dates[0],
|
||||
dates[1],
|
||||
clearExisting ? null : this.continuationToken
|
||||
);
|
||||
} else {
|
||||
response = await this.apiService.getEventsCipher(
|
||||
this.params.entityId,
|
||||
dates[0],
|
||||
dates[1],
|
||||
clearExisting ? null : this.continuationToken
|
||||
);
|
||||
}
|
||||
|
||||
this.continuationToken = response.continuationToken;
|
||||
const events = await Promise.all(
|
||||
const events: EventView[] = await Promise.all(
|
||||
response.data.map(async (r) => {
|
||||
const userId = r.actingUserId == null ? r.userId : r.actingUserId;
|
||||
const eventInfo = await this.eventService.getEventInfo(r);
|
||||
@@ -129,8 +153,10 @@ export class EntityEventsComponent implements OnInit {
|
||||
this.showUser && userId != null && this.orgUsersUserIdMap.has(userId)
|
||||
? this.orgUsersUserIdMap.get(userId)
|
||||
: null;
|
||||
return {
|
||||
|
||||
return new EventView({
|
||||
message: eventInfo.message,
|
||||
humanReadableMessage: eventInfo.humanReadableMessage,
|
||||
appIcon: eventInfo.appIcon,
|
||||
appName: eventInfo.appName,
|
||||
userId: userId,
|
||||
@@ -139,18 +165,29 @@ export class EntityEventsComponent implements OnInit {
|
||||
date: r.date,
|
||||
ip: r.ipAddress,
|
||||
type: r.type,
|
||||
};
|
||||
installationId: r.installationId,
|
||||
systemUser: r.systemUser,
|
||||
serviceAccountId: r.serviceAccountId,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
if (!clearExisting && this.events != null && this.events.length > 0) {
|
||||
this.events = this.events.concat(events);
|
||||
if (!clearExisting && this.dataSource.data != null && this.dataSource.data.length > 0) {
|
||||
this.dataSource.data = this.dataSource.data.concat(events);
|
||||
} else {
|
||||
this.events = events;
|
||||
this.dataSource.data = events;
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
this.morePromise = null;
|
||||
this.refreshPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strongly typed helper to open a EntityEventsComponent as a dialog
|
||||
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||
* @param config Configuration for the dialog
|
||||
*/
|
||||
export const openEntityEventsDialog = (
|
||||
dialogService: DialogServiceAbstraction,
|
||||
config: DialogConfig<EntityEventsDialogParams>
|
||||
) => {
|
||||
return dialogService.open<void, EntityEventsDialogParams>(EntityEventsComponent, config);
|
||||
};
|
||||
|
||||
@@ -23,12 +23,16 @@
|
||||
</app-callout>
|
||||
<ng-container *ngIf="!done">
|
||||
<app-callout type="warning" *ngIf="users.length > 0 && !error">
|
||||
{{ removeUsersWarning }}
|
||||
<p>{{ removeUsersWarning }}</p>
|
||||
<p *ngIf="this.showNoMasterPasswordWarning">
|
||||
{{ "removeMembersWithoutMasterPasswordWarning" | i18n }}
|
||||
</p>
|
||||
</app-callout>
|
||||
<table class="table table-hover table-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">{{ "user" | i18n }}</th>
|
||||
<th *ngIf="this.showNoMasterPasswordWarning">{{ "details" | i18n }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr *ngFor="let user of users">
|
||||
@@ -39,6 +43,15 @@
|
||||
{{ user.email }}
|
||||
<small class="text-muted d-block" *ngIf="user.name">{{ user.name }}</small>
|
||||
</td>
|
||||
<td *ngIf="this.showNoMasterPasswordWarning">
|
||||
<span class="text-muted d-block tw-lowercase">
|
||||
<ng-container *ngIf="user.hasMasterPassword === true"> - </ng-container>
|
||||
<ng-container *ngIf="user.hasMasterPassword === false">
|
||||
<i class="bwi bwi-exclamation-triangle" aria-hidden="true"></i>
|
||||
{{ "noMasterPassword" | i18n }}
|
||||
</ng-container>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</ng-container>
|
||||
|
||||
@@ -12,13 +12,23 @@ import { BulkUserDetails } from "./bulk-status.component";
|
||||
})
|
||||
export class BulkRemoveComponent {
|
||||
@Input() organizationId: string;
|
||||
@Input() users: BulkUserDetails[];
|
||||
@Input() set users(value: BulkUserDetails[]) {
|
||||
this._users = value;
|
||||
this.showNoMasterPasswordWarning = this._users.some((u) => u.hasMasterPassword === false);
|
||||
}
|
||||
|
||||
get users(): BulkUserDetails[] {
|
||||
return this._users;
|
||||
}
|
||||
|
||||
private _users: BulkUserDetails[];
|
||||
|
||||
statuses: Map<string, string> = new Map();
|
||||
|
||||
loading = false;
|
||||
done = false;
|
||||
error: string;
|
||||
showNoMasterPasswordWarning = false;
|
||||
|
||||
constructor(
|
||||
protected apiService: ApiService,
|
||||
|
||||
@@ -23,12 +23,16 @@
|
||||
</app-callout>
|
||||
<ng-container *ngIf="!done">
|
||||
<app-callout type="warning" *ngIf="users.length > 0 && !error && isRevoking">
|
||||
{{ "revokeUsersWarning" | i18n }}
|
||||
<p>{{ "revokeUsersWarning" | i18n }}</p>
|
||||
<p *ngIf="this.showNoMasterPasswordWarning">
|
||||
{{ "removeMembersWithoutMasterPasswordWarning" | i18n }}
|
||||
</p>
|
||||
</app-callout>
|
||||
<table class="table table-hover table-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">{{ "user" | i18n }}</th>
|
||||
<th *ngIf="this.showNoMasterPasswordWarning">{{ "details" | i18n }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr *ngFor="let user of users">
|
||||
@@ -39,6 +43,15 @@
|
||||
{{ user.email }}
|
||||
<small class="text-muted d-block" *ngIf="user.name">{{ user.name }}</small>
|
||||
</td>
|
||||
<td *ngIf="this.showNoMasterPasswordWarning">
|
||||
<span class="text-muted d-block tw-lowercase">
|
||||
<ng-container *ngIf="user.hasMasterPassword === true"> - </ng-container>
|
||||
<ng-container *ngIf="user.hasMasterPassword === false">
|
||||
<i class="bwi bwi-exclamation-triangle" aria-hidden="true"></i>
|
||||
{{ "noMasterPassword" | i18n }}
|
||||
</ng-container>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</ng-container>
|
||||
|
||||
@@ -20,6 +20,7 @@ export class BulkRestoreRevokeComponent {
|
||||
loading = false;
|
||||
done = false;
|
||||
error: string;
|
||||
showNoMasterPasswordWarning = false;
|
||||
|
||||
constructor(
|
||||
protected i18nService: I18nService,
|
||||
@@ -29,6 +30,7 @@ export class BulkRestoreRevokeComponent {
|
||||
this.isRevoking = config.data.isRevoking;
|
||||
this.organizationId = config.data.organizationId;
|
||||
this.users = config.data.users;
|
||||
this.showNoMasterPasswordWarning = this.users.some((u) => u.hasMasterPassword === false);
|
||||
}
|
||||
|
||||
get bulkTitle() {
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface BulkUserDetails {
|
||||
name: string;
|
||||
email: string;
|
||||
status: OrganizationUserStatusType | ProviderUserStatusType;
|
||||
hasMasterPassword?: boolean;
|
||||
}
|
||||
|
||||
type BulkStatusEntry = {
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
} from "../../../shared/components/access-selector";
|
||||
|
||||
import { commaSeparatedEmails } from "./validators/comma-separated-emails.validator";
|
||||
import { freeOrgSeatLimitReachedValidator } from "./validators/free-org-inv-limit-reached.validator";
|
||||
|
||||
export enum MemberDialogTab {
|
||||
Role = 0,
|
||||
@@ -46,6 +47,7 @@ export interface MemberDialogParams {
|
||||
name: string;
|
||||
organizationId: string;
|
||||
organizationUserId: string;
|
||||
allOrganizationUserEmails: string[];
|
||||
usesKeyConnector: boolean;
|
||||
initialTab?: MemberDialogTab;
|
||||
}
|
||||
@@ -72,13 +74,14 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||
canUseCustomPermissions: boolean;
|
||||
PermissionMode = PermissionMode;
|
||||
canUseSecretsManager: boolean;
|
||||
showNoMasterPasswordWarning = false;
|
||||
|
||||
protected organization: Organization;
|
||||
protected collectionAccessItems: AccessItemView[] = [];
|
||||
protected groupAccessItems: AccessItemView[] = [];
|
||||
protected tabIndex: MemberDialogTab;
|
||||
protected formGroup = this.formBuilder.group({
|
||||
emails: ["", [Validators.required, commaSeparatedEmails]],
|
||||
emails: ["", { updateOn: "blur" }],
|
||||
type: OrganizationUserType.User,
|
||||
externalId: this.formBuilder.control({ value: "", disabled: true }),
|
||||
accessAllCollections: false,
|
||||
@@ -166,6 +169,20 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||
this.canUseCustomPermissions = organization.useCustomPermissions;
|
||||
this.canUseSecretsManager = organization.useSecretsManager && flagEnabled("secretsManager");
|
||||
|
||||
const emailsControlValidators = [
|
||||
Validators.required,
|
||||
commaSeparatedEmails,
|
||||
freeOrgSeatLimitReachedValidator(
|
||||
this.organization,
|
||||
this.params.allOrganizationUserEmails,
|
||||
this.i18nService.t("subscriptionFreePlan", organization.seats)
|
||||
),
|
||||
];
|
||||
|
||||
const emailsControl = this.formGroup.get("emails");
|
||||
emailsControl.setValidators(emailsControlValidators);
|
||||
emailsControl.updateValueAndValidity();
|
||||
|
||||
this.collectionAccessItems = [].concat(
|
||||
collections.map((c) => mapCollectionToAccessItemView(c))
|
||||
);
|
||||
@@ -179,6 +196,9 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||
throw new Error("Could not find user to edit.");
|
||||
}
|
||||
this.isRevoked = userDetails.status === OrganizationUserStatusType.Revoked;
|
||||
this.showNoMasterPasswordWarning =
|
||||
userDetails.status > OrganizationUserStatusType.Invited &&
|
||||
userDetails.hasMasterPassword === false;
|
||||
const assignedCollectionsPermissions = {
|
||||
editAssignedCollections: userDetails.permissions.editAssignedCollections,
|
||||
deleteAssignedCollections: userDetails.permissions.deleteAssignedCollections,
|
||||
@@ -366,7 +386,7 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||
? "removeUserConfirmationKeyConnector"
|
||||
: "removeOrgUserConfirmation";
|
||||
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
let confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "removeUserIdAccess", placeholders: [this.params.name] },
|
||||
content: { key: message },
|
||||
type: SimpleDialogType.WARNING,
|
||||
@@ -376,6 +396,14 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.showNoMasterPasswordWarning) {
|
||||
confirmed = await this.noMasterPasswordConfirmationDialog();
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
await this.organizationUserService.deleteOrganizationUser(
|
||||
this.params.organizationId,
|
||||
this.params.organizationUserId
|
||||
@@ -394,7 +422,7 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
let confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "revokeUserId", placeholders: [this.params.name] },
|
||||
content: { key: "revokeUserConfirmation" },
|
||||
acceptButtonText: { key: "revokeAccess" },
|
||||
@@ -405,6 +433,14 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.showNoMasterPasswordWarning) {
|
||||
confirmed = await this.noMasterPasswordConfirmationDialog();
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
await this.organizationUserService.revokeOrganizationUser(
|
||||
this.params.organizationId,
|
||||
this.params.organizationUserId
|
||||
@@ -450,6 +486,19 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||
private close(result: MemberDialogResult) {
|
||||
this.dialogRef.close(result);
|
||||
}
|
||||
|
||||
private noMasterPasswordConfirmationDialog() {
|
||||
return this.dialogService.openSimpleDialog({
|
||||
title: {
|
||||
key: "removeOrgUserNoMasterPasswordTitle",
|
||||
},
|
||||
content: {
|
||||
key: "removeOrgUserNoMasterPasswordDesc",
|
||||
placeholders: [this.params.name],
|
||||
},
|
||||
type: SimpleDialogType.WARNING,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function mapCollectionToAccessItemView(
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { AbstractControl, FormControl, ValidationErrors } from "@angular/forms";
|
||||
|
||||
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { ProductType } from "@bitwarden/common/enums";
|
||||
|
||||
import { freeOrgSeatLimitReachedValidator } from "./free-org-inv-limit-reached.validator";
|
||||
|
||||
const orgFactory = (props: Partial<Organization> = {}) =>
|
||||
Object.assign(
|
||||
new Organization(),
|
||||
{
|
||||
id: "myOrgId",
|
||||
enabled: true,
|
||||
type: OrganizationUserType.Admin,
|
||||
},
|
||||
props
|
||||
);
|
||||
|
||||
describe("freeOrgSeatLimitReachedValidator", () => {
|
||||
let organization: Organization;
|
||||
let allOrganizationUserEmails: string[];
|
||||
let validatorFn: (control: AbstractControl) => ValidationErrors | null;
|
||||
|
||||
beforeEach(() => {
|
||||
allOrganizationUserEmails = ["user1@example.com"];
|
||||
});
|
||||
|
||||
it("should return null when control value is empty", () => {
|
||||
validatorFn = freeOrgSeatLimitReachedValidator(
|
||||
organization,
|
||||
allOrganizationUserEmails,
|
||||
"You cannot invite more than 2 members without upgrading your plan."
|
||||
);
|
||||
const control = new FormControl("");
|
||||
|
||||
const result = validatorFn(control);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null when control value is null", () => {
|
||||
validatorFn = freeOrgSeatLimitReachedValidator(
|
||||
organization,
|
||||
allOrganizationUserEmails,
|
||||
"You cannot invite more than 2 members without upgrading your plan."
|
||||
);
|
||||
const control = new FormControl(null);
|
||||
|
||||
const result = validatorFn(control);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null when max seats are not exceeded on free plan", () => {
|
||||
organization = orgFactory({
|
||||
planProductType: ProductType.Free,
|
||||
seats: 2,
|
||||
});
|
||||
validatorFn = freeOrgSeatLimitReachedValidator(
|
||||
organization,
|
||||
allOrganizationUserEmails,
|
||||
"You cannot invite more than 2 members without upgrading your plan."
|
||||
);
|
||||
const control = new FormControl("user2@example.com");
|
||||
|
||||
const result = validatorFn(control);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return validation error when max seats are exceeded on free plan", () => {
|
||||
organization = orgFactory({
|
||||
planProductType: ProductType.Free,
|
||||
seats: 2,
|
||||
});
|
||||
const errorMessage = "You cannot invite more than 2 members without upgrading your plan.";
|
||||
validatorFn = freeOrgSeatLimitReachedValidator(
|
||||
organization,
|
||||
allOrganizationUserEmails,
|
||||
"You cannot invite more than 2 members without upgrading your plan."
|
||||
);
|
||||
const control = new FormControl("user2@example.com,user3@example.com");
|
||||
|
||||
const result = validatorFn(control);
|
||||
|
||||
expect(result).toStrictEqual({ freePlanLimitReached: { message: errorMessage } });
|
||||
});
|
||||
|
||||
it("should return null when not on free plan", () => {
|
||||
const control = new FormControl("user2@example.com,user3@example.com");
|
||||
organization = orgFactory({
|
||||
planProductType: ProductType.Enterprise,
|
||||
seats: 100,
|
||||
});
|
||||
validatorFn = freeOrgSeatLimitReachedValidator(
|
||||
organization,
|
||||
allOrganizationUserEmails,
|
||||
"You cannot invite more than 2 members without upgrading your plan."
|
||||
);
|
||||
|
||||
const result = validatorFn(control);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms";
|
||||
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { ProductType } from "@bitwarden/common/enums";
|
||||
|
||||
/**
|
||||
* Checks if the limit of free organization seats has been reached when adding new users
|
||||
* @param organization An object representing the organization
|
||||
* @param allOrganizationUserEmails An array of strings with existing user email addresses
|
||||
* @param errorMessage A localized string to display if validation fails
|
||||
* @returns A function that validates an `AbstractControl` and returns `ValidationErrors` or `null`
|
||||
*/
|
||||
export function freeOrgSeatLimitReachedValidator(
|
||||
organization: Organization,
|
||||
allOrganizationUserEmails: string[],
|
||||
errorMessage: string
|
||||
): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
if (control.value === "" || !control.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const newEmailsToAdd = control.value
|
||||
.split(",")
|
||||
.filter(
|
||||
(newEmailToAdd: string) =>
|
||||
newEmailToAdd &&
|
||||
!allOrganizationUserEmails.some((existingEmail) => existingEmail === newEmailToAdd)
|
||||
);
|
||||
|
||||
return organization.planProductType === ProductType.Free &&
|
||||
allOrganizationUserEmails.length + newEmailsToAdd.length > organization.seats
|
||||
? { freePlanLimitReached: { message: errorMessage } }
|
||||
: null;
|
||||
};
|
||||
}
|
||||
@@ -317,7 +317,6 @@
|
||||
</ng-container>
|
||||
<ng-template #addEdit></ng-template>
|
||||
<ng-template #groupsTemplate></ng-template>
|
||||
<ng-template #eventsTemplate></ng-template>
|
||||
<ng-template #confirmTemplate></ng-template>
|
||||
<ng-template #resetPasswordTemplate></ng-template>
|
||||
<ng-template #bulkStatusTemplate></ng-template>
|
||||
|
||||
@@ -16,10 +16,10 @@ import {
|
||||
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
|
||||
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
|
||||
import {
|
||||
SimpleDialogType,
|
||||
DialogServiceAbstraction,
|
||||
SimpleDialogCloseType,
|
||||
SimpleDialogOptions,
|
||||
SimpleDialogType,
|
||||
} from "@bitwarden/angular/services/dialog";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -55,7 +55,7 @@ import { CollectionData } from "@bitwarden/common/vault/models/data/collection.d
|
||||
import { Collection } from "@bitwarden/common/vault/models/domain/collection";
|
||||
import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response";
|
||||
|
||||
import { EntityEventsComponent } from "../../../admin-console/organizations/manage/entity-events.component";
|
||||
import { openEntityEventsDialog } from "../../../admin-console/organizations/manage/entity-events.component";
|
||||
import { BasePeopleComponent } from "../../../common/base.people.component";
|
||||
import { GroupService } from "../core";
|
||||
import { OrganizationUserView } from "../core/views/organization-user.view";
|
||||
@@ -81,8 +81,6 @@ export class PeopleComponent
|
||||
{
|
||||
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
|
||||
groupsModalRef: ViewContainerRef;
|
||||
@ViewChild("eventsTemplate", { read: ViewContainerRef, static: true })
|
||||
eventsModalRef: ViewContainerRef;
|
||||
@ViewChild("confirmTemplate", { read: ViewContainerRef, static: true })
|
||||
confirmModalRef: ViewContainerRef;
|
||||
@ViewChild("resetPasswordTemplate", { read: ViewContainerRef, static: true })
|
||||
@@ -398,6 +396,7 @@ export class PeopleComponent
|
||||
name: this.userNamePipe.transform(user),
|
||||
organizationId: this.organization.id,
|
||||
organizationUserId: user != null ? user.id : null,
|
||||
allOrganizationUserEmails: this.allUsers?.map((user) => user.email) ?? [],
|
||||
usesKeyConnector: user?.usesKeyConnector,
|
||||
initialTab: initialTab,
|
||||
},
|
||||
@@ -513,12 +512,14 @@ export class PeopleComponent
|
||||
}
|
||||
|
||||
async events(user: OrganizationUserView) {
|
||||
await this.modalService.openViewRef(EntityEventsComponent, this.eventsModalRef, (comp) => {
|
||||
comp.name = this.userNamePipe.transform(user);
|
||||
comp.organizationId = this.organization.id;
|
||||
comp.entityId = user.id;
|
||||
comp.showUser = false;
|
||||
comp.entity = "user";
|
||||
await openEntityEventsDialog(this.dialogService, {
|
||||
data: {
|
||||
name: this.userNamePipe.transform(user),
|
||||
organizationId: this.organization.id,
|
||||
entityId: user.id,
|
||||
showUser: false,
|
||||
entity: "user",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -546,7 +547,7 @@ export class PeopleComponent
|
||||
? "removeUserConfirmationKeyConnector"
|
||||
: "removeOrgUserConfirmation";
|
||||
|
||||
return await this.dialogService.openSimpleDialog({
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: {
|
||||
key: "removeUserIdAccess",
|
||||
placeholders: [this.userNamePipe.transform(user)],
|
||||
@@ -554,6 +555,35 @@ export class PeopleComponent
|
||||
content: { key: content },
|
||||
type: SimpleDialogType.WARNING,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.status > OrganizationUserStatusType.Invited && user.hasMasterPassword === false) {
|
||||
return await this.noMasterPasswordConfirmationDialog(user);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async revokeUserConfirmationDialog(user: OrganizationUserView) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "revokeAccess", placeholders: [this.userNamePipe.transform(user)] },
|
||||
content: this.revokeWarningMessage(),
|
||||
acceptButtonText: { key: "revokeAccess" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.status > OrganizationUserStatusType.Invited && user.hasMasterPassword === false) {
|
||||
return await this.noMasterPasswordConfirmationDialog(user);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async showBulkStatus(
|
||||
@@ -608,4 +638,17 @@ export class PeopleComponent
|
||||
modal.close();
|
||||
}
|
||||
}
|
||||
|
||||
private async noMasterPasswordConfirmationDialog(user: OrganizationUserView) {
|
||||
return this.dialogService.openSimpleDialog({
|
||||
title: {
|
||||
key: "removeOrgUserNoMasterPasswordTitle",
|
||||
},
|
||||
content: {
|
||||
key: "removeOrgUserNoMasterPasswordDesc",
|
||||
placeholders: [this.userNamePipe.transform(user)],
|
||||
},
|
||||
type: SimpleDialogType.WARNING,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,95 +1,66 @@
|
||||
<div class="page-header">
|
||||
<h1>{{ "organizationInfo" | i18n }}</h1>
|
||||
</div>
|
||||
<h1 bitTypography="h1" class="tw-pb-2.5">{{ "organizationInfo" | i18n }}</h1>
|
||||
<div *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
<form
|
||||
*ngIf="org && !loading"
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="name">{{ "organizationName" | i18n }}</label>
|
||||
<input
|
||||
id="name"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Name"
|
||||
[(ngModel)]="org.name"
|
||||
[disabled]="selfHosted"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="billingEmail">{{ "billingEmail" | i18n }}</label>
|
||||
<input
|
||||
id="billingEmail"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="BillingEmail"
|
||||
[(ngModel)]="org.billingEmail"
|
||||
[disabled]="selfHosted || !canEditSubscription"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="businessName">{{ "businessName" | i18n }}</label>
|
||||
<input
|
||||
id="businessName"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="BusinessName"
|
||||
[(ngModel)]="org.businessName"
|
||||
[disabled]="selfHosted || !canEditSubscription"
|
||||
/>
|
||||
</div>
|
||||
<form *ngIf="org && !loading" #form [bitSubmit]="submit" [formGroup]="formGroup">
|
||||
<div class="tw-grid tw-grid-cols-2 tw-gap-5">
|
||||
<div>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "organizationName" | i18n }}</bit-label>
|
||||
<input bitInput id="orgName" type="text" formControlName="orgName" />
|
||||
</bit-form-field>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "billingEmail" | i18n }}</bit-label>
|
||||
<input bitInput id="billingEmail" formControlName="billingEmail" type="email" />
|
||||
</bit-form-field>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "businessName" | i18n }}</bit-label>
|
||||
<input bitInput id="businessName" formControlName="businessName" type="text" />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div>
|
||||
<bit-avatar [text]="org.name" [id]="org.id" size="large"></bit-avatar>
|
||||
<app-account-fingerprint
|
||||
[fingerprintMaterial]="organizationId"
|
||||
[publicKeyBuffer]="publicKeyBuffer"
|
||||
fingerprintLabel="{{ 'yourOrganizationsFingerprint' | i18n }}"
|
||||
>
|
||||
</app-account-fingerprint>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" buttonType="primary" bitButton [loading]="form.loading">
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
<ng-container *ngIf="canUseApi">
|
||||
<div class="secondary-header border-0 mb-0">
|
||||
<h1>{{ "apiKey" | i18n }}</h1>
|
||||
</div>
|
||||
<h1 bitTypography="h1" class="tw-mt-16 tw-pb-2.5">{{ "apiKey" | i18n }}</h1>
|
||||
<p>
|
||||
{{ "apiKeyDesc" | i18n }}
|
||||
<a href="https://docs.bitwarden.com" target="_blank" rel="noopener">
|
||||
{{ "learnMore" | i18n }}
|
||||
</a>
|
||||
</p>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="viewApiKey()">
|
||||
<button type="button" bitButton buttonType="secondary" (click)="viewApiKey()">
|
||||
{{ "viewApiKey" | i18n }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="rotateApiKey()">
|
||||
<button type="button" bitButton buttonType="secondary" (click)="rotateApiKey()">
|
||||
{{ "rotateApiKey" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<div class="secondary-header text-danger border-0 mb-0">
|
||||
<h1>{{ "dangerZone" | i18n }}</h1>
|
||||
</div>
|
||||
<div class="card border-danger">
|
||||
<div class="card-body">
|
||||
<p>{{ "dangerZoneDesc" | i18n }}</p>
|
||||
<button type="button" class="btn btn-outline-danger" (click)="deleteOrganization()">
|
||||
{{ "deleteOrganization" | i18n }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger" (click)="purgeVault()">
|
||||
{{ "purgeVault" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<h1 bitTypography="h1" class="tw-mt-16 tw-pb-2.5 !tw-text-danger">{{ "dangerZone" | i18n }}</h1>
|
||||
<div class="tw-rounded tw-border tw-border-solid tw-border-danger-500 tw-bg-background tw-p-5">
|
||||
<p>{{ "dangerZoneDesc" | i18n }}</p>
|
||||
<button type="button" bitButton buttonType="danger" (click)="deleteOrganization()">
|
||||
{{ "deleteOrganization" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="danger" (click)="purgeVault()">
|
||||
{{ "purgeVault" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<ng-template #purgeOrganizationTemplate></ng-template>
|
||||
<ng-template #apiKeyTemplate></ng-template>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { lastValueFrom } from "rxjs";
|
||||
import { combineLatest, lastValueFrom, Subject, switchMap, takeUntil, from } from "rxjs";
|
||||
|
||||
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
@@ -13,6 +14,7 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
import { ApiKeyComponent } from "../../../settings/api-key.component";
|
||||
import { PurgeVaultComponent } from "../../../settings/purge-vault.component";
|
||||
@@ -23,7 +25,6 @@ import { DeleteOrganizationDialogResult, openDeleteOrganizationDialog } from "./
|
||||
selector: "app-org-account",
|
||||
templateUrl: "account.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class AccountComponent {
|
||||
@ViewChild("purgeOrganizationTemplate", { read: ViewContainerRef, static: true })
|
||||
purgeModalRef: ViewContainerRef;
|
||||
@@ -40,7 +41,29 @@ export class AccountComponent {
|
||||
formPromise: Promise<OrganizationResponse>;
|
||||
taxFormPromise: Promise<unknown>;
|
||||
|
||||
private organizationId: string;
|
||||
// FormGroup validators taken from server Organization domain object
|
||||
protected formGroup = this.formBuilder.group({
|
||||
orgName: this.formBuilder.control(
|
||||
{ value: "", disabled: true },
|
||||
{
|
||||
validators: [Validators.required, Validators.maxLength(50)],
|
||||
updateOn: "change",
|
||||
}
|
||||
),
|
||||
billingEmail: this.formBuilder.control(
|
||||
{ value: "", disabled: true },
|
||||
{ validators: [Validators.required, Validators.email, Validators.maxLength(256)] }
|
||||
),
|
||||
businessName: this.formBuilder.control(
|
||||
{ value: "", disabled: true },
|
||||
{ validators: [Validators.maxLength(50)] }
|
||||
),
|
||||
});
|
||||
|
||||
protected organizationId: string;
|
||||
protected publicKeyBuffer: ArrayBuffer;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private modalService: ModalService,
|
||||
@@ -52,53 +75,88 @@ export class AccountComponent {
|
||||
private router: Router,
|
||||
private organizationService: OrganizationService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private dialogService: DialogServiceAbstraction
|
||||
private dialogService: DialogServiceAbstraction,
|
||||
private formBuilder: FormBuilder
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.selfHosted = this.platformUtilsService.isSelfHost();
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.organizationId = params.organizationId;
|
||||
this.canEditSubscription = this.organizationService.get(
|
||||
this.organizationId
|
||||
).canEditSubscription;
|
||||
try {
|
||||
this.org = await this.organizationApiService.get(this.organizationId);
|
||||
this.canUseApi = this.org.useApi;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
});
|
||||
this.loading = false;
|
||||
this.route.parent.parent.params
|
||||
.pipe(
|
||||
switchMap((params) => {
|
||||
return combineLatest([
|
||||
// Organization domain
|
||||
this.organizationService.get$(params.organizationId),
|
||||
// OrganizationResponse for form population
|
||||
from(this.organizationApiService.get(params.organizationId)),
|
||||
// Organization Public Key
|
||||
from(this.organizationApiService.getKeys(params.organizationId)),
|
||||
]);
|
||||
}),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe(([organization, orgResponse, orgKeys]) => {
|
||||
// Set domain level organization variables
|
||||
this.organizationId = organization.id;
|
||||
this.canEditSubscription = organization.canEditSubscription;
|
||||
this.canUseApi = organization.useApi;
|
||||
|
||||
// Org Response
|
||||
this.org = orgResponse;
|
||||
|
||||
// Public Key Buffer for Org Fingerprint Generation
|
||||
this.publicKeyBuffer = Utils.fromB64ToArray(orgKeys?.publicKey)?.buffer;
|
||||
|
||||
// Patch existing values
|
||||
this.formGroup.patchValue({
|
||||
orgName: this.org.name,
|
||||
billingEmail: this.org.billingEmail,
|
||||
businessName: this.org.businessName,
|
||||
});
|
||||
|
||||
// Update disabled states - reactive forms prefers not using disabled attribute
|
||||
if (!this.selfHosted) {
|
||||
this.formGroup.get("orgName").enable();
|
||||
}
|
||||
|
||||
if (!this.selfHosted || this.canEditSubscription) {
|
||||
this.formGroup.get("billingEmail").enable();
|
||||
this.formGroup.get("businessName").enable();
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
const request = new OrganizationUpdateRequest();
|
||||
request.name = this.org.name;
|
||||
request.businessName = this.org.businessName;
|
||||
request.billingEmail = this.org.billingEmail;
|
||||
ngOnDestroy(): void {
|
||||
// You must first call .next() in order for the notifier to properly close subscriptions using takeUntil
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
// Backfill pub/priv key if necessary
|
||||
if (!this.org.hasPublicAndPrivateKeys) {
|
||||
const orgShareKey = await this.cryptoService.getOrgKey(this.organizationId);
|
||||
const orgKeys = await this.cryptoService.makeKeyPair(orgShareKey);
|
||||
request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
|
||||
}
|
||||
|
||||
this.formPromise = this.organizationApiService.save(this.organizationId, request);
|
||||
await this.formPromise;
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("organizationUpdated")
|
||||
);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
submit = async () => {
|
||||
this.formGroup.markAllAsTouched();
|
||||
if (this.formGroup.invalid) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const request = new OrganizationUpdateRequest();
|
||||
request.name = this.formGroup.value.orgName;
|
||||
request.businessName = this.formGroup.value.businessName;
|
||||
request.billingEmail = this.formGroup.value.billingEmail;
|
||||
|
||||
// Backfill pub/priv key if necessary
|
||||
if (!this.org.hasPublicAndPrivateKeys) {
|
||||
const orgShareKey = await this.cryptoService.getOrgKey(this.organizationId);
|
||||
const orgKeys = await this.cryptoService.makeKeyPair(orgShareKey);
|
||||
request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
|
||||
}
|
||||
|
||||
this.formPromise = this.organizationApiService.save(this.organizationId, request);
|
||||
await this.formPromise;
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("organizationUpdated"));
|
||||
};
|
||||
|
||||
async deleteOrganization() {
|
||||
const dialog = openDeleteOrganizationDialog(this.dialogService, {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { LooseComponentsModule, SharedModule } from "../../../shared";
|
||||
import { AccountFingerprintComponent } from "../../../shared/components/account-fingerprint/account-fingerprint.component";
|
||||
import { PoliciesModule } from "../../organizations/policies";
|
||||
|
||||
import { AccountComponent } from "./account.component";
|
||||
@@ -9,7 +10,13 @@ import { SettingsComponent } from "./settings.component";
|
||||
import { TwoFactorSetupComponent } from "./two-factor-setup.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, LooseComponentsModule, PoliciesModule, OrganizationSettingsRoutingModule],
|
||||
imports: [
|
||||
SharedModule,
|
||||
LooseComponentsModule,
|
||||
PoliciesModule,
|
||||
OrganizationSettingsRoutingModule,
|
||||
AccountFingerprintComponent,
|
||||
],
|
||||
declarations: [SettingsComponent, AccountComponent, TwoFactorSetupComponent],
|
||||
})
|
||||
export class OrganizationSettingsModule {}
|
||||
|
||||
@@ -60,6 +60,16 @@
|
||||
>
|
||||
{{ "singleSignOn" | i18n }}
|
||||
</a>
|
||||
<ng-container *appIfFeature="FeatureFlag.TrustedDeviceEncryption">
|
||||
<a
|
||||
routerLink="device-approvals"
|
||||
class="list-group-item"
|
||||
routerLinkActive="active"
|
||||
*ngIf="org.canManageUsersPassword"
|
||||
>
|
||||
{{ "deviceApprovals" | i18n }}
|
||||
</a>
|
||||
</ng-container>
|
||||
<a
|
||||
routerLink="scim"
|
||||
class="list-group-item"
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Observable, switchMap } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
|
||||
@Component({
|
||||
selector: "app-org-settings",
|
||||
@@ -11,6 +12,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
})
|
||||
export class SettingsComponent implements OnInit {
|
||||
organization$: Observable<Organization>;
|
||||
FeatureFlag = FeatureFlag;
|
||||
|
||||
constructor(private route: ActivatedRoute, private organizationService: OrganizationService) {}
|
||||
|
||||
|
||||
@@ -247,13 +247,17 @@ export abstract class BasePeopleComponent<
|
||||
this.actionPromise = null;
|
||||
}
|
||||
|
||||
async revoke(user: UserType) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
protected async revokeUserConfirmationDialog(user: UserType) {
|
||||
return this.dialogService.openSimpleDialog({
|
||||
title: { key: "revokeAccess", placeholders: [this.userNamePipe.transform(user)] },
|
||||
content: this.revokeWarningMessage(),
|
||||
acceptButtonText: { key: "revokeAccess" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
});
|
||||
}
|
||||
|
||||
async revoke(user: UserType) {
|
||||
const confirmed = await this.revokeUserConfirmationDialog(user);
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<router-outlet></router-outlet>
|
||||
<div class="container my-5 text-muted text-center">
|
||||
<div class="tw-mb-1">
|
||||
<div class="tw-mb-1" *ngIf="!isSelfHosted">
|
||||
<bit-menu #environmentOptions>
|
||||
<a bitMenuItem href="https://vault.bitwarden.com" class="pr-4">
|
||||
<i
|
||||
|
||||
@@ -11,9 +11,10 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
})
|
||||
export class FrontendLayoutComponent implements OnInit, OnDestroy {
|
||||
version: string;
|
||||
isSelfHosted: boolean;
|
||||
euServerFlagEnabled: boolean;
|
||||
year = "2015";
|
||||
isEuServer = true;
|
||||
euServerFlagEnabled: boolean;
|
||||
|
||||
constructor(
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
@@ -23,6 +24,7 @@ export class FrontendLayoutComponent implements OnInit, OnDestroy {
|
||||
async ngOnInit() {
|
||||
this.year = new Date().getFullYear().toString();
|
||||
this.version = await this.platformUtilsService.getApplicationVersion();
|
||||
this.isSelfHosted = this.platformUtilsService.isSelfHost();
|
||||
this.euServerFlagEnabled = await this.configService.getFeatureFlagBool(
|
||||
FeatureFlag.DisplayEuEnvironmentFlag
|
||||
);
|
||||
|
||||
@@ -46,19 +46,11 @@
|
||||
Customize
|
||||
</button>
|
||||
</div>
|
||||
<hr />
|
||||
<p *ngIf="fingerprint">
|
||||
{{ "yourAccountsFingerprint" | i18n }}:
|
||||
<a
|
||||
href="https://bitwarden.com/help/fingerprint-phrase/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i></a
|
||||
><br />
|
||||
<code>{{ fingerprint }}</code>
|
||||
</p>
|
||||
<app-account-fingerprint
|
||||
[fingerprintMaterial]="fingerprintMaterial"
|
||||
fingerprintLabel="{{ 'yourAccountsFingerprint' | i18n }}"
|
||||
>
|
||||
</app-account-fingerprint>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
|
||||
@@ -3,10 +3,8 @@ import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { UpdateProfileRequest } from "@bitwarden/common/auth/models/request/update-profile.request";
|
||||
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -21,7 +19,7 @@ import { ChangeAvatarComponent } from "./change-avatar.component";
|
||||
export class ProfileComponent implements OnInit, OnDestroy {
|
||||
loading = true;
|
||||
profile: ProfileResponse;
|
||||
fingerprint: string;
|
||||
fingerprintMaterial: string;
|
||||
|
||||
formPromise: Promise<any>;
|
||||
@ViewChild("avatarModalTemplate", { read: ViewContainerRef, static: true })
|
||||
@@ -32,9 +30,7 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private cryptoService: CryptoService,
|
||||
private logService: LogService,
|
||||
private keyConnectorService: KeyConnectorService,
|
||||
private stateService: StateService,
|
||||
private modalService: ModalService
|
||||
) {}
|
||||
@@ -42,12 +38,7 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
async ngOnInit() {
|
||||
this.profile = await this.apiService.getProfile();
|
||||
this.loading = false;
|
||||
const fingerprint = await this.cryptoService.getFingerprint(
|
||||
await this.stateService.getUserId()
|
||||
);
|
||||
if (fingerprint != null) {
|
||||
this.fingerprint = fingerprint.join("-");
|
||||
}
|
||||
this.fingerprintMaterial = await this.stateService.getUserId();
|
||||
}
|
||||
|
||||
async ngOnDestroy() {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<ng-container>
|
||||
<hr />
|
||||
<p *ngIf="fingerprint">
|
||||
{{ fingerprintLabel }}:
|
||||
<a
|
||||
href="https://bitwarden.com/help/fingerprint-phrase/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i
|
||||
></a>
|
||||
<br />
|
||||
<code class="tw-text-code">{{ fingerprint }}</code>
|
||||
</p>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
|
||||
import { SharedModule } from "../../shared.module";
|
||||
|
||||
@Component({
|
||||
selector: "app-account-fingerprint",
|
||||
templateUrl: "account-fingerprint.component.html",
|
||||
standalone: true,
|
||||
imports: [SharedModule],
|
||||
})
|
||||
export class AccountFingerprintComponent implements OnInit {
|
||||
@Input() fingerprintMaterial: string;
|
||||
@Input() publicKeyBuffer: ArrayBuffer;
|
||||
@Input() fingerprintLabel: string;
|
||||
|
||||
protected fingerprint: string;
|
||||
|
||||
constructor(private cryptoService: CryptoService) {}
|
||||
|
||||
async ngOnInit() {
|
||||
// TODO - In the future, remove this code and use the fingerprint pipe once merged
|
||||
const generatedFingerprint = await this.cryptoService.getFingerprint(
|
||||
this.fingerprintMaterial,
|
||||
this.publicKeyBuffer
|
||||
);
|
||||
this.fingerprint = generatedFingerprint?.join("-") ?? null;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { NgModule } from "@angular/core";
|
||||
import { OrganizationSwitcherComponent } from "../admin-console/components/organization-switcher.component";
|
||||
import { OrganizationCreateModule } from "../admin-console/organizations/create/organization-create.module";
|
||||
import { OrganizationLayoutComponent } from "../admin-console/organizations/layouts/organization-layout.component";
|
||||
import { EntityEventsComponent as OrgEntityEventsComponent } from "../admin-console/organizations/manage/entity-events.component";
|
||||
import { EventsComponent as OrgEventsComponent } from "../admin-console/organizations/manage/events.component";
|
||||
import { UserConfirmComponent as OrgUserConfirmComponent } from "../admin-console/organizations/manage/user-confirm.component";
|
||||
import { AcceptFamilySponsorshipComponent } from "../admin-console/organizations/sponsorships/accept-family-sponsorship.component";
|
||||
@@ -108,6 +107,7 @@ import { AddEditComponent as OrgAddEditComponent } from "../vault/org-vault/add-
|
||||
import { AttachmentsComponent as OrgAttachmentsComponent } from "../vault/org-vault/attachments.component";
|
||||
import { CollectionsComponent as OrgCollectionsComponent } from "../vault/org-vault/collections.component";
|
||||
|
||||
import { AccountFingerprintComponent } from "./components/account-fingerprint/account-fingerprint.component";
|
||||
import { UserVerificationModule } from "./components/user-verification";
|
||||
import { SharedModule } from "./shared.module";
|
||||
|
||||
@@ -122,6 +122,7 @@ import { SharedModule } from "./shared.module";
|
||||
UserVerificationModule,
|
||||
ChangeKdfModule,
|
||||
DynamicAvatarComponent,
|
||||
AccountFingerprintComponent,
|
||||
],
|
||||
declarations: [
|
||||
PremiumBadgeComponent,
|
||||
@@ -166,7 +167,6 @@ import { SharedModule } from "./shared.module";
|
||||
OrganizationPlansComponent,
|
||||
OrgAttachmentsComponent,
|
||||
OrgCollectionsComponent,
|
||||
OrgEntityEventsComponent,
|
||||
OrgEventsComponent,
|
||||
OrgExposedPasswordsReportComponent,
|
||||
OrgInactiveTwoFactorReportComponent,
|
||||
@@ -271,7 +271,6 @@ import { SharedModule } from "./shared.module";
|
||||
OrganizationPlansComponent,
|
||||
OrgAttachmentsComponent,
|
||||
OrgCollectionsComponent,
|
||||
OrgEntityEventsComponent,
|
||||
OrgEventsComponent,
|
||||
OrgExposedPasswordsReportComponent,
|
||||
OrgInactiveTwoFactorReportComponent,
|
||||
|
||||
@@ -96,5 +96,4 @@
|
||||
<ng-template #attachments></ng-template>
|
||||
<ng-template #cipherAddEdit></ng-template>
|
||||
<ng-template #collectionsModal></ng-template>
|
||||
<ng-template #eventsTemplate></ng-template>
|
||||
</div>
|
||||
|
||||
@@ -56,7 +56,7 @@ import { CollectionView } from "@bitwarden/common/vault/models/view/collection.v
|
||||
import { Icons } from "@bitwarden/components";
|
||||
|
||||
import { GroupService, GroupView } from "../../admin-console/organizations/core";
|
||||
import { EntityEventsComponent } from "../../admin-console/organizations/manage/entity-events.component";
|
||||
import { openEntityEventsDialog } from "../../admin-console/organizations/manage/entity-events.component";
|
||||
import { VaultFilterService } from "../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";
|
||||
import { VaultFilter } from "../../vault/individual-vault/vault-filter/shared/models/vault-filter.model";
|
||||
import {
|
||||
@@ -109,8 +109,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
cipherAddEditModalRef: ViewContainerRef;
|
||||
@ViewChild("collectionsModal", { read: ViewContainerRef, static: true })
|
||||
collectionsModalRef: ViewContainerRef;
|
||||
@ViewChild("eventsTemplate", { read: ViewContainerRef, static: true })
|
||||
eventsModalRef: ViewContainerRef;
|
||||
|
||||
trashCleanupWarning: string = null;
|
||||
activeFilter: VaultFilter = new VaultFilter();
|
||||
@@ -885,12 +883,14 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async viewEvents(cipher: CipherView) {
|
||||
await this.modalService.openViewRef(EntityEventsComponent, this.eventsModalRef, (comp) => {
|
||||
comp.name = cipher.name;
|
||||
comp.organizationId = this.organization.id;
|
||||
comp.entityId = cipher.id;
|
||||
comp.showUser = true;
|
||||
comp.entity = "cipher";
|
||||
await openEntityEventsDialog(this.dialogService, {
|
||||
data: {
|
||||
name: cipher.name,
|
||||
organizationId: this.organization.id,
|
||||
entityId: cipher.id,
|
||||
showUser: true,
|
||||
entity: "cipher",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user