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

Compare commits

...

36 Commits

Author SHA1 Message Date
Thomas Rittson
35034c2ced Revert jslib 2022-05-20 15:26:04 +10:00
Thomas Rittson
27977ff4fe Revert jslib 2022-05-20 15:25:24 +10:00
Thomas Rittson
2ba01990a2 Revert jslib 2022-05-20 15:22:10 +10:00
Thomas Rittson
b45d249a7d Update to use new VaultFilter model 2022-05-20 15:22:10 +10:00
Thomas Rittson
48508ef87a Remove unneeded subclass method 2022-05-20 15:22:10 +10:00
Thomas Rittson
bcfc09a173 Revert "Get all collections from server if required"
This reverts commit eae38e2f9c.
2022-05-20 15:22:10 +10:00
Thomas Rittson
ddcd6d1137 Remove unused code 2022-05-20 15:22:10 +10:00
Thomas Rittson
7c3f0320e7 Autoresolve dependencies 2022-05-20 15:22:10 +10:00
Thomas Rittson
b3f9136aea Get all collections from server if required 2022-05-20 15:22:10 +10:00
André Filipe da Silva Bispo
eab478da0c PS-502: Remove extraneous comma from web vault footer (#1697)
- removed comma from footer files

Co-authored-by: André Bispo <abispo@bitwarden.com>
2022-05-19 18:30:38 +01:00
Matt Gibson
5a78853de5 [PS-655] Add Organization_SponsorshipsSynced event type. (#1696)
* Add `Organization_SponsorshipsSynced` event type.

Update events display to handle events triggered by installations rather than users

* Update jslib
2022-05-19 10:28:26 -05:00
Robyn MacCallum
b4ddce1da2 Make spacing match other rows in vault filter (#1695)
* Make spacing match other rows in vault filter

* Add spaces in headings
2022-05-18 10:12:01 -04:00
Addison Beck
6ee47f0057 [fix] Remove function implementation that was moved to jslib (#1692)
* [fix] Remove function implementation that was moved to jslib

* [dep] Update jslib
2022-05-17 21:08:39 -04: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
45 changed files with 296 additions and 176 deletions

View File

@@ -22,9 +22,9 @@ const routes: Routes = [
component: ManageComponent,
canActivate: [PermissionsGuard],
data: {
permissions: [
NavigationPermissionsService.getPermissions("manage").concat(Permissions.ManageSso),
],
permissions: NavigationPermissionsService.getPermissions("manage").concat(
Permissions.ManageSso
),
},
children: [
{

2
jslib

Submodule jslib updated: 1370006f6e...3cb94623e2

View File

@@ -55,7 +55,7 @@ export class LockComponent extends BaseLockComponent {
await super.ngOnInit();
this.onSuccessfulSubmit = async () => {
const previousUrl = this.routerService.getPreviousUrl();
if (previousUrl !== "/" && previousUrl.indexOf("lock") === -1) {
if (previousUrl && previousUrl !== "/" && previousUrl.indexOf("lock") === -1) {
this.successRoute = previousUrl;
}
this.router.navigateByUrl(this.successRoute);

View File

@@ -74,7 +74,7 @@ export class LoginComponent extends BaseLoginComponent {
if (qParams.premium != null) {
this.routerService.setPreviousUrl("/settings/premium");
} else if (qParams.org != null) {
const route = this.router.createUrlTree(["settings/create-organization"], {
const route = this.router.createUrlTree(["create-organization"], {
queryParams: { plan: qParams.org },
});
this.routerService.setPreviousUrl(route.toString());

View File

@@ -71,7 +71,7 @@ export class RegisterComponent extends BaseRegisterComponent {
} else if (qParams.org != null) {
this.showCreateOrgMessage = true;
this.referenceData.flow = qParams.org;
const route = this.router.createUrlTree(["settings/create-organization"], {
const route = this.router.createUrlTree(["create-organization"], {
queryParams: { plan: qParams.org },
});
this.routerService.setPreviousUrl(route.toString());

View File

@@ -122,17 +122,20 @@ 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: user != null ? user.name : this.i18nService.t("unknown"),
userName: r.installationId != null ? `Installation: ${r.installationId}` : userName,
userEmail: user != null ? user.email : "",
date: r.date,
ip: r.ipAddress,
type: r.type,
installationId: r.installationId,
});
})
);

View File

@@ -58,7 +58,7 @@
</li>
<bit-menu-divider></bit-menu-divider>
<li class="tw-list-none" role="none">
<a bit-menu-item routerLink="/settings/create-organization">
<a bit-menu-item routerLink="/create-organization">
<i class="bwi bwi-plus mr-2"></i>
{{ "newOrganization" | i18n }}</a
>

View File

@@ -1,6 +1,6 @@
<div class="container footer text-muted">
<div class="row">
<div class="col">&copy; {{ year }}, Bitwarden Inc.</div>
<div class="col">&copy; {{ year }} Bitwarden Inc.</div>
<div class="col text-center"></div>
<div class="col text-right">
{{ "versionNumber" | i18n: version }}

View File

@@ -1,5 +1,5 @@
<router-outlet></router-outlet>
<div class="container my-5 text-muted text-center">
&copy; {{ year }}, Bitwarden Inc. <br />
&copy; {{ year }} Bitwarden Inc. <br />
{{ "versionNumber" | i18n: version }}
</div>

View File

@@ -38,7 +38,7 @@
<li>
<button
[bitMenuTriggerFor]="accountMenu"
class="tw-border-0 tw-bg-transparent tw-text-contrast tw-opacity-70 hover:tw-opacity-90"
class="tw-border-0 tw-bg-transparent tw-text-alt2 tw-opacity-70 hover:tw-opacity-90"
>
<i class="bwi bwi-user-circle bwi-lg" aria-hidden="true"></i>
<i class="bwi bwi-caret-down bwi-sm" aria-hidden="true"></i>

View File

@@ -1,5 +1,6 @@
import { Component, OnInit } from "@angular/core";
import { Component, NgZone, OnInit } from "@angular/core";
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
@@ -31,7 +32,9 @@ export class NavbarComponent implements OnInit {
private providerService: ProviderService,
private syncService: SyncService,
private organizationService: OrganizationService,
private i18nService: I18nService
private i18nService: I18nService,
private broadcasterService: BroadcasterService,
private ngZone: NgZone
) {
this.selfHosted = this.platformUtilsService.isSelfHost();
}
@@ -49,8 +52,24 @@ export class NavbarComponent implements OnInit {
}
this.providers = await this.providerService.getAll();
this.organizations = await this.buildOrganizations();
this.broadcasterService.subscribe(this.constructor.name, async (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case "organizationCreated":
if (this.organizations.length < 1) {
this.organizations = await this.buildOrganizations();
}
break;
}
});
});
}
async buildOrganizations() {
const allOrgs = await this.organizationService.getAll();
this.organizations = allOrgs
return allOrgs
.filter((org) => OrgNavigationPermissionsService.canAccessAdmin(org))
.sort(Utils.getSortFunction(this.i18nService, "name"));
}

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";
@@ -245,7 +244,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
OrgCollectionAddEditComponent,
OrgCollectionsComponent,
OrgEntityEventsComponent,
OrgEntityUsersComponent,
OrgEventsComponent,
OrgExportComponent,
OrgExposedPasswordsReportComponent,
@@ -406,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

@@ -16,7 +16,7 @@
aria-hidden="true"
></i>
</button>
<h3 class="filter-title">{{ collectionsGrouping.name | i18n }}</h3>
<h3 class="filter-title">&nbsp;{{ collectionsGrouping.name | i18n }}</h3>
</div>
<ul id="collection-filters" *ngIf="!isCollapsed(collectionsGrouping)" class="filter-options">
<ng-template #recursiveCollections let-collections>
@@ -51,7 +51,7 @@
class="bwi bwi-collection bwi-fw"
aria-hidden="true"
></i
>{{ c.node.name }}
>&nbsp;{{ c.node.name }}
</button>
</span>
<ul

View File

@@ -1,4 +1,4 @@
<ng-container *ngIf="!hide && !activeFilter.selectedOrganizationId">
<ng-container *ngIf="!hide">
<div class="filter-heading">
<button
class="toggle-button"
@@ -16,9 +16,7 @@
}"
></i>
</button>
<h3 class="filter-title">
{{ "folders" | i18n }}
</h3>
<h3 class="filter-title">&nbsp;{{ "folders" | i18n }}</h3>
<button
class="text-muted ml-auto add-button"
(click)="addFolder()"
@@ -56,7 +54,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
>{{ f.node.name }}
>&nbsp;{{ f.node.name }}
</button>
<button
class="edit-button"

View File

@@ -12,7 +12,7 @@
</li>
<li class="filter-option">
<span class="filter-buttons">
<a href="#" routerLink="/settings/create-organization" class="filter-button">
<a href="#" routerLink="/create-organization" class="filter-button">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "newOrganization" | i18n }}
</a>
@@ -47,9 +47,9 @@
</button>
<a
href="#"
routerLink="/settings/create-organization"
routerLink="/create-organization"
class="text-muted ml-auto create-organization-link"
appA11yTitle="{{ 'addOrganization' | i18n }}"
appA11yTitle="{{ 'newOrganization' | i18n }}"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
</a>
@@ -85,7 +85,7 @@
</button>
</div>
</ng-container>
<ng-container *ngSwitchCase="'organizationMember'">
<ng-container *ngSwitchDefault>
<div class="filter-heading">
<button
class="toggle-button"
@@ -112,9 +112,10 @@
</button>
<a
href="#"
routerLink="/settings/create-organization"
routerLink="/create-organization"
class="text-muted ml-auto create-organization-link"
appA11yTitle="{{ 'addOrganization' | i18n }}"
appA11yTitle="{{ 'newOrganization' | i18n }}"
*ngIf="!(displayMode === 'singleOrganizationPolicy')"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
</a>

View File

@@ -82,6 +82,7 @@ export class OrganizationOptionsComponent {
this.platformUtilsService.showToast("success", null, "Unlinked SSO");
await this.load();
} catch (e) {
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message);
this.logService.error(e);
}
}
@@ -106,6 +107,7 @@ export class OrganizationOptionsComponent {
this.platformUtilsService.showToast("success", null, this.i18nService.t("leftOrganization"));
await this.load();
} catch (e) {
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message);
this.logService.error(e);
}
}
@@ -173,6 +175,7 @@ export class OrganizationOptionsComponent {
this.platformUtilsService.showToast("success", null, this.i18nService.t(toastStringRef));
await this.load();
} catch (e) {
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message);
this.logService.error(e);
}
}

View File

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

View File

@@ -27,14 +27,4 @@ export class VaultFilterComponent extends BaseVaultFilterComponent {
searchTextChanged() {
this.onSearchTextChanged.emit(this.searchText);
}
// This method exists because the vault component gets its data mixed up during the initial sync on first login. It looks for data before the sync is complete.
// It should be removed as soon as doing so makes sense.
async reloadOrganizations() {
this.organizations = await this.vaultFilterService.buildOrganizations();
}
async initCollections() {
return await this.vaultFilterService.buildCollections(this.organization?.id);
}
}

View File

@@ -1,12 +1,6 @@
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";
@@ -30,19 +24,6 @@ import { VaultFilterComponent } from "./vault-filter.component";
TypeFilterComponent,
],
exports: [VaultFilterComponent],
providers: [
{
provide: VaultFilterService,
useClass: VaultFilterService,
deps: [
StateService,
OrganizationService,
FolderService,
CipherService,
CollectionService,
PolicyService,
],
},
],
providers: [VaultFilterService],
})
export class VaultFilterModule {}

View File

@@ -1,3 +0,0 @@
import { VaultFilterService as BaseVaultFilterService } from "jslib-angular/modules/vault-filter/vault-filter.service";
export class VaultFilterService extends BaseVaultFilterService {}

View File

@@ -32,19 +32,26 @@
</small>
</h1>
<div class="ml-auto d-flex">
<app-vault-bulk-actions [ciphersComponent]="ciphersComponent" [deleted]="deleted">
<app-vault-bulk-actions
[ciphersComponent]="ciphersComponent"
[deleted]="activeFilter.status === 'trash'"
>
</app-vault-bulk-actions>
<button
type="button"
class="btn btn-outline-primary btn-sm"
(click)="addCipher()"
*ngIf="!deleted"
*ngIf="activeFilter.status !== 'trash'"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "addItem" | i18n }}
</button>
</div>
</div>
<app-callout type="warning" *ngIf="deleted" icon="bwi-exclamation-triangle">
<app-callout
type="warning"
*ngIf="activeFilter.status === 'trash'"
icon="bwi-exclamation-triangle"
>
{{ trashCleanupWarning }}
</app-callout>
<app-vault-ciphers
@@ -95,7 +102,10 @@
</div>
<div class="card-body">
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
<a class="btn btn-block btn-outline-secondary" routerLink="/settings/premium">
<a
class="btn btn-block btn-outline-secondary"
routerLink="/settings/subscription/premium"
>
{{ "goPremium" | i18n }}
</a>
</div>

View File

@@ -209,7 +209,7 @@ export class IndividualVaultComponent implements OnInit, OnDestroy {
cipherPassesFilter = cipher.type === this.activeFilter.cipherType;
}
if (
this.activeFilter.selectedFolderId != null &&
this.activeFilter.selectedFolder &&
this.activeFilter.selectedFolderId != "none" &&
cipherPassesFilter
) {

View File

@@ -98,6 +98,7 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
this.vaultFilterComponent.reloadCollectionsAndFolders(
new VaultFilter({
selectedOrganizationId: this.organization.id,
useAdminCollections: this.organization.canEditAnyCollection,
} as Partial<VaultFilter>)
),
this.ciphersComponent.refresh(),
@@ -110,7 +111,10 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
});
}
await this.vaultFilterComponent.reloadCollectionsAndFolders(
new VaultFilter({ selectedOrganizationId: this.organization.id } as Partial<VaultFilter>)
new VaultFilter({
selectedOrganizationId: this.organization.id,
useAdminCollections: this.organization.canEditAnyCollection,
} as Partial<VaultFilter>)
);
await this.ciphersComponent.reload();
@@ -123,7 +127,11 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
this.route.queryParams.subscribe(async (params) => {
if (params.cipherId) {
if ((await this.cipherService.get(params.cipherId)) != null) {
if (
// Handle users with implicit collection access since they use the admin endpoint
this.organization.canEditAnyCollection ||
(await this.cipherService.get(params.cipherId)) != null
) {
this.editCipherId(params.cipherId);
} else {
this.platformUtilsService.showToast(
@@ -168,7 +176,7 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
cipherPassesFilter = cipher.type === this.activeFilter.cipherType;
}
if (
this.activeFilter.selectedFolderId != null &&
this.activeFilter.selectedFolder != null &&
this.activeFilter.selectedFolderId != "none" &&
cipherPassesFilter
) {

View File

@@ -1,5 +1,5 @@
import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, CanActivate, Router } from "@angular/router";
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
@@ -17,7 +17,7 @@ export class PermissionsGuard implements CanActivate {
private syncService: SyncService
) {}
async canActivate(route: ActivatedRouteSnapshot) {
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
// TODO: We need to fix this issue once and for all.
if ((await this.syncService.getLastSync()) == null) {
await this.syncService.fullSync(false);
@@ -39,6 +39,16 @@ export class PermissionsGuard implements CanActivate {
const permissions = route.data == null ? [] : (route.data.permissions as Permissions[]);
if (permissions != null && !org.hasAnyPermission(permissions)) {
// Handle linkable ciphers for organizations the user only has view access to
// https://bitwarden.atlassian.net/browse/EC-203
if (state.root.queryParamMap.has("cipherId")) {
return this.router.createUrlTree(["/vault"], {
queryParams: {
cipherId: state.root.queryParamMap.get("cipherId"),
},
});
}
this.platformUtilsService.showToast("error", null, this.i18nService.t("accessDenied"));
return this.router.createUrlTree(["/"]);
}

View File

@@ -1,3 +1,4 @@
<app-navbar></app-navbar>
<div class="org-nav" *ngIf="organization">
<div class="container d-flex">
<div class="d-flex flex-column">
@@ -35,3 +36,4 @@
</div>
</div>
<router-outlet></router-outlet>
<app-footer></app-footer>

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,7 +14,7 @@ import { CipherView } from "jslib-common/models/view/cipherView";
import { ExposedPasswordsReportComponent as BaseExposedPasswordsReportComponent } from "../../reports/exposed-passwords-report.component";
@Component({
selector: "app-exposed-passwords-report",
selector: "app-org-exposed-passwords-report",
templateUrl: "../../reports/exposed-passwords-report.component.html",
})
export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportComponent {
@@ -41,12 +41,10 @@ export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportC
}
ngOnInit() {
const dynamicSuper = Object.getPrototypeOf(this.constructor.prototype);
this.route.parent.parent.params.subscribe(async (params) => {
this.organization = await this.organizationService.get(params.organizationId);
this.manageableCiphers = await this.cipherService.getAll();
// TODO: We should do something about this, calling super in an async function is bad
dynamicSuper.ngOnInit();
await this.checkAccess();
});
}

View File

@@ -155,6 +155,11 @@ const routes: Routes = [
.IndividualVaultModule,
},
{ path: "sends", component: SendComponent, data: { title: "Send" } },
{
path: "create-organization",
component: CreateOrganizationComponent,
data: { titleId: "newOrganization" },
},
{
path: "settings",
component: SettingsComponent,
@@ -181,11 +186,6 @@ const routes: Routes = [
loadChildren: async () =>
(await import("./settings/subscription-routing.module")).SubscriptionRoutingModule,
},
{
path: "create-organization",
component: CreateOrganizationComponent,
data: { titleId: "newOrganization" },
},
{
path: "emergency-access",
children: [
@@ -229,15 +229,15 @@ const routes: Routes = [
(await import("./reports/reports-routing.module")).ReportsRoutingModule,
},
{ path: "setup/families-for-enterprise", component: FamiliesForEnterpriseSetupComponent },
{
path: "organizations",
loadChildren: () =>
import("./organizations/organization-routing.module").then(
(m) => m.OrganizationsRoutingModule
),
},
],
},
{
path: "organizations",
loadChildren: () =>
import("./organizations/organization-routing.module").then(
(m) => m.OrganizationsRoutingModule
),
},
];
@NgModule({

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,6 +307,9 @@ 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

@@ -68,6 +68,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
if (await this.keyConnectorService.getUsesKeyConnector()) {
this.router.navigate(["/settings/security/two-factor"]);
}
await super.ngOnInit();
}
async rotateEncKeyClicked() {

View File

@@ -1,5 +1,11 @@
<div class="page-header">
<h1>{{ "newOrganization" | i18n }}</h1>
<div class="container page-content">
<div class="row">
<div class="col-12">
<div class="page-header">
<h1>{{ "newOrganization" | i18n }}</h1>
</div>
<p>{{ "newOrganizationDesc" | i18n }}</p>
<app-organization-plans></app-organization-plans>
</div>
</div>
</div>
<p>{{ "newOrganizationDesc" | i18n }}</p>
<app-organization-plans></app-organization-plans>

View File

@@ -5,6 +5,7 @@ import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
@@ -68,7 +69,8 @@ export class OrganizationPlansComponent implements OnInit {
private syncService: SyncService,
private policyService: PolicyService,
private organizationService: OrganizationService,
private logService: LogService
private logService: LogService,
private messagingService: MessagingService
) {
this.selfHosted = platformUtilsService.isSelfHost();
}
@@ -298,6 +300,7 @@ export class OrganizationPlansComponent implements OnInit {
this.formPromise = doSubmit();
const organizationId = await this.formPromise;
this.onSuccess.emit({ organizationId: organizationId });
this.messagingService.send("organizationCreated", organizationId);
} catch (e) {
this.logService.error(e);
}

View File

@@ -54,7 +54,11 @@ export class SettingsComponent implements OnInit, OnDestroy {
this.premium = await this.tokenService.getPremium();
this.hasFamilySponsorshipAvailable = await this.organizationService.canManageSponsorships();
const hasPremiumFromOrg = await this.stateService.getCanAccessPremium();
const billing = await this.apiService.getUserBillingHistory();
this.hideSubscription = !this.premium && hasPremiumFromOrg && billing.hasNoHistory;
let billing = null;
if (!this.selfHosted) {
billing = await this.apiService.getUserBillingHistory();
}
this.hideSubscription =
!this.premium && hasPremiumFromOrg && (this.selfHosted || billing?.hasNoHistory);
}
}

View File

@@ -8,6 +8,7 @@
<td class="table-action-right">
<div class="dropdown" appListDropdown>
<button
*ngIf="!sponsoringOrg.familySponsorshipToDelete"
class="btn btn-outline-secondary dropdown-toggle"
type="button"
id="dropdownMenuButton"
@@ -21,7 +22,7 @@
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<button
#resendEmailBtn
*ngIf="!isSelfHosted"
*ngIf="!isSelfHosted && !sponsoringOrg.familySponsorshipValidUntil"
[appApiAction]="resendEmailPromise"
class="dropdown-item btn-submit"
[disabled]="resendEmailBtn.loading"

View File

@@ -73,10 +73,14 @@ export class TaxInfoComponent {
this.logService.error(e);
}
} else {
const taxInfo = await this.apiService.getTaxInfo();
if (taxInfo) {
this.taxInfo.postalCode = taxInfo.postalCode;
this.taxInfo.country = taxInfo.country || "US";
try {
const taxInfo = await this.apiService.getTaxInfo();
if (taxInfo) {
this.taxInfo.postalCode = taxInfo.postalCode;
this.taxInfo.country = taxInfo.country || "US";
}
} catch (e) {
this.logService.error(e);
}
}
this.pristine = Object.assign({}, this.taxInfo);
@@ -86,9 +90,16 @@ export class TaxInfoComponent {
}
});
const taxRates = await this.apiService.getTaxRates();
this.taxRates = taxRates.data;
this.loading = false;
try {
const taxRates = await this.apiService.getTaxRates();
if (taxRates) {
this.taxRates = taxRates.data;
}
} catch (e) {
this.logService.error(e);
} finally {
this.loading = false;
}
}
get taxRate() {

View File

@@ -295,16 +295,6 @@
(blur)="saveUsernameOptions()"
/>
</div>
<div class="form-group col-4">
<label for="simplelogin-hostname">{{ "hostname" | i18n }}</label>
<input
id="simplelogin-hostname"
class="form-control"
type="text"
[(ngModel)]="usernameOptions.forwardedSimpleLoginHostname"
(blur)="saveUsernameOptions()"
/>
</div>
</div>
<div class="row" *ngIf="usernameOptions.forwardedService === 'anonaddy'">
<div class="form-group col-4">

View File

@@ -77,7 +77,7 @@
</button>
<a
href="#"
routerLink="/settings/create-organization"
routerLink="/create-organization"
class="btn btn-primary"
*ngIf="!organizations || !organizations.length"
>

View File

@@ -4669,7 +4669,7 @@
"message": "Email Sent"
},
"revokeSponsorshipConfirmation": {
"message": "After removing this account, the Families organization owner will be responsible for this subscription and related invoices. Are you sure you want to continue?"
"message": "After removing this account, the Families plan sponsorship will expire at the end of the billing period. You will not be able to redeem a new sponsorship offer until the existing one expires. Are you sure you want to continue?"
},
"removeSponsorshipSuccess": {
"message": "Sponsorship Removed"
@@ -5005,7 +5005,7 @@
"message": "Service"
},
"unknownCipher": {
"message": "Unknown Item, you may need to login with another account to access this item."
"message": "Unknown Item, you may need to request permission to access this item."
},
"cannotSponsorSelf": {
"message": "You cannot redeem for the active account. Enter a different email."
@@ -5041,6 +5041,9 @@
"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": {

View File

@@ -116,6 +116,7 @@
}
text-decoration: none;
}
max-width: 90%;
}
.edit-button {

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:;`,
},
];
}