1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-06 18:43:25 +00:00

[SG-360] Remove the /modules/ folder (#3225)

* Move Web's SharedModule to /app/shared/

This commit relocates `SharedModule` from `/app/modules` to `/app/shared` to
align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure)

All other changes are just to adjust imports that reference `SharedModule`.

* Move /modules/pipes to /shared/pipes

This commit relocates `PipesModule` from `/app/modules` to `/app/shared` to
align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure)

All other changes are just to adjust imports that reference `PipesModule`.

* Move LooseComponentsModule to /shared/

This commit relocates `LooseComponentsModule` from `/app/modules` to `/app/shared` to
align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure)

All other changes are just to adjust imports that reference `LooseComponentsModule`.

* Move VerticalStepperModule to /shared/

This commit relocates `VerticalStepperModule` from `/app/modules` to `/app/shared` to
align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure)

All other changes are just to adjust imports that reference `VerticalStepperModule`.

* Move TrialInitiationModule to /shared/

This commit relocates `TrialInitiationModule` & `RegisterFormModule` from `/app/modules` to `/app/shared` to
align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure)

All other changes are just to adjust imports that reference `TrialInitiationModule` or `RegisterFormModule`.

* Move /modules/organization to /organization

This commit relocates all modules in `/app/modules/organization` to `/app/organization` to
align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure)

All other changes are just to adjust imports that reference the moved modules.

* Move /modules/vault/ to /vault

This commit relocates the IndividualVaultModule to `/app/modules/vault`, and the OrganizationVaultModule to `/app/organization/vault` to
align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure)

All other changes are just to adjust imports that reference the moved modules.

* Move VaultFiltersModule to /vault

This commit relocates the `VaultFilterModule` to `/app/vault/vault-filter`, and the OrganizationVaultFilterComponent to `/app/organization/vault/vault-filter` to
align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure)

All other changes are just to adjust imports that reference the moved modules.

* Remove the /modules/ folder from desktop

This commit relocates the `VaultFilterModule` to `/app/vault/vault-filter`, and the OrganizationVaultFilterComponent to `/app/organization/vault/vault-filter` to
align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure)

All other changes are just to adjust imports that reference the moved modules.

* Move Libs' VaultFiltersComponent to /vault/

This commit moves the lib's logic for `VaultFiltersModule` from
`/modules/` to `/vault/`

All other changes are just to adjust imports that reference the moved
files.

* Rename VaultModule -> SharedVaultModule

* Rename IndividualVaultModule -> VaultModule

* Rename OrganizationVaultModule -> VaultModule

* Rename OrganizationVaultFilterComponent

Rename OrganizationVaultFilterComponent to VaultFilterComponent

* Seperate the two VaultFilterComponents

This commit seperate the `OrganizationVaultFilterComponent` from the `VaultFilerModule`, which is only used by the individual vault.
A `VaultFilterSharedModule` was created to declare shared components and provide shared services between the two implementations.
This was done to align with best practices for NgModules.

* [r] Move VerticalStepperModule to /account/

More specifically, /account/trial/

* [r] Declare PaymentComponent in LooseComponentsModule

`PaymentComponent` is not reused across domains and should not be
declared in `SharedModule`.

I've moved it to `LooseComponentsModule` for now, but later it will need
to be exported from a `SettingsModule`.

* [r] Declare TaxInfoComponent in LooseComponentsModule

* [r] Reloacte Pipes out of /shared/

* [r] Extract locales out of SharedModule

* [r] Add documentation to shared module

* [r] Cleanup imports

* [r] Use an index.ts file for /shared/

* [r] Add eslint rule restricting access to /shared/

Co-authored-by: Hinton <hinton@users.noreply.github.com>
This commit is contained in:
Addison Beck
2022-08-08 15:08:35 -04:00
committed by GitHub
parent 56ce85c69c
commit 95bb429281
120 changed files with 289 additions and 293 deletions

View File

@@ -0,0 +1,12 @@
import { NgModule } from "@angular/core";
import { SharedModule } from "../../shared";
import { OrganizationInformationComponent } from "./organization-information.component";
@NgModule({
imports: [SharedModule],
declarations: [OrganizationInformationComponent],
exports: [OrganizationInformationComponent],
})
export class OrganizationCreateModule {}

View File

@@ -0,0 +1,38 @@
<form #form [formGroup]="formGroup" *ngIf="nameOnly">
<bit-form-field>
<bit-label>{{ "organizationName" | i18n }}</bit-label>
<input bitInput type="text" formControlName="name" />
</bit-form-field>
</form>
<form #form [formGroup]="formGroup" *ngIf="!nameOnly">
<h2>{{ "generalInformation" | i18n }}</h2>
<div class="tw-flex tw-w-full tw-space-x-4" *ngIf="createOrganization">
<bit-form-field class="tw-w-1/2">
<bit-label>{{ "organizationName" | i18n }}</bit-label>
<input bitInput type="text" formControlName="name" />
</bit-form-field>
<bit-form-field class="tw-w-1/2">
<bit-label>{{ "billingEmail" | i18n }}</bit-label>
<input bitInput type="email" formControlName="billingEmail" />
</bit-form-field>
<bit-form-field class="tw-w-1/2" *ngIf="isProvider">
<bit-label>{{ "clientOwnerEmail" | i18n }}</bit-label>
<input bitInput type="email" formControlName="clientOwnerEmail" />
</bit-form-field>
</div>
<div *ngIf="!isProvider && !acceptingSponsorship">
<input
type="checkbox"
name="businessOwned"
formControlName="businessOwned"
(change)="changedBusinessOwned.emit()"
/>
<bit-label for="businessOwned" class="tw-mb-3">{{ "accountOwnedBusiness" | i18n }}</bit-label>
<div class="tw-mt-4" *ngIf="formGroup.controls['businessOwned'].value">
<bit-form-field class="tw-w-1/2">
<bit-label>{{ "businessName" | i18n }}</bit-label>
<input bitInput type="text" formControlName="businessName" />
</bit-form-field>
</div>
</div>
</form>

View File

@@ -0,0 +1,15 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { UntypedFormGroup } from "@angular/forms";
@Component({
selector: "app-org-info",
templateUrl: "organization-information.component.html",
})
export class OrganizationInformationComponent {
@Input() nameOnly = false;
@Input() createOrganization = true;
@Input() isProvider = false;
@Input() acceptingSponsorship = false;
@Input() formGroup: UntypedFormGroup;
@Output() changedBusinessOwned = new EventEmitter<void>();
}

View File

@@ -20,9 +20,8 @@ import {
import { ListResponse } from "@bitwarden/common/models/response/listResponse";
import { CollectionView } from "@bitwarden/common/models/view/collectionView";
import { EntityUsersComponent } from "../../modules/organizations/manage/entity-users.component";
import { CollectionAddEditComponent } from "./collection-add-edit.component";
import { EntityUsersComponent } from "./entity-users.component";
@Component({
selector: "app-org-manage-collections",

View File

@@ -0,0 +1,180 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="userAccessTitle">
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="modal-header">
<h2 class="modal-title" id="userAccessTitle">
{{ "userAccess" | i18n }}
<small>{{ entityName }}</small>
</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" *ngIf="loading || !users">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</div>
<cdk-virtual-scroll-viewport
itemSize="46"
minBufferPx="600"
maxBufferPx="1200"
[style]="scrollViewportStyle"
>
<div class="modal-body" *ngIf="!loading && users && searchedUsers">
<div class="d-flex">
<div class="mr-3">
<label class="sr-only" for="search">{{ "search" | i18n }}</label>
<input
type="search"
class="form-control form-control-sm"
id="search"
placeholder="{{ 'search' | i18n }}"
name="SearchText"
[(ngModel)]="searchText"
/>
</div>
<div class="btn-group btn-group-sm" role="group">
<button
type="button"
class="btn btn-outline-secondary"
[ngClass]="{ active: !showSelected }"
(click)="filterSelected(false)"
>
{{ "all" | i18n }}
</button>
<button
type="button"
class="btn btn-outline-secondary"
[ngClass]="{ active: showSelected }"
(click)="filterSelected(true)"
>
{{ "selected" | i18n }}
<span bitBadge badgeType="info" *ngIf="selectedCount">{{ selectedCount }}</span>
</button>
</div>
</div>
<ng-container *ngIf="!searchedUsers.length">
<hr />
{{ "noUsersInList" | i18n }}
</ng-container>
<table class="table table-hover table-list mb-0" [hidden]="!searchedUsers.length">
<thead>
<tr>
<th>&nbsp;</th>
<th>&nbsp;</th>
<th>{{ "name" | i18n }}</th>
<th *ngIf="entity === 'collection'">&nbsp;</th>
<th>{{ "userType" | i18n }}</th>
<th width="100" class="text-center" *ngIf="entity === 'collection'">
{{ "hidePasswords" | i18n }}
</th>
<th width="100" class="text-center" *ngIf="entity === 'collection'">
{{ "readOnly" | i18n }}
</th>
</tr>
</thead>
<tbody>
<tr *cdkVirtualFor="let u of searchedUsers" class="">
<td class="table-list-checkbox" (click)="check(u)">
<input
type="checkbox"
[(ngModel)]="u.checked"
name="{{ u.id.substr(0, 8) }}_Checked"
[disabled]="entity === 'collection' && u.accessAll"
(change)="selectedChanged(u)"
appStopProp
/>
</td>
<td width="30" (click)="check(u)">
<app-avatar
[data]="u | userName"
[email]="u.email"
size="25"
[circle]="true"
[fontSize]="14"
>
</app-avatar>
</td>
<td>
{{ u.email }}
<span
bitBadge
badgeType="secondary"
*ngIf="u.status === organizationUserStatusType.Invited"
>{{ "invited" | i18n }}</span
>
<span
bitBadge
badgeType="warning"
*ngIf="u.status === organizationUserStatusType.Accepted"
>{{ "accepted" | i18n }}</span
>
<small class="text-muted d-block" *ngIf="u.name">{{ u.name }}</small>
</td>
<td *ngIf="entity === 'collection'">
<ng-container *ngIf="u.accessAll">
<i
class="bwi bwi-filter"
title="{{ 'userAccessAllItems' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "userAccessAllItems" | i18n }}</span>
</ng-container>
</td>
<td>
<span *ngIf="u.type === organizationUserType.Owner">{{ "owner" | i18n }}</span>
<span *ngIf="u.type === organizationUserType.Admin">{{ "admin" | i18n }}</span>
<span *ngIf="u.type === organizationUserType.Manager">{{
"manager" | i18n
}}</span>
<span *ngIf="u.type === organizationUserType.User">{{ "user" | i18n }}</span>
<span *ngIf="u.type === organizationUserType.Custom">{{ "custom" | i18n }}</span>
</td>
<td class="text-center" *ngIf="entity === 'collection'">
<input
type="checkbox"
[(ngModel)]="u.hidePasswords"
name="{{ u.id.substr(0, 8) }}_HidePasswords"
[disabled]="u.accessAll || !u.checked"
/>
</td>
<td class="text-center" *ngIf="entity === 'collection'">
<input
type="checkbox"
[(ngModel)]="u.readOnly"
name="{{ u.id.substr(0, 8) }}_ReadOnly"
[disabled]="u.accessAll || !u.checked"
/>
</td>
</tr>
</tbody>
</table>
</div>
</cdk-virtual-scroll-viewport>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,156 @@
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType";
import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType";
import { Utils } from "@bitwarden/common/misc/utils";
import { SelectionReadOnlyRequest } from "@bitwarden/common/models/request/selectionReadOnlyRequest";
import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/models/response/organizationUserResponse";
@Component({
selector: "app-entity-users",
templateUrl: "entity-users.component.html",
providers: [SearchPipe],
})
export class EntityUsersComponent implements OnInit {
@Input() entity: "group" | "collection";
@Input() entityId: string;
@Input() entityName: string;
@Input() organizationId: string;
@Output() onEditedUsers = new EventEmitter();
organizationUserType = OrganizationUserType;
organizationUserStatusType = OrganizationUserStatusType;
showSelected = false;
loading = true;
formPromise: Promise<any>;
selectedCount = 0;
searchText: string;
private allUsers: OrganizationUserUserDetailsResponse[] = [];
constructor(
private search: SearchPipe,
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private logService: LogService
) {}
async ngOnInit() {
await this.loadUsers();
this.loading = false;
}
get users() {
if (this.showSelected) {
return this.allUsers.filter((u) => (u as any).checked);
} else {
return this.allUsers;
}
}
get searchedUsers() {
return this.search.transform(this.users, this.searchText, "name", "email", "id");
}
get scrollViewportStyle() {
return `min-height: 120px; height: ${120 + this.searchedUsers.length * 46}px`;
}
async loadUsers() {
const users = await this.apiService.getOrganizationUsers(this.organizationId);
this.allUsers = users.data.map((r) => r).sort(Utils.getSortFunction(this.i18nService, "email"));
if (this.entity === "group") {
const response = await this.apiService.getGroupUsers(this.organizationId, this.entityId);
if (response != null && users.data.length > 0) {
response.forEach((s) => {
const user = users.data.filter((u) => u.id === s);
if (user != null && user.length > 0) {
(user[0] as any).checked = true;
}
});
}
} else if (this.entity === "collection") {
const response = await this.apiService.getCollectionUsers(this.organizationId, this.entityId);
if (response != null && users.data.length > 0) {
response.forEach((s) => {
const user = users.data.filter((u) => !u.accessAll && u.id === s.id);
if (user != null && user.length > 0) {
(user[0] as any).checked = true;
(user[0] as any).readOnly = s.readOnly;
(user[0] as any).hidePasswords = s.hidePasswords;
}
});
}
}
this.allUsers.forEach((u) => {
if (this.entity === "collection" && u.accessAll) {
(u as any).checked = true;
}
if ((u as any).checked) {
this.selectedCount++;
}
});
}
check(u: OrganizationUserUserDetailsResponse) {
if (this.entity === "collection" && u.accessAll) {
return;
}
(u as any).checked = !(u as any).checked;
this.selectedChanged(u);
}
selectedChanged(u: OrganizationUserUserDetailsResponse) {
if ((u as any).checked) {
this.selectedCount++;
} else {
if (this.entity === "collection") {
(u as any).readOnly = false;
(u as any).hidePasswords = false;
}
this.selectedCount--;
}
}
filterSelected(showSelected: boolean) {
this.showSelected = showSelected;
}
async submit() {
try {
if (this.entity === "group") {
const selections = this.users.filter((u) => (u as any).checked).map((u) => u.id);
this.formPromise = this.apiService.putGroupUsers(
this.organizationId,
this.entityId,
selections
);
} else {
const selections = this.users
.filter((u) => (u as any).checked && !u.accessAll)
.map(
(u) =>
new SelectionReadOnlyRequest(u.id, !!(u as any).readOnly, !!(u as any).hidePasswords)
);
this.formPromise = this.apiService.putCollectionUsers(
this.organizationId,
this.entityId,
selections
);
}
await this.formPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("updatedUsers"));
this.onEditedUsers.emit();
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -12,8 +12,7 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { Utils } from "@bitwarden/common/misc/utils";
import { GroupResponse } from "@bitwarden/common/models/response/groupResponse";
import { EntityUsersComponent } from "../../modules/organizations/manage/entity-users.component";
import { EntityUsersComponent } from "./entity-users.component";
import { GroupAddEditComponent } from "./group-add-edit.component";
@Component({

View File

@@ -0,0 +1,13 @@
import { ScrollingModule } from "@angular/cdk/scrolling";
import { NgModule } from "@angular/core";
import { SharedModule } from "../../shared";
import { EntityUsersComponent } from "./entity-users.component";
@NgModule({
imports: [SharedModule, ScrollingModule],
declarations: [EntityUsersComponent],
exports: [EntityUsersComponent],
})
export class OrganizationManageModule {}

View File

@@ -4,8 +4,6 @@ import { RouterModule, Routes } from "@angular/router";
import { AuthGuard } from "@bitwarden/angular/guards/auth.guard";
import { Permissions } from "@bitwarden/common/enums/permissions";
import { OrganizationVaultModule } from "../modules/vault/modules/organization-vault/organization-vault.module";
import { PermissionsGuard } from "./guards/permissions.guard";
import { OrganizationLayoutComponent } from "./layouts/organization-layout.component";
import { CollectionsComponent } from "./manage/collections.component";
@@ -26,6 +24,7 @@ import { ReusedPasswordsReportComponent } from "./tools/reused-passwords-report.
import { ToolsComponent } from "./tools/tools.component";
import { UnsecuredWebsitesReportComponent } from "./tools/unsecured-websites-report.component";
import { WeakPasswordsReportComponent } from "./tools/weak-passwords-report.component";
import { VaultModule } from "./vault/vault.module";
const routes: Routes = [
{
@@ -39,7 +38,7 @@ const routes: Routes = [
{ path: "", pathMatch: "full", redirectTo: "vault" },
{
path: "vault",
loadChildren: () => OrganizationVaultModule,
loadChildren: () => VaultModule,
},
{
path: "tools",

View File

@@ -1,8 +1,5 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
@@ -13,21 +10,14 @@ import { ImportService as ImportServiceAbstraction } from "@bitwarden/common/abs
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { ImportService } from "@bitwarden/common/services/import.service";
import { LooseComponentsModule } from "../../../modules/loose-components.module";
import { LooseComponentsModule, SharedModule } from "../../../shared";
import { OrganizationExportComponent } from "./org-export.component";
import { OrganizationImportExportRoutingModule } from "./org-import-export-routing.module";
import { OrganizationImportComponent } from "./org-import.component";
@NgModule({
imports: [
CommonModule,
JslibModule,
LooseComponentsModule,
FormsModule,
ReactiveFormsModule,
OrganizationImportExportRoutingModule,
],
imports: [SharedModule, LooseComponentsModule, OrganizationImportExportRoutingModule],
declarations: [OrganizationImportComponent, OrganizationExportComponent],
providers: [
{

View File

@@ -0,0 +1,52 @@
<div
class="modal fade"
role="dialog"
aria-modal="true"
aria-labelledby="enrollMasterPasswordResetTitle"
>
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="modal-header">
<h2 class="modal-title" id="enrollMasterPasswordResetTitle">
{{ "enrollPasswordReset" | i18n }}
</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<app-callout type="warning">
{{ "resetPasswordEnrollmentWarning" | i18n }}
</app-callout>
<app-user-verification [(ngModel)]="verification" name="secret"> </app-user-verification>
</div>
<div class="modal-footer">
<bit-submit-button [loading]="form.loading">
{{ "submit" | i18n }}
</bit-submit-button>
<button
bitButton
buttonType="secondary"
type="button"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span>
{{ "cancel" | i18n }}
</span>
</button>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,82 @@
import { Component } from "@angular/core";
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
import { ModalConfig } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { SyncService } from "@bitwarden/common/abstractions/sync.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification.service";
import { Utils } from "@bitwarden/common/misc/utils";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/models/request/organizationUserResetPasswordEnrollmentRequest";
import { Verification } from "@bitwarden/common/types/verification";
@Component({
selector: "app-enroll-master-password-reset",
templateUrl: "enroll-master-password-reset.component.html",
})
export class EnrollMasterPasswordReset {
organization: Organization;
verification: Verification;
formPromise: Promise<any>;
constructor(
private userVerificationService: UserVerificationService,
private apiService: ApiService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private cryptoService: CryptoService,
private syncService: SyncService,
private logService: LogService,
private modalRef: ModalRef,
config: ModalConfig
) {
this.organization = config.data.organization;
}
async submit() {
let toastStringRef = "withdrawPasswordResetSuccess";
this.formPromise = this.userVerificationService
.buildRequest(this.verification, OrganizationUserResetPasswordEnrollmentRequest)
.then(async (request) => {
// Set variables
let keyString: string = null;
// Retrieve Public Key
const orgKeys = await this.apiService.getOrganizationKeys(this.organization.id);
if (orgKeys == null) {
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
}
const publicKey = Utils.fromB64ToArray(orgKeys.publicKey);
// RSA Encrypt user's encKey.key with organization public key
const encKey = await this.cryptoService.getEncKey();
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey.buffer);
keyString = encryptedKey.encryptedString;
toastStringRef = "enrollPasswordResetSuccess";
// Create request and execute enrollment
request.resetPasswordKey = keyString;
await this.apiService.putOrganizationUserResetPasswordEnrollment(
this.organization.id,
this.organization.userId,
request
);
await this.syncService.fullSync(true);
});
try {
await this.formPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t(toastStringRef));
this.modalRef.close();
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -0,0 +1,13 @@
import { ScrollingModule } from "@angular/cdk/scrolling";
import { NgModule } from "@angular/core";
import { LooseComponentsModule, SharedModule } from "../../shared";
import { EnrollMasterPasswordReset } from "./enroll-master-password-reset.component";
@NgModule({
imports: [SharedModule, ScrollingModule, LooseComponentsModule],
declarations: [EnrollMasterPasswordReset],
exports: [EnrollMasterPasswordReset],
})
export class OrganizationUserModule {}

View File

@@ -0,0 +1,28 @@
import { Component } from "@angular/core";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { VaultFilterComponent as BaseVaultFilterComponent } from "../../../vault/vault-filter/vault-filter.component";
@Component({
selector: "app-organization-vault-filter",
templateUrl: "../../../vault/vault-filter/vault-filter.component.html",
})
export class VaultFilterComponent extends BaseVaultFilterComponent {
hideOrganizations = true;
hideFavorites = true;
hideFolders = true;
organization: Organization;
async initCollections() {
if (this.organization.canEditAnyCollection) {
return await this.vaultFilterService.buildAdminCollections(this.organization.id);
}
return await this.vaultFilterService.buildCollections(this.organization.id);
}
async reloadCollectionsAndFolders() {
this.collections = await this.initCollections();
}
}

View File

@@ -0,0 +1,12 @@
import { NgModule } from "@angular/core";
import { VaultFilterSharedModule } from "../../../vault/vault-filter/shared/vault-filter-shared.module";
import { VaultFilterComponent } from "./vault-filter.component";
@NgModule({
imports: [VaultFilterSharedModule],
declarations: [VaultFilterComponent],
exports: [VaultFilterComponent],
})
export class VaultFilterModule {}

View File

@@ -0,0 +1,16 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { VaultComponent } from "./vault.component";
const routes: Routes = [
{
path: "",
component: VaultComponent,
data: { titleId: "vaults" },
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class VaultRoutingModule {}

View File

@@ -0,0 +1,67 @@
<div class="container page-content">
<div class="row">
<div class="col-3">
<div class="groupings">
<div class="content">
<div class="inner-content">
<app-organization-vault-filter
#vaultFilter
[activeFilter]="activeFilter"
(onFilterChange)="applyVaultFilter($event)"
(onSearchTextChanged)="filterSearchText($event)"
></app-organization-vault-filter>
</div>
</div>
</div>
</div>
<div class="col-9">
<div class="page-header d-flex">
<h1>
{{ "vaultItems" | i18n }}
<small #actionSpinner [appApiAction]="ciphersComponent.actionPromise">
<ng-container *ngIf="actionSpinner.loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
</small>
</h1>
<div class="ml-auto d-flex">
<app-vault-bulk-actions
[ciphersComponent]="ciphersComponent"
[deleted]="deleted"
[organization]="organization"
>
</app-vault-bulk-actions>
<button
type="button"
class="btn btn-outline-primary btn-sm ml-auto"
(click)="addCipher()"
*ngIf="!deleted"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "addItem" | i18n }}
</button>
</div>
</div>
<app-callout type="warning" *ngIf="deleted" icon="bwi bwi-exclamation-triangle">
{{ trashCleanupWarning }}
</app-callout>
<app-org-vault-ciphers
(onCipherClicked)="editCipher($event)"
(onAttachmentsClicked)="editCipherAttachments($event)"
(onAddCipher)="addCipher()"
(onCollectionsClicked)="editCipherCollections($event)"
(onEventsClicked)="viewEvents($event)"
(onCloneClicked)="cloneCipher($event)"
>
</app-org-vault-ciphers>
</div>
</div>
</div>
<ng-template #attachments></ng-template>
<ng-template #cipherAddEdit></ng-template>
<ng-template #collections></ng-template>
<ng-template #eventsTemplate></ng-template>

View File

@@ -0,0 +1,331 @@
import {
ChangeDetectorRef,
Component,
NgZone,
OnDestroy,
OnInit,
ViewChild,
ViewContainerRef,
} from "@angular/core";
import { ActivatedRoute, Params, Router } from "@angular/router";
import { first } from "rxjs/operators";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service";
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { SyncService } from "@bitwarden/common/abstractions/sync.service";
import { CipherType } from "@bitwarden/common/enums/cipherType";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { CipherView } from "@bitwarden/common/models/view/cipherView";
import { VaultService } from "../../vault/shared/vault.service";
import { EntityEventsComponent } from "../manage/entity-events.component";
import { AddEditComponent } from "./add-edit.component";
import { AttachmentsComponent } from "./attachments.component";
import { CiphersComponent } from "./ciphers.component";
import { CollectionsComponent } from "./collections.component";
import { VaultFilterComponent } from "./vault-filter/vault-filter.component";
const BroadcasterSubscriptionId = "OrgVaultComponent";
@Component({
selector: "app-org-vault",
templateUrl: "vault.component.html",
})
export class VaultComponent implements OnInit, OnDestroy {
@ViewChild("vaultFilter", { static: true })
vaultFilterComponent: VaultFilterComponent;
@ViewChild(CiphersComponent, { static: true }) ciphersComponent: CiphersComponent;
@ViewChild("attachments", { read: ViewContainerRef, static: true })
attachmentsModalRef: ViewContainerRef;
@ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true })
cipherAddEditModalRef: ViewContainerRef;
@ViewChild("collections", { read: ViewContainerRef, static: true })
collectionsModalRef: ViewContainerRef;
@ViewChild("eventsTemplate", { read: ViewContainerRef, static: true })
eventsModalRef: ViewContainerRef;
organization: Organization;
collectionId: string = null;
type: CipherType = null;
trashCleanupWarning: string = null;
activeFilter: VaultFilter = new VaultFilter();
// This is a hack to avoid redundant api calls that fetch OrganizationVaultFilterComponent collections
// When it makes sense to do so we should leverage some other communication method for change events that isn't directly tied to the query param for organizationId
// i.e. exposing the VaultFiltersService to the OrganizationSwitcherComponent to make relevant updates from a change event instead of just depending on the router
firstLoaded = true;
constructor(
private route: ActivatedRoute,
private organizationService: OrganizationService,
private router: Router,
private changeDetectorRef: ChangeDetectorRef,
private syncService: SyncService,
private i18nService: I18nService,
private modalService: ModalService,
private messagingService: MessagingService,
private broadcasterService: BroadcasterService,
private ngZone: NgZone,
private platformUtilsService: PlatformUtilsService,
private vaultService: VaultService,
private cipherService: CipherService,
private passwordRepromptService: PasswordRepromptService
) {}
ngOnInit() {
this.trashCleanupWarning = this.i18nService.t(
this.platformUtilsService.isSelfHost()
? "trashCleanupWarningSelfHosted"
: "trashCleanupWarning"
);
this.route.parent.params.subscribe(async (params: any) => {
this.organization = await this.organizationService.get(params.organizationId);
this.vaultFilterComponent.organization = this.organization;
this.ciphersComponent.organization = this.organization;
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
this.ciphersComponent.searchText = this.vaultFilterComponent.searchText = qParams.search;
if (!this.organization.canViewAllCollections) {
await this.syncService.fullSync(false);
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case "syncCompleted":
if (message.successfully) {
await Promise.all([
this.vaultFilterComponent.reloadCollectionsAndFolders(),
this.ciphersComponent.refresh(),
]);
this.changeDetectorRef.detectChanges();
}
break;
}
});
});
}
if (this.firstLoaded) {
await this.vaultFilterComponent.reloadCollectionsAndFolders();
}
this.firstLoaded = true;
await this.ciphersComponent.reload();
if (qParams.viewEvents != null) {
const cipher = this.ciphersComponent.ciphers.filter((c) => c.id === qParams.viewEvents);
if (cipher.length > 0) {
this.viewEvents(cipher[0]);
}
}
this.route.queryParams.subscribe(async (params) => {
const cipherId = getCipherIdFromParams(params);
if (cipherId) {
if (
// Handle users with implicit collection access since they use the admin endpoint
this.organization.canEditAnyCollection ||
(await this.cipherService.get(cipherId)) != null
) {
this.editCipherId(cipherId);
} else {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("unknownCipher")
);
this.router.navigate([], {
queryParams: { cipherId: null, itemId: null },
queryParamsHandling: "merge",
});
}
}
});
});
});
}
get deleted(): boolean {
return this.activeFilter.status === "trash";
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
async applyVaultFilter(vaultFilter: VaultFilter) {
this.ciphersComponent.showAddNew = vaultFilter.status !== "trash";
this.activeFilter = vaultFilter;
await this.ciphersComponent.reload(
this.activeFilter.buildFilter(),
vaultFilter.status === "trash"
);
this.vaultFilterComponent.searchPlaceholder =
this.vaultService.calculateSearchBarLocalizationString(this.activeFilter);
this.go();
}
filterSearchText(searchText: string) {
this.ciphersComponent.searchText = searchText;
this.ciphersComponent.search(200);
}
async editCipherAttachments(cipher: CipherView) {
if (this.organization.maxStorageGb == null || this.organization.maxStorageGb === 0) {
this.messagingService.send("upgradeOrganization", { organizationId: cipher.organizationId });
return;
}
let madeAttachmentChanges = false;
const [modal] = await this.modalService.openViewRef(
AttachmentsComponent,
this.attachmentsModalRef,
(comp) => {
comp.organization = this.organization;
comp.cipherId = cipher.id;
comp.onUploadedAttachment.subscribe(() => (madeAttachmentChanges = true));
comp.onDeletedAttachment.subscribe(() => (madeAttachmentChanges = true));
}
);
modal.onClosed.subscribe(async () => {
if (madeAttachmentChanges) {
await this.ciphersComponent.refresh();
}
madeAttachmentChanges = false;
});
}
async editCipherCollections(cipher: CipherView) {
const [modal] = await this.modalService.openViewRef(
CollectionsComponent,
this.collectionsModalRef,
(comp) => {
if (this.organization.canEditAnyCollection) {
comp.collectionIds = cipher.collectionIds;
comp.collections = this.vaultFilterComponent.collections.fullList.filter(
(c) => !c.readOnly && c.id != null
);
}
comp.organization = this.organization;
comp.cipherId = cipher.id;
comp.onSavedCollections.subscribe(async () => {
modal.close();
await this.ciphersComponent.refresh();
});
}
);
}
async addCipher() {
const component = await this.editCipher(null);
component.organizationId = this.organization.id;
component.type = this.type;
if (this.organization.canEditAnyCollection) {
component.collections = this.vaultFilterComponent.collections.fullList.filter(
(c) => !c.readOnly && c.id != null
);
}
if (this.collectionId != null) {
component.collectionIds = [this.collectionId];
}
}
async editCipher(cipher: CipherView) {
return this.editCipherId(cipher?.id);
}
async editCipherId(cipherId: string) {
const cipher = await this.cipherService.get(cipherId);
if (cipher != null && cipher.reprompt != 0) {
if (!(await this.passwordRepromptService.showPasswordPrompt())) {
this.go({ cipherId: null, itemId: null });
return;
}
}
const [modal, childComponent] = await this.modalService.openViewRef(
AddEditComponent,
this.cipherAddEditModalRef,
(comp) => {
comp.organization = this.organization;
comp.cipherId = cipherId;
comp.onSavedCipher.subscribe(async () => {
modal.close();
await this.ciphersComponent.refresh();
});
comp.onDeletedCipher.subscribe(async () => {
modal.close();
await this.ciphersComponent.refresh();
});
comp.onRestoredCipher.subscribe(async () => {
modal.close();
await this.ciphersComponent.refresh();
});
}
);
modal.onClosedPromise().then(() => {
this.go({ cipherId: null, itemId: null });
});
return childComponent;
}
async cloneCipher(cipher: CipherView) {
const component = await this.editCipher(cipher);
component.cloneMode = true;
component.organizationId = this.organization.id;
if (this.organization.canEditAnyCollection) {
component.collections = this.vaultFilterComponent.collections.fullList.filter(
(c) => !c.readOnly && c.id != null
);
}
// Regardless of Admin state, the collection Ids need to passed manually as they are not assigned value
// in the add-edit componenet
component.collectionIds = cipher.collectionIds;
}
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";
});
}
private go(queryParams: any = null) {
if (queryParams == null) {
queryParams = {
type: this.activeFilter.cipherType,
collectionId: this.activeFilter.selectedCollectionId,
deleted: this.deleted ? true : null,
};
}
this.router.navigate([], {
relativeTo: this.route,
queryParams: queryParams,
queryParamsHandling: "merge",
replaceUrl: true,
});
}
}
/**
* Allows backwards compatibility with
* old links that used the original `cipherId` param
*/
const getCipherIdFromParams = (params: Params): string => {
return params["itemId"] || params["cipherId"];
};

View File

@@ -0,0 +1,15 @@
import { NgModule } from "@angular/core";
import { VaultSharedModule } from "../../vault/shared/vault-shared.module";
import { CiphersComponent } from "./ciphers.component";
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
import { VaultRoutingModule } from "./vault-routing.module";
import { VaultComponent } from "./vault.component";
@NgModule({
imports: [VaultSharedModule, VaultRoutingModule, VaultFilterModule],
declarations: [VaultComponent, CiphersComponent],
exports: [VaultComponent],
})
export class VaultModule {}