1
0
mirror of https://github.com/bitwarden/web synced 2025-12-06 00:03:28 +00:00

Compare commits

..

32 Commits

Author SHA1 Message Date
Hinton
283f295481 Change fields to field(s) 2022-05-30 13:47:02 +02:00
Hinton
749ead5863 Move error summary below form 2022-05-23 17:15:21 +02:00
Hinton
8d369bcf84 Add error summary 2022-05-23 15:27:49 +02:00
Hinton
aa7fb1befb Add input i18n 2022-05-20 16:57:55 +02:00
Hinton
43ca31e0ae Validate before submit 2022-05-17 22:19:27 +02:00
Hinton
0dfc8c5a0b Merge branch 'master' of github.com:bitwarden/web into feature/cl-forms
# Conflicts:
#	jslib
2022-05-17 21:59:13 +02:00
Oscar Hinton
7b55c8ad1a [EC-203] Add logic for falling back to the users vault (#1688)
* Add logic for falling back to the users vault

* Update error message

* Add jira ticket
2022-05-17 12:46:42 -04:00
Robyn MacCallum
30057d2ac4 Fix switcher not appearing for users affected by single org policy (#1689) 2022-05-17 10:04:43 -04:00
Robyn MacCallum
6f7b712bc7 [SG-16] Fix various small bugs (#1686)
* Fix all items showing in No Folder

* Fix folders not showing for orgs

* Fix missing toasts if there are errors with org options
2022-05-17 09:20:39 -04:00
Justin Baur
45da771404 Fix Defects (#1683) 2022-05-16 09:45:35 -04:00
Jake Fink
902c620eb6 change revoke text (#1684) 2022-05-16 09:08:25 -04:00
Matt Gibson
ca35ccbd35 PS-515 Fix/org users list performance (#1673)
* [PS-515] Use virtual scroll to speed up long user lists

WIP: this is currently showing a large blank area under the last user. Need to figure out why virtual-scroll-spacer is sized too large.

* Fix cdk-virtual-scroll styling

* Format csp for readability

* Set Viewport height

The viewport height was

* Calculate viewport height from item size

Virtual scroll viewports need set heights, we can emulate the old modal behavior by calculating an approximate height required by the viewport to display all items. It will not go beyond the window due to the `.modal-dialog-scrollable` class

* Remove modal css changes

* pr review
2022-05-13 15:52:58 -04:00
Kyle Spearrin
85aa4274f3 remove hostname from simplelogin. update jslib (#1679) 2022-05-13 15:36:37 -04:00
Addison Beck
ffb63a1cc7 [fix] Various Trash filter bugs from EUVR (#1681)
* [fix] Hide the Add Item button when filtering for trash

* [fix] Use correct bulk actions for trash filter
2022-05-13 15:14:51 -04:00
Addison Beck
3501be9484 [fix] Add max width to vault filter buttons to keep content unified (#1678) 2022-05-13 15:14:39 -04:00
Addison Beck
5d1522b77a [fix] Check policies when reloading organizations for filters (#1677) 2022-05-13 15:14:27 -04:00
Oscar Hinton
be30d47038 [EC-200] Handle an edge case where ciphers were not selectable (#1674) 2022-05-13 15:32:15 +02:00
Thomas Rittson
888892b3e7 Revert accidental cnesting of org routes (#1670) 2022-05-13 08:31:30 +10:00
Oscar Hinton
f4f3e8c574 [SG-279] Fix unlock button not working after logging in through SSO (#1672) 2022-05-12 16:21:50 +02:00
Oscar Hinton
3367736c7e [SG-267] Exposed passwords report: console error (#1671) 2022-05-12 15:47:18 +02:00
Vincent Salucci
e3e7fce70a [bug:euvr] Self-hosted instance hiding subscription nav item (#1669) 2022-05-12 08:43:27 -04:00
Thomas Rittson
74bdfe2602 Fix permission checking when accessing manage SSO (#1663) 2022-05-12 09:12:02 +10:00
Thomas Rittson
9e01d47a3f Update Go Premium link (#1660) 2022-05-12 06:53:18 +10:00
Vincent Salucci
67de7d4bfb [bug:euvr] Change password init fix (#1667) 2022-05-11 15:39:56 -04:00
Vincent Salucci
f5245a280e [bug:euvr] Create Organization Updates (#1664) 2022-05-11 15:39:39 -04:00
Addison Beck
1dc9502676 [fix] Update the navbar when creating an organization for the first time (#1668) 2022-05-11 14:10:02 -04:00
Robyn MacCallum
ccf0d64a7b Use correct localization string (#1665) 2022-05-11 13:32:42 -04:00
Jake Fink
dc2078ae58 add better error handling to tax call (#1666) 2022-05-11 11:13:42 -04:00
Thomas Rittson
da470ad709 [SG-234] Fix account menu icon colour (#1653) 2022-05-11 08:38:45 +10:00
Hinton
c1801dfc61 Merge branch 'master' of github.com:bitwarden/web into feature/cl-forms
# Conflicts:
#	jslib
2022-05-10 16:40:13 +02:00
Hinton
59a53740a4 Merge branch 'master' of github.com:bitwarden/web into feature/cl-forms
# Conflicts:
#	jslib
#	src/app/organizations/settings/account.component.html
#	src/app/organizations/settings/account.component.ts
#	src/app/oss.module.ts
2022-05-10 14:51:10 +02:00
Hinton
dd2c2b9720 Update Accounts form to use reactive forms and bit-form-field 2022-05-03 11:21:42 +02:00
33 changed files with 350 additions and 276 deletions

View File

@@ -19,8 +19,8 @@ jobs:
name: Setup
runs-on: ubuntu-20.04
outputs:
release_version: ${{ steps.version.outputs.version }}
tag_version: ${{ steps.version.outputs.version }}
release_version: ${{ steps.version.outputs.package }}
tag_version: ${{ steps.version.outputs.tag }}
branch_name: ${{ steps.branch.outputs.branch_name }}
steps:
- name: Branch check
@@ -38,11 +38,20 @@ jobs:
- name: Check Release Version
id: version
uses: bitwarden/gh-actions/release-version-check@ea9fab01d76940267b4147cc1c4542431246b9f6
with:
release-type: ${{ github.event.inputs.release_type }}
project-type: ts
file: package.json
run: |
version=$( jq -r ".version" package.json)
previous_release_tag_version=$(
curl -sL https://api.github.com/repos/$GITHUB_REPOSITORY/releases/latest | jq -r ".tag_name"
)
if [ "v$version" == "$previous_release_tag_version" ] && \
[ "${{ github.event.inputs.release_type }}" == "Initial Release" ]; then
echo "[!] Already released v$version. Please bump version to continue"
exit 1
fi
echo "::set-output name=package::$version"
echo "::set-output name=tag::v$version"
- name: Get branch name
id: branch

2
jslib

Submodule jslib updated: 6f117b9901...3bf25edd3e

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@bitwarden/web-vault",
"version": "2022.5.2",
"version": "2.28.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@bitwarden/web-vault",
"version": "2022.5.2",
"version": "2.28.1",
"hasInstallScript": true,
"license": "GPL-3.0",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@bitwarden/web-vault",
"version": "2022.5.2",
"version": "2.28.1",
"license": "GPL-3.0",
"repository": "https://github.com/bitwarden/web",
"scripts": {

View File

@@ -122,20 +122,17 @@ export abstract class BaseEventsComponent {
const userId = r.actingUserId == null ? r.userId : r.actingUserId;
const eventInfo = await this.eventService.getEventInfo(r);
const user = this.getUserName(r, userId);
const userName = user != null ? user.name : this.i18nService.t("unknown");
return new EventView({
message: eventInfo.message,
humanReadableMessage: eventInfo.humanReadableMessage,
appIcon: eventInfo.appIcon,
appName: eventInfo.appName,
userId: userId,
userName: r.installationId != null ? `Installation: ${r.installationId}` : userName,
userName: user != null ? user.name : this.i18nService.t("unknown"),
userEmail: user != null ? user.email : "",
date: r.date,
ip: r.ipAddress,
type: r.type,
installationId: r.installationId,
});
})
);

View File

@@ -35,7 +35,6 @@ import { BulkStatusComponent as OrgBulkStatusComponent } from "../organizations/
import { CollectionAddEditComponent as OrgCollectionAddEditComponent } from "../organizations/manage/collection-add-edit.component";
import { CollectionsComponent as OrgManageCollectionsComponent } from "../organizations/manage/collections.component";
import { EntityEventsComponent as OrgEntityEventsComponent } from "../organizations/manage/entity-events.component";
import { EntityUsersComponent as OrgEntityUsersComponent } from "../organizations/manage/entity-users.component";
import { EventsComponent as OrgEventsComponent } from "../organizations/manage/events.component";
import { GroupAddEditComponent as OrgGroupAddEditComponent } from "../organizations/manage/group-add-edit.component";
import { GroupsComponent as OrgGroupsComponent } from "../organizations/manage/groups.component";
@@ -115,6 +114,7 @@ import { EmergencyAccessTakeoverComponent } from "../settings/emergency-access-t
import { EmergencyAccessViewComponent } from "../settings/emergency-access-view.component";
import { EmergencyAccessComponent } from "../settings/emergency-access.component";
import { EmergencyAddEditComponent } from "../settings/emergency-add-edit.component";
import { LinkSsoComponent } from "../settings/link-sso.component";
import { OrganizationPlansComponent } from "../settings/organization-plans.component";
import { PaymentMethodComponent } from "../settings/payment-method.component";
import { PaymentComponent } from "../settings/payment.component";
@@ -223,6 +223,7 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
HintComponent,
ImportComponent,
InactiveTwoFactorReportComponent,
LinkSsoComponent,
LockComponent,
LoginComponent,
MasterPasswordPolicyComponent,
@@ -243,7 +244,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
OrgCollectionAddEditComponent,
OrgCollectionsComponent,
OrgEntityEventsComponent,
OrgEntityUsersComponent,
OrgEventsComponent,
OrgExportComponent,
OrgExposedPasswordsReportComponent,
@@ -383,6 +383,7 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
HintComponent,
ImportComponent,
InactiveTwoFactorReportComponent,
LinkSsoComponent,
LockComponent,
LoginComponent,
MasterPasswordPolicyComponent,
@@ -403,7 +404,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
OrgCollectionAddEditComponent,
OrgCollectionsComponent,
OrgEntityEventsComponent,
OrgEntityUsersComponent,
OrgEventsComponent,
OrgExportComponent,
OrgExposedPasswordsReportComponent,

View File

@@ -29,52 +29,52 @@
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</div>
<div
class="modal-body"
*ngIf="
!loading && users && (users | search: searchText:'name':'email':'id') as searchedUsers
"
<cdk-virtual-scroll-viewport
itemSize="46"
minBufferPx="600"
maxBufferPx="1200"
[style]="scrollViewportStyle"
>
<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 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 class="badge badge-pill badge-info" *ngIf="selectedCount">{{
selectedCount
}}</span>
</button>
</div>
</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 class="badge badge-pill badge-info" *ngIf="selectedCount">{{
selectedCount
}}</span>
</button>
</div>
</div>
<ng-container *ngIf="!searchedUsers.length">
<hr />
{{ "noUsersInList" | i18n }}
</ng-container>
<ng-container *ngIf="searchedUsers.length">
<table class="table table-hover table-list mb-0">
<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>
@@ -91,7 +91,7 @@
</tr>
</thead>
<tbody>
<tr *ngFor="let u of searchedUsers">
<tr *cdkVirtualFor="let u of searchedUsers" class="">
<td class="table-list-checkbox" (click)="check(u)">
<input
type="checkbox"
@@ -164,8 +164,8 @@
</tr>
</tbody>
</table>
</ng-container>
</div>
</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>

View File

@@ -1,5 +1,6 @@
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { SearchPipe } from "jslib-angular/pipes/search.pipe";
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
@@ -13,6 +14,7 @@ import { OrganizationUserUserDetailsResponse } from "jslib-common/models/respons
@Component({
selector: "app-entity-users",
templateUrl: "entity-users.component.html",
providers: [SearchPipe],
})
export class EntityUsersComponent implements OnInit {
@Input() entity: "group" | "collection";
@@ -33,6 +35,7 @@ export class EntityUsersComponent implements OnInit {
private allUsers: OrganizationUserUserDetailsResponse[] = [];
constructor(
private search: SearchPipe,
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
@@ -52,6 +55,14 @@ export class EntityUsersComponent implements OnInit {
}
}
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"));

View File

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

View File

@@ -53,7 +53,13 @@ import localeZhTw from "@angular/common/locales/zh-Hant";
import { NgModule } from "@angular/core";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { RouterModule } from "@angular/router";
import { BadgeModule, ButtonModule, CalloutModule, MenuModule } from "@bitwarden/components";
import {
BadgeModule,
ButtonModule,
CalloutModule,
FormFieldModule,
MenuModule,
} from "@bitwarden/components";
import { InfiniteScrollModule } from "ngx-infinite-scroll";
import { ToastrModule } from "ngx-toastr";
@@ -126,6 +132,7 @@ registerLocaleData(localeZhTw, "zh-TW");
BadgeModule,
ButtonModule,
MenuModule,
FormFieldModule,
],
exports: [
CommonModule,
@@ -136,6 +143,7 @@ registerLocaleData(localeZhTw, "zh-TW");
ReactiveFormsModule,
RouterModule,
BadgeModule,
FormFieldModule,
ButtonModule,
CalloutModule,
ToastrModule,

View File

@@ -16,7 +16,7 @@
aria-hidden="true"
></i>
</button>
<h3 class="filter-title">&nbsp;{{ collectionsGrouping.name | i18n }}</h3>
<h3 class="filter-title">{{ collectionsGrouping.name | i18n }}</h3>
</div>
<ul id="collection-filters" *ngIf="!isCollapsed(collectionsGrouping)" class="filter-options">
<ng-template #recursiveCollections let-collections>
@@ -31,7 +31,7 @@
<button
class="toggle-button"
*ngIf="c.children.length"
(click)="toggleCollapse(c.node)"
(click)="collapse(c.node)"
title="{{ 'toggleCollapse' | i18n }}"
[attr.aria-expanded]="!isCollapsed(c.node)"
[attr.aria-controls]="c.node.name + '_children'"
@@ -51,7 +51,7 @@
class="bwi bwi-collection bwi-fw"
aria-hidden="true"
></i
>&nbsp;{{ c.node.name }}
>{{ c.node.name }}
</button>
</span>
<ul

View File

@@ -16,7 +16,9 @@
}"
></i>
</button>
<h3 class="filter-title">&nbsp;{{ "folders" | i18n }}</h3>
<h3 class="filter-title">
{{ "folders" | i18n }}
</h3>
<button
class="text-muted ml-auto add-button"
(click)="addFolder()"
@@ -54,7 +56,7 @@
</button>
<button class="filter-button" (click)="applyFilter(f.node)">
<i *ngIf="f.children.length === 0" class="bwi bwi-fw bwi-folder" aria-hidden="true"></i
>&nbsp;{{ f.node.name }}
>{{ f.node.name }}
</button>
<button
class="edit-button"

View File

@@ -14,7 +14,7 @@
<span class="filter-buttons">
<a href="#" routerLink="/create-organization" class="filter-button">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
&nbsp;{{ "newOrganization" | i18n }}
{{ "newOrganization" | i18n }}
</a>
</span>
</li>
@@ -45,6 +45,14 @@
>
&nbsp;{{ organizationGrouping.name | i18n }}
</button>
<a
href="#"
routerLink="/create-organization"
class="text-muted ml-auto create-organization-link"
appA11yTitle="{{ 'newOrganization' | i18n }}"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
</a>
</div>
<ul id="organization-filters" *ngIf="!isCollapsed" class="filter-options">
<li
@@ -67,14 +75,6 @@
</ng-container>
</span>
</li>
<li class="filter-option">
<span class="filter-buttons">
<a href="#" routerLink="/create-organization" class="filter-button">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
&nbsp;{{ "newOrganization" | i18n }}
</a>
</span>
</li>
</ul>
</ng-container>
<ng-container *ngSwitchCase="'singleOrganizationAndPersonalOwnershipPolicies'">
@@ -110,6 +110,15 @@
>
&nbsp;{{ organizationGrouping.name | i18n }}
</button>
<a
href="#"
routerLink="/create-organization"
class="text-muted ml-auto create-organization-link"
appA11yTitle="{{ 'newOrganization' | i18n }}"
*ngIf="!(displayMode === 'singleOrganizationPolicy')"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
</a>
</div>
<ul id="organization-filters" *ngIf="!isCollapsed" class="filter-options">
<li class="filter-option" [ngClass]="{ active: activeFilter.myVaultOnly }">
@@ -140,14 +149,6 @@
</ng-container>
</span>
</li>
<li class="filter-option" *ngIf="!(displayMode === 'singleOrganizationPolicy')">
<span class="filter-buttons">
<a href="#" routerLink="/create-organization" class="filter-button">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
&nbsp;{{ "newOrganization" | i18n }}
</a>
</span>
</li>
</ul>
</ng-container>
</ng-container>

View File

@@ -15,7 +15,9 @@
}"
></i>
</button>
<h3>&nbsp;{{ "types" | i18n }}</h3>
<h3>
{{ "types" | i18n }}
</h3>
</div>
<ul id="type-filters" *ngIf="!isCollapsed" class="filter-options">
<li
@@ -24,14 +26,14 @@
>
<span class="filter-buttons">
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.Login)">
<i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i>&nbsp;{{ "typeLogin" | i18n }}
<i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i>{{ "typeLogin" | i18n }}
</button>
</span>
</li>
<li class="filter-option" [ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.Card }">
<span class="filter-buttons">
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.Card)">
<i class="bwi bwi-fw bwi-credit-card" aria-hidden="true"></i>&nbsp;{{ "typeCard" | i18n }}
<i class="bwi bwi-fw bwi-credit-card" aria-hidden="true"></i>{{ "typeCard" | i18n }}
</button>
</span>
</li>
@@ -41,7 +43,7 @@
>
<span class="filter-buttons">
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.Identity)">
<i class="bwi bwi-fw bwi-id-card" aria-hidden="true"></i>&nbsp;{{ "typeIdentity" | i18n }}
<i class="bwi bwi-fw bwi-id-card" aria-hidden="true"></i>{{ "typeIdentity" | i18n }}
</button>
</span>
</li>
@@ -51,9 +53,7 @@
>
<span class="filter-buttons">
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.SecureNote)">
<i class="bwi bwi-fw bwi-sticky-note" aria-hidden="true"></i>&nbsp;{{
"typeSecureNote" | i18n
}}
<i class="bwi bwi-fw bwi-sticky-note" aria-hidden="true"></i>{{ "typeSecureNote" | i18n }}
</button>
</span>
</li>

View File

@@ -1,28 +0,0 @@
import { Component, Input } from "@angular/core";
import { Organization } from "jslib-common/models/domain/organization";
import { VaultFilterComponent } from "./vault-filter.component";
@Component({
selector: "app-organization-vault-filter",
templateUrl: "vault-filter.component.html",
})
export class OrganizationVaultFilterComponent extends VaultFilterComponent {
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

@@ -27,6 +27,7 @@
appAutofocus
/>
<app-organization-filter
*ngIf="showOrgFilter"
[hide]="hideOrganizations"
[activeFilter]="activeFilter"
[collapsedFilterNodes]="collapsedFilterNodes"
@@ -38,7 +39,7 @@
></app-organization-filter>
<div class="filter">
<app-status-filter
[hideFavorites]="hideFavorites"
[hideFavorites]="!showFavorites"
[hideTrash]="hideTrash"
[activeFilter]="activeFilter"
(onFilterChange)="applyFilter($event)"
@@ -54,7 +55,7 @@
</div>
<div class="filter">
<app-folder-filter
[hide]="hideFolders"
[hide]="!showFolders"
[activeFilter]="activeFilter"
[collapsedFilterNodes]="collapsedFilterNodes"
[folderNodes]="folders"

View File

@@ -1,22 +1,26 @@
import { Component, EventEmitter, Output } from "@angular/core";
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { VaultFilterComponent as BaseVaultFilterComponent } from "jslib-angular/modules/vault-filter/vault-filter.component";
import { VaultFilterService } from "./vault-filter.service";
import { VaultFilterService } from "jslib-angular/modules/vault-filter/vault-filter.service";
import { Organization } from "jslib-common/models/domain/organization";
@Component({
selector: "app-vault-filter",
templateUrl: "vault-filter.component.html",
})
export class VaultFilterComponent extends BaseVaultFilterComponent {
@Input() showOrgFilter = true;
@Input() showFolders = true;
@Input() showFavorites = true;
@Output() onSearchTextChanged = new EventEmitter<string>();
searchPlaceholder: string;
searchText = "";
constructor(protected vaultFilterService: VaultFilterService) {
// This empty constructor is required to provide the web vaultFilterService subclass to super()
// TODO: refactor this to use proper dependency injection
organization: Organization;
constructor(vaultFilterService: VaultFilterService) {
super(vaultFilterService);
}
@@ -33,4 +37,8 @@ export class VaultFilterComponent extends BaseVaultFilterComponent {
this.activeSingleOrganizationPolicy =
await this.vaultFilterService.checkForSingleOrganizationPolicy();
}
async initCollections() {
return await this.vaultFilterService.buildCollections(this.organization?.id);
}
}

View File

@@ -1,17 +1,22 @@
import { NgModule } from "@angular/core";
import { VaultFilterService } from "jslib-angular/modules/vault-filter/vault-filter.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CollectionService } from "jslib-common/abstractions/collection.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SharedModule } from "../shared.module";
import { CollectionFilterComponent } from "./components/collection-filter.component";
import { FolderFilterComponent } from "./components/folder-filter.component";
import { LinkSsoComponent } from "./components/link-sso.component";
import { OrganizationFilterComponent } from "./components/organization-filter.component";
import { OrganizationOptionsComponent } from "./components/organization-options.component";
import { StatusFilterComponent } from "./components/status-filter.component";
import { TypeFilterComponent } from "./components/type-filter.component";
import { OrganizationVaultFilterComponent } from "./organization-vault-filter.component";
import { VaultFilterComponent } from "./vault-filter.component";
import { VaultFilterService } from "./vault-filter.service";
@NgModule({
imports: [SharedModule],
@@ -23,10 +28,21 @@ import { VaultFilterService } from "./vault-filter.service";
OrganizationOptionsComponent,
StatusFilterComponent,
TypeFilterComponent,
OrganizationVaultFilterComponent,
LinkSsoComponent,
],
exports: [VaultFilterComponent, OrganizationVaultFilterComponent],
providers: [VaultFilterService],
exports: [VaultFilterComponent],
providers: [
{
provide: VaultFilterService,
useClass: VaultFilterService,
deps: [
StateService,
OrganizationService,
FolderService,
CipherService,
CollectionService,
PolicyService,
],
},
],
})
export class VaultFilterModule {}

View File

@@ -1,54 +1,3 @@
import { Injectable } from "@angular/core";
import { DynamicTreeNode } from "jslib-angular/modules/vault-filter/models/dynamic-tree-node.model";
import { VaultFilterService as BaseVaultFilterService } from "jslib-angular/modules/vault-filter/vault-filter.service";
import { ApiService } from "jslib-common/abstractions/api.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CollectionService } from "jslib-common/abstractions/collection.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { CollectionData } from "jslib-common/models/data/collectionData";
import { Collection } from "jslib-common/models/domain/collection";
import { CollectionDetailsResponse } from "jslib-common/models/response/collectionResponse";
import { CollectionView } from "jslib-common/models/view/collectionView";
@Injectable()
export class VaultFilterService extends BaseVaultFilterService {
constructor(
stateService: StateService,
organizationService: OrganizationService,
folderService: FolderService,
cipherService: CipherService,
collectionService: CollectionService,
policyService: PolicyService,
protected apiService: ApiService
) {
super(
stateService,
organizationService,
folderService,
cipherService,
collectionService,
policyService
);
}
async buildAdminCollections(organizationId: string) {
let result: CollectionView[] = [];
const collectionResponse = await this.apiService.getCollections(organizationId);
if (collectionResponse?.data != null && collectionResponse.data.length) {
const collectionDomains = collectionResponse.data.map(
(r: CollectionDetailsResponse) => new Collection(new CollectionData(r))
);
result = await this.collectionService.decryptMany(collectionDomains);
}
const nestedCollections = await this.collectionService.getAllNested(result);
return new DynamicTreeNode<CollectionView>({
fullList: result,
nestedList: nestedCollections,
});
}
}
export class VaultFilterService extends BaseVaultFilterService {}

View File

@@ -4,12 +4,15 @@
<div class="groupings">
<div class="content">
<div class="inner-content">
<app-organization-vault-filter
<app-vault-filter
#vaultFilter
[showFolders]="false"
[showFavorites]="false"
[activeFilter]="activeFilter"
[showOrgFilter]="false"
(onFilterChange)="applyVaultFilter($event)"
(onSearchTextChanged)="filterSearchText($event)"
></app-organization-vault-filter>
></app-vault-filter>
</div>
</div>
</div>

View File

@@ -29,7 +29,7 @@ import { AddEditComponent } from "../../../../organizations/vault/add-edit.compo
import { AttachmentsComponent } from "../../../../organizations/vault/attachments.component";
import { CiphersComponent } from "../../../../organizations/vault/ciphers.component";
import { CollectionsComponent } from "../../../../organizations/vault/collections.component";
import { OrganizationVaultFilterComponent } from "../../../vault-filter/organization-vault-filter.component";
import { VaultFilterComponent } from "../../../vault-filter/vault-filter.component";
import { VaultService } from "../../vault.service";
const BroadcasterSubscriptionId = "OrgVaultComponent";
@@ -39,8 +39,7 @@ const BroadcasterSubscriptionId = "OrgVaultComponent";
templateUrl: "organization-vault.component.html",
})
export class OrganizationVaultComponent implements OnInit, OnDestroy {
@ViewChild("vaultFilter", { static: true })
vaultFilterComponent: OrganizationVaultFilterComponent;
@ViewChild("vaultFilter", { static: true }) vaultFilterComponent: VaultFilterComponent;
@ViewChild(CiphersComponent, { static: true }) ciphersComponent: CiphersComponent;
@ViewChild("attachments", { read: ViewContainerRef, static: true })
attachmentsModalRef: ViewContainerRef;
@@ -58,11 +57,6 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
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,
@@ -101,7 +95,11 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
case "syncCompleted":
if (message.successfully) {
await Promise.all([
this.vaultFilterComponent.reloadCollectionsAndFolders(),
this.vaultFilterComponent.reloadCollectionsAndFolders(
new VaultFilter({
selectedOrganizationId: this.organization.id,
} as Partial<VaultFilter>)
),
this.ciphersComponent.refresh(),
]);
this.changeDetectorRef.detectChanges();
@@ -111,12 +109,9 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
});
});
}
if (!this.firstLoaded) {
await this.vaultFilterComponent.reloadCollectionsAndFolders();
}
this.firstLoaded = false;
await this.vaultFilterComponent.reloadCollectionsAndFolders(
new VaultFilter({ selectedOrganizationId: this.organization.id } as Partial<VaultFilter>)
);
await this.ciphersComponent.reload();
if (qParams.viewEvents != null) {

View File

@@ -20,8 +20,9 @@ import {
import { ListResponse } from "jslib-common/models/response/listResponse";
import { CollectionView } from "jslib-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

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

View File

@@ -14,62 +14,50 @@
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
[formGroup]="formData"
>
<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 || !canManageBilling"
/>
</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 || !canManageBilling"
/>
</div>
<div class="form-group">
<label for="identifier">{{ "identifier" | i18n }}</label>
<input
id="identifier"
class="form-control"
type="text"
name="Identifier"
[(ngModel)]="org.identifier"
/>
</div>
<bit-form-field>
<bit-label>{{ "organizationName" | i18n }}</bit-label>
<input bitInput type="text" formControlName="name" />
</bit-form-field>
<bit-form-field>
<bit-label>{{ "billingEmail" | i18n }}</bit-label>
<input bitInput type="email" formControlName="billingEmail" />
</bit-form-field>
<bit-form-field>
<bit-label>{{ "businessName" | i18n }}</bit-label>
<input bitInput type="text" formControlName="businessName" />
</bit-form-field>
<bit-form-field>
<bit-label>{{ "identifier" | i18n }}</bit-label>
<input bitInput type="text" formControlName="identifier" />
</bit-form-field>
</div>
<div class="col-6">
<app-avatar data="{{ org.name }}" dynamic="true" size="75" fontSize="35"></app-avatar>
<app-avatar
[data]="formData.get('name').value"
dynamic="true"
size="75"
fontSize="35"
></app-avatar>
</div>
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<button
type="submit"
bit-button
buttonType="primary"
class="btn-submit"
[disabled]="form.loading"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span>
</button>
<bit-error-summary *ngIf="showErrorSummary" [formGroup]="formData"></bit-error-summary>
</form>
<ng-container *ngIf="canUseApi">
<div class="secondary-header border-0 mb-0">

View File

@@ -1,4 +1,5 @@
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { ModalService } from "jslib-angular/services/modal.service";
@@ -41,10 +42,19 @@ export class AccountComponent {
org: OrganizationResponse;
formPromise: Promise<any>;
taxFormPromise: Promise<any>;
showErrorSummary = false;
private organizationId: string;
formData = this.formBuilder.group({
name: ["", [Validators.required]],
billingEmail: ["", [Validators.required, Validators.email]],
businessName: [],
identifier: [],
});
constructor(
private formBuilder: FormBuilder,
private modalService: ModalService,
private apiService: ApiService,
private i18nService: I18nService,
@@ -60,6 +70,12 @@ export class AccountComponent {
async ngOnInit() {
this.selfHosted = this.platformUtilsService.isSelfHost();
if (this.selfHosted) {
this.formData.disable();
} else {
this.formData.enable();
}
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
this.canManageBilling = (
@@ -68,6 +84,13 @@ export class AccountComponent {
try {
this.org = await this.apiService.getOrganization(this.organizationId);
this.canUseApi = this.org.useApi;
this.formData.setValue({
name: this.org.name,
billingEmail: this.org.billingEmail,
businessName: this.org.businessName,
identifier: this.org.identifier,
});
} catch (e) {
this.logService.error(e);
}
@@ -76,12 +99,19 @@ export class AccountComponent {
}
async submit() {
this.formData.markAllAsTouched();
this.showErrorSummary = true;
if (!this.formData.valid) {
return;
}
try {
const request = new OrganizationUpdateRequest();
request.name = this.org.name;
request.businessName = this.org.businessName;
request.billingEmail = this.org.billingEmail;
request.identifier = this.org.identifier;
request.name = this.formData.get("name").value;
request.businessName = this.formData.get("businessName").value;
request.billingEmail = this.formData.get("billingEmail").value;
request.identifier = this.formData.get("identifier").value;
// Backfill pub/priv key if necessary
if (!this.org.hasPublicAndPrivateKeys) {

View File

@@ -1,6 +1,7 @@
import { NgModule } from "@angular/core";
import { LooseComponentsModule } from "./modules/loose-components.module";
import { OrganizationManageModule } from "./modules/organizations/manage/organization-manage.module";
import { PipesModule } from "./modules/pipes/pipes.module";
import { SharedModule } from "./modules/shared.module";
import { VaultFilterModule } from "./modules/vault-filter/vault-filter.module";
@@ -13,6 +14,7 @@ import { OrganizationBadgeModule } from "./modules/vault/modules/organization-ba
VaultFilterModule,
OrganizationBadgeModule,
PipesModule,
OrganizationManageModule,
],
exports: [LooseComponentsModule, VaultFilterModule, OrganizationBadgeModule, PipesModule],
bootstrap: [],

View File

@@ -307,9 +307,6 @@ export class EventService {
case EventType.Organization_DisabledKeyConnector:
msg = humanReadableMsg = this.i18nService.t("disabledKeyConnector");
break;
case EventType.Organization_SponsorshipsSynced:
msg = humanReadableMsg = this.i18nService.t("sponsorshipsSynced");
break;
// Policies
case EventType.Policy_Updated: {
msg = this.i18nService.t("modifiedPolicyId", this.formatPolicyId(ev));

View File

@@ -40,13 +40,11 @@ export class GeneratorComponent extends BaseGeneratorComponent {
route,
window
);
if (platformUtilsService.isSelfHost()) {
// Cannot use Firefox Relay on self hosted web vaults due to CORS issues with Firefox Relay API
this.forwardOptions.splice(
this.forwardOptions.findIndex((o) => o.value === "firefoxrelay"),
1
);
}
// Cannot use Firefox Relay on the web vault yet due to CORS issues with Firefox Relay API
this.forwardOptions.splice(
this.forwardOptions.findIndex((o) => o.value === "firefoxrelay"),
1
);
}
async history() {

View File

@@ -4674,8 +4674,8 @@
"removeSponsorshipSuccess": {
"message": "Sponsorship Removed"
},
"ssoKeyConnectorError": {
"message": "Key Connector error: make sure Key Connector is available and working correctly."
"ssoKeyConnectorUnavailable": {
"message": "Unable to reach the Key Connector, try again later."
},
"keyConnectorUrl": {
"message": "Key Connector URL"
@@ -5041,9 +5041,6 @@
"message": "Last Sync",
"Description": "Used as a prefix to indicate the last time a sync occured. Example \"Last sync 1968-11-16 00:00:00\""
},
"sponsorshipsSynced": {
"message": "Self-hosted sponsorships synced."
},
"billingManagedByProvider": {
"message": "Managed by $PROVIDER$",
"placeholders": {
@@ -5069,5 +5066,20 @@
},
"apiAccessToken": {
"message": "API Access Token"
},
"inputRequired": {
"message": "Input is required."
},
"inputEmail": {
"message": "Input is not an email-address."
},
"fieldsNeedAttention": {
"message": "$COUNT$ field(s) above need your attention.",
"placeholders": {
"count": {
"content": "$1",
"example": "4"
}
}
}
}

View File

@@ -14,6 +14,14 @@
font-size: $font-size-base;
}
a.create-organization-link {
&:hover {
@include themify($themes) {
color: themed("iconHover") !important;
}
}
}
button {
@extend .no-btn;
}

View File

@@ -204,8 +204,60 @@ const devServer =
return [
{
key: "Content-Security-Policy",
value:
"default-src 'self'; script-src 'self' 'sha256-ryoU+5+IUZTuUyTElqkrQGBJXr1brEv6r2CA62WUw8w=' https://js.stripe.com https://js.braintreegateway.com https://www.paypalobjects.com; style-src 'self' https://assets.braintreegateway.com https://*.paypal.com 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=' 'sha256-JVRXyYPueLWdwGwY9m/7u4QlZ1xeQdqUj2t8OVIzZE4='; img-src 'self' data: https://icons.bitwarden.net https://*.paypal.com https://www.paypalobjects.com https://q.stripe.com https://haveibeenpwned.com https://www.gravatar.com; child-src 'self' https://js.stripe.com https://assets.braintreegateway.com https://*.paypal.com https://*.duosecurity.com; frame-src 'self' https://js.stripe.com https://assets.braintreegateway.com https://*.paypal.com https://*.duosecurity.com; connect-src 'self' wss://notifications.bitwarden.com https://notifications.bitwarden.com https://cdn.bitwarden.net https://api.pwnedpasswords.com https://2fa.directory/api/v3/totp.json https://api.stripe.com https://www.paypal.com https://api.braintreegateway.com https://client-analytics.braintreegateway.com https://*.braintree-api.com https://*.blob.core.windows.net https://app.simplelogin.io/api/alias/random/new https://app.anonaddy.com/api/v1/aliases; object-src 'self' blob:;",
value: `
default-src 'self';
script-src
'self'
'sha256-ryoU+5+IUZTuUyTElqkrQGBJXr1brEv6r2CA62WUw8w='
https://js.stripe.com
https://js.braintreegateway.com
https://www.paypalobjects.com;
style-src
'self'
https://assets.braintreegateway.com
https://*.paypal.com
'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='
'sha256-JVRXyYPueLWdwGwY9m/7u4QlZ1xeQdqUj2t8OVIzZE4=';
'sha256-0xHKHIT3+e2Gknxsm/cpErSprhL+o254L/y5bljg74U='
img-src
'self'
data:
https://icons.bitwarden.net
https://*.paypal.com
https://www.paypalobjects.com
https://q.stripe.com
https://haveibeenpwned.com
https://www.gravatar.com;
child-src
'self'
https://js.stripe.com
https://assets.braintreegateway.com
https://*.paypal.com
https://*.duosecurity.com;
frame-src
'self'
https://js.stripe.com
https://assets.braintreegateway.com
https://*.paypal.com
https://*.duosecurity.com;
connect-src
'self'
wss://notifications.bitwarden.com
https://notifications.bitwarden.com
https://cdn.bitwarden.net
https://api.pwnedpasswords.com
https://2fa.directory/api/v3/totp.json
https://api.stripe.com
https://www.paypal.com
https://api.braintreegateway.com
https://client-analytics.braintreegateway.com
https://*.braintree-api.com
https://*.blob.core.windows.net
https://app.simplelogin.io/api/alias/random/new
https://app.anonaddy.com/api/v1/aliases;
object-src
'self'
blob:;`,
},
];
}