1
0
mirror of https://github.com/bitwarden/web synced 2025-12-10 21:33:16 +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 name: Setup
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
outputs: outputs:
release_version: ${{ steps.version.outputs.version }} release_version: ${{ steps.version.outputs.package }}
tag_version: ${{ steps.version.outputs.version }} tag_version: ${{ steps.version.outputs.tag }}
branch_name: ${{ steps.branch.outputs.branch_name }} branch_name: ${{ steps.branch.outputs.branch_name }}
steps: steps:
- name: Branch check - name: Branch check
@@ -38,11 +38,20 @@ jobs:
- name: Check Release Version - name: Check Release Version
id: version id: version
uses: bitwarden/gh-actions/release-version-check@ea9fab01d76940267b4147cc1c4542431246b9f6 run: |
with: version=$( jq -r ".version" package.json)
release-type: ${{ github.event.inputs.release_type }} previous_release_tag_version=$(
project-type: ts curl -sL https://api.github.com/repos/$GITHUB_REPOSITORY/releases/latest | jq -r ".tag_name"
file: package.json )
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 - name: Get branch name
id: branch id: branch

2
jslib

Submodule jslib updated: 6f117b9901...3bf25edd3e

4
package-lock.json generated
View File

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

View File

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

View File

@@ -122,20 +122,17 @@ export abstract class BaseEventsComponent {
const userId = r.actingUserId == null ? r.userId : r.actingUserId; const userId = r.actingUserId == null ? r.userId : r.actingUserId;
const eventInfo = await this.eventService.getEventInfo(r); const eventInfo = await this.eventService.getEventInfo(r);
const user = this.getUserName(r, userId); const user = this.getUserName(r, userId);
const userName = user != null ? user.name : this.i18nService.t("unknown");
return new EventView({ return new EventView({
message: eventInfo.message, message: eventInfo.message,
humanReadableMessage: eventInfo.humanReadableMessage, humanReadableMessage: eventInfo.humanReadableMessage,
appIcon: eventInfo.appIcon, appIcon: eventInfo.appIcon,
appName: eventInfo.appName, appName: eventInfo.appName,
userId: userId, userId: userId,
userName: r.installationId != null ? `Installation: ${r.installationId}` : userName, userName: user != null ? user.name : this.i18nService.t("unknown"),
userEmail: user != null ? user.email : "", userEmail: user != null ? user.email : "",
date: r.date, date: r.date,
ip: r.ipAddress, ip: r.ipAddress,
type: r.type, 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 { CollectionAddEditComponent as OrgCollectionAddEditComponent } from "../organizations/manage/collection-add-edit.component";
import { CollectionsComponent as OrgManageCollectionsComponent } from "../organizations/manage/collections.component"; import { CollectionsComponent as OrgManageCollectionsComponent } from "../organizations/manage/collections.component";
import { EntityEventsComponent as OrgEntityEventsComponent } from "../organizations/manage/entity-events.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 { EventsComponent as OrgEventsComponent } from "../organizations/manage/events.component";
import { GroupAddEditComponent as OrgGroupAddEditComponent } from "../organizations/manage/group-add-edit.component"; import { GroupAddEditComponent as OrgGroupAddEditComponent } from "../organizations/manage/group-add-edit.component";
import { GroupsComponent as OrgGroupsComponent } from "../organizations/manage/groups.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 { EmergencyAccessViewComponent } from "../settings/emergency-access-view.component";
import { EmergencyAccessComponent } from "../settings/emergency-access.component"; import { EmergencyAccessComponent } from "../settings/emergency-access.component";
import { EmergencyAddEditComponent } from "../settings/emergency-add-edit.component"; import { EmergencyAddEditComponent } from "../settings/emergency-add-edit.component";
import { LinkSsoComponent } from "../settings/link-sso.component";
import { OrganizationPlansComponent } from "../settings/organization-plans.component"; import { OrganizationPlansComponent } from "../settings/organization-plans.component";
import { PaymentMethodComponent } from "../settings/payment-method.component"; import { PaymentMethodComponent } from "../settings/payment-method.component";
import { PaymentComponent } from "../settings/payment.component"; import { PaymentComponent } from "../settings/payment.component";
@@ -223,6 +223,7 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
HintComponent, HintComponent,
ImportComponent, ImportComponent,
InactiveTwoFactorReportComponent, InactiveTwoFactorReportComponent,
LinkSsoComponent,
LockComponent, LockComponent,
LoginComponent, LoginComponent,
MasterPasswordPolicyComponent, MasterPasswordPolicyComponent,
@@ -243,7 +244,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
OrgCollectionAddEditComponent, OrgCollectionAddEditComponent,
OrgCollectionsComponent, OrgCollectionsComponent,
OrgEntityEventsComponent, OrgEntityEventsComponent,
OrgEntityUsersComponent,
OrgEventsComponent, OrgEventsComponent,
OrgExportComponent, OrgExportComponent,
OrgExposedPasswordsReportComponent, OrgExposedPasswordsReportComponent,
@@ -383,6 +383,7 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
HintComponent, HintComponent,
ImportComponent, ImportComponent,
InactiveTwoFactorReportComponent, InactiveTwoFactorReportComponent,
LinkSsoComponent,
LockComponent, LockComponent,
LoginComponent, LoginComponent,
MasterPasswordPolicyComponent, MasterPasswordPolicyComponent,
@@ -403,7 +404,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
OrgCollectionAddEditComponent, OrgCollectionAddEditComponent,
OrgCollectionsComponent, OrgCollectionsComponent,
OrgEntityEventsComponent, OrgEntityEventsComponent,
OrgEntityUsersComponent,
OrgEventsComponent, OrgEventsComponent,
OrgExportComponent, OrgExportComponent,
OrgExposedPasswordsReportComponent, OrgExposedPasswordsReportComponent,

View File

@@ -29,52 +29,52 @@
></i> ></i>
<span class="sr-only">{{ "loading" | i18n }}</span> <span class="sr-only">{{ "loading" | i18n }}</span>
</div> </div>
<div <cdk-virtual-scroll-viewport
class="modal-body" itemSize="46"
*ngIf=" minBufferPx="600"
!loading && users && (users | search: searchText:'name':'email':'id') as searchedUsers maxBufferPx="1200"
" [style]="scrollViewportStyle"
> >
<div class="d-flex"> <div class="modal-body" *ngIf="!loading && users && searchedUsers">
<div class="mr-3"> <div class="d-flex">
<label class="sr-only" for="search">{{ "search" | i18n }}</label> <div class="mr-3">
<input <label class="sr-only" for="search">{{ "search" | i18n }}</label>
type="search" <input
class="form-control form-control-sm" type="search"
id="search" class="form-control form-control-sm"
placeholder="{{ 'search' | i18n }}" id="search"
name="SearchText" placeholder="{{ 'search' | i18n }}"
[(ngModel)]="searchText" 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>
<div class="btn-group btn-group-sm" role="group"> <ng-container *ngIf="!searchedUsers.length">
<button <hr />
type="button" {{ "noUsersInList" | i18n }}
class="btn btn-outline-secondary" </ng-container>
[ngClass]="{ active: !showSelected }" <table class="table table-hover table-list mb-0" [hidden]="!searchedUsers.length">
(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">
<thead> <thead>
<tr> <tr>
<th>&nbsp;</th> <th>&nbsp;</th>
@@ -91,7 +91,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let u of searchedUsers"> <tr *cdkVirtualFor="let u of searchedUsers" class="">
<td class="table-list-checkbox" (click)="check(u)"> <td class="table-list-checkbox" (click)="check(u)">
<input <input
type="checkbox" type="checkbox"
@@ -164,8 +164,8 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
</ng-container> </div>
</div> </cdk-virtual-scroll-viewport>
<div class="modal-footer"> <div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"> <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> <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 { 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 { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service"; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service"; import { LogService } from "jslib-common/abstractions/log.service";
@@ -13,6 +14,7 @@ import { OrganizationUserUserDetailsResponse } from "jslib-common/models/respons
@Component({ @Component({
selector: "app-entity-users", selector: "app-entity-users",
templateUrl: "entity-users.component.html", templateUrl: "entity-users.component.html",
providers: [SearchPipe],
}) })
export class EntityUsersComponent implements OnInit { export class EntityUsersComponent implements OnInit {
@Input() entity: "group" | "collection"; @Input() entity: "group" | "collection";
@@ -33,6 +35,7 @@ export class EntityUsersComponent implements OnInit {
private allUsers: OrganizationUserUserDetailsResponse[] = []; private allUsers: OrganizationUserUserDetailsResponse[] = [];
constructor( constructor(
private search: SearchPipe,
private apiService: ApiService, private apiService: ApiService,
private i18nService: I18nService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, 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() { async loadUsers() {
const users = await this.apiService.getOrganizationUsers(this.organizationId); const users = await this.apiService.getOrganizationUsers(this.organizationId);
this.allUsers = users.data.map((r) => r).sort(Utils.getSortFunction(this.i18nService, "email")); 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 { NgModule } from "@angular/core";
import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { RouterModule } from "@angular/router"; 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 { InfiniteScrollModule } from "ngx-infinite-scroll";
import { ToastrModule } from "ngx-toastr"; import { ToastrModule } from "ngx-toastr";
@@ -126,6 +132,7 @@ registerLocaleData(localeZhTw, "zh-TW");
BadgeModule, BadgeModule,
ButtonModule, ButtonModule,
MenuModule, MenuModule,
FormFieldModule,
], ],
exports: [ exports: [
CommonModule, CommonModule,
@@ -136,6 +143,7 @@ registerLocaleData(localeZhTw, "zh-TW");
ReactiveFormsModule, ReactiveFormsModule,
RouterModule, RouterModule,
BadgeModule, BadgeModule,
FormFieldModule,
ButtonModule, ButtonModule,
CalloutModule, CalloutModule,
ToastrModule, ToastrModule,

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,9 @@
}" }"
></i> ></i>
</button> </button>
<h3>&nbsp;{{ "types" | i18n }}</h3> <h3>
{{ "types" | i18n }}
</h3>
</div> </div>
<ul id="type-filters" *ngIf="!isCollapsed" class="filter-options"> <ul id="type-filters" *ngIf="!isCollapsed" class="filter-options">
<li <li
@@ -24,14 +26,14 @@
> >
<span class="filter-buttons"> <span class="filter-buttons">
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.Login)"> <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> </button>
</span> </span>
</li> </li>
<li class="filter-option" [ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.Card }"> <li class="filter-option" [ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.Card }">
<span class="filter-buttons"> <span class="filter-buttons">
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.Card)"> <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> </button>
</span> </span>
</li> </li>
@@ -41,7 +43,7 @@
> >
<span class="filter-buttons"> <span class="filter-buttons">
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.Identity)"> <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> </button>
</span> </span>
</li> </li>
@@ -51,9 +53,7 @@
> >
<span class="filter-buttons"> <span class="filter-buttons">
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.SecureNote)"> <button class="filter-button" (click)="applyFilter(cipherTypeEnum.SecureNote)">
<i class="bwi bwi-fw bwi-sticky-note" aria-hidden="true"></i>&nbsp;{{ <i class="bwi bwi-fw bwi-sticky-note" aria-hidden="true"></i>{{ "typeSecureNote" | i18n }}
"typeSecureNote" | i18n
}}
</button> </button>
</span> </span>
</li> </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 appAutofocus
/> />
<app-organization-filter <app-organization-filter
*ngIf="showOrgFilter"
[hide]="hideOrganizations" [hide]="hideOrganizations"
[activeFilter]="activeFilter" [activeFilter]="activeFilter"
[collapsedFilterNodes]="collapsedFilterNodes" [collapsedFilterNodes]="collapsedFilterNodes"
@@ -38,7 +39,7 @@
></app-organization-filter> ></app-organization-filter>
<div class="filter"> <div class="filter">
<app-status-filter <app-status-filter
[hideFavorites]="hideFavorites" [hideFavorites]="!showFavorites"
[hideTrash]="hideTrash" [hideTrash]="hideTrash"
[activeFilter]="activeFilter" [activeFilter]="activeFilter"
(onFilterChange)="applyFilter($event)" (onFilterChange)="applyFilter($event)"
@@ -54,7 +55,7 @@
</div> </div>
<div class="filter"> <div class="filter">
<app-folder-filter <app-folder-filter
[hide]="hideFolders" [hide]="!showFolders"
[activeFilter]="activeFilter" [activeFilter]="activeFilter"
[collapsedFilterNodes]="collapsedFilterNodes" [collapsedFilterNodes]="collapsedFilterNodes"
[folderNodes]="folders" [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 { VaultFilterComponent as BaseVaultFilterComponent } from "jslib-angular/modules/vault-filter/vault-filter.component";
import { VaultFilterService } from "jslib-angular/modules/vault-filter/vault-filter.service";
import { VaultFilterService } from "./vault-filter.service"; import { Organization } from "jslib-common/models/domain/organization";
@Component({ @Component({
selector: "app-vault-filter", selector: "app-vault-filter",
templateUrl: "vault-filter.component.html", templateUrl: "vault-filter.component.html",
}) })
export class VaultFilterComponent extends BaseVaultFilterComponent { export class VaultFilterComponent extends BaseVaultFilterComponent {
@Input() showOrgFilter = true;
@Input() showFolders = true;
@Input() showFavorites = true;
@Output() onSearchTextChanged = new EventEmitter<string>(); @Output() onSearchTextChanged = new EventEmitter<string>();
searchPlaceholder: string; searchPlaceholder: string;
searchText = ""; searchText = "";
constructor(protected vaultFilterService: VaultFilterService) { organization: Organization;
// This empty constructor is required to provide the web vaultFilterService subclass to super()
// TODO: refactor this to use proper dependency injection constructor(vaultFilterService: VaultFilterService) {
super(vaultFilterService); super(vaultFilterService);
} }
@@ -33,4 +37,8 @@ export class VaultFilterComponent extends BaseVaultFilterComponent {
this.activeSingleOrganizationPolicy = this.activeSingleOrganizationPolicy =
await this.vaultFilterService.checkForSingleOrganizationPolicy(); 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 { 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 { SharedModule } from "../shared.module";
import { CollectionFilterComponent } from "./components/collection-filter.component"; import { CollectionFilterComponent } from "./components/collection-filter.component";
import { FolderFilterComponent } from "./components/folder-filter.component"; import { FolderFilterComponent } from "./components/folder-filter.component";
import { LinkSsoComponent } from "./components/link-sso.component";
import { OrganizationFilterComponent } from "./components/organization-filter.component"; import { OrganizationFilterComponent } from "./components/organization-filter.component";
import { OrganizationOptionsComponent } from "./components/organization-options.component"; import { OrganizationOptionsComponent } from "./components/organization-options.component";
import { StatusFilterComponent } from "./components/status-filter.component"; import { StatusFilterComponent } from "./components/status-filter.component";
import { TypeFilterComponent } from "./components/type-filter.component"; import { TypeFilterComponent } from "./components/type-filter.component";
import { OrganizationVaultFilterComponent } from "./organization-vault-filter.component";
import { VaultFilterComponent } from "./vault-filter.component"; import { VaultFilterComponent } from "./vault-filter.component";
import { VaultFilterService } from "./vault-filter.service";
@NgModule({ @NgModule({
imports: [SharedModule], imports: [SharedModule],
@@ -23,10 +28,21 @@ import { VaultFilterService } from "./vault-filter.service";
OrganizationOptionsComponent, OrganizationOptionsComponent,
StatusFilterComponent, StatusFilterComponent,
TypeFilterComponent, TypeFilterComponent,
OrganizationVaultFilterComponent,
LinkSsoComponent,
], ],
exports: [VaultFilterComponent, OrganizationVaultFilterComponent], exports: [VaultFilterComponent],
providers: [VaultFilterService], providers: [
{
provide: VaultFilterService,
useClass: VaultFilterService,
deps: [
StateService,
OrganizationService,
FolderService,
CipherService,
CollectionService,
PolicyService,
],
},
],
}) })
export class VaultFilterModule {} 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 { 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 {}
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,
});
}
}

View File

@@ -4,12 +4,15 @@
<div class="groupings"> <div class="groupings">
<div class="content"> <div class="content">
<div class="inner-content"> <div class="inner-content">
<app-organization-vault-filter <app-vault-filter
#vaultFilter #vaultFilter
[showFolders]="false"
[showFavorites]="false"
[activeFilter]="activeFilter" [activeFilter]="activeFilter"
[showOrgFilter]="false"
(onFilterChange)="applyVaultFilter($event)" (onFilterChange)="applyVaultFilter($event)"
(onSearchTextChanged)="filterSearchText($event)" (onSearchTextChanged)="filterSearchText($event)"
></app-organization-vault-filter> ></app-vault-filter>
</div> </div>
</div> </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 { AttachmentsComponent } from "../../../../organizations/vault/attachments.component";
import { CiphersComponent } from "../../../../organizations/vault/ciphers.component"; import { CiphersComponent } from "../../../../organizations/vault/ciphers.component";
import { CollectionsComponent } from "../../../../organizations/vault/collections.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"; import { VaultService } from "../../vault.service";
const BroadcasterSubscriptionId = "OrgVaultComponent"; const BroadcasterSubscriptionId = "OrgVaultComponent";
@@ -39,8 +39,7 @@ const BroadcasterSubscriptionId = "OrgVaultComponent";
templateUrl: "organization-vault.component.html", templateUrl: "organization-vault.component.html",
}) })
export class OrganizationVaultComponent implements OnInit, OnDestroy { export class OrganizationVaultComponent implements OnInit, OnDestroy {
@ViewChild("vaultFilter", { static: true }) @ViewChild("vaultFilter", { static: true }) vaultFilterComponent: VaultFilterComponent;
vaultFilterComponent: OrganizationVaultFilterComponent;
@ViewChild(CiphersComponent, { static: true }) ciphersComponent: CiphersComponent; @ViewChild(CiphersComponent, { static: true }) ciphersComponent: CiphersComponent;
@ViewChild("attachments", { read: ViewContainerRef, static: true }) @ViewChild("attachments", { read: ViewContainerRef, static: true })
attachmentsModalRef: ViewContainerRef; attachmentsModalRef: ViewContainerRef;
@@ -58,11 +57,6 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
trashCleanupWarning: string = null; trashCleanupWarning: string = null;
activeFilter: VaultFilter = new VaultFilter(); 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( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private organizationService: OrganizationService, private organizationService: OrganizationService,
@@ -101,7 +95,11 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
case "syncCompleted": case "syncCompleted":
if (message.successfully) { if (message.successfully) {
await Promise.all([ await Promise.all([
this.vaultFilterComponent.reloadCollectionsAndFolders(), this.vaultFilterComponent.reloadCollectionsAndFolders(
new VaultFilter({
selectedOrganizationId: this.organization.id,
} as Partial<VaultFilter>)
),
this.ciphersComponent.refresh(), this.ciphersComponent.refresh(),
]); ]);
this.changeDetectorRef.detectChanges(); this.changeDetectorRef.detectChanges();
@@ -111,12 +109,9 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
}); });
}); });
} }
await this.vaultFilterComponent.reloadCollectionsAndFolders(
if (!this.firstLoaded) { new VaultFilter({ selectedOrganizationId: this.organization.id } as Partial<VaultFilter>)
await this.vaultFilterComponent.reloadCollectionsAndFolders(); );
}
this.firstLoaded = false;
await this.ciphersComponent.reload(); await this.ciphersComponent.reload();
if (qParams.viewEvents != null) { if (qParams.viewEvents != null) {

View File

@@ -20,8 +20,9 @@ import {
import { ListResponse } from "jslib-common/models/response/listResponse"; import { ListResponse } from "jslib-common/models/response/listResponse";
import { CollectionView } from "jslib-common/models/view/collectionView"; 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 { CollectionAddEditComponent } from "./collection-add-edit.component";
import { EntityUsersComponent } from "./entity-users.component";
@Component({ @Component({
selector: "app-org-manage-collections", 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 { Utils } from "jslib-common/misc/utils";
import { GroupResponse } from "jslib-common/models/response/groupResponse"; 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"; import { GroupAddEditComponent } from "./group-add-edit.component";
@Component({ @Component({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4674,8 +4674,8 @@
"removeSponsorshipSuccess": { "removeSponsorshipSuccess": {
"message": "Sponsorship Removed" "message": "Sponsorship Removed"
}, },
"ssoKeyConnectorError": { "ssoKeyConnectorUnavailable": {
"message": "Key Connector error: make sure Key Connector is available and working correctly." "message": "Unable to reach the Key Connector, try again later."
}, },
"keyConnectorUrl": { "keyConnectorUrl": {
"message": "Key Connector URL" "message": "Key Connector URL"
@@ -5041,9 +5041,6 @@
"message": "Last Sync", "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\"" "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": { "billingManagedByProvider": {
"message": "Managed by $PROVIDER$", "message": "Managed by $PROVIDER$",
"placeholders": { "placeholders": {
@@ -5069,5 +5066,20 @@
}, },
"apiAccessToken": { "apiAccessToken": {
"message": "API Access Token" "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; font-size: $font-size-base;
} }
a.create-organization-link {
&:hover {
@include themify($themes) {
color: themed("iconHover") !important;
}
}
}
button { button {
@extend .no-btn; @extend .no-btn;
} }

View File

@@ -204,8 +204,60 @@ const devServer =
return [ return [
{ {
key: "Content-Security-Policy", key: "Content-Security-Policy",
value: 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:;", 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:;`,
}, },
]; ];
} }