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

Compare commits

...

418 Commits

Author SHA1 Message Date
Thomas Rittson
69bf3d7fb7 Remove eager loading of module 2022-06-02 15:41:01 +10:00
Thomas Rittson
97b1d0ece6 Fix prettier 2022-06-02 15:00:31 +10:00
Thomas Rittson
8a52dc3456 Add ManageRoutingModule, lazy loading 2022-06-02 14:44:47 +10:00
Thomas Rittson
68d68b9cc3 Create SettingsRoutingModule, lazy load 2022-06-02 14:38:59 +10:00
Thomas Rittson
1a290c9fea Add Tools routing module, lazy loading 2022-06-02 14:35:41 +10:00
Thomas Rittson
194d1bb9cf Create ToolsModule 2022-06-02 14:27:54 +10:00
Thomas Rittson
44483c2fa4 Create ManageModule 2022-06-02 14:24:01 +10:00
Thomas Rittson
a5da0338ab Create SettingsModule 2022-06-02 14:18:55 +10:00
Thomas Rittson
1f7c2e32b5 Create PoliciesModule 2022-06-02 14:06:54 +10:00
Thomas Rittson
9aba19f3a2 Fix organizationModule imports after merge 2022-06-02 13:53:28 +10:00
Thomas Rittson
547ceb4f0d Merge branch 'master' into refactor/organization-module 2022-06-02 13:41:57 +10:00
Kyle Spearrin
52406bc969 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-01 20:33:06 -04:00
Thomas Rittson
d7dd2435fc Fix Manage SSO permissions (#1709) 2022-06-02 07:20:07 +10:00
Matt Gibson
ae3788d42b [PS-616] Feature/require auth for enroll password reset (#1698)
* Make enrollment a modal component

* Add verification to org user module

* Move enroll to component

* Update warning for modal

* update jslib

* Use ModalRef to close the modal

* Use bit-button
2022-06-01 09:04:49 -05:00
Jordan Cooks
0e62e2ec2b Link to correct doc for Org User Types (#1676)
Help center document linked is for the Provider Portal; this modal is for Organization Users. Correcting link to help docs.
2022-06-01 15:47:49 +02:00
Daniel James Smith
95d177da38 Fix expanding/collapsing of nested collections (#1722) 2022-06-01 14:46:48 +02:00
Joseph Flinn
ad0512e344 Version check typo (#1717)
* fixing dumb issue

* Fixing another dumb mistake
2022-05-31 18:08:32 -07:00
Joseph Flinn
8fe9504cd1 Updating the path for the Release Version Check (#1716) 2022-05-31 17:48:59 -07:00
Joseph Flinn
b8aa25b981 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>
2022-05-31 16:11:26 -07:00
github-actions[bot]
5d09ddbc8d Bumped version to 2022.05.0 (#1715)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-05-31 15:41:37 -07:00
Gbubemi Smith
bde9a28f2b sync jslib master (#1712) 2022-05-30 10:56:14 +01:00
Gbubemi Smith
dfb03a53c0 sync jslib master (#1711) 2022-05-27 17:28:16 +01:00
Thomas Rittson
b98b391283 Update jslib (#1710) 2022-05-27 13:11:41 +10:00
Gbubemi Smith
f195aee90c Revert "[ps-136] Igonre accented characters in vault search (#1690)" (#1706)
This reverts commit 58d9ac5ebc.
2022-05-26 10:05:36 +01:00
Thomas Rittson
eefcda7e41 [EC-177] Update Key Connector error message (#1705)
* Update Key Connector error message

* Update jslib
2022-05-26 07:00:59 +10:00
Thomas Avery
42cd171685 Swap to heading tags on presented headings (#1707) 2022-05-25 15:10:13 -05:00
Robyn MacCallum
8ef27713f1 [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:18:00 -04:00
Robyn MacCallum
57b647bde5 Fix 'Link SSO' not appearing (#1702) 2022-05-23 19:41:21 -04:00
Robyn MacCallum
681ace4b1b Use vaultFilter cipher type (#1701) 2022-05-23 16:09:52 -04:00
Gbubemi Smith
58d9ac5ebc [ps-136] Igonre accented characters in vault search (#1690)
* removed accented character from serach input field

* updated jslib
2022-05-20 15:22:44 +01:00
André Filipe da Silva Bispo
eab478da0c PS-502: Remove extraneous comma from web vault footer (#1697)
- removed comma from footer files

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

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

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

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

* [dep] Update jslib
2022-05-17 21:08:39 -04:00
Oscar Hinton
7b55c8ad1a [EC-203] Add logic for falling back to the users vault (#1688)
* Add logic for falling back to the users vault

* Update error message

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

* Fix folders not showing for orgs

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

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

* Fix cdk-virtual-scroll styling

* Format csp for readability

* Set Viewport height

The viewport height was

* Calculate viewport height from item size

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

* Remove modal css changes

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

* [fix] Use correct bulk actions for trash filter
2022-05-13 15:14:51 -04:00
Addison Beck
3501be9484 [fix] Add max width to vault filter buttons to keep content unified (#1678) 2022-05-13 15:14:39 -04:00
Addison Beck
5d1522b77a [fix] Check policies when reloading organizations for filters (#1677) 2022-05-13 15:14:27 -04:00
Oscar Hinton
be30d47038 [EC-200] Handle an edge case where ciphers were not selectable (#1674) 2022-05-13 15:32:15 +02:00
Thomas Rittson
888892b3e7 Revert accidental cnesting of org routes (#1670) 2022-05-13 08:31:30 +10:00
Oscar Hinton
f4f3e8c574 [SG-279] Fix unlock button not working after logging in through SSO (#1672) 2022-05-12 16:21:50 +02:00
Oscar Hinton
3367736c7e [SG-267] Exposed passwords report: console error (#1671) 2022-05-12 15:47:18 +02:00
Vincent Salucci
e3e7fce70a [bug:euvr] Self-hosted instance hiding subscription nav item (#1669) 2022-05-12 08:43:27 -04:00
Thomas Rittson
74bdfe2602 Fix permission checking when accessing manage SSO (#1663) 2022-05-12 09:12:02 +10:00
Thomas Rittson
9e01d47a3f Update Go Premium link (#1660) 2022-05-12 06:53:18 +10:00
Vincent Salucci
67de7d4bfb [bug:euvr] Change password init fix (#1667) 2022-05-11 15:39:56 -04:00
Vincent Salucci
f5245a280e [bug:euvr] Create Organization Updates (#1664) 2022-05-11 15:39:39 -04:00
Addison Beck
1dc9502676 [fix] Update the navbar when creating an organization for the first time (#1668) 2022-05-11 14:10:02 -04:00
Robyn MacCallum
ccf0d64a7b Use correct localization string (#1665) 2022-05-11 13:32:42 -04:00
Jake Fink
dc2078ae58 add better error handling to tax call (#1666) 2022-05-11 11:13:42 -04:00
Thomas Rittson
eb7d8d3071 Fix linting and style 2022-05-11 13:29:28 +10:00
Thomas Rittson
96bde49aa7 Remove Organization prefix, remove unneeded imports 2022-05-11 13:27:25 +10:00
Thomas Rittson
bb8165555b Use SharedModule and LayoutsModule in ProvidersModule 2022-05-11 13:08:59 +10:00
Thomas Rittson
fda00fce4b Do not import and re-export SharedModule in OssModule 2022-05-11 13:07:56 +10:00
Thomas Rittson
64d7530b36 Use standard pattern for lazy loading modules 2022-05-11 12:55:42 +10:00
Thomas Rittson
b81a83ebd3 Remove exports, tidy up names 2022-05-11 12:08:56 +10:00
Thomas Rittson
7cf50d09db Move org vault into organization-vault module 2022-05-11 11:51:33 +10:00
Thomas Rittson
0c4a1507e0 Rearrange components 2022-05-11 11:18:39 +10:00
Thomas Rittson
9eef75a3d3 Update import names and paths 2022-05-11 10:05:35 +10:00
Thomas Rittson
91a0fb1de1 Create org module file, not connected yet 2022-05-11 09:59:06 +10:00
Thomas Rittson
da470ad709 [SG-234] Fix account menu icon colour (#1653) 2022-05-11 08:38:45 +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
Addison Beck
81c6a4b1df [fix] Override collection filter init to consider organization vault (#1652)
* [fix] Override collection filter init to consider organization vault

* [dep] Update jslib
2022-05-10 09:04:39 -04:00
Thomas Rittson
da62cec6f0 Remove testing requirements from PR template (#1654) 2022-05-10 15:03:35 +02:00
Robyn MacCallum
474df5ba5e [SG-232 & SG-251] Fix color issues with organization badge (#1649)
* Fix color issues with organization badge

* Use tokenService to get account name

* Remove unused import
2022-05-10 07:15:29 -04:00
Thomas Rittson
2c609fc6fd [EC-151] Hide Subscription/Billing information for Provider-managed organizations (#1614) 2022-05-10 17:41:52 +10:00
Addison Beck
f8a2fae82b [fix] Add missing Create Org button to filters (#1650) 2022-05-09 21:29:18 -04:00
Matt Gibson
2f04c07262 Check for null cipher in edit (#1646)
Null ciphers signify a _new_ cipher, so no password repromt is required
2022-05-09 13:53:31 -05:00
Vincent Salucci
f81195c920 [euvr] Settings -> Account Settings label (#1648) 2022-05-09 12:13:27 -04:00
Oscar Hinton
d031b53c74 [EC-196] Move provider last in navbar (#1647) 2022-05-09 18:07:31 +02:00
Thomas Rittson
468007a984 [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>
2022-05-09 08:21:52 -04:00
Oscar Hinton
bc054236ad [EC-189] Resolve password reprompt not appearing on linkable cipher (#1643) 2022-05-06 11:29:45 +02:00
Vince Grassia
1c31d090a3 DEVOPS-758 - Move Web deploy from GitHub Pages to CloudFlare Pages (#1627) 2022-05-03 12:21:27 -04:00
dwbit
f8d942c02c Update localization description for 'random' (#1633)
Adding description string for 'random'
2022-05-03 09:02:40 -04:00
Kyle Spearrin
248938ca00 Forwarded email providers to username generator (#1628)
* forwarded emails

* firefox relay

* remove firefox relay

* update jslib ref

* remove dupe logService
2022-05-02 10:32:15 -04:00
Oscar Hinton
06d95bb224 [PS-381] Fix locale being empty when not configuring a language (#1631) 2022-05-02 15:32:44 +02:00
Thomas Rittson
446f2027b4 [PS-74] Fix user authentication state checks (#1632)
* Update to use new authStatus method

* Delete unused services and import

* update jslib
2022-04-30 10:51:33 +10:00
Federico Maccaroni
1f0d496f21 PS-79 Updated two-factor component to to align to jslib change to send the deviceId on 2fa email resend code (#1624) 2022-04-29 13:05:05 -03:00
Thomas Rittson
2b03162bfd [EC-154] [BEEEP] Remove factory providers in Angular DI (#1609)
* use InjectionTokens

* Use InitService
2022-04-29 09:45:47 +02:00
github-actions[bot]
f586359610 Bumped version to 2.28.1 (#1629)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-04-28 11:50:04 -07:00
Matt Gibson
96641cf195 [PS-301] Load OssModule from BitwardenLicense (#1626) 2022-04-28 07:39:59 -05:00
dwbit
572758c598 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."
2022-04-27 17:06:50 -04:00
dwbit
df7db8ad07 Add description for the A-Z & a-z items (#1615) 2022-04-27 17:06:16 -04:00
dwbit
0439d37c14 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'
2022-04-27 17:05:30 -04:00
Matt Gibson
97f38aa654 Fix login sponsorship redirect (#1620) 2022-04-27 08:09:38 -05:00
Oscar Hinton
0444b78ad1 [EC-143] [BEEEP] Allow linking to ciphers (#1579)
Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
2022-04-25 15:41:44 +02:00
Daniel James Smith
705251fbe2 Add aria attributes to password gen options (#1611) 2022-04-22 17:58:42 +02:00
Oscar Hinton
27853481d8 Add reorganization notice (#1610) 2022-04-22 14:25:02 +02:00
Thomas Rittson
c0511f25ca [PS-211] [PS-212] Make Generator page accessible (#1607)
* Fix grouping of radiobutton inputs

* Add role=radiogroup

* Add aria-labelledBy to radio button groups
2022-04-21 09:51:00 -04:00
Oscar Hinton
1be62ac222 [EC-161] Bump braintree (#1606) 2022-04-20 20:51:33 +02:00
github-actions[bot]
8304104a7a Bumped version to 2.28.0 (#1603)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-04-19 14:14:40 -07:00
Thomas Rittson
56808a7dbb Update jslib (#1602)
* Update jslib

* Update name of UserVerificationComponent
2022-04-19 09:49:59 +02:00
Joseph Flinn
609c13faf4 Bumping the pinned commit for the download-artifact to bypass the github api issue (#1598) 2022-04-14 16:05:20 -07:00
github-actions[bot]
62b20a5c6d Autosync the updated translations (#1590)
Co-authored-by: github-actions <>
2022-04-08 11:45:22 +02:00
Matt Gibson
d56bf1211e Add descriptions to vague messages (#1586)
* Add descriptions to vague messages

* Fix typo
2022-04-07 08:20:38 -05:00
Daniel James Smith
8831f96fc2 [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
2022-04-06 19:10:44 +02:00
github-actions[bot]
f26dc27515 Autosync the updated translations (#1577)
Co-authored-by: github-actions <>
2022-04-01 12:29:52 +02:00
Kyle Spearrin
cb8a40d9cd generator updates (#1575)
* update generator

* update css

* add link to help article

* update jslib

* fix oss module and user type tip icon

* update jslib

* Revert "update jslib"

This reverts commit b2b13ace5e.

* revert jslib update
2022-03-31 23:32:57 -04:00
Vince Grassia
2652a2deae Add release logic for 'web-sh' image (#1573) 2022-03-30 17:21:00 -04:00
Robyn MacCallum
e1c0c9f009 update jslib and remove date pipe from routing module (#1572) 2022-03-29 22:56:02 +02:00
Oscar Hinton
612442c1bb Cherry pick premium badge and reports page (#1525, #1536) (#1571) 2022-03-29 20:55:47 +01:00
Kyle Spearrin
23b02a770a add username generation to generator (#1566)
* add username generation to generator

* move bottom buttons into existing containers
2022-03-29 15:02:48 -04:00
Robyn MacCallum
42ececbcf5 add DatePipe to providers in routing module (#1570) 2022-03-29 12:55:27 -04:00
Kyle Spearrin
11034de7d1 resolve build errors with latest jslib ref (#1565) 2022-03-25 11:00:40 -04:00
github-actions[bot]
571aaf31c4 Autosync the updated translations (#1564)
Co-authored-by: github-actions <>
2022-03-25 01:23:30 +01:00
Oscar Hinton
0884e2d761 Bump node-forge (#1562) 2022-03-24 11:54:32 +01:00
Oscar Hinton
00975e6896 Use the new KDF constants (#1559) 2022-03-24 11:15:48 +01:00
Thomas Rittson
2c43249e98 Restore order of ngModule imports (#1560) 2022-03-24 07:25:10 +10:00
Matt Gibson
575847f252 Update configurations for self-hosted (#1558)
* Update configurations for self-hosted

* Revert "Update configurations for self-hosted"

This reverts commit a1ec06c834.

* Use selfhosted.json to configure dev env
2022-03-23 13:53:41 -05:00
Oscar Hinton
d6c181c997 Define Angular CLI globals to support tree shaking (#1541) 2022-03-22 10:00:07 +01:00
Thomas Rittson
9bb004923c [JslibModule] Refactor to use JslibModule (#1556) 2022-03-21 20:40:26 +10:00
github-actions[bot]
e08726463e Autosync the updated translations (#1554)
Co-authored-by: github-actions <>
2022-03-18 01:17:26 +01:00
Justin Baur
fdf93b610c Update CSP rule for azure blobs (#1552) 2022-03-17 09:54:14 -04:00
Vince Grassia
144038ed1c Move checkout steps before setup-node steps in Build workflow (#1553) 2022-03-17 09:02:50 -04:00
Vince Grassia
5cb5e37270 Add Node caching (#1549) 2022-03-16 11:18:47 -04:00
github-actions[bot]
e266a740ba Bump version to 2.27.0 (#1545)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-03-15 15:39:35 -06:00
Chad Scharf
3b0fc94239 Update SECURITY.md (#1544)
* Update SECURITY.md

Add link to our HackerOne program for submitting potential security issues.

* Fix extra space (prettier)

* Revise language on SECURITY.md
2022-03-15 15:45:05 -04:00
Matt Gibson
32e27b5f08 Update jslib (#1542) 2022-03-15 08:11:47 -05:00
Thomas Rittson
317c40386f Update jslib (#1540) 2022-03-15 15:14:40 +10:00
github-actions[bot]
c9eeca7def Autosync the updated translations (#1537)
Co-authored-by: github-actions <>
2022-03-11 02:31:15 +01:00
Thomas Rittson
902c568c09 Remove undefined template variable (#1534) 2022-03-11 07:20:03 +10:00
Joseph Flinn
153870693b Update hotfix release branch name to hotfix-rc (#1532) 2022-03-09 12:46:20 -08:00
Addison Beck
a8cd2a6cf7 [bug] Partially revert #1516 (#1531) 2022-03-09 11:16:53 -05:00
Vince Grassia
7404da9b3c Add web-sh repository to QA container registry (#1528) 2022-03-08 16:39:54 -05:00
Oscar Hinton
9b40ce1024 Component Library Scaffolding (#1407) 2022-03-08 18:18:03 +01:00
Daniel James Smith
80ffa965e1 Pull jslib for https://github.com/bitwarden/jslib/pull/714 (#1526) 2022-03-08 13:52:56 +01:00
Oscar Hinton
57f1a5e380 Remove msbuild from workflow (#1522) 2022-03-08 11:48:12 +01:00
Daniel James Smith
18f1929f65 Pull in Dashlane and Myki importer (#1523)
* Pull jslib

* Update copy for Dashlane json and csv importers
2022-03-08 09:01:19 +10:00
Micaiah Martin
5cb3941190 BEEEP - organize linting in CI (#1520) 2022-03-07 11:02:23 -05:00
Daniel James Smith
0e515bc6c1 Remove 2fa.directory/v2 from CSP (#1519) 2022-03-04 19:04:49 +01:00
Addison Beck
e103ddf02f [feature] Refine content of the organization delete request confirmation warning (#1508)
This commit updates the organization delete request confirmation warning based on new copy from the product team.

Changes are as follows:
* Add a load toggle to the organization delete modal, as we now have data to collect.
* Adjust how the families for enterprise error state for invalid sponserships connects with the organization delete component. Previously it just sent in a localization key to use for the description, but this commit adds a union type for identifying different delete flows and moves the FOE description localization key into the template with a condition.
* Move the callout on the organization delete component to above the description text.
* Adjust content of the typical organization delete request description based on copy from the product team.
  * This includes a list of item types in use by the organization that will be deleted and the amount of each type that exist in the organization.
2022-03-04 15:03:48 +01:00
github-actions[bot]
8242989b9d Autosync the updated translations (#1518)
Co-authored-by: github-actions <>
2022-03-04 01:25:14 +01:00
Vincent Salucci
5e7d94efb8 [Captcha] Implement captcha for 2fa (#1513)
* [Captcha] Implement captcha for 2fa

* Removed center justified captcha for now
2022-03-03 18:20:43 -06:00
Addison Beck
3bc8955dd5 [dep] Update jslib (#1517) 2022-03-03 20:14:19 +01:00
Vince Grassia
bc05d27082 Add logic for pushing latest image to QA container registry (#1515) 2022-03-03 14:00:41 -05:00
Addison Beck
e93c155885 [bug] Set full width on login (#1516) 2022-03-03 12:17:19 -05:00
Daniel James Smith
1076749635 Add importing of 1passwords 1pux files (#1507)
* Pull in jslib

* Install jszip

* Display help on selecting 1pux importer

* Unzip 1pux and pass content of export.data to 1pux importer

* Update jslib
2022-03-03 15:41:48 +01:00
Thomas Rittson
06e1af6d48 Improve SSO Config validation (#1332)
* Break form controls up into reusable components

* Add proper form styling, validation, inline error messages, etc

* Move control options into class instead of template

* Add accessibility
2022-03-03 11:08:41 +01:00
github-actions[bot]
cf9a90d10e Bumped version to 2.26.2 (#1511)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-03-02 15:08:58 -06:00
Matt Gibson
6e8c15bccd Fix mobile + url encoding issue (#1510) 2022-03-02 14:49:35 -06:00
Matt Gibson
7d018e4b59 Fix dev server connector headers (#1509) 2022-03-02 14:27:34 -06:00
Chad Scharf
f832cb4138 Remove artifact binding from SSO config UI (#1502) 2022-02-28 13:43:31 -05:00
Addison Beck
b8a23cf014 [feature] Implement scope warning for exports (#1504) 2022-02-28 11:37:02 -05:00
Daniel James Smith
d0c0e80b6c BEEEP: Add missing languages (#1501)
* Pull in jslib

* Import and register missing locales

* Update supportedTranslationLocales
2022-02-25 20:59:07 +01:00
Thomas Rittson
98fb71fcb6 Update jslib (#1494)
* Update jslib

* Add i18n error message

* Update imports
2022-02-25 10:19:37 -05:00
Micaiah Martin
1b52b5a98a Added dry run logic (#1497) 2022-02-25 09:07:54 -05:00
github-actions[bot]
c3e5c74253 Autosync the updated translations (#1500)
Co-authored-by: github-actions <>
2022-02-25 12:27:49 +01:00
Micaiah Martin
df5b175cdf Remove workflows from build trigger (#1496) 2022-02-24 08:42:14 -06:00
Oscar Hinton
1c495e87c9 Add eslint (#1437) 2022-02-24 12:10:07 +01:00
Oscar Hinton
01f128a4a9 Remove dotnet restore (#1493) 2022-02-22 16:22:45 +01:00
Oscar Hinton
a4d5b145ac Exclude connectors from CSP rules (#1489) 2022-02-22 15:46:59 +01:00
Chad Scharf
d944e0e25c We're Hiring (#1492)
Added link to README.md for Bitwarden Careers page.
2022-02-22 14:04:06 +01:00
Vincent Salucci
d141ccca52 [Icons Bug] fa-remove -> bwi-close (#1487) 2022-02-20 17:03:58 -06:00
Micaiah Martin
9e872bed2c Create initial workflow (#1482) 2022-02-18 13:28:53 -06:00
github-actions[bot]
c071b692f2 Autosync the updated translations (#1486)
Co-authored-by: github-actions <>
2022-02-18 10:24:33 +01:00
Matt Gibson
041bb1bf0a Enforce Hold label (#1478)
* Enforce Hold label

* Linting

Co-authored-by: Micaiah Martin <77340197+mimartin12@users.noreply.github.com>
2022-02-16 08:43:56 -06:00
Thomas Rittson
0b5e1eb256 Exclude jslib from prettier hook (#1458)
* Exclude jslib from prettier hook
2022-02-16 07:41:41 +10:00
Addison Beck
8c39fdb21e [lib] Update jslib (#1479) 2022-02-15 15:05:49 -05:00
Addison Beck
ca3efc8fee [bug] Disable state cache (#1477) 2022-02-14 12:26:22 -05:00
github-actions[bot]
c323f38f16 Autosync the updated translations (#1461)
Co-authored-by: github-actions <>
2022-02-11 22:48:28 +01:00
Addison Beck
9df4eb4c0d [bug] Store last sync in memory (#1471) 2022-02-11 03:39:48 -05:00
Addison Beck
1712ed53be [bug] Store vault data in memory (#1470) 2022-02-11 09:21:51 +01:00
github-actions[bot]
45a39f6200 Bumped version to 2.26.1 (#1468)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-02-10 21:51:45 -08:00
Thomas Rittson
a2d241263b Update jslib (#1466) 2022-02-11 00:30:37 -05:00
Thomas Rittson
5987d3deda Update jslib (#1465) 2022-02-11 14:58:33 +10:00
Thomas Rittson
080a3c655e Update jslib (#1464) 2022-02-11 14:30:07 +10:00
Justin Baur
dac48242b7 Update jslib (#1462) 2022-02-10 22:16:44 -05:00
Robyn MacCallum
e4d9ab52a0 update jslib (#1460)
* update jslib

* Remove call to tokenService.clear() from logout
2022-02-10 19:51:36 -05:00
Thomas Rittson
aee8a2661e Fix Copy Verification Code not showing after first log in (#1459)
* Move init logic to load()
2022-02-11 09:48:54 +10:00
Robyn MacCallum
ff6bb236c0 Update jslib (#1457) 2022-02-10 14:22:44 -05:00
Vincent Salucci
f79b20294a [Help] Update links to new pattern (#1454)
* [Help] Update links to new pattern

* Close help

* Update jslib
2022-02-08 17:44:47 -06:00
github-actions[bot]
3a0c34b934 Bump version to 2.26.0 (#1452)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-02-08 15:54:35 -07:00
github-actions[bot]
e09df347f4 Bump version to 1.26.0 (#1450)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-02-08 14:46:12 -07:00
Oscar Hinton
e68ab0031d Client & Version headers (#1434) 2022-02-08 13:22:31 +01:00
Justin Baur
64416c9406 Send in error message (#1449) 2022-02-07 16:15:49 -05:00
Matt Gibson
6779adb064 Handle password protected import export (#1448)
This updates requirements without implementing any way for the UI to
interact with the new feature
2022-02-07 14:15:22 -06:00
Thomas Rittson
1b28a4b954 Update client for authService refactor (#1387)
Co-authored-by: Hinton <oscar@oscarhinton.com>
2022-02-07 20:24:49 +01:00
Addison Beck
6320498fb3 [chore] Update jslib (#1447) 2022-02-07 12:04:51 -05:00
Oscar Hinton
bfd5f3e564 Fix register button using wrong icon (#1446) 2022-02-07 12:28:28 +01:00
Thomas Rittson
c755443735 Make husky pre-commit hook executable (#1432) 2022-02-07 12:18:33 +10:00
Justin Baur
0e5f2530a9 Switch option values to be a number (#1444) 2022-02-04 16:59:20 -05:00
Vincent Salucci
5105633fa4 [Icons] Fix button icon/text margins (#1443) 2022-02-04 13:49:44 -06:00
github-actions[bot]
e975056c21 Autosync the updated translations (#1441)
Co-authored-by: github-actions <>
2022-02-04 12:48:56 +01:00
Daniel James Smith
be21167ef8 Pull jslib for https://github.com/bitwarden/jslib/pull/654 (#1439) 2022-02-03 23:44:28 +01:00
Addison Beck
e09898e4d8 Update jslib (#1436) 2022-02-03 13:54:15 -05:00
Vincent Salucci
868d235faa [Icons] FF - requested icon changes (#1435)
* [Icons] Removed FA

* Icon changes // Webpack correction // Padding updates
2022-02-03 10:20:31 -06:00
Oscar Hinton
5c764a95f4 Add CSP for development (#1431) 2022-02-03 10:17:33 +01:00
Jake Fink
596c3e86e9 Master password policy is not checked when accepting invite from an existing account (#1371)
* validate password against org policy and create update-password component

* linting and prettier

* [bug] Default rememberEmail to true (#1429)

* switching the dashes to underscores for the branch name (#1433)

(cherry picked from commit 8910430dfb)

* fix merge conflicts

* Update src/app/accounts/update-password.component.html

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

* Update src/locales/en/messages.json

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

* update jslib

* prettier

Co-authored-by: Addison Beck <abeck@bitwarden.com>
Co-authored-by: Joseph Flinn <58369717+joseph-flinn@users.noreply.github.com>
Co-authored-by: Justin Baur <admin@justinbaur.com>
2022-02-03 00:00:57 -05:00
Addison Beck
8030da2ed5 [bug] Default rememberEmail to true (#1429) 2022-02-02 11:20:15 -05:00
Joseph Flinn
8910430dfb switching the dashes to underscores for the branch name (#1433) 2022-02-02 07:30:19 -08:00
Joseph Flinn
6bf6d4b47f Reverting tis change for next release (#1430) 2022-02-02 06:22:25 -08:00
Oscar Hinton
ca199a398e Fix captcha and webauthn-fallback not using theme_light (#1427) 2022-02-01 15:46:16 +01:00
Chad Scharf
61ab2fbda3 Hide unsolicited SSO option in config (#1425) 2022-01-31 17:43:56 -05:00
Addison Beck
d79f074825 [bug] Extend GlobalState to supply correct default theme (#1422)
* [bug] Extend GlobalState to supply correct default theme

The default theme for most clients is System, but web uses Light.
We need to extend GlobalState in web to reflect this.

* [chore] Update jslib
2022-01-31 14:53:55 -05:00
Oscar Hinton
e3b962a779 Apply prettier on help pr (#1423) 2022-01-31 20:31:50 +01:00
DanHillesheim
cc657eb853 Update help site URLs (#1409) 2022-01-31 20:11:27 +01:00
DanHillesheim
e14a266ee0 Registration page updates (#1390)
Co-authored-by: Hinton <oscar@oscarhinton.com>
2022-01-31 20:10:35 +01:00
Thomas Rittson
e1732cfa10 Fix various bugs in Options page (#1418)
* Remove duplicate message in toast

* Set starting variables properly
2022-01-31 09:11:25 -05:00
Addison Beck
ce1ae208d1 [bug] Update theme.js to refelect new storage structure (#1416)
* [bug] Update theme.js to refelect new storage structure

* [bug] Remove unecassary defaults
2022-01-28 11:30:45 -05:00
Addison Beck
6996b06fa2 [chore] Update jslib (#1415) 2022-01-28 09:07:33 -05:00
github-actions[bot]
dc503d3461 Autosync the updated translations (#1414)
Co-authored-by: github-actions <>
2022-01-28 11:06:25 +01:00
Daniel James Smith
d95db8fb74 BEEEP: Add importer for Keeper in json format (#1392)
* Updated instructions to export a csv file from Keeper

* Add instructions to export a json file from Keeper

* Bump jslib to include Keeper json importer

* Revert change to README.md

* Pull in jslib
2022-01-27 21:51:56 +01:00
Justin Baur
1a219daa12 Remove F4E vault card (#1413)
* Remove F4E card from vault page

* Remove unneeded property
2022-01-27 15:47:56 -05:00
Vincent Salucci
2ae98887b7 [Icons] Update Font Sheet (#1343)
* [Icons] Update to new font sheet

* Rebased - updated all icon remaining icon references

* Temporarily Updating gitmodules branch

* Fixed class reference

* Revert temporary gitmodule branch

* Icon updates/changes

* Pull jslib m-icon-updates latest

* Prettier

* Update jslib to master

* Reset jslib to master

* Removed obsolete variable reference, replaced bolt references

* Removed all instances of base class - maps create automatically

* Updated toast icon references

* Imported styles to reference variable/map

* Reverted to using base class

* Update jslib

* Rename eye-2 to eye and eye-slash-2 to eye-slash

* Bump jslib

* Remove duplicate scss

* Remove old fa

* Update fallback image

* Bump jslib

* Rename eye-2 to eye, and eye-slash-2 to eye-slash

* Fix 404

* Fix integrity of bootstrap.min.css

* Fix callout missing bwi

* Add bwi to change-kdf

* Remove bwi from callout again

* Bump jslib

Co-authored-by: Hinton <oscar@oscarhinton.com>
2022-01-27 11:25:58 -06:00
Joseph Flinn
f0c47252e4 Updating the base Docker image for testing purposes (#1411) 2022-01-27 07:25:13 -08:00
Danny Murphy
2ffe3bd6ad Cleanup of the SCSS Variables (#1255)
* Clean up variable names and comments

* Fix Option Colour - issue #1338

* Update old scss variable name

Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
2022-01-26 14:54:15 +10:00
Oscar Hinton
f387a4d469 Remove old u2f connector (#1406) 2022-01-21 15:00:53 +01:00
github-actions[bot]
a0f1b4dd0d Autosync the updated translations (#1404)
Co-authored-by: github-actions <>
2022-01-21 11:53:09 +01:00
Thomas Rittson
84a65edc08 Move keyConnectorService call to syncService (#1405) 2022-01-21 19:33:09 +10:00
Addison Beck
caad11c571 [chore] Update jslib (#1403) 2022-01-20 09:38:34 -05:00
Daniel James Smith
b73449159d Update copy on fingerprint phrase prompt (#1399) 2022-01-20 10:07:47 +01:00
Oscar Hinton
bf48434d0f Rename package to @bitwarden/web-vault (#1396) 2022-01-17 17:23:14 +01:00
Oscar Hinton
b6d2d5bf71 [BEEEP] Use shared tsconfig (#1393) 2022-01-17 15:53:19 +01:00
Oscar Hinton
dfd62c7c3a Fix webpack assets using wrong name (#1391) 2022-01-14 13:35:44 +01:00
github-actions[bot]
41d3bd8cf2 Autosync the updated translations (#1389)
Co-authored-by: github-actions <>
2022-01-14 11:21:40 +01:00
Vince Grassia
3292d119fe Update Version Bump action (#1388) 2022-01-12 16:06:50 -05:00
Christian Oliff
b8de92435b Add Inputmode for tel and email (identities) (#1384) 2022-01-12 10:07:56 +01:00
Oscar Hinton
fd1d512a0f Run prettier on #1232 (#1383) 2022-01-10 14:50:54 +01:00
Daniel James Smith
14b8903d9a Fix items not opening when they had a password reprompt set (#1381) 2022-01-10 14:12:35 +01:00
Simon Legner
45284eefb3 Compress images u2fkey/yubikey using avif/webp (#1232)
Co-authored-by: Hinton <oscar@oscarhinton.com>
2022-01-10 12:37:21 +01:00
Daniel James Smith
49f6cfab7f Fixed linting issues (ran prettier) (#1379) 2022-01-07 14:28:25 +01:00
github-actions[bot]
2d271460e3 Autosync the updated translations (#1378)
Co-authored-by: github-actions <>
2022-01-07 13:51:08 +01:00
github-actions[bot]
241004f13b Bumped version to 2.25.1 (#1376)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-01-06 14:15:35 -08:00
Oscar Hinton
2f5d0201fe [BEEEP] Add script for optimizing images (#1374) 2022-01-06 21:20:35 +01:00
Daniel James Smith
7ffb5db310 Add --ignore-unknown to prettier on lint-staged (#1375) 2022-01-06 16:47:55 +01:00
Daniel James Smith
6603521d88 Add all filetypes to prettier and ignore via .prettierignore (#1373) 2022-01-06 15:13:29 +01:00
Addison Beck
d066e0586a [bug] Add defaults for vault timeout (#1365)
* [bug] Add state defaults for vault timeout

* [chore] Update jslib
2022-01-04 11:15:58 -05:00
Daniel James Smith
d0e661b84b Update year in copyright (#1370) 2022-01-03 17:14:50 +01:00
github-actions[bot]
6fa77cef88 Autosync the updated translations (#1368)
Co-authored-by: github-actions <>
2022-01-01 17:51:25 +01:00
Robyn MacCallum
6f408b871f Rename fb to formBuilder (#1369) 2021-12-31 10:06:07 -05:00
Addison Beck
8a9b992757 [bug(Account Switching)] Allow for never lock for dev setups (#1345)
* [bug(Account Switching)] Allow for never lock for dev setups

* [chore] Remove unecassary import

* [style] Ran prettier

* [chore] Update jslib
2021-12-28 10:47:41 -05:00
github-actions[bot]
55ecc4b804 Autosync the updated translations (#1359)
Co-authored-by: github-actions <>
2021-12-24 01:09:22 +01:00
Joseph Flinn
a71ce448f4 Change QA deploy SP & Re-enable feature branch deploy (#1358) 2021-12-23 09:14:10 -05:00
Jake Fink
bc82ae961e update jslib (#1356) 2021-12-21 13:09:09 -05:00
Linus Aarnio
ebcfdcd8a4 Add credit card logos to allow displaying icons based on brand (#1280)
Co-authored-by: Hinton <oscar@oscarhinton.com>
2021-12-21 12:08:08 +01:00
Vince Grassia
8991dcbf32 Set image tag to be 'dev' for now (#1353) 2021-12-20 13:16:53 -05:00
Micaiah Martin
cc9b9c91d7 Patch CI to allow for redeployments. Add Github actions to prettier .ignore as well. (#1352)
* Added inputs for reruns

* Added github workflows to .prettierignore

* Moved the Redeploy logic into the setup job
2021-12-20 10:15:43 -07:00
Daniel James Smith
3880d60101 Fix missing translation for unlinking SSO dialog (#1350) 2021-12-17 21:11:02 +01:00
Jake Fink
f5fdb34f7d Bug/sso properties globalstate (#1342)
* move sso properties in jslib to globalstate

* update jslib

* update jslib with prettier changes
2021-12-17 11:24:58 -05:00
Oscar Hinton
5b8f2034c3 Add .git-blame-ignore-revs (#1349) 2021-12-17 16:07:28 +01:00
Oscar Hinton
56477eb39c Apply Prettier (#1347) 2021-12-17 15:57:11 +01:00
Oscar Hinton
2b0a9d995e Add Prettier configuration (#1346) 2021-12-17 15:44:44 +01:00
github-actions[bot]
595722dfa1 Autosync the updated translations (#1348)
Co-authored-by: github-actions <>
2021-12-17 01:17:36 +01:00
Vince Grassia
6a1e683a93 Update workflows (#1344) 2021-12-16 11:46:26 -05:00
Addison Beck
97ca771a00 [bug] Correct bad BroadcasterService import (#1341)
* [bug] Correct bad BroadcasterService import

* [chore] update jslib
2021-12-14 22:07:56 -05:00
Jake Fink
214f82e142 send configType and redirectBehavior as int instead of string for request (#1339) 2021-12-14 13:25:48 -05:00
Addison Beck
17ae5ee57c [Account Switching] [Refactor] Implement new account centric services (#1220)
* [chore] updated services.module to use account services

* [refactor] sorted services provided by services.module

* [chore] removed references to deleted jslib services

* [chore] used activeAccount over storageService for account level storage items

* [chore] resolved linter warnings

* Refactor activeAccountService to stateService

* [bug] Remove uneeded calls to state service on logout

This was causing console erros on logout. Clearing of data is handled fully in dedicated services, clearing them in state afterwards is essentially a redundant call.

* [bug] Add back null locked callback to VaultTimeoutService

* Move call to get showUpdateKey

* [bug] Ensure HtmlStorageService does not override StateService options and locations

* [bug] Adjust theme logic to pull from the new storage locations

* [bug] Correct theme not sticking on refresh

* [bug] Add enableFullWidth to the account model

* [bug] fix theme option empty when light is selected

* [bug] init state on application start

* [bug] Reinit state when coming back from a lock

* [style] Fix lint complaints

* [bug] Clean state on logout

* [chore] Resolved merge issues

* [bug] Correct default for enableGravitars

* Bump angular to 12.

* Remove angular.json

* Bump rxjs

* Fix build errors, remove file-loader with asset/resource

* Use contenthash

* Bump jslib

* Bump ngx-toastr

* [chore] resolve issues from merge

* [chore] resolve issues from merge

* [bug] Add missing bracket

* Use newer import syntax

* [bug] Correct service orge

* [style] Fix lint complaints

* [chore] update jslib

* [review] Address code review

* [review] Address code review

* [review] Rename providerService to webProviderService

Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com>
Co-authored-by: Hinton <oscar@oscarhinton.com>
2021-12-14 11:10:26 -05:00
Daniel James Smith
71075cf878 Bump node to v16 (#1336)
* Pull in jslib

* Bump engines required to node 16 and npm 8

* Bump @types/node to 16

* Modify build.yml to build with node 16 and npm 8

* Update requirements in README.md

* Removed step to install npm8
npm8 is included in node v16

* Pull jslib
2021-12-13 17:16:57 +01:00
Thomas Rittson
56e2c86a7f Fix name for compatibility with config.js (#1334) 2021-12-10 18:10:20 +10:00
github-actions[bot]
8fba2a693e Autosync the updated translations (#1333)
Co-authored-by: github-actions <>
2021-12-10 01:18:21 +01:00
Oscar Hinton
f582d3e7a6 Bump angular to v12 (#1325) 2021-12-09 22:12:53 +01:00
Vince Grassia
75984a2e37 Remove old 'release' ref in workflow (#1328) 2021-12-07 22:49:27 -05:00
github-actions[bot]
1cba6dc3b9 Bumped version to 2.25.0 (#1327)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2021-12-07 19:46:24 -07:00
Oscar Hinton
a803d58c52 Fix components trying to use anglar bradcastservice vs interface (#1326) 2021-12-07 23:16:47 +01:00
Oscar Hinton
d5c0783619 Replace toaster library (#1322) 2021-12-07 20:41:45 +01:00
github-actions[bot]
35a7d6434a Autosync the updated translations (#1318)
Co-authored-by: github-actions <>
2021-12-07 17:06:08 +01:00
Oscar Hinton
78942cabf2 BEEEP: Refactor services DI (#1313) 2021-12-03 02:32:58 +00:00
Matt Gibson
d9231ae3f3 Fix families sponsorship redeem page (#1321)
* Display sponsorship warning when sponsoring an org

Move actions to drop down menu

Fix revoke cancel success popup

* Only show warning when sponsorship exists
2021-12-01 19:48:05 -06:00
Micaiah Martin
bca7c14319 Create workflow to automatically bump version (#1320) 2021-12-01 12:45:06 -07:00
Thomas Rittson
221931ecaa Update jslib (#1319)
* Update jslib

* Update constructors

* Update jslib
2021-11-29 10:14:49 +10:00
Justin Baur
4b856d9016 Fix basePrice to reflect the sponsorship (#1311)
* Fix basePrice to reflect the sponsorship

* Ran linter

* Add latest copy

* Remove unneeded if

* Fix times

* Stopped hardcoding basePrice

* Stopped hardcoding 40 in UI

* Switch to single small block

* Update jslib

* Revert "Update jslib"

This reverts commit 28534f2230.

* Revert "Remove unneeded if"

This reverts commit 5540b19998.

* Fix revert issue
2021-11-24 16:12:06 -05:00
Matt Gibson
4029554658 Fix formatting and title of sponsoring org drop down (#1317) 2021-11-24 14:41:57 -06:00
Matt Gibson
6ec22a9408 Add sponsorship pre validate to families redeem page (#1315)
* Add sponsorship pre validate to families redeem page

* Update messaging

* update jslib
2021-11-24 14:31:16 -06:00
Matt Gibson
9cc7dfb884 Force sponsorship friendly name to recipient address (#1316) 2021-11-24 13:38:38 -06:00
Oscar Hinton
dca12def8d Run npm lint in CI (#1314) 2021-11-24 19:23:31 +01:00
Arun Pattni
cbf65c5f42 Use 2fa.directory API v3 in inactive 2FA report (#1103)
* Use 2fa.directory API v3 in inactive 2FA report

* Fix issues

* Fix lint error

* Apply suggestions from code review

* Apply style suggestions

* Style fixes
2021-11-24 16:25:01 +01:00
Matt Gibson
f8c943c042 Display sponsored status for sponsored org subscription (#1312)
* Display sponsored status for sponsored org subscription

* Linter fixes
2021-11-24 08:33:34 -06:00
Matt Gibson
346052922e Fix typo (#1310) 2021-11-23 13:49:32 -06:00
Thomas Rittson
2973d06c9f [Key Connector] Fix Key Connector Url test (#1308) 2021-11-23 21:11:47 +10:00
Oscar Hinton
0490314cff [KeyConnector] Updated remove warning in details view (#1309) 2021-11-22 18:39:20 +01:00
Justin Baur
a6abb74810 Feature/families for enterprise (#1300)
* Added manual routing

* Families for enterprise/account settings (#1290)

* Added sponsored families page

* Revert "Added manual routing"

This reverts commit a970ba78ff.

* Add messages to page

* Remove stages and simplify design

* Switch to new figma design

* Add screen reader

* Add calls to server

* Reorder methods

* Used to organization filters

* Connected page to server

* Add preliminary text to subscription page

* Sponsor existing family organization flow

* Update jslib

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

* Add revoke sponsorship flow

* Add spinner to send offer button

* Determine if subscription has sponsored items

* Work on subscription button

* Add  message for new family organization

* Families for enterprise/subscription page (#1292)

* Work on subscription button

* Determine if subscription has sponsored items

* Work on subscriptions page

* Add  message for new family organization

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

* Families for enterprise/redeem card (#1295)

* Add toast localization message

* Use helpers to property display sponsorship items

* Split table rows into component so buttons load (#1296)

* Split table rows into component so buttons load

* Update jslib

* Families for enterprise/localizations (#1299)

* Add more localizations

* Remove unneeded comments

* Fix help article

* Run linting

* Do not show redeem button if no orgs exist to redeem

* Implement new process for accepting sponsorships

* Hide business checkbox

* Update jslib

* Removed commented code

* Remove commented html

* Cleaned up imports

* Use proper message

* Remove merge conflict message

* Remove confusing comment

* Listened to PR feedback

* Remove unused property

* Update help text

* Fix aria labels

* Add try catch

* Made toast before emit

* Minor copy changes

* Update jslib

* Remove unneeded loading

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
2021-11-22 08:41:40 -05:00
Oscar Hinton
0ce00a15e7 Update warning when removing an account using key connector (#1307) 2021-11-19 15:13:55 +01:00
github-actions[bot]
cd90949d27 Autosync the updated translations (#1306)
Co-authored-by: github-actions <>
2021-11-19 13:03:40 +01:00
Thomas Rittson
0d0eb609d3 [Key Connector] Test that Key Connector URL can be reached before saving (#1291)
* Test that Key Connector URL can be reached before saving

* Update jslib

* Add styling to validation messages

* Use inline button, fix styling

* Add accessibility call out to form validation
2021-11-19 21:22:05 +10:00
Thomas Rittson
7c902e61d6 [Key Connector] Add firstSsoLogin event (#1305)
* Add firstSsoLogin event

* Update jslib
2021-11-19 06:01:28 +10:00
Oscar Hinton
1e5c2c35e5 Update verify password/otp component (#1303) 2021-11-18 20:51:04 +01:00
Daniel James Smith
977fdef787 Use Mql.addListener for backwards compat (Safari < v14) (#1304) 2021-11-18 19:03:20 +01:00
Oscar Hinton
d6c419bad8 Disable key connector when org doesn't have the feature (#1301) 2021-11-17 12:11:20 +01:00
Thomas Rittson
f740d8b057 Update jslib and use new UserVerificationService pattern (#1302)
* Use try/catch pattern for userVerification

* Update deps
2021-11-17 09:37:36 +10:00
github-actions[bot]
8889722388 Autosync the updated translations (#1298)
Co-authored-by: github-actions <>
2021-11-15 10:59:18 -05:00
Thomas Rittson
01503f137d Update jslib (#1297) 2021-11-12 09:20:28 +10:00
Oscar Hinton
6171aa89a8 Add required KeyConnectorService to vaultTimeoutService (#1294) 2021-11-11 14:37:16 +01:00
Thomas Rittson
40c37143e0 Fix linting in connectors (#1293) 2021-11-11 13:35:35 +10:00
Vince Grassia
57031e7752 Version bump (#1288) 2021-11-10 14:59:54 -05:00
Oscar Hinton
db5a8df64e [KeyConnector] Add support for key connector OTP (#1256)
Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
2021-11-09 19:24:26 +01:00
github-actions[bot]
e5eb5d61fe Autosync the updated translations (#1278)
Co-authored-by: github-actions <>
2021-11-09 12:50:32 -05:00
Kyle Spearrin
9061af54bf limit duo connector hosts to duo-owned domains (#1283) 2021-11-09 12:17:30 -05:00
Kyle Spearrin
83fed7d66f sanitize data inputs for captcha connector (#1284) 2021-11-09 12:16:10 -05:00
Kyle Spearrin
f8aea1e861 don't use innerHTML for sso handOffMessage (#1285) 2021-11-09 12:15:58 -05:00
Kyle Spearrin
5b6fb16591 remove callbackUri input for fixed mobile uri (#1282) 2021-11-09 11:36:41 -05:00
Justin Baur
278cf2ca40 Fix free trial text to show at the right time (#1281) 2021-11-08 13:14:57 -05:00
Vince Grassia
fe15de02e5 Change release workflow to only allow releases from rc or hotfix branches (#1279) 2021-11-08 09:48:53 -05:00
Justin Baur
b164a39abc Update payment info (#1274)
* Added manual routing

* Add additional copy for free trial

* Revert

* Fix formatting

* Switch text to be on the top of the payment info

* Update to put text at top of the screen
2021-11-05 14:59:45 -04:00
Joseph Flinn
e5f77e2c4e Hotfix Crowdin push (#1276)
* trying a fix for the Crowdin source destination

* adding the dest file extension

* removing testing code
2021-11-04 14:47:39 -07:00
Joseph Flinn
cf460096af Rework Crowdin Integration (#1275)
* Updating the Crowdin push process

* removing the test code in the check-failures job

* Adding a scheduled trigger to the crowdin-pull workflow

* switching the crowdin pull schedule to Friday instead of Saturday

* fixing a indentation issue
2021-11-04 11:15:29 -07:00
Robyn MacCallum
1403ecfa6f Minor dark mode fixes (#1273)
* Fix badge colors and nav bar color

* Make input background transparent in dark mode

* fix badge colors

* remove extra space
2021-11-04 08:35:51 -04:00
Thomas Rittson
8b60d50050 [Linked fields] Add Linked Field as custom field type (#1206)
* Add linked fields

* Update to use Field.linkedId

* Update jslib
2021-11-04 07:41:04 +10:00
Matt Gibson
cf5823fe71 Fix repeat ng insert on safari (#1270) 2021-11-01 16:14:31 -05:00
Matt Gibson
bb0b5f2d87 Show upgrade plan button for free orgs. (#1269)
* Show upgrade plan button for free orgs.

* Add families plan callout for subscription upgrade
2021-11-01 14:29:46 -05:00
Robyn MacCallum
2700caf2a8 Fix jumbo sized WebAuthn logo (#1251)
* Fix jumbo sized WebAuthn logo

* Fix styling on 2FA modals

* Fix so that text does not go below image

* Rearrange items in modal and add new icons

* make spacing a little wider

* Remove 1 from mfaTypes, we now have both versions

Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
2021-10-28 08:20:37 -04:00
Joseph Flinn
523b18156c Adding fixes from last night's release (#1263) 2021-10-27 13:08:37 -07:00
Matt Gibson
7219b394a0 update jslib (#1265)
* update jslib

* update jslib
2021-10-27 13:47:51 -05:00
Matt Gibson
383c29c761 Null Check subscription prior to use (#1264) 2021-10-27 10:03:41 -05:00
Robyn MacCallum
b5231425fb Use useAlertRole input added to app-callout (#1254)
* Use enforceAlert input added to app-callout to still give alerts for screen readers on important callouts

* Update input variable name

* Add brackets to pass value correctly
2021-10-27 08:25:06 -04:00
Thomas Rittson
7cb48e3a81 Add PR template (#1260) 2021-10-27 19:01:32 +10:00
Thomas Rittson
664d10cd06 Add Safari importer (#1261)
* Add instructions for Safari import

* Update jslib
2021-10-27 18:58:15 +10:00
github-actions[bot]
a6a34788a8 Autosync the updated translations (#1259)
Co-authored-by: github-actions <>
2021-10-26 15:10:10 -07:00
Joseph Flinn
381ec7af67 enabling the new release branch to push its docker images (#1258) 2021-10-26 14:20:28 -07:00
Joseph Flinn
8be377c7f8 Version Bump 2.24.0 (#1257) 2021-10-26 13:47:39 -07:00
Oscar Hinton
c46ca2f9e2 Crypto Agent (#1243) 2021-10-26 01:14:16 +02:00
Matt Gibson
6d4f163824 Update local web development instructions (#1208)
* Indicate production with NODE_ENV

* Use local.json config to point to Bitwarden production APIs

* Add proxy configuration to cloud and qa environment

* Move notifications to urls

Co-authored-by: Hinton <oscar@oscarhinton.com>
2021-10-22 07:50:08 -05:00
Thomas Rittson
6c581b3ebc Fixes for dynamic modal a11y (#1237)
* Remove tabindex from modal component templates

* Remove tabindex from modal component templates

* Update jslib
2021-10-22 07:30:25 +10:00
Joseph Flinn
618f950cae Change release branch constraints (#1248)
* updating the release branch constraints

* updating the self host docker image build and release with the new release branch

* renaming the release job for selfhost docker release

* removing unneeded line

* removing the master branch release ci code execution

* updating some verbiage
2021-10-21 10:31:41 -07:00
Matt Gibson
9dd859af7a Limit collection actions presented to permitted (#1247)
* Limit collection actions presented to permitted

* Revert useless move

* Limit vault view to editable ciphers and collections

* Update jslib

* PR review
2021-10-20 16:17:27 -05:00
Oscar Hinton
044ac513ae Remove empty catch blocks, and update tslint rule (#1226) 2021-10-20 18:30:04 +02:00
Thomas Rittson
4447b89b05 Fix btn-link colors in dark mode (#1246) 2021-10-20 07:47:18 +10:00
Matt Gibson
1de569e64d Remove unnecessary fallbacks (#1245)
Web is in lock-step to server version. We do not need fallbacks
since we're sure the server version will support collection permissions split.
2021-10-19 08:08:15 -05:00
Matt Gibson
3ee61fef96 Add user uses to new permission model (#1228) 2021-10-18 13:50:26 -05:00
Vince Grassia
f63b395736 Add notify constraint (#1244) 2021-10-15 13:06:59 -04:00
Jake Fink
ee3c3294f3 Add loading spinner icon to emergency access view (#1235)
* add loading spinner icon to emergency access view

* remove extra space

* Revert changes to package-lock.json
2021-10-14 19:40:35 -04:00
Kyle Spearrin
a7a3381124 New Crowdin updates (#1242)
* New translations messages.json (Romanian)

* New translations messages.json (Korean)

* New translations messages.json (Vietnamese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Ukrainian)

* New translations messages.json (Turkish)

* New translations messages.json (French)

* New translations messages.json (Serbian (Cyrillic))

* New translations messages.json (Slovenian)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Dutch)

* New translations messages.json (Swedish)

* New translations messages.json (Japanese)

* New translations messages.json (Czech)

* New translations messages.json (Italian)

* New translations messages.json (Spanish)

* New translations messages.json (Afrikaans)

* New translations messages.json (Bulgarian)

* New translations messages.json (Catalan)

* New translations messages.json (Belarusian)

* New translations messages.json (Danish)

* New translations messages.json (German)

* New translations messages.json (Greek)

* New translations messages.json (Finnish)

* New translations messages.json (Hebrew)

* New translations messages.json (Hungarian)

* New translations messages.json (English, United Kingdom)

* New translations messages.json (Serbian (Latin))

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Kannada)

* New translations messages.json (Sinhala)

* New translations messages.json (Malayalam)

* New translations messages.json (Filipino)

* New translations messages.json (Esperanto)

* New translations messages.json (Bengali)

* New translations messages.json (Hindi)

* New translations messages.json (Azerbaijani)

* New translations messages.json (Latvian)

* New translations messages.json (Estonian)

* New translations messages.json (Norwegian Nynorsk)

* New translations messages.json (Croatian)

* New translations messages.json (Indonesian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (English, India)
2021-10-14 19:23:08 -04:00
Thomas Rittson
98bd41d4b1 [Refactor] Use rxjs first instead of unsubscribe from queryParams (#1229)
* Use rxjs first instead of unsubscribe

* Use rxjs first instead of unsubscribe

* Update jslib

* Update jslib

* Downgrade jslib to before breaking changes
2021-10-15 08:59:43 +10:00
github-actions[bot]
356262975c Autosync the updated translations (#1241)
Co-authored-by: github-actions <>
2021-10-14 18:00:44 -04:00
Vince Grassia
a35024e61d Add Slack alerts for Build workflow failures (#1240) 2021-10-14 14:34:31 -04:00
Matt Portune
df9733081b Mobile WebAuthn connector updates (#1236)
* support for returning to app via button press and updated mobile connector UI

* added client-driven header text and fixed lint issues
2021-10-14 11:53:34 -04:00
Oscar Hinton
db9ab9f51e Remove max from minutes in vault timeout input (#1239) 2021-10-14 15:54:50 +02:00
Oscar Hinton
1b8f316066 Move avatar component to jslib (#1233) 2021-10-14 09:11:20 +02:00
Oscar Hinton
c3a910e785 Prevent disabling single org when max vault timeout policy is enabled (#1230) 2021-10-14 09:01:23 +02:00
Oscar Hinton
4b4b5910e3 Fix sso copy buttons not behaving correctly (#1234) 2021-10-14 08:48:42 +02:00
Oscar Hinton
471490f14f Bump jsib (#1231) 2021-10-12 14:51:10 +02:00
Oscar Hinton
009e125afd Bump jslib (#1227) 2021-10-11 19:40:38 +02:00
Thomas Rittson
c682f460b2 Enforce password reprompt from reports pages (#1225) 2021-10-11 09:32:30 -04:00
Vincent Salucci
fa6f33cbc5 [Reset Password] Update jslib (#1222)
* Update jslib

* Updated constructor
2021-10-08 16:55:23 -05:00
Danny Murphy
ae7493efcf Toast without Navbar Styling (#1223)
* layout_frontend toast update

Changes top when the navbar isn't present so the toast position appears where expected

* Update toasts.scss

* Update toasts.scss
2021-10-08 20:48:33 +02:00
Danny Murphy
fc7a7281fe Toast without Navbar Styling (#1210)
* layout_frontend toast update

Changes top when the navbar isn't present so the toast position appears where expected

* Update toasts.scss
2021-10-08 20:25:25 +02:00
Joseph Flinn
7b21e380cb Add release assets (#1218)
* adding new build artifacts

* fixing some version issues

* fixing syntax error

* fixing asset names

* updating the release workflow to release the new build assets
2021-10-08 09:54:42 -07:00
Thomas Rittson
2e4c6b7828 Update jslib (#1221) 2021-10-08 09:56:40 +10:00
Danny Murphy
d4b13c461d Further Dark Theme QA Fixes (#1217)
* Add webAuthn logo for dark theme

* Add alt tags to 2FA logo images
2021-10-08 08:47:32 +10:00
Matt Gibson
37752b566b Match formatting to other button groups (#1219) 2021-10-07 08:05:17 -05:00
Oscar Hinton
3eda0aa2cd Remove Business Portal and add SSO configuration (#1213) 2021-10-06 20:45:45 +02:00
Oscar Hinton
4ff38c7148 Add validation to ensure maximum vault timeout is larger than 0 (#1215) 2021-10-06 17:27:55 +02:00
Matt Gibson
998d36a5d1 Feature/split manage collections permission (#1211)
* Update guard services and routing

* Add depenent checkbox to handle sub permissions

* Present new collections premissions

* Use new split permissions

* Rename to nested-checkbox.component

* Clarify css class name

* update jslib
2021-10-05 11:12:44 -05:00
Thomas Rittson
7a43510cf5 Various Dark Theme fixes per QA feedback (#1212)
* Fix CORS issue on in-line theming javascript

* Fix date picker icon color

* Add comment

* Fix table theming in dark mode

* Selfhosted navbar fix

* Rename selector to avoid clashing with bootstrap

* Do not set initial theme if default

* Fix .text-danger style in dropdown lists

* Fix toast style, restructure toast and card scss

* Fix table and dropdown list hover color

* Use callout component for Disable Send warning

* Remove unneeded theming for hovering over links

* Undo changes to register enterprise2 layout

* Apply theming to Safari input field icons

e.g. Caps lock, password autofill

* Selectively apply themed logo CSS

* Fix unrelated linting

* Fix webpack config to bundle theme.js

Co-authored-by: Danny Murphy <6512845+dltmurphy@users.noreply.github.com>
2021-10-05 20:03:24 +10:00
Danny Murphy
0c02cfea2f Dark Theme (#1017)
* Stylesheets

* Theme Configuration

* Options Area

* swal2 style

* Icon styling

* Fix theme not saving

* Update English

* Update messages.json

* dropdown and login logo

* btn-link and totp fix

* Organisation Styling

* Update webauthn-fallback.ts

* Fix contrast issues

* Add Paypal Container and Loading svg file

* Password Generator contrast fix

* Dark Mode Fix buttons and foreground

* Fix button hover

* Fix Styles after rebase

* Add hover on nav dropdown-item

* Disable Theme Preview

* Options Fix for Default Theme Changes

* Updated Colour Scheme

* Toast fix

* Button and Text Styling

* Options Update and Messages Fix

* Added Search Icon and Fixed Callout styling

* Add theme styling to Stripe

* Refactor logic for setting color

* Reorder logic to avoid race condition

* PayPal Loading and Misc Fix

* text-state bug fix

* Badge Colour Fix

* Remove PayPal Tagline

The colour cannot be styled so it's not visible on a dark theme

* Adding the Styling from #1131

* Update to New Design

* Form and Nav restyle

* Modal Opacity and Callout

* Nav Colours

* Missing Borders

* Light theme fix

* Improved border for listgroup

* Change Org Nav Colour

* Save theme to localStorage for persistence

* Undo change to Wired image

* !Important removal and tweaks

* Fix regression with navbar

* Light theme by default

* Refactor to use getEffectiveTheme

* Refactor theme constants to use enum

* Set theme in index.html before app loads

* Use scss selector to set logo image

* Export Sass to TS

* Update jslib

Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
2021-09-30 08:06:20 +10:00
Oscar Hinton
aa58749b34 Bump signalr to 5.0.10 (#1207) 2021-09-28 16:19:07 +02:00
Matt Gibson
c98a189430 Organization autoscaling (#1193)
* Add seat autoscale component

* Move small description under title

* tweak autoscale terminology

* Linter fixes

* Use single component for org subscription updates

* Delete unused localization string

* Clarify max bill copy

* Remove cancel from org subscription adjustment

* Update jslib

* PR review

* update jslib

* Simplify success toast
2021-09-27 14:23:12 -05:00
Joseph Flinn
1df2225a52 Ran the new linter over the web project and found some errors (#1197) 2021-09-24 09:28:37 -07:00
Oscar Hinton
f8b0c2ffe4 Use Webfonts from jslib instead of downloading them using gulp (#1205)
* Use Webfonts from jslib instead of downloading them using gulp

* Bump jslib.
2021-09-24 12:24:58 -04:00
Vincent Salucci
ce3311a0dc [Reset Password v1] Refactor ForcePasswordReset flow (#1188)
* [Reset Password v1] Refactor ForcePasswordReset flow

* Update jslib
2021-09-24 08:33:45 -05:00
Joseph Flinn
15ea87d6b6 Revert "adding temp workflow to enable the deploy workflow on the rc branch (#1201)" (#1203)
This reverts commit 0481bf07e2.
2021-09-23 06:15:13 -07:00
Joseph Flinn
0481bf07e2 adding temp workflow to enable the deploy workflow on the rc branch (#1201) 2021-09-21 18:58:22 -07:00
Joseph Flinn
7d01ad4e20 Version Bump (#1200)
* Bumping version for September's release

* Manually updating the version in the package-lock.json
2021-09-21 14:49:26 -07:00
github-actions[bot]
9db6f0bfc2 Autosync the updated translations (#1199)
Co-authored-by: github-actions <>
2021-09-21 13:54:05 -07:00
Joseph Flinn
ab0ce71db8 Updating to new CI model (#1196)
* starting the new pipeline model update

* updating the deploy portion of the pipeline

* adding a stub for the release notes

* removing the redundant deploy workflow

* fixing the cloud job. Adding a npm pre-cache

* updating the hashFile for the caches

* removing the cache-hit check since the logic doesn't work for node_modules

* checking out the repo in the precache

* removing the pre-cache step. Seems to slow down the pipeline overall

* ghpage-deploy with the correct input for the versions

* testing a custom action for the DCT setup

* fixing a typo

* fixing the shell issue in the custom action

* removing a conditional to run a test

* testing redaction

* fixing the weird colon inline with run issue

* commenting out the DCT for testing

* test passed. Updating the release pipeline with the new Setup DCT action

* updating the DCT setup action hash

* updating the release workflow with the linter suggestions
2021-09-21 09:37:17 -07:00
Thomas Rittson
582ddc041b Move custom fields to separate components (#1192)
* Move add-edit custom fields to own component

* Update jslib

* Fix import

* Update jslib
2021-09-21 10:48:36 +10:00
Oscar Hinton
f1e0f70375 Use explicit import paths (#1195)
* Update imports to not use index files

* Bump jslib
2021-09-17 15:44:34 +02:00
Vincent Salucci
eaba23d4ba [SSO/Auto Enroll] Fixed typo for banner (#1194) 2021-09-16 23:00:57 -05:00
Vincent Salucci
ebb945a0c4 Update jslib (#1191) 2021-09-15 21:32:55 -05:00
Oscar Hinton
7daba63c56 Add policy for disabling personal vault export (#1189) 2021-09-15 21:05:02 +02:00
Oscar Hinton
30d2aeb6a3 Update build commands (#1180) 2021-09-14 13:26:26 +02:00
Matt Gibson
c82d1b3c50 Use api action directive for confirm action (#1153) 2021-09-13 07:46:16 -05:00
Thomas Rittson
8180aaa4cc Add warning about 2FA when changing account email (#1186)
* Add warning about 2FA when changing account email

* Fix linting

* Fix code style and warning wording
2021-09-13 10:49:24 +10:00
Oscar Hinton
a1c1fea976 Vault Timeout Policy (#1171) 2021-09-10 15:27:00 +02:00
Thomas Rittson
17166dad4d Update jslib (#1185) 2021-09-10 07:50:54 +10:00
Joseph Flinn
7f76084109 Move WebConstants values to environment config files (#1184)
* Moving the web constants to the app config for more flexibility

* removing personal integrations from QA

* changing the PayPal Configuration setup to match the pattern in the services module

* removing the webConstants file after successful test

* renaming the braintree config key to something more understandable
2021-09-09 14:18:46 -07:00
Matt Portune
fb89421b09 Remove redundant error messaging (#1187)
* Remove redundant error messaging

Remove the "WebAuth Error" prefix from WebAuthn error strings

* Update src/connectors/webauthn.ts

Co-authored-by: Oscar Hinton <oscar@oscarhinton.com>

Co-authored-by: Oscar Hinton <oscar@oscarhinton.com>
2021-09-09 11:13:33 -04:00
Joseph Flinn
9972c8ac61 Updating jslib for the removal of the web constants (#1183) 2021-09-08 12:50:44 -07:00
Joseph Flinn
7e95476dce Adding QA info.json file (#1182)
* adding a file to the QA container to enable easy tracking the version of the currently deployed web client

* adding another visual representation of the QA version
2021-09-08 10:08:31 -07:00
Matt Portune
ded636ba0c Possible fix for blocked nav on some devices (#1181)
I have a device that is blocking navigation (per chrome dev tools) on the success callback for reasons unknown.  After comparing with the captcha connector (which works flawlessly), the only difference I can find is that captcha doesn't do anything else after `document.location.replace` for mobile.  I'm not sure if this is the culprit but it can't hurt to try.
2021-09-08 10:32:53 -04:00
Oscar Hinton
9269774aed Add additional context to issue template (#1179) 2021-09-08 10:58:46 +02:00
Thomas Rittson
dd47eed7c7 Disable personal imports if Personal Ownership policy applies (#1176)
* Disable imports if personal ownership policy set

* Add missing await
2021-09-08 07:19:49 +10:00
Vincent Salucci
f584950dda [SSO/Auto Enroll] Set Password banner (#1169)
* [SSO/Auto Enroll] Set Password banner

* Update jslib
2021-09-03 16:26:38 -05:00
Thomas Rittson
3a25b1fb20 Add event logging for ResetSsoLink (#1173)
* Add event logging for ResetSsoLink

* Updated jslib with new event-type

Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>
2021-09-03 10:20:59 -04:00
Vince Grassia
9832deb20c Update workflows with linter suggestions (#1174) 2021-09-02 16:05:17 -04:00
Vincent Salucci
ca00fda023 [Policy] Reset Password data bugfix (#1175) 2021-09-02 11:42:27 -05:00
Thomas Rittson
bc73452400 Move policy checks within policyService (#1149)
* Refactor: use new policyService methods

* Update jslib
2021-09-02 07:51:04 +10:00
Oscar Hinton
cc359e905b Add issue template and template chooser (#1164)
* Add issue template, and template chooser
2021-09-01 16:18:26 +02:00
Oscar Hinton
7fd9427801 Bump version to 2.22.3 (#1170) 2021-09-01 09:54:19 +02:00
Oscar Hinton
6878794bd0 Add notifications to development config (#1166) 2021-08-31 20:05:41 +02:00
Oscar Hinton
e69e85d8b3 [Provider] Verify user is owner of organization (#1167) 2021-08-31 19:01:29 +02:00
Oscar Hinton
2235664bed Fix ModalService refactor bugs (#1168) 2021-08-31 16:42:43 +02:00
Matt Portune
f08b6e7975 Remove assetlinks (#1165)
* remove assetlinks copy command

* remove android debug key hash

* Delete assetlinks.json

delete assetlinks file

* remove assetlinks copy command
2021-08-30 21:40:34 -04:00
Vincent Salucci
2e868c8111 [Callout] Removed redundant code (#1131)
* [Callout] ARemoved redundant code

* Fixed formatting

* Update jslib

* Updated ul styling for policy options callout

* Update jslib
2021-08-30 16:19:31 -05:00
Oscar Hinton
1c3488a8db Fix Require SSO Policy prerequisite check (#1163) 2021-08-28 16:27:37 +02:00
Joseph Flinn
9c187e9430 Simplifying the crowdin sync workflow (#1155) 2021-08-27 10:08:26 -07:00
Joseph Flinn
b9d0226ede fixing env var spelling error (#1160) 2021-08-27 09:12:50 -07:00
Joseph Flinn
bb30f3b7c3 Enable redeploying same image for QA (#1159)
* Fixing the redeploy of the same docker image to QA

* removing a line that snuck in
2021-08-27 08:40:35 -07:00
Oscar Hinton
fa4e5250b9 Add show/hide button to password prompt (#1034) 2021-08-27 14:50:58 +02:00
Oscar Hinton
7c8e95d408 Disable auto prompt for mobile chrome (#1156) 2021-08-26 18:14:18 +02:00
Oscar Hinton
ccdf05a635 Add connector for mobile webauthn (#1154)
Adds a dedicated connector for handling WebAuthN for our mobile application. Which uses redirects instead of postMessage.
2021-08-26 17:39:52 +02:00
Matt Gibson
66bd8be2c9 Set urls from config file (#1151)
* Set environment URLs in webpack config.

* Provide non NULL dev server

* QA env uses the pq TLD

* Include icons in qa env

* Move base configs to develop.

local configurations should be done in the `./config/local.json` file.

* Fix config override loading to default to development

* Standardize url formatting

* Limit QA settings to those set in production

* Set self hosted in a config

* Specify cloud instead of production

Self hosted and cloud are both production environments.
The ENV setting is used to specify the env type while
NODE_ENV specifies whether development error handling and services.

* Update config instructions

* Remove invalid json

* Change env `production` references to `cloud`

* Fix formatting
2021-08-25 13:15:31 -05:00
Oscar Hinton
2cbe023a38 Refactor orgnaization policy management (#1147) 2021-08-25 16:10:17 +02:00
Joseph Flinn
8a259516df reverting the Chinese translations that got swapped (#1150) 2021-08-24 14:41:52 -07:00
Joseph Flinn
9bb252f954 Crowdin Automation Cleanup (#1148)
* adding a "global variable" syntax for env vars

* switching diff branch to a testing branch instead of master

* adding base branch to the PR creation

* adding the diff branch to be the base branch to branch off of

* switching the diff branch back to master

* updating the last half of the workflow to use the new global var format
2021-08-24 11:58:34 -07:00
Chad Scharf
26cc36a91e Version bump 2.22.2 (#1142) 2021-08-20 16:13:42 -04:00
Oscar Hinton
f9e375f5ad Fix role not being displayed in organization user table (#1141) 2021-08-20 15:36:13 -04:00
Daniel James Smith
c7de347cec Fixed order of supportedLocales to have en as fallback again (#1140) 2021-08-19 22:41:20 +02:00
Oscar Hinton
f2e591086e Bump version to 2.22.1 (#1138) 2021-08-19 13:27:23 +02:00
Oscar Hinton
361022fc26 Overwrite icon service url for prod (#1137) 2021-08-19 10:13:35 +02:00
github-actions[bot]
d8a684da92 Autosync Crowdin translations (#1134)
Co-authored-by: github-actions <>
2021-08-18 15:27:07 -07:00
Joseph Flinn
c1cdd8a843 adding in a line that was mistakenly removed (#1133) 2021-08-18 14:24:34 -07:00
Oscar Hinton
4e134823df Avoid showing provider form if proivder is set up (#1128) 2021-08-18 11:35:43 +02:00
Matt Gibson
cdab6e7091 2.22.0 (#1130) 2021-08-17 14:03:59 -05:00
Matt Portune
a7153d183b Update app-id.json (#1129)
testing sha256 apk key hash against debug build
2021-08-17 10:29:20 -04:00
Thomas Rittson
bbdddcef6e Fix bug causing duplicate error messages (#1124) 2021-08-16 13:29:48 +10:00
Joseph Flinn
55b27d4607 adding logic to gracefully handle scenarios with no crowdin changes (#1126) 2021-08-13 13:27:56 -07:00
Matt Gibson
b47835df68 Set iframe allow on window load (#1125)
* Set webauthn allow on initial page load

* Update jslib
2021-08-13 09:23:51 -05:00
Matt Gibson
919af717b9 Do not call parent if callback given (#1123) 2021-08-12 17:01:18 -05:00
Joseph Flinn
b9b20bc36b Fix crowdin sync (#1122)
* fixing syntax error

* changing the way we check the number of build status tries

* adding in the Crowdin Api Token env var to the main step

* Breaking up the Crowdin update step into smaller manageable steps

* fixing env var for the download step

* fixing build id env for download

* Fixing PR branch env vars

* adding in a different way of pushing if branch already exists

* fixing the git bot user
2021-08-11 09:01:29 -07:00
676 changed files with 120736 additions and 52172 deletions

View File

@@ -12,7 +12,7 @@ insert_final_newline = true
[*.{js,ts,scss,html}]
charset = utf-8
indent_style = space
indent_size = 4
indent_size = 2
[*.{ts}]
quote_type = single

8
.eslintignore Normal file
View File

@@ -0,0 +1,8 @@
**/dist
**/build
jslib
webpack.config.js
scripts/optimize.js
config.js
**/node_modules

31
.eslintrc.json Normal file
View File

@@ -0,0 +1,31 @@
{
"root": true,
"env": {
"browser": true
},
"extends": ["./jslib/shared/eslintrc.json"],
"rules": {
"import/order": [
"error",
{
"alphabetize": {
"order": "asc"
},
"newlines-between": "always",
"pathGroups": [
{
"pattern": "jslib-*/**",
"group": "external",
"position": "after"
},
{
"pattern": "src/**/*",
"group": "parent",
"position": "before"
}
],
"pathGroupsExcludedImportTypes": ["builtin"]
}
]
}
}

2
.git-blame-ignore-revs Normal file
View File

@@ -0,0 +1,2 @@
# Apply Prettier https://github.com/bitwarden/web/pull/1347
56477eb39cfd8a73c9920577d24d75fed36e2cf5

4
.gitattributes vendored
View File

@@ -1,3 +1 @@
*.sh eol=lf
.dockerignore eol=lf
dockerfile eol=lf
* text=auto eol=lf

93
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View File

@@ -0,0 +1,93 @@
name: Bug Report
description: File a bug report
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
Please do not submit feature requests. The [Community Forums](https://community.bitwarden.com) has a section for submitting, voting for, and discussing product feature requests.
- type: textarea
id: reproduce
attributes:
label: Steps To Reproduce
description: How can we reproduce the behavior.
value: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. Click on '...'
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Result
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual Result
description: A clear and concise description of what is happening.
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots or Videos
description: If applicable, add screenshots and/or a short video to help explain your problem.
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Add any other context about the problem here.
- type: dropdown
id: os
attributes:
label: Operating System
description: What operating system are you seeing the problem on?
multiple: true
options:
- Windows
- macOS
- Linux
- Android
- iOS
validations:
required: true
- type: input
id: os-version
attributes:
label: Operating System Version
description: What version of the operating system(s) are you seeing the problem on?
- type: dropdown
id: browsers
attributes:
label: Web Browser
description: What browser(s) are you seeing the problem on?
multiple: true
options:
- Chrome
- Safari
- Microsoft Edge
- Firefox
- Opera
- Brave
- Vivaldi
validations:
required: true
- type: input
id: browser-version
attributes:
label: Browser Version
description: What version of the browser(s) are you seeing the problem on?
- type: input
id: version
attributes:
label: Build Version
description: What version of our software are you running? (Bottom of the page)
validations:
required: true

14
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
blank_issues_enabled: false
contact_links:
- name: Feature Requests
url: https://community.bitwarden.com/c/feature-requests/
about: Request new features using the Community Forums. Please search existing feature requests before making a new one.
- name: Bitwarden Community Forums
url: https://community.bitwarden.com
about: Please visit the community forums for general community discussion, support and the development roadmap.
- name: Customer Support
url: https://bitwarden.com/contact/
about: Please contact our customer support for account issues and general customer support.
- name: Security Issues
url: https://hackerone.com/bitwarden
about: We use HackerOne to manage security disclosures.

28
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,28 @@
## Type of change
- [ ] Bug fix
- [ ] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [ ] Other
## Objective
<!--Describe what the purpose of this PR is. For example: what bug you're fixing or what new feature you're adding-->
## Code changes
<!--Explain the changes you've made to each file or major component. This should help the reviewer understand your changes-->
<!--Also refer to any related changes or PRs in other repositories-->
- **file.ext:** Description of what was changed and why
## Screenshots
<!--Required for any UI changes. Delete if not applicable-->
## Before you submit
- [ ] I have checked for **linting** errors (`npm run lint`) (required)
- [ ] This change requires a **documentation update** (notify the documentation team)
- [ ] This change has particular **deployment requirements** (notify the DevOps team)

View File

@@ -1,3 +1,4 @@
---
name: Build
on:
@@ -8,13 +9,17 @@ on:
required: false
push:
branches-ignore:
- 'l10n_master'
- 'gh-pages'
- "l10n_master"
- "gh-pages"
- "deploy"
paths-ignore:
- '.github/workflows/**'
jobs:
cloc:
name: CLOC
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
@@ -28,25 +33,59 @@ jobs:
run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
build-selfhost:
name: Build SelfHost Docker image
runs-on: ubuntu-latest
lint:
name: Lint
runs-on: ubuntu-20.04
steps:
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '14'
- name: Update NPM
run: |
npm install -g npm@7
- name: Checkout repo
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
- name: Cache npm
id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with:
path: '~/.npm'
key: ${{ runner.os }}-${{ github.run_id }}-npm-${{ hashFiles('**/package-lock.json') }}
path: "~/.npm"
key: ${{ runner.os }}-npm-lint-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
setup:
name: Setup
runs-on: ubuntu-20.04
outputs:
version: ${{ steps.version.outputs.value }}
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Get GitHub sha as version
id: version
run: echo "::set-output name=value::${GITHUB_SHA:0:7}"
build-oss-selfhost:
name: Build OSS zip
runs-on: ubuntu-20.04
needs:
- setup
- lint
env:
_VERSION: ${{ needs.setup.outputs.version }}
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Set up Node
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: "16"
- name: Print environment
run: |
@@ -58,56 +97,125 @@ jobs:
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
- name: Login to Azure
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Install dependencies
run: npm ci
- name: Retrieve secrets
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
with:
keyvault: "bitwarden-prod-kv"
secrets: "docker-password,
docker-username,
dct-delegate-2-repo-passphrase,
dct-delegate-2-key"
- name: Log into Docker
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
run: echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
env:
DOCKER_USERNAME: ${{ steps.retrieve-secrets.outputs.docker-username }}
DOCKER_PASSWORD: ${{ steps.retrieve-secrets.outputs.docker-password }}
- name: Setup Docker Trust
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
- name: Build OSS selfhost
run: |
mkdir -p ~/.docker/trust/private
npm run dist:oss:selfhost
zip -r web-$_VERSION-selfhosted-open-source.zip build
echo "$DCT_DELEGATE_KEY" > ~/.docker/trust/private/$DCT_DELEGATION_KEY_ID.key
env:
DCT_DELEGATION_KEY_ID: "c9bde8ec820701516491e5e03d3a6354e7bd66d05fa3df2b0062f68b116dc59c"
DCT_DELEGATE_KEY: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-key }}
- name: Upload build artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: web-${{ env._VERSION }}-selfhosted-open-source.zip
path: ./web-${{ env._VERSION }}-selfhosted-open-source.zip
if-no-files-found: error
build-cloud:
name: Build Cloud zip
runs-on: ubuntu-20.04
needs:
- setup
- lint
env:
_VERSION: ${{ needs.setup.outputs.version }}
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Restore
run: dotnet tool restore
- name: Set up Node
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: "16"
- name: Print environment
run: |
whoami
node --version
npm --version
gulp --version
docker --version
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
- name: Install dependencies
run: npm install
run: npm ci
- name: Build Cloud
run: |
npm run dist:bit:cloud
zip -r web-$_VERSION-cloud-COMMERCIAL.zip build
- name: Upload build artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: web-${{ env._VERSION }}-cloud-COMMERCIAL.zip
path: ./web-${{ env._VERSION }}-cloud-COMMERCIAL.zip
if-no-files-found: error
build-commercial-selfhost:
name: Build SelfHost Docker image
runs-on: ubuntu-20.04
needs:
- setup
- lint
env:
_VERSION: ${{ needs.setup.outputs.version }}
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Set up Node
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: "16"
- name: Print environment
run: |
whoami
node --version
npm --version
gulp --version
docker --version
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
- name: Setup DCT
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
id: setup-dct
uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff
with:
azure-creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
azure-keyvault-name: "bitwarden-prod-kv"
- name: Install dependencies
run: npm ci
- name: Build
run: |
echo -e "# Building Web\n"
echo "Building app"
echo "npm version $(npm --version)"
npm run dist:selfhost
npm run dist:bit:selfhost
zip -r web-$_VERSION-selfhosted-COMMERCIAL.zip build
- name: Upload build artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: web-${{ env._VERSION }}-selfhosted-COMMERCIAL.zip
path: ./web-${{ env._VERSION }}-selfhosted-COMMERCIAL.zip
if-no-files-found: error
- name: Build Docker image
run: |
echo -e "\nBuilding Docker image"
docker --version
docker build -t bitwarden/web .
@@ -120,48 +228,81 @@ jobs:
if: github.ref == 'refs/heads/master'
run: docker tag bitwarden/web bitwarden/web:dev
- name: Tag hotfix branch
if: github.ref == 'refs/heads/hotfix-rc'
run: docker tag bitwarden/web bitwarden/web:hotfix-rc
- name: List Docker images
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
run: docker images
- name: Push rc images
- name: Push rc image
if: github.ref == 'refs/heads/rc'
run: docker push bitwarden/web:rc
env:
env:
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
- name: Push dev images
- name: Push dev image
if: github.ref == 'refs/heads/master'
run: docker push bitwarden/web:dev
env:
env:
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
- name: Push hotfix image
if: github.ref == 'refs/heads/hotfix-rc'
run: docker push bitwarden/web:hotfix-rc
env:
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
- name: Log out of Docker
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
run: |
docker logout
echo "DOCKER_CONTENT_TRUST=0" >> $GITHUB_ENV
- name: Login to Azure - QA Subscription
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
with:
creds: ${{ secrets.AZURE_QA_KV_CREDENTIALS }}
- name: Login to Azure ACR
run: az acr login -n bitwardenqa
- name: Tag and Push RC to Azure ACR QA registry
env:
REGISTRY: bitwardenqa.azurecr.io
run: |
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") # slash safe branch name
if [[ "$IMAGE_TAG" == "master" ]]; then
IMAGE_TAG=dev
fi
docker tag bitwarden/web \
$REGISTRY/web-sh:$IMAGE_TAG
docker push $REGISTRY/web-sh:$IMAGE_TAG
- name: Log out of Docker
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
run: docker logout
build-qa:
name: Build QA Docker image
runs-on: ubuntu-latest
name: Build Docker images for QA environment
runs-on: ubuntu-20.04
needs:
- setup
- lint
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
with:
node-version: '14'
- name: Update NPM
run: |
npm install -g npm@7
- name: Cache npm
id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with:
path: '~/.npm'
key: ${{ runner.os }}-${{ github.run_id }}-npm-${{ hashFiles('**/package-lock.json') }}
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: "16"
- name: Print environment
run: |
@@ -181,12 +322,6 @@ jobs:
- name: Log into container registry
run: az acr login -n bitwardenqa
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Restore
run: dotnet tool restore
- name: Install dependencies
run: npm ci
@@ -195,26 +330,32 @@ jobs:
echo -e "# Building Web\n"
echo "Building app"
echo "npm version $(npm --version)"
npm run build:qa
VERSION=$( jq -r ".version" package.json)
jq --arg version "$VERSION - ${GITHUB_SHA:0:7}" '.version = $version' package.json > package.json.tmp
mv package.json.tmp package.json
npm run build:bit:qa
echo "{\"commit_hash\": \"$GITHUB_SHA\", \"ref\": \"$GITHUB_REF\"}" | jq . > build/info.json
echo -e "\nBuilding Docker image"
docker --version
docker build -t bitwardenqa.azurecr.io/web .
- name: Get image tag
id: image_tag
id: image-tag
run: |
IMAGE_TAG=$(echo "$GITHUB_REF" | awk '{split($0, a, "/"); print a[3];}')
TAG_EXTENSION=${{ github.events.inputs.custom_tag_extension }}
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g")
TAG_EXTENSION=${{ github.event.inputs.custom_tag_extension }}
if [[ $TAG_EXTENSION ]]; then
IMAGE_TAG=$IMAGE_TAG-$TAG_EXTENSION
fi
fi
echo "::set-output name=value::$IMAGE_TAG"
- name: Tag image
env:
IMAGE_TAG: ${{ steps.image_tag.outputs.value }}
IMAGE_TAG: ${{ steps.image-tag.outputs.value }}
run: docker tag bitwardenqa.azurecr.io/web "bitwardenqa.azurecr.io/web:$IMAGE_TAG"
- name: Tag dev
@@ -226,7 +367,7 @@ jobs:
- name: Push image
env:
IMAGE_TAG: ${{ steps.image_tag.outputs.value }}
IMAGE_TAG: ${{ steps.image-tag.outputs.value }}
run: docker push "bitwardenqa.azurecr.io/web:$IMAGE_TAG"
- name: Push dev images
@@ -239,30 +380,26 @@ jobs:
windows:
name: Test code on Windows
runs-on: windows-latest
runs-on: windows-2019
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Set up NuGet
uses: nuget/setup-nuget@04b0c2b8d1b97922f67eca497d7cf0bf17b8ffe1
with:
nuget-version: 'latest'
- name: Set up MSBuild
uses: microsoft/setup-msbuild@c26a08ba26249b81327e26f6ef381897b6a8754d
nuget-version: "latest"
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
with:
node-version: '14'
- name: Update NPM
run: |
npm install -g npm@7
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: "16"
- name: Print environment
run: |
nuget help | grep Version
msbuild -version
dotnet --info
node --version
npm --version
echo "GitHub ref: $GITHUB_REF"
@@ -271,12 +408,118 @@ jobs:
GITHUB_REF: ${{ github.ref }}
GITHUB_EVENT: ${{ github.event_name }}
- name: Install dependencies
run: npm ci
- name: NPM build
run: npm run build:bit:cloud
crowdin-push:
name: Crowdin Push
if: github.ref == 'refs/heads/master'
needs:
- build-oss-selfhost
- build-cloud
- build-commercial-selfhost
- build-qa
runs-on: ubuntu-20.04
env:
_CROWDIN_PROJECT_ID: "308189"
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: npm install
run: npm install
- name: Login to Azure
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: npm build
run: npm run build:prod
- name: Retrieve secrets
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
with:
keyvault: "bitwarden-prod-kv"
secrets: "crowdin-api-token"
- name: Upload Sources
uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea # v1.3.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
with:
config: crowdin.yml
crowdin_branch_name: master
upload_sources: true
upload_translations: false
check-failures:
name: Check for failures
if: always()
runs-on: ubuntu-20.04
needs:
- cloc
- setup
- lint
- build-oss-selfhost
- build-cloud
- build-commercial-selfhost
- build-qa
- crowdin-push
- windows
steps:
- name: Check if any job failed
if: ${{ (github.ref == 'refs/heads/master') || (github.ref == 'refs/heads/rc') }}
env:
CLOC_STATUS: ${{ needs.cloc.result }}
LINT_STATUS: ${{ needs.lint.result }}
SETUP_STATUS: ${{ needs.setup.result }}
BUILD_OSS_SELFHOST_STATUS: ${{ needs.build-oss-selfhost.result }}
BUILD_CLOUD_STATUS: ${{ needs.build-cloud.result }}
BUILD_COMMERCIAL_SELFHOST_STATUS: ${{ needs.build-commercial-selfhost.result }}
BUILD_QA_STATUS: ${{ needs.build-qa.result }}
CROWDIN_PUSH_STATUS: ${{ needs.crowdin-push.result }}
WINDOWS_STATUS: ${{ needs.windows.result }}
run: |
if [ "$CLOC_STATUS" = "failure" ]; then
exit 1
elif [ "$LINT_STATUS" = "failure" ]; then
exit 1
elif [ "$SETUP_STATUS" = "failure" ]; then
exit 1
elif [ "$BUILD_OSS_SELFHOST_STATUS" = "failure" ]; then
exit 1
elif [ "$BUILD_CLOUD_STATUS" = "failure" ]; then
exit 1
elif [ "$BUILD_COMMERCIAL_SELFHOST_STATUS" = "failure" ]; then
exit 1
elif [ "$BUILD_QA_STATUS" = "failure" ]; then
exit 1
elif [ "$CROWDIN_PUSH_STATUS" = "failure" ]; then
exit 1
elif [ "$WINDOWS_STATUS" = "failure" ]; then
exit 1
fi
- name: Login to Azure - Prod Subscription
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
if: failure()
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
if: failure()
with:
keyvault: "bitwarden-prod-kv"
secrets: "devops-alerts-slack-webhook-url"
- name: Notify Slack on failure
uses: act10ns/slack@e4e71685b9b239384b0f676a63c32367f59c2522 # v1.2.2
if: failure()
env:
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
with:
status: ${{ job.status }}

49
.github/workflows/crowdin-pull.yml vendored Normal file
View File

@@ -0,0 +1,49 @@
---
name: Crowdin Pull
on:
workflow_dispatch:
inputs: {}
schedule:
- cron: "0 0 * * 5"
jobs:
crowdin-pull:
name: Pull
runs-on: ubuntu-20.04
env:
_CROWDIN_PROJECT_ID: "308189"
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Login to Azure
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
with:
keyvault: "bitwarden-prod-kv"
secrets: "crowdin-api-token"
- name: Download translations
uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea # v1.3.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
with:
config: crowdin.yml
crowdin_branch_name: master
upload_sources: false
upload_translations: false
download_translations: true
github_user_name: "github-actions"
github_user_email: "<>"
commit_message: "Autosync the updated translations"
localization_branch_name: crowdin-auto-sync
create_pull_request: true
pull_request_title: "Autosync Crowdin Translations"
pull_request_body: "Autosync the updated translations"

View File

@@ -1,133 +0,0 @@
name: Crowdin Sync
on:
workflow_dispatch:
inputs: {}
#schedule:
# - cron: '0 0 * * *'
jobs:
crowdin-sync:
name: Autosync
runs-on: ubuntu-20.04
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Setup git config
run: |
git config user.name = "GitHub Action Bot"
git config user.email = "<>"
- name: Get Crowndin Sync Branch
id: branch
run: |
BRANCH_NAME=crowdin-auto-sync
BRANCH_EXISTED=true
git fetch -a
git switch master
if [ $(git branch -a | egrep "remotes/origin/${BRANCH_NAME}$" | wc -l) -eq 0 ]; then
BRANCH_EXISTED=false
git switch -c $BRANCH_NAME
else
git switch $BRANCH_NAME
fi
git branch
echo "::set-output name=branch-existed::${BRANCH_EXISTED}"
echo "::set-output name=branch-name::${BRANCH_NAME}"
- name: Login to Azure
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
with:
keyvault: "bitwarden-prod-kv"
secrets: "crowdin-api-token"
- name: Get Crowdin updates
env:
CROWDIN_BASE_URL="https://api.crowdin.com/api/v2/projects"
CROWDIN_PROJECT_ID="308189"
run: |
# Step 1: GET master branchId
BRANCH_ID=$(
curl -s -H "Authorization: Bearer $CROWDIN_API_TOKEN" \
$CROWDIN_BASE_URL/$CROWDIN_PROJECT_ID/branches | jq -r '.data[0].data.id'
)
# Step 2: POST Build the translations and get store build id
BUILD_ID=$(
curl -X POST -s \
-H "Authorization: Bearer $CROWDIN_API_TOKEN" \
-H "Content-Type: application/json" \
$CROWDIN_BASE_URL/$CROWDIN_PROJECT_ID/translations/builds \
-d "{\"branchId\": $BRANCH_ID}" | jq -r '.data.id'
)
MAX_TRIES=12
for try in {1..$MAX_TRIES}; do
BRANCH_STATUS=$(
curl -s -H "Authorization: Bearer $CROWDIN_API_TOKEN" \
$CROWDIN_BASE_URL/$CROWDIN_PROJECT_ID/translations/builds/$BUILD_ID | jq -r '.data.status'
)
echo "[*] Build status: $BRANCH_STATUS"
if [[ "$BRANCH_STATUS" == "finished" ]]; then
break
fi
if [[ $try -eq $MAX_TRIES ]]; then
echo "[!] Exceeded tries: $try"
exit 1
else
sleep 5
fi
done
# Step 4: when build is finished, get download url
DOWNLOAD_URL=$(
curl -s -H "Authorization: Bearer $CROWDIN_API_TOKEN" \
$CROWDIN_BASE_URL/$CROWDIN_PROJECT_ID/translations/builds/$BUILD_ID/download | jq -r '.data.url'
)
# Step 5: download the translations via the download url
SAVE_FILE=translations.zip
curl -s $DOWNLOAD_URL --output $SAVE_FILE
echo "[*] Saved to: $SAVE_FILE"
# Step 6: Unzip and cleanup
unzip -o $SAVE_FILE
rm $SAVE_FILE
- name: Commit changes
env:
BRANCH_NAME: ${{ steps.branch.outputs.branch-name }}
run: |
echo "[*] Adding new translations"
git add .
echo "=====Translations Changed====="
git status
echo "=============================="
echo "[*] Committing"
git commit -m "Autosync Crowdin translations"
echo "[*] Pushing"
git push -u origin $BRANCH_NAME
- name: Create/Update PR
env:
BRANCH_NAME: ${{ steps.cherry-pick.outputs.branch-name }}
BRANCH_EXISTED: ${{ steps.cherry-pick.outputs.branch-existed }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ "$BRANCH_EXISTED" == "false" ]; then
echo "[*] Creating PR"
gh pr create --title "Autosync Crowdin Translations" \
--body "Autosync the updated translations"
else
echo "[*] Existing PR updated"
fi

View File

@@ -1,73 +0,0 @@
name: Deploy
on:
workflow_dispatch:
inputs:
release_version:
description: "Release Tag Version <vX.X.X>"
required: true
release:
types:
- published
jobs:
deploy:
name: Deploy Web Vault
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
with:
ref: gh-pages
- name: Get release version
id: release-version
run: |
if [[ "${{ github.event_name }}" == "release" ]]; then
echo "::set-output name=version::${{ github.event.release.tag_name }}"
else
echo "::set-output name=version::${{ github.event.inputs.release_version }}"
fi
- name: Create deploy branch
run: |
git switch -c deploy-${{ steps.release-version.outputs.version }}
git push -u origin deploy-${{ steps.release-version.outputs.version }}
- name: Checkout Repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
with:
ref: rc
- name: setup git config
run: |
git config user.name = "GitHub Action Bot"
git config user.email = "<>"
git config --global url."https://github.com/".insteadOf ssh://git@github.com/
git config --global url."https://".insteadOf ssh://
- name: Install and Build
run: |
npm run sub:init
npm ci
npm run dist
- name: Deploy GitHub Pages
uses: crazy-max/ghaction-github-pages@db4476a01402e1a7ce05f41832040eef16d14925 # v2.5.0
with:
target_branch: deploy-${{ steps.release-version.outputs.version }}
build_dir: build
keep_history: true
commit_message: "Staging deploy ${{ steps.release-version.outputs.version }}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create Deploy PR
run: |
gh pr create --title "Deploy $VERSION" --body "Deploying $VERSION" --base gh-pages --head "$PR_BRANCH"
env:
VERSION: ${{ steps.release-version.outputs.version }}
PR_BRANCH: deploy-${{ steps.release-version.outputs.version }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

16
.github/workflows/enforce-labels.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
---
name: Enforce PR labels
on:
pull_request:
types: [labeled, unlabeled, opened, edited, synchronize]
jobs:
enforce-label:
name: EnforceLabel
runs-on: ubuntu-20.04
steps:
- name: Enforce Label
uses: yogevbd/enforce-label-action@8d1e1709b1011e6d90400a0e6cf7c0b77aa5efeb
with:
BANNED_LABELS: "hold"
BANNED_LABELS_DESCRIPTION: "PRs on hold cannot be merged"

View File

@@ -1,6 +1,7 @@
---
name: QA Deploy
on:
on:
workflow_dispatch:
inputs:
image_extension:
@@ -8,22 +9,21 @@ on:
required: false
env:
QA_CLUSTER_RESOURCE_GROUP: "bitwarden-devops"
QA_CLUSTER_NAME: "dev-aks"
QA_K8S_NAMESPACE: "bw-qa"
QA_K8S_APP_NAME: "bw-web"
_QA_CLUSTER_RESOURCE_GROUP: "bw-env-qa"
_QA_CLUSTER_NAME: "bw-aks-qa"
_QA_K8S_NAMESPACE: "bw-qa"
_QA_K8S_APP_NAME: "bw-web"
jobs:
deploy:
name: Deploy QA Web
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Checkout Repo
uses: actions/checkout@v2
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Setup
run:
export PATH=$PATH:~/work/web/web
run: export PATH=$PATH:~/work/web/web
- name: Login to Azure
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
@@ -35,36 +35,37 @@ jobs:
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
with:
keyvault: "bitwarden-qa-kv"
secrets: "dev-aks-kubectl-credentials"
secrets: "qa-aks-kubectl-credentials"
- name: Login to dev-aks-kubectl SP
- name: Login with qa-aks-kubectl-credentials SP
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
with:
creds: ${{ env.dev-aks-kubectl-credentials }}
creds: ${{ env.qa-aks-kubectl-credentials }}
- name: Setup AKS access
env:
USER_ID: ${{ env.qa-kubectl-managed-identity-clientId }}
#env:
# USER_ID: ${{ env.qa-kubectl-managed-identity-clientId }}
run: |
echo "---az install---"
az aks install-cli --install-location ./kubectl --kubelogin-install-location ./kubelogin
echo "---az get-creds---"
az aks get-credentials -n $QA_CLUSTER_NAME -g $QA_CLUSTER_RESOURCE_GROUP
az aks get-credentials -n $_QA_CLUSTER_NAME -g $_QA_CLUSTER_RESOURCE_GROUP
- name: Get image tag
id: image_tag
run: |
IMAGE_TAG=$(echo "$GITHUB_REF" | awk '{split($0, a, "/"); print a[3];}')
TAG_EXTENSION=${{ github.events.inputs.image_extension }}
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g")
TAG_EXTENSION=${{ github.event.inputs.image_extension }}
if [[ $TAG_EXTENSION ]]; then
IMAGE_TAG=$IMAGE_TAG-$TAG_EXTENSION
fi
fi
echo "::set-output name=value::$IMAGE_TAG"
- name: Deploy Web image
env:
IMAGE_TAG: ${{ steps.image_tag.outputs.value }}
run: |
kubectl set image -n $QA_K8S_NAMESPACE deployment/web web=bitwardenqa.azurecr.io/web:$IMAGE_TAG --record
kubectl rollout status deployment/web -n $QA_K8S_NAMESPACE
kubectl set image -n $_QA_K8S_NAMESPACE deployment/web web=bitwardenqa.azurecr.io/web:$IMAGE_TAG --record
kubectl rollout restart -n $_QA_K8S_NAMESPACE deployment/web
kubectl rollout status deployment/web -n $_QA_K8S_NAMESPACE

View File

@@ -1,157 +1,334 @@
---
name: Release
on:
workflow_dispatch:
inputs:
release_tag_name_input:
description: "Release Tag Name <X.X.X>"
release_type:
description: 'Release Options'
required: true
default: 'Initial Release'
type: choice
options:
- Initial Release
- Redeploy
- Dry Run
jobs:
setup:
runs-on: ubuntu-latest
name: Setup
runs-on: ubuntu-20.04
outputs:
release_upload_url: ${{ steps.create_release.outputs.upload_url }}
release_version: ${{ steps.create_tags.outputs.package_version }}
tag_version: ${{ steps.create_tags.outputs.tag_version }}
release_version: ${{ steps.version.outputs.version }}
tag_version: ${{ steps.version.outputs.version }}
branch_name: ${{ steps.branch.outputs.branch_name }}
steps:
- name: Branch check
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
run: |
if [[ "$GITHUB_REF" != "refs/heads/rc" ]]; then
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then
echo "==================================="
echo "[!] Can only release from rc branch"
echo "[!] Can only release from the 'rc' or 'hotfix-rc' branches"
echo "==================================="
exit 1
fi
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # 2.3.4
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # 2.4.0
- name: Create Release Vars
id: create_tags
run: |
case "${RELEASE_TAG_NAME_INPUT:0:1}" in
v)
echo "RELEASE_NAME=${RELEASE_TAG_NAME_INPUT:1}" >> $GITHUB_ENV
echo "RELEASE_TAG_NAME=$RELEASE_TAG_NAME_INPUT" >> $GITHUB_ENV
echo "::set-output name=package_version::${RELEASE_TAG_NAME_INPUT:1}"
echo "::set-output name=tag_version::$RELEASE_TAG_NAME_INPUT"
;;
[0-9])
echo "RELEASE_NAME=$RELEASE_TAG_NAME_INPUT" >> $GITHUB_ENV
echo "RELEASE_TAG_NAME=v$RELEASE_TAG_NAME_INPUT" >> $GITHUB_ENV
echo "::set-output name=package_version::$RELEASE_TAG_NAME_INPUT"
echo "::set-output name=tag_version::v$RELEASE_TAG_NAME_INPUT"
;;
*)
exit 1
;;
esac
env:
RELEASE_TAG_NAME_INPUT: ${{ github.event.inputs.release_tag_name_input }}
- name: Create Draft Release
id: create_release
uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e # 1.1.4 - Repo Archived
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check Release Version
id: version
uses: bitwarden/gh-actions/release-version-check@ea9fab01d76940267b4147cc1c4542431246b9f6
with:
tag_name: ${{ env.RELEASE_TAG_NAME }}
release_name: Version ${{ env.RELEASE_NAME }}
draft: true
prerelease: false
release-type: ${{ github.event.inputs.release_type }}
project-type: ts
file: package.json
ubuntu:
runs-on: ubuntu-latest
- name: Get branch name
id: branch
run: |
BRANCH_NAME=$(basename ${{ github.ref }})
echo "::set-output name=branch_name::$BRANCH_NAME"
self-host:
name: Release self-host docker
runs-on: ubuntu-20.04
needs: setup
env:
RELEASE_VERSION: ${{ needs.setup.outputs.release_version }}
_BRANCH_NAME: ${{ needs.setup.outputs.branch_name }}
_RELEASE_VERSION: ${{ needs.setup.outputs.release_version }}
_RELEASE_OPTION: ${{ github.event.inputs.release_type }}
steps:
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '14'
- name: Update NPM
run: |
npm install -g npm@7
- name: Print environment
run: |
whoami
node --version
npm --version
gulp --version
docker --version
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
- name: Login to Azure
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
with:
keyvault: "bitwarden-prod-kv"
secrets: "docker-password,
docker-username,
dct-delegate-2-repo-passphrase,
dct-delegate-2-key"
- name: Log into Docker
run: echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
env:
DOCKER_USERNAME: ${{ steps.retrieve-secrets.outputs.docker-username }}
DOCKER_PASSWORD: ${{ steps.retrieve-secrets.outputs.docker-password }}
- name: Setup Docker Trust
if: github.ref == 'refs/heads/master' || github.event_name == 'release' || github.ref == 'refs/heads/rc'
run: |
mkdir -p ~/.docker/trust/private
echo "$DCT_DELEGATE_KEY" > ~/.docker/trust/private/$DCT_DELEGATION_KEY_ID.key
env:
DCT_DELEGATION_KEY_ID: "c9bde8ec820701516491e5e03d3a6354e7bd66d05fa3df2b0062f68b116dc59c"
DCT_DELEGATE_KEY: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-key }}
echo "Github Release Option: $_RELEASE_OPTION"
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
- name: Restore
run: dotnet tool restore
########## DockerHub ##########
- name: Setup DCT
id: setup-dct
uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff
with:
azure-creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
azure-keyvault-name: "bitwarden-prod-kv"
- name: Build
- name: Pull latest selfhost image
run: |
echo -e "# Building Web\n"
echo "Building app"
echo "npm version $(npm --version)"
npm install
npm run dist:selfhost
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
docker pull bitwarden/web:latest
else
docker pull bitwarden/web:$_BRANCH_NAME
fi
echo -e "\nBuilding Docker image"
docker --version
docker build -t bitwarden/web .
- name: Tag version and latest
run: |
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
docker tag bitwarden/web:latest bitwarden/web:dryrun
else
docker tag bitwarden/web:$_BRANCH_NAME bitwarden/web:$_RELEASE_VERSION
docker tag bitwarden/web:$_BRANCH_NAME bitwarden/web:latest
fi
- name: Tag version
run: docker tag bitwarden/web bitwarden/web:$RELEASE_VERSION
- name: List Docker images
run: docker images
- name: Push latest images
run: docker push bitwarden/web:latest
env:
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
- name: Push version images
run: docker push bitwarden/web:$RELEASE_VERSION
- name: Push version and latest image
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
env:
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
run: |
docker push bitwarden/web:$_RELEASE_VERSION
docker push bitwarden/web:latest
- name: Log out of Docker and disable Docker Notary
run: |
docker logout
echo "DOCKER_CONTENT_TRUST=0" >> $GITHUB_ENV
########## ACR ##########
- name: Login to Azure - QA Subscription
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
with:
creds: ${{ secrets.AZURE_QA_KV_CREDENTIALS }}
- name: Login to Azure ACR
run: az acr login -n bitwardenqa
- name: Tag version and latest
env:
REGISTRY: bitwardenqa.azurecr.io
run: |
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
docker tag bitwarden/web:latest $REGISTRY/web:dryrun
else
docker tag bitwarden/web:$_BRANCH_NAME $REGISTRY/web:$_RELEASE_VERSION
docker tag bitwarden/web:$_BRANCH_NAME $REGISTRY/web:latest
docker tag bitwarden/web:$_BRANCH_NAME $REGISTRY/web-sh:$_RELEASE_VERSION
docker tag bitwarden/web:$_BRANCH_NAME $REGISTRY/web-sh:latest
fi
- name: Push version and latest image
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
env:
REGISTRY: bitwardenqa.azurecr.io
run: |
docker push $REGISTRY/web:$_RELEASE_VERSION
docker push $REGISTRY/web:latest
docker push $REGISTRY/web-sh:$_RELEASE_VERSION
docker push $REGISTRY/web-sh:latest
- name: Log out of Docker
run: docker logout
ghpages-deploy:
name: Deploy Web Vault to GitHub Pages
runs-on: ubuntu-20.04
needs:
- setup
- self-host
env:
_RELEASE_VERSION: ${{ needs.setup.outputs.release_version }}
_TAG_VERSION: ${{ needs.setup.outputs.tag_version }}
steps:
- name: Checkout Repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
with:
ref: gh-pages
- name: Create gh-pages-deploy branch
run: |
git switch -c gh-pages-deploy-$_TAG_VERSION
git push -u origin gh-pages-deploy-$_TAG_VERSION
- name: Checkout Repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
- name: Setup git config
run: |
git config user.name = "GitHub Action Bot"
git config user.email = "<>"
git config --global url."https://github.com/".insteadOf ssh://git@github.com/
git config --global url."https://".insteadOf ssh://
- name: Download latest cloud asset
uses: bitwarden/gh-actions/download-artifacts@c1fa8e09871a860862d6bbe36184b06d2c7e35a8
with:
workflow: build.yml
workflow_conclusion: success
branch: ${{ needs.setup.outputs.branch_name }}
artifacts: web-*-cloud-COMMERCIAL.zip
# This should result in a build directory in the current working directory
- name: Unzip build asset
run: unzip web-*-cloud-COMMERCIAL.zip
- name: Deploy GitHub Pages
uses: crazy-max/ghaction-github-pages@a117e4aa1fb4854d021546d2abdfac95be568a3a # v2.6.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
target_branch: gh-pages-deploy-${{ needs.setup.outputs.tag_version }}
build_dir: build
keep_history: true
commit_message: "Staging deploy ${{ needs.setup.outputs.release_version }}"
dry_run: ${{ github.event.inputs.release_type == 'Dry Run' }}
- name: Create GitHub Pages Deploy PR
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
env:
PR_BRANCH: gh-pages-deploy-${{ env._TAG_VERSION }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr create --title "Deploy $_RELEASE_VERSION to GitHub Pages" \
--body "Deploying $_RELEASE_VERSION" \
--base gh-pages \
--head "$PR_BRANCH"
cfpages-deploy:
name: Deploy Web Vault to CloudFlare Pages branch
runs-on: ubuntu-20.04
needs:
- setup
- self-host
env:
_RELEASE_VERSION: ${{ needs.setup.outputs.release_version }}
_TAG_VERSION: ${{ needs.setup.outputs.tag_version }}
steps:
- name: Checkout Repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
- name: Download latest cloud asset
uses: bitwarden/gh-actions/download-artifacts@c1fa8e09871a860862d6bbe36184b06d2c7e35a8
with:
workflow: build.yml
workflow_conclusion: success
branch: ${{ needs.setup.outputs.branch_name }}
artifacts: web-*-cloud-COMMERCIAL.zip
# This should result in a build directory in the current working directory
- name: Unzip build asset
run: unzip web-*-cloud-COMMERCIAL.zip
- name: Checkout Repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
with:
ref: deploy
path: deployment
- name: Setup git config
run: |
git config --global user.name = "GitHub Action Bot"
git config --global user.email = "<>"
git config --global url."https://github.com/".insteadOf ssh://git@github.com/
git config --global url."https://".insteadOf ssh://
- name: Deploy CloudFlare Pages
run: |
rm -rf ./*
cp -R ../build/* .
working-directory: deployment
- name: Create cf-pages-deploy branch
run: |
git switch -c cf-pages-deploy-$_TAG_VERSION
git add .
git commit -m "Staging deploy ${{ needs.setup.outputs.release_version }}"
git push -u origin cf-pages-deploy-$_TAG_VERSION
working-directory: deployment
- name: Create CloudFlare Pages Deploy PR
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
env:
PR_BRANCH: cf-pages-deploy-${{ env._TAG_VERSION }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr create --title "Deploy $_RELEASE_VERSION to CloudFlare Pages" \
--body "Deploying $_RELEASE_VERSION" \
--base deploy \
--head "$PR_BRANCH"
release:
name: Create GitHub Release
runs-on: ubuntu-20.04
needs:
- setup
- self-host
- ghpages-deploy
- cfpages-deploy
steps:
- name: Download latest build artifacts
uses: bitwarden/gh-actions/download-artifacts@23433be15ed6fd046ce12b6889c5184a8d9c8783
with:
workflow: build.yml
workflow_conclusion: success
branch: ${{ needs.setup.outputs.branch_name }}
artifacts: "web-*-selfhosted-COMMERCIAL.zip,
web-*-selfhosted-open-source.zip"
- name: Rename assets
run: |
mv web-*-selfhosted-COMMERCIAL.zip web-${{ needs.setup.outputs.release_version }}-selfhosted-COMMERCIAL.zip
mv web-*-selfhosted-open-source.zip web-${{ needs.setup.outputs.release_version }}-selfhosted-open-source.zip
- name: Create release
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
uses: ncipollo/release-action@40bb172bd05f266cf9ba4ff965cb61e9ee5f6d01
with:
name: "Version ${{ needs.setup.outputs.release_version }}"
commit: ${{ github.sha }}
tag: "${{ needs.setup.outputs.tag_version }}"
body: "<insert release notes here>"
artifacts: "web-${{ needs.setup.outputs.release_version }}-selfhosted-COMMERCIAL.zip,
web-${{ needs.setup.outputs.release_version }}-selfhosted-open-source.zip"
token: ${{ secrets.GITHUB_TOKEN }}
draft: true
dry-run:
name: Dry Run Cleanup
runs-on: ubuntu-20.04
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
env:
_TAG_VERSION: ${{ needs.setup.outputs.tag_version }}
needs:
- setup
- release
steps:
- name: Checkout repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # 2.4.0
- name: Remove gh-pages-deploy branch
run: git push origin --delete gh-pages-deploy-$_TAG_VERSION
- name: Remove cf-pages-deploy branch
run: git push origin --delete cf-pages-deploy-$_TAG_VERSION

71
.github/workflows/version-bump.yml vendored Normal file
View File

@@ -0,0 +1,71 @@
---
name: Version Bump
on:
workflow_dispatch:
inputs:
version_number:
description: "New Version"
required: true
jobs:
bump_props_version:
name: "Create version_bump_${{ github.event.inputs.version_number }} branch"
runs-on: ubuntu-20.04
steps:
- name: Checkout Branch
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
- name: Create Version Branch
run: |
git switch -c version_bump_${{ github.event.inputs.version_number }}
git push -u origin version_bump_${{ github.event.inputs.version_number }}
- name: Checkout Version Branch
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
with:
ref: version_bump_${{ github.event.inputs.version_number }}
- name: Bump Version - package.json
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
with:
version: ${{ github.event.inputs.version_number }}
file_path: "./package.json"
- name: Bump Version - package-lock.json
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
with:
version: ${{ github.event.inputs.version_number }}
file_path: "./package-lock.json"
- name: Commit files
run: |
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a
- name: Push changes
run: git push -u origin version_bump_${{ github.event.inputs.version_number }}
- name: Create Version PR
env:
PR_BRANCH: "version_bump_${{ github.event.inputs.version_number }}"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
BASE_BRANCH: master
TITLE: "Bump version to ${{ github.event.inputs.version_number }}"
run: |
gh pr create --title "$TITLE" \
--base "$BASE" \
--head "$PR_BRANCH" \
--label "version update" \
--label "automated pr" \
--body "
## Type of change
- [ ] Bug fix
- [ ] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [X] Other
## Objective
Automated version bump to ${{ github.event.inputs.version_number }}"

11
.github/workflows/workflow-linter.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
---
name: Workflow Linter
on:
pull_request:
paths:
- .github/workflows/**
jobs:
call-workflow:
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@master

2
.gitignore vendored
View File

@@ -12,4 +12,4 @@ dist/
*.swp
build/
!dev-server.shared.pem
config/development.json
config/local.json

4
.husky/pre-commit Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged

12
.prettierignore Normal file
View File

@@ -0,0 +1,12 @@
# Build directories
build
dist
jslib
# External libraries / auto synced locales
src/locales
src/404/*.min.css
# Github Workflows
.github/workflows

3
.prettierrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"printWidth": 100
}

View File

@@ -6,17 +6,12 @@ Please visit our [Community Forums](https://community.bitwarden.com/) for genera
Here is how you can get involved:
* **Request a new feature:** Go to the [Feature Requests category](https://community.bitwarden.com/c/feature-requests/) of the Community Forums. Please search existing feature requests before making a new one
* **Write code for a new feature:** Make a new post in the [Github Contributions category](https://community.bitwarden.com/c/github-contributions/) of the Community Forums. Include a description of your proposed contribution, screeshots, and links to any relevant feature requests. This helps get feedback from the community and Bitwarden team members before you start writing code
* **Report a bug or submit a bugfix:** Use Github issues and pull requests
* **Write documentation:** Submit a pull request to the [Bitwarden help repository](https://github.com/bitwarden/help)
* **Help other users:** Go to the [User-to-User Support category](https://community.bitwarden.com/c/support/) on the Community Forums
* **Translate:** See the localization (l10n) section below
- **Request a new feature:** Go to the [Feature Requests category](https://community.bitwarden.com/c/feature-requests/) of the Community Forums. Please search existing feature requests before making a new one
- **Write code for a new feature:** Make a new post in the [Github Contributions category](https://community.bitwarden.com/c/github-contributions/) of the Community Forums. Include a description of your proposed contribution, screeshots, and links to any relevant feature requests. This helps get feedback from the community and Bitwarden team members before you start writing code
- **Report a bug or submit a bugfix:** Use Github issues and pull requests
- **Write documentation:** Submit a pull request to the [Bitwarden help repository](https://github.com/bitwarden/help)
- **Help other users:** Go to the [Ask the Bitwarden Community category](https://community.bitwarden.com/c/support/) on the Community Forums
- **Translate:** See the localization (l10n) section below
## Contributor Agreement
@@ -24,9 +19,9 @@ Please sign the [Contributor Agreement](https://cla-assistant.io/bitwarden/web)
## Pull Request Guidelines
* use `npm run lint` and fix any linting suggestions before submitting a pull request
* commit any pull requests against the `master` branch
* include a link to your Community Forums post
- use `npm run lint` and fix any linting suggestions before submitting a pull request
- commit any pull requests against the `master` branch
- include a link to your Community Forums post
# Localization (l10n)
@@ -36,6 +31,6 @@ We use a translation tool called [Crowdin](https://crowdin.com) to help manage o
If you are interested in helping translate the Bitwarden web vault into another language (or make a translation correction), please register an account at Crowdin and join our project here: https://crowdin.com/project/bitwarden-web
If the language that you are interested in translating is not already listed, create a new account on Crowdin, join the project, and contact the project owner (https://crowdin.com/profile/kspearrin).
If the language that you are interested in translating is not already listed, create a new account on Crowdin, join the project, and contact the project owner (https://crowdin.com/profile/dwbit).
You can read Crowdin's getting started guide for translators here: https://support.crowdin.com/crowdin-intro/

View File

@@ -1,52 +0,0 @@
<!--
Please do not submit feature requests. The [Community Forums][1] has a
section for submitting, voting for, and discussing product feature requests.
[1]: https://community.bitwarden.com
-->
## Describe the Bug
<!-- Comment:
A clear and concise description of what the bug is.
-->
## Steps To Reproduce
<!-- Comment:
How can we reproduce the behavior:
-->
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. Click on '...'
## Expected Result
<!-- Comment:
A clear and concise description of what you expected to happen.
-->
## Actual Result
<!-- Comment:
A clear and concise description of what is happening.
-->
## Screenshots or Videos
<!-- Comment:
If applicable, add screenshots and/or a short video to help explain your problem.
-->
## Environment
- Operating system: [e.g. Windows 10, Mac OS Catalina]
- Browser: [e.g. Firefox 73.0.1]
- Build Version (Bottom of the page): [2.13.0]
## Additional Context
<!-- Comment:
Add any other context about the problem here.
-->

View File

@@ -1,3 +1,9 @@
> **Repository Reorganization in Progress**
>
> We are currently migrating some projects over to a mono repository. For existing PR's we will be providing documentation on how to move/migrate them. To minimize the overhead we are actively reviewing open PRs. If possible please ensure any pending comments are resolved as soon as possible.
>
> New pull requests created during this transition period may not get addressed —if needed, please create a new PR after the reorganization is complete.
<p align="center">
<img src="https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/web-vault-macbook.png" alt="" width="600" height="358" />
</p>
@@ -23,8 +29,8 @@
### Requirements
- [Node.js](https://nodejs.org) v14.17 or greater
- NPM v7
- [Node.js](https://nodejs.org) v16.13.1 or greater
- NPM v8
### Run the app
@@ -32,7 +38,7 @@ For local development, run the app with:
```
npm install
npm run build:watch
npm run build:oss:watch
```
You can now access the web vault in your browser at `https://localhost:8080`.
@@ -41,30 +47,52 @@ If you want to point the development web vault to the production APIs, you can r
```
npm install
ENV=production npm run build:watch
ENV=cloud npm run build:oss:watch
```
You can also manually adjusting your API endpoint settings by adding `config/development.json` overriding any of the values in `config/base.json`. For example:
You can also manually adjusting your API endpoint settings by adding `config/local.json` overriding any of the following values:
```typescript
```json
{
"dev": {
"proxyApi": "http://your-api-url",
"proxyIdentity": "http://your-identity-url",
"proxyEvents": "http://your-events-url",
"proxyNotifications": "http://your-notifications-url",
"proxyPortal": "http://your-portal-url",
"allowedHosts": ["hostnames-to-allow-in-webpack"]
},
"urls": {}
}
```
To pick up the overrides in the newly created `config/development.json` file, run the app with:
Where the `urls` object is defined by the [Urls type in jslib](https://github.com/bitwarden/jslib/blob/master/common/src/abstractions/environment.service.ts).
```
npm run build:dev:watch
```
## We're Hiring!
Interested in contributing in a big way? Consider joining our team! We're hiring for many positions. Please take a look at our [Careers page](https://bitwarden.com/careers/) to see what opportunities are currently open as well as what it's like to work at Bitwarden.
## Contribute
Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file.
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file.
## Prettier
We recently migrated to using Prettier as code formatter. All previous branches will need to updated to avoid large merge conflicts using the following steps:
1. Check out your local Branch
2. Run `git merge 2b0a9d995e0147601ca8ae4778434a19354a60c2`
3. Resolve any merge conflicts, commit.
4. Run `npm run prettier`
5. Commit
6. Run `git merge -Xours 56477eb39cfd8a73c9920577d24d75fed36e2cf5`
7. Push
### Git blame
We also recommend that you configure git to ignore the prettier revision using:
```bash
git config blame.ignoreRevsFile .git-blame-ignore-revs
```

View File

@@ -1,39 +1,11 @@
Bitwarden believes that working with security researchers across the globe is crucial to keeping our
users safe. If you believe you've found a security issue in our product or service, we encourage you to
notify us. We welcome working with you to resolve the issue promptly. Thanks in advance!
Bitwarden believes that working with security researchers across the globe is crucial to keeping our users safe. If you believe you've found a security issue in our product or service, we encourage you to please submit a report through our [HackerOne Program](https://hackerone.com/bitwarden/). We welcome working with you to resolve the issue promptly. Thanks in advance!
# Disclosure Policy
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every
effort to quickly resolve the issue.
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a
third-party. We may publicly disclose the issue before resolving it, if appropriate.
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or
degradation of our service. Only interact with accounts you own or with explicit permission of the
account holder.
- If you would like to encrypt your report, please use the PGP key with long ID
`0xDE6887086F892325FEC04CC0D847525B6931381F` (available in the public keyserver pool).
# In-scope
- Security issues in any current release of Bitwarden. This includes the web vault, browser extension,
and mobile apps (iOS and Android). Product downloads are available at https://bitwarden.com. Source
code is available at https://github.com/bitwarden.
# Exclusions
The following bug classes are out-of scope:
- Bugs that are already reported on any of Bitwarden's issue trackers (https://github.com/bitwarden),
or that we already know of. Note that some of our issue tracking is private.
- Issues in an upstream software dependency (ex: Xamarin, ASP.NET) which are already reported to the
upstream maintainer.
- Attacks requiring physical access to a user's device.
- Self-XSS
- Issues related to software or protocols not under Bitwarden's control
- Vulnerabilities in outdated versions of Bitwarden
- Missing security best practices that do not directly lead to a vulnerability
- Issues that do not have any impact on the general public
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every effort to quickly resolve the issue.
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a third-party. We may publicly disclose the issue before resolving it, if appropriate.
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or degradation of our service. Only interact with accounts you own or with explicit permission of the account holder.
- If you would like to encrypt your report, please use the PGP key with long ID `0xDE6887086F892325FEC04CC0D847525B6931381F` (available in the public keyserver pool).
While researching, we'd like to ask you to refrain from:
@@ -42,4 +14,8 @@ While researching, we'd like to ask you to refrain from:
- Social engineering (including phishing) of Bitwarden staff or contractors
- Any physical attempts against Bitwarden property or data centers
# We want to help you!
If you have something that you feel is close to exploitation, or if you'd like some information regarding the internal API, or generally have any questions regarding the app that would help in your efforts, please email us at https://bitwarden.com/contact and ask for that information. As stated above, Bitwarden wants to help you find issues, and is more than willing to help.
Thank you for helping keep Bitwarden and our users safe!

View File

@@ -1,15 +1,15 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
const routes: Routes = [
{
path: 'providers',
loadChildren: async () => (await import('./providers/providers.module')).ProvidersModule,
},
{
path: "providers",
loadChildren: async () => (await import("./providers/providers.module")).ProvidersModule,
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class AppRoutingModule { }
export class AppRoutingModule {}

View File

@@ -0,0 +1,21 @@
import { Component } from "@angular/core";
import { AppComponent as BaseAppComponent } from "src/app/app.component";
import { DisablePersonalVaultExportPolicy } from "./policies/disable-personal-vault-export.component";
import { MaximumVaultTimeoutPolicy } from "./policies/maximum-vault-timeout.component";
@Component({
selector: "app-root",
templateUrl: "../../../src/app/app.component.html",
})
export class AppComponent extends BaseAppComponent {
ngOnInit() {
super.ngOnInit();
this.policyListService.addPolicies([
new MaximumVaultTimeoutPolicy(),
new DisablePersonalVaultExportPolicy(),
]);
}
}

View File

@@ -1,30 +1,46 @@
import { ToasterModule } from 'angular2-toaster';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { DragDropModule } from "@angular/cdk/drag-drop";
import { OverlayModule } from "@angular/cdk/overlay";
import { NgModule } from "@angular/core";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { RouterModule } from "@angular/router";
import { InfiniteScrollModule } from "ngx-infinite-scroll";
import { DragDropModule } from '@angular/cdk/drag-drop';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { JslibModule } from "jslib-angular/jslib.module";
import { AppRoutingModule } from './app-routing.module';
import { OssRoutingModule } from "src/app/oss-routing.module";
import { OssModule } from "src/app/oss.module";
import { ServicesModule } from "src/app/services/services.module";
import { WildcardRoutingModule } from "src/app/wildcard-routing.module";
import { AppComponent } from 'src/app/app.component';
import { OssRoutingModule } from 'src/app/oss-routing.module';
import { OssModule } from 'src/app/oss.module';
import { ServicesModule } from 'src/app/services/services.module';
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { OrganizationsModule } from "./organizations/organizations.module";
import { DisablePersonalVaultExportPolicyComponent } from "./policies/disable-personal-vault-export.component";
import { MaximumVaultTimeoutPolicyComponent } from "./policies/maximum-vault-timeout.component";
@NgModule({
imports: [
OssModule,
BrowserAnimationsModule,
FormsModule,
ServicesModule,
ToasterModule.forRoot(),
InfiniteScrollModule,
DragDropModule,
AppRoutingModule,
OssRoutingModule,
],
bootstrap: [AppComponent],
imports: [
OverlayModule,
OssModule,
JslibModule,
BrowserAnimationsModule,
FormsModule,
ReactiveFormsModule,
ServicesModule,
InfiniteScrollModule,
DragDropModule,
AppRoutingModule,
OssRoutingModule,
OrganizationsModule, // Must be after OssRoutingModule for competing routes to resolve properly
RouterModule,
WildcardRoutingModule, // Needs to be last to catch all non-existing routes
],
declarations: [
AppComponent,
DisablePersonalVaultExportPolicyComponent,
MaximumVaultTimeoutPolicyComponent,
],
bootstrap: [AppComponent],
})
export class AppModule { }
export class AppModule {}

View File

@@ -1,17 +1,17 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import 'bootstrap';
import 'jquery';
import 'popper.js';
import "bootstrap";
import "jquery";
import "popper.js";
// tslint:disable-next-line
require('src/scss/styles.scss');
require("src/scss/styles.scss");
require("src/scss/tailwind.css");
import { AppModule } from './app.module';
import { AppModule } from "./app.module";
if (process.env.NODE_ENV === 'production') {
enableProdMode();
if (process.env.NODE_ENV === "production") {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });

View File

@@ -0,0 +1,68 @@
import { Directive, Input, OnInit, Self } from "@angular/core";
import { ControlValueAccessor, FormControl, NgControl, Validators } from "@angular/forms";
import { dirtyRequired } from "jslib-angular/validators/dirty.validator";
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
@Directive()
export abstract class BaseCvaComponent implements ControlValueAccessor, OnInit {
get describedById() {
return this.showDescribedBy ? this.controlId + "Desc" : null;
}
get showDescribedBy() {
return this.helperText != null || this.controlDir.control.hasError("required");
}
get isRequired() {
return (
this.controlDir.control.hasValidator(Validators.required) ||
this.controlDir.control.hasValidator(dirtyRequired)
);
}
@Input() label: string;
@Input() controlId: string;
@Input() helperText: string;
internalControl = new FormControl("");
protected onChange: any;
protected onTouched: any;
constructor(@Self() public controlDir: NgControl) {
this.controlDir.valueAccessor = this;
}
ngOnInit() {
this.internalControl.valueChanges.subscribe(this.onValueChangesInternal);
}
onBlurInternal() {
this.onTouched();
}
// CVA interfaces
writeValue(value: string) {
this.internalControl.setValue(value);
}
registerOnChange(fn: any) {
this.onChange = fn;
}
registerOnTouched(fn: any) {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean) {
if (isDisabled) {
this.internalControl.disable();
} else {
this.internalControl.enable();
}
}
protected onValueChangesInternal: any = (value: string) => this.onChange(value);
// End CVA interfaces
}

View File

@@ -0,0 +1,16 @@
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
[attr.id]="controlId"
[attr.aria-describedby]="describedById"
[formControl]="internalControl"
(blur)="onBlurInternal()"
/>
<label class="form-check-label" [attr.for]="controlId">{{ label }}</label>
</div>
<small *ngIf="showDescribedBy" [attr.id]="describedById" class="form-text text-muted">{{
helperText
}}</small>
</div>

View File

@@ -0,0 +1,10 @@
import { Component } from "@angular/core";
import { BaseCvaComponent } from "./base-cva.component";
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
@Component({
selector: "app-input-checkbox",
templateUrl: "input-checkbox.component.html",
})
export class InputCheckboxComponent extends BaseCvaComponent {}

View File

@@ -0,0 +1,26 @@
<div class="form-group">
<label>{{ label }}</label>
<div class="input-group">
<input class="form-control" readonly [value]="controlValue" />
<div class="input-group-append" *ngIf="showLaunch">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'launch' | i18n }}"
(click)="launchUri(controlValue)"
>
<i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i>
</button>
</div>
<div class="input-group-append" *ngIf="showCopy">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyValue' | i18n }}"
(click)="copy(controlValue)"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,25 @@
import { Component, Input } from "@angular/core";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
@Component({
selector: "app-input-text-readonly",
templateUrl: "input-text-readonly.component.html",
})
export class InputTextReadOnlyComponent {
@Input() controlValue: string;
@Input() label: string;
@Input() showCopy = true;
@Input() showLaunch = false;
constructor(private platformUtilsService: PlatformUtilsService) {}
copy(value: string) {
this.platformUtilsService.copyToClipboard(value);
}
launchUri(url: string) {
this.platformUtilsService.launchUri(url);
}
}

View File

@@ -0,0 +1,33 @@
<div class="form-group">
<label [attr.for]="controlId">
{{ label }}
<small *ngIf="isRequired" class="text-muted form-text d-inline"
>({{ "required" | i18n }})</small
>
</label>
<input
[formControl]="internalControl"
class="form-control"
[attr.id]="controlId"
[attr.aria-describedby]="describedById"
[attr.aria-invalid]="controlDir.control.invalid"
(blur)="onBlurInternal()"
/>
<div *ngIf="showDescribedBy" [attr.id]="describedById">
<small
*ngIf="helperText != null && !controlDir.control.hasError(helperTextSameAsError)"
class="form-text text-muted"
>
{{ helperText }}
</small>
<small class="error-inline" *ngIf="controlDir.control.hasError('required')" role="alert">
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
<span class="sr-only">{{ "error" | i18n }}:</span>
{{
controlDir.control.hasError(helperTextSameAsError)
? helperText
: ("fieldRequiredError" | i18n: label)
}}
</small>
</div>
</div>

View File

@@ -0,0 +1,48 @@
import { Component, Input, OnInit } from "@angular/core";
import { BaseCvaComponent } from "./base-cva.component";
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
@Component({
selector: "app-input-text[label][controlId]",
templateUrl: "input-text.component.html",
})
export class InputTextComponent extends BaseCvaComponent implements OnInit {
@Input() helperTextSameAsError: string;
@Input() requiredErrorMessage: string;
@Input() stripSpaces = false;
transformValue: (value: string) => string = null;
ngOnInit() {
super.ngOnInit();
if (this.stripSpaces) {
this.transformValue = this.doStripSpaces;
}
}
writeValue(value: string) {
this.internalControl.setValue(value == null ? "" : value);
}
protected onValueChangesInternal: any = (value: string) => {
let newValue = value;
if (this.transformValue != null) {
newValue = this.transformValue(value);
this.internalControl.setValue(newValue, { emitEvent: false });
}
this.onChange(newValue);
};
protected onValueChangeInternal(value: string) {
let newValue = value;
if (this.transformValue != null) {
newValue = this.transformValue(value);
this.internalControl.setValue(newValue, { emitEvent: false });
}
}
private doStripSpaces(value: string) {
return value.replace(/ /g, "");
}
}

View File

@@ -0,0 +1,19 @@
<div class="form-group">
<label [attr.for]="controlId">
{{ label }}
<small *ngIf="isRequired" class="text-muted form-text d-inline"
>({{ "required" | i18n }})</small
>
</label>
<select
class="form-control"
[attr.id]="controlId"
[attr.aria-invalid]="controlDir.control.invalid"
[formControl]="internalControl"
(blur)="onBlurInternal()"
>
<option *ngFor="let o of selectOptions" [ngValue]="o.value" disabled="{{ o.disabled }}">
{{ o.name }}
</option>
</select>
</div>

View File

@@ -0,0 +1,14 @@
import { Component, Input } from "@angular/core";
import { SelectOptions } from "jslib-angular/interfaces/selectOptions";
import { BaseCvaComponent } from "./base-cva.component";
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
@Component({
selector: "app-select",
templateUrl: "select.component.html",
})
export class SelectComponent extends BaseCvaComponent {
@Input() selectOptions: SelectOptions[];
}

View File

@@ -0,0 +1,448 @@
<div class="page-header d-flex">
<h1>{{ "singleSignOn" | i18n }}</h1>
</div>
<ng-container *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<form
#form
(ngSubmit)="submit()"
[formGroup]="ssoConfigForm"
[appApiAction]="formPromise"
*ngIf="!loading"
>
<p>
{{ "ssoPolicyHelpStart" | i18n }}
<a routerLink="../policies">{{ "ssoPolicyHelpLink" | i18n }}</a>
{{ "ssoPolicyHelpEnd" | i18n }}
<br />
{{ "ssoPolicyHelpKeyConnector" | i18n }}
</p>
<!-- Root form -->
<ng-container>
<app-input-checkbox
controlId="enabled"
[formControl]="enabled"
[label]="'allowSso' | i18n"
[helperText]="'allowSsoDesc' | i18n"
></app-input-checkbox>
<div class="form-group">
<label>{{ "memberDecryptionOption" | i18n }}</label>
<div class="form-check form-check-block">
<input
class="form-check-input"
type="radio"
id="memberDecryptionPass"
[value]="false"
formControlName="keyConnectorEnabled"
/>
<label class="form-check-label" for="memberDecryptionPass">
{{ "masterPass" | i18n }}
<small>{{ "memberDecryptionPassDesc" | i18n }}</small>
</label>
</div>
<div class="form-check mt-2 form-check-block">
<input
class="form-check-input"
type="radio"
id="memberDecryptionKey"
[value]="true"
formControlName="keyConnectorEnabled"
[attr.disabled]="!organization.useKeyConnector || null"
/>
<label class="form-check-label" for="memberDecryptionKey">
{{ "keyConnector" | i18n }}
<a
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
href="https://bitwarden.com/help/about-key-connector/"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
<small>{{ "memberDecryptionKeyConnectorDesc" | i18n }}</small>
</label>
</div>
</div>
<!-- Key Connector -->
<ng-container *ngIf="ssoConfigForm.get('keyConnectorEnabled').value">
<app-callout type="warning" [useAlertRole]="true">
{{ "keyConnectorWarning" | i18n }}
</app-callout>
<div class="form-group">
<label for="keyConnectorUrl">
{{ "keyConnectorUrl" | i18n }}
<small class="text-muted form-text d-inline">({{ "required" | i18n }})</small>
</label>
<div class="input-group">
<input
class="form-control"
formControlName="keyConnectorUrl"
id="keyConnectorUrl"
aria-describedby="keyConnectorUrlDesc"
(change)="haveTestedKeyConnector = false"
appInputStripSpaces
appA11yInvalid
/>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
(click)="validateKeyConnectorUrl()"
[disabled]="!enableTestKeyConnector"
>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
*ngIf="keyConnectorUrl.pending"
></i>
<span *ngIf="!keyConnectorUrl.pending">
{{ "keyConnectorTest" | i18n }}
</span>
</button>
</div>
</div>
<div *ngIf="haveTestedKeyConnector" id="keyConnectorUrlDesc" aria-live="polite">
<small
class="error-inline"
*ngIf="keyConnectorUrl.hasError('invalidUrl'); else keyConnectorSuccess"
>
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
<span class="sr-only">{{ "error" | i18n }}:</span>
{{ "keyConnectorTestFail" | i18n }}
</small>
<ng-template #keyConnectorSuccess>
<small class="text-success">
<i class="bwi bwi-check-circle" aria-hidden="true"></i>
{{ "keyConnectorTestSuccess" | i18n }}
</small>
</ng-template>
</div>
</div>
</ng-container>
<app-select
controlId="type"
[label]="'type' | i18n"
[selectOptions]="ssoTypeOptions"
formControlName="configType"
>
</app-select>
</ng-container>
<!-- OIDC -->
<div
*ngIf="ssoConfigForm.get('configType').value === ssoType.OpenIdConnect"
[formGroup]="openIdForm"
>
<div class="config-section">
<h2 class="secondary-header">{{ "openIdConnectConfig" | i18n }}</h2>
<app-input-text-readonly
[label]="'callbackPath' | i18n"
[controlValue]="callbackPath"
></app-input-text-readonly>
<app-input-text-readonly
[label]="'signedOutCallbackPath' | i18n"
[controlValue]="signedOutCallbackPath"
></app-input-text-readonly>
<app-input-text
[label]="'authority' | i18n"
controlId="authority"
[stripSpaces]="true"
formControlName="authority"
></app-input-text>
<app-input-text
[label]="'clientId' | i18n"
controlId="clientId"
[stripSpaces]="true"
formControlName="clientId"
></app-input-text>
<app-input-text
[label]="'clientSecret' | i18n"
controlId="clientSecret"
[stripSpaces]="true"
formControlName="clientSecret"
></app-input-text>
<app-input-text
[label]="'metadataAddress' | i18n"
controlId="metadataAddress"
[stripSpaces]="true"
[helperText]="'openIdAuthorityRequired' | i18n"
formControlName="metadataAddress"
></app-input-text>
<app-select
controlId="redirectBehavior"
[label]="'oidcRedirectBehavior' | i18n"
[selectOptions]="connectRedirectOptions"
formControlName="redirectBehavior"
>
</app-select>
<app-input-checkbox
controlId="getClaimsFromUserInfoEndpoint"
formControlName="getClaimsFromUserInfoEndpoint"
[label]="'getClaimsFromUserInfoEndpoint' | i18n"
></app-input-checkbox>
<!-- Optional customizations -->
<div
class="section-header d-flex flex-row align-items-center mt-3 mb-3"
(click)="toggleOpenIdCustomizations()"
>
<h3 class="mb-0 mr-2" id="customizations-header">
{{ "openIdOptionalCustomizations" | i18n }}
</h3>
<button
class="mb-1 btn btn-link"
type="button"
appStopClick
role="button"
aria-controls="customizations"
[attr.aria-expanded]="showOpenIdCustomizations"
aria-labelledby="customizations-header"
>
<i
class="bwi"
aria-hidden="true"
[ngClass]="{
'bwi-angle-down': !showOpenIdCustomizations,
'bwi-chevron-up': showOpenIdCustomizations
}"
></i>
</button>
</div>
<div id="customizations" [hidden]="!showOpenIdCustomizations">
<app-input-text
[label]="'additionalScopes' | i18n"
controlId="additionalScopes"
[helperText]="'separateMultipleWithComma' | i18n"
formControlName="additionalScopes"
></app-input-text>
<app-input-text
[label]="'additionalUserIdClaimTypes' | i18n"
controlId="additionalUserIdClaimTypes"
[helperText]="'separateMultipleWithComma' | i18n"
formControlName="additionalUserIdClaimTypes"
></app-input-text>
<app-input-text
[label]="'additionalEmailClaimTypes' | i18n"
controlId="additionalEmailClaimTypes"
[helperText]="'separateMultipleWithComma' | i18n"
formControlName="additionalEmailClaimTypes"
></app-input-text>
<app-input-text
[label]="'additionalNameClaimTypes' | i18n"
controlId="additionalNameClaimTypes"
[helperText]="'separateMultipleWithComma' | i18n"
formControlName="additionalNameClaimTypes"
></app-input-text>
<app-input-text
[label]="'acrValues' | i18n"
controlId="acrValues"
helperText="acr_values"
formControlName="acrValues"
></app-input-text>
<app-input-text
[label]="'expectedReturnAcrValue' | i18n"
controlId="expectedReturnAcrValue"
helperText="acr_validation"
formControlName="expectedReturnAcrValue"
></app-input-text>
</div>
</div>
</div>
<!-- SAML2 SP -->
<div *ngIf="ssoConfigForm.get('configType').value === ssoType.Saml2" [formGroup]="samlForm">
<!-- SAML2 SP -->
<div class="config-section">
<h2 class="secondary-header">{{ "samlSpConfig" | i18n }}</h2>
<app-input-text-readonly
[label]="'spEntityId' | i18n"
[controlValue]="spEntityId"
></app-input-text-readonly>
<app-input-text-readonly
[label]="'spMetadataUrl' | i18n"
[controlValue]="spMetadataUrl"
[showLaunch]="true"
></app-input-text-readonly>
<app-input-text-readonly
[label]="'spAcsUrl' | i18n"
[controlValue]="spAcsUrl"
></app-input-text-readonly>
<app-select
controlId="spNameIdFormat"
[label]="'spNameIdFormat' | i18n"
[selectOptions]="saml2NameIdFormatOptions"
formControlName="spNameIdFormat"
>
</app-select>
<app-select
controlId="spOutboundSigningAlgorithm"
[label]="'spOutboundSigningAlgorithm' | i18n"
[selectOptions]="samlSigningAlgorithmOptions"
formControlName="spOutboundSigningAlgorithm"
>
</app-select>
<app-select
controlId="spSigningBehavior"
[label]="'spSigningBehavior' | i18n"
[selectOptions]="saml2SigningBehaviourOptions"
formControlName="spSigningBehavior"
>
</app-select>
<app-select
controlId="spMinIncomingSigningAlgorithm"
[label]="'spMinIncomingSigningAlgorithm' | i18n"
[selectOptions]="samlSigningAlgorithmOptions"
formControlName="spMinIncomingSigningAlgorithm"
>
</app-select>
<app-input-checkbox
controlId="spWantAssertionsSigned"
formControlName="spWantAssertionsSigned"
[label]="'spWantAssertionsSigned' | i18n"
></app-input-checkbox>
<app-input-checkbox
controlId="spValidateCertificates"
formControlName="spValidateCertificates"
[label]="'spValidateCertificates' | i18n"
></app-input-checkbox>
</div>
<!-- SAML2 IDP -->
<div class="config-section">
<h2 class="secondary-header">{{ "samlIdpConfig" | i18n }}</h2>
<app-input-text
[label]="'idpEntityId' | i18n"
controlId="idpEntityId"
formControlName="idpEntityId"
></app-input-text>
<app-select
controlId="idpBindingType"
[label]="'idpBindingType' | i18n"
[selectOptions]="saml2BindingTypeOptions"
formControlName="idpBindingType"
>
</app-select>
<app-input-text
[label]="'idpSingleSignOnServiceUrl' | i18n"
controlId="idpSingleSignOnServiceUrl"
[helperText]="'idpSingleSignOnServiceUrlRequired' | i18n"
[stripSpaces]="true"
formControlName="idpSingleSignOnServiceUrl"
></app-input-text>
<app-input-text
[label]="'idpSingleLogoutServiceUrl' | i18n"
controlId="idpSingleLogoutServiceUrl"
[stripSpaces]="true"
formControlName="idpSingleLogoutServiceUrl"
></app-input-text>
<div class="form-group">
<label for="idpX509PublicCert">
{{ "idpX509PublicCert" | i18n }}
<small class="text-muted form-text d-inline">({{ "required" | i18n }})</small>
</label>
<textarea
formControlName="idpX509PublicCert"
class="form-control form-control-sm text-monospace"
rows="6"
id="idpX509PublicCert"
appA11yInvalid
aria-describedby="idpX509PublicCertDesc"
></textarea>
<small
id="idpX509PublicCertDesc"
class="error-inline"
role="alert"
*ngIf="samlForm.get('idpX509PublicCert').hasError('required')"
>
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
<span class="sr-only">{{ "error" | i18n }}:</span>
{{ "fieldRequiredError" | i18n: ("idpX509PublicCert" | i18n) }}
</small>
</div>
<app-select
controlId="idpOutboundSigningAlgorithm"
[label]="'idpOutboundSigningAlgorithm' | i18n"
[selectOptions]="samlSigningAlgorithmOptions"
formControlName="idpOutboundSigningAlgorithm"
>
</app-select>
<!--TODO: Uncomment once Unsolicited IdP Response is supported-->
<!-- <app-input-checkbox
controlId="idpAllowUnsolicitedAuthnResponse"
formControlName="idpAllowUnsolicitedAuthnResponse"
[label]="'idpAllowUnsolicitedAuthnResponse' | i18n"
></app-input-checkbox> -->
<app-input-checkbox
controlId="idpAllowOutboundLogoutRequests"
formControlName="idpAllowOutboundLogoutRequests"
[label]="'idpAllowOutboundLogoutRequests' | i18n"
></app-input-checkbox>
<app-input-checkbox
controlId="idpWantAuthnRequestsSigned"
formControlName="idpWantAuthnRequestsSigned"
[label]="'idpSignAuthenticationRequests' | i18n"
></app-input-checkbox>
</div>
</div>
<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>
<div
id="errorSummary"
class="error-summary text-danger"
*ngIf="this.getErrorCount(ssoConfigForm) as errorCount"
>
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
<span class="sr-only">{{ "error" | i18n }}:</span>
{{
(errorCount === 1 ? "formErrorSummarySingle" : "formErrorSummaryPlural") | i18n: errorCount
}}
</div>
</form>

View File

@@ -0,0 +1,318 @@
import { Component, OnInit } from "@angular/core";
import { AbstractControl, FormBuilder, FormGroup } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { SelectOptions } from "jslib-angular/interfaces/selectOptions";
import { dirtyRequired } from "jslib-angular/validators/dirty.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 {
OpenIdConnectRedirectBehavior,
Saml2BindingType,
Saml2NameIdFormat,
Saml2SigningBehavior,
SsoType,
} from "jslib-common/enums/ssoEnums";
import { Utils } from "jslib-common/misc/utils";
import { SsoConfigApi } from "jslib-common/models/api/ssoConfigApi";
import { Organization } from "jslib-common/models/domain/organization";
import { OrganizationSsoRequest } from "jslib-common/models/request/organization/organizationSsoRequest";
import { OrganizationSsoResponse } from "jslib-common/models/response/organization/organizationSsoResponse";
import { SsoConfigView } from "jslib-common/models/view/ssoConfigView";
const defaultSigningAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
@Component({
selector: "app-org-manage-sso",
templateUrl: "sso.component.html",
})
export class SsoComponent implements OnInit {
readonly ssoType = SsoType;
readonly ssoTypeOptions: SelectOptions[] = [
{ name: this.i18nService.t("selectType"), value: SsoType.None, disabled: true },
{ name: "OpenID Connect", value: SsoType.OpenIdConnect },
{ name: "SAML 2.0", value: SsoType.Saml2 },
];
readonly samlSigningAlgorithms = [
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
"http://www.w3.org/2000/09/xmldsig#rsa-sha384",
"http://www.w3.org/2000/09/xmldsig#rsa-sha512",
"http://www.w3.org/2000/09/xmldsig#rsa-sha1",
];
readonly saml2SigningBehaviourOptions: SelectOptions[] = [
{
name: "If IdP Wants Authn Requests Signed",
value: Saml2SigningBehavior.IfIdpWantAuthnRequestsSigned,
},
{ name: "Always", value: Saml2SigningBehavior.Always },
{ name: "Never", value: Saml2SigningBehavior.Never },
];
readonly saml2BindingTypeOptions: SelectOptions[] = [
{ name: "Redirect", value: Saml2BindingType.HttpRedirect },
{ name: "HTTP POST", value: Saml2BindingType.HttpPost },
];
readonly saml2NameIdFormatOptions: SelectOptions[] = [
{ name: "Not Configured", value: Saml2NameIdFormat.NotConfigured },
{ name: "Unspecified", value: Saml2NameIdFormat.Unspecified },
{ name: "Email Address", value: Saml2NameIdFormat.EmailAddress },
{ name: "X.509 Subject Name", value: Saml2NameIdFormat.X509SubjectName },
{ name: "Windows Domain Qualified Name", value: Saml2NameIdFormat.WindowsDomainQualifiedName },
{ name: "Kerberos Principal Name", value: Saml2NameIdFormat.KerberosPrincipalName },
{ name: "Entity Identifier", value: Saml2NameIdFormat.EntityIdentifier },
{ name: "Persistent", value: Saml2NameIdFormat.Persistent },
{ name: "Transient", value: Saml2NameIdFormat.Transient },
];
readonly connectRedirectOptions: SelectOptions[] = [
{ name: "Redirect GET", value: OpenIdConnectRedirectBehavior.RedirectGet },
{ name: "Form POST", value: OpenIdConnectRedirectBehavior.FormPost },
];
showOpenIdCustomizations = false;
loading = true;
haveTestedKeyConnector = false;
organizationId: string;
organization: Organization;
formPromise: Promise<any>;
callbackPath: string;
signedOutCallbackPath: string;
spEntityId: string;
spMetadataUrl: string;
spAcsUrl: string;
enabled = this.formBuilder.control(false);
openIdForm = this.formBuilder.group(
{
authority: ["", dirtyRequired],
clientId: ["", dirtyRequired],
clientSecret: ["", dirtyRequired],
metadataAddress: [],
redirectBehavior: [OpenIdConnectRedirectBehavior.RedirectGet, dirtyRequired],
getClaimsFromUserInfoEndpoint: [],
additionalScopes: [],
additionalUserIdClaimTypes: [],
additionalEmailClaimTypes: [],
additionalNameClaimTypes: [],
acrValues: [],
expectedReturnAcrValue: [],
},
{
updateOn: "blur",
}
);
samlForm = this.formBuilder.group(
{
spNameIdFormat: [Saml2NameIdFormat.NotConfigured],
spOutboundSigningAlgorithm: [defaultSigningAlgorithm],
spSigningBehavior: [Saml2SigningBehavior.IfIdpWantAuthnRequestsSigned],
spMinIncomingSigningAlgorithm: [defaultSigningAlgorithm],
spWantAssertionsSigned: [],
spValidateCertificates: [],
idpEntityId: ["", dirtyRequired],
idpBindingType: [Saml2BindingType.HttpRedirect],
idpSingleSignOnServiceUrl: [],
idpSingleLogoutServiceUrl: [],
idpX509PublicCert: ["", dirtyRequired],
idpOutboundSigningAlgorithm: [defaultSigningAlgorithm],
idpAllowUnsolicitedAuthnResponse: [],
idpAllowOutboundLogoutRequests: [true],
idpWantAuthnRequestsSigned: [],
},
{
updateOn: "blur",
}
);
ssoConfigForm = this.formBuilder.group({
configType: [SsoType.None],
keyConnectorEnabled: [false],
keyConnectorUrl: [""],
openId: this.openIdForm,
saml: this.samlForm,
});
constructor(
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private apiService: ApiService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private organizationService: OrganizationService
) {}
async ngOnInit() {
this.ssoConfigForm.get("configType").valueChanges.subscribe((newType: SsoType) => {
if (newType === SsoType.OpenIdConnect) {
this.openIdForm.enable();
this.samlForm.disable();
} else if (newType === SsoType.Saml2) {
this.openIdForm.disable();
this.samlForm.enable();
} else {
this.openIdForm.disable();
this.samlForm.disable();
}
});
this.samlForm
.get("spSigningBehavior")
.valueChanges.subscribe(() =>
this.samlForm.get("idpX509PublicCert").updateValueAndValidity()
);
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
await this.load();
});
}
async load() {
this.organization = await this.organizationService.get(this.organizationId);
const ssoSettings = await this.apiService.getOrganizationSso(this.organizationId);
this.populateForm(ssoSettings);
this.callbackPath = ssoSettings.urls.callbackPath;
this.signedOutCallbackPath = ssoSettings.urls.signedOutCallbackPath;
this.spEntityId = ssoSettings.urls.spEntityId;
this.spMetadataUrl = ssoSettings.urls.spMetadataUrl;
this.spAcsUrl = ssoSettings.urls.spAcsUrl;
this.loading = false;
}
async submit() {
this.validateForm(this.ssoConfigForm);
if (this.ssoConfigForm.get("keyConnectorEnabled").value) {
await this.validateKeyConnectorUrl();
}
if (!this.ssoConfigForm.valid) {
this.readOutErrors();
return;
}
const request = new OrganizationSsoRequest();
request.enabled = this.enabled.value;
request.data = SsoConfigApi.fromView(this.ssoConfigForm.value as SsoConfigView);
this.formPromise = this.apiService.postOrganizationSso(this.organizationId, request);
try {
const response = await this.formPromise;
this.populateForm(response);
this.platformUtilsService.showToast("success", null, this.i18nService.t("ssoSettingsSaved"));
} catch {
// Logged by appApiAction, do nothing
}
this.formPromise = null;
}
async validateKeyConnectorUrl() {
if (this.haveTestedKeyConnector) {
return;
}
this.keyConnectorUrl.markAsPending();
try {
await this.apiService.getKeyConnectorAlive(this.keyConnectorUrl.value);
this.keyConnectorUrl.updateValueAndValidity();
} catch {
this.keyConnectorUrl.setErrors({
invalidUrl: true,
});
}
this.haveTestedKeyConnector = true;
}
toggleOpenIdCustomizations() {
this.showOpenIdCustomizations = !this.showOpenIdCustomizations;
}
getErrorCount(form: FormGroup): number {
return Object.values(form.controls).reduce((acc: number, control: AbstractControl) => {
if (control instanceof FormGroup) {
return acc + this.getErrorCount(control);
}
if (control.errors == null) {
return acc;
}
return acc + Object.keys(control.errors).length;
}, 0);
}
get enableTestKeyConnector() {
return (
this.ssoConfigForm.get("keyConnectorEnabled").value &&
!Utils.isNullOrWhitespace(this.keyConnectorUrl?.value)
);
}
get keyConnectorUrl() {
return this.ssoConfigForm.get("keyConnectorUrl");
}
get samlSigningAlgorithmOptions(): SelectOptions[] {
return this.samlSigningAlgorithms.map((algorithm) => ({ name: algorithm, value: algorithm }));
}
private validateForm(form: FormGroup) {
Object.values(form.controls).forEach((control: AbstractControl) => {
if (control.disabled) {
return;
}
if (control instanceof FormGroup) {
this.validateForm(control);
} else {
control.markAsDirty();
control.markAsTouched();
control.updateValueAndValidity();
}
});
}
private populateForm(ssoSettings: OrganizationSsoResponse) {
this.enabled.setValue(ssoSettings.enabled);
if (ssoSettings.data != null) {
const ssoConfigView = new SsoConfigView(ssoSettings.data);
this.ssoConfigForm.patchValue(ssoConfigView);
}
}
private readOutErrors() {
const errorText = this.i18nService.t("error");
const errorCount = this.getErrorCount(this.ssoConfigForm);
const errorCountText = this.i18nService.t(
errorCount === 1 ? "formErrorSummarySingle" : "formErrorSummaryPlural",
errorCount.toString()
);
const div = document.createElement("div");
div.className = "sr-only";
div.id = "srErrorCount";
div.setAttribute("aria-live", "polite");
div.innerText = errorText + ": " + errorCountText;
const existing = document.getElementById("srErrorCount");
if (existing != null) {
existing.remove();
}
document.body.append(div);
}
}

View File

@@ -0,0 +1,46 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { AuthGuard } from "jslib-angular/guards/auth.guard";
import { Permissions } from "jslib-common/enums/permissions";
import { PermissionsGuard } from "src/app/organizations/guards/permissions.guard";
import { OrganizationLayoutComponent } from "src/app/organizations/layouts/organization-layout.component";
import { ManageComponent } from "src/app/organizations/manage/manage.component";
import { NavigationPermissionsService } from "src/app/organizations/services/navigation-permissions.service";
import { SsoComponent } from "./manage/sso.component";
const routes: Routes = [
{
path: "organizations/:organizationId",
component: OrganizationLayoutComponent,
canActivate: [AuthGuard, PermissionsGuard],
children: [
{
path: "manage",
component: ManageComponent,
canActivate: [PermissionsGuard],
data: {
permissions: NavigationPermissionsService.getPermissions("manage"),
},
children: [
{
path: "sso",
component: SsoComponent,
canActivate: [PermissionsGuard],
data: {
permissions: [Permissions.ManageSso],
},
},
],
},
],
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class OrganizationsRoutingModule {}

View File

@@ -0,0 +1,32 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { JslibModule } from "jslib-angular/jslib.module";
import { InputCheckboxComponent } from "./components/input-checkbox.component";
import { InputTextReadOnlyComponent } from "./components/input-text-readonly.component";
import { InputTextComponent } from "./components/input-text.component";
import { SelectComponent } from "./components/select.component";
import { SsoComponent } from "./manage/sso.component";
import { OrganizationsRoutingModule } from "./organizations-routing.module";
// Form components are for use in the SSO Configuration Form only and should not be exported for use elsewhere.
// They will be deprecated by the Component Library.
@NgModule({
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
JslibModule,
OrganizationsRoutingModule,
],
declarations: [
InputCheckboxComponent,
InputTextComponent,
InputTextReadOnlyComponent,
SelectComponent,
SsoComponent,
],
})
export class OrganizationsModule {}

View File

@@ -0,0 +1,12 @@
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="enabled"
[formControl]="enabled"
name="Enabled"
/>
<label class="form-check-label" for="enabled">{{ "enabled" | i18n }}</label>
</div>
</div>

View File

@@ -0,0 +1,21 @@
import { Component } from "@angular/core";
import { PolicyType } from "jslib-common/enums/policyType";
import {
BasePolicy,
BasePolicyComponent,
} from "src/app/organizations/policies/base-policy.component";
export class DisablePersonalVaultExportPolicy extends BasePolicy {
name = "disablePersonalVaultExport";
description = "disablePersonalVaultExportDesc";
type = PolicyType.DisablePersonalVaultExport;
component = DisablePersonalVaultExportPolicyComponent;
}
@Component({
selector: "policy-disable-personal-vault-export",
templateUrl: "disable-personal-vault-export.component.html",
})
export class DisablePersonalVaultExportPolicyComponent extends BasePolicyComponent {}

View File

@@ -0,0 +1,47 @@
<app-callout type="tip" title="{{ 'prerequisite' | i18n }}">
{{ "requireSsoPolicyReq" | i18n }}
</app-callout>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="enabled"
[formControl]="enabled"
name="Enabled"
/>
<label class="form-check-label" for="enabled">{{ "enabled" | i18n }}</label>
</div>
</div>
<div [formGroup]="data">
<div class="form-group">
<label for="hours">{{ "maximumVaultTimeoutLabel" | i18n }}</label>
<div class="row">
<div class="col-6">
<input
id="hours"
class="form-control"
type="number"
min="0"
name="hours"
formControlName="hours"
/>
<small>{{ "hours" | i18n }}</small>
</div>
<div class="col-6">
<input
id="minutes"
class="form-control"
type="number"
min="0"
max="59"
name="minutes"
formControlName="minutes"
/>
<small>{{ "minutes" | i18n }}</small>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,70 @@
import { Component } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PolicyType } from "jslib-common/enums/policyType";
import { PolicyRequest } from "jslib-common/models/request/policyRequest";
import {
BasePolicy,
BasePolicyComponent,
} from "src/app/organizations/policies/base-policy.component";
export class MaximumVaultTimeoutPolicy extends BasePolicy {
name = "maximumVaultTimeout";
description = "maximumVaultTimeoutDesc";
type = PolicyType.MaximumVaultTimeout;
component = MaximumVaultTimeoutPolicyComponent;
}
@Component({
selector: "policy-maximum-timeout",
templateUrl: "maximum-vault-timeout.component.html",
})
export class MaximumVaultTimeoutPolicyComponent extends BasePolicyComponent {
data = this.formBuilder.group({
hours: [null],
minutes: [null],
});
constructor(private formBuilder: FormBuilder, private i18nService: I18nService) {
super();
}
loadData() {
const minutes = this.policyResponse.data?.minutes;
if (minutes == null) {
return;
}
this.data.patchValue({
hours: Math.floor(minutes / 60),
minutes: minutes % 60,
});
}
buildRequestData() {
if (this.data.value.hours == null && this.data.value.minutes == null) {
return null;
}
return {
minutes: this.data.value.hours * 60 + this.data.value.minutes,
};
}
buildRequest(policiesEnabledMap: Map<PolicyType, boolean>): Promise<PolicyRequest> {
const singleOrgEnabled = policiesEnabledMap.get(PolicyType.SingleOrg) ?? false;
if (this.enabled.value && !singleOrgEnabled) {
throw new Error(this.i18nService.t("requireSsoPolicyReqError"));
}
const data = this.buildRequestData();
if (data?.minutes == null || data?.minutes <= 0) {
throw new Error(this.i18nService.t("invalidMaximumVaultTimeout"));
}
return super.buildRequest(policiesEnabledMap);
}
}

View File

@@ -1,35 +1,46 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="addTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="addTitle">
{{'addExistingOrganization' | 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">
<div class="card-body text-center" *ngIf="loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
{{'loading' | i18n}}
</div>
<ng-container *ngIf="!loading">
<table class="table table-hover table-list">
<tr *ngFor="let o of organizations">
<td width="30">
<app-avatar [data]="o.name" size="25" [circle]="true" [fontSize]="14"></app-avatar>
</td>
<td>
{{o.name}}
</td>
<td>
<button class="btn btn-outline-secondary pull-right" (click)="add(o)" [disabled]="formPromise">Add</button>
</td>
</tr>
</table>
</ng-container>
</div>
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="addTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="addTitle">
{{ "addExistingOrganization" | 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">
<div class="card-body text-center" *ngIf="loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
{{ "loading" | i18n }}
</div>
<ng-container *ngIf="!loading">
<table class="table table-hover table-list">
<tr *ngFor="let o of organizations">
<td width="30">
<app-avatar [data]="o.name" size="25" [circle]="true" [fontSize]="14"></app-avatar>
</td>
<td>
{{ o.name }}
</td>
<td>
<button
class="btn btn-outline-secondary pull-right"
(click)="add(o)"
[disabled]="formPromise"
>
Add
</button>
</td>
</tr>
</table>
</ng-container>
</div>
</div>
</div>
</div>

View File

@@ -1,83 +1,84 @@
import {
Component,
EventEmitter,
Input,
OnInit,
Output
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Component, EventEmitter, Input, OnInit, Output } 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 { UserService } from 'jslib-common/abstractions/user.service';
import { ValidationService } from "jslib-angular/services/validation.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { ProviderService } from "jslib-common/abstractions/provider.service";
import { Organization } from "jslib-common/models/domain/organization";
import { Provider } from "jslib-common/models/domain/provider";
import { ValidationService } from 'jslib-angular/services/validation.service';
import { ProviderService } from '../services/provider.service';
import { Organization } from 'jslib-common/models/domain/organization';
import { Provider } from 'jslib-common/models/domain/provider';
import { PlanType } from 'jslib-common/enums/planType';
import { WebProviderService } from "../services/webProvider.service";
@Component({
selector: 'provider-add-organization',
templateUrl: 'add-organization.component.html',
selector: "provider-add-organization",
templateUrl: "add-organization.component.html",
})
export class AddOrganizationComponent implements OnInit {
@Input() providerId: string;
@Input() organizations: Organization[];
@Output() onAddedOrganization = new EventEmitter();
@Input() providerId: string;
@Input() organizations: Organization[];
@Output() onAddedOrganization = new EventEmitter();
provider: Provider;
formPromise: Promise<any>;
loading = true;
provider: Provider;
formPromise: Promise<any>;
loading = true;
constructor(
private providerService: ProviderService,
private webProviderService: WebProviderService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private validationService: ValidationService
) {}
constructor(private userService: UserService, private providerService: ProviderService,
private toasterService: ToasterService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private validationService: ValidationService,
private apiService: ApiService) { }
async ngOnInit() {
await this.load();
}
async ngOnInit() {
await this.load();
async load() {
if (this.providerId == null) {
return;
}
async load() {
if (this.providerId == null) {
return;
}
this.provider = await this.providerService.get(this.providerId);
this.provider = await this.userService.getProvider(this.providerId);
this.loading = false;
}
this.loading = false;
async add(organization: Organization) {
if (this.formPromise) {
return;
}
async add(organization: Organization) {
if (this.formPromise) {
return;
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("addOrganizationConfirmation", organization.name, this.provider.name),
organization.name,
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('addOrganizationConfirmation', organization.name, this.provider.name), organization.name,
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
try {
this.formPromise = this.providerService.addOrganizationToProvider(this.providerId, organization.id);
await this.formPromise;
} catch (e) {
this.validationService.showError(e);
return;
} finally {
this.formPromise = null;
}
this.toasterService.popAsync('success', null, this.i18nService.t('organizationJoinedProvider'));
this.onAddedOrganization.emit();
if (!confirmed) {
return false;
}
try {
this.formPromise = this.webProviderService.addOrganizationToProvider(
this.providerId,
organization.id
);
await this.formPromise;
} catch (e) {
this.validationService.showError(e);
return;
} finally {
this.formPromise = null;
}
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("organizationJoinedProvider")
);
this.onAddedOrganization.emit();
}
}

View File

@@ -1,62 +1,90 @@
<div class="page-header d-flex">
<h1>{{'clients' | i18n}}</h1>
<h1>{{ "clients" | i18n }}</h1>
<div class="ml-auto d-flex">
<div>
<label class="sr-only" for="search">{{'search' | i18n}}</label>
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
[(ngModel)]="searchText">
</div>
<a class="btn btn-sm btn-outline-primary ml-3" routerLink="create" *ngIf="manageOrganizations">
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
{{'newClientOrganization' | i18n}}
</a>
<button class="btn btn-sm btn-outline-primary ml-3" (click)="addExistingOrganization()"
*ngIf="manageOrganizations && showAddExisting">
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
{{'addExistingOrganization' | i18n}}
</button>
<div class="ml-auto d-flex">
<div>
<label class="sr-only" for="search">{{ "search" | i18n }}</label>
<input
type="search"
class="form-control form-control-sm"
id="search"
placeholder="{{ 'search' | i18n }}"
[(ngModel)]="searchText"
/>
</div>
<a class="btn btn-sm btn-outline-primary ml-3" routerLink="create" *ngIf="manageOrganizations">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "newClientOrganization" | i18n }}
</a>
<button
class="btn btn-sm btn-outline-primary ml-3"
(click)="addExistingOrganization()"
*ngIf="manageOrganizations && showAddExisting"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "addExistingOrganization" | i18n }}
</button>
</div>
</div>
<ng-container *ngIf="loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container
*ngIf="!loading && (clients | search:searchText:'organizationName':'id') as searchedClients">
<p *ngIf="!searchedClients.length">{{'noClientsInList' | i18n}}</p>
<ng-container *ngIf="searchedClients.length">
<table class="table table-hover table-list" infiniteScroll [infiniteScrollDistance]="1"
[infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
<tbody>
<tr *ngFor="let o of searchedClients">
<td width="30">
<app-avatar [data]="o.organizationName" size="25" [circle]="true" [fontSize]="14"></app-avatar>
</td>
<td>
<a [routerLink]="['/organizations', o.organizationId]">{{o.organizationName}}</a>
</td>
<td class="table-list-options" *ngIf="manageOrganizations">
<div class="dropdown" appListDropdown>
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
appA11yTitle="{{'options' | i18n}}">
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(o)">
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
{{'remove' | i18n}}
</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</ng-container>
*ngIf="!loading && (clients | search: searchText:'organizationName':'id') as searchedClients"
>
<p *ngIf="!searchedClients.length">{{ "noClientsInList" | i18n }}</p>
<ng-container *ngIf="searchedClients.length">
<table
class="table table-hover table-list"
infiniteScroll
[infiniteScrollDistance]="1"
[infiniteScrollDisabled]="!isPaging()"
(scrolled)="loadMore()"
>
<tbody>
<tr *ngFor="let o of searchedClients">
<td width="30">
<app-avatar
[data]="o.organizationName"
size="25"
[circle]="true"
[fontSize]="14"
></app-avatar>
</td>
<td>
<a [routerLink]="['/organizations', o.organizationId]">{{ o.organizationName }}</a>
</td>
<td class="table-list-options" *ngIf="manageOrganizations">
<div class="dropdown" appListDropdown>
<button
class="btn btn-outline-secondary dropdown-toggle"
type="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(o)">
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
{{ "remove" | i18n }}
</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</ng-container>
</ng-container>
<ng-template #add></ng-template>

View File

@@ -1,169 +1,179 @@
import {
Component,
ComponentFactoryResolver,
OnInit,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { first } from "rxjs/operators";
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 { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { SearchService } from 'jslib-common/abstractions/search.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { ModalService } from "jslib-angular/services/modal.service";
import { ValidationService } from "jslib-angular/services/validation.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 { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { ProviderService } from "jslib-common/abstractions/provider.service";
import { SearchService } from "jslib-common/abstractions/search.service";
import { PlanType } from "jslib-common/enums/planType";
import { ProviderUserType } from "jslib-common/enums/providerUserType";
import { Organization } from "jslib-common/models/domain/organization";
import { ProviderOrganizationOrganizationDetailsResponse } from "jslib-common/models/response/provider/providerOrganizationResponse";
import { PlanType } from 'jslib-common/enums/planType';
import { ProviderUserType } from 'jslib-common/enums/providerUserType';
import { WebProviderService } from "../services/webProvider.service";
import { ValidationService } from 'jslib-angular/services/validation.service';
import { AddOrganizationComponent } from "./add-organization.component";
import {
ProviderOrganizationOrganizationDetailsResponse
} from 'jslib-common/models/response/provider/providerOrganizationResponse';
import { Organization } from 'jslib-common/models/domain/organization';
import { ModalComponent } from 'src/app/modal.component';
import { ProviderService } from '../services/provider.service';
import { AddOrganizationComponent } from './add-organization.component';
const DisallowedPlanTypes = [PlanType.Free, PlanType.FamiliesAnnually2019, PlanType.FamiliesAnnually];
const DisallowedPlanTypes = [
PlanType.Free,
PlanType.FamiliesAnnually2019,
PlanType.FamiliesAnnually,
];
@Component({
templateUrl: 'clients.component.html',
templateUrl: "clients.component.html",
})
export class ClientsComponent implements OnInit {
@ViewChild("add", { read: ViewContainerRef, static: true }) addModalRef: ViewContainerRef;
@ViewChild('add', { read: ViewContainerRef, static: true }) addModalRef: ViewContainerRef;
providerId: any;
searchText: string;
addableOrganizations: Organization[];
loading = true;
manageOrganizations = false;
showAddExisting = false;
providerId: any;
searchText: string;
addableOrganizations: Organization[];
loading = true;
manageOrganizations = false;
showAddExisting = false;
clients: ProviderOrganizationOrganizationDetailsResponse[];
pagedClients: ProviderOrganizationOrganizationDetailsResponse[];
clients: ProviderOrganizationOrganizationDetailsResponse[];
pagedClients: ProviderOrganizationOrganizationDetailsResponse[];
modal: ModalComponent;
protected didScroll = false;
protected pageSize = 100;
protected actionPromise: Promise<any>;
private pagedClientsCount = 0;
protected didScroll = false;
protected pageSize = 100;
protected actionPromise: Promise<any>;
private pagedClientsCount = 0;
constructor(
private route: ActivatedRoute,
private providerService: ProviderService,
private apiService: ApiService,
private searchService: SearchService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private validationService: ValidationService,
private webProviderService: WebProviderService,
private logService: LogService,
private modalService: ModalService,
private organizationService: OrganizationService
) {}
constructor(private route: ActivatedRoute, private userService: UserService,
private apiService: ApiService, private searchService: SearchService,
private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
private toasterService: ToasterService, private validationService: ValidationService,
private providerService: ProviderService, private componentFactoryResolver: ComponentFactoryResolver,
private logService: LogService) { }
async ngOnInit() {
this.route.parent.params.subscribe(async (params) => {
this.providerId = params.providerId;
async ngOnInit() {
this.route.parent.params.subscribe(async params => {
this.providerId = params.providerId;
await this.load();
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
this.searchText = qParams.search;
});
});
}
async load() {
const response = await this.apiService.getProviderClients(this.providerId);
this.clients = response.data != null && response.data.length > 0 ? response.data : [];
this.manageOrganizations =
(await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin;
const candidateOrgs = (await this.organizationService.getAll()).filter(
(o) => o.isOwner && o.providerId == null
);
const allowedOrgsIds = await Promise.all(
candidateOrgs.map((o) => this.apiService.getOrganization(o.id))
).then((orgs) =>
orgs.filter((o) => !DisallowedPlanTypes.includes(o.planType)).map((o) => o.id)
);
this.addableOrganizations = candidateOrgs.filter((o) => allowedOrgsIds.includes(o.id));
this.showAddExisting = this.addableOrganizations.length !== 0;
this.loading = false;
}
isPaging() {
const searching = this.isSearching();
if (searching && this.didScroll) {
this.resetPaging();
}
return !searching && this.clients && this.clients.length > this.pageSize;
}
isSearching() {
return this.searchService.isSearchable(this.searchText);
}
async resetPaging() {
this.pagedClients = [];
this.loadMore();
}
loadMore() {
if (!this.clients || this.clients.length <= this.pageSize) {
return;
}
const pagedLength = this.pagedClients.length;
let pagedSize = this.pageSize;
if (pagedLength === 0 && this.pagedClientsCount > this.pageSize) {
pagedSize = this.pagedClientsCount;
}
if (this.clients.length > pagedLength) {
this.pagedClients = this.pagedClients.concat(
this.clients.slice(pagedLength, pagedLength + pagedSize)
);
}
this.pagedClientsCount = this.pagedClients.length;
this.didScroll = this.pagedClients.length > this.pageSize;
}
async addExistingOrganization() {
const [modal] = await this.modalService.openViewRef(
AddOrganizationComponent,
this.addModalRef,
(comp) => {
comp.providerId = this.providerId;
comp.organizations = this.addableOrganizations;
comp.onAddedOrganization.subscribe(async () => {
try {
await this.load();
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
this.searchText = qParams.search;
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();
}
});
modal.close();
} catch (e) {
this.logService.error(`Handled exception: ${e}`);
}
});
}
);
}
async remove(organization: ProviderOrganizationOrganizationDetailsResponse) {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("detachOrganizationConfirmation"),
organization.organizationName,
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return false;
}
async load() {
const response = await this.apiService.getProviderClients(this.providerId);
this.clients = response.data != null && response.data.length > 0 ? response.data : [];
this.manageOrganizations = (await this.userService.getProvider(this.providerId)).type === ProviderUserType.ProviderAdmin;
const candidateOrgs = (await this.userService.getAllOrganizations()).filter(o => o.providerId == null);
const allowedOrgsIds = await Promise.all(candidateOrgs.map(o => this.apiService.getOrganization(o.id))).then(orgs =>
orgs.filter(o => !DisallowedPlanTypes.includes(o.planType))
.map(o => o.id));
this.addableOrganizations = candidateOrgs.filter(o => allowedOrgsIds.includes(o.id));
this.showAddExisting = this.addableOrganizations.length != 0;
this.loading = false;
}
isPaging() {
const searching = this.isSearching();
if (searching && this.didScroll) {
this.resetPaging();
}
return !searching && this.clients && this.clients.length > this.pageSize;
}
isSearching() {
return this.searchService.isSearchable(this.searchText);
}
async resetPaging() {
this.pagedClients = [];
this.loadMore();
}
loadMore() {
if (!this.clients || this.clients.length <= this.pageSize) {
return;
}
const pagedLength = this.pagedClients.length;
let pagedSize = this.pageSize;
if (pagedLength === 0 && this.pagedClientsCount > this.pageSize) {
pagedSize = this.pagedClientsCount;
}
if (this.clients.length > pagedLength) {
this.pagedClients = this.pagedClients.concat(this.clients.slice(pagedLength, pagedLength + pagedSize));
}
this.pagedClientsCount = this.pagedClients.length;
this.didScroll = this.pagedClients.length > this.pageSize;
}
addExistingOrganization() {
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.addModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<AddOrganizationComponent>(AddOrganizationComponent, this.addModalRef);
childComponent.providerId = this.providerId;
childComponent.organizations = this.addableOrganizations;
childComponent.onAddedOrganization.subscribe(async () => {
try {
await this.load();
this.modal.close();
} catch (e) {
this.logService.error(`Handled exception: ${e}`);
}
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
async remove(organization: ProviderOrganizationOrganizationDetailsResponse) {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('detachOrganizationConfirmation'), organization.organizationName,
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
this.actionPromise = this.providerService.detachOrganizastion(this.providerId, organization.id);
try {
await this.actionPromise;
this.toasterService.popAsync('success', null, this.i18nService.t('detachedOrganization', organization.organizationName));
await this.load();
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
this.actionPromise = this.webProviderService.detachOrganizastion(
this.providerId,
organization.id
);
try {
await this.actionPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("detachedOrganization", organization.organizationName)
);
await this.load();
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
}

View File

@@ -1,5 +1,5 @@
<div class="page-header">
<h1>{{'newClientOrganization' | i18n}}</h1>
<h1>{{ "newClientOrganization" | i18n }}</h1>
</div>
<p>{{'newClientOrganizationDesc' | i18n}}</p>
<p>{{ "newClientOrganizationDesc" | i18n }}</p>
<app-organization-plans [providerId]="providerId"></app-organization-plans>

View File

@@ -1,26 +1,23 @@
import {
Component,
OnInit,
ViewChild,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Component, OnInit, ViewChild } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { OrganizationPlansComponent } from 'src/app/settings/organization-plans.component';
import { OrganizationPlansComponent } from "src/app/settings/organization-plans.component";
@Component({
selector: 'app-create-organization',
templateUrl: 'create-organization.component.html',
selector: "app-create-organization",
templateUrl: "create-organization.component.html",
})
export class CreateOrganizationComponent implements OnInit {
@ViewChild(OrganizationPlansComponent, { static: true }) orgPlansComponent: OrganizationPlansComponent;
@ViewChild(OrganizationPlansComponent, { static: true })
orgPlansComponent: OrganizationPlansComponent;
providerId: string;
providerId: string;
constructor(private route: ActivatedRoute) { }
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.route.parent.params.subscribe(async params => {
this.providerId = params.providerId;
});
}
ngOnInit() {
this.route.parent.params.subscribe(async (params) => {
this.providerId = params.providerId;
});
}
}

View File

@@ -0,0 +1,26 @@
import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, CanActivate, Router } from "@angular/router";
import { ProviderService } from "jslib-common/abstractions/provider.service";
import { Permissions } from "jslib-common/enums/permissions";
@Injectable()
export class PermissionsGuard implements CanActivate {
constructor(private providerService: ProviderService, private router: Router) {}
async canActivate(route: ActivatedRouteSnapshot) {
const provider = await this.providerService.get(route.params.providerId);
const permissions = route.data == null ? null : (route.data.permissions as Permissions[]);
if (
(permissions.indexOf(Permissions.AccessEventLogs) !== -1 && provider.canAccessEventLogs) ||
(permissions.indexOf(Permissions.ManageProvider) !== -1 && provider.isProviderAdmin) ||
(permissions.indexOf(Permissions.ManageUsers) !== -1 && provider.canManageUsers)
) {
return true;
}
this.router.navigate(["/providers", provider.id]);
return false;
}
}

View File

@@ -0,0 +1,31 @@
import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, CanActivate, Router } from "@angular/router";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { ProviderService } from "jslib-common/abstractions/provider.service";
@Injectable()
export class ProviderGuard implements CanActivate {
constructor(
private router: Router,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private providerService: ProviderService
) {}
async canActivate(route: ActivatedRouteSnapshot) {
const provider = await this.providerService.get(route.params.providerId);
if (provider == null) {
this.router.navigate(["/"]);
return false;
}
if (!provider.isProviderAdmin && !provider.enabled) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("providerIsDisabled"));
this.router.navigate(["/"]);
return false;
}
return true;
}
}

View File

@@ -1,35 +1,46 @@
<div class="mt-5 d-flex justify-content-center" *ngIf="loading">
<div>
<img src="/src/images/logo-dark@2x.png" class="mb-4 logo" alt="Bitwarden">
<p class="text-center">
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</p>
</div>
<div>
<img class="mb-4 logo logo-themed" alt="Bitwarden" />
<p class="text-center">
<i
class="bwi bwi-spinner bwi-spin bwi-2x text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</p>
</div>
</div>
<div class="container" *ngIf="!loading && !authed">
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{'joinProvider' | i18n}}</p>
<div class="card d-block">
<div class="card-body">
<p class="text-center">
{{providerName}}
<strong class="d-block mt-2">{{email}}</strong>
</p>
<p>{{'joinProviderDesc' | i18n}}</p>
<hr>
<div class="d-flex">
<a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block">
{{'logIn' | i18n}}
</a>
<a routerLink="/register" [queryParams]="{email: email}"
class="btn btn-primary btn-block ml-2 mt-0">
{{'createAccount' | i18n}}
</a>
</div>
</div>
</div>
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{ "joinProvider" | i18n }}</p>
<div class="card d-block">
<div class="card-body">
<p class="text-center">
{{ providerName }}
<strong class="d-block mt-2">{{ email }}</strong>
</p>
<p>{{ "joinProviderDesc" | i18n }}</p>
<hr />
<div class="d-flex">
<a
routerLink="/login"
[queryParams]="{ email: email }"
class="btn btn-primary btn-block"
>
{{ "logIn" | i18n }}
</a>
<a
routerLink="/register"
[queryParams]="{ email: email }"
class="btn btn-primary btn-block ml-2 mt-0"
>
{{ "createAccount" | i18n }}
</a>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,48 +1,55 @@
import { Component } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Toast, ToasterService } from 'angular2-toaster';
import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { BaseAcceptComponent } from 'src/app/common/base.accept.component';
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 { StateService } from "jslib-common/abstractions/state.service";
import { ProviderUserAcceptRequest } from "jslib-common/models/request/provider/providerUserAcceptRequest";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { ProviderUserAcceptRequest } from 'jslib-common/models/request/provider/providerUserAcceptRequest';
import { BaseAcceptComponent } from "src/app/common/base.accept.component";
@Component({
selector: 'app-accept-provider',
templateUrl: 'accept-provider.component.html',
selector: "app-accept-provider",
templateUrl: "accept-provider.component.html",
})
export class AcceptProviderComponent extends BaseAcceptComponent {
providerName: string;
providerName: string;
failedMessage = 'providerInviteAcceptFailed';
failedMessage = "providerInviteAcceptFailed";
requiredParameters = ['providerId', 'providerUserId', 'token'];
requiredParameters = ["providerId", "providerUserId", "token"];
constructor(router: Router, toasterService: ToasterService, i18nService: I18nService, route: ActivatedRoute,
userService: UserService, stateService: StateService, private apiService: ApiService) {
super(router, toasterService, i18nService, route, userService, stateService);
}
constructor(
router: Router,
i18nService: I18nService,
route: ActivatedRoute,
stateService: StateService,
private apiService: ApiService,
platformUtilService: PlatformUtilsService
) {
super(router, platformUtilService, i18nService, route, stateService);
}
async authedHandler(qParams: any) {
const request = new ProviderUserAcceptRequest();
request.token = qParams.token;
async authedHandler(qParams: any) {
const request = new ProviderUserAcceptRequest();
request.token = qParams.token;
await this.apiService.postProviderUserAccept(qParams.providerId, qParams.providerUserId, request);
const toast: Toast = {
type: 'success',
title: this.i18nService.t('inviteAccepted'),
body: this.i18nService.t('providerInviteAcceptedDesc'),
timeout: 10000,
};
this.toasterService.popAsync(toast);
this.router.navigate(['/vault']);
}
await this.apiService.postProviderUserAccept(
qParams.providerId,
qParams.providerUserId,
request
);
this.platformUtilService.showToast(
"success",
this.i18nService.t("inviteAccepted"),
this.i18nService.t("providerInviteAcceptedDesc"),
{ timeout: 10000 }
);
this.router.navigate(["/vault"]);
}
async unauthedHandler(qParams: any) {
this.providerName = qParams.providerName;
}
async unauthedHandler(qParams: any) {
this.providerName = qParams.providerName;
}
}

View File

@@ -1,38 +1,33 @@
import {
Component,
Input,
} from '@angular/core';
import { Component, Input } from "@angular/core";
import { ProviderUserBulkConfirmRequest } from 'jslib-common/models/request/provider/providerUserBulkConfirmRequest';
import { ProviderUserBulkRequest } from 'jslib-common/models/request/provider/providerUserBulkRequest';
import { ProviderUserStatusType } from "jslib-common/enums/providerUserStatusType";
import { ProviderUserBulkConfirmRequest } from "jslib-common/models/request/provider/providerUserBulkConfirmRequest";
import { ProviderUserBulkRequest } from "jslib-common/models/request/provider/providerUserBulkRequest";
import { ProviderUserStatusType } from 'jslib-common/enums/providerUserStatusType';
import { BulkConfirmComponent as OrganizationBulkConfirmComponent } from 'src/app/organizations/manage/bulk/bulk-confirm.component';
import { BulkUserDetails } from 'src/app/organizations/manage/bulk/bulk-status.component';
import { BulkConfirmComponent as OrganizationBulkConfirmComponent } from "src/app/organizations/manage/bulk/bulk-confirm.component";
import { BulkUserDetails } from "src/app/organizations/manage/bulk/bulk-status.component";
@Component({
templateUrl: '/src/app/organizations/manage/bulk/bulk-confirm.component.html',
templateUrl: "../../../../../../src/app/organizations/manage/bulk/bulk-confirm.component.html",
})
export class BulkConfirmComponent extends OrganizationBulkConfirmComponent {
@Input() providerId: string;
@Input() providerId: string;
protected isAccepted(user: BulkUserDetails) {
return user.status === ProviderUserStatusType.Accepted;
}
protected isAccepted(user: BulkUserDetails) {
return user.status === ProviderUserStatusType.Accepted;
}
protected async getPublicKeys() {
const request = new ProviderUserBulkRequest(this.filteredUsers.map((user) => user.id));
return await this.apiService.postProviderUsersPublicKey(this.providerId, request);
}
protected async getPublicKeys() {
const request = new ProviderUserBulkRequest(this.filteredUsers.map(user => user.id));
return await this.apiService.postProviderUsersPublicKey(this.providerId, request);
}
protected getCryptoKey() {
return this.cryptoService.getProviderKey(this.providerId);
}
protected getCryptoKey() {
return this.cryptoService.getProviderKey(this.providerId);
}
protected async postConfirmRequest(userIdsWithKeys: any[]) {
const request = new ProviderUserBulkConfirmRequest(userIdsWithKeys);
return await this.apiService.postProviderUserBulkConfirm(this.providerId, request);
}
protected async postConfirmRequest(userIdsWithKeys: any[]) {
const request = new ProviderUserBulkConfirmRequest(userIdsWithKeys);
return await this.apiService.postProviderUserBulkConfirm(this.providerId, request);
}
}

View File

@@ -1,21 +1,17 @@
import {
Component,
Input,
} from '@angular/core';
import { Component, Input } from "@angular/core";
import { ProviderUserBulkRequest } from 'jslib-common/models/request/provider/providerUserBulkRequest';
import { ProviderUserBulkRequest } from "jslib-common/models/request/provider/providerUserBulkRequest";
import { BulkRemoveComponent as OrganizationBulkRemoveComponent } from 'src/app/organizations/manage/bulk/bulk-remove.component';
import { BulkRemoveComponent as OrganizationBulkRemoveComponent } from "src/app/organizations/manage/bulk/bulk-remove.component";
@Component({
templateUrl: '/src/app/organizations/manage/bulk/bulk-remove.component.html',
templateUrl: "../../../../../../src/app/organizations/manage/bulk/bulk-remove.component.html",
})
export class BulkRemoveComponent extends OrganizationBulkRemoveComponent {
@Input() providerId: string;
@Input() providerId: string;
async deleteUsers() {
const request = new ProviderUserBulkRequest(this.users.map(user => user.id));
return await this.apiService.deleteManyProviderUsers(this.providerId, request);
}
async deleteUsers() {
const request = new ProviderUserBulkRequest(this.users.map((user) => user.id));
return await this.apiService.deleteManyProviderUsers(this.providerId, request);
}
}

View File

@@ -1,68 +1,107 @@
<div class="page-header d-flex">
<h1>{{'eventLogs' | i18n}}</h1>
<div class="ml-auto d-flex">
<div class="form-inline">
<label class="sr-only" for="start">{{'startDate' | i18n}}</label>
<input type="datetime-local" class="form-control form-control-sm" id="start"
placeholder="{{'startDate' | i18n}}" [(ngModel)]="start" placeholder="YYYY-MM-DDTHH:MM"
(change)="dirtyDates = true">
<span class="mx-2">-</span>
<label class="sr-only" for="end">{{'endDate' | i18n}}</label>
<input type="datetime-local" class="form-control form-control-sm" id="end"
placeholder="{{'endDate' | i18n}}" [(ngModel)]="end" placeholder="YYYY-MM-DDTHH:MM"
(change)="dirtyDates = true">
</div>
<form #refreshForm [appApiAction]="refreshPromise" class="d-inline">
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="loadEvents(true)"
[disabled]="loaded && refreshForm.loading">
<i class="fa fa-refresh fa-fw" aria-hidden="true" [ngClass]="{'fa-spin': loaded && refreshForm.loading}"></i>
{{'refresh' | i18n}}
</button>
</form>
<form #exportForm [appApiAction]="exportPromise" class="d-inline">
<button type="button" class="btn btn-sm btn-outline-primary btn-submit manual ml-3"
[ngClass]="{loading:exportForm.loading}" (click)="exportEvents()"
[disabled]="loaded && exportForm.loading || dirtyDates">
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
<span>{{'export' | i18n}}</span>
</button>
</form>
<h1>{{ "eventLogs" | i18n }}</h1>
<div class="ml-auto d-flex">
<div class="form-inline">
<label class="sr-only" for="start">{{ "startDate" | i18n }}</label>
<input
type="datetime-local"
class="form-control form-control-sm"
id="start"
placeholder="{{ 'startDate' | i18n }}"
[(ngModel)]="start"
placeholder="YYYY-MM-DDTHH:MM"
(change)="dirtyDates = true"
/>
<span class="mx-2">-</span>
<label class="sr-only" for="end">{{ "endDate" | i18n }}</label>
<input
type="datetime-local"
class="form-control form-control-sm"
id="end"
placeholder="{{ 'endDate' | i18n }}"
[(ngModel)]="end"
placeholder="YYYY-MM-DDTHH:MM"
(change)="dirtyDates = true"
/>
</div>
<form #refreshForm [appApiAction]="refreshPromise" class="d-inline">
<button
type="button"
class="btn btn-sm btn-outline-primary ml-3"
(click)="loadEvents(true)"
[disabled]="loaded && refreshForm.loading"
>
<i
class="bwi bwi-refresh bwi-fw"
aria-hidden="true"
[ngClass]="{ 'bwi-spin': loaded && refreshForm.loading }"
></i>
{{ "refresh" | i18n }}
</button>
</form>
<form #exportForm [appApiAction]="exportPromise" class="d-inline">
<button
type="button"
class="btn btn-sm btn-outline-primary btn-submit manual ml-3"
[ngClass]="{ loading: exportForm.loading }"
(click)="exportEvents()"
[disabled]="(loaded && exportForm.loading) || dirtyDates"
>
<i class="bwi bwi-spinner bwi-spin" aria-hidden="true"></i>
<span>{{ "export" | i18n }}</span>
</button>
</form>
</div>
</div>
<ng-container *ngIf="!loaded">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container *ngIf="loaded">
<p *ngIf="!events || !events.length">{{'noEventsInList' | i18n}}</p>
<table class="table table-hover" *ngIf="events && events.length">
<thead>
<tr>
<th class="border-top-0" width="210">{{'timestamp' | i18n}}</th>
<th class="border-top-0" width="40">
<span class="sr-only">{{'device' | i18n}}</span>
</th>
<th class="border-top-0" width="150">{{'user' | i18n}}</th>
<th class="border-top-0">{{'event' | i18n}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let e of events">
<td>{{e.date | date:'medium'}}</td>
<td>
<i class="text-muted fa fa-lg {{e.appIcon}}" title="{{e.appName}}, {{e.ip}}" aria-hidden="true"></i>
<span class="sr-only">{{e.appName}}, {{e.ip}}</span>
</td>
<td>
<span title="{{e.userEmail}}">{{e.userName}}</span>
</td>
<td [innerHTML]="e.message"></td>
</tr>
</tbody>
</table>
<button #moreBtn [appApiAction]="morePromise" type="button" class="btn btn-block btn-link btn-submit"
(click)="loadEvents(false)" [disabled]="loaded && moreBtn.loading" *ngIf="continuationToken">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'loadMore' | i18n}}</span>
</button>
<p *ngIf="!events || !events.length">{{ "noEventsInList" | i18n }}</p>
<table class="table table-hover" *ngIf="events && events.length">
<thead>
<tr>
<th class="border-top-0" width="210">{{ "timestamp" | i18n }}</th>
<th class="border-top-0" width="40">
<span class="sr-only">{{ "device" | i18n }}</span>
</th>
<th class="border-top-0" width="150">{{ "user" | i18n }}</th>
<th class="border-top-0">{{ "event" | i18n }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let e of events">
<td>{{ e.date | date: "medium" }}</td>
<td>
<i
class="text-muted bwi bwi-lg {{ e.appIcon }}"
title="{{ e.appName }}, {{ e.ip }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ e.appName }}, {{ e.ip }}</span>
</td>
<td>
<span title="{{ e.userEmail }}">{{ e.userName }}</span>
</td>
<td [innerHTML]="e.message"></td>
</tr>
</tbody>
</table>
<button
#moreBtn
[appApiAction]="morePromise"
type="button"
class="btn btn-block btn-link btn-submit"
(click)="loadEvents(false)"
[disabled]="loaded && moreBtn.loading"
*ngIf="continuationToken"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "loadMore" | i18n }}</span>
</button>
</ng-container>

View File

@@ -1,71 +1,79 @@
import {
Component,
OnInit,
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { ExportService } from 'jslib-common/abstractions/export.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { UserNamePipe } from "jslib-angular/pipes/user-name.pipe";
import { ApiService } from "jslib-common/abstractions/api.service";
import { ExportService } from "jslib-common/abstractions/export.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { ProviderService } from "jslib-common/abstractions/provider.service";
import { EventResponse } from "jslib-common/models/response/eventResponse";
import { UserNamePipe } from 'jslib-angular/pipes/user-name.pipe';
import { EventResponse } from 'jslib-common/models/response/eventResponse';
import { EventService } from 'src/app/services/event.service';
import { BaseEventsComponent } from 'src/app/common/base.events.component';
import { BaseEventsComponent } from "src/app/common/base.events.component";
import { EventService } from "src/app/services/event.service";
@Component({
selector: 'provider-events',
templateUrl: 'events.component.html',
selector: "provider-events",
templateUrl: "events.component.html",
})
export class EventsComponent extends BaseEventsComponent implements OnInit {
exportFileName: string = 'provider-events';
providerId: string;
exportFileName = "provider-events";
providerId: string;
private providerUsersUserIdMap = new Map<string, any>();
private providerUsersIdMap = new Map<string, any>();
private providerUsersUserIdMap = new Map<string, any>();
private providerUsersIdMap = new Map<string, any>();
constructor(private apiService: ApiService, private route: ActivatedRoute, eventService: EventService,
i18nService: I18nService, toasterService: ToasterService, private userService: UserService,
exportService: ExportService, platformUtilsService: PlatformUtilsService, private router: Router,
logService: LogService, private userNamePipe: UserNamePipe) {
super(eventService, i18nService, toasterService, exportService, platformUtilsService, logService);
}
constructor(
private apiService: ApiService,
private route: ActivatedRoute,
eventService: EventService,
i18nService: I18nService,
private providerService: ProviderService,
exportService: ExportService,
platformUtilsService: PlatformUtilsService,
private router: Router,
logService: LogService,
private userNamePipe: UserNamePipe
) {
super(eventService, i18nService, exportService, platformUtilsService, logService);
}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async params => {
this.providerId = params.providerId;
const provider = await this.userService.getProvider(this.providerId);
if (provider == null || !provider.useEvents) {
this.router.navigate(['/providers', this.providerId]);
return;
}
await this.load();
});
}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.providerId = params.providerId;
const provider = await this.providerService.get(this.providerId);
if (provider == null || !provider.useEvents) {
this.router.navigate(["/providers", this.providerId]);
return;
}
await this.load();
});
}
async load() {
const response = await this.apiService.getProviderUsers(this.providerId);
response.data.forEach(u => {
const name = this.userNamePipe.transform(u);
this.providerUsersIdMap.set(u.id, { name: name, email: u.email });
this.providerUsersUserIdMap.set(u.userId, { name: name, email: u.email });
});
await this.loadEvents(true);
this.loaded = true;
}
async load() {
const response = await this.apiService.getProviderUsers(this.providerId);
response.data.forEach((u) => {
const name = this.userNamePipe.transform(u);
this.providerUsersIdMap.set(u.id, { name: name, email: u.email });
this.providerUsersUserIdMap.set(u.userId, { name: name, email: u.email });
});
await this.loadEvents(true);
this.loaded = true;
}
protected requestEvents(startDate: string, endDate: string, continuationToken: string) {
return this.apiService.getEventsProvider(this.providerId, startDate, endDate, continuationToken);
}
protected requestEvents(startDate: string, endDate: string, continuationToken: string) {
return this.apiService.getEventsProvider(
this.providerId,
startDate,
endDate,
continuationToken
);
}
protected getUserName(r: EventResponse, userId: string) {
return userId != null && this.providerUsersUserIdMap.has(userId) ? this.providerUsersUserIdMap.get(userId) : null;
}
protected getUserName(r: EventResponse, userId: string) {
return userId != null && this.providerUsersUserIdMap.has(userId)
? this.providerUsersUserIdMap.get(userId)
: null;
}
}

View File

@@ -1,22 +1,30 @@
<div class="container page-content">
<div class="row">
<div class="col-3">
<div class="card" *ngIf="provider">
<div class="card-header">{{'manage' | i18n}}</div>
<div class="list-group list-group-flush">
<a routerLink="people" class="list-group-item" routerLinkActive="active"
*ngIf="provider.canManageUsers">
{{'people' | i18n}}
</a>
<a routerLink="events" class="list-group-item" routerLinkActive="active"
*ngIf="provider.canAccessEventLogs && accessEvents">
{{'eventLogs' | i18n}}
</a>
</div>
</div>
</div>
<div class="col-9">
<router-outlet></router-outlet>
<div class="row">
<div class="col-3">
<div class="card" *ngIf="provider">
<div class="card-header">{{ "manage" | i18n }}</div>
<div class="list-group list-group-flush">
<a
routerLink="people"
class="list-group-item"
routerLinkActive="active"
*ngIf="provider.canManageUsers"
>
{{ "people" | i18n }}
</a>
<a
routerLink="events"
class="list-group-item"
routerLinkActive="active"
*ngIf="provider.canAccessEventLogs && accessEvents"
>
{{ "eventLogs" | i18n }}
</a>
</div>
</div>
</div>
<div class="col-9">
<router-outlet></router-outlet>
</div>
</div>
</div>

View File

@@ -1,27 +1,23 @@
import {
Component,
OnInit,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { UserService } from 'jslib-common/abstractions/user.service';
import { Provider } from 'jslib-common/models/domain/provider';
import { ProviderService } from "jslib-common/abstractions/provider.service";
import { Provider } from "jslib-common/models/domain/provider";
@Component({
selector: 'provider-manage',
templateUrl: 'manage.component.html',
selector: "provider-manage",
templateUrl: "manage.component.html",
})
export class ManageComponent implements OnInit {
provider: Provider;
accessEvents = false;
provider: Provider;
accessEvents = false;
constructor(private route: ActivatedRoute, private userService: UserService) { }
constructor(private route: ActivatedRoute, private providerService: ProviderService) {}
ngOnInit() {
this.route.parent.params.subscribe(async params => {
this.provider = await this.userService.getProvider(params.providerId);
this.accessEvents = this.provider.useEvents;
});
}
ngOnInit() {
this.route.parent.params.subscribe(async (params) => {
this.provider = await this.providerService.get(params.providerId);
this.accessEvents = this.provider.useEvents;
});
}
}

View File

@@ -1,145 +1,229 @@
<div class="page-header d-flex">
<h1>{{'people' | i18n}}</h1>
<div class="ml-auto d-flex">
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary" [ngClass]="{active: status == null}"
(click)="filter(null)">
{{'all' | i18n}}
<span class="badge badge-pill badge-info" *ngIf="allCount">{{allCount}}</span>
</button>
<button type="button" class="btn btn-outline-secondary"
[ngClass]="{active: status == userStatusType.Invited}"
(click)="filter(userStatusType.Invited)">
{{'invited' | i18n}}
<span class="badge badge-pill badge-info" *ngIf="invitedCount">{{invitedCount}}</span>
</button>
<button type="button" class="btn btn-outline-secondary"
[ngClass]="{active: status == userStatusType.Accepted}"
(click)="filter(userStatusType.Accepted)">
{{'accepted' | i18n}}
<span class="badge badge-pill badge-warning" *ngIf="acceptedCount">{{acceptedCount}}</span>
</button>
</div>
<div class="ml-3">
<label class="sr-only" for="search">{{'search' | i18n}}</label>
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
[(ngModel)]="searchText">
</div>
<div class="dropdown ml-3" appListDropdown>
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" id="bulkActionsButton"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" appA11yTitle="{{'options' | i18n}}">
<i class="fa fa-cog" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="bulkActionsButton">
<button class="dropdown-item" appStopClick (click)="bulkReinvite()">
<i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
{{'reinviteSelected' | i18n}}
</button>
<button class="dropdown-item text-success" appStopClick (click)="bulkConfirm()"
*ngIf="showBulkConfirmUsers">
<i class="fa fa-fw fa-check" aria-hidden="true"></i>
{{'confirmSelected' | i18n}}
</button>
<button class="dropdown-item text-danger" appStopClick (click)="bulkRemove()">
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
{{'remove' | i18n}}
</button>
<div class="dropdown-divider"></div>
<button class="dropdown-item" appStopClick (click)="selectAll(true)">
<i class="fa fa-fw fa-check-square-o" aria-hidden="true"></i>
{{'selectAll' | i18n}}
</button>
<button class="dropdown-item" appStopClick (click)="selectAll(false)">
<i class="fa fa-fw fa-minus-square-o" aria-hidden="true"></i>
{{'unselectAll' | i18n}}
</button>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="invite()">
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
{{'inviteUser' | i18n}}
</button>
<h1>{{ "people" | i18n }}</h1>
<div class="ml-auto d-flex">
<div class="btn-group btn-group-sm" role="group">
<button
type="button"
class="btn btn-outline-secondary"
[ngClass]="{ active: status == null }"
(click)="filter(null)"
>
{{ "all" | i18n }}
<span class="badge badge-pill badge-info" *ngIf="allCount">{{ allCount }}</span>
</button>
<button
type="button"
class="btn btn-outline-secondary"
[ngClass]="{ active: status == userStatusType.Invited }"
(click)="filter(userStatusType.Invited)"
>
{{ "invited" | i18n }}
<span class="badge badge-pill badge-info" *ngIf="invitedCount">{{ invitedCount }}</span>
</button>
<button
type="button"
class="btn btn-outline-secondary"
[ngClass]="{ active: status == userStatusType.Accepted }"
(click)="filter(userStatusType.Accepted)"
>
{{ "accepted" | i18n }}
<span class="badge badge-pill badge-warning" *ngIf="acceptedCount">{{
acceptedCount
}}</span>
</button>
</div>
<div class="ml-3">
<label class="sr-only" for="search">{{ "search" | i18n }}</label>
<input
type="search"
class="form-control form-control-sm"
id="search"
placeholder="{{ 'search' | i18n }}"
[(ngModel)]="searchText"
/>
</div>
<div class="dropdown ml-3" appListDropdown>
<button
class="btn btn-sm btn-outline-secondary dropdown-toggle"
type="button"
id="bulkActionsButton"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="bwi bwi-cog" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="bulkActionsButton">
<button class="dropdown-item" appStopClick (click)="bulkReinvite()">
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
{{ "reinviteSelected" | i18n }}
</button>
<button
class="dropdown-item text-success"
appStopClick
(click)="bulkConfirm()"
*ngIf="showBulkConfirmUsers"
>
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
{{ "confirmSelected" | i18n }}
</button>
<button class="dropdown-item text-danger" appStopClick (click)="bulkRemove()">
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
{{ "remove" | i18n }}
</button>
<div class="dropdown-divider"></div>
<button class="dropdown-item" appStopClick (click)="selectAll(true)">
<i class="bwi bwi-fw bwi-check-square" aria-hidden="true"></i>
{{ "selectAll" | i18n }}
</button>
<button class="dropdown-item" appStopClick (click)="selectAll(false)">
<i class="bwi bwi-fw bwi-minus-square" aria-hidden="true"></i>
{{ "unselectAll" | i18n }}
</button>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="invite()">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "inviteUser" | i18n }}
</button>
</div>
</div>
<ng-container *ngIf="loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container
*ngIf="!loading && (isPaging() ? pagedUsers : users | search:searchText:'name':'email':'id') as searchedUsers">
<p *ngIf="!searchedUsers.length">{{'noUsersInList' | i18n}}</p>
<ng-container *ngIf="searchedUsers.length">
<app-callout type="info" title="{{'confirmUsers' | i18n}}" icon="fa-check-circle" *ngIf="showConfirmUsers">
{{'providerUsersNeedConfirmed' | i18n}}
</app-callout>
<table class="table table-hover table-list" infiniteScroll [infiniteScrollDistance]="1"
[infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
<tbody>
<tr *ngFor="let u of searchedUsers">
<td (click)="checkUser(u)" class="table-list-checkbox">
<input type="checkbox" [(ngModel)]="u.checked" appStopProp>
</td>
<td width="30">
<app-avatar [data]="u | userName" [email]="u.email" size="25" [circle]="true"
[fontSize]="14"></app-avatar>
</td>
<td>
<a href="#" appStopClick (click)="edit(u)">{{u.email}}</a>
<span class="badge badge-secondary"
*ngIf="u.status === userStatusType.Invited">{{'invited' | i18n}}</span>
<span class="badge badge-warning"
*ngIf="u.status === userStatusType.Accepted">{{'accepted' | i18n}}</span>
<small class="text-muted d-block" *ngIf="u.name">{{u.name}}</small>
</td>
<td>
<ng-container *ngIf="u.twoFactorEnabled">
<i class="fa fa-lock" title="{{'userUsingTwoStep' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'userUsingTwoStep' | i18n}}</span>
</ng-container>
</td>
<td>
<span *ngIf="u.type === userType.ProviderAdmin">{{'providerAdmin' | i18n}}</span>
<span *ngIf="u.type === userType.ServiceUser">{{'serviceUser' | i18n}}</span>
<span *ngIf="u.type === userType.Custom">{{'custom' | i18n}}</span>
</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown>
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
appA11yTitle="{{'options' | i18n}}">
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="#" appStopClick (click)="reinvite(u)"
*ngIf="u.status === userStatusType.Invited">
<i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
{{'resendInvitation' | i18n}}
</a>
<a class="dropdown-item text-success" href="#" appStopClick (click)="confirm(u)"
*ngIf="u.status === userStatusType.Accepted">
<i class="fa fa-fw fa-check" aria-hidden="true"></i>
{{'confirm' | i18n}}
</a>
<a class="dropdown-item" href="#" appStopClick (click)="groups(u)" *ngIf="accessGroups">
<i class="fa fa-fw fa-sitemap" aria-hidden="true"></i>
{{'groups' | i18n}}
</a>
<a class="dropdown-item" href="#" appStopClick (click)="events(u)"
*ngIf="accessEvents && u.status === userStatusType.Confirmed">
<i class="fa fa-fw fa-file-text-o" aria-hidden="true"></i>
{{'eventLogs' | i18n}}
</a>
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(u)">
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
{{'remove' | i18n}}
</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</ng-container>
*ngIf="
!loading &&
(isPaging() ? pagedUsers : (users | search: searchText:'name':'email':'id')) as searchedUsers
"
>
<p *ngIf="!searchedUsers.length">{{ "noUsersInList" | i18n }}</p>
<ng-container *ngIf="searchedUsers.length">
<app-callout
type="info"
title="{{ 'confirmUsers' | i18n }}"
icon="bwi bwi-check-circle"
*ngIf="showConfirmUsers"
>
{{ "providerUsersNeedConfirmed" | i18n }}
</app-callout>
<table
class="table table-hover table-list"
infiniteScroll
[infiniteScrollDistance]="1"
[infiniteScrollDisabled]="!isPaging()"
(scrolled)="loadMore()"
>
<tbody>
<tr *ngFor="let u of searchedUsers">
<td (click)="checkUser(u)" class="table-list-checkbox">
<input type="checkbox" [(ngModel)]="u.checked" appStopProp />
</td>
<td width="30">
<app-avatar
[data]="u | userName"
[email]="u.email"
size="25"
[circle]="true"
[fontSize]="14"
>
</app-avatar>
</td>
<td>
<a href="#" appStopClick (click)="edit(u)">{{ u.email }}</a>
<span class="badge badge-secondary" *ngIf="u.status === userStatusType.Invited">{{
"invited" | i18n
}}</span>
<span class="badge badge-warning" *ngIf="u.status === userStatusType.Accepted">{{
"accepted" | i18n
}}</span>
<small class="text-muted d-block" *ngIf="u.name">{{ u.name }}</small>
</td>
<td>
<ng-container *ngIf="u.twoFactorEnabled">
<i
class="bwi bwi-lock"
title="{{ 'userUsingTwoStep' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "userUsingTwoStep" | i18n }}</span>
</ng-container>
</td>
<td>
<span *ngIf="u.type === userType.ProviderAdmin">{{ "providerAdmin" | i18n }}</span>
<span *ngIf="u.type === userType.ServiceUser">{{ "serviceUser" | i18n }}</span>
<span *ngIf="u.type === userType.Custom">{{ "custom" | i18n }}</span>
</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown>
<button
class="btn btn-outline-secondary dropdown-toggle"
type="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a
class="dropdown-item"
href="#"
appStopClick
(click)="reinvite(u)"
*ngIf="u.status === userStatusType.Invited"
>
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
{{ "resendInvitation" | i18n }}
</a>
<a
class="dropdown-item text-success"
href="#"
appStopClick
(click)="confirm(u)"
*ngIf="u.status === userStatusType.Accepted"
>
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
{{ "confirm" | i18n }}
</a>
<a
class="dropdown-item"
href="#"
appStopClick
(click)="groups(u)"
*ngIf="accessGroups"
>
<i class="bwi bwi-fw bwi-sitemap" aria-hidden="true"></i>
{{ "groups" | i18n }}
</a>
<a
class="dropdown-item"
href="#"
appStopClick
(click)="events(u)"
*ngIf="accessEvents && u.status === userStatusType.Confirmed"
>
<i class="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>
{{ "eventLogs" | i18n }}
</a>
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(u)">
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
{{ "remove" | i18n }}
</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</ng-container>
</ng-container>
<ng-template #addEdit></ng-template>
<ng-template #eventsTemplate></ng-template>

View File

@@ -1,286 +1,284 @@
import {
Component,
ComponentFactoryResolver,
OnInit,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
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 { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { SearchService } from 'jslib-common/abstractions/search.service';
import { StorageService } from 'jslib-common/abstractions/storage.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { SearchPipe } from "jslib-angular/pipes/search.pipe";
import { UserNamePipe } from "jslib-angular/pipes/user-name.pipe";
import { ModalService } from "jslib-angular/services/modal.service";
import { ValidationService } from "jslib-angular/services/validation.service";
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 { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { ProviderService } from "jslib-common/abstractions/provider.service";
import { SearchService } from "jslib-common/abstractions/search.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { ProviderUserStatusType } from "jslib-common/enums/providerUserStatusType";
import { ProviderUserType } from "jslib-common/enums/providerUserType";
import { ProviderUserBulkRequest } from "jslib-common/models/request/provider/providerUserBulkRequest";
import { ProviderUserConfirmRequest } from "jslib-common/models/request/provider/providerUserConfirmRequest";
import { ListResponse } from "jslib-common/models/response/listResponse";
import { ProviderUserBulkResponse } from "jslib-common/models/response/provider/providerUserBulkResponse";
import { ProviderUserUserDetailsResponse } from "jslib-common/models/response/provider/providerUserResponse";
import { ValidationService } from 'jslib-angular/services/validation.service';
import { BasePeopleComponent } from "src/app/common/base.people.component";
import { BulkStatusComponent } from "src/app/organizations/manage/bulk/bulk-status.component";
import { EntityEventsComponent } from "src/app/organizations/manage/entity-events.component";
import { ProviderUserStatusType } from 'jslib-common/enums/providerUserStatusType';
import { ProviderUserType } from 'jslib-common/enums/providerUserType';
import { SearchPipe } from 'jslib-angular/pipes/search.pipe';
import { UserNamePipe } from 'jslib-angular/pipes/user-name.pipe';
import { ListResponse } from 'jslib-common/models/response/listResponse';
import { ProviderUserUserDetailsResponse } from 'jslib-common/models/response/provider/providerUserResponse';
import { ProviderUserBulkRequest } from 'jslib-common/models/request/provider/providerUserBulkRequest';
import { ProviderUserConfirmRequest } from 'jslib-common/models/request/provider/providerUserConfirmRequest';
import { ProviderUserBulkResponse } from 'jslib-common/models/response/provider/providerUserBulkResponse';
import { BasePeopleComponent } from 'src/app/common/base.people.component';
import { ModalComponent } from 'src/app/modal.component';
import { BulkStatusComponent } from 'src/app/organizations/manage/bulk/bulk-status.component';
import { EntityEventsComponent } from 'src/app/organizations/manage/entity-events.component';
import { BulkConfirmComponent } from './bulk/bulk-confirm.component';
import { BulkRemoveComponent } from './bulk/bulk-remove.component';
import { UserAddEditComponent } from './user-add-edit.component';
import { BulkConfirmComponent } from "./bulk/bulk-confirm.component";
import { BulkRemoveComponent } from "./bulk/bulk-remove.component";
import { UserAddEditComponent } from "./user-add-edit.component";
@Component({
selector: 'provider-people',
templateUrl: 'people.component.html',
selector: "provider-people",
templateUrl: "people.component.html",
})
export class PeopleComponent extends BasePeopleComponent<ProviderUserUserDetailsResponse> implements OnInit {
export class PeopleComponent
extends BasePeopleComponent<ProviderUserUserDetailsResponse>
implements OnInit
{
@ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
groupsModalRef: ViewContainerRef;
@ViewChild("eventsTemplate", { read: ViewContainerRef, static: true })
eventsModalRef: ViewContainerRef;
@ViewChild("bulkStatusTemplate", { read: ViewContainerRef, static: true })
bulkStatusModalRef: ViewContainerRef;
@ViewChild("bulkConfirmTemplate", { read: ViewContainerRef, static: true })
bulkConfirmModalRef: ViewContainerRef;
@ViewChild("bulkRemoveTemplate", { read: ViewContainerRef, static: true })
bulkRemoveModalRef: ViewContainerRef;
@ViewChild('addEdit', { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
@ViewChild('groupsTemplate', { read: ViewContainerRef, static: true }) groupsModalRef: ViewContainerRef;
@ViewChild('eventsTemplate', { read: ViewContainerRef, static: true }) eventsModalRef: ViewContainerRef;
@ViewChild('bulkStatusTemplate', { read: ViewContainerRef, static: true }) bulkStatusModalRef: ViewContainerRef;
@ViewChild('bulkConfirmTemplate', { read: ViewContainerRef, static: true }) bulkConfirmModalRef: ViewContainerRef;
@ViewChild('bulkRemoveTemplate', { read: ViewContainerRef, static: true }) bulkRemoveModalRef: ViewContainerRef;
userType = ProviderUserType;
userStatusType = ProviderUserStatusType;
providerId: string;
accessEvents = false;
userType = ProviderUserType;
userStatusType = ProviderUserStatusType;
providerId: string;
accessEvents = false;
constructor(
apiService: ApiService,
private route: ActivatedRoute,
i18nService: I18nService,
modalService: ModalService,
platformUtilsService: PlatformUtilsService,
cryptoService: CryptoService,
private router: Router,
searchService: SearchService,
validationService: ValidationService,
logService: LogService,
searchPipe: SearchPipe,
userNamePipe: UserNamePipe,
stateService: StateService,
private providerService: ProviderService
) {
super(
apiService,
searchService,
i18nService,
platformUtilsService,
cryptoService,
validationService,
modalService,
logService,
searchPipe,
userNamePipe,
stateService
);
}
constructor(apiService: ApiService, private route: ActivatedRoute,
i18nService: I18nService, componentFactoryResolver: ComponentFactoryResolver,
platformUtilsService: PlatformUtilsService, toasterService: ToasterService,
cryptoService: CryptoService, private userService: UserService, private router: Router,
storageService: StorageService, searchService: SearchService, validationService: ValidationService,
logService: LogService, searchPipe: SearchPipe, userNamePipe: UserNamePipe) {
super(apiService, searchService, i18nService, platformUtilsService, toasterService, cryptoService,
storageService, validationService, componentFactoryResolver, logService, searchPipe, userNamePipe);
}
ngOnInit() {
this.route.parent.params.subscribe(async (params) => {
this.providerId = params.providerId;
const provider = await this.providerService.get(this.providerId);
ngOnInit() {
this.route.parent.params.subscribe(async params => {
this.providerId = params.providerId;
const provider = await this.userService.getProvider(this.providerId);
if (!provider.canManageUsers) {
this.router.navigate(["../"], { relativeTo: this.route });
return;
}
if (!provider.canManageUsers) {
this.router.navigate(['../'], { relativeTo: this.route });
return;
}
this.accessEvents = provider.useEvents;
this.accessEvents = provider.useEvents;
await this.load();
await this.load();
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
this.searchText = qParams.search;
if (qParams.viewEvents != null) {
const user = this.users.filter((u) => u.id === qParams.viewEvents);
if (user.length > 0 && user[0].status === ProviderUserStatusType.Confirmed) {
this.events(user[0]);
}
}
});
});
}
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
this.searchText = qParams.search;
if (qParams.viewEvents != null) {
const user = this.users.filter(u => u.id === qParams.viewEvents);
if (user.length > 0 && user[0].status === ProviderUserStatusType.Confirmed) {
this.events(user[0]);
}
}
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();
}
});
getUsers(): Promise<ListResponse<ProviderUserUserDetailsResponse>> {
return this.apiService.getProviderUsers(this.providerId);
}
deleteUser(id: string): Promise<any> {
return this.apiService.deleteProviderUser(this.providerId, id);
}
reinviteUser(id: string): Promise<any> {
return this.apiService.postProviderUserReinvite(this.providerId, id);
}
async confirmUser(user: ProviderUserUserDetailsResponse, publicKey: Uint8Array): Promise<any> {
const providerKey = await this.cryptoService.getProviderKey(this.providerId);
const key = await this.cryptoService.rsaEncrypt(providerKey.key, publicKey.buffer);
const request = new ProviderUserConfirmRequest();
request.key = key.encryptedString;
await this.apiService.postProviderUserConfirm(this.providerId, user.id, request);
}
async edit(user: ProviderUserUserDetailsResponse) {
const [modal] = await this.modalService.openViewRef(
UserAddEditComponent,
this.addEditModalRef,
(comp) => {
comp.name = this.userNamePipe.transform(user);
comp.providerId = this.providerId;
comp.providerUserId = user != null ? user.id : null;
comp.onSavedUser.subscribe(() => {
modal.close();
this.load();
});
}
getUsers(): Promise<ListResponse<ProviderUserUserDetailsResponse>> {
return this.apiService.getProviderUsers(this.providerId);
}
deleteUser(id: string): Promise<any> {
return this.apiService.deleteProviderUser(this.providerId, id);
}
reinviteUser(id: string): Promise<any> {
return this.apiService.postProviderUserReinvite(this.providerId, id);
}
async confirmUser(user: ProviderUserUserDetailsResponse, publicKey: Uint8Array): Promise<any> {
const providerKey = await this.cryptoService.getProviderKey(this.providerId);
const key = await this.cryptoService.rsaEncrypt(providerKey.key, publicKey.buffer);
const request = new ProviderUserConfirmRequest();
request.key = key.encryptedString;
await this.apiService.postProviderUserConfirm(this.providerId, user.id, request);
}
edit(user: ProviderUserUserDetailsResponse) {
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.addEditModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<UserAddEditComponent>(
UserAddEditComponent, this.addEditModalRef);
childComponent.name = this.userNamePipe.transform(user);
childComponent.providerId = this.providerId;
childComponent.providerUserId = user != null ? user.id : null;
childComponent.onSavedUser.subscribe(() => {
this.modal.close();
this.load();
});
childComponent.onDeletedUser.subscribe(() => {
this.modal.close();
this.removeUser(user);
comp.onDeletedUser.subscribe(() => {
modal.close();
this.removeUser(user);
});
}
);
}
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
async events(user: ProviderUserUserDetailsResponse) {
await this.modalService.openViewRef(EntityEventsComponent, this.eventsModalRef, (comp) => {
comp.name = this.userNamePipe.transform(user);
comp.providerId = this.providerId;
comp.entityId = user.id;
comp.showUser = false;
comp.entity = "user";
});
}
async bulkRemove() {
if (this.actionPromise != null) {
return;
}
async events(user: ProviderUserUserDetailsResponse) {
if (this.modal != null) {
this.modal.close();
}
const [modal] = await this.modalService.openViewRef(
BulkRemoveComponent,
this.bulkRemoveModalRef,
(comp) => {
comp.providerId = this.providerId;
comp.users = this.getCheckedUsers();
}
);
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.eventsModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<EntityEventsComponent>(
EntityEventsComponent, this.eventsModalRef);
await modal.onClosedPromise();
await this.load();
}
childComponent.name = this.userNamePipe.transform(user);
childComponent.providerId = this.providerId;
childComponent.entityId = user.id;
childComponent.showUser = false;
childComponent.entity = 'user';
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
async bulkReinvite() {
if (this.actionPromise != null) {
return;
}
async bulkRemove() {
if (this.actionPromise != null) {
return;
}
const users = this.getCheckedUsers();
const filteredUsers = users.filter((u) => u.status === ProviderUserStatusType.Invited);
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.bulkRemoveModalRef.createComponent(factory).instance;
const childComponent = this.modal.show(BulkRemoveComponent, this.bulkRemoveModalRef);
childComponent.providerId = this.providerId;
childComponent.users = this.getCheckedUsers();
this.modal.onClosed.subscribe(async () => {
await this.load();
this.modal = null;
});
if (filteredUsers.length <= 0) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("noSelectedUsersApplicable")
);
return;
}
async bulkReinvite() {
if (this.actionPromise != null) {
return;
}
try {
const request = new ProviderUserBulkRequest(filteredUsers.map((user) => user.id));
const response = this.apiService.postManyProviderUserReinvite(this.providerId, request);
this.showBulkStatus(
users,
filteredUsers,
response,
this.i18nService.t("bulkReinviteMessage")
);
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
const users = this.getCheckedUsers();
const filteredUsers = users.filter(u => u.status === ProviderUserStatusType.Invited);
if (filteredUsers.length <= 0) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('noSelectedUsersApplicable'));
return;
}
try {
const request = new ProviderUserBulkRequest(filteredUsers.map(user => user.id));
const response = this.apiService.postManyProviderUserReinvite(this.providerId, request);
this.showBulkStatus(users, filteredUsers, response, this.i18nService.t('bulkReinviteMessage'));
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
async bulkConfirm() {
if (this.actionPromise != null) {
return;
}
async bulkConfirm() {
if (this.actionPromise != null) {
return;
}
const [modal] = await this.modalService.openViewRef(
BulkConfirmComponent,
this.bulkConfirmModalRef,
(comp) => {
comp.providerId = this.providerId;
comp.users = this.getCheckedUsers();
}
);
if (this.modal != null) {
this.modal.close();
}
await modal.onClosedPromise();
await this.load();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.bulkConfirmModalRef.createComponent(factory).instance;
const childComponent = this.modal.show(BulkConfirmComponent, this.bulkConfirmModalRef);
private async showBulkStatus(
users: ProviderUserUserDetailsResponse[],
filteredUsers: ProviderUserUserDetailsResponse[],
request: Promise<ListResponse<ProviderUserBulkResponse>>,
successfullMessage: string
) {
const [modal, childComponent] = await this.modalService.openViewRef(
BulkStatusComponent,
this.bulkStatusModalRef,
(comp) => {
comp.loading = true;
}
);
childComponent.providerId = this.providerId;
childComponent.users = this.getCheckedUsers();
// Workaround to handle closing the modal shortly after it has been opened
let close = false;
modal.onShown.subscribe(() => {
if (close) {
modal.close();
}
});
this.modal.onClosed.subscribe(async () => {
await this.load();
this.modal = null;
try {
const response = await request;
if (modal) {
const keyedErrors: any = response.data
.filter((r) => r.error !== "")
.reduce((a, x) => ({ ...a, [x.id]: x.error }), {});
const keyedFilteredUsers: any = filteredUsers.reduce((a, x) => ({ ...a, [x.id]: x }), {});
childComponent.users = users.map((user) => {
let message = keyedErrors[user.id] ?? successfullMessage;
// eslint-disable-next-line
if (!keyedFilteredUsers.hasOwnProperty(user.id)) {
message = this.i18nService.t("bulkFilteredMessage");
}
return {
user: user,
error: keyedErrors.hasOwnProperty(user.id), // eslint-disable-line
message: message,
};
});
childComponent.loading = false;
}
} catch {
close = true;
modal.close();
}
private async showBulkStatus(users: ProviderUserUserDetailsResponse[], filteredUsers: ProviderUserUserDetailsResponse[],
request: Promise<ListResponse<ProviderUserBulkResponse>>, successfullMessage: string) {
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.bulkStatusModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<BulkStatusComponent>(
BulkStatusComponent, this.bulkStatusModalRef);
childComponent.loading = true;
// Workaround to handle closing the modal shortly after it has been opened
let close = false;
this.modal.onShown.subscribe(() => {
if (close) {
this.modal.close();
}
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
try {
const response = await request;
if (this.modal) {
const keyedErrors: any = response.data.filter(r => r.error !== '').reduce((a, x) => ({ ...a, [x.id]: x.error }), {});
const keyedFilteredUsers: any = filteredUsers.reduce((a, x) => ({ ...a, [x.id]: x }), {});
childComponent.users = users.map(user => {
let message = keyedErrors[user.id] ?? successfullMessage;
if (!keyedFilteredUsers.hasOwnProperty(user.id)) {
message = this.i18nService.t('bulkFilteredMessage');
}
return {
user: user,
error: keyedErrors.hasOwnProperty(user.id),
message: message,
};
});
childComponent.loading = false;
}
} catch {
close = true;
if (this.modal) {
this.modal.close();
}
}
}
}
}

View File

@@ -1,71 +1,124 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title" id="userAddEditTitle">
{{title}}
<small class="text-muted" *ngIf="name">{{name}}</small>
</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" *ngIf="loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</div>
<div class="modal-body" *ngIf="!loading">
<ng-container *ngIf="!editMode">
<p>{{'providerInviteUserDesc' | i18n}}</p>
<div class="form-group mb-4">
<label for="emails">{{'email' | i18n}}</label>
<input id="emails" class="form-control" type="text" name="Emails" [(ngModel)]="emails" required
appAutoFocus>
<small class="text-muted">{{'inviteMultipleEmailDesc' | i18n : '20'}}</small>
</div>
</ng-container>
<h3>
{{'userType' | i18n}}
<a target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}"
href="https://bitwarden.com/help/article/user-types-access-control/#user-types">
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</h3>
<div class="form-check mt-2 form-check-block">
<input class="form-check-input" type="radio" name="userType" id="userTypeServiceUser"
[value]="userType.ServiceUser" [(ngModel)]="type">
<label class="form-check-label" for="userTypeServiceUser">
{{'serviceUser' | i18n}}
<small>{{'serviceUserDesc' | i18n}}</small>
</label>
</div>
<div class="form-check mt-2 form-check-block">
<input class="form-check-input" type="radio" name="userType" id="userTypeProviderAdmin"
[value]="userType.ProviderAdmin" [(ngModel)]="type">
<label class="form-check-label" for="userTypeProviderAdmin">
{{'providerAdmin' | i18n}}
<small>{{'providerAdminDesc' | i18n}}</small>
</label>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-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)="delete()" class="btn btn-outline-danger"
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"
[appApiAction]="deletePromise">
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading" aria-hidden="true"></i>
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"
title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
</div>
</div>
</form>
</div>
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="modal-header">
<h2 class="modal-title" id="userAddEditTitle">
{{ title }}
<small class="text-muted" *ngIf="name">{{ name }}</small>
</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="modal-body" *ngIf="!loading">
<ng-container *ngIf="!editMode">
<p>{{ "providerInviteUserDesc" | i18n }}</p>
<div class="form-group mb-4">
<label for="emails">{{ "email" | i18n }}</label>
<input
id="emails"
class="form-control"
type="text"
name="Emails"
[(ngModel)]="emails"
required
appAutoFocus
/>
<small class="text-muted">{{ "inviteMultipleEmailDesc" | i18n: "20" }}</small>
</div>
</ng-container>
<h3>
{{ "userType" | i18n }}
<a
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
href="https://bitwarden.com/help/provider-users/"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
</h3>
<div class="form-check mt-2 form-check-block">
<input
class="form-check-input"
type="radio"
name="userType"
id="userTypeServiceUser"
[value]="userType.ServiceUser"
[(ngModel)]="type"
/>
<label class="form-check-label" for="userTypeServiceUser">
{{ "serviceUser" | i18n }}
<small>{{ "serviceUserDesc" | i18n }}</small>
</label>
</div>
<div class="form-check mt-2 form-check-block">
<input
class="form-check-input"
type="radio"
name="userType"
id="userTypeProviderAdmin"
[value]="userType.ProviderAdmin"
[(ngModel)]="type"
/>
<label class="form-check-label" for="userTypeProviderAdmin">
{{ "providerAdmin" | i18n }}
<small>{{ "providerAdminDesc" | i18n }}</small>
</label>
</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)="delete()"
class="btn btn-outline-danger"
appA11yTitle="{{ 'delete' | i18n }}"
*ngIf="editMode"
[disabled]="deleteBtn.loading"
[appApiAction]="deletePromise"
>
<i
class="bwi bwi-trash bwi-lg bwi-fw"
[hidden]="deleteBtn.loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!deleteBtn.loading"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
</div>
</div>
</form>
</div>
</div>

View File

@@ -1,104 +1,118 @@
import {
Component,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { ToasterService } from 'angular2-toaster';
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 { ProviderUserInviteRequest } from 'jslib-common/models/request/provider/providerUserInviteRequest';
import { PermissionsApi } from 'jslib-common/models/api/permissionsApi';
import { ProviderUserType } from 'jslib-common/enums/providerUserType';
import { ProviderUserUpdateRequest } from 'jslib-common/models/request/provider/providerUserUpdateRequest';
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 { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { ProviderUserType } from "jslib-common/enums/providerUserType";
import { PermissionsApi } from "jslib-common/models/api/permissionsApi";
import { ProviderUserInviteRequest } from "jslib-common/models/request/provider/providerUserInviteRequest";
import { ProviderUserUpdateRequest } from "jslib-common/models/request/provider/providerUserUpdateRequest";
@Component({
selector: 'provider-user-add-edit',
templateUrl: 'user-add-edit.component.html',
selector: "provider-user-add-edit",
templateUrl: "user-add-edit.component.html",
})
export class UserAddEditComponent implements OnInit {
@Input() name: string;
@Input() providerUserId: string;
@Input() providerId: string;
@Output() onSavedUser = new EventEmitter();
@Output() onDeletedUser = new EventEmitter();
@Input() name: string;
@Input() providerUserId: string;
@Input() providerId: string;
@Output() onSavedUser = new EventEmitter();
@Output() onDeletedUser = new EventEmitter();
loading = true;
editMode: boolean = false;
title: string;
emails: string;
type: ProviderUserType = ProviderUserType.ServiceUser;
permissions = new PermissionsApi();
showCustom = false;
access: 'all' | 'selected' = 'selected';
formPromise: Promise<any>;
deletePromise: Promise<any>;
userType = ProviderUserType;
loading = true;
editMode = false;
title: string;
emails: string;
type: ProviderUserType = ProviderUserType.ServiceUser;
permissions = new PermissionsApi();
showCustom = false;
access: "all" | "selected" = "selected";
formPromise: Promise<any>;
deletePromise: Promise<any>;
userType = ProviderUserType;
constructor(private apiService: ApiService, private i18nService: I18nService,
private toasterService: ToasterService, private platformUtilsService: PlatformUtilsService) { }
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private logService: LogService
) {}
async ngOnInit() {
this.editMode = this.loading = this.providerUserId != null;
async ngOnInit() {
this.editMode = this.loading = this.providerUserId != null;
if (this.editMode) {
this.editMode = true;
this.title = this.i18nService.t('editUser');
try {
const user = await this.apiService.getProviderUser(this.providerId, this.providerUserId);
this.type = user.type;
} catch { }
} else {
this.title = this.i18nService.t('inviteUser');
}
this.loading = false;
if (this.editMode) {
this.editMode = true;
this.title = this.i18nService.t("editUser");
try {
const user = await this.apiService.getProviderUser(this.providerId, this.providerUserId);
this.type = user.type;
} catch (e) {
this.logService.error(e);
}
} else {
this.title = this.i18nService.t("inviteUser");
}
async submit() {
try {
if (this.editMode) {
const request = new ProviderUserUpdateRequest();
request.type = this.type;
this.formPromise = this.apiService.putProviderUser(this.providerId, this.providerUserId, request);
} else {
const request = new ProviderUserInviteRequest();
request.emails = this.emails.trim().split(/\s*,\s*/);
request.type = this.type;
this.formPromise = this.apiService.postProviderUserInvite(this.providerId, request);
}
await this.formPromise;
this.toasterService.popAsync('success', null,
this.i18nService.t(this.editMode ? 'editedUserId' : 'invitedUsers', this.name));
this.onSavedUser.emit();
} catch { }
this.loading = false;
}
async submit() {
try {
if (this.editMode) {
const request = new ProviderUserUpdateRequest();
request.type = this.type;
this.formPromise = this.apiService.putProviderUser(
this.providerId,
this.providerUserId,
request
);
} else {
const request = new ProviderUserInviteRequest();
request.emails = this.emails.trim().split(/\s*,\s*/);
request.type = this.type;
this.formPromise = this.apiService.postProviderUserInvite(this.providerId, request);
}
await this.formPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.name)
);
this.onSavedUser.emit();
} catch (e) {
this.logService.error(e);
}
}
async delete() {
if (!this.editMode) {
return;
}
async delete() {
if (!this.editMode) {
return;
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('removeUserConfirmation'), this.name,
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
try {
this.deletePromise = this.apiService.deleteProviderUser(this.providerId, this.providerUserId);
await this.deletePromise;
this.toasterService.popAsync('success', null, this.i18nService.t('removedUserId', this.name));
this.onDeletedUser.emit();
} catch { }
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("removeUserConfirmation"),
this.name,
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return false;
}
try {
this.deletePromise = this.apiService.deleteProviderUser(this.providerId, this.providerUserId);
await this.deletePromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("removedUserId", this.name)
);
this.onDeletedUser.emit();
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -1,44 +1,44 @@
<app-navbar></app-navbar>
<div class="org-nav" *ngIf="provider">
<div class="container d-flex">
<div class="d-flex flex-column">
<div class="my-auto d-flex align-items-center pl-1">
<app-avatar [data]="provider.name" size="45" [circle]="true"></app-avatar>
<div class="org-name ml-3">
<span>{{provider.name}}</span>
<small class="text-muted">{{'provider' | i18n}}</small>
</div>
<div class="ml-3 card border-danger text-danger bg-transparent" *ngIf="!provider.enabled">
<div class="card-body py-2">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
{{'providerIsDisabled' | i18n}}
</div>
</div>
</div>
<ul class="nav nav-tabs" *ngIf="showMenuBar">
<li class="nav-item">
<a class="nav-link" routerLink="clients" routerLinkActive="active">
<i class="fa fa-university" aria-hidden="true"></i>
{{'clients' | i18n}}
</a>
</li>
<li class="nav-item" *ngIf="showManageTab">
<a class="nav-link" [routerLink]="manageRoute" routerLinkActive="active">
<i class="fa fa-sliders" aria-hidden="true"></i>
{{'manage' | i18n}}
</a>
</li>
<li class="nav-item" *ngIf="showSettingsTab">
<a class="nav-link" routerLink="settings" routerLinkActive="active">
<i class="fa fa-cogs" aria-hidden="true"></i>
{{'settings' | i18n}}
</a>
</li>
</ul>
<div class="container d-flex">
<div class="d-flex flex-column">
<div class="my-auto d-flex align-items-center pl-1">
<app-avatar [data]="provider.name" size="45" [circle]="true"></app-avatar>
<div class="org-name ml-3">
<span>{{ provider.name }}</span>
<small class="text-muted">{{ "provider" | i18n }}</small>
</div>
<div class="ml-3 card border-danger text-danger bg-transparent" *ngIf="!provider.enabled">
<div class="card-body py-2">
<i class="bwi bwi-exclamation-triangle" aria-hidden="true"></i>
{{ "providerIsDisabled" | i18n }}
</div>
</div>
</div>
<ul class="nav nav-tabs" *ngIf="showMenuBar">
<li class="nav-item">
<a class="nav-link" routerLink="clients" routerLinkActive="active">
<i class="bwi bwi-bank" aria-hidden="true"></i>
{{ "clients" | i18n }}
</a>
</li>
<li class="nav-item" *ngIf="showManageTab">
<a class="nav-link" [routerLink]="manageRoute" routerLinkActive="active">
<i class="bwi bwi-sliders" aria-hidden="true"></i>
{{ "manage" | i18n }}
</a>
</li>
<li class="nav-item" *ngIf="showSettingsTab">
<a class="nav-link" routerLink="settings" routerLinkActive="active">
<i class="bwi bwi-cogs" aria-hidden="true"></i>
{{ "settings" | i18n }}
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="container page-content">
<router-outlet></router-outlet>
<router-outlet></router-outlet>
</div>
<app-footer></app-footer>

View File

@@ -1,51 +1,49 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { UserService } from 'jslib-common/abstractions/user.service';
import { Provider } from 'jslib-common/models/domain/provider';
import { ProviderService } from "jslib-common/abstractions/provider.service";
import { Provider } from "jslib-common/models/domain/provider";
@Component({
selector: 'providers-layout',
templateUrl: 'providers-layout.component.html',
selector: "providers-layout",
templateUrl: "providers-layout.component.html",
})
export class ProvidersLayoutComponent {
provider: Provider;
private providerId: string;
provider: Provider;
private providerId: string;
constructor(private route: ActivatedRoute, private providerService: ProviderService) {}
constructor(private route: ActivatedRoute, private userService: UserService) { }
ngOnInit() {
document.body.classList.remove("layout_frontend");
this.route.params.subscribe(async (params) => {
this.providerId = params.providerId;
await this.load();
});
}
ngOnInit() {
document.body.classList.remove('layout_frontend');
this.route.params.subscribe(async params => {
this.providerId = params.providerId;
await this.load();
});
}
async load() {
this.provider = await this.userService.getProvider(this.providerId);
}
get showMenuBar() {
return this.showManageTab || this.showSettingsTab;
}
get showManageTab() {
return this.provider.canManageUsers || this.provider.canAccessEventLogs;
}
get showSettingsTab() {
return this.provider.isProviderAdmin;
}
get manageRoute(): string {
switch (true) {
case this.provider.canManageUsers:
return 'manage/people';
case this.provider.canAccessEventLogs:
return 'manage/events';
}
async load() {
this.provider = await this.providerService.get(this.providerId);
}
get showMenuBar() {
return this.showManageTab || this.showSettingsTab;
}
get showManageTab() {
return this.provider.canManageUsers || this.provider.canAccessEventLogs;
}
get showSettingsTab() {
return this.provider.isProviderAdmin;
}
get manageRoute(): string {
switch (true) {
case this.provider.canManageUsers:
return "manage/people";
case this.provider.canAccessEventLogs:
return "manage/events";
}
}
}

View File

@@ -1,123 +1,121 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { AuthGuardService } from 'jslib-angular/services/auth-guard.service';
import { Permissions } from 'jslib-common/enums/permissions';
import { AuthGuard } from "jslib-angular/guards/auth.guard";
import { Permissions } from "jslib-common/enums/permissions";
import { AddOrganizationComponent } from './clients/add-organization.component';
import { ClientsComponent } from './clients/clients.component';
import { CreateOrganizationComponent } from './clients/create-organization.component';
import { AcceptProviderComponent } from './manage/accept-provider.component';
import { EventsComponent } from './manage/events.component';
import { ManageComponent } from './manage/manage.component';
import { PeopleComponent } from './manage/people.component';
import { ProvidersLayoutComponent } from './providers-layout.component';
import { SettingsComponent } from './settings/settings.component';
import { SetupProviderComponent } from './setup/setup-provider.component';
import { SetupComponent } from './setup/setup.component';
import { FrontendLayoutComponent } from "src/app/layouts/frontend-layout.component";
import { ProvidersComponent } from "src/app/providers/providers.component";
import { FrontendLayoutComponent } from 'src/app/layouts/frontend-layout.component';
import { ProvidersComponent } from 'src/app/providers/providers.component';
import { ProviderGuardService } from './services/provider-guard.service';
import { ProviderTypeGuardService } from './services/provider-type-guard.service';
import { AccountComponent } from './settings/account.component';
import { ClientsComponent } from "./clients/clients.component";
import { CreateOrganizationComponent } from "./clients/create-organization.component";
import { PermissionsGuard } from "./guards/provider-type.guard";
import { ProviderGuard } from "./guards/provider.guard";
import { AcceptProviderComponent } from "./manage/accept-provider.component";
import { EventsComponent } from "./manage/events.component";
import { ManageComponent } from "./manage/manage.component";
import { PeopleComponent } from "./manage/people.component";
import { ProvidersLayoutComponent } from "./providers-layout.component";
import { AccountComponent } from "./settings/account.component";
import { SettingsComponent } from "./settings/settings.component";
import { SetupProviderComponent } from "./setup/setup-provider.component";
import { SetupComponent } from "./setup/setup.component";
const routes: Routes = [
{
path: '',
canActivate: [AuthGuardService],
component: ProvidersComponent,
},
{
path: '',
component: FrontendLayoutComponent,
{
path: "",
canActivate: [AuthGuard],
component: ProvidersComponent,
},
{
path: "",
component: FrontendLayoutComponent,
children: [
{
path: "setup-provider",
component: SetupProviderComponent,
data: { titleId: "setupProvider" },
},
{
path: "accept-provider",
component: AcceptProviderComponent,
data: { titleId: "acceptProvider" },
},
],
},
{
path: "",
canActivate: [AuthGuard],
children: [
{
path: "setup",
component: SetupComponent,
},
{
path: ":providerId",
component: ProvidersLayoutComponent,
canActivate: [ProviderGuard],
children: [
{
path: 'setup-provider',
component: SetupProviderComponent,
data: { titleId: 'setupProvider' },
},
{
path: 'accept-provider',
component: AcceptProviderComponent,
data: { titleId: 'acceptProvider' },
},
{ path: "", pathMatch: "full", redirectTo: "clients" },
{ path: "clients/create", component: CreateOrganizationComponent },
{ path: "clients", component: ClientsComponent, data: { titleId: "clients" } },
{
path: "manage",
component: ManageComponent,
children: [
{
path: "",
pathMatch: "full",
redirectTo: "people",
},
{
path: "people",
component: PeopleComponent,
canActivate: [PermissionsGuard],
data: {
titleId: "people",
permissions: [Permissions.ManageUsers],
},
},
{
path: "events",
component: EventsComponent,
canActivate: [PermissionsGuard],
data: {
titleId: "eventLogs",
permissions: [Permissions.AccessEventLogs],
},
},
],
},
{
path: "settings",
component: SettingsComponent,
children: [
{
path: "",
pathMatch: "full",
redirectTo: "account",
},
{
path: "account",
component: AccountComponent,
canActivate: [PermissionsGuard],
data: {
titleId: "myProvider",
permissions: [Permissions.ManageProvider],
},
},
],
},
],
},
{
path: '',
canActivate: [AuthGuardService],
children: [
{
path: 'setup',
component: SetupComponent,
},
{
path: ':providerId',
component: ProvidersLayoutComponent,
canActivate: [ProviderGuardService],
children: [
{ path: '', pathMatch: 'full', redirectTo: 'clients' },
{ path: 'clients/create', component: CreateOrganizationComponent },
{ path: 'clients', component: ClientsComponent, data: { titleId: 'clients' } },
{
path: 'manage',
component: ManageComponent,
children: [
{
path: '',
pathMatch: 'full',
redirectTo: 'people',
},
{
path: 'people',
component: PeopleComponent,
canActivate: [ProviderTypeGuardService],
data: {
titleId: 'people',
permissions: [Permissions.ManageUsers],
},
},
{
path: 'events',
component: EventsComponent,
canActivate: [ProviderTypeGuardService],
data: {
titleId: 'eventLogs',
permissions: [Permissions.AccessEventLogs],
},
},
],
},
{
path: 'settings',
component: SettingsComponent,
children: [
{
path: '',
pathMatch: 'full',
redirectTo: 'account',
},
{
path: 'account',
component: AccountComponent,
canActivate: [ProviderTypeGuardService],
data: {
titleId: 'myProvider',
permissions: [Permissions.ManageProvider],
},
},
],
},
],
},
],
},
},
],
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ProvidersRoutingModule { }
export class ProvidersRoutingModule {}

View File

@@ -1,62 +1,56 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ComponentFactoryResolver, NgModule } from "@angular/core";
import { ProviderGuardService } from './services/provider-guard.service';
import { ProviderTypeGuardService } from './services/provider-type-guard.service';
import { ProviderService } from './services/provider.service';
import { ModalService } from "jslib-angular/services/modal.service";
import { ProvidersLayoutComponent } from './providers-layout.component';
import { ProvidersRoutingModule } from './providers-routing.module';
import { LayoutsModule } from "src/app/layouts/layouts.module";
import { SharedModule } from "src/app/modules/shared.module";
import { AddOrganizationComponent } from './clients/add-organization.component';
import { ClientsComponent } from './clients/clients.component';
import { CreateOrganizationComponent } from './clients/create-organization.component';
import { AcceptProviderComponent } from './manage/accept-provider.component';
import { BulkConfirmComponent } from './manage/bulk/bulk-confirm.component';
import { BulkRemoveComponent } from './manage/bulk/bulk-remove.component';
import { EventsComponent } from './manage/events.component';
import { ManageComponent } from './manage/manage.component';
import { PeopleComponent } from './manage/people.component';
import { UserAddEditComponent } from './manage/user-add-edit.component';
import { AccountComponent } from './settings/account.component';
import { SettingsComponent } from './settings/settings.component';
import { SetupProviderComponent } from './setup/setup-provider.component';
import { SetupComponent } from './setup/setup.component';
import { OssModule } from 'src/app/oss.module';
import { AddOrganizationComponent } from "./clients/add-organization.component";
import { ClientsComponent } from "./clients/clients.component";
import { CreateOrganizationComponent } from "./clients/create-organization.component";
import { PermissionsGuard } from "./guards/provider-type.guard";
import { ProviderGuard } from "./guards/provider.guard";
import { AcceptProviderComponent } from "./manage/accept-provider.component";
import { BulkConfirmComponent } from "./manage/bulk/bulk-confirm.component";
import { BulkRemoveComponent } from "./manage/bulk/bulk-remove.component";
import { EventsComponent } from "./manage/events.component";
import { ManageComponent } from "./manage/manage.component";
import { PeopleComponent } from "./manage/people.component";
import { UserAddEditComponent } from "./manage/user-add-edit.component";
import { ProvidersLayoutComponent } from "./providers-layout.component";
import { ProvidersRoutingModule } from "./providers-routing.module";
import { WebProviderService } from "./services/webProvider.service";
import { AccountComponent } from "./settings/account.component";
import { SettingsComponent } from "./settings/settings.component";
import { SetupProviderComponent } from "./setup/setup-provider.component";
import { SetupComponent } from "./setup/setup.component";
@NgModule({
imports: [
CommonModule,
FormsModule,
OssModule,
ProvidersRoutingModule,
],
declarations: [
AcceptProviderComponent,
AccountComponent,
AddOrganizationComponent,
BulkConfirmComponent,
BulkRemoveComponent,
ClientsComponent,
CreateOrganizationComponent,
EventsComponent,
ManageComponent,
PeopleComponent,
ProvidersLayoutComponent,
SettingsComponent,
SetupComponent,
SetupProviderComponent,
UserAddEditComponent,
],
providers: [
ProviderService,
ProviderGuardService,
ProviderTypeGuardService,
],
imports: [SharedModule, LayoutsModule, ProvidersRoutingModule],
declarations: [
AcceptProviderComponent,
AccountComponent,
AddOrganizationComponent,
BulkConfirmComponent,
BulkRemoveComponent,
ClientsComponent,
CreateOrganizationComponent,
EventsComponent,
ManageComponent,
PeopleComponent,
ProvidersLayoutComponent,
SettingsComponent,
SetupComponent,
SetupProviderComponent,
UserAddEditComponent,
],
providers: [WebProviderService, ProviderGuard, PermissionsGuard],
})
export class ProvidersModule {}
export class ProvidersModule {
constructor(modalService: ModalService, componentFactoryResolver: ComponentFactoryResolver) {
modalService.registerComponentFactoryResolver(
AddOrganizationComponent,
componentFactoryResolver
);
}
}

View File

@@ -1,32 +0,0 @@
import { Injectable } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivate,
Router,
} from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { UserService } from 'jslib-common/abstractions/user.service';
@Injectable()
export class ProviderGuardService implements CanActivate {
constructor(private userService: UserService, private router: Router,
private toasterService: ToasterService, private i18nService: I18nService) { }
async canActivate(route: ActivatedRouteSnapshot) {
const provider = await this.userService.getProvider(route.params.providerId);
if (provider == null) {
this.router.navigate(['/']);
return false;
}
if (!provider.isProviderAdmin && !provider.enabled) {
this.toasterService.popAsync('error', null, this.i18nService.t('providerIsDisabled'));
this.router.navigate(['/']);
return false;
}
return true;
}
}

View File

@@ -1,31 +0,0 @@
import { Injectable } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivate,
Router,
} from '@angular/router';
import { UserService } from 'jslib-common/abstractions/user.service';
import { Permissions } from 'jslib-common/enums/permissions';
@Injectable()
export class ProviderTypeGuardService implements CanActivate {
constructor(private userService: UserService, private router: Router) { }
async canActivate(route: ActivatedRouteSnapshot) {
const provider = await this.userService.getProvider(route.params.providerId);
const permissions = route.data == null ? null : route.data.permissions as Permissions[];
if (
(permissions.indexOf(Permissions.AccessEventLogs) !== -1 && provider.canAccessEventLogs) ||
(permissions.indexOf(Permissions.ManageProvider) !== -1 && provider.isProviderAdmin) ||
(permissions.indexOf(Permissions.ManageUsers) !== -1 && provider.canManageUsers)
) {
return true;
}
this.router.navigate(['/providers', provider.id]);
return false;
}
}

View File

@@ -1,32 +0,0 @@
import { Injectable } from '@angular/core';
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { SyncService } from 'jslib-common/abstractions/sync.service';
import { ProviderAddOrganizationRequest } from 'jslib-common/models/request/provider/providerAddOrganizationRequest';
@Injectable()
export class ProviderService {
constructor(private cryptoService: CryptoService, private syncService: SyncService, private apiService: ApiService) {}
async addOrganizationToProvider(providerId: string, organizationId: string) {
const orgKey = await this.cryptoService.getOrgKey(organizationId);
const providerKey = await this.cryptoService.getProviderKey(providerId);
const encryptedOrgKey = await this.cryptoService.encrypt(orgKey.key, providerKey);
const request = new ProviderAddOrganizationRequest();
request.organizationId = organizationId;
request.key = encryptedOrgKey.encryptedString;
const response = await this.apiService.postProviderAddOrganization(providerId, request);
await this.syncService.fullSync(true);
return response;
}
async detachOrganizastion(providerId: string, organizationId: string): Promise<any> {
await this.apiService.deleteProviderOrganization(providerId, organizationId);
await this.syncService.fullSync(true);
}
}

View File

@@ -0,0 +1,35 @@
import { Injectable } from "@angular/core";
import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { ProviderAddOrganizationRequest } from "jslib-common/models/request/provider/providerAddOrganizationRequest";
@Injectable()
export class WebProviderService {
constructor(
private cryptoService: CryptoService,
private syncService: SyncService,
private apiService: ApiService
) {}
async addOrganizationToProvider(providerId: string, organizationId: string) {
const orgKey = await this.cryptoService.getOrgKey(organizationId);
const providerKey = await this.cryptoService.getProviderKey(providerId);
const encryptedOrgKey = await this.cryptoService.encrypt(orgKey.key, providerKey);
const request = new ProviderAddOrganizationRequest();
request.organizationId = organizationId;
request.key = encryptedOrgKey.encryptedString;
const response = await this.apiService.postProviderAddOrganization(providerId, request);
await this.syncService.fullSync(true);
return response;
}
async detachOrganizastion(providerId: string, organizationId: string): Promise<any> {
await this.apiService.deleteProviderOrganization(providerId, organizationId);
await this.syncService.fullSync(true);
}
}

View File

@@ -1,30 +1,52 @@
<div class="page-header">
<h1>{{'myProvider' | i18n}}</h1>
<h1>{{ "myProvider" | i18n }}</h1>
</div>
<div *ngIf="loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</div>
<form *ngIf="provider && !loading" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="name">{{'providerName' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="provider.name"
[disabled]="selfHosted">
</div>
<div class="form-group">
<label for="billingEmail">{{'billingEmail' | i18n}}</label>
<input id="billingEmail" class="form-control" type="text" name="BillingEmail"
[(ngModel)]="provider.billingEmail" [disabled]="selfHosted">
</div>
</div>
<div class="col-6">
<app-avatar data="{{provider.name}}" dynamic="true" size="75" fontSize="35"></app-avatar>
</div>
<form
*ngIf="provider && !loading"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="name">{{ "providerName" | i18n }}</label>
<input
id="name"
class="form-control"
type="text"
name="Name"
[(ngModel)]="provider.name"
[disabled]="selfHosted"
/>
</div>
<div class="form-group">
<label for="billingEmail">{{ "billingEmail" | i18n }}</label>
<input
id="billingEmail"
class="form-control"
type="text"
name="BillingEmail"
[(ngModel)]="provider.billingEmail"
[disabled]="selfHosted"
/>
</div>
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'save' | i18n}}</span>
</button>
<div class="col-6">
<app-avatar data="{{ provider.name }}" dynamic="true" size="75" fontSize="35"></app-avatar>
</div>
</div>
<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>
</form>

View File

@@ -1,62 +1,63 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
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 { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { SyncService } from 'jslib-common/abstractions/sync.service';
import { ProviderUpdateRequest } from 'jslib-common/models/request/provider/providerUpdateRequest';
import { ProviderResponse } from 'jslib-common/models/response/provider/providerResponse';
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 { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { ProviderUpdateRequest } from "jslib-common/models/request/provider/providerUpdateRequest";
import { ProviderResponse } from "jslib-common/models/response/provider/providerResponse";
@Component({
selector: 'provider-account',
templateUrl: 'account.component.html',
selector: "provider-account",
templateUrl: "account.component.html",
})
export class AccountComponent {
selfHosted = false;
loading = true;
provider: ProviderResponse;
formPromise: Promise<any>;
taxFormPromise: Promise<any>;
selfHosted = false;
loading = true;
provider: ProviderResponse;
formPromise: Promise<any>;
taxFormPromise: Promise<any>;
private providerId: string;
private providerId: string;
constructor(private apiService: ApiService, private i18nService: I18nService,
private toasterService: ToasterService, private route: ActivatedRoute,
private syncService: SyncService, private platformUtilsService: PlatformUtilsService,
private logService: LogService) { }
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private route: ActivatedRoute,
private syncService: SyncService,
private platformUtilsService: PlatformUtilsService,
private logService: LogService
) {}
async ngOnInit() {
this.selfHosted = this.platformUtilsService.isSelfHost();
this.route.parent.parent.params.subscribe(async params => {
this.providerId = params.providerId;
try {
this.provider = await this.apiService.getProvider(this.providerId);
} catch (e) {
this.logService.error(`Handled exception: ${e}`);
}
});
this.loading = false;
}
async submit() {
try {
const request = new ProviderUpdateRequest();
request.name = this.provider.name;
request.businessName = this.provider.businessName;
request.billingEmail = this.provider.billingEmail;
this.formPromise = this.apiService.putProvider(this.providerId, request).then(() => {
return this.syncService.fullSync(true);
});
await this.formPromise;
this.toasterService.popAsync('success', null, this.i18nService.t('providerUpdated'));
} catch (e) {
this.logService.error(`Handled exception: ${e}`);
}
async ngOnInit() {
this.selfHosted = this.platformUtilsService.isSelfHost();
this.route.parent.parent.params.subscribe(async (params) => {
this.providerId = params.providerId;
try {
this.provider = await this.apiService.getProvider(this.providerId);
} catch (e) {
this.logService.error(`Handled exception: ${e}`);
}
});
this.loading = false;
}
async submit() {
try {
const request = new ProviderUpdateRequest();
request.name = this.provider.name;
request.businessName = this.provider.businessName;
request.billingEmail = this.provider.billingEmail;
this.formPromise = this.apiService.putProvider(this.providerId, request).then(() => {
return this.syncService.fullSync(true);
});
await this.formPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("providerUpdated"));
} catch (e) {
this.logService.error(`Handled exception: ${e}`);
}
}
}

View File

@@ -1,17 +1,17 @@
<div class="container page-content">
<div class="row">
<div class="col-3">
<div class="card">
<div class="card-header">{{'settings' | i18n}}</div>
<div class="list-group list-group-flush">
<a routerLink="account" class="list-group-item" routerLinkActive="active">
{{'myProvider' | i18n}}
</a>
</div>
</div>
</div>
<div class="col-9">
<router-outlet></router-outlet>
<div class="row">
<div class="col-3">
<div class="card">
<div class="card-header">{{ "settings" | i18n }}</div>
<div class="list-group list-group-flush">
<a routerLink="account" class="list-group-item" routerLinkActive="active">
{{ "myProvider" | i18n }}
</a>
</div>
</div>
</div>
<div class="col-9">
<router-outlet></router-outlet>
</div>
</div>
</div>

View File

@@ -1,20 +1,18 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { ProviderService } from "jslib-common/abstractions/provider.service";
@Component({
selector: 'provider-settings',
templateUrl: 'settings.component.html',
selector: "provider-settings",
templateUrl: "settings.component.html",
})
export class SettingsComponent {
constructor(private route: ActivatedRoute, private userService: UserService,
private platformUtilsService: PlatformUtilsService) { }
constructor(private route: ActivatedRoute, private providerService: ProviderService) {}
ngOnInit() {
this.route.parent.params.subscribe(async params => {
const provider = await this.userService.getProvider(params.providerId);
});
}
ngOnInit() {
this.route.parent.params.subscribe(async (params) => {
await this.providerService.get(params.providerId);
});
}
}

View File

@@ -1,27 +1,35 @@
<div class="mt-5 d-flex justify-content-center" *ngIf="loading">
<div>
<img src="/src/images/logo-dark@2x.png" class="mb-4 logo" alt="Bitwarden">
<p class="text-center">
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</p>
</div>
<div>
<img class="mb-4 logo logo-themed" alt="Bitwarden" />
<p class="text-center">
<i
class="bwi bwi-spinner bwi-spin bwi-2x text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</p>
</div>
</div>
<div class="container" *ngIf="!loading && !authed">
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{'setupProvider' | i18n}}</p>
<div class="card d-block">
<div class="card-body">
<p>{{'setupProviderLoginDesc' | i18n}}</p>
<hr>
<div class="d-flex">
<a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block">
{{'logIn' | i18n}}
</a>
</div>
</div>
</div>
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{ "setupProvider" | i18n }}</p>
<div class="card d-block">
<div class="card-body">
<p>{{ "setupProviderLoginDesc" | i18n }}</p>
<hr />
<div class="d-flex">
<a
routerLink="/login"
[queryParams]="{ email: email }"
class="btn btn-primary btn-block"
>
{{ "logIn" | i18n }}
</a>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,22 +1,22 @@
import { Component } from '@angular/core';
import { Component } from "@angular/core";
import { BaseAcceptComponent } from 'src/app/common/base.accept.component';
import { BaseAcceptComponent } from "src/app/common/base.accept.component";
@Component({
selector: 'app-setup-provider',
templateUrl: 'setup-provider.component.html',
selector: "app-setup-provider",
templateUrl: "setup-provider.component.html",
})
export class SetupProviderComponent extends BaseAcceptComponent {
failedShortMessage = "inviteAcceptFailedShort";
failedMessage = "inviteAcceptFailed";
failedShortMessage = 'inviteAcceptFailedShort';
failedMessage = 'inviteAcceptFailed';
requiredParameters = ["providerId", "email", "token"];
requiredParameters = ['providerId', 'email', 'token'];
async authedHandler(qParams: any) {
this.router.navigate(["/providers/setup"], { queryParams: qParams });
}
async authedHandler(qParams: any) {
this.router.navigate(['/providers/setup'], {queryParams: qParams});
}
// tslint:disable-next-line
async unauthedHandler(qParams: any) {}
async unauthedHandler(qParams: any) {
// Empty
}
}

View File

@@ -1,32 +1,39 @@
<app-navbar></app-navbar>
<div class="container page-content">
<div class="page-header">
<h1>{{'setupProvider' | i18n}}</h1>
<div class="page-header">
<h1>{{ "setupProvider" | i18n }}</h1>
</div>
<p>{{ "setupProviderDesc" | i18n }}</p>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="loading">
<h2 class="mt-5">{{ "generalInformation" | i18n }}</h2>
<div class="row">
<div class="form-group col-6">
<label for="name">{{ "providerName" | i18n }}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required />
</div>
<div class="form-group col-6">
<label for="billingEmail">{{ "billingEmail" | i18n }}</label>
<input
id="billingEmail"
class="form-control"
type="text"
name="BillingEmail"
[(ngModel)]="billingEmail"
required
/>
</div>
</div>
<p>{{'setupProviderDesc' | i18n}}</p>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="loading">
<h2 class="mt-5">{{'generalInformation' | i18n}}</h2>
<div class="row">
<div class="form-group col-6">
<label for="name">{{'providerName' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required>
</div>
<div class="form-group col-6">
<label for="billingEmail">{{'billingEmail' | i18n}}</label>
<input id="billingEmail" class="form-control" type="text" name="BillingEmail" [(ngModel)]="billingEmail" required>
</div>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'submit' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" *ngIf="showCancel">
{{'cancel' | i18n}}
</button>
</div>
</form>
<div class="mt-4">
<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>{{ "submit" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" *ngIf="showCancel">
{{ "cancel" | i18n }}
</button>
</div>
</form>
</div>
<app-footer></app-footer>

View File

@@ -1,94 +1,97 @@
import {
Component,
OnInit,
} from '@angular/core';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import {
Toast,
ToasterService,
} from 'angular2-toaster';
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { ValidationService } from 'jslib-angular/services/validation.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { SyncService } from 'jslib-common/abstractions/sync.service';
import { ProviderSetupRequest } from 'jslib-common/models/request/provider/providerSetupRequest';
import { ValidationService } from "jslib-angular/services/validation.service";
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 { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { ProviderSetupRequest } from "jslib-common/models/request/provider/providerSetupRequest";
@Component({
selector: 'provider-setup',
templateUrl: 'setup.component.html',
selector: "provider-setup",
templateUrl: "setup.component.html",
})
export class SetupComponent implements OnInit {
loading = true;
authed = false;
email: string;
formPromise: Promise<any>;
loading = true;
authed = false;
email: string;
formPromise: Promise<any>;
providerId: string;
token: string;
name: string;
billingEmail: string;
providerId: string;
token: string;
name: string;
billingEmail: string;
constructor(private router: Router, private toasterService: ToasterService,
private i18nService: I18nService, private route: ActivatedRoute,
private cryptoService: CryptoService, private apiService: ApiService,
private syncService: SyncService, private validationService: ValidationService) { }
constructor(
private router: Router,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private route: ActivatedRoute,
private cryptoService: CryptoService,
private apiService: ApiService,
private syncService: SyncService,
private validationService: ValidationService
) {}
ngOnInit() {
document.body.classList.remove('layout_frontend');
let fired = false;
this.route.queryParams.subscribe(async qParams => {
if (fired) {
return;
}
fired = true;
const error = qParams.providerId == null || qParams.email == null || qParams.token == null;
ngOnInit() {
document.body.classList.remove("layout_frontend");
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
const error = qParams.providerId == null || qParams.email == null || qParams.token == null;
if (error) {
const toast: Toast = {
type: 'error',
title: null,
body: this.i18nService.t('emergencyInviteAcceptFailed'),
timeout: 10000,
};
this.toasterService.popAsync(toast);
this.router.navigate(['/']);
} else {
this.providerId = qParams.providerId;
this.token = qParams.token;
}
});
}
if (error) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("emergencyInviteAcceptFailed"),
{ timeout: 10000 }
);
this.router.navigate(["/"]);
return;
}
async submit() {
this.formPromise = this.doSubmit();
await this.formPromise;
this.formPromise = null;
}
this.providerId = qParams.providerId;
this.token = qParams.token;
async doSubmit() {
try {
const shareKey = await this.cryptoService.makeShareKey();
const key = shareKey[0].encryptedString;
const request = new ProviderSetupRequest();
request.name = this.name;
request.billingEmail = this.billingEmail;
request.token = this.token;
request.key = key;
const provider = await this.apiService.postProviderSetup(this.providerId, request);
this.toasterService.popAsync('success', null, this.i18nService.t('providerSetup'));
await this.syncService.fullSync(true);
this.router.navigate(['/providers', provider.id]);
} catch (e) {
this.validationService.showError(e);
// Check if provider exists, redirect if it does
try {
const provider = await this.apiService.getProvider(this.providerId);
if (provider.name != null) {
this.router.navigate(["/providers", provider.id], { replaceUrl: true });
}
} catch (e) {
this.validationService.showError(e);
this.router.navigate(["/"]);
}
});
}
async submit() {
this.formPromise = this.doSubmit();
await this.formPromise;
this.formPromise = null;
}
async doSubmit() {
try {
const shareKey = await this.cryptoService.makeShareKey();
const key = shareKey[0].encryptedString;
const request = new ProviderSetupRequest();
request.name = this.name;
request.billingEmail = this.billingEmail;
request.token = this.token;
request.key = key;
const provider = await this.apiService.postProviderSetup(this.providerId, request);
this.platformUtilsService.showToast("success", null, this.i18nService.t("providerSetup"));
await this.syncService.fullSync(true);
this.router.navigate(["/providers", provider.id]);
} catch (e) {
this.validationService.showError(e);
}
}
}

View File

@@ -1,12 +1,12 @@
const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin;
const { AngularWebpackPlugin } = require("@ngtools/webpack");
const webpackConfig = require('../webpack.config');
const webpackConfig = require("../webpack.config");
webpackConfig.entry['app/main'] = './bitwarden_license/src/app/main.ts';
webpackConfig.plugins[webpackConfig.plugins.length -1] = new AngularCompilerPlugin({
tsConfigPath: 'tsconfig.json',
entryModule: 'bitwarden_license/src/app/app.module#AppModule',
sourceMap: true,
webpackConfig.entry["app/main"] = "./bitwarden_license/src/app/main.ts";
webpackConfig.plugins[webpackConfig.plugins.length - 1] = new AngularWebpackPlugin({
tsConfigPath: "tsconfig.json",
entryModule: "bitwarden_license/src/app/app.module#AppModule",
sourceMap: true,
});
module.exports = webpackConfig;

View File

@@ -1,29 +1,36 @@
function load(envName) {
const envOverrides = {
'production': () => require('./config/production.json'),
'qa': () => require('./config/qa.json'),
'development': () => require('./config/development.json'),
};
const baseConfig = require('./config/base.json');
const overrideConfig = envOverrides.hasOwnProperty(envName) ? envOverrides[envName]() : {};
return {
...baseConfig,
...overrideConfig
};
return {
...require("./config/base.json"),
...loadConfig(envName),
...loadConfig("local"),
dev: {
...require("./config/base.json").dev,
...loadConfig(envName).dev,
...loadConfig("local").dev,
},
};
}
function log(configObj) {
const repeatNum = 50
console.log(`${"=".repeat(repeatNum)}\nenvConfig`)
Object.entries(configObj).map(([key, value]) => {
console.log(` ${key}: ${value}`)
})
console.log(`${"=".repeat(repeatNum)}`)
const repeatNum = 50;
console.log(`${"=".repeat(repeatNum)}\nenvConfig`);
console.log(JSON.stringify(configObj, null, 2));
console.log(`${"=".repeat(repeatNum)}`);
}
function loadConfig(configName) {
try {
return require(`./config/${configName}.json`);
} catch (e) {
if (e instanceof Error && e.code === "MODULE_NOT_FOUND") {
return {};
} else {
throw e;
}
}
}
module.exports = {
load,
log
load,
log,
};

View File

@@ -1,8 +1,13 @@
{
"proxyApi": "http://localhost:4000",
"proxyIdentity": "http://localhost:33656",
"proxyEvents": "http://localhost:46273",
"proxyNotifications": "http://localhost:61840",
"proxyPortal": "http://localhost:52313",
"allowedHosts": []
"urls": {},
"stripeKey": "pk_test_KPoCfZXu7mznb9uSCPZ2JpTD",
"braintreeKey": "sandbox_r72q8jq6_9pnxkwm75f87sdc2",
"paypal": {
"businessId": "AD3LAUZSNVPJY",
"buttonAction": "https://www.sandbox.paypal.com/cgi-bin/webscr"
},
"dev": {
"port": 8080,
"allowedHosts": "auto"
}
}

17
config/cloud.json Normal file
View File

@@ -0,0 +1,17 @@
{
"urls": {
"icons": "https://icons.bitwarden.net",
"notifications": "https://notifications.bitwarden.com"
},
"stripeKey": "pk_live_bpN0P37nMxrMQkcaHXtAybJk",
"braintreeKey": "production_qfbsv8kc_njj2zjtyngtjmbjd",
"paypal": {
"businessId": "4ZDA7DLUUJGMN",
"buttonAction": "https://www.paypal.com/cgi-bin/webscr"
},
"dev": {
"proxyApi": "https://api.bitwarden.com",
"proxyIdentity": "https://identity.bitwarden.com",
"proxyEvents": "https://events.bitwarden.com"
}
}

11
config/development.json Normal file
View File

@@ -0,0 +1,11 @@
{
"urls": {
"notifications": "http://localhost:61840"
},
"dev": {
"proxyApi": "http://localhost:4000",
"proxyIdentity": "http://localhost:33656",
"proxyEvents": "http://localhost:46273",
"proxyNotifications": "http://localhost:61840"
}
}

View File

@@ -1,7 +0,0 @@
{
"proxyApi": "https://api.bitwarden.com",
"proxyIdentity": "https://identity.bitwarden.com",
"proxyEvents": "https://events.bitwarden.com",
"proxyNotifications": "https://notifications.bitwarden.com",
"proxyPortal": "https://portal.bitwarden.com"
}

View File

@@ -1,7 +1,11 @@
{
"proxyApi": "https://api.qa.bitwarden.com",
"proxyIdentity": "https://identity.qa.bitwarden.com",
"proxyEvents": "https://events.qa.bitwarden.com",
"proxyNotifications": "https://notifications.qa.bitwarden.com",
"proxyPortal": "https://portal.qa.bitwarden.com"
"urls": {
"icons": "https://icons.qa.bitwarden.pw",
"notifications": "https://notifications.qa.bitwarden.pw"
},
"dev": {
"proxyApi": "https://api.qa.bitwarden.pw",
"proxyIdentity": "https://identity.qa.bitwarden.pw",
"proxyEvents": "https://events.qa.bitwarden.pw"
}
}

9
config/selfhosted.json Normal file
View File

@@ -0,0 +1,9 @@
{
"dev": {
"proxyApi": "http://localhost:4001",
"proxyIdentity": "http://localhost:33657",
"proxyEvents": "http://localhost:46274",
"proxyNotifications": "http://localhost:61841",
"port": 8081
}
}

View File

@@ -1,5 +1,9 @@
project_id_env: _CROWDIN_PROJECT_ID
api_token_env: CROWDIN_API_TOKEN
preserve_hierarchy: true
files:
- source: /src/locales/en/messages.json
dest: /src/locales/en/%file_name%.%file_extension%
translation: /src/locales/%two_letters_code%/%original_file_name%
update_option: update_as_unapproved
languages_mapping:

View File

@@ -31,7 +31,6 @@ mkhomedir_helper $USERNAME
chown -R $USERNAME:$GROUPNAME /etc/bitwarden
cp /etc/bitwarden/web/app-id.json /app/app-id.json
cp /etc/bitwarden/web/assetlinks.json /app/assetlinks.json
chown -R $USERNAME:$GROUPNAME /app
chown -R $USERNAME:$GROUPNAME /bitwarden_server

View File

@@ -1,37 +0,0 @@
const gulp = require('gulp');
const googleWebFonts = require('gulp-google-webfonts');
const del = require('del');
const package = require('./package.json');
const fs = require('fs');
const paths = {
node_modules: './node_modules/',
src: './src/',
build: './build/',
cssDir: './src/css/',
};
function clean() {
return del([paths.cssDir]);
}
function webfonts() {
return gulp.src('./webfonts.list')
.pipe(googleWebFonts({
fontsDir: 'webfonts',
cssFilename: 'webfonts.css',
format: 'woff',
}))
.pipe(gulp.dest(paths.cssDir));
};
function version(cb) {
fs.writeFileSync(paths.build + 'version.json', '{"version":"' + package.version + '"}');
cb();
}
exports.clean = clean;
exports.webfonts = gulp.series(clean, webfonts);
exports.prebuild = gulp.series(clean, webfonts);
exports.version = version;
exports.postdist = version;

2
jslib

Submodule jslib updated: c70c8ecc24...0d658ba26d

25460
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "bitwarden-web",
"version": "2.21.1",
"name": "@bitwarden/web-vault",
"version": "2022.05.0",
"license": "GPL-3.0",
"repository": "https://github.com/bitwarden/web",
"scripts": {
@@ -11,84 +11,109 @@
"symlink:win": "rm -rf ./jslib && cmd /c mklink /J .\\jslib ..\\jslib",
"symlink:mac": "npm run symlink:lin",
"symlink:lin": "rm -rf ./jslib && ln -s ../jslib ./jslib",
"build": "gulp prebuild && webpack -c bitwarden_license/webpack.config.js",
"build:oss": "gulp prebuild && webpack",
"build:watch": "gulp prebuild && webpack serve -c bitwarden_license/webpack.config.js",
"build:watch:oss": "gulp prebuild && webpack serve",
"build:dev": "cross-env ENV=development npm run build",
"build:dev:watch": "cross-env ENV=development npm run build:watch",
"build:qa": "cross-env NODE_ENV=production ENV=qa npm run build",
"build:qa:watch": "cross-env NODE_ENV=production ENV=qa npm run build:watch",
"build:prod": "cross-env NODE_ENV=production ENV=production npm run build",
"build:prod:oss": "cross-env NODE_ENV=production ENV=production npm run build:oss",
"build:prod:watch": "cross-env NODE_ENV=production ENV=production npm run build:watch",
"build:selfhost": "cross-env SELF_HOST=true npm run build:watch",
"build:selfhost:watch": "cross-env SELF_HOST=true npm run build:watch",
"build:selfhost:prod": "cross-env SELF_HOST=true NODE_ENV=production npm run build",
"build:selfhost:prod:oss": "cross-env SELF_HOST=true NODE_ENV=production npm run build:oss",
"build:selfhost:prod:watch": "cross-env SELF_HOST=true NODE_ENV=production npm run build:watch",
"build:oss": "webpack",
"build:bit": "webpack -c bitwarden_license/webpack.config.js",
"build:oss:watch": "webpack serve",
"build:bit:watch": "webpack serve -c bitwarden_license/webpack.config.js",
"build:bit:dev": "cross-env ENV=development npm run build:bit",
"build:bit:dev:watch": "cross-env ENV=development npm run build:bit:watch",
"build:bit:qa": "cross-env NODE_ENV=production ENV=qa npm run build:bit",
"build:bit:cloud": "cross-env NODE_ENV=production ENV=cloud npm run build:bit",
"build:oss:selfhost:watch": "cross-env ENV=selfhosted npm run build:oss:watch",
"build:bit:selfhost:watch": "cross-env ENV=selfhosted npm run build:bit:watch",
"build:oss:selfhost:prod": "cross-env ENV=selfhosted NODE_ENV=production npm run build:oss",
"build:bit:selfhost:prod": "cross-env ENV=selfhosted NODE_ENV=production npm run build:bit",
"clean:l10n": "git push origin --delete l10n_master",
"dist": "npm run build:prod && gulp postdist",
"dist:oss": "npm run build:prod:oss && gulp postdist",
"dist:selfhost": "npm run build:selfhost:prod && gulp postdist",
"dist:selfhost:oss": "npm run build:selfhost:prod:oss && gulp postdist",
"deploy": "npm run dist && gh-pages -d build",
"deploy:dev": "npm run dist && gh-pages -d build -r git@github.com:kspearrin/bitwarden-web-dev.git",
"lint": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' || true",
"lint:fix": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' --fix"
"dist:bit:cloud": "npm run build:bit:cloud",
"dist:oss:selfhost": "npm run build:oss:selfhost:prod",
"dist:bit:selfhost": "npm run build:bit:selfhost:prod",
"deploy": "npm run dist:bit && gh-pages -d build",
"deploy:dev": "npm run dist:bit && gh-pages -d build -r git@github.com:kspearrin/bitwarden-web-dev.git",
"lint": "eslint . && prettier --check .",
"lint:fix": "eslint . --fix",
"prettier": "prettier --write .",
"prepare": "husky install"
},
"devDependencies": {
"@angular/compiler-cli": "^11.2.11",
"@ngtools/webpack": "^11.2.10",
"@angular/compiler-cli": "^12.2.13",
"@ngtools/webpack": "^12.2.13",
"@types/jquery": "^3.5.5",
"@types/node": "^14.17.2",
"@types/node": "^16.11.12",
"@types/webcrypto": "^0.0.28",
"@types/webpack": "^4.4.27",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^6.4.0",
"@types/webpack": "^5.28.0",
"@typescript-eslint/eslint-plugin": "^5.10.1",
"@typescript-eslint/parser": "^5.10.1",
"autoprefixer": "^10.4.2",
"buffer": "^6.0.3",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^10.0.0",
"cross-env": "^7.0.3",
"css-loader": "^5.2.3",
"del": "^6.0.0",
"file-loader": "^6.2.0",
"css-loader": "^6.5.1",
"eslint": "^8.7.0",
"eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-typescript": "^2.5.0",
"eslint-plugin-import": "^2.25.4",
"gh-pages": "^3.1.0",
"gulp": "^4.0.2",
"gulp-google-webfonts": "^4.0.0",
"html-loader": "^1.3.2",
"html-webpack-plugin": "^4.5.1",
"mini-css-extract-plugin": "^1.5.0",
"html-loader": "^3.0.1",
"html-webpack-injector": "1.1.4",
"html-webpack-plugin": "^5.5.0",
"husky": "^7.0.4",
"lint-staged": "^12.1.2",
"mini-css-extract-plugin": "^2.4.5",
"postcss": "^8.4.6",
"postcss-loader": "^6.2.1",
"prettier": "2.5.1",
"process": "^0.11.10",
"rimraf": "^3.0.2",
"sass": "^1.32.10",
"sass-loader": "^10.1.1",
"style-loader": "^2.0.0",
"tapable": "^1.1.3",
"terser-webpack-plugin": "^4.2.3",
"ts-loader": "^8.1.0",
"tslint": "^6.1.3",
"tslint-loader": "^3.5.4",
"typescript": "4.1.5",
"webpack": "^4.46.0",
"webpack-cli": "^4.6.0",
"webpack-dev-server": "^3.11.2"
"sass-loader": "^12.4.0",
"style-loader": "^3.3.1",
"tailwindcss": "^3.0.18",
"terser-webpack-plugin": "^5.2.5",
"ts-loader": "^9.2.5",
"typescript": "4.3.5",
"util": "^0.12.4",
"webpack": "^5.64.4",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.6.0"
},
"dependencies": {
"@angular/animations": "^12.2.13",
"@angular/cdk": "^12.2.13",
"@angular/common": "^12.2.13",
"@angular/compiler": "^12.2.13",
"@angular/core": "^12.2.13",
"@angular/forms": "^12.2.13",
"@angular/platform-browser": "^12.2.13",
"@angular/platform-browser-dynamic": "^12.2.13",
"@angular/router": "^12.2.13",
"@bitwarden/jslib-angular": "file:jslib/angular",
"@bitwarden/jslib-common": "file:jslib/common",
"angular2-toaster": "11.0.1",
"bootstrap": "4.6.0",
"braintree-web-drop-in": "1.30.1",
"braintree-web-drop-in": "1.33.1",
"browser-hrtime": "^1.1.8",
"core-js": "^3.11.0",
"date-input-polyfill": "^2.14.0",
"font-awesome": "4.7.0",
"jquery": "3.6.0",
"jszip": "^3.7.1",
"ngx-infinite-scroll": "^10.0.1",
"ngx-toastr": "14.1.4",
"node-forge": "^1.3.1",
"popper.js": "1.16.1",
"qrious": "4.0.2",
"rxjs": "^7.4.0",
"sweetalert2": "^10.16.6",
"webcrypto-shim": "0.1.7",
"whatwg-fetch": "3.6.2"
"whatwg-fetch": "3.6.2",
"zone.js": "0.11.4"
},
"engines": {
"node": "~14",
"npm": "~7"
"node": "~16",
"npm": "~8"
},
"lint-staged": {
"./!(jslib)**": "prettier --ignore-unknown --write",
"*.ts": "eslint --fix",
"*.png": "node scripts/optimize.js"
}
}

4
postcss.config.js Normal file
View File

@@ -0,0 +1,4 @@
/* eslint-disable no-undef */
module.exports = {
plugins: [require("tailwindcss"), require("autoprefixer"), require("postcss-nested")],
};

21
scripts/optimize.js Normal file
View File

@@ -0,0 +1,21 @@
const child_process = require("child_process");
const path = require("path");
const images = process.argv.slice(2);
images.forEach((img) => {
switch (img.split(".").pop()) {
case "png":
child_process.execSync(
`npx @squoosh/cli --oxipng {} --output-dir "${path.dirname(img)}" "${img}"`
);
break;
case "jpg":
child_process.execSync(
`npx @squoosh/cli --mozjpeg {"quality":85,"baseline":false,"arithmetic":false,"progressive":true,"optimize_coding":true,"smoothing":0,"color_space":3,"quant_table":3,"trellis_multipass":false,"trellis_opt_zero":false,"trellis_opt_table":false,"trellis_loops":1,"auto_subsample":true,"chroma_subsample":2,"separate_chroma_quality":false,"chroma_quality":75} --output-dir "${path.dirname(
img
)}" "${img}"`
);
break;
}
});

View File

@@ -1,50 +1,52 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="/404/bootstrap.min.css" rel="stylesheet" type="text/css"
integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l">
<link href="/404/font-awesome.min.css" rel="stylesheet" type="text/css"
integrity="sha512-SfTiTlX6kk+qitfevl/7LibUOeJWlt9rbyDn92a1DqWOw9vWG2MFoays0sgObmWazO5BQPiFucnnEAjpAB+/Sw==">
<link href="/404/styles.css" rel="stylesheet" type="text/css">
<link
href="/404/bootstrap.min.css"
rel="stylesheet"
type="text/css"
integrity="sha384-hA/ESrxp2b05ywLtD9YwM6m+pNyLRY4+ruk6dWK00SM4k6SQs0bfrITJVSf6uZyH"
/>
<link href="/404/styles.css" rel="stylesheet" type="text/css" />
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/favicon-16x16.png">
<link rel="mask-icon" href="/images/icons/safari-pinned-tab.svg" color="#175DDC">
<link rel="manifest" href="/manifest.json">
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/favicon-16x16.png" />
<link rel="mask-icon" href="/images/icons/safari-pinned-tab.svg" color="#175DDC" />
<link rel="manifest" href="/manifest.json" />
<title>Page not found!</title>
<meta name="description" content="404 Page Not Found">
</head>
<title>Page not found!</title>
<meta name="description" content="404 Page Not Found" />
</head>
<body>
<div class="banner">
<div class="container inner banner">
<div class="row align-items-center">
<div class="col brand">
<i class="fa fa-shield"></i>&nbsp;
<strong>bit</strong>warden</span>
</div>
</div>
</div>
<body>
<div class="banner">
<div class="container inner banner">
<div class="row align-items-center">
<div class="col brand">
<i class="bwi bwi-shield"></i>&nbsp; <strong>bit</strong>warden
</div>
</div>
<div class="container inner content">
<h2>Page not found!</h2>
<p>Sorry, but the page you were looking for could not be found.</p>
<p>
<a href="/">
<img src="/images/404.png" class="img-fluid" alt="404 image" width="80%"/>
</a>
</p>
<p>You can <a href="/">return to the web vault</a>, check our <a href="https://status.bitwarden.com/">status page</a>
or <a href="https://bitwarden.com/contact/">contact us</a>.</p>
</div>
<div class="container footer text-muted content">
© Copyright 2021 Bitwarden, Inc.
</div>
</body>
</div>
</div>
<div class="container inner content">
<h2>Page not found!</h2>
<p>Sorry, but the page you were looking for could not be found.</p>
<p>
<a href="/">
<img src="/images/404.png" class="img-fluid" alt="404 image" width="80%" />
</a>
</p>
<p>
You can <a href="/">return to the web vault</a>, check our
<a href="https://status.bitwarden.com/">status page</a> or
<a href="https://bitwarden.com/contact/">contact us</a>.
</p>
</div>
<div class="container footer text-muted content">© Copyright 2022 Bitwarden, Inc.</div>
</body>
</html>

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More