1
0
mirror of https://github.com/bitwarden/web synced 2025-12-10 13:23:15 +00:00

Compare commits

...

25 Commits

Author SHA1 Message Date
gbubemismith
63aa95d55b Revert "[ps-136] Igonre accented characters in vault search (#1690)"
This reverts commit 58d9ac5ebc.
2022-05-24 23:33:38 +01:00
gbubemismith
eab8908206 Merge branch 'master' into bug/ps-136-search-accented-characters 2022-05-19 19:04:55 +01: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
gbubemismith
51ee899c5e Merge branch 'master' into bug/ps-136-search-accented-characters 2022-05-19 15:27:05 +01:00
gbubemismith
26065d3542 updated jslib 2022-05-19 15:26:31 +01: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
gbubemismith
5094a710ac Merge branch 'master' into bug/ps-136-search-accented-characters 2022-05-17 14:17:18 +01:00
gbubemismith
67ebf88837 removed accented character from serach input field 2022-05-17 14:16:43 +01: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
32 changed files with 213 additions and 114 deletions

2
jslib

Submodule jslib updated: 1370006f6e...3cb94623e2

View File

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

View File

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

@@ -1,6 +1,6 @@
<div class="container footer text-muted"> <div class="container footer text-muted">
<div class="row"> <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-center"></div>
<div class="col text-right"> <div class="col text-right">
{{ "versionNumber" | i18n: version }} {{ "versionNumber" | i18n: version }}

View File

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

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

@@ -16,7 +16,7 @@
aria-hidden="true" aria-hidden="true"
></i> ></i>
</button> </button>
<h3 class="filter-title">{{ collectionsGrouping.name | i18n }}</h3> <h3 class="filter-title">&nbsp;{{ 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>
@@ -51,7 +51,7 @@
class="bwi bwi-collection bwi-fw" class="bwi bwi-collection bwi-fw"
aria-hidden="true" aria-hidden="true"
></i ></i
>{{ c.node.name }} >&nbsp;{{ c.node.name }}
</button> </button>
</span> </span>
<ul <ul

View File

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

View File

@@ -85,7 +85,7 @@
</button> </button>
</div> </div>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'organizationMember'"> <ng-container *ngSwitchDefault>
<div class="filter-heading"> <div class="filter-heading">
<button <button
class="toggle-button" class="toggle-button"
@@ -115,6 +115,7 @@
routerLink="/create-organization" routerLink="/create-organization"
class="text-muted ml-auto create-organization-link" class="text-muted ml-auto create-organization-link"
appA11yTitle="{{ 'newOrganization' | i18n }}" appA11yTitle="{{ 'newOrganization' | i18n }}"
*ngIf="!(displayMode === 'singleOrganizationPolicy')"
> >
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i> <i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
</a> </a>

View File

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

View File

@@ -15,9 +15,7 @@
}" }"
></i> ></i>
</button> </button>
<h3> <h3>&nbsp;{{ "types" | i18n }}</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
@@ -26,14 +24,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>{{ "typeLogin" | i18n }} <i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i>&nbsp;{{ "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>{{ "typeCard" | i18n }} <i class="bwi bwi-fw bwi-credit-card" aria-hidden="true"></i>&nbsp;{{ "typeCard" | i18n }}
</button> </button>
</span> </span>
</li> </li>
@@ -43,7 +41,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>{{ "typeIdentity" | i18n }} <i class="bwi bwi-fw bwi-id-card" aria-hidden="true"></i>&nbsp;{{ "typeIdentity" | i18n }}
</button> </button>
</span> </span>
</li> </li>
@@ -53,7 +51,9 @@
> >
<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>{{ "typeSecureNote" | i18n }} <i class="bwi bwi-fw bwi-sticky-note" aria-hidden="true"></i>&nbsp;{{
"typeSecureNote" | i18n
}}
</button> </button>
</span> </span>
</li> </li>

View File

@@ -28,12 +28,6 @@ export class VaultFilterComponent extends BaseVaultFilterComponent {
this.onSearchTextChanged.emit(this.searchText); 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() { async initCollections() {
return await this.vaultFilterService.buildCollections(this.organization?.id); return await this.vaultFilterService.buildCollections(this.organization?.id);
} }

View File

@@ -32,19 +32,26 @@
</small> </small>
</h1> </h1>
<div class="ml-auto d-flex"> <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> </app-vault-bulk-actions>
<button <button
type="button" type="button"
class="btn btn-outline-primary btn-sm" class="btn btn-outline-primary btn-sm"
(click)="addCipher()" (click)="addCipher()"
*ngIf="!deleted" *ngIf="activeFilter.status !== 'trash'"
> >
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "addItem" | i18n }} <i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "addItem" | i18n }}
</button> </button>
</div> </div>
</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 }} {{ trashCleanupWarning }}
</app-callout> </app-callout>
<app-vault-ciphers <app-vault-ciphers

View File

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

View File

@@ -123,7 +123,11 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
this.route.queryParams.subscribe(async (params) => { this.route.queryParams.subscribe(async (params) => {
if (params.cipherId) { 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); this.editCipherId(params.cipherId);
} else { } else {
this.platformUtilsService.showToast( this.platformUtilsService.showToast(
@@ -168,7 +172,7 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
cipherPassesFilter = cipher.type === this.activeFilter.cipherType; cipherPassesFilter = cipher.type === this.activeFilter.cipherType;
} }
if ( if (
this.activeFilter.selectedFolderId != null && this.activeFilter.selectedFolder != null &&
this.activeFilter.selectedFolderId != "none" && this.activeFilter.selectedFolderId != "none" &&
cipherPassesFilter cipherPassesFilter
) { ) {

View File

@@ -1,5 +1,5 @@
import { Injectable } from "@angular/core"; 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 { I18nService } from "jslib-common/abstractions/i18n.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service"; import { OrganizationService } from "jslib-common/abstractions/organization.service";
@@ -17,7 +17,7 @@ export class PermissionsGuard implements CanActivate {
private syncService: SyncService private syncService: SyncService
) {} ) {}
async canActivate(route: ActivatedRouteSnapshot) { async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
// TODO: We need to fix this issue once and for all. // TODO: We need to fix this issue once and for all.
if ((await this.syncService.getLastSync()) == null) { if ((await this.syncService.getLastSync()) == null) {
await this.syncService.fullSync(false); await this.syncService.fullSync(false);
@@ -39,6 +39,16 @@ export class PermissionsGuard implements CanActivate {
const permissions = route.data == null ? [] : (route.data.permissions as Permissions[]); const permissions = route.data == null ? [] : (route.data.permissions as Permissions[]);
if (permissions != null && !org.hasAnyPermission(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")); this.platformUtilsService.showToast("error", null, this.i18nService.t("accessDenied"));
return this.router.createUrlTree(["/"]); return this.router.createUrlTree(["/"]);
} }

View File

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

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

View File

@@ -229,15 +229,15 @@ const routes: Routes = [
(await import("./reports/reports-routing.module")).ReportsRoutingModule, (await import("./reports/reports-routing.module")).ReportsRoutingModule,
}, },
{ path: "setup/families-for-enterprise", component: FamiliesForEnterpriseSetupComponent }, { 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({ @NgModule({

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

@@ -54,7 +54,11 @@ export class SettingsComponent implements OnInit, OnDestroy {
this.premium = await this.tokenService.getPremium(); this.premium = await this.tokenService.getPremium();
this.hasFamilySponsorshipAvailable = await this.organizationService.canManageSponsorships(); this.hasFamilySponsorshipAvailable = await this.organizationService.canManageSponsorships();
const hasPremiumFromOrg = await this.stateService.getCanAccessPremium(); const hasPremiumFromOrg = await this.stateService.getCanAccessPremium();
const billing = await this.apiService.getUserBillingHistory(); let billing = null;
this.hideSubscription = !this.premium && hasPremiumFromOrg && billing.hasNoHistory; 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"> <td class="table-action-right">
<div class="dropdown" appListDropdown> <div class="dropdown" appListDropdown>
<button <button
*ngIf="!sponsoringOrg.familySponsorshipToDelete"
class="btn btn-outline-secondary dropdown-toggle" class="btn btn-outline-secondary dropdown-toggle"
type="button" type="button"
id="dropdownMenuButton" id="dropdownMenuButton"
@@ -21,7 +22,7 @@
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton"> <div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<button <button
#resendEmailBtn #resendEmailBtn
*ngIf="!isSelfHosted" *ngIf="!isSelfHosted && !sponsoringOrg.familySponsorshipValidUntil"
[appApiAction]="resendEmailPromise" [appApiAction]="resendEmailPromise"
class="dropdown-item btn-submit" class="dropdown-item btn-submit"
[disabled]="resendEmailBtn.loading" [disabled]="resendEmailBtn.loading"

View File

@@ -295,16 +295,6 @@
(blur)="saveUsernameOptions()" (blur)="saveUsernameOptions()"
/> />
</div> </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>
<div class="row" *ngIf="usernameOptions.forwardedService === 'anonaddy'"> <div class="row" *ngIf="usernameOptions.forwardedService === 'anonaddy'">
<div class="form-group col-4"> <div class="form-group col-4">

View File

@@ -4669,7 +4669,7 @@
"message": "Email Sent" "message": "Email Sent"
}, },
"revokeSponsorshipConfirmation": { "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": { "removeSponsorshipSuccess": {
"message": "Sponsorship Removed" "message": "Sponsorship Removed"
@@ -5005,7 +5005,7 @@
"message": "Service" "message": "Service"
}, },
"unknownCipher": { "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": { "cannotSponsorSelf": {
"message": "You cannot redeem for the active account. Enter a different email." "message": "You cannot redeem for the active account. Enter a different email."
@@ -5041,6 +5041,9 @@
"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": {

View File

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

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