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

Compare commits

...

44 Commits

Author SHA1 Message Date
github-actions[bot]
06227f27ab Bumped version to 2022.5.1 (#1727)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
(cherry picked from commit 6da0925374)
2022-06-07 11:49:51 -07:00
Addison Beck
66391b4376 [fix] Pull org admin vault collections directly from the API (#1700) (#1726)
* [fix] Pull org admin vault collections directly from the API

* [refactor] Establish pattern for interfaces in module scoped services

* [fix] Remove access modifiers from constructor params for an extended service

* [fix] Rename skipFilterCollectionRefresh to firstLoaded

* [fix] Simplify hiding unwanted filters in the org vault

* Revert "[refactor] Establish pattern for interfaces in module scoped services"

This reverts commit f1edae5a3e.

* Update src/app/modules/vault-filter/vault-filter.component.ts

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>

* Update src/app/modules/vault-filter/organization-vault-filter.component.ts

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>

* [fix] Ran prettier

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
2022-06-06 13:42:57 -04:00
Kyle Spearrin
1c09629bbc Allow firefox relay service in generator (#1723)
* Allow firefox relay service in generator

* Update generator.component.ts

* Update generator.component.ts

* make pretty
2022-06-02 14:01:33 -04:00
Jake Fink
e2d9498167 add better error handling to tax call (#1666) 2022-06-02 10:02:31 -04:00
Joseph Flinn
ec80782d8f Version check typo (#1717)
* fixing dumb issue

* Fixing another dumb mistake

(cherry picked from commit ad0512e344)
2022-05-31 18:09:11 -07:00
Joseph Flinn
3acee89646 Updating the path for the Release Version Check (#1716)
(cherry picked from commit 8fe9504cd1)
2022-05-31 17:50:37 -07:00
Joseph Flinn
589faf3608 Updating the version check to the new Github action (#1714)
* Updating the version check to the new Github action

* Update .github/workflows/release.yml

Co-authored-by: Micaiah Martin <77340197+mimartin12@users.noreply.github.com>

* switching action branch tag to commit tag

Co-authored-by: Micaiah Martin <77340197+mimartin12@users.noreply.github.com>
(cherry picked from commit b8aa25b981)
2022-05-31 16:13:41 -07:00
github-actions[bot]
639cc461ae Bumped version to 2022.05.0 (#1715)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
(cherry picked from commit 5d09ddbc8d)
2022-05-31 16:13:31 -07:00
Thomas Rittson
4562920d30 Update jslib 2022-05-27 13:12:37 +10:00
Matt Gibson
c6f4e4de64 update jslib 2022-05-26 13:19:49 -04:00
Matt Gibson
f323963a4e [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

(cherry picked from commit 5a78853de5)
2022-05-26 13:06:46 -04:00
Thomas Rittson
f6c8b6d8cb [EC-177] Update Key Connector error message (#1705)
* Update Key Connector error message

* Update jslib
2022-05-26 07:32:52 +10:00
Robyn MacCallum
11f34be18e [SG-345] Switch in-line "+" add org button to more explicit version (#1703)
* switch in-line plus add org button to more explicit version

* Add spaces to line up text better
2022-05-25 10:23:43 -04:00
Robyn MacCallum
647e9555d0 Fix 'Link SSO' not appearing (#1702) 2022-05-23 19:44:54 -04:00
Matt Gibson
ad7b6e6388 Revert "[PS-655] Add Organization_SponsorshipsSynced event type. (#1696)"
This reverts commit aa29a07678.
2022-05-19 11:52:40 -04:00
Matt Gibson
718ca197b5 Revert "update jslib"
This reverts commit 338d593cd1.
2022-05-19 11:52:36 -04:00
Matt Gibson
338d593cd1 update jslib 2022-05-19 11:41:13 -04:00
Matt Gibson
aa29a07678 [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

(cherry picked from commit 5a78853de5)
2022-05-19 11:39:06 -04:00
Robyn MacCallum
0722522801 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:14:39 -04:00
Oscar Hinton
5731c6449b [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:47:21 -04:00
Robyn MacCallum
eaf083be44 Fix switcher not appearing for users affected by single org policy (#1689) 2022-05-17 10:06:20 -04:00
Robyn MacCallum
8435c85c3c [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:23:12 -04:00
Justin Baur
3d1c2dad74 Fix Defects (#1683) 2022-05-16 09:47:24 -04:00
Jake Fink
2f632f7096 change revoke text (#1684) 2022-05-16 09:15:45 -04:00
Kyle Spearrin
9b63c81b56 remove hostname from simplelogin. update jslib (#1680) 2022-05-13 15:36:48 -04:00
Addison Beck
34cb463e94 [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:17:19 -04:00
Addison Beck
7bbe3d7b28 [fix] Add max width to vault filter buttons to keep content unified (#1678) 2022-05-13 15:17:09 -04:00
Addison Beck
0564b52f1f [fix] Check policies when reloading organizations for filters (#1677) 2022-05-13 15:16:58 -04:00
Oscar Hinton
cb1f08d74b [EC-200] Handle an edge case where ciphers were not selectable (#1674)
(cherry picked from commit be30d47038)
2022-05-13 15:33:33 +02:00
Thomas Rittson
f3bf972d26 Revert accidental cnesting of org routes (#1670) 2022-05-13 08:32:26 +10:00
Oscar Hinton
02256aad87 [SG-279] Fix unlock button not working after logging in through SSO (#1672)
(cherry picked from commit f4f3e8c574)
2022-05-12 16:22:11 +02:00
Oscar Hinton
466e0c8c35 [SG-267] Exposed passwords report: console error (#1671)
(cherry picked from commit 3367736c7e)
2022-05-12 15:48:08 +02:00
Vincent Salucci
0543e76432 [bug:euvr] Self-hosted instance hiding subscription nav item (#1669) 2022-05-12 08:44:14 -04:00
Thomas Rittson
34527160fc Fix permission checking when accessing manage SSO (#1663) 2022-05-12 09:12:34 +10:00
Thomas Rittson
f590fe5435 Update Go Premium link (#1660) 2022-05-12 06:54:41 +10:00
Vincent Salucci
0f64c16c18 [bug:euvr] Change password init fix (#1667) 2022-05-11 15:44:45 -04:00
Vincent Salucci
1632a24b1e [bug:euvr] Create Organization Updates (#1664) 2022-05-11 15:44:18 -04:00
Addison Beck
0691af1dcd [fix] Update the navbar when creating an organization for the first time (#1668) 2022-05-11 14:51:05 -04:00
Robyn MacCallum
3e59617fb1 Use correct localization string (#1665) 2022-05-11 13:37:26 -04:00
Thomas Rittson
268b16ca98 [SG-234] Fix account menu icon colour (#1653) 2022-05-11 08:40:24 +10:00
Robyn MacCallum
9f977cfc68 Various small vault filter fixes (#1659) 2022-05-10 17:33:53 -04:00
Matt Gibson
9627782a04 Feature/self hosted families for enterprise (#1658)
* Remove F4E card from vault page

* Billing Sync Api Keys / Free Bitwarden Families Page updates (#1597)

* Work on billing sync key maintenance

* Work on i18n

* Work on localizations

* Work on sync status text

* Work on sync status copy

* Assign default null value

* Work on billing sync UI

* Work on sponsorships page

* Add inline form validation

* Fix lint

* Prettier

* Final touches on designs from Figma

* Polish

* Address PR feedback

* Use more generic validator

* Update jslib

* Update jslib

* Remove comment

* [PS-248] Feature/manage billing sync connection (#1601)

* Fix modal footer rounded corners

* Add billing sync messages

* Add manage billing sync to self hosted org subscription component

* Handle create vs update in component

* Update src/app/settings/billing-sync-key.component.html

Co-authored-by: Justin Baur <admin@justinbaur.com>

* update jslib

* Linter fixes

* Remove duplicate modalService

Co-authored-by: Justin Baur <admin@justinbaur.com>

* Fix/f4e self hosting section (#1616)

* Translate messages

* Move download license subform to below download license button

* Chore/merge/self hosted families for enterprise (#1617)

* [EC-142] Fix error during import of 1pux containing new email field format (#1585)

* Pull in changes made on https://github.com/bitwarden/jslib/pull/758

* Update package-lock.json

* Add descriptions to vague messages (#1586)

* Add descriptions to vague messages

* Fix typo

* Autosync the updated translations (#1590)

Co-authored-by: github-actions <>

* Bumping the pinned commit for the download-artifact to bypass the github api issue (#1598)

* Update jslib (#1602)

* Update jslib

* Update name of UserVerificationComponent

* Bumped version to 2.28.0 (#1603)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* [EC-161] Bump braintree (#1606)

* [PS-211] [PS-212] Make Generator page accessible (#1607)

* Fix grouping of radiobutton inputs

* Add role=radiogroup

* Add aria-labelledBy to radio button groups

* Add reorganization notice (#1610)

* Add aria attributes to password gen options (#1611)

* [EC-143] [BEEEP] Allow linking to ciphers (#1579)

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>

* update jslib

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Joseph Flinn <58369717+joseph-flinn@users.noreply.github.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: Oscar Hinton <oscar@oscarhinton.com>

* Chore/merge/self hosted families for enterprise (#1621)

* [EC-142] Fix error during import of 1pux containing new email field format (#1585)

* Pull in changes made on https://github.com/bitwarden/jslib/pull/758

* Update package-lock.json

* Add descriptions to vague messages (#1586)

* Add descriptions to vague messages

* Fix typo

* Autosync the updated translations (#1590)

Co-authored-by: github-actions <>

* Bumping the pinned commit for the download-artifact to bypass the github api issue (#1598)

* Update jslib (#1602)

* Update jslib

* Update name of UserVerificationComponent

* Bumped version to 2.28.0 (#1603)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* [EC-161] Bump braintree (#1606)

* [PS-211] [PS-212] Make Generator page accessible (#1607)

* Fix grouping of radiobutton inputs

* Add role=radiogroup

* Add aria-labelledBy to radio button groups

* Add reorganization notice (#1610)

* Add aria attributes to password gen options (#1611)

* [EC-143] [BEEEP] Allow linking to ciphers (#1579)

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>

* Fix login sponsorship redirect (#1620)

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Joseph Flinn <58369717+joseph-flinn@users.noreply.github.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: Oscar Hinton <oscar@oscarhinton.com>

* Change to new name (#1642)

* Chore/merge/self hosted families for enterprise (#1651)

* [EC-142] Fix error during import of 1pux containing new email field format (#1585)

* Pull in changes made on https://github.com/bitwarden/jslib/pull/758

* Update package-lock.json

* Add descriptions to vague messages (#1586)

* Add descriptions to vague messages

* Fix typo

* Autosync the updated translations (#1590)

Co-authored-by: github-actions <>

* Bumping the pinned commit for the download-artifact to bypass the github api issue (#1598)

* Update jslib (#1602)

* Update jslib

* Update name of UserVerificationComponent

* Bumped version to 2.28.0 (#1603)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* [EC-161] Bump braintree (#1606)

* [PS-211] [PS-212] Make Generator page accessible (#1607)

* Fix grouping of radiobutton inputs

* Add role=radiogroup

* Add aria-labelledBy to radio button groups

* Add reorganization notice (#1610)

* Add aria attributes to password gen options (#1611)

* [EC-143] [BEEEP] Allow linking to ciphers (#1579)

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>

* Fix login sponsorship redirect (#1620)

* Contribution Documentation edits (#1599)

Making corrections to the mobile contributions doc:

    Update Crowdin contact from Kyle to dwbit.
    Update 'User-to-User Support' forum category to 'Ask the Bitwarden Community'

* Add description for the A-Z & a-z items (#1615)

* Add description for reports message (#1600)

Add "Vault Health Reports can be used to evaluate the security of your Bitwarden Personal or Organization Vault" description to the source string, "Identify and close security gaps in your online accounts by clicking the reports below."

* [PS-301] Load OssModule from BitwardenLicense (#1626)

* Bumped version to 2.28.1 (#1629)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* [EC-154] [BEEEP] Remove factory providers in Angular DI (#1609)

* use InjectionTokens

* Use InitService

* PS-79 Updated two-factor component to to align to jslib change to send the deviceId on 2fa email resend code (#1624)

* [PS-74] Fix user authentication state checks (#1632)

* Update to use new authStatus method

* Delete unused services and import

* update jslib

* [PS-381] Fix locale being empty when not configuring a language (#1631)

* Forwarded email providers to username generator (#1628)

* forwarded emails

* firefox relay

* remove firefox relay

* update jslib ref

* remove dupe logService

* Update localization description for 'random' (#1633)

Adding description string for 'random'

* DEVOPS-758 - Move Web deploy from GitHub Pages to CloudFlare Pages (#1627)

* [EC-189] Resolve password reprompt not appearing on linkable cipher (#1643)

* [SG-220] End User Vault Refresh (#1640)

* Add premium badge component (#1525)

* [Vault Refresh] Nav update and Options -> Preferences (#1530)

* Update jslib

* [End User Vault Refresh] Security sub-page (#1538)

* [End User Vault Refresh] Security section

* Updated routing module

* Update routing for change-password

* Updated buttons of all modified classes // imported button module

* Converted modified class to use bit-callout

* removed comments

* Update small button to current cl button

* Update jslib and consequential updates

* [End User Vault Refresh] Vault - remove Org and Provider cards (#1529)

* Update reports page (#1536)

* [End User Vault Refresh] Organizations - updated nav and route permissions (#1551)

* Add Organizations link to navbar

* Update route permissions and guards

* Use NavigationPermissionsService to unify route permissions

* Rename "My Vault" to "Vaults" (#1569)

* [euvr] Adjust Vault width based on card visibility (#1588)

* [SG-31 End User Vault Refresh] Account Menu updates (#1596)

* Add menuModule

* Use bit-menu for account menu

* Fix styling, replace CSS with TW

* Change out bootstrap styling

* Fix styling

* Fix styling

* Rename My Account to Account Settings

* WIP use Avatar for account menu

* Revert "WIP use Avatar for account menu"

This reverts commit d58bea4874.

* Update jslib from feature branch

* [End User Vault Refresh] SG-16: Organization filters (#1595)

* [feature] Base implementation of EUVR filter changes

* [refactor] Relocated vault-filters to app/modules

* [refactor] Reuse vault-filters component for organizations

* [refactor] Remove unused org filter component

* [bug] .gitmodules branch change

* [bug] Load organization filters after sync during login

* [refactor] Introduce a SharedModule

* [refactor] Created a home for loose components

* [refactor] Convert VaultComponent and OrgVaultComponent into a pair of modules

* [refactor] Implement <bit-menu> for organization filter actions

* [feature] Improve a11y standards of the vault filters module

* [bug] Recreate package-lock.json

* Fix build issue

* [bug] Remove duplicate this.go() call

* [fix] Use correct filter-buttons class

Co-authored-by: addison <addisonbeck1@gmail.com>
Co-authored-by: Hinton <oscar@oscarhinton.com>

* [SG-32] Add Ownership badge to vault items (#1623)

* [feature] Base implementation of EUVR filter changes

* [refactor] Relocated vault-filters to app/modules

* [refactor] Reuse vault-filters component for organizations

* [refactor] Remove unused org filter component

* [bug] .gitmodules branch change

* [bug] Load organization filters after sync during login

* [refactor] Introduce a SharedModule

* [refactor] Created a home for loose components

* [refactor] Convert VaultComponent and OrgVaultComponent into a pair of modules

* [refactor] Implement <bit-menu> for organization filter actions

* [feature] Improve a11y standards of the vault filters module

* [bug] Recreate package-lock.json

* Fix build issue

* [bug] Remove duplicate this.go() call

* Add organization owner badge to vault items

* Fix capitalization

* Re-organize new components into modules

* Use tailwind css class

Co-authored-by: addison <addisonbeck1@gmail.com>
Co-authored-by: Hinton <oscar@oscarhinton.com>

* [EUVR] Merge master into feature branch (#1637)

* Update jslib (#1602)

* Update jslib

* Update name of UserVerificationComponent

* Bumped version to 2.28.0 (#1603)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* [EC-161] Bump braintree (#1606)

* [PS-211] [PS-212] Make Generator page accessible (#1607)

* Fix grouping of radiobutton inputs

* Add role=radiogroup

* Add aria-labelledBy to radio button groups

* Add reorganization notice (#1610)

* Add aria attributes to password gen options (#1611)

* [EC-143] [BEEEP] Allow linking to ciphers (#1579)

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>

* Fix login sponsorship redirect (#1620)

* Contribution Documentation edits (#1599)

Making corrections to the mobile contributions doc:

    Update Crowdin contact from Kyle to dwbit.
    Update 'User-to-User Support' forum category to 'Ask the Bitwarden Community'

* Add description for the A-Z & a-z items (#1615)

* Add description for reports message (#1600)

Add "Vault Health Reports can be used to evaluate the security of your Bitwarden Personal or Organization Vault" description to the source string, "Identify and close security gaps in your online accounts by clicking the reports below."

* [PS-301] Load OssModule from BitwardenLicense (#1626)

* Bumped version to 2.28.1 (#1629)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* [EC-154] [BEEEP] Remove factory providers in Angular DI (#1609)

* use InjectionTokens

* Use InitService

* PS-79 Updated two-factor component to to align to jslib change to send the deviceId on 2fa email resend code (#1624)

* [PS-74] Fix user authentication state checks (#1632)

* Update to use new authStatus method

* Delete unused services and import

* update jslib

* [PS-381] Fix locale being empty when not configuring a language (#1631)

* Forwarded email providers to username generator (#1628)

* forwarded emails

* firefox relay

* remove firefox relay

* update jslib ref

* remove dupe logService

* Update localization description for 'random' (#1633)

Adding description string for 'random'

* DEVOPS-758 - Move Web deploy from GitHub Pages to CloudFlare Pages (#1627)

* Update jslib

* Run npm i after merge with master

* Update name of UserVerificationComponent

* Fix lazy loading of routing modules

* Routing modules should have routing in their name

* Revert "Fix lazy loading of routing modules"

This reverts commit 59d4e6e06c.

* Do not eagerly load feature modules

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Oscar Hinton <oscar@oscarhinton.com>
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
Co-authored-by: dwbit <98768076+dwbit@users.noreply.github.com>
Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com>
Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com>

* Do not render org options menu until loaded (#1638)

* [SG-31 End User Vault Refresh] Update cipher options menu (#1593)

* Update Vault cipher option menus

* Update Send list to use same style

* [SG-207] [EUVR] Remove Organizations from Settings page (#1619)

* [fix] Cut off overflow text for link buttons (#1639)

* [SG-225] Remove BaseGuard (#1641)

* [SG-34 End User Vault Refresh] Organization Switcher (#1550)

* [euvr] Subscription/Billing updates (#1576)

* [euvr] Subscription changes

* Revert testing bang

* Removed final instance of getUserBilling

* Moved to feature/endUserVaultRefresh remote branch and updated to latest

* Removed org-billing changes

* Updated premium component header

* Updated stateservice path

* Updated billing component name

* Reverting org-billing decouple

* Using tailwind classes for CL objects

* Added TODO

* Removed divider for components within new tab nav

* Update jslib/add components to loose-components module

* Updated routing lazy load module name to match existing pattern

* Fixed bug with redirect // Added button type // Removed headers for tabbed pages

* Revert changes to .gitmodules

* [dep] Update jslib

Co-authored-by: Oscar Hinton <oscar@oscarhinton.com>
Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
Co-authored-by: Vincent Salucci <vincesalucci21@gmail.com>
Co-authored-by: addison <addisonbeck1@gmail.com>
Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
Co-authored-by: dwbit <98768076+dwbit@users.noreply.github.com>
Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com>
Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com>

* [EC-196] Move provider last in navbar (#1647)

* [euvr] Settings -> Account Settings label (#1648)

* Check for null cipher in edit (#1646)

Null ciphers signify a _new_ cipher, so no password repromt is required

* [fix] Add missing Create Org button to filters (#1650)

* update jslib

* Fix package-lock

* Fix component declarations

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Joseph Flinn <58369717+joseph-flinn@users.noreply.github.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: Oscar Hinton <oscar@oscarhinton.com>
Co-authored-by: dwbit <98768076+dwbit@users.noreply.github.com>
Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com>
Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com>
Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
Co-authored-by: Vincent Salucci <vincesalucci21@gmail.com>
Co-authored-by: addison <addisonbeck1@gmail.com>
Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com>

* Update jslib

* update jslib

Co-authored-by: Justin Baur <admin@justinbaur.com>
Co-authored-by: Justin Baur <136baur@gmail.com>
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Joseph Flinn <58369717+joseph-flinn@users.noreply.github.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: Oscar Hinton <oscar@oscarhinton.com>
Co-authored-by: dwbit <98768076+dwbit@users.noreply.github.com>
Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com>
Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com>
Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
Co-authored-by: Vincent Salucci <vincesalucci21@gmail.com>
Co-authored-by: addison <addisonbeck1@gmail.com>
Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com>
2022-05-10 16:11:59 -05:00
Addison Beck
c5877cd063 [fix] Remove an extra sync call (#1657) 2022-05-10 13:42:16 -04:00
Robyn MacCallum
136e8897ae Check for null cipher in edit (#1656) 2022-05-10 13:33:16 -04:00
57 changed files with 1660 additions and 1003 deletions

View File

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

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: 00deb38de5...6f117b9901

1304
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

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

@@ -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

@@ -58,6 +58,7 @@ import { SingleOrgPolicyComponent } from "../organizations/policies/single-org.c
import { TwoFactorAuthenticationPolicyComponent } from "../organizations/policies/two-factor-authentication.component";
import { AccountComponent as OrgAccountComponent } from "../organizations/settings/account.component";
import { AdjustSubscription } from "../organizations/settings/adjust-subscription.component";
import { BillingSyncApiKeyComponent } from "../organizations/settings/billing-sync-api-key.component";
import { ChangePlanComponent } from "../organizations/settings/change-plan.component";
import { DeleteOrganizationComponent } from "../organizations/settings/delete-organization.component";
import { DownloadLicenseComponent } from "../organizations/settings/download-license.component";
@@ -66,6 +67,7 @@ import { OrganizationBillingComponent } from "../organizations/settings/organiza
import { OrganizationSubscriptionComponent } from "../organizations/settings/organization-subscription.component";
import { SettingsComponent as OrgSettingComponent } from "../organizations/settings/settings.component";
import { TwoFactorSetupComponent as OrgTwoFactorSetupComponent } from "../organizations/settings/two-factor-setup.component";
import { AcceptFamilySponsorshipComponent } from "../organizations/sponsorships/accept-family-sponsorship.component";
import { FamiliesForEnterpriseSetupComponent } from "../organizations/sponsorships/families-for-enterprise-setup.component";
import { ExportComponent as OrgExportComponent } from "../organizations/tools/export.component";
import { ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent } from "../organizations/tools/exposed-passwords-report.component";
@@ -98,6 +100,7 @@ import { AddCreditComponent } from "../settings/add-credit.component";
import { AdjustPaymentComponent } from "../settings/adjust-payment.component";
import { AdjustStorageComponent } from "../settings/adjust-storage.component";
import { ApiKeyComponent } from "../settings/api-key.component";
import { BillingSyncKeyComponent } from "../settings/billing-sync-key.component";
import { ChangeEmailComponent } from "../settings/change-email.component";
import { ChangeKdfComponent } from "../settings/change-kdf.component";
import { ChangePasswordComponent } from "../settings/change-password.component";
@@ -112,7 +115,6 @@ import { EmergencyAccessTakeoverComponent } from "../settings/emergency-access-t
import { EmergencyAccessViewComponent } from "../settings/emergency-access-view.component";
import { EmergencyAccessComponent } from "../settings/emergency-access.component";
import { EmergencyAddEditComponent } from "../settings/emergency-add-edit.component";
import { LinkSsoComponent } from "../settings/link-sso.component";
import { OrganizationPlansComponent } from "../settings/organization-plans.component";
import { PaymentMethodComponent } from "../settings/payment-method.component";
import { PaymentComponent } from "../settings/payment.component";
@@ -171,6 +173,7 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
declarations: [
PremiumBadgeComponent,
AcceptEmergencyComponent,
AcceptFamilySponsorshipComponent,
AcceptOrganizationComponent,
AccessComponent,
AccountComponent,
@@ -183,6 +186,8 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
AdjustSubscription,
ApiKeyComponent,
AttachmentsComponent,
BillingSyncApiKeyComponent,
BillingSyncKeyComponent,
BreachReportComponent,
BulkActionsComponent,
BulkDeleteComponent,
@@ -218,7 +223,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
HintComponent,
ImportComponent,
InactiveTwoFactorReportComponent,
LinkSsoComponent,
LockComponent,
LoginComponent,
MasterPasswordPolicyComponent,
@@ -379,7 +383,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
HintComponent,
ImportComponent,
InactiveTwoFactorReportComponent,
LinkSsoComponent,
LockComponent,
LoginComponent,
MasterPasswordPolicyComponent,

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,9 +12,9 @@
</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 }}
&nbsp;{{ "newOrganization" | i18n }}
</a>
</span>
</li>
@@ -45,14 +45,6 @@
>
&nbsp;{{ organizationGrouping.name | i18n }}
</button>
<a
href="#"
routerLink="/settings/create-organization"
class="text-muted ml-auto create-organization-link"
appA11yTitle="{{ 'addOrganization' | i18n }}"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
</a>
</div>
<ul id="organization-filters" *ngIf="!isCollapsed" class="filter-options">
<li
@@ -75,19 +67,25 @@
</ng-container>
</span>
</li>
</ul>
</ng-container>
<ng-container *ngSwitchCase="'singleOrganizationAndPersonalOwnershipPolicies'">
<ul class="filter-options">
<li class="filter-option active">
<button class="filter-button">
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
{{ organizations[0].name }}
</button>
<li class="filter-option">
<span class="filter-buttons">
<a href="#" routerLink="/create-organization" class="filter-button">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
&nbsp;{{ "newOrganization" | i18n }}
</a>
</span>
</li>
</ul>
</ng-container>
<ng-container *ngSwitchCase="'organizationMember'">
<ng-container *ngSwitchCase="'singleOrganizationAndPersonalOwnershipPolicies'">
<div class="filter-heading">
<button class="filter-button active">
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
{{ organizations[0].name }}
</button>
</div>
</ng-container>
<ng-container *ngSwitchDefault>
<div class="filter-heading">
<button
class="toggle-button"
@@ -112,14 +110,6 @@
>
&nbsp;{{ organizationGrouping.name | i18n }}
</button>
<a
href="#"
routerLink="/settings/create-organization"
class="text-muted ml-auto create-organization-link"
appA11yTitle="{{ 'addOrganization' | i18n }}"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
</a>
</div>
<ul id="organization-filters" *ngIf="!isCollapsed" class="filter-options">
<li class="filter-option" [ngClass]="{ active: activeFilter.myVaultOnly }">
@@ -140,7 +130,7 @@
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
{{ organization.name }}
</button>
<ng-container *ngIf="organization.id === activeFilter.selectedOrganizationId">
<ng-container>
<button [bitMenuTriggerFor]="orgMenu" class="org-options ml-auto">
<i class="bwi bwi-ellipsis-v" aria-hidden="true"></i>
</button>
@@ -150,6 +140,14 @@
</ng-container>
</span>
</li>
<li class="filter-option" *ngIf="!(displayMode === 'singleOrganizationPolicy')">
<span class="filter-buttons">
<a href="#" routerLink="/create-organization" class="filter-button">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
&nbsp;{{ "newOrganization" | i18n }}
</a>
</span>
</li>
</ul>
</ng-container>
</ng-container>

View File

@@ -35,7 +35,6 @@ export class OrganizationOptionsComponent {
) {}
async ngOnInit() {
await this.syncService.fullSync(true);
await this.load();
}
@@ -83,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);
}
}
@@ -107,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);
}
}
@@ -174,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

@@ -0,0 +1,28 @@
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

@@ -26,22 +26,19 @@
autocomplete="off"
appAutofocus
/>
<div class="filter">
<app-organization-filter
*ngIf="showOrgFilter"
[hide]="hideOrganizations"
[activeFilter]="activeFilter"
[collapsedFilterNodes]="collapsedFilterNodes"
[organizations]="organizations"
[activePersonalOwnershipPolicy]="activePersonalOwnershipPolicy"
[activeSingleOrganizationPolicy]="activeSingleOrganizationPolicy"
(onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)"
(onFilterChange)="applyFilter($event)"
></app-organization-filter>
</div>
<app-organization-filter
[hide]="hideOrganizations"
[activeFilter]="activeFilter"
[collapsedFilterNodes]="collapsedFilterNodes"
[organizations]="organizations"
[activePersonalOwnershipPolicy]="activePersonalOwnershipPolicy"
[activeSingleOrganizationPolicy]="activeSingleOrganizationPolicy"
(onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)"
(onFilterChange)="applyFilter($event)"
></app-organization-filter>
<div class="filter">
<app-status-filter
[hideFavorites]="!showFavorites"
[hideFavorites]="hideFavorites"
[hideTrash]="hideTrash"
[activeFilter]="activeFilter"
(onFilterChange)="applyFilter($event)"
@@ -57,7 +54,7 @@
</div>
<div class="filter">
<app-folder-filter
[hide]="!showFolders"
[hide]="hideFolders"
[activeFilter]="activeFilter"
[collapsedFilterNodes]="collapsedFilterNodes"
[folderNodes]="folders"

View File

@@ -1,26 +1,22 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { Component, EventEmitter, Output } from "@angular/core";
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 { Organization } from "jslib-common/models/domain/organization";
import { VaultFilterService } from "./vault-filter.service";
@Component({
selector: "app-vault-filter",
templateUrl: "vault-filter.component.html",
})
export class VaultFilterComponent extends BaseVaultFilterComponent {
@Input() showOrgFilter = true;
@Input() showFolders = true;
@Input() showFavorites = true;
@Output() onSearchTextChanged = new EventEmitter<string>();
searchPlaceholder: string;
searchText = "";
organization: Organization;
constructor(vaultFilterService: VaultFilterService) {
constructor(protected vaultFilterService: VaultFilterService) {
// This empty constructor is required to provide the web vaultFilterService subclass to super()
// TODO: refactor this to use proper dependency injection
super(vaultFilterService);
}
@@ -32,9 +28,9 @@ export class VaultFilterComponent extends BaseVaultFilterComponent {
// 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);
this.activePersonalOwnershipPolicy =
await this.vaultFilterService.checkForPersonalOwnershipPolicy();
this.activeSingleOrganizationPolicy =
await this.vaultFilterService.checkForSingleOrganizationPolicy();
}
}

View File

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

View File

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

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
) {
@@ -352,7 +352,7 @@ export class IndividualVaultComponent implements OnInit, OnDestroy {
async editCipherId(id: string) {
const cipher = await this.cipherService.get(id);
if (cipher.reprompt != 0) {
if (cipher != null && cipher.reprompt != 0) {
if (!(await this.passwordRepromptService.showPasswordPrompt())) {
this.go({ cipherId: null });
return;

View File

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

View File

@@ -29,7 +29,7 @@ import { AddEditComponent } from "../../../../organizations/vault/add-edit.compo
import { AttachmentsComponent } from "../../../../organizations/vault/attachments.component";
import { CiphersComponent } from "../../../../organizations/vault/ciphers.component";
import { CollectionsComponent } from "../../../../organizations/vault/collections.component";
import { VaultFilterComponent } from "../../../vault-filter/vault-filter.component";
import { OrganizationVaultFilterComponent } from "../../../vault-filter/organization-vault-filter.component";
import { VaultService } from "../../vault.service";
const BroadcasterSubscriptionId = "OrgVaultComponent";
@@ -39,7 +39,8 @@ const BroadcasterSubscriptionId = "OrgVaultComponent";
templateUrl: "organization-vault.component.html",
})
export class OrganizationVaultComponent implements OnInit, OnDestroy {
@ViewChild("vaultFilter", { static: true }) vaultFilterComponent: VaultFilterComponent;
@ViewChild("vaultFilter", { static: true })
vaultFilterComponent: OrganizationVaultFilterComponent;
@ViewChild(CiphersComponent, { static: true }) ciphersComponent: CiphersComponent;
@ViewChild("attachments", { read: ViewContainerRef, static: true })
attachmentsModalRef: ViewContainerRef;
@@ -57,6 +58,11 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
trashCleanupWarning: string = null;
activeFilter: VaultFilter = new VaultFilter();
// This is a hack to avoid redundant api calls that fetch OrganizationVaultFilterComponent collections
// When it makes sense to do so we should leverage some other communication method for change events that isn't directly tied to the query param for organizationId
// i.e. exposing the VaultFiltersService to the OrganizationSwitcherComponent to make relevant updates from a change event instead of just depending on the router
firstLoaded = true;
constructor(
private route: ActivatedRoute,
private organizationService: OrganizationService,
@@ -86,7 +92,7 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
this.ciphersComponent.organization = this.organization;
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
// this.ciphersComponent.searchText = this.vaultFilterComponent.search = qParams.search;
this.ciphersComponent.searchText = this.vaultFilterComponent.searchText = qParams.search;
if (!this.organization.canViewAllCollections) {
await this.syncService.fullSync(false);
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
@@ -95,11 +101,7 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
case "syncCompleted":
if (message.successfully) {
await Promise.all([
this.vaultFilterComponent.reloadCollectionsAndFolders(
new VaultFilter({
selectedOrganizationId: this.organization.id,
} as Partial<VaultFilter>)
),
this.vaultFilterComponent.reloadCollectionsAndFolders(),
this.ciphersComponent.refresh(),
]);
this.changeDetectorRef.detectChanges();
@@ -109,9 +111,12 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
});
});
}
await this.vaultFilterComponent.reloadCollectionsAndFolders(
new VaultFilter({ selectedOrganizationId: this.organization.id } as Partial<VaultFilter>)
);
if (!this.firstLoaded) {
await this.vaultFilterComponent.reloadCollectionsAndFolders();
}
this.firstLoaded = false;
await this.ciphersComponent.reload();
if (qParams.viewEvents != null) {
@@ -123,7 +128,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 +177,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

@@ -0,0 +1,117 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="billingSyncApiKeyTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="modal-header">
<h2 class="modal-title" id="billingSyncApiKeyTitle">
{{ (hasBillingToken ? "viewBillingSyncToken" : "generateBillingSyncToken") | i18n }}
</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<app-user-verification
[(ngModel)]="masterPassword"
ngDefaultControl
name="secret"
*ngIf="!clientSecret"
>
</app-user-verification>
<ng-container *ngIf="clientSecret && showRotateScreen">
<p>{{ "rotateBillingSyncTokenTitle" | i18n }}</p>
<app-callout type="warning">
{{ "rotateBillingSyncTokenWarning" | i18n }}
</app-callout>
</ng-container>
<div *ngIf="clientSecret && !showRotateScreen">
<p>{{ "copyPasteBillingSync" | i18n }}</p>
<label for="clientSecret">Billing Sync Key</label>
<div class="input-group">
<input
id="clientSecret"
class="form-control text-monospace"
type="text"
[(ngModel)]="clientSecret"
name="clientSecret"
disabled
/>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
(click)="copy()"
[appA11yTitle]="'copy' | i18n"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="small text-muted mt-2" *ngIf="showLastSyncText">
<b class="font-weight-semibold">{{ "lastSync" | i18n }}:</b>
{{ lastSyncDate | date: "medium" }}
</div>
<div class="small text-danger mt-2" *ngIf="showAwaitingSyncText">
<i class="bwi bwi-error"></i>
{{
(daysBetween === 1 ? "awaitingSyncSingular" : "awaitingSyncPlural")
| i18n: daysBetween
}}
</div>
</div>
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-primary btn-submit"
[disabled]="form.loading"
*ngIf="!clientSecret || showRotateScreen"
>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
*ngIf="form.loading"
></i>
<span>
{{ submitButtonText }}
</span>
</button>
<button
type="button"
class="btn btn-outline-secondary"
data-dismiss="modal"
*ngIf="!showRotateScreen"
>
{{ "close" | i18n }}
</button>
<button
type="button"
class="btn btn-outline-secondary"
*ngIf="showRotateScreen"
(click)="cancelRotate()"
>
{{ "cancel" | i18n }}
</button>
<button
type="button"
class="btn btn-outline-secondary"
*ngIf="clientSecret && !showRotateScreen"
(click)="rotateToken()"
>
{{ "rotateToken" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,108 @@
import { Component } from "@angular/core";
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { UserVerificationService } from "jslib-common/abstractions/userVerification.service";
import { OrganizationApiKeyType } from "jslib-common/enums/organizationApiKeyType";
import { OrganizationApiKeyRequest } from "jslib-common/models/request/organizationApiKeyRequest";
import { ApiKeyResponse } from "jslib-common/models/response/apiKeyResponse";
import { Verification } from "jslib-common/types/verification";
@Component({
selector: "app-billing-sync-api-key",
templateUrl: "billing-sync-api-key.component.html",
})
export class BillingSyncApiKeyComponent {
organizationId: string;
hasBillingToken: boolean;
showRotateScreen: boolean;
masterPassword: Verification;
formPromise: Promise<ApiKeyResponse>;
clientSecret?: string;
keyRevisionDate?: Date;
lastSyncDate?: Date = null;
constructor(
private userVerificationService: UserVerificationService,
private apiService: ApiService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService
) {}
copy() {
this.platformUtilsService.copyToClipboard(this.clientSecret);
}
async submit() {
if (this.showRotateScreen) {
this.formPromise = this.userVerificationService
.buildRequest(this.masterPassword, OrganizationApiKeyRequest)
.then((request) => {
request.type = OrganizationApiKeyType.BillingSync;
return this.apiService.postOrganizationRotateApiKey(this.organizationId, request);
});
const response = await this.formPromise;
await this.load(response);
this.showRotateScreen = false;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("billingSyncApiKeyRotated")
);
} else {
this.formPromise = this.userVerificationService
.buildRequest(this.masterPassword, OrganizationApiKeyRequest)
.then((request) => {
request.type = OrganizationApiKeyType.BillingSync;
return this.apiService.postOrganizationApiKey(this.organizationId, request);
});
const response = await this.formPromise;
await this.load(response);
}
}
async load(response: ApiKeyResponse) {
this.clientSecret = response.apiKey;
this.keyRevisionDate = response.revisionDate;
this.hasBillingToken = true;
const syncStatus = await this.apiService.getSponsorshipSyncStatus(this.organizationId);
this.lastSyncDate = syncStatus.lastSyncDate;
}
cancelRotate() {
this.showRotateScreen = false;
}
rotateToken() {
this.showRotateScreen = true;
}
private dayDiff(date1: Date, date2: Date): number {
const diffTime = Math.abs(date2.getTime() - date1.getTime());
return Math.round(diffTime / (1000 * 60 * 60 * 24));
}
get submitButtonText(): string {
if (this.showRotateScreen) {
return this.i18nService.t("rotateToken");
}
return this.i18nService.t(this.hasBillingToken ? "continue" : "generateToken");
}
get showLastSyncText(): boolean {
// If the keyRevisionDate is later than the lastSyncDate we need to show
// a warning that they need to put the billing sync key in their self hosted install
return this.lastSyncDate && this.lastSyncDate > this.keyRevisionDate;
}
get showAwaitingSyncText(): boolean {
return this.lastSyncDate && this.lastSyncDate <= this.keyRevisionDate;
}
get daysBetween(): number {
return this.dayDiff(this.keyRevisionDate, new Date());
}
}

View File

@@ -188,10 +188,10 @@
></app-adjust-storage>
</div>
</ng-container>
<h2 class="spaced-header">{{ "additionalOptions" | i18n }}</h2>
<!--Switch to i18n-->
<h2 class="spaced-header">{{ "selfHostingTitle" | i18n }}</h2>
<p class="mb-4">
{{ "additionalOptionsDesc" | i18n }}
{{ "selfHostingEnterpriseOrganizationSectionCopy" | i18n }}
</p>
<div class="d-flex">
<button
@@ -203,6 +203,27 @@
>
{{ "downloadLicense" | i18n }}
</button>
<button
type="button"
class="btn btn-outline-secondary ml-1"
(click)="manageBillingSync()"
*ngIf="canManageBillingSync"
>
{{ (hasBillingSyncToken ? "manageBillingSync" : "setUpBillingSync") | i18n }}
</button>
</div>
<div class="mt-3" *ngIf="showDownloadLicense">
<app-download-license
[organizationId]="organizationId"
(onDownloaded)="closeDownloadLicense()"
(onCanceled)="closeDownloadLicense()"
></app-download-license>
</div>
<h2 class="spaced-header">{{ "additionalOptions" | i18n }}</h2>
<p class="mb-4">
{{ "additionalOptionsDesc" | i18n }}
</p>
<div class="d-flex">
<button
#cancelBtn
type="button"
@@ -216,13 +237,6 @@
<span>{{ "cancelSubscription" | i18n }}</span>
</button>
</div>
<div class="mt-3" *ngIf="showDownloadLicense">
<app-download-license
[organizationId]="organizationId"
(onDownloaded)="closeDownloadLicense()"
(onCanceled)="closeDownloadLicense()"
></app-download-license>
</div>
</ng-container>
<ng-container *ngIf="selfHosted">
<dl>
@@ -269,5 +283,31 @@
></app-update-license>
</div>
</div>
<div *ngIf="showBillingSyncKey">
<h2 class="mt-5">
{{ "billingSync" | i18n }}
</h2>
<p>
{{ "billingSyncDesc" | i18n }}
</p>
<button
type="button"
class="btn btn-outline-secondary"
(click)="manageBillingSyncSelfHosted()"
>
{{ "manageBillingSync" | i18n }}
</button>
<small class="form-text text-muted" *ngIf="billingSyncSetUp">
{{ "lastSync" | i18n }}:
<span *ngIf="userOrg.familySponsorshipLastSyncDate != null">
{{ userOrg.familySponsorshipLastSyncDate | date: "medium" }}
</span>
<span *ngIf="userOrg.familySponsorshipLastSyncDate == null">
{{ "never" | i18n | lowercase }}
</span>
</small>
</div>
</ng-container>
</ng-container>
<ng-template #setupBillingSyncTemplate></ng-template>
<ng-template #rotateBillingSyncKeyTemplate></ng-template>

View File

@@ -1,21 +1,34 @@
import { Component, OnInit } from "@angular/core";
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { ModalRef } from "jslib-angular/components/modal/modal.ref";
import { ModalService } from "jslib-angular/services/modal.service";
import { ApiService } from "jslib-common/abstractions/api.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 { OrganizationApiKeyType } from "jslib-common/enums/organizationApiKeyType";
import { OrganizationConnectionType } from "jslib-common/enums/organizationConnectionType";
import { PlanType } from "jslib-common/enums/planType";
import { BillingSyncConfigApi } from "jslib-common/models/api/billingSyncConfigApi";
import { Organization } from "jslib-common/models/domain/organization";
import { OrganizationConnectionResponse } from "jslib-common/models/response/organizationConnectionResponse";
import { OrganizationSubscriptionResponse } from "jslib-common/models/response/organizationSubscriptionResponse";
import { BillingSyncKeyComponent } from "src/app/settings/billing-sync-key.component";
import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component";
@Component({
selector: "app-org-subscription",
templateUrl: "organization-subscription.component.html",
})
export class OrganizationSubscriptionComponent implements OnInit {
@ViewChild("setupBillingSyncTemplate", { read: ViewContainerRef, static: true })
setupBillingSyncModalRef: ViewContainerRef;
loading = false;
firstLoaded = false;
organizationId: string;
@@ -25,17 +38,24 @@ export class OrganizationSubscriptionComponent implements OnInit {
adjustStorageAdd = true;
showAdjustStorage = false;
showUpdateLicense = false;
showBillingSyncKey = false;
showDownloadLicense = false;
showChangePlan = false;
sub: OrganizationSubscriptionResponse;
selfHosted = false;
hasBillingSyncToken: boolean;
userOrg: Organization;
existingBillingSyncConnection: OrganizationConnectionResponse<BillingSyncConfigApi>;
removeSponsorshipPromise: Promise<any>;
cancelPromise: Promise<any>;
reinstatePromise: Promise<any>;
@ViewChild("rotateBillingSyncKeyTemplate", { read: ViewContainerRef, static: true })
billingSyncKeyViewContainerRef: ViewContainerRef;
billingSyncKeyRef: [ModalRef, BillingSyncKeyComponent];
constructor(
private apiService: ApiService,
private platformUtilsService: PlatformUtilsService,
@@ -43,7 +63,8 @@ export class OrganizationSubscriptionComponent implements OnInit {
private messagingService: MessagingService,
private route: ActivatedRoute,
private organizationService: OrganizationService,
private logService: LogService
private logService: LogService,
private modalService: ModalService
) {
this.selfHosted = platformUtilsService.isSelfHost();
}
@@ -63,10 +84,27 @@ export class OrganizationSubscriptionComponent implements OnInit {
this.loading = true;
this.userOrg = await this.organizationService.get(this.organizationId);
if (this.userOrg.canManageBilling) {
this.sub = await this.apiService.getOrganizationSubscription(this.organizationId);
}
const apiKeyResponse = await this.apiService.getOrganizationApiKeyInformation(
this.organizationId
);
this.hasBillingSyncToken = apiKeyResponse.data.some(
(i) => i.keyType === OrganizationApiKeyType.BillingSync
);
if (this.selfHosted) {
this.showBillingSyncKey = await this.apiService.getCloudCommunicationsEnabled();
}
if (this.showBillingSyncKey) {
this.existingBillingSyncConnection = await this.apiService.getOrganizationConnection(
this.organizationId,
OrganizationConnectionType.CloudBillingSync,
BillingSyncConfigApi
);
}
this.loading = false;
}
@@ -138,6 +176,20 @@ export class OrganizationSubscriptionComponent implements OnInit {
this.showDownloadLicense = !this.showDownloadLicense;
}
async manageBillingSync() {
const [ref] = await this.modalService.openViewRef(
BillingSyncApiKeyComponent,
this.setupBillingSyncModalRef,
(comp) => {
comp.organizationId = this.organizationId;
comp.hasBillingToken = this.hasBillingSyncToken;
}
);
ref.onClosed.subscribe(async () => {
await this.load();
});
}
closeDownloadLicense() {
this.showDownloadLicense = false;
}
@@ -200,6 +252,24 @@ export class OrganizationSubscriptionComponent implements OnInit {
}
}
async manageBillingSyncSelfHosted() {
this.billingSyncKeyRef = await this.modalService.openViewRef(
BillingSyncKeyComponent,
this.billingSyncKeyViewContainerRef,
(comp) => {
comp.entityId = this.organizationId;
comp.existingConnectionId = this.existingBillingSyncConnection?.id;
comp.billingSyncKey = this.existingBillingSyncConnection?.config?.billingSyncKey;
comp.setParentConnection = (
connection: OrganizationConnectionResponse<BillingSyncConfigApi>
) => {
this.existingBillingSyncConnection = connection;
this.billingSyncKeyRef[0].close();
};
}
);
}
get isExpired() {
return (
this.sub != null && this.sub.expiration != null && new Date(this.sub.expiration) < new Date()
@@ -266,6 +336,16 @@ export class OrganizationSubscriptionComponent implements OnInit {
);
}
get canManageBillingSync() {
return (
!this.selfHosted &&
(this.sub.planType === PlanType.EnterpriseAnnually ||
this.sub.planType === PlanType.EnterpriseMonthly ||
this.sub.planType === PlanType.EnterpriseAnnually2019 ||
this.sub.planType === PlanType.EnterpriseMonthly2019)
);
}
get subscriptionDesc() {
if (this.sub.planType === PlanType.Free) {
return this.i18nService.t("subscriptionFreePlan", this.sub.seats.toString());
@@ -293,4 +373,8 @@ export class OrganizationSubscriptionComponent implements OnInit {
get showChangePlanButton() {
return this.subscription == null && this.sub.planType === PlanType.Free && !this.showChangePlan;
}
get billingSyncSetUp() {
return this.existingBillingSyncConnection?.id != null;
}
}

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

@@ -58,7 +58,8 @@ export class CiphersComponent extends BaseCiphersComponent {
);
}
async load(filter: (cipher: CipherView) => boolean = null) {
async load(filter: (cipher: CipherView) => boolean = null, deleted = false) {
this.deleted = deleted || false;
if (this.organization.canEditAnyCollection) {
this.accessEvents = this.organization.useEvents;
this.allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organization.id);

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

@@ -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

@@ -0,0 +1,69 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="billingSyncTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="modal-header">
<h2 class="modal-title" id="billingSyncTitle">{{ "manageBillingSync" | i18n }}</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>{{ "billingSyncKeyDesc" | i18n }}</p>
<div class="form-group">
<label for="billingSyncKey"
>{{ "billingSyncKey" | i18n }} <small>(</small><small>{{ "required" | i18n }}</small
><small>)</small></label
>
<input
id="billingSyncKey"
type="input"
name="billingSyncKey"
class="form-control"
[(ngModel)]="billingSyncKey"
required
appAutofocus
appInputVerbatim
/>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
<div class="ml-auto">
<button
#deleteBtn
type="button"
(click)="deleteConnection()"
class="btn btn-outline-danger"
appA11yTitle="{{ 'delete' | i18n }}"
[disabled]="form.loading"
>
<i class="bwi bwi-trash bwi-lg bwi-fw" [hidden]="form.loading" aria-hidden="true"></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!form.loading"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
</div>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,61 @@
import { Component } from "@angular/core";
import { ApiService } from "jslib-common/abstractions/api.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { OrganizationConnectionType } from "jslib-common/enums/organizationConnectionType";
import { Utils } from "jslib-common/misc/utils";
import { BillingSyncConfigApi } from "jslib-common/models/api/billingSyncConfigApi";
import { BillingSyncConfigRequest } from "jslib-common/models/request/billingSyncConfigRequest";
import { OrganizationConnectionRequest } from "jslib-common/models/request/organizationConnectionRequest";
import { OrganizationConnectionResponse } from "jslib-common/models/response/organizationConnectionResponse";
@Component({
selector: "app-billing-sync-key",
templateUrl: "billing-sync-key.component.html",
})
export class BillingSyncKeyComponent {
entityId: string;
existingConnectionId: string;
billingSyncKey: string;
setParentConnection: (connection: OrganizationConnectionResponse<BillingSyncConfigApi>) => void;
formPromise: Promise<OrganizationConnectionResponse<BillingSyncConfigApi>> | Promise<void>;
constructor(private apiService: ApiService, private logService: LogService) {}
async submit() {
try {
const request = new OrganizationConnectionRequest(
this.entityId,
OrganizationConnectionType.CloudBillingSync,
true,
new BillingSyncConfigRequest(this.billingSyncKey)
);
if (this.existingConnectionId == null) {
this.formPromise = this.apiService.createOrganizationConnection(
request,
BillingSyncConfigApi
);
} else {
this.formPromise = this.apiService.updateOrganizationConnection(
request,
BillingSyncConfigApi,
this.existingConnectionId
);
}
const response = (await this
.formPromise) as OrganizationConnectionResponse<BillingSyncConfigApi>;
this.existingConnectionId = response?.id;
this.billingSyncKey = response?.config?.billingSyncKey;
this.setParentConnection(response);
} catch (e) {
this.logService.error(e);
}
}
async deleteConnection() {
this.formPromise = this.apiService.deleteOrganizationConnection(this.existingConnectionId);
await this.formPromise;
this.setParentConnection(null);
}
}

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

@@ -20,33 +20,55 @@
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
[formGroup]="sponsorshipForm"
ngNativeValidate
*ngIf="anyOrgsAvailable"
>
<div *ngIf="moreThanOneOrgAvailable" class="form-group col-7">
<div class="form-group col-7">
<label for="availableSponsorshipOrg">{{ "familiesSponsoringOrgSelect" | i18n }}</label>
<select
id="availableSponsorshipOrg"
name="Available Sponsorship Organization"
[(ngModel)]="selectedSponsorshipOrgId"
formControlName="selectedSponsorshipOrgId"
class="form-control"
required
>
<option value="">-- {{ "select" | i18n }} --</option>
<option disabled="true" value="">-- {{ "select" | i18n }} --</option>
<option *ngFor="let o of availableSponsorshipOrgs" [ngValue]="o.id">{{ o.name }}</option>
</select>
</div>
<div class="form-group col-7">
<label for="accountEmail">{{ "sponsoredFamiliesEmail" | i18n }}:</label>
<label for="sponsorshipEmail">{{ "sponsoredFamiliesEmail" | i18n }}:</label>
<input
id="accountEmail"
id="sponsorshipEmail"
class="form-control"
inputmode="email"
[(ngModel)]="sponsorshipEmail"
formControlName="sponsorshipEmail"
name="sponsorshipEmail"
required
[attr.aria-invalid]="sponsorshipEmailControl.invalid"
/>
<button class="btn btn-primary btn-submit mt-4" type="submit" [disabled]="form.loading">
<small
aria-errormessage="sponsorshipEmail"
*ngIf="sponsorshipEmailControl.errors?.notAllowedValue"
class="error-inline"
role="alert"
>
<i class="bwi bwi-error" aria-hidden="true"></i>
{{ "cannotSponsorSelf" | i18n }}
</small>
<small
aria-errormessage="sponsorshipEmail"
*ngIf="sponsorshipEmailControl.errors?.email"
class="error-inline"
role="alert"
>
<i class="bwi bwi-error" aria-hidden="true"></i>
{{ "invalidEmail" | i18n }}
</small>
</div>
<div class="form-group col-7">
<button class="btn btn-primary btn-submit mt-2" type="submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "redeem" | i18n }}</span>
</button>
@@ -59,12 +81,18 @@
<tr>
<th>{{ "recipient" | i18n }}</th>
<th>{{ "sponsoringOrg" | i18n }}</th>
<th>{{ "status" | i18n }}</th>
<th></th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let o of activeSponsorshipOrgs">
<tr sponsoring-org-row [sponsoringOrg]="o" (sponsorshipRemoved)="load(true)"></tr>
<tr
sponsoring-org-row
[sponsoringOrg]="o"
[isSelfHosted]="isSelfHosted"
(sponsorshipRemoved)="load(true)"
></tr>
</ng-container>
</tbody>
</table>

View File

@@ -1,9 +1,12 @@
import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { notAllowedValueAsync } from "jslib-angular/validators/notAllowedValueAsync.validator";
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { PlanSponsorshipType } from "jslib-common/enums/planSponsorshipType";
import { Organization } from "jslib-common/models/domain/organization";
@@ -17,30 +20,54 @@ export class SponsoredFamiliesComponent implements OnInit {
availableSponsorshipOrgs: Organization[] = [];
activeSponsorshipOrgs: Organization[] = [];
selectedSponsorshipOrgId = "";
sponsorshipEmail = "";
// Conditional display properties
formPromise: Promise<any>;
sponsorshipForm: FormGroup;
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private syncService: SyncService,
private organizationService: OrganizationService
) {}
private organizationService: OrganizationService,
private formBuilder: FormBuilder,
private stateService: StateService
) {
this.sponsorshipForm = this.formBuilder.group({
selectedSponsorshipOrgId: [
"",
{
validators: [Validators.required],
},
],
sponsorshipEmail: [
"",
{
validators: [Validators.email],
asyncValidators: [
notAllowedValueAsync(async () => await this.stateService.getEmail(), true),
],
updateOn: "blur",
},
],
});
}
async ngOnInit() {
await this.load();
}
async submit() {
this.formPromise = this.apiService.postCreateSponsorship(this.selectedSponsorshipOrgId, {
sponsoredEmail: this.sponsorshipEmail,
planSponsorshipType: PlanSponsorshipType.FamiliesForEnterprise,
friendlyName: this.sponsorshipEmail,
});
this.formPromise = this.apiService.postCreateSponsorship(
this.sponsorshipForm.value.selectedSponsorshipOrgId,
{
sponsoredEmail: this.sponsorshipForm.value.sponsorshipEmail,
planSponsorshipType: PlanSponsorshipType.FamiliesForEnterprise,
friendlyName: this.sponsorshipForm.value.sponsorshipEmail,
}
);
await this.formPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("sponsorshipCreated"));
@@ -67,14 +94,19 @@ export class SponsoredFamiliesComponent implements OnInit {
);
if (this.availableSponsorshipOrgs.length === 1) {
this.selectedSponsorshipOrgId = this.availableSponsorshipOrgs[0].id;
this.sponsorshipForm.patchValue({
selectedSponsorshipOrgId: this.availableSponsorshipOrgs[0].id,
});
}
this.loading = false;
}
get sponsorshipEmailControl() {
return this.sponsorshipForm.controls["sponsorshipEmail"];
}
private async resetForm() {
this.sponsorshipEmail = "";
this.selectedSponsorshipOrgId = "";
this.sponsorshipForm.reset();
}
get anyActiveSponsorships(): boolean {
@@ -85,7 +117,7 @@ export class SponsoredFamiliesComponent implements OnInit {
return this.availableSponsorshipOrgs.length > 0;
}
get moreThanOneOrgAvailable(): boolean {
return this.availableSponsorshipOrgs.length > 1;
get isSelfHosted(): boolean {
return this.platformUtilsService.isSelfHost();
}
}

View File

@@ -2,9 +2,13 @@
{{ sponsoringOrg.familySponsorshipFriendlyName }}
</td>
<td>{{ sponsoringOrg.name }}</td>
<td>
<span [ngClass]="statusClass">{{ statusMessage }}</span>
</td>
<td class="table-action-right">
<div class="dropdown" appListDropdown>
<button
*ngIf="!sponsoringOrg.familySponsorshipToDelete"
class="btn btn-outline-secondary dropdown-toggle"
type="button"
id="dropdownMenuButton"
@@ -18,6 +22,7 @@
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<button
#resendEmailBtn
*ngIf="!isSelfHosted && !sponsoringOrg.familySponsorshipValidUntil"
[appApiAction]="resendEmailPromise"
class="dropdown-item btn-submit"
[disabled]="resendEmailBtn.loading"

View File

@@ -1,4 +1,5 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { formatDate } from "@angular/common";
import { Component, EventEmitter, Input, Output, OnInit } from "@angular/core";
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
@@ -10,11 +11,15 @@ import { Organization } from "jslib-common/models/domain/organization";
selector: "[sponsoring-org-row]",
templateUrl: "sponsoring-org-row.component.html",
})
export class SponsoringOrgRowComponent {
export class SponsoringOrgRowComponent implements OnInit {
@Input() sponsoringOrg: Organization = null;
@Input() isSelfHosted = false;
@Output() sponsorshipRemoved = new EventEmitter();
statusMessage = "loading";
statusClass: "text-success" | "text-danger" = "text-success";
revokeSponsorshipPromise: Promise<any>;
resendEmailPromise: Promise<any>;
@@ -25,6 +30,15 @@ export class SponsoringOrgRowComponent {
private platformUtilsService: PlatformUtilsService
) {}
ngOnInit(): void {
this.setStatus(
this.isSelfHosted,
this.sponsoringOrg.familySponsorshipToDelete,
this.sponsoringOrg.familySponsorshipValidUntil,
this.sponsoringOrg.familySponsorshipLastSyncDate
);
}
async revokeSponsorship() {
try {
this.revokeSponsorshipPromise = this.doRevokeSponsorship();
@@ -43,6 +57,10 @@ export class SponsoringOrgRowComponent {
this.resendEmailPromise = null;
}
get isSentAwaitingSync() {
return this.isSelfHosted && !this.sponsoringOrg.familySponsorshipLastSyncDate;
}
private async doRevokeSponsorship() {
const isConfirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("revokeSponsorshipConfirmation"),
@@ -60,4 +78,53 @@ export class SponsoringOrgRowComponent {
this.platformUtilsService.showToast("success", null, this.i18nService.t("reclaimedFreePlan"));
this.sponsorshipRemoved.emit();
}
private setStatus(
selfHosted: boolean,
toDelete?: boolean,
validUntil?: Date,
lastSyncDate?: Date
) {
/*
* Possible Statuses:
* Requested (self-hosted only)
* Sent
* Active
* RequestRevoke
* RevokeWhenExpired
*/
if (toDelete && validUntil) {
// They want to delete but there is a valid until date which means there is an active sponsorship
this.statusMessage = this.i18nService.t(
"revokeWhenExpired",
formatDate(validUntil, "MM/dd/yyyy", this.i18nService.locale)
);
this.statusClass = "text-danger";
} else if (toDelete) {
// They want to delete and we don't have a valid until date so we can
// this should only happen on a self-hosted install
this.statusMessage = this.i18nService.t("requestRemoved");
this.statusClass = "text-danger";
} else if (validUntil) {
// They don't want to delete and they have a valid until date
// that means they are actively sponsoring someone
this.statusMessage = this.i18nService.t("active");
this.statusClass = "text-success";
} else if (selfHosted && lastSyncDate) {
// We are on a self-hosted install and it has been synced but we have not gotten
// a valid until date so we can't know if they are actively sponsoring someone
this.statusMessage = this.i18nService.t("sent");
this.statusClass = "text-success";
} else if (!selfHosted) {
// We are in cloud and all other status checks have been false therefore we have
// sent the request but it hasn't been accepted yet
this.statusMessage = this.i18nService.t("sent");
this.statusClass = "text-success";
} else {
// We are on a self-hosted install and we have not synced yet
this.statusMessage = this.i18nService.t("requested");
this.statusClass = "text-success";
}
}
}

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

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

View File

@@ -50,7 +50,8 @@
organizationName="{{ c.organizationId | orgNameFromId: organizations }}"
profileName="{{ profileName }}"
(onOrganizationClicked)="onOrganizationClicked(c.organizationId)"
></app-org-badge>
>
</app-org-badge>
</td>
<td class="table-list-options">
<button

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

@@ -916,7 +916,7 @@
},
"specialCharacters": {
"message": "Special Characters (!@#$%^&*)"
},
},
"numWords": {
"message": "Number of Words"
},
@@ -3034,10 +3034,10 @@
"message": "Adjustments to your subscription will result in prorated changes to your billing totals. You cannot invite more than $COUNT$ users without increasing your subscription seats.",
"placeholders": {
"count": {
"content": "$1",
"example": "50"
}
}
"content": "$1",
"example": "50"
}
}
},
"seatsToAdd": {
"message": "Seats To Add"
@@ -4395,10 +4395,10 @@
"message": "Your Master Password does not meet the policy requirements of this organization. In order to join the organization, you must update your Master Password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
},
"maximumVaultTimeout": {
"message": "Vault Timeout"
"message": "Vault Timeout"
},
"maximumVaultTimeoutDesc": {
"message": "Configure a maximum vault timeout for all users."
"message": "Configure a maximum vault timeout for all users."
},
"maximumVaultTimeoutLabel": {
"message": "Maximum Vault Timeout"
@@ -4594,7 +4594,7 @@
"message": "Enter your personal email to redeem Bitwarden Families"
},
"sponsoredFamiliesLeaveCopy": {
"message": "If you leave or are removed from the sponsoring organization, your Families plan will expire at the end of the billing period."
"message": "If you remove an offer or are removed from the sponsoring organization, your Families sponsorship will expire at the next renewal date."
},
"acceptBitwardenFamiliesHelp": {
"message": "Accept offer for an existing organization or create a new Families organization."
@@ -4669,13 +4669,13 @@
"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"
},
"ssoKeyConnectorUnavailable": {
"message": "Unable to reach the Key Connector, try again later."
"ssoKeyConnectorError": {
"message": "Key Connector error: make sure Key Connector is available and working correctly."
},
"keyConnectorUrl": {
"message": "Key Connector URL"
@@ -4803,6 +4803,75 @@
"freeWithSponsorship": {
"message": "FREE with sponsorship"
},
"viewBillingSyncToken": {
"message": "View Billing Sync Token"
},
"generateBillingSyncToken": {
"message": "Generate Billing Sync Token"
},
"copyPasteBillingSync": {
"message": "Copy and paste this token into the Billing Sync settings of your self-hosted organization."
},
"billingSyncCanAccess": {
"message": "Your Billing Sync token can access and edit this organization's subscription settings."
},
"manageBillingSync": {
"message": "Manage Billing Sync"
},
"setUpBillingSync": {
"message": "Set Up Billing Sync"
},
"generateToken": {
"message": "Generate Token"
},
"rotateToken": {
"message": "Rotate Token"
},
"rotateBillingSyncTokenWarning": {
"message": "If you proceed, you will need to re-setup billing sync on your self-hosted server."
},
"rotateBillingSyncTokenTitle": {
"message": "Rotating the Billing Sync Token will invalidate the previous token."
},
"selfHostingTitle": {
"message": "Self-Hosting"
},
"selfHostingEnterpriseOrganizationSectionCopy": {
"message": "To set-up your organization on your own server, you will need to upload your license file. To support Free Families plans and advanced billing capabilities for your self-hosted organization, you will need to set up billing sync."
},
"billingSyncApiKeyRotated": {
"message": "Token rotated."
},
"billingSync": {
"message": "Billing Sync"
},
"billingSyncDesc": {
"message": "Billing Sync provides Free Families plans for members and advanced billing capabilities by linking your self-hosted Bitwarden to the Bitwarden cloud server."
},
"billingSyncKeyDesc": {
"message": "A Billing Sync Token from your cloud organization's subscription settings is required to complete this form."
},
"billingSyncKey": {
"message": "Billing Sync Token"
},
"active": {
"message": "Active"
},
"inactive": {
"message": "Inactive"
},
"sentAwaitingSync": {
"message": "Sent (Awaiting Sync)"
},
"sent": {
"message": "Sent"
},
"requestRemoved": {
"message": "Removed (Awaiting Sync)"
},
"requested": {
"message": "Requested"
},
"formErrorSummaryPlural": {
"message": "$COUNT$ fields above need your attention.",
"placeholders": {
@@ -4935,6 +5004,46 @@
"service": {
"message": "Service"
},
"unknownCipher": {
"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."
},
"revokeWhenExpired": {
"message": "Expires $DATE$",
"placeholders": {
"date": {
"content": "$1",
"example": "12/31/2020"
}
}
},
"awaitingSyncSingular": {
"message": "Token rotated $DAYS$ day ago. Update the billing sync token in your self-hosted organization settings.",
"placeholders": {
"days": {
"content": "$1",
"example": "1"
}
}
},
"awaitingSyncPlural": {
"message": "Token rotated $DAYS$ days ago. Update the billing sync token in your self-hosted organization settings.",
"placeholders": {
"days": {
"content": "$1",
"example": "1"
}
}
},
"lastSync": {
"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": {
@@ -4960,8 +5069,5 @@
},
"apiAccessToken": {
"message": "API Access Token"
},
"unknownCipher": {
"message": "Unknown Item, you may need to login with another account to access this item."
}
}

View File

@@ -68,7 +68,6 @@
}
.modal-footer {
border-radius: 0.3rem 0.3rem 0 0;
justify-content: flex-start;
@include themify($themes) {
background-color: themed("footerBackgroundColor");

View File

@@ -14,22 +14,12 @@
font-size: $font-size-base;
}
a.create-organization-link {
&:hover {
@include themify($themes) {
color: themed("iconHover") !important;
}
}
}
button {
@extend .no-btn;
}
h3,
button.filter-button {
text-transform: uppercase;
text-transform: uppercase;
margin: 0;
@include themify($themes) {
color: themed("textMuted");
@@ -118,6 +108,7 @@
}
text-decoration: none;
}
max-width: 90%;
}
.edit-button {