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

Compare commits

...

550 Commits

Author SHA1 Message Date
Chad Scharf
d823e8522c Bump version to 2.16.2 (#668) 2020-10-09 10:49:56 -04:00
paulussujono
6bc5ac46b7 ️ added autofocus on first field of modal forms (#667)
added to modals:
- invite user
- add item
- add collection
- add folder
2020-10-06 09:06:44 -04:00
Kyle Spearrin
1193a93f86 map en-IN 2020-09-28 14:21:11 -04:00
Addison Beck
4cd052e009 Default selection plan upgrade fix (#658)
* fixed a broken default selection for plan upgrades

* added a semicolon
2020-09-18 14:15:24 -04:00
Kyle Spearrin
949f61f1a4 bump version 2020-09-15 17:04:21 -04:00
Kyle Spearrin
2145c3f88c language updates 2020-09-15 13:38:46 -04:00
Kyle Spearrin
bb71d5dc0a New Crowdin updates (#655)
* New translations messages.json (French)

* New translations messages.json (Portuguese)

* 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 (Swedish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Polish)

* New translations messages.json (Bulgarian)

* New translations messages.json (Dutch)

* New translations messages.json (Japanese)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (Finnish)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Catalan)

* New translations messages.json (Portuguese, Brazilian)
2020-09-15 12:55:23 -04:00
Chad Scharf
41856ff6af 653 - fix user agent detection for Edge (#654)
* 653 - fix user agent detection for Edge

* Update edge detection to only new version

* update jslib

* update jslib

* fix jslib ref constructor
2020-09-15 10:31:12 -04:00
Addison Beck
a1388ddab7 fixed the cvc learn more link in the payment component (#652) 2020-09-14 15:53:24 -04:00
Kyle Spearrin
b2d13f586d New Crowdin updates (#651)
* New translations messages.json (French)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Swedish)

* New translations messages.json (Turkish)

* New translations messages.json (Ukrainian)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Vietnamese)

* New translations messages.json (Croatian)

* New translations messages.json (Russian)

* New translations messages.json (Estonian)

* New translations messages.json (Latvian)

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

* New translations messages.json (Esperanto)

* New translations messages.json (Malayalam)

* New translations messages.json (Sinhala)

* New translations messages.json (Slovak)

* New translations messages.json (Portuguese)

* New translations messages.json (Spanish)

* New translations messages.json (German)

* New translations messages.json (Afrikaans)

* New translations messages.json (Belarusian)

* New translations messages.json (Bulgarian)

* New translations messages.json (Catalan)

* New translations messages.json (Czech)

* New translations messages.json (Danish)

* New translations messages.json (Greek)

* New translations messages.json (Polish)

* New translations messages.json (Finnish)

* New translations messages.json (Hebrew)

* New translations messages.json (Hungarian)

* New translations messages.json (Italian)

* New translations messages.json (Japanese)

* New translations messages.json (Korean)

* New translations messages.json (Dutch)

* New translations messages.json (Norwegian Bokmal)
2020-09-14 10:49:58 -04:00
Kyle Spearrin
9f0cd586ee only use memory storage for vault data keys (#650)
* only use memory storage for vault data keys

* add lastSync_ to memory storage
2020-09-14 08:35:53 -04:00
Addison Beck
ce67497d3a added localization variable for link sso (#648) 2020-09-11 14:22:56 -04:00
Kyle Spearrin
0dc26e589a New Crowdin updates (#646)
* New translations messages.json (Spanish)

* New translations messages.json (Catalan)

* New translations messages.json (Danish)

* New translations messages.json (German)

* New translations messages.json (Hungarian)

* New translations messages.json (Italian)

* New translations messages.json (Dutch)

* New translations messages.json (Polish)

* New translations messages.json (Portuguese)

* New translations messages.json (Russian)

* New translations messages.json (Swedish)

* New translations messages.json (Ukrainian)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Estonian)

* New translations messages.json (English, United Kingdom)
2020-09-09 16:10:35 -04:00
Kyle Spearrin
e14a676eea switch from session storage to memory storage (#644) 2020-09-08 13:47:20 -04:00
Chad Scharf
11cf89493d form promise added for sso prevalidation (#643) 2020-09-08 12:18:13 -04:00
Kyle Spearrin
5be121ec71 New Crowdin updates (#642)
* New translations messages.json (French)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Swedish)

* New translations messages.json (Turkish)

* New translations messages.json (Ukrainian)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Vietnamese)

* New translations messages.json (Croatian)

* New translations messages.json (Russian)

* New translations messages.json (Estonian)

* New translations messages.json (Latvian)

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

* New translations messages.json (Esperanto)

* New translations messages.json (Malayalam)

* New translations messages.json (Sinhala)

* New translations messages.json (Slovak)

* New translations messages.json (Portuguese)

* New translations messages.json (Spanish)

* New translations messages.json (German)

* New translations messages.json (Afrikaans)

* New translations messages.json (Belarusian)

* New translations messages.json (Bulgarian)

* New translations messages.json (Catalan)

* New translations messages.json (Czech)

* New translations messages.json (Danish)

* New translations messages.json (Greek)

* New translations messages.json (Polish)

* New translations messages.json (Finnish)

* New translations messages.json (Hebrew)

* New translations messages.json (Hungarian)

* New translations messages.json (Italian)

* New translations messages.json (Japanese)

* New translations messages.json (Korean)

* New translations messages.json (Dutch)

* New translations messages.json (Norwegian Bokmal)
2020-09-08 11:29:01 -04:00
Kyle Spearrin
95e58b5e69 update jslib 2020-09-08 11:25:04 -04:00
Chad Scharf
506fd22280 Update jslib - sso pre-validation (#641) 2020-09-08 10:41:24 -04:00
Addison Beck
d79b12dedc updated jslib (#640) 2020-09-08 09:26:50 -04:00
Chad Scharf
599cd7299c bump version (#637) 2020-09-05 21:24:45 -04:00
Addison Beck
18d26b79af updated jslib (#636) 2020-09-04 16:05:42 -04:00
Addison Beck
1f81b81a58 updated jslib (#635) 2020-09-04 14:18:09 -04:00
Kyle Spearrin
cc5e420484 adjust margins 2020-09-04 12:07:14 -04:00
Chad Scharf
b4eaa48765 Updated favicon to new standard (#634) 2020-09-03 17:02:53 -04:00
Addison Beck
76354741be Filter out custom plans from consideration on org create (#631) 2020-09-02 15:53:45 -04:00
Chad Scharf
1b466609f0 SSO pre-validation messages (#628) 2020-08-31 16:48:09 -04:00
Kyle Spearrin
7e11b8bb5a disable certain org settings fields when selfhost (#627) 2020-08-28 11:22:30 -04:00
Vincent Salucci
b251e1f73c [SSO] Add set-password loading placeholder (#626)
* Preparing for new jslib // removed resetMasterPassword variable // Added sync service

* initial commit of loading set password

* Update jslib (e55528e -> 700e945)

* center justify text

* Reverted testing data
2020-08-28 08:56:51 -05:00
Kyle Spearrin
fa11382c08 adjust paths to portal 2020-08-27 16:12:20 -04:00
Addison Beck
e17a49acd5 Sso link existing user (#616)
* created and applied link-sso component

* implemented linking existing user to sso

* removed an unused import

* created and applied link-sso component

* implemented linking existing user to sso

* removed an unused import

* merge

* added a token to the sso linking flow

* [jslib] Update (5d874d0 -> 6ab444a) (#618)

* Update jslib (5d874d0 -> 6ab444a)

* Update dependency flows

* created and applied link-sso component

* implemented linking existing user to sso

* removed an unused import

* merge

* added a token to the sso linking flow

* implemented linking existing user to sso

* removed an unused import

* account for some variable shakeup in jslib for link sso

* updated jslib

* updated jslib

* still trying to fix jslib

* finally, really, truly updated jslib

Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
2020-08-27 11:44:04 -04:00
Addison Beck
bc71ffa6f2 Product description updates (#625)
* updated product messages on org create

* formatted messages.json

* formatted messages.json
2020-08-26 14:44:15 -04:00
Kyle Spearrin
95dc3c92c5 few fixes to plan changes (#624) 2020-08-25 14:21:03 -04:00
Kyle Spearrin
2135accaf4 yoti web support (#623) 2020-08-25 09:25:22 -04:00
Vincent Salucci
429c38fc66 [jslib] Update (5d874d0 -> 6ab444a) (#618)
* Update jslib (5d874d0 -> 6ab444a)

* Update dependency flows
2020-08-21 13:40:48 -05:00
Kyle Spearrin
56e92b1695 cleanup various sso tasks (#617) 2020-08-20 16:39:05 -04:00
Matt Smith
b2685d455b Modifications made to support Browser Extension SSO (#605)
* Update feature/sso jslib 261a200 -> 2e823ea (#589)

* [SSO] Reset master password  (#580)

* Initial commit reset master password (sso)

* Reverted order of two factor/reset password conditional

* Added necessary resetMasterPassword flag for potential entry into RMP flow

* Complete Revamp: Reverted Register // Deleted reset-master-password // updated sso/(settings)change password to use use super class // Adjust routing/messages // Created (accounts) change-password

* Updated button -> Set Master Password

* Refactored change password sub classes to use new submit pattern

* Cleaned import statements

* Update jslib (7fa5178 -> fe167be)

* Update jslib fe167be - >34632e5

* Fixed sso base class import

* merge master

* Fixed missing semicolon // updated jslib to whats in feature/sso

* Fixed two factor formatting

* Added new change password component to app module

* Updated component selector

* updating jslib 34632e5 -> 2e823ea

* Fixed lint warning in two-factor component

Co-authored-by: Kyle Spearrin <kyle.spearrin@gmail.com>

* Update jslib to 101c568 (#594)

* Support for dynamic clientid (#595)

* support third party sso clients

* jslib update

* update jslib

* Modifications made for Browser Extension SSO

* Brought web specific ssocomponent into module

* Removed sso complete transition

* Fixed remaining merge issues

* Removed un-needed block of code.

* Moved processing to sso-connector.

* Removed unused import

* Fixed curly braces..

* Linter fixes

* Aligned verbiage for process message handler

* Lintr fixes

* Firefox can't handle closing the window this way.

* Update sso.ts

Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
Co-authored-by: Kyle Spearrin <kyle.spearrin@gmail.com>
Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com>
2020-08-20 14:30:22 -05:00
Kyle Spearrin
abfd1fa254 abstract set password to jslib (#614) 2020-08-19 11:15:04 -04:00
Oscar Hinton
24a5717e27 Fix @ngtools/webpack version (#613) 2020-08-18 16:19:20 -04:00
Kyle Spearrin
9d9503b00e lock duo sdk to specific commit 2020-08-18 11:28:21 -04:00
Kyle Spearrin
7b0579ccf3 update ngtools for webpack 2020-08-18 11:09:37 -04:00
Kyle Spearrin
df84dff54f jquery types updates 2020-08-18 10:57:42 -04:00
Kyle Spearrin
367c09f7e6 update tsnode 2020-08-18 10:52:45 -04:00
Kyle Spearrin
46967dc126 node types to resolve iterator error 2020-08-18 10:05:19 -04:00
Kyle Spearrin
e0ede7ba74 call api to set password with key parameters (#609)
* call api to set password with key parameters

* update ssoCompleteRegistration string
2020-08-17 15:04:59 -04:00
Oscar Hinton
1fe7554818 Upgrade Angular CDK (#610) 2020-08-17 12:14:55 -04:00
Oscar Hinton
eff3332fef Upgrade Angular to 9 (#606)
* Upgrade Angular to 8

* Upgrade Angular to 9

* Fix format

* Fix import sorting
2020-08-17 10:04:38 -04:00
Kyle Spearrin
caea4775b3 SSO feature (#604)
* Update feature/sso jslib 261a200 -> 2e823ea (#589)

* [SSO] Reset master password  (#580)

* Initial commit reset master password (sso)

* Reverted order of two factor/reset password conditional

* Added necessary resetMasterPassword flag for potential entry into RMP flow

* Complete Revamp: Reverted Register // Deleted reset-master-password // updated sso/(settings)change password to use use super class // Adjust routing/messages // Created (accounts) change-password

* Updated button -> Set Master Password

* Refactored change password sub classes to use new submit pattern

* Cleaned import statements

* Update jslib (7fa5178 -> fe167be)

* Update jslib fe167be - >34632e5

* Fixed sso base class import

* merge master

* Fixed missing semicolon // updated jslib to whats in feature/sso

* Fixed two factor formatting

* Added new change password component to app module

* Updated component selector

* updating jslib 34632e5 -> 2e823ea

* Fixed lint warning in two-factor component

Co-authored-by: Kyle Spearrin <kyle.spearrin@gmail.com>

* Update jslib to 101c568 (#594)

* Support for dynamic clientid (#595)

* support third party sso clients

* jslib update

* update jslib

* Update change-password.component.ts

* Update sso.component.ts

* Update app.module.ts

Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
2020-08-13 14:32:07 -04:00
Addison Beck
5f04950358 Price and Plan Updates (#598)
* added the multi select checkbox to org ciphers

* wired up select all/none

* allowed for bulk delete of ciphers from the org vault

* refactored bulk actions into a dedicated component

* tweaked formatting settings and reformatted files

* moved some shared code to jslib

* some more formatting fixes

* undid jslib connection changes

* removed a function that was moved to jslib

* reset jslib again?

* set up delete many w/admin cipher methods

* removed extra href tags

* added organization id to bulk delete request model when coming from an org vault

* fixed up some compiler warnings for formatting

* updated organization create component to pull list of plans from static store

* wired up the organization create page to new data struct

* continued work on plan updates

* accounted for the subscription screen in plan updates

* adjusted for code review changes from server PR for plan updates

* cleaned up linter errors

* changed a few variable names

* moved price information, added sales tax and subtotal labels

* code review fixups for bulk delete from org vault

* added back a removed parameter from the vault component

* seperated some imports with newlines

* updated jslib

* resolved some build errors

* updated names to reflect server name changes for plan updates

* adjusted logic for using annual total for annual prices in server model

* rearranged an import for the linter

* broke up an async call

* updated organization create component to pull list of plans from static store

* wired up the organization create page to new data struct

* continued work on plan updates

* accounted for the subscription screen in plan updates

* adjusted for code review changes from server PR for plan updates

* cleaned up linter errors

* changed a few variable names

* moved price information, added sales tax and subtotal labels

* updated names to reflect server name changes for plan updates

* adjusted logic for using annual total for annual prices in server model

* rearranged an import for the linter

* broke up an async call

* resolved merge fun

* updated jslib

* made plans a public variable

* removed sales tax hooks

* added a getter for selected plan interval

* went a little too crazy with the interval getter

* formatting

* added a semicolon

* updated jslib

Co-authored-by: Addison Beck <addisonbeck@MacBook-Pro.local>
2020-08-12 17:16:38 -04:00
Kyle Spearrin
c46af91240 implement identifier update in org settings (#601) 2020-08-12 16:46:18 -04:00
Oscar Hinton
f5034effd2 Upgrade TypeScript (#600)
* Upgrade typescript to 3.6.5.

* Resolve compile error and warnings
2020-08-12 15:43:26 -04:00
Addison Beck
20408347fb Allow Bulk Delete In Org Vault (#577)
* added the multi select checkbox to org ciphers

* wired up select all/none

* allowed for bulk delete of ciphers from the org vault

* refactored bulk actions into a dedicated component

* tweaked formatting settings and reformatted files

* moved some shared code to jslib

* some more formatting fixes

* undid jslib connection changes

* removed a function that was moved to jslib

* reset jslib again?

* set up delete many w/admin cipher methods

* removed extra href tags

* added organization id to bulk delete request model when coming from an org vault

* fixed up some compiler warnings for formatting

* code review fixups for bulk delete from org vault

* added back a removed parameter from the vault component

* seperated some imports with newlines

* updated jslib

* resolved some build errors

* code review cleanup for bulk delete from an org vault

* code review cleanup for bulk delete from an org vault

* code review cleanup for bulk delete from an org vault

* code review cleanup for bulk delete from an org vault

* updated jslib to latest

Co-authored-by: Addison Beck <addisonbeck@MacBook-Pro.local>
2020-08-11 11:30:30 -04:00
Kyle Spearrin
49d5bfd3e7 update jslib 2020-08-03 15:25:24 -04:00
Vincent Salucci
e99d1a74fd update jslib (f301b92 -> 101c568) (#593) 2020-08-03 07:56:47 -05:00
Vincent Salucci
43d1cede98 update jslib (261a200 -> f301b92) (#590) 2020-08-01 17:18:18 -05:00
Vincent Salucci
091fc93645 update jslib (fe167be -> 261a200) (#588) 2020-07-31 13:54:13 -05:00
Kyle Spearrin
dfe2771ba7 Taiwan 2020-07-31 06:38:25 -04:00
Kyle Spearrin
d3664321fd fix download link 2020-07-28 22:52:13 -04:00
Chad Scharf
2e01ff7826 Fix modal-body div missing #583 (#585)
* Fix modal-body div missing #583

* Revert "Fix modal-body div missing #583"

This reverts commit 38f0cae82d.

* Fixing modal-body div missing #583
2020-07-28 09:55:59 -04:00
Vincent Salucci
59d5a7439d Update jslib 7fa5178 -> fe167be (#584) 2020-07-27 13:20:13 -05:00
K. Sasa
6e3edd75eb Consistent: Replaced the clipboard icon with a clone icon to improve UX (#582)
* Replace copy value button fa-clipboard to fa-clone

* Replace clone item button fa-clone to fa-files-o
2020-07-27 13:21:11 -04:00
Oscar Hinton
78992444bf Support biometric changes in jslib (#571) 2020-07-24 14:39:39 -04:00
Chad Scharf
f1dea8fb1a Transition reference id to data (#578)
* Transition reference id to data

* reference event request model change
2020-07-21 10:43:38 -04:00
Kyle Spearrin
04e5ab0d01 update jslib 2020-07-16 10:56:54 -04:00
Kyle Spearrin
22a1cef498 SSO support (#575)
* support for sso

* resetMasterPassword

* update jslib

* [Enterprise] Added button to launch portal (#570)

* initial commit

* Added Enterprise button and used new business portal bool

* Reverting services module local changes

* Formatted some new lines

* Closed alerts on lock (#572)

Co-authored-by: Addison Beck <addisonbeck@MacBook-Pro.local>

* Updated enterprise URL dev (port) (#574)

Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
Co-authored-by: Addison Beck <addisonbeck1@gmail.com>
Co-authored-by: Addison Beck <addisonbeck@MacBook-Pro.local>
2020-07-16 09:18:25 -04:00
Kyle Spearrin
98eaeddbfd update jslib 2020-07-16 09:16:20 -04:00
Vincent Salucci
00e4df2dd3 Updated enterprise URL dev (port) (#574) 2020-07-14 09:12:49 -05:00
Addison Beck
cfb4133152 Closed alerts on lock (#572)
Co-authored-by: Addison Beck <addisonbeck@MacBook-Pro.local>
2020-07-09 15:11:28 -05:00
Vincent Salucci
42361d17b5 [Enterprise] Added button to launch portal (#570)
* initial commit

* Added Enterprise button and used new business portal bool

* Reverting services module local changes

* Formatted some new lines
2020-07-07 13:32:22 -05:00
Vincent Salucci
02ee95506c Update jslib (57ace40 -> d308245) (#569) 2020-07-07 09:46:44 -05:00
Kyle Spearrin
a749946457 bump version 2020-06-29 11:31:52 -04:00
Kyle Spearrin
18fb86c243 New Crowdin updates (#565)
* New translations messages.json (French)

* New translations messages.json (Portuguese)

* New translations messages.json (Estonian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Ukrainian)

* New translations messages.json (Swedish)

* New translations messages.json (Russian)

* New translations messages.json (Polish)

* New translations messages.json (Spanish)

* New translations messages.json (Dutch)

* New translations messages.json (Japanese)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (Danish)

* New translations messages.json (Catalan)

* New translations messages.json (Bulgarian)

* New translations messages.json (English, United Kingdom)
2020-06-29 11:30:21 -04:00
Kyle Spearrin
7597e4006c New Crowdin updates (#564)
* New translations messages.json (French)

* New translations messages.json (Portuguese)

* New translations messages.json (Sinhala)

* New translations messages.json (Esperanto)

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

* New translations messages.json (Estonian)

* New translations messages.json (Croatian)

* New translations messages.json (Portuguese, Brazilian)

* 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 (Swedish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Polish)

* New translations messages.json (Spanish)

* New translations messages.json (German)

* New translations messages.json (Afrikaans)

* New translations messages.json (Belarusian)

* New translations messages.json (Bulgarian)

* New translations messages.json (Catalan)

* New translations messages.json (Czech)

* New translations messages.json (Danish)

* New translations messages.json (Greek)

* New translations messages.json (Dutch)

* New translations messages.json (Finnish)

* New translations messages.json (Hebrew)

* New translations messages.json (Hungarian)

* New translations messages.json (Italian)

* New translations messages.json (Japanese)

* New translations messages.json (Korean)

* New translations messages.json (Norwegian Bokmal)
2020-06-26 21:24:31 -04:00
Chad Scharf
50be5f4895 Merge pull request #563 from bitwarden/v2.15.0
version bump
2020-06-25 17:20:47 -04:00
Chad Scharf
326fb47593 version bump 2020-06-25 17:02:17 -04:00
Chad Scharf
240c576bad Merge branch 'feature/reference-id' 2020-06-25 16:36:28 -04:00
Chad Scharf
88c8c8ae55 referenceId PR feedback + lint fix 2020-06-25 16:30:45 -04:00
Kyle Spearrin
394a7e42fb a few tweaks for hidden passwords (#561)
* a few tweaks for hidden passwords

* revert org layout changes

* revert column size change
2020-06-25 15:55:50 -04:00
Chad Scharf
869ee217eb Updated jslib 2020-06-25 15:51:26 -04:00
Chad Scharf
03dbe272fc Added referenceId to register component 2020-06-25 15:18:21 -04:00
Chad Scharf
87973e9775 Merge pull request #558 from bitwarden/feature/tax-info-collection
Feature/tax info collection
2020-06-18 11:33:08 -04:00
Chad Scharf
4450b1aa81 update jslib 2020-06-18 11:30:36 -04:00
Chad Scharf
57575ea322 Merge pull request #487 from clayadams5226/patch-1
Update ISSUE_TEMPLATE.md
2020-06-18 09:29:35 -04:00
Chad Scharf
68d3d7abfd Combine tax info with other updates 2020-06-17 20:11:30 -04:00
Chad Scharf
4502a966a1 PR feedback, loading spinner 2020-06-17 13:35:39 -04:00
Chad Scharf
e523733b2c Merge branch 'feature/tax-info-collection' of https://github.com/bitwarden/web into feature/tax-info-collection
Merge conflicts and jslib update
2020-06-17 13:21:43 -04:00
Chad Scharf
3864f1d950 Revert services.module.ts 2020-06-17 13:20:06 -04:00
Chad Scharf
4bdb9c8632 Collect tax info for payments 2020-06-17 13:20:06 -04:00
Chad Scharf
b1c098614c tax info collection zip + VAT 2020-06-17 13:20:06 -04:00
Vincent Salucci
4309064804 [Enterprise] Added environment checks (#559)
* Update jslib (2b6657a -> 28d21ca)

* Environment variable checks
2020-06-16 09:35:25 -05:00
Chad Scharf
f91e67ad6b Revert services.module.ts 2020-06-12 19:51:00 -04:00
Chad Scharf
d63ec210c7 Collect tax info for payments 2020-06-12 19:33:29 -04:00
Chad Scharf
3d160ee1df Merge pull request #538 from Hinton/feature/hide-passwords
Add support for collections with hide passwords
2020-06-11 14:51:05 -04:00
Hinton
51b482f57d Merge branch 'master' of https://github.com/bitwarden/web into feature/hide-passwords 2020-06-11 20:33:43 +02:00
Hinton
b367c4b4ce Update jslib 2020-06-11 20:28:22 +02:00
Chad Scharf
7432ad310c Merge pull request #557 from bitwarden/feature/enterprise-landing-page
Layout images and styling for register page
2020-06-11 12:04:18 -04:00
Chad Scharf
5b02202efb Llayout images and styling for register page 2020-06-11 11:27:46 -04:00
Chad Scharf
23056bcd63 tax info collection zip + VAT 2020-06-08 17:24:05 -04:00
Kyle Spearrin
2b0c92a4ea stub alternate layout support for register page (#550) 2020-06-04 14:12:37 -04:00
Kyle Spearrin
d669d43fe4 update jslib 2020-06-04 12:48:11 -04:00
hinton
426e0edfb5 Allow editing of newly added fields 2020-06-03 20:46:32 +02:00
Kyle Spearrin
2cc0aa6f3d a few cleanup items for full width setting change (#547) 2020-06-02 09:56:16 -04:00
Chad Scharf
f895916fbb Merge pull request #543 from syntax-error752/feature/UIscaling
Update CSS to allow for larger screens.
2020-06-01 19:27:28 -04:00
syntaxerror752
fea3bba0df Changed method of keeping the logon box the same size 2020-06-02 08:29:58 +10:00
hinton
7ed7321219 Mark "hidden" fields and totp as disabled. 2020-06-01 21:59:58 +02:00
hinton
b2bf192677 Merge branch 'master' of https://github.com/bitwarden/web into feature/hide-passwords 2020-06-01 21:38:17 +02:00
syntaxerror752
d323e775ca Removed the need for the messageing service to be in app.component.ts 2020-05-31 22:02:41 +10:00
syntaxerror752
22a00b2341 Added toggle full width function
Added toggle full width function.
Added messaging service to trigger function.
Added CSS to keep login box the same size.
2020-05-30 18:30:41 +10:00
syntaxerror752
f36bba6406 Revert last commit due to requested changes
Revert last commit due to requested changes.
Renamed variable.
2020-05-30 11:12:15 +10:00
syntaxerror752
674c583881 Update HTML and TS scripts for UI scaling 2020-05-29 23:08:03 +10:00
syntaxerror752
eb5ad7c6dc Added UI scaling tickbox to options menu 2020-05-29 21:28:26 +10:00
Kyle Spearrin
ca771eb04c New Crowdin translations (#546)
* New translations messages.json (Czech)

* New translations messages.json (Danish)

* New translations messages.json (Portuguese)

* New translations messages.json (Chinese Simplified)
2020-05-28 20:06:27 -04:00
Vincent Salucci
d705b8ab33 Update jslib 2858724 -> 212a2e3 (#545) 2020-05-28 13:58:49 -05:00
Kyle Spearrin
9454eda082 New Crowdin translations (#544)
* New translations messages.json (Afrikaans)

* New translations messages.json (German)

* New translations messages.json (Finnish)

* New translations messages.json (Italian)

* New translations messages.json (Polish)

* New translations messages.json (English, United Kingdom)
2020-05-26 10:31:40 -04:00
hinton
7d5329e186 Add hide password checkboxes to add/edit collection. 2020-05-23 11:15:23 +02:00
Kyle Spearrin
18979a7f1a Add support for greek language (#541) 2020-05-22 23:14:26 -04:00
Vincent Salucci
7301158e54 [Paging] Added for Organization Users, Pages, and Collections (#539)
* Updating jslib

* Added paging for Organizational Users, Groups, and Collections

* Updated jslib fb7335b -> 2858724
2020-05-22 11:26:43 -05:00
hinton
5b9c41f29a Use correct variable for view password 2020-05-22 09:26:57 +02:00
Kyle Spearrin
179884cf93 New translations messages.json (German) (#540) 2020-05-21 15:08:42 -04:00
hinton
5bc01ea13e Add support for collections with hide passwords 2020-05-21 15:58:55 +02:00
Kyle Spearrin
ca43db8d93 New Crowdin translations (#537)
* New translations messages.json (French)

* New translations messages.json (Dutch)

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

* New translations messages.json (Estonian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Ukrainian)

* New translations messages.json (Swedish)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Korean)

* New translations messages.json (Spanish)

* New translations messages.json (Japanese)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (Hebrew)

* New translations messages.json (Finnish)

* New translations messages.json (Greek)

* New translations messages.json (German)

* New translations messages.json (Czech)

* New translations messages.json (Catalan)

* New translations messages.json (Bulgarian)

* New translations messages.json (Norwegian Bokmal)
2020-05-21 09:48:59 -04:00
Kyle Spearrin
f4cb5e6632 update jslib 2020-05-20 15:42:51 -04:00
Kyle Spearrin
da2e740e65 bump version 2020-05-18 22:02:44 -04:00
Kyle Spearrin
2f0d2bdf32 New Crowdin translations (#533)
* New translations messages.json (French)

* New translations messages.json (Polish)

* New translations messages.json (Esperanto)

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

* New translations messages.json (Estonian)

* New translations messages.json (Croatian)

* New translations messages.json (Portuguese, Brazilian)

* 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 (Swedish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese)

* New translations messages.json (Dutch)

* New translations messages.json (Spanish)

* New translations messages.json (Korean)

* New translations messages.json (Japanese)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (Hebrew)

* New translations messages.json (Finnish)

* New translations messages.json (Greek)

* New translations messages.json (German)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Catalan)

* New translations messages.json (Bulgarian)

* New translations messages.json (Belarusian)

* New translations messages.json (Afrikaans)

* New translations messages.json (Norwegian Bokmal)
2020-05-18 15:56:43 -04:00
Kyle Spearrin
97eedb2034 fixing a few bug, asset updates, tweaks (#532)
* fixing a few bug, asset updates, tweaks

* dont save until save button clicked
2020-05-18 09:51:20 -04:00
Kyle Spearrin
3ac46e62cb updated formatting 2020-05-08 11:54:49 -04:00
Srdjan Milic
97db3635af fix: webpack.config.js plugin (#525) 2020-05-08 11:42:28 -04:00
Chad Scharf
e3464da19a Merge pull request #527 from bitwarden/soft-delete
Soft delete feature
2020-05-08 11:17:02 -04:00
Chad Scharf
ec3ee8fbb3 Merge branch 'master' into soft-delete 2020-05-08 09:32:59 -04:00
Kyle Spearrin
96208d3760 brand color updates 2020-05-05 16:59:33 -04:00
Kyle Spearrin
5bb61c0730 color updates + jslib 2020-05-05 16:32:45 -04:00
Srdjan Milic
858f86d9df fix: package.json info (#523) 2020-05-01 12:21:23 -04:00
Vincent Salucci
aa1e5a11ad [Auto Logout] Added warning dialog for log out action (#518)
* Added warning dialog for log out timeout action

* Reverting testing service module endpoints
2020-04-25 08:13:30 -05:00
Kyle Spearrin
ded8865914 Null check allUsers (#515) 2020-04-20 00:17:06 -04:00
Kyle Spearrin
da1437a268 update lunr types (#514) 2020-04-14 15:55:22 -04:00
Chad Scharf
599f831a09 Merge pull request #513 from bitwarden/soft-delete-toast
[Soft Delete] - Deleted message (sent to trash)
2020-04-14 15:19:12 -04:00
Chad Scharf
23b532e2bf [Soft Delete] - Deleted message (sent to trash) 2020-04-14 15:06:54 -04:00
Chad Scharf
9f1b8ae58f Merge pull request #511 from bitwarden/soft-delete-chad
[Soft Delete] - Added trash and related functionality to web vault
2020-04-10 13:56:33 -04:00
Chad Scharf
d62850f82d [Soft Delete] enable copy/view operations in trash 2020-04-10 13:42:37 -04:00
Chad Scharf
41a0cfd0a2 [Soft Delete] - Added trash and related functionality to web vault 2020-04-08 16:48:30 -04:00
Vincent Salucci
fb6e85c56b Update jslib (28e3fff -> 72e3893) (#510)
* Update jslib (28e3fff -> 72e3893)

* Updated lock description, updated vaultTimeoutService init

Co-authored-by: Vincent Salucci <vsalucci@bitwarden.com>
2020-04-06 13:07:09 -05:00
Vincent Salucci
d58550c2b8 [Auto-Logout] Implement upstream changes (#506)
* Initial commit of auto logout functionality

* Update jslib 31a2574 -> 28e3fff

* Reverting prod URLs

* Set log out expired param to false

Co-authored-by: Vincent Salucci <vsalucci@bitwarden.com>
2020-03-30 09:59:47 -05:00
Kyle Spearrin
5bf3ca2708 New Crowdin translations (#505)
* New translations messages.json (Hebrew)

* New translations messages.json (Ukrainian)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Korean)

* New translations messages.json (Japanese)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (Greek)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

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

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Catalan)

* New translations messages.json (Bulgarian)
2020-03-24 14:50:53 -04:00
Kyle Spearrin
3e4a7e7a56 version bump 2020-03-21 00:55:44 -04:00
Kyle Spearrin
5d17de227b update jslib 2020-03-21 00:20:06 -04:00
Vincent Salucci
0d985c0221 Update jslib (0a30c7e -> 3ad546c) (#500)
Co-authored-by: Vincent Salucci <vsalucci@bitwarden.com>
2020-03-18 13:06:06 -05:00
Kyle Spearrin
eaa6bc12ce bump version 2020-03-12 21:16:53 -04:00
Kyle Spearrin
b3337df774 New Crowdin translations (#492)
* New translations messages.json (Afrikaans)

* New translations messages.json (Hebrew)

* New translations messages.json (Ukrainian)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Korean)

* New translations messages.json (Japanese)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (Greek)

* New translations messages.json (Belarusian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

* New translations messages.json (Esperanto)

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

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Croatian)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Catalan)

* New translations messages.json (Bulgarian)

* New translations messages.json (Vietnamese)
2020-03-12 21:14:55 -04:00
Kyle Spearrin
6c8c5bcde6 update jslib 2020-03-12 20:27:50 -04:00
Vincent Salucci
d255f6add4 Enforce passphrase policy (#490)
* Update jslib and initial commit for passphrase policy

* Removed unused strings

* Pulling in latest jslib (44b86f5 -> 36241e9)

* Made revision requests

Co-authored-by: Vincent Salucci <vsalucci@bitwarden.com>
2020-03-11 10:35:12 -05:00
brunohunziker
84dde72990 Total number of organization users added (#489) 2020-03-10 12:06:25 -04:00
Kyle Spearrin
5dfeee548d New Crowdin translations (#485)
* New translations messages.json (Turkish)

* New translations messages.json (Polish)

* New translations messages.json (German)

* New translations messages.json (Ukrainian)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Greek)

* New translations messages.json (German)

* New translations messages.json (Estonian)

* New translations messages.json (Danish)

* New translations messages.json (Bulgarian)
2020-03-06 11:28:34 -05:00
Clayton
fba2102518 Update ISSUE_TEMPLATE.md
Added a uniform template to be used for all issues that are reported.
2020-03-06 08:27:39 -05:00
Kyle Spearrin
09516b4d4e init masterPassMinComplexity as null 2020-03-05 22:19:46 -05:00
Kyle Spearrin
b7b74d8f1f New Crowdin translations (#484)
* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Japanese)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Catalan)
2020-03-05 11:51:31 -05:00
Kyle Spearrin
80d3cd3126 New Crowdin translations (#483)
* New translations messages.json (Portuguese)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (Dutch)

* New translations messages.json (Chinese Simplified)
2020-03-05 11:47:32 -05:00
Kyle Spearrin
bbd416ba24 New Crowdin translations (#482)
* New translations messages.json (Afrikaans)

* New translations messages.json (Hebrew)

* New translations messages.json (Ukrainian)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Korean)

* New translations messages.json (Japanese)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (Greek)

* New translations messages.json (Belarusian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

* New translations messages.json (Esperanto)

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

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Croatian)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Catalan)

* New translations messages.json (Bulgarian)

* New translations messages.json (Vietnamese)

* New translations messages.json (Portuguese)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Hungarian)

* New translations messages.json (Dutch)
2020-03-05 09:38:47 -05:00
Kyle Spearrin
75563660f0 update package lock 2020-03-05 09:22:08 -05:00
Kyle Spearrin
c2197bcc53 bump version 2020-03-05 09:18:04 -05:00
Kyle Spearrin
12114c786b add select option to password score list 2020-03-04 16:00:23 -05:00
Kyle Spearrin
73c192ad18 update jslib and swal2 styles 2020-03-04 14:14:27 -05:00
Kyle Spearrin
465564325e remove p tags 2020-03-04 10:57:00 -05:00
Vincent Salucci
7c0d093be5 Fix password score display switch statement (#481) 2020-03-04 09:21:52 -06:00
Kyle Spearrin
a1fbe6b970 no bottom margin 2020-03-03 23:23:40 -05:00
Vincent Salucci
305d86f765 Update complexity score display (#479)
* Update complexity score

* Simplifying switch statement
2020-03-03 15:37:54 -06:00
Vincent Salucci
e7e5816ded Enforce Master Password Policies (Change/Register) (#478)
* Initial commit for change password mp policy enforcement

* Initial commit of mp policy for registering

* Testing Register component

* Final testing complete

* Reverting service module URLs

* Requested changes and build fix

* Updated submit function
2020-03-03 10:20:28 -06:00
Vincent Salucci
cd9b1b906c Update jslib (6210396 -> da9b9b4) (#477) 2020-03-02 14:25:00 -06:00
MartB
0b5a74aa9f sweetalert: ported to sweetalert2 and simplified code. (#465)
No styling changes besides making the "primary" button-text bold (aligned with desktop app)
2020-03-02 13:52:09 -05:00
Vincent Salucci
c3407ac35a Update jslib (6c52942..6210396) (#476) 2020-03-02 11:40:58 -06:00
Kyle Spearrin
c9699647d7 load policies on register if org invite (#475) 2020-03-02 11:51:05 -05:00
Kyle Spearrin
aac011d3b3 react to jslib #77 changes (#474) 2020-02-28 16:57:54 -05:00
Kyle Spearrin
e2108ff85b Show reason for invite accept failure if available (#473) 2020-02-28 15:27:02 -05:00
Vincent Salucci
5c492f893b Show policy in effect banner for password generator (#472)
* Show Password Generator Policy in effect banner

* Extra character cleanup

* Updated back to base setUrls

* Updated app-callout class to info
2020-02-28 13:48:48 -06:00
Vincent Salucci
2877b3c63d Update jslib (862057d -> 6c52942) (#471) 2020-02-28 11:15:43 -06:00
Kyle Spearrin
1d94185078 Add copy descriptions and warnings to policies (#470) 2020-02-27 13:07:33 -05:00
Vincent Salucci
a27eddae56 Enforce Password Generator Policy Options (#469)
* Initial commit for enforcing password generator policy options

* Revert to previous isDev URL setup
2020-02-26 18:32:57 -06:00
Vincent Salucci
5ed830205d Update jslib (98ae9b0 -> 862057d) (#468) 2020-02-26 17:04:58 -06:00
Kyle Spearrin
aeca6f04f9 encryptr instructions 2020-02-24 23:19:57 -05:00
Kyle Spearrin
c099ff7662 encryptr importer 2020-02-20 16:55:10 -05:00
Kyle Spearrin
83ba366558 Admin config for master password policy (#463)
* Admin config for master password policy

* UI cleanup and master pass options improvements

* ui tweaks
2020-02-19 21:25:46 -05:00
Vincent Salucci
6129fdb6e5 Update jslib (fd260df -> 98ae9b0) (#464) 2020-02-19 14:03:42 -06:00
Kyle Spearrin
8db66bf282 bitwarden inc. 2020-02-18 22:25:04 -05:00
Vincent Salucci
b7cd18b715 Allow organizational admins to assign clone ownership (#458) 2020-02-12 15:11:38 -06:00
Kyle Spearrin
6ed991593a deploy on master 2020-02-12 15:39:48 -05:00
Vincent Salucci
ccf3d49fc4 Implement Clone item functionality (personal/org) (#457)
* Clone personal/org items

* Removed ability to delete during clone process
2020-02-10 14:03:36 -05:00
Kyle Spearrin
7e95e44f1d add support for check payment method type 2020-02-07 16:48:46 -05:00
Vincent Salucci
a5de11d002 Update jslib (bb459ce -> 3b8df85) (#455) 2020-02-07 11:12:57 -05:00
Vincent Salucci
756bd82a46 Update jslib (3a40cb8 -> bb459ce) (#452) 2020-02-04 23:50:53 -05:00
Vincent Salucci
f9ce4a2f81 Update jslib (3d2e2cb -> 3a40cb8) and npm sub:pull script (#450) 2020-02-03 23:10:47 -05:00
Kyle Spearrin
088301c4be configure some policy data 2020-01-29 17:49:20 -05:00
Kyle Spearrin
f7f70408c9 update jslib and construct policy service 2020-01-28 22:42:20 -05:00
Kyle Spearrin
292d713423 symlink jslib on mac/lin 2020-01-27 12:46:25 -05:00
Kyle Spearrin
e02eadc9f7 remove angualr http and upgrade node-sass 2020-01-27 09:04:07 -05:00
Naoaki Iwakiri
6e66df59b7 Stop showing score 3 passwords as weak passwords (#445) 2020-01-27 08:30:19 -05:00
Kyle Spearrin
00b9f4cab6 set policy data to null 2020-01-20 08:59:06 -05:00
Kyle Spearrin
f6fb56229e policy edit 2020-01-20 08:57:55 -05:00
Kyle Spearrin
5b770084c9 policies enabled badge 2020-01-15 17:05:08 -05:00
Kyle Spearrin
a2472e0cf5 stub out policies management page 2020-01-15 16:51:42 -05:00
Kyle Spearrin
4de7b52044 stub out policies menu 2020-01-15 15:42:30 -05:00
Kyle Spearrin
1e100d1bf1 list height fix 2020-01-13 08:45:08 -05:00
Kyle Spearrin
d00fb9e0a5 update jslib 2020-01-13 07:49:16 -05:00
Kyle Spearrin
f5d8673ad4 update signalr client 2020-01-09 17:30:35 -05:00
Kyle Spearrin
bd2cba1f31 make hidden options button more accessible 2020-01-08 09:46:27 -05:00
Kyle Spearrin
45c07b7c39 Append copy textarea to model for all browsers 2019-12-31 14:35:58 -05:00
Kyle Spearrin
36244d58aa avast json importer 2019-12-20 13:30:01 -05:00
Kyle Spearrin
e968d5a2a5 update jslib 2019-11-26 08:35:08 -05:00
Kyle Spearrin
84df9cca87 codebook csv importer 2019-11-25 16:10:55 -05:00
Kyle Spearrin
e550989ce2 autocomplete off for search inputs 2019-11-25 08:20:53 -05:00
Kyle Spearrin
94edc1e284 fix twofactorauth.org API path. resolves #422 2019-10-29 08:04:09 -04:00
Kyle Spearrin
b9f8cad578 npm i 2019-10-21 08:48:41 -04:00
Kyle Spearrin
02eb382ae7 show enabled check always when 2fa enabled 2019-10-14 16:44:38 -04:00
Kyle Spearrin
1ecc092f08 update jslib 2019-10-11 13:38:51 -04:00
Kyle Spearrin
191fa922d2 more a11y updates 2019-10-11 11:47:41 -04:00
Kyle Spearrin
fb817f1ca7 more a11y fixes 2019-10-11 11:22:21 -04:00
Kyle Spearrin
9c2f128585 ally title work 2019-10-11 10:35:24 -04:00
Kyle Spearrin
9ebd700317 sr text for shared and attachments 2019-10-10 11:42:05 -04:00
Kyle Spearrin
9ab6cf31fd npm audit fix 2019-10-07 15:00:21 -04:00
Kyle Spearrin
bb5c114b8d New translations messages.json (Dutch) (#418) 2019-10-04 21:12:34 -04:00
Kyle Spearrin
1f2a724d32 New Crowdin translations (#417)
* New translations messages.json (Czech)

* New translations messages.json (Dutch)

* New translations messages.json (Hungarian)

* New translations messages.json (Portuguese, Brazilian)
2019-10-04 15:38:49 -04:00
Kyle Spearrin
9b28203757 buttercup csv importer 2019-10-04 10:11:34 -04:00
Kyle Spearrin
ac9f30f5f0 New Crowdin translations (#416)
* New translations messages.json (Bulgarian)

* New translations messages.json (Hebrew)

* New translations messages.json (Ukrainian)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Korean)

* New translations messages.json (Japanese)

* New translations messages.json (Italian)

* New translations messages.json (Greek)

* New translations messages.json (Danish)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Esperanto)

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

* New translations messages.json (Czech)

* New translations messages.json (Croatian)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Catalan)

* New translations messages.json (Afrikaans)

* New translations messages.json (Hungarian)

* New translations messages.json (Estonian)

* New translations messages.json (Dutch)

* New translations messages.json (Vietnamese)
2019-10-02 10:35:45 -04:00
Kyle Spearrin
b13b0a66ce symlink 2019-09-25 16:10:13 -04:00
Kyle Spearrin
fcfdd5bc76 update jslib 2019-09-24 16:03:36 -04:00
Kyle Spearrin
cdbbc37d59 update jslib 2019-09-23 14:39:40 -04:00
Kyle Spearrin
4ba4af7cf9 bump version 2019-09-20 07:48:53 -04:00
Kyle Spearrin
89708d1fd6 limit sub and billing actions when using iap 2019-09-19 16:34:44 -04:00
Kyle Spearrin
6cb48c186e restrict changing payment method with iap 2019-09-19 15:46:33 -04:00
Kyle Spearrin
a1c9c47c89 blackberry importer 2019-09-11 17:08:33 -04:00
Kyle Spearrin
85cc2865b6 show locale name for language selection 2019-09-06 09:33:35 -04:00
Kyle Spearrin
2dc74b26f3 version bump 2019-08-30 14:19:52 -04:00
Kyle Spearrin
3d0ed43920 update jslib 2019-08-29 10:02:39 -04:00
Kyle Spearrin
dc54943a19 added hebrew language 2019-08-29 07:20:36 -04:00
Kyle Spearrin
c6ae5368fe securesafe and logmeonce csv importers 2019-08-26 10:12:36 -04:00
Kyle Spearrin
c947354517 locale string typo 2019-08-23 07:58:42 -04:00
Kyle Spearrin
076f01b65f data port 2019-08-20 17:23:27 -04:00
Kyle Spearrin
e37292a276 isViewOpen returns promise 2019-08-20 13:47:58 -04:00
Kyle Spearrin
7d76473580 sca card failure warning 2019-08-10 19:51:49 -04:00
Kyle Spearrin
8bafbbd2ff handle seats and storage adjustment for sca 2019-08-10 13:43:47 -04:00
Kyle Spearrin
80c5dff5ad adjust storage with payment intent/method handling 2019-08-10 13:00:07 -04:00
Kyle Spearrin
a4571a2617 handleCardPayment for incomplete payments 2019-08-09 23:57:30 -04:00
Kyle Spearrin
18608a8b63 fix lint issue 2019-08-09 11:14:46 -04:00
Kyle Spearrin
c9116ad7ab update jslib 2019-08-03 19:59:25 -04:00
Kyle Spearrin
d982902986 update jslib 2019-08-02 09:51:11 -04:00
Kyle Spearrin
3ab6868460 New Crowdin translations (#404)
* New translations messages.json (Afrikaans)

* New translations messages.json (Hungarian)

* New translations messages.json (Ukrainian)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Korean)

* New translations messages.json (Japanese)

* New translations messages.json (Italian)

* New translations messages.json (Hebrew)

* New translations messages.json (Catalan)

* New translations messages.json (Greek)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

* New translations messages.json (Esperanto)

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

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Croatian)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Vietnamese)

* New translations messages.json (Portuguese)
2019-07-26 21:45:48 -04:00
Kyle Spearrin
8d5974d0f8 docker health check 2019-07-26 11:59:59 -04:00
Kyle Spearrin
35a64afdf9 bump version 2019-07-25 20:48:14 -04:00
Kyle Spearrin
1ed850324d upgrade signalr libs 2019-07-25 20:42:26 -04:00
Kyle Spearrin
8f886df84f setComponentParameters for modal 2019-07-25 12:24:32 -04:00
Kyle Spearrin
55481b255b exportedOrganizationVault l10n 2019-07-12 17:15:40 -04:00
Kyle Spearrin
b0b9d8445e export vault event 2019-07-12 17:11:50 -04:00
Kyle Spearrin
3a2f04006f event log from copy on listing 2019-07-12 15:34:27 -04:00
Kyle Spearrin
1aacd4ece1 add ref for event service 2019-07-12 11:05:30 -04:00
Kyle Spearrin
f0e3e3b6f9 client events for edit page 2019-07-12 10:41:18 -04:00
Kyle Spearrin
26533713ff update jslib 2019-07-12 00:01:14 -04:00
Kyle Spearrin
b55d54eb5b syb out event log processing and event list desc 2019-07-11 22:03:12 -04:00
Kyle Spearrin
01cb57c9fb allow 2 mil KDF iterations 2019-07-06 23:35:38 -04:00
Kyle Spearrin
eb85464f8d New Crowdin translations (#398)
* New translations messages.json (English, United Kingdom)

* New translations messages.json (Estonian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Spanish)

* New translations messages.json (Polish)

* New translations messages.json (Catalan)

* New translations messages.json (Dutch)

* New translations messages.json (Japanese)

* New translations messages.json (Japanese)

* New translations messages.json (French)

* New translations messages.json (French)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Dutch)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Spanish)

* New translations messages.json (Italian)

* New translations messages.json (French)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Czech)

* New translations messages.json (French)

* New translations messages.json (Ukrainian)
2019-07-04 08:44:01 -04:00
Kyle Spearrin
d30fcf8dca New Crowdin translations (#397)
* New translations messages.json (Afrikaans)

* New translations messages.json (Hungarian)

* New translations messages.json (Ukrainian)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Korean)

* New translations messages.json (Japanese)

* New translations messages.json (Italian)

* New translations messages.json (Hebrew)

* New translations messages.json (Catalan)

* New translations messages.json (Greek)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

* New translations messages.json (Esperanto)

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

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Croatian)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Vietnamese)
2019-07-03 15:15:38 -04:00
Kyle Spearrin
004d14eaf4 fix html for checkbox on gen 2019-07-02 17:01:19 -04:00
Kyle Spearrin
d1a7c3390a capitalize and include num for pass gen 2019-07-02 16:54:46 -04:00
Kyle Spearrin
132c4139ad update jslib 2019-07-02 08:44:45 -04:00
Kyle Spearrin
0aa664fb4f re-set favicon state on login/unlock 2019-07-02 08:44:29 -04:00
Kyle Spearrin
d25dc1a23f myki importer 2019-06-28 23:17:22 -04:00
Kyle Spearrin
3d5f22b67d select one collection string 2019-06-26 17:45:53 -04:00
Kyle Spearrin
cf6ae951d2 events urls from web project 2019-06-25 12:19:18 -04:00
Kyle Spearrin
cca9384cd7 simlink for windows 2019-06-24 21:13:09 -04:00
Kyle Spearrin
e7b2557bcd logged in as on 2019-06-04 00:06:15 -04:00
Kyle Spearrin
dad084b309 update jslib 2019-05-27 10:31:28 -04:00
Kyle Spearrin
e7fea1b138 services on 2fa report with software tokens only 2019-05-27 08:15:02 -04:00
Kyle Spearrin
b24d7df789 clear desc 2019-05-16 08:00:22 -04:00
Kyle Spearrin
c2f801b6a9 add korean to i18n 2019-05-15 08:52:43 -04:00
Kyle Spearrin
20112688ab bump version 2019-05-11 21:12:58 -04:00
Kyle Spearrin
2a19bdd8d1 correct launch icon 2019-04-26 22:44:10 -04:00
Kyle Spearrin
2d95806feb import all botostrap except toasts 2019-04-26 22:29:51 -04:00
Kyle Spearrin
40da48a106 password wallet txt importer 2019-04-26 20:55:15 -04:00
Kyle Spearrin
df81d9fd5f launch uri adjustments 2019-04-26 09:12:37 -04:00
Shawn Beachy
1060775cad Add a "launch site" button directly to the list of ciphers (#384)
* Add a button to launch the primary uri for a site straight from the list.

* Take cues from the add-edit component on properly checking if we can launch.

* Move the launch button to the dropdown menu.

* Take LoginView as launch parameter instead of LoginUriView.
2019-04-26 09:07:57 -04:00
Kyle Spearrin
96cc9c681c switch to terser minimizer with safari 10 fix 2019-04-20 20:54:50 -04:00
Kyle Spearrin
b4200fba60 support authBlocked message 2019-04-18 10:11:04 -04:00
Kyle Spearrin
2ded5228cb change plan is only for free subs 2019-04-17 11:06:21 -04:00
Kyle Spearrin
7be58fb884 update jslib 2019-04-15 23:04:13 -04:00
Kyle Spearrin
a29e9e11f7 focus length input on change 2019-04-15 22:37:29 -04:00
Kyle Spearrin
18c89e4fa5 premises 2019-04-04 00:45:53 -04:00
Kyle Spearrin
84dd370cfb add A11yTitleDirective 2019-04-02 09:48:49 -04:00
Kyle Spearrin
b45c79d65b send modal state messages 2019-04-02 09:29:20 -04:00
Kyle Spearrin
52a5086f7e dont use flex for password positioning 2019-04-01 13:56:34 -04:00
Kyle Spearrin
3980dc7e84 update bootstrap 2019-03-29 00:20:49 -04:00
Kyle Spearrin
ffd0608dda npm audit fix 2019-03-29 00:12:07 -04:00
Kyle Spearrin
322bc90920 drag n drop cleanup 2019-03-28 11:59:53 -04:00
Kovah
9685f2c2b3 Drag n drop sorting for custom fields (#370)
* Implement custom field ordering with new handle placement

* Update reference for jslib
2019-03-28 11:39:49 -04:00
Kyle Spearrin
342871a216 update jslib 2019-03-28 11:37:33 -04:00
Kyle Spearrin
1cd1ab07a2 New Crowdin translations (#369)
* New translations messages.json (Catalan)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Danish)

* New translations messages.json (Dutch)

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

* New translations messages.json (Estonian)

* New translations messages.json (French)

* New translations messages.json (German)

* New translations messages.json (Italian)

* New translations messages.json (Portuguese)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Russian)

* New translations messages.json (Spanish)

* New translations messages.json (Ukrainian)
2019-03-25 17:40:31 -04:00
Kyle Spearrin
137be678c0 update jslib 2019-03-25 09:16:55 -04:00
Kyle Spearrin
bcf0aaab17 update jslib 2019-03-23 12:32:14 -04:00
Kyle Spearrin
9c331e1777 New Crowdin translations (#368)
* New translations messages.json (Afrikaans)

* New translations messages.json (Italian)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Korean)

* New translations messages.json (Japanese)

* New translations messages.json (Hungarian)

* New translations messages.json (Catalan)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

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

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Croatian)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Ukrainian)
2019-03-21 23:12:25 -04:00
Kyle Spearrin
789516e573 changeBillingPlanUpgrade 2019-03-21 22:59:11 -04:00
Kyle Spearrin
02f964c7d9 New Crowdin translations (#367)
* New translations messages.json (Afrikaans)

* New translations messages.json (Italian)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Korean)

* New translations messages.json (Japanese)

* New translations messages.json (Hungarian)

* New translations messages.json (Catalan)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

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

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Croatian)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Ukrainian)
2019-03-21 22:36:29 -04:00
Kyle Spearrin
ea4d1de772 org plan upgrade 2019-03-21 21:38:56 -04:00
Kyle Spearrin
0f3d71a504 shared org plans component 2019-03-21 13:11:40 -04:00
Kyle Spearrin
5dc00a8bc6 update jslib 2019-03-21 10:08:29 -04:00
Kyle Spearrin
5690e3fe9e close buttons on cards 2019-03-20 10:16:01 -04:00
Kyle Spearrin
f6fcb280fc sub out change plan component 2019-03-20 10:11:51 -04:00
Kyle Spearrin
65a20815bf download license component 2019-03-20 09:56:50 -04:00
Kyle Spearrin
9a55202a9f PUSH_DOCKER checks 2019-03-19 22:17:03 -04:00
Kyle Spearrin
06ec65fb10 update jslib 2019-03-19 15:54:13 -04:00
Kyle Spearrin
371ecd9d3a page at 200 2019-03-19 14:55:19 -04:00
Kyle Spearrin
ff3fce821c bump version 2019-03-19 12:53:59 -04:00
Kyle Spearrin
cc706a48da paging ciphers for better performance 2019-03-19 12:44:22 -04:00
Kyle Spearrin
e4093209cc update jslib 2019-03-18 15:02:41 -04:00
Kyle Spearrin
1f7e5632ac ignore cli from jslib 2019-03-16 08:38:38 -04:00
Kyle Spearrin
bda9e7b2b2 bump version 2019-03-16 00:46:32 -04:00
Kyle Spearrin
5427ddb8d6 fix confirmModalRef 2019-03-15 21:04:57 -04:00
Kyle Spearrin
b34d40252f New Crowdin translations (#364)
* New translations messages.json (Catalan)

* New translations messages.json (French)

* New translations messages.json (Spanish)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Italian)

* New translations messages.json (German)

* New translations messages.json (Finnish)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Estonian)

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

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Croatian)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Ukrainian)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Slovak)

* New translations messages.json (Slovak)

* New translations messages.json (Slovak)
2019-03-13 14:52:36 -04:00
Kyle Spearrin
f73d74dd73 move to ps appveyor 2019-03-13 10:38:49 -04:00
Kyle Spearrin
eb48b8e65f back to cmd 2019-03-12 16:10:08 -04:00
Kyle Spearrin
ce0fe368ab set git pathing 2019-03-12 16:02:39 -04:00
Kyle Spearrin
34ef71707a move to ps commands 2019-03-12 15:37:32 -04:00
Kyle Spearrin
ffeb9dbaa5 update jslib 2019-03-12 10:45:47 -04:00
Kyle Spearrin
62a1d09f48 New Crowdin translations (#362)
* New translations messages.json (Catalan)

* New translations messages.json (Japanese)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Korean)

* New translations messages.json (Italian)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

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

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Croatian)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Ukrainian)
2019-03-09 09:20:07 -05:00
Kyle Spearrin
60039de67d only log into docker when password available 2019-03-08 16:38:21 -05:00
Kyle Spearrin
526df6e41a remove double build 2019-03-08 15:05:23 -05:00
Kyle Spearrin
cf8b451e35 APPVEYOR_RE_BUILD is True 2019-03-08 14:48:37 -05:00
Kyle Spearrin
059260d318 ci builds 2019-03-08 14:44:02 -05:00
Kyle Spearrin
45134f903d update jslib 2019-03-07 23:55:13 -05:00
Kyle Spearrin
fefe4edda1 collection externalId 2019-03-07 15:18:05 -05:00
Kyle Spearrin
aabb1bc264 get/rotate org api key 2019-03-07 11:18:45 -05:00
Kyle Spearrin
02ba2d3b60 update jslib 2019-03-07 10:03:09 -05:00
Kyle Spearrin
925c5aa389 update jslib 2019-03-06 21:37:50 -05:00
Kyle Spearrin
2b6ce14a32 update jslib 2019-03-05 17:25:01 -05:00
Kyle Spearrin
ed45e524b9 New Crowdin translations (#360)
* New translations messages.json (Estonian)

* New translations messages.json (Russian)
2019-03-04 20:16:49 -05:00
Kyle Spearrin
e645204e37 New Crowdin translations (#359)
* New translations messages.json (Czech)

* New translations messages.json (Estonian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Russian)

* New translations messages.json (Turkish)
2019-03-02 15:47:19 -05:00
Kyle Spearrin
ff1429c6b3 update jslib 2019-03-02 13:58:26 -05:00
Kyle Spearrin
abe17a02c4 update jslib 2019-02-27 14:39:55 -05:00
Kyle Spearrin
7bde73102b readFromClipboard implemented in web 2019-02-26 22:42:30 -05:00
Kyle Spearrin
3f27093f82 update jslib 2019-02-25 16:36:00 -05:00
Kyle Spearrin
37ed53cb3c show icon for wiretransfers 2019-02-25 09:22:25 -05:00
Kyle Spearrin
4b20d3ef0a New Crowdin translations (#357)
* New translations messages.json (Catalan)

* New translations messages.json (Italian)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Korean)

* New translations messages.json (German)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Ukrainian)
2019-02-25 09:14:24 -05:00
Kyle Spearrin
12492b5749 add image_url to paypal checkout 2019-02-24 22:12:20 -05:00
Kyle Spearrin
af2b422730 handle credit types 2019-02-23 20:34:21 -05:00
Kyle Spearrin
0c63f65aa7 refundNoun 2019-02-22 23:12:12 -05:00
Kyle Spearrin
4b2d1e6745 update jslib 2019-02-22 21:15:32 -05:00
Kyle Spearrin
25e6a03435 New Crowdin translations (#356)
* New translations messages.json (Catalan)

* New translations messages.json (Japanese)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Korean)

* New translations messages.json (Italian)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

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

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Croatian)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Ukrainian)
2019-02-22 15:50:18 -05:00
Kyle Spearrin
d681f91de9 update jslib 2019-02-22 15:07:55 -05:00
Kyle Spearrin
12e2bcbbd9 go back to previous url after lock 2019-02-22 13:17:10 -05:00
Kyle Spearrin
ec3e438c99 show bitcoin icon on transaction listing 2019-02-22 12:47:04 -05:00
Kyle Spearrin
2089237d23 add credit via bitpay 2019-02-21 22:48:59 -05:00
Kyle Spearrin
7bcd0ac3e5 account_credit:1 2019-02-21 21:48:02 -05:00
Kyle Spearrin
8e9ab12219 billing imrovements 2019-02-21 18:03:39 -05:00
Kyle Spearrin
33b539858f format html files 2019-02-21 16:50:37 -05:00
Kyle Spearrin
cdfd828a8b dont send token when null 2019-02-21 00:03:40 -05:00
Kyle Spearrin
22727b5abe support credit on get token method 2019-02-20 20:39:40 -05:00
Kyle Spearrin
041cf1268d credit fixes 2019-02-20 20:37:27 -05:00
Kyle Spearrin
fb3afbdc76 credit payment method 2019-02-20 20:16:06 -05:00
Kyle Spearrin
1f6632146b add credit via paypal 2019-02-20 17:33:05 -05:00
Kyle Spearrin
944187f276 ie11 fix 2019-02-19 22:00:55 -05:00
Kyle Spearrin
81eb2189ca New Crowdin translations (#354)
* New translations messages.json (Catalan)

* New translations messages.json (Czech)

* New translations messages.json (Dutch)

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

* New translations messages.json (Finnish)

* New translations messages.json (French)

* New translations messages.json (German)

* New translations messages.json (Italian)

* New translations messages.json (Japanese)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Portuguese)

* New translations messages.json (Russian)

* New translations messages.json (Spanish)
2019-02-19 21:40:59 -05:00
Kyle Spearrin
4fc90984d8 pass payment method type 2019-02-19 17:06:01 -05:00
Kyle Spearrin
0b1abc9ab0 more style fixes 2019-02-19 00:23:15 -05:00
Kyle Spearrin
212d81b93c New Crowdin translations (#353)
* New translations messages.json (Catalan)

* New translations messages.json (Japanese)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Korean)

* New translations messages.json (Italian)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

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

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Croatian)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Ukrainian)
2019-02-19 00:03:44 -05:00
Kyle Spearrin
238ac22b85 styling 2019-02-18 23:53:36 -05:00
Kyle Spearrin
773f0be84a move to stripe elements 2019-02-18 23:40:04 -05:00
Kyle Spearrin
e45c988637 account credit/balance 2019-02-18 17:34:57 -05:00
Kyle Spearrin
8305b49046 dont show billing page on self host 2019-02-18 16:10:32 -05:00
Kyle Spearrin
92b2601ba2 split billing and subscription management up 2019-02-18 15:28:23 -05:00
Kyle Spearrin
af8ab752ad update jslib 2019-02-17 21:16:59 -05:00
Kyle Spearrin
e45105ccb3 Merge branch 'master' of github.com:bitwarden/web 2019-02-16 16:04:39 -05:00
Kyle Spearrin
9114b68659 dont show add/remove seats when sub canceled 2019-02-16 16:04:35 -05:00
Kyle Spearrin
cd2e091580 New Crowdin translations (#349)
* New translations messages.json (Catalan)

* New translations messages.json (Japanese)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Korean)

* New translations messages.json (Italian)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

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

* New translations messages.json (Dutch)

* New translations messages.json (Croatian)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Ukrainian)

* New translations messages.json (Czech)

* New translations messages.json (Danish)

* New translations messages.json (French)

* New translations messages.json (Portuguese)

* New translations messages.json (Russian)
2019-02-14 22:54:13 -05:00
Kyle Spearrin
38d8f83587 update python-gnomekeyring link 2019-02-14 09:07:48 -05:00
Kyle Spearrin
5f3b6501d7 update jslib 2019-02-13 22:09:06 -05:00
Kyle Spearrin
f35efbdd5b fix deps 2019-02-13 21:58:16 -05:00
Kyle Spearrin
961954364a move to lock service is locked 2019-02-13 21:55:11 -05:00
Kyle Spearrin
259725882a remembear csv importer 2019-02-13 15:32:41 -05:00
Kyle Spearrin
fb2288c4bc show loading on braintree 2019-02-09 21:54:09 -05:00
Kyle Spearrin
0220f4519d billing page invoices and transactions 2019-02-09 00:19:54 -05:00
Kyle Spearrin
3432243acb update jslib 2019-02-02 22:33:47 -05:00
ShirokaiLon
27a32463d9 Add trackBy option (#342) 2019-02-02 22:31:33 -05:00
Kyle Spearrin
b47f7e8cf1 enable paypal for orgs. and paypal method changes 2019-01-31 12:11:23 -05:00
Kyle Spearrin
459bc69032 fix year in frontend footer 2019-01-29 12:52:11 -05:00
Kyle Spearrin
378b4bb8c1 update jslib 2019-01-28 11:10:55 -05:00
Kyle Spearrin
6a5712070f added kaspersky 2019-01-28 09:21:00 -05:00
Kyle Spearrin
48e125881b update jslib 2019-01-26 21:31:41 -05:00
Kyle Spearrin
e6cec93f2c postinstall task for fixing sweetalert typings 2019-01-25 15:05:22 -05:00
Kyle Spearrin
b5726393f3 update to angular 7 2019-01-25 14:05:09 -05:00
Kyle Spearrin
82010e4fa3 update jslib 2019-01-24 12:05:16 -05:00
Kyle Spearrin
eb99fe58dd refresh token and UI when license updated 2019-01-24 08:54:33 -05:00
Kyle Spearrin
a18e7ab2da flex copy on web generator 2019-01-23 17:05:36 -05:00
Kyle Spearrin
e1f78f519c flex copy directive 2019-01-23 16:27:34 -05:00
Kyle Spearrin
50a57727fe update jslib 2019-01-20 23:03:17 -05:00
Kyle Spearrin
4bbb7f82b4 update jslib 2019-01-18 23:42:19 -05:00
Kyle Spearrin
1da4cf8907 New Crowdin translations (#335)
* New translations messages.json (Catalan)

* New translations messages.json (Slovak)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Polish)

* New translations messages.json (Italian)

* New translations messages.json (German)

* New translations messages.json (Estonian)

* New translations messages.json (Croatian)
2019-01-18 16:09:04 -05:00
Kyle Spearrin
978a58391b add ca language 2019-01-18 15:59:04 -05:00
Kyle Spearrin
b2be44e372 use some 2019-01-17 10:47:03 -05:00
Kyle Spearrin
650fc6aa27 null checks on query param sub 2019-01-16 23:30:32 -05:00
Kyle Spearrin
47bda7d789 card exp month and year empty string defaults 2019-01-16 23:26:39 -05:00
Kyle Spearrin
64f41f004d en-GB locale 2019-01-15 20:49:15 -05:00
Kyle Spearrin
68f69074cb New Crowdin translations (#330)
* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Italian)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Korean)

* New translations messages.json (Japanese)

* New translations messages.json (Hungarian)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

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

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Croatian)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Ukrainian)
2019-01-15 20:11:55 -05:00
Kyle Spearrin
4d3fb52956 Revert "New Crowdin translations (#323)"
This reverts commit 1f39761f8c.
2019-01-15 20:09:21 -05:00
Kyle Spearrin
18a23d6844 Revert "New Crowdin translations (#327)"
This reverts commit bdf653bc70.
2019-01-15 20:09:11 -05:00
Kyle Spearrin
bdf653bc70 New Crowdin translations (#327)
* New translations messages.json (Chinese Simplified)

* New translations messages.json (Spanish)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Italian)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (German)

* New translations messages.json (French)

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

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Ukrainian)
2019-01-15 19:07:18 -05:00
Kyle Spearrin
0aaa351797 en-GB support 2019-01-15 17:53:49 -05:00
Kyle Spearrin
1f39761f8c New Crowdin translations (#323)
* New translations messages.json (Chinese Simplified)

* New translations messages.json (Japanese)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Korean)

* New translations messages.json (Italian)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

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

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Croatian)

* New translations messages.json (Ukrainian)

* New translations messages.json (Portuguese)
2019-01-15 17:34:41 -05:00
Kyle Spearrin
c7914fa8e4 bump version 2019-01-15 11:35:59 -05:00
Kyle Spearrin
a48cc2a7f3 always allow chrome to use u2f 2019-01-10 11:23:16 -05:00
Kyle Spearrin
3942409c9a lock screen improvements 2019-01-08 00:32:35 -05:00
Kyle Spearrin
6b0719db45 allow launching URLs without protocol than end with tld 2019-01-07 10:33:27 -05:00
Kyle Spearrin
9728116836 pass messagingService dependency 2019-01-03 10:24:29 -05:00
Kyle Spearrin
6cffabe259 f secure importer 2019-01-03 09:58:48 -05:00
Kyle Spearrin
28b20cc8ba update jslib 2019-01-03 00:18:42 -05:00
Kyle Spearrin
62b012941e update jslib 2019-01-01 23:17:12 -05:00
Kyle Spearrin
1b94ac383c New Crowdin translations (#316)
* New translations messages.json (Czech)

* New translations messages.json (German)

* New translations messages.json (Korean)

* New translations messages.json (Polish)
2018-12-31 12:57:33 -05:00
Kyle Spearrin
9a96ef2623 avast csv option 2018-12-31 12:42:27 -05:00
Kyle Spearrin
6480750757 New Crowdin translations (#315)
* New translations messages.json (Bulgarian)

* New translations messages.json (Danish)

* New translations messages.json (Dutch)

* New translations messages.json (Estonian)

* New translations messages.json (French)

* New translations messages.json (Italian)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Portuguese)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Russian)

* New translations messages.json (Slovak)

* New translations messages.json (Spanish)

* New translations messages.json (Swedish)

* New translations messages.json (Ukrainian)
2018-12-28 10:13:16 -05:00
Kyle Spearrin
df313560c2 added new languages 2018-12-28 10:06:38 -05:00
Kyle Spearrin
be0e832589 New Crowdin translations (#314)
* New translations messages.json (Chinese Simplified)

* New translations messages.json (Korean)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Japanese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Ukrainian)
2018-12-27 09:21:37 -05:00
Kyle Spearrin
9f87f551fd bump version 2018-12-27 09:15:36 -05:00
Kyle Spearrin
5804c57236 unsubscribe from queryparams observable 2018-12-20 10:06:40 -05:00
Kyle Spearrin
7efd81191a show indicator if two-step login is enabled 2018-12-19 11:30:02 -05:00
Kyle Spearrin
84bea20891 duo_web_sdk 2018-12-18 17:19:55 -05:00
Kyle Spearrin
c2b9b6e162 update jslib 2018-12-18 17:00:16 -05:00
Kyle Spearrin
3720b9481f install and use duo_web_sdk w/ npm 2018-12-18 17:00:03 -05:00
Kyle Spearrin
b565d40ec7 New Crowdin translations (#310)
* New translations messages.json (Chinese Simplified)

* New translations messages.json (Korean)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Japanese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Ukrainian)
2018-12-17 23:20:39 -05:00
Kyle Spearrin
0e1f2e721f bitwarden json importer 2018-12-17 13:21:16 -05:00
Kyle Spearrin
951a22b90e make file format select list first 2018-12-17 11:07:44 -05:00
Kyle Spearrin
1dd88a690b support for json exports 2018-12-17 10:54:18 -05:00
Kyle Spearrin
55ba78c66a bump version 2018-12-15 21:56:41 -05:00
Kyle Spearrin
b0364041e2 should be useTotp 2018-12-15 21:54:32 -05:00
Kyle Spearrin
6b9c90f99b update jslib 2018-12-14 17:20:31 -05:00
Kyle Spearrin
4050bc1da8 accessReports typo 2018-12-14 14:50:15 -05:00
Kyle Spearrin
4bb9051136 show reports with upgrade message 2018-12-14 14:48:12 -05:00
Kyle Spearrin
ceca4fbe53 add more org reports 2018-12-14 14:42:04 -05:00
Kyle Spearrin
9b7c0288d4 inactive 2fa report for orgs 2018-12-14 14:22:30 -05:00
Kyle Spearrin
392a90c02c exposed passwords report for orgs 2018-12-14 13:56:01 -05:00
Kyle Spearrin
7a58f6d967 make sure routerService is newed 2018-12-14 11:15:50 -05:00
Kyle Spearrin
35d1e51f9b update jslib 2018-12-13 14:38:03 -05:00
Kyle Spearrin
9729a4c724 enpass json importer 2018-12-13 14:34:43 -05:00
Kyle Spearrin
f13713a055 update jslib 2018-12-13 10:59:05 -05:00
Kyle Spearrin
31655f7832 userInputs for strength check based on username 2018-12-12 19:37:27 -05:00
Kyle Spearrin
fb4bb81595 dashlane json importer 2018-12-12 17:06:22 -05:00
Kyle Spearrin
31cb6916c6 null check 2018-12-12 15:01:23 -05:00
Kyle Spearrin
61d37615af New translations messages.json (Portuguese) (#308) 2018-12-12 12:57:01 -05:00
Kyle Spearrin
eaa7701696 New translations messages.json (Portuguese) (#307) 2018-12-12 12:49:10 -05:00
Kyle Spearrin
d6cff8e0b0 Merge branch 'master' of github.com:bitwarden/web 2018-12-12 12:46:20 -05:00
Kyle Spearrin
8ba761b33c add missing "that" 2018-12-12 12:46:17 -05:00
Kyle Spearrin
05e7e452df New Crowdin translations (#306)
* New translations messages.json (Chinese Simplified)

* New translations messages.json (Korean)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Japanese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Ukrainian)
2018-12-12 12:03:03 -05:00
Kyle Spearrin
3f0fd4f771 use cache instead of async 2018-12-12 11:22:11 -05:00
Kyle Spearrin
a587c1d1da async the weak password checks 2018-12-12 10:34:05 -05:00
Kyle Spearrin
c3355f7fe4 premium reports feature 2018-12-12 10:05:54 -05:00
Kyle Spearrin
c182d874af premium labels for reports section 2018-12-12 09:45:50 -05:00
Kyle Spearrin
ab9ebfb667 fix i18n 2018-12-12 09:30:57 -05:00
Kyle Spearrin
cb953eda61 premium checks on reports 2018-12-12 09:29:51 -05:00
Kyle Spearrin
93c291dba1 base cipher report component class 2018-12-12 09:11:10 -05:00
Kyle Spearrin
603a1ef046 format numbers 2018-12-12 08:54:52 -05:00
Kyle Spearrin
5a504b00fb update report language 2018-12-12 08:53:44 -05:00
Kyle Spearrin
dfa59dc93d instructions language update 2018-12-12 00:02:57 -05:00
Kyle Spearrin
534bcdd52c rearrange reports 2018-12-11 23:29:36 -05:00
Kyle Spearrin
ea032bf551 inactive 2fa report 2018-12-11 23:25:05 -05:00
Kyle Spearrin
b44eee8d81 hasLoaded moved to load method 2018-12-11 22:10:26 -05:00
Kyle Spearrin
8f57ada128 exposed passwords report 2018-12-11 22:09:16 -05:00
Kyle Spearrin
3963990831 weak passwords report 2018-12-11 17:49:51 -05:00
Kyle Spearrin
dc1ffafdf3 hasLoaded spinners 2018-12-11 15:16:45 -05:00
Kyle Spearrin
4a0b4de322 unsecured websites report 2018-12-11 15:11:16 -05:00
Kyle Spearrin
0ede65e9ca add help link for searching 2018-12-11 14:51:44 -05:00
Kyle Spearrin
0ebf30b8b6 reused passwords report 2018-12-11 14:47:41 -05:00
Kyle Spearrin
4222b192c4 max-height on icon image 2018-12-08 14:36:55 -05:00
Kyle Spearrin
4a301aaec3 bump version 2018-12-08 14:11:57 -05:00
Kyle Spearrin
97a3a97a15 colorized password 2018-12-08 14:11:10 -05:00
Kyle Spearrin
c526d73e23 bump version 2018-12-08 10:48:05 -05:00
Kyle Spearrin
58baf137aa dont apply old pipe search during select all filter 2018-12-08 10:47:22 -05:00
Matej Kramny
066ab1500f enable clear button in search input types (#300) 2018-12-07 20:07:34 -05:00
Andrew Peng
224a468712 Fix typo (#298) 2018-12-03 15:39:33 -05:00
Kyle Spearrin
dd282383d7 use router.navigate rather than location 2018-11-30 10:28:46 -05:00
Kyle Spearrin
9a99a95b15 update jslib 2018-11-28 09:52:49 -05:00
Alexandre Lapeyre
e814494e37 fix logos in breach report page (#296) 2018-11-28 09:51:43 -05:00
Kyle Spearrin
90a0155be1 bump version 2018-11-28 08:55:24 -05:00
Kyle Spearrin
555d40408d normalize email on password change 2018-11-28 08:54:33 -05:00
Kyle Spearrin
0c3fbeb0b7 update gulp to 4.0.0 2018-11-27 12:47:06 -05:00
Kyle Spearrin
2f27decaa1 bump version 2018-11-26 15:36:17 -05:00
Kyle Spearrin
867115659f re-enable key rotation on master password change 2018-11-26 15:36:09 -05:00
Kyle Spearrin
6282ab58db hide key rotation option 2018-11-26 12:59:45 -05:00
Kyle Spearrin
95c25c1bcd fix help articles 2018-11-26 12:25:27 -05:00
Kyle Spearrin
74a62de7d0 New Crowdin translations (#295)
* New translations messages.json (Czech)

* New translations messages.json (Danish)

* New translations messages.json (Dutch)

* New translations messages.json (French)

* New translations messages.json (Italian)

* New translations messages.json (Japanese)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Polish)

* New translations messages.json (Portuguese)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Russian)

* New translations messages.json (Slovak)

* New translations messages.json (Spanish)
2018-11-26 08:41:59 -05:00
Kyle Spearrin
7fc021648d update jslib 2018-11-26 08:31:28 -05:00
Kyle Spearrin
95914ad312 Merge branch 'master' of github.com:bitwarden/web 2018-11-23 08:20:25 -05:00
Kyle Spearrin
5ed00e037a bump version 2018-11-23 08:20:22 -05:00
Kyle Spearrin
6f3074536a New Crowdin translations (#292)
* New translations messages.json (Chinese Simplified)

* New translations messages.json (Korean)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Japanese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Ukrainian)
2018-11-21 09:16:53 -05:00
Kyle Spearrin
21f5cb36bb To ensure the integrity 2018-11-21 09:04:46 -05:00
Kyle Spearrin
7b7592822f New Crowdin translations (#290)
* New translations messages.json (Chinese Simplified)

* New translations messages.json (Korean)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Japanese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Ukrainian)
2018-11-20 22:35:17 -05:00
Kyle Spearrin
9c7b7b0d75 premium access addon for orgs 2018-11-20 16:38:00 -05:00
Kyle Spearrin
d88b23c42d learn more about encryption key changes from help article 2018-11-20 13:05:52 -05:00
Kyle Spearrin
1602c0aca2 link to fingerprint phrase article 2018-11-16 11:20:44 -05:00
Kyle Spearrin
384978a511 fix old attachments article info 2018-11-16 09:17:33 -05:00
Kyle Spearrin
afd7a0494f fix password meter aria-valuenow 2018-11-15 14:46:51 -05:00
Kyle Spearrin
ac1f8a69e1 allow bulk sharing of items with new attachments 2018-11-15 12:56:07 -05:00
Kyle Spearrin
05cfa99ea0 fingerprint phrase confirmation 2018-11-14 23:13:50 -05:00
Kyle Spearrin
9b43ccbbc0 updateKey helper 2018-11-14 16:22:57 -05:00
Kyle Spearrin
6d8b156455 old attachments check when rotating enc key 2018-11-14 15:54:13 -05:00
Kyle Spearrin
1b9943a4c8 allow swal single button 2018-11-14 15:45:03 -05:00
Kyle Spearrin
8232a4c9c8 fix old attachments by reuploading them 2018-11-14 15:20:17 -05:00
Kyle Spearrin
9d4d64c95a update jslib 2018-11-13 20:43:56 -05:00
Kyle Spearrin
2d0acc7663 add enc key rotation option during master password change 2018-11-13 11:06:16 -05:00
Kyle Spearrin
4231ed74ba adjust password strength meter 2018-11-13 09:10:44 -05:00
Kyle Spearrin
912e1cf89f getPasswordStrengthUserInput on password change 2018-11-12 23:26:00 -05:00
Kyle Spearrin
26d4fb8005 fix aslignment with invisible progress bar 2018-11-12 23:06:28 -05:00
Kyle Spearrin
4a6c0b39a8 add typings for zxcvbn 2018-11-12 23:03:20 -05:00
Kyle Spearrin
9d01bba170 weak password checks on master password change 2018-11-12 23:00:58 -05:00
Kyle Spearrin
85c0ddba10 password strength checks during registration 2018-11-12 22:54:40 -05:00
Kyle Spearrin
2664059812 caret spacing 2018-11-09 22:25:07 -05:00
Kyle Spearrin
b7e4d9c806 toggle collapse string update 2018-11-09 17:50:26 -05:00
Kyle Spearrin
95b91f0ce2 added collpase/expand functions to groupings 2018-11-09 17:45:01 -05:00
Kyle Spearrin
f0407e4327 catch any errors when generating fingerprint 2018-11-07 23:19:48 -05:00
Kyle Spearrin
a7555f56e7 print user's fingerprint when confirming 2018-11-07 23:15:50 -05:00
Kyle Spearrin
b093ed33b2 update jslib 2018-11-06 15:53:51 -05:00
Kyle Spearrin
ec1a45ba18 update jslib 2018-11-06 15:51:52 -05:00
Kyle Spearrin
def5dc3b0f New Crowdin translations (#286)
* New translations messages.json (Chinese Simplified)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Danish)

* New translations messages.json (Dutch)

* New translations messages.json (Estonian)

* New translations messages.json (Finnish)

* New translations messages.json (French)

* New translations messages.json (Japanese)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Polish)

* New translations messages.json (Portuguese)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Russian)

* New translations messages.json (Spanish)
2018-11-06 15:51:05 -05:00
Kyle Spearrin
24ec89c220 open PDF in new window using built-in browser viewer 2018-11-06 09:46:17 -05:00
Kyle Spearrin
303e70bb58 update jslib 2018-11-06 09:05:46 -05:00
Kyle Spearrin
ec3e92fc19 set blob type 2018-10-30 09:54:14 -04:00
Kyle Spearrin
5ae776309d New Crowdin translations (#284)
* New translations messages.json (Chinese Simplified)

* New translations messages.json (Korean)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Japanese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Ukrainian)
2018-10-29 10:26:38 -04:00
Kyle Spearrin
76dd606a48 additionalStorageIntervalDesc 2018-10-29 10:07:03 -04:00
Kyle Spearrin
8998798fa4 always load nested collections 2018-10-29 10:06:42 -04:00
Kyle Spearrin
60ee82ca47 always loading nested now 2018-10-26 10:49:14 -04:00
Kyle Spearrin
e1284002a9 cleanup imports 2018-10-26 08:29:33 -04:00
Kyle Spearrin
8252512784 nested collections 2018-10-25 12:19:35 -04:00
Kyle Spearrin
1390d7eb1d display nested folders 2018-10-25 09:38:52 -04:00
Kyle Spearrin
8da1bb13ff dont stop prob on label simple label click for cb list 2018-10-24 22:15:09 -04:00
Kyle Spearrin
340e377b37 filter collections for current org 2018-10-24 22:09:36 -04:00
Kyle Spearrin
171589fb3d missing searchText property 2018-10-24 22:01:38 -04:00
Kyle Spearrin
bcd07cce0d New Crowdin translations (#281)
* New translations messages.json (Chinese Simplified)

* New translations messages.json (Korean)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Japanese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Ukrainian)
2018-10-24 13:03:03 -04:00
Kyle Spearrin
68880114b4 bump version 2018-10-23 22:57:36 -04:00
Kyle Spearrin
eb2360ae24 update jslib 2018-10-23 16:18:01 -04:00
Kyle Spearrin
62712a352b update jslib 2018-10-23 12:04:28 -04:00
Kyle Spearrin
745e6c1715 use base collections component from jslib 2018-10-23 12:04:05 -04:00
Kyle Spearrin
e20a75eb0c use share component from jslib 2018-10-23 10:33:40 -04:00
Kyle Spearrin
a24c41ff25 set org id and collections if filtered 2018-10-22 16:46:48 -04:00
Kyle Spearrin
69f0339bd5 set collections for org admin 2018-10-22 14:48:17 -04:00
Kyle Spearrin
5e7c9a7278 add ownership and collection assignment from add/edit 2018-10-19 12:44:52 -04:00
Kyle Spearrin
726c323fe1 accessAll is only for collection assignments 2018-10-18 12:25:25 -04:00
SoulSeekkor
e96cbe2710 Added .gitattributes file to files requiring LF endings are properly checked out on Windows. (#279) 2018-10-18 12:15:54 -04:00
Kyle Spearrin
323e54b4bd filtering 2018-10-18 12:15:13 -04:00
Kyle Spearrin
7ab132bbf6 add thead for entity users 2018-10-17 23:04:39 -04:00
Kyle Spearrin
6b09210a80 manage group users 2018-10-17 22:56:49 -04:00
Kyle Spearrin
be80d62c01 manage collection users for entity-users 2018-10-17 22:20:42 -04:00
Kyle Spearrin
30587d625a fixes to showAdd and filtering on load for non-admins 2018-10-17 16:09:09 -04:00
Kyle Spearrin
af43cd407e undo manage rules for org groupings listing 2018-10-17 15:57:39 -04:00
Kyle Spearrin
647388e475 showAddNew only if admin 2018-10-17 15:51:31 -04:00
Kyle Spearrin
329e06ac30 null check in view 2018-10-17 15:46:13 -04:00
Kyle Spearrin
5d96138720 lint fix 2018-10-17 11:23:01 -04:00
Kyle Spearrin
66b275605c load manage collections a manager has access to 2018-10-17 11:20:27 -04:00
Kyle Spearrin
9b7478c0c7 manager sees their assigned collections from org vault view 2018-10-17 11:19:10 -04:00
Kyle Spearrin
668271bb31 add basic org manager access and UI elements 2018-10-17 10:53:04 -04:00
271 changed files with 78580 additions and 12386 deletions

3
.gitattributes vendored Normal file
View File

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

View File

@@ -5,6 +5,7 @@ LABEL com.bitwarden.product="bitwarden"
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gosu \
curl \
&& rm -rf /var/lib/apt/lists/*
ENV ASPNETCORE_URLS http://+:5000
@@ -14,4 +15,6 @@ COPY ./build .
COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh
HEALTHCHECK CMD curl -f http://localhost:5000 || exit 1
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -3,3 +3,50 @@ 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.
-->

83
appveyor.yml Normal file
View File

@@ -0,0 +1,83 @@
image:
- Visual Studio 2017
- Ubuntu1804
branches:
except:
- l10n_master
- gh-pages
services:
- docker
stack: node 10
init:
- ps: |
if($isWindows) {
Install-Product node 10
}
install:
- ps: |
$env:PACKAGE_VERSION = (Get-Content -Raw -Path .\package.json | ConvertFrom-Json).version
$env:PUSH_DOCKER = "false"
$env:PROD_DEPLOY = "false"
$env:TAG_NAME = ""
if($env:APPVEYOR_REPO_TAG -eq "true" -and $env:APPVEYOR_RE_BUILD -eq "True") {
$env:PROD_DEPLOY = "true"
$env:TAG_NAME = $env:APPVEYOR_REPO_TAG_NAME.TrimStart("v")
echo "This is a production deployment for ${env:TAG_NAME}."
}
if("${env:DOCKER_USERNAME}" -ne "" -and "${env:DOCKER_PASSWORD}" -ne "") {
$env:PUSH_DOCKER = "true"
}
if($isWindows) {
choco install cloc --no-progress
cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
}
before_build:
- node --version
- npm --version
- sh: |
if [ "${PUSH_DOCKER}" == "true" ]
then
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
fi
- cmd: set "GIT_PATH=C:\Program Files\Git\mingw64\libexec\git-core"
- cmd: set "PATH=%GIT_PATH%;%PATH%"
build_script:
- sh: chmod +x ./build.sh
- ps: |
if($isLinux) {
./build.sh
./build.sh tag dev
if($env:PROD_DEPLOY -eq "true") {
./build.sh tag beta
./build.sh tag $env:TAG_NAME
}
docker images
if($env:PUSH_DOCKER -eq "true") {
./build.sh push dev
if($env:PROD_DEPLOY -eq "true") {
./build.sh push beta
./build.sh push latest
./build.sh push $env:TAG_NAME
}
}
}
- cmd: npm install
- cmd: npm run build:prod
after_build:
- sh: |
if [ "${PUSH_DOCKER}" == "true" ]
then
docker logout
fi

View File

@@ -8,3 +8,5 @@ files:
pt-BR: pt_BR
zh-CN: zh_CN
zh-TW: zh_TW
en-GB: en_GB
en-IN: en_IN

View File

@@ -5,6 +5,7 @@ const package = require('./package.json');
const fs = require('fs');
const paths = {
node_modules: './node_modules/',
src: './src/',
build: './build/',
cssDir: './src/css/',
@@ -24,12 +25,13 @@ function webfonts() {
.pipe(gulp.dest(paths.cssDir));
};
function version() {
function version(cb) {
fs.writeFileSync(paths.build + 'version.json', '{"version":"' + package.version + '"}');
cb();
}
gulp.task('clean', clean);
gulp.task('webfonts', ['clean'], webfonts);
gulp.task('prebuild', ['webfonts']);
gulp.task('version', version);
gulp.task('postdist', ['version']);
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: 2f6426deb4...f30d6f8027

8615
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,16 @@
{
"name": "bitwarden-web",
"version": "2.4.0",
"version": "2.16.2",
"license": "GPL-3.0",
"repository": "https://github.com/bitwarden/web",
"scripts": {
"sub:init": "git submodule update --init --recursive",
"sub:update": "git submodule update --remote",
"sub:pull": "git submodule foreach git pull",
"sub:pull": "git submodule foreach git pull origin master",
"postinstall": "npm run sub:init",
"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",
"build:watch": "gulp prebuild && webpack-dev-server",
"build:prod": "gulp prebuild && cross-env NODE_ENV=production webpack",
@@ -23,70 +28,75 @@
"lint:fix": "tslint src/**/*.ts --fix"
},
"devDependencies": {
"@angular/compiler-cli": "^6.1.7",
"@ngtools/webpack": "^6.2.1",
"@types/jquery": "^3.3.6",
"@types/lunr": "^2.1.6",
"@angular/compiler-cli": "^9.1.12",
"@ngtools/webpack": "^9.1.12",
"@types/jquery": "^3.5.1",
"@types/lunr": "^2.3.3",
"@types/node": "^10.17.28",
"@types/node-forge": "^0.7.5",
"@types/papaparse": "^4.5.3",
"@types/webcrypto": "^0.0.28",
"@types/webpack": "^4.4.11",
"@types/zxcvbn": "^4.4.0",
"angular2-template-loader": "^0.6.2",
"clean-webpack-plugin": "^0.1.19",
"copy-webpack-plugin": "^4.5.2",
"cross-env": "^5.2.0",
"css-loader": "^1.0.0",
"del": "^3.0.0",
"extract-text-webpack-plugin": "next",
"file-loader": "^2.0.0",
"gh-pages": "^1.2.0",
"gulp": "^3.9.1",
"gulp": "^4.0.0",
"gulp-google-webfonts": "^2.0.0",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"node-sass": "^4.9.3",
"mini-css-extract-plugin": "^0.9.0",
"node-sass": "^4.13.1",
"sass-loader": "^7.1.0",
"style-loader": "^0.23.0",
"ts-loader": "^5.1.0",
"tslint": "^5.11.0",
"tslint-loader": "^3.6.0",
"typescript": "^2.7.2",
"webpack": "^4.18.0",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.8"
"terser-webpack-plugin": "^1.2.3",
"ts-loader": "^7.0.5",
"tslint": "^6.1.3",
"tslint-loader": "^3.5.4",
"typescript": "3.8.3",
"webpack": "^4.29.0",
"webpack-cli": "^3.2.1",
"webpack-dev-server": "^3.1.14"
},
"dependencies": {
"@angular/animations": "6.1.7",
"@angular/common": "6.1.7",
"@angular/compiler": "6.1.7",
"@angular/core": "6.1.7",
"@angular/forms": "6.1.7",
"@angular/http": "6.1.7",
"@angular/platform-browser": "6.1.7",
"@angular/platform-browser-dynamic": "6.1.7",
"@angular/router": "6.1.7",
"@angular/upgrade": "6.1.7",
"@aspnet/signalr": "1.0.4",
"@aspnet/signalr-protocol-msgpack": "1.0.4",
"angular2-toaster": "6.1.0",
"angulartics2": "6.3.0",
"bootstrap": "4.1.3",
"@angular/animations": "9.1.12",
"@angular/cdk": "9.2.4",
"@angular/common": "9.1.12",
"@angular/compiler": "9.1.12",
"@angular/core": "9.1.12",
"@angular/forms": "9.1.12",
"@angular/platform-browser": "9.1.12",
"@angular/platform-browser-dynamic": "9.1.12",
"@angular/router": "9.1.12",
"@microsoft/signalr": "3.1.0",
"@microsoft/signalr-protocol-msgpack": "3.1.0",
"angular2-toaster": "8.0.0",
"angulartics2": "9.1.0",
"big-integer": "1.6.36",
"bootstrap": "4.3.1",
"braintree-web-drop-in": "1.13.0",
"core-js": "2.5.7",
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git",
"core-js": "2.6.2",
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git#410a9186cc34663c4913b17d6528067cd3331f1d",
"font-awesome": "4.7.0",
"jquery": "3.3.1",
"jquery": "3.4.1",
"lunr": "2.3.3",
"ngx-infinite-scroll": "6.0.1",
"ngx-infinite-scroll": "7.0.1",
"node-forge": "0.7.6",
"papaparse": "4.6.0",
"popper.js": "1.14.4",
"qrious": "4.0.2",
"rxjs": "6.3.2",
"sweetalert": "2.1.0",
"rxjs": "6.6.2",
"sweetalert2": "9.8.1",
"tslib": "^2.0.1",
"web-animations-js": "2.3.1",
"webcrypto-shim": "0.1.4",
"whatwg-fetch": "3.0.0",
"zone.js": "0.8.26"
"zone.js": "0.10.3",
"zxcvbn": "4.4.2"
}
}

View File

@@ -2,7 +2,8 @@
<div>
<img 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}}"></i>
<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>
@@ -22,7 +23,8 @@
<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">
<a routerLink="/register" [queryParams]="{email: email}"
class="btn btn-primary btn-block ml-2 mt-0">
{{'createAccount' | i18n}}
</a>
</div>

View File

@@ -44,6 +44,7 @@ export class AcceptOrganizationComponent implements OnInit {
fired = true;
await this.stateService.remove('orgInvitation');
let error = qParams.organizationId == null || qParams.organizationUserId == null || qParams.token == null;
let errorMessage: string = null;
if (!error) {
this.authed = await this.userService.isAuthenticated();
if (this.authed) {
@@ -61,8 +62,9 @@ export class AcceptOrganizationComponent implements OnInit {
};
this.toasterService.popAsync(toast);
this.router.navigate(['/vault']);
} catch {
} catch (e) {
error = true;
errorMessage = e.message;
}
} else {
await this.stateService.save('orgInvitation', qParams);
@@ -76,7 +78,14 @@ export class AcceptOrganizationComponent implements OnInit {
}
if (error) {
this.toasterService.popAsync('error', null, this.i18nService.t('inviteAcceptFailed'));
const toast: Toast = {
type: 'error',
title: null,
body: errorMessage != null ? this.i18nService.t('inviteAcceptFailedShort', errorMessage) :
this.i18nService.t('inviteAcceptFailed'),
timeout: 10000,
};
this.toasterService.popAsync(toast);
this.router.navigate(['/']);
}

View File

@@ -6,15 +6,15 @@
<div class="card-body">
<div class="form-group">
<label for="email">{{'emailAddress' | i18n}}</label>
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required appAutofocus inputmode="email"
appInputVerbatim="false">
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required
appAutofocus inputmode="email" appInputVerbatim="false">
<small class="form-text text-muted">{{'enterEmailToGetHint' | i18n}}</small>
</div>
<hr>
<div class="d-flex">
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
<span [hidden]="form.loading">{{'submit' | i18n}}</span>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{'cancel' | i18n}}

View File

@@ -1,8 +1,8 @@
<form (ngSubmit)="submit()" class="container" ngNativeValidate>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="text-center mb-4">
<i class="fa fa-lock fa-4x text-muted"></i>
<i class="fa fa-lock fa-4x text-muted" aria-hidden="true"></i>
</p>
<p class="lead text-center mx-4 mb-4">{{'yourVaultIsLocked' | i18n}}</p>
<div class="card d-block">
@@ -10,18 +10,26 @@
<div class="form-group">
<label for="masterPassword">{{'masterPass' | i18n}}</label>
<div class="d-flex">
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}" name="MasterPassword" class="text-monospace form-control"
[(ngModel)]="masterPassword" required appAutofocus appInputVerbatim>
<button type="button" class="ml-1 btn btn-link" title="{{'toggleVisibility' | i18n}}" (click)="togglePassword()">
<i class="fa fa-lg" [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
name="MasterPassword" class="text-monospace form-control" [(ngModel)]="masterPassword"
required appAutofocus appInputVerbatim>
<button type="button" class="ml-1 btn btn-link" appA11yTitle="{{'toggleVisibility' | i18n}}"
(click)="togglePassword()">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
</div>
<small class="text-muted form-text">
{{'loggedInAsEmailOn' | i18n : email : webVaultHostname}}
</small>
</div>
<hr>
<div class="d-flex">
<button type="submit" class="btn btn-primary btn-block">
<i class="fa fa-unlock-alt"></i>
{{'unlock' | i18n}}
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
<span>
<i class="fa fa-unlock-alt" aria-hidden="true"></i> {{'unlock' | i18n}}
</span>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
<button type="button" class="btn btn-outline-secondary btn-block ml-2 mt-0" (click)="logOut()">
{{'logOut' | i18n}}

View File

@@ -1,14 +1,16 @@
import {
Component,
OnInit,
} from '@angular/core';
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StateService } from 'jslib/abstractions/state.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { UserService } from 'jslib/abstractions/user.service';
import { VaultTimeoutService } from 'jslib/abstractions/vaultTimeout.service';
import { RouterService } from '../services/router.service';
@@ -18,15 +20,19 @@ import { LockComponent as BaseLockComponent } from 'jslib/angular/components/loc
selector: 'app-lock',
templateUrl: 'lock.component.html',
})
export class LockComponent extends BaseLockComponent implements OnInit {
export class LockComponent extends BaseLockComponent {
constructor(router: Router, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, messagingService: MessagingService,
userService: UserService, cryptoService: CryptoService,
private routerService: RouterService) {
super(router, i18nService, platformUtilsService, messagingService, userService, cryptoService);
storageService: StorageService, vaultTimeoutService: VaultTimeoutService,
environmentService: EnvironmentService, private routerService: RouterService,
stateService: StateService, apiService: ApiService) {
super(router, i18nService, platformUtilsService, messagingService, userService, cryptoService,
storageService, vaultTimeoutService, environmentService, stateService, apiService);
}
async ngOnInit() {
await super.ngOnInit();
const authed = await this.userService.isAuthenticated();
if (!authed) {
this.router.navigate(['/']);
@@ -34,9 +40,12 @@ export class LockComponent extends BaseLockComponent implements OnInit {
this.router.navigate(['vault']);
}
const previousUrl = this.routerService.getPreviousUrl();
if (previousUrl !== '/' && previousUrl.indexOf('lock') === -1) {
this.successRoute = previousUrl;
}
this.onSuccessfulSubmit = () => {
const previousUrl = this.routerService.getPreviousUrl();
if (previousUrl !== '/' && previousUrl.indexOf('lock') === -1) {
this.successRoute = previousUrl;
}
this.router.navigate([this.successRoute]);
};
}
}

View File

@@ -7,15 +7,19 @@
<div class="card-body">
<div class="form-group">
<label for="email">{{'emailAddress' | i18n}}</label>
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required inputmode="email" appInputVerbatim="false">
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required
inputmode="email" appInputVerbatim="false">
</div>
<div class="form-group">
<label for="masterPassword">{{'masterPass' | i18n}}</label>
<div class="d-flex">
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}" name="MasterPassword" class="text-monospace form-control"
[(ngModel)]="masterPassword" required appInputVerbatim>
<button type="button" class="ml-1 btn btn-link" title="{{'toggleVisibility' | i18n}}" (click)="togglePassword()">
<i class="fa fa-lg" [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
name="MasterPassword" class="text-monospace form-control" [(ngModel)]="masterPassword"
required appInputVerbatim>
<button type="button" class="ml-1 btn btn-link" appA11yTitle="{{'toggleVisibility' | i18n}}"
(click)="togglePassword()">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
</div>
<small class="form-text">
@@ -23,19 +27,26 @@
</small>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="rememberEmail" name="RememberEmail" [(ngModel)]="rememberEmail">
<input type="checkbox" class="form-check-input" id="rememberEmail" name="RememberEmail"
[(ngModel)]="rememberEmail">
<label class="form-check-label" for="rememberEmail">{{'rememberEmail' | i18n}}</label>
</div>
<hr>
<div class="d-flex">
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
<span>
<i class="fa fa-sign-in"></i> {{'logIn' | i18n}}
<i class="fa fa-sign-in" aria-hidden="true"></i> {{'logIn' | i18n}}
</span>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
<a routerLink="/register" [queryParams]="{email: email}" class="btn btn-outline-secondary btn-block ml-2 mt-0">
<i class="fa fa-pencil-square-o"></i> {{'createAccount' | i18n}}
<a routerLink="/register" [queryParams]="{email: email}"
class="btn btn-outline-secondary btn-block ml-2 mt-0">
<i class="fa fa-pencil-square-o" aria-hidden="true"></i> {{'createAccount' | i18n}}
</a>
</div>
<div class="d-flex">
<a routerLink="/sso" class="btn btn-outline-secondary btn-block mt-2">
<i class="fa fa-bank" aria-hidden="true"></i> {{'enterpriseSingleSignOn' | i18n}}
</a>
</div>
</div>

View File

@@ -5,7 +5,10 @@ import {
} from '@angular/router';
import { AuthService } from 'jslib/abstractions/auth.service';
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StateService } from 'jslib/abstractions/state.service';
import { StorageService } from 'jslib/abstractions/storage.service';
@@ -19,14 +22,19 @@ import { LoginComponent as BaseLoginComponent } from 'jslib/angular/components/l
export class LoginComponent extends BaseLoginComponent {
constructor(authService: AuthService, router: Router,
i18nService: I18nService, private route: ActivatedRoute,
storageService: StorageService, private stateService: StateService,
platformUtilsService: PlatformUtilsService) {
super(authService, router, platformUtilsService, i18nService, storageService);
storageService: StorageService, stateService: StateService,
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
passwordGenerationService: PasswordGenerationService, cryptoFunctionService: CryptoFunctionService) {
super(authService, router,
platformUtilsService, i18nService,
stateService, environmentService,
passwordGenerationService, cryptoFunctionService,
storageService);
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
}
async ngOnInit() {
this.route.queryParams.subscribe(async (qParams) => {
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
if (qParams.email != null && qParams.email.indexOf('@') > -1) {
this.email = qParams.email;
}
@@ -37,6 +45,9 @@ export class LoginComponent extends BaseLoginComponent {
{ route: '/settings/create-organization', qParams: { plan: qParams.org } });
}
await super.ngOnInit();
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();
}
});
}

View File

@@ -7,14 +7,14 @@
<p>{{'deleteRecoverDesc' | i18n}}</p>
<div class="form-group">
<label for="email">{{'emailAddress' | i18n}}</label>
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required appAutofocus inputmode="email"
appInputVerbatim="false">
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required
appAutofocus inputmode="email" appInputVerbatim="false">
</div>
<hr>
<div class="d-flex">
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
<span>{{'submit' | i18n}}</span>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{'cancel' | i18n}}

View File

@@ -5,28 +5,29 @@
<div class="card">
<div class="card-body">
<p>{{'recoverAccountTwoStepDesc' | i18n}}
<a href="https://help.bitwarden.com/article/lost-two-step-device/" target="_blank" rel="noopener">{{'learnMore' | i18n}}</a>
<a href="https://help.bitwarden.com/article/lost-two-step-device/" target="_blank"
rel="noopener">{{'learnMore' | i18n}}</a>
</p>
<div class="form-group">
<label for="email">{{'emailAddress' | i18n}}</label>
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required appAutofocus inputmode="email"
appInputVerbatim="false">
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required
appAutofocus inputmode="email" appInputVerbatim="false">
</div>
<div class="form-group">
<label for="masterPassword">{{'masterPass' | i18n}}</label>
<input id="masterPassword" type="password" name="MasterPassword" class="form-control" [(ngModel)]="masterPassword" required
appInputVerbatim>
<input id="masterPassword" type="password" name="MasterPassword" class="form-control"
[(ngModel)]="masterPassword" required appInputVerbatim>
</div>
<div class="form-group">
<label for="recoveryCode">{{'recoveryCodeTitle' | i18n}}</label>
<input id="recoveryCode" class="text-monospace form-control" type="text" name="RecoveryCode" [(ngModel)]="recoveryCode" required
appInputVerbatim>
<input id="recoveryCode" class="text-monospace form-control" type="text" name="RecoveryCode"
[(ngModel)]="recoveryCode" required appInputVerbatim>
</div>
<hr>
<div class="d-flex">
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
<span>{{'submit' | i18n}}</span>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{'cancel' | i18n}}

View File

@@ -1,66 +1,149 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{'createAccount' | i18n}}</p>
<div class="card d-block">
<div class="card-body">
<app-callout title="{{'createOrganizationStep1' | i18n}}" type="info" icon="fa-thumb-tack" *ngIf="showCreateOrgMessage">
{{'createOrganizationCreatePersonalAccount' | i18n}}
</app-callout>
<div class="form-group">
<label for="email">{{'emailAddress' | i18n}}</label>
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required [appAutofocus]="email === ''"
inputmode="email" appInputVerbatim="false">
<small class="form-text text-muted">{{'emailAddressDesc' | i18n}}</small>
</div>
<div class="form-group">
<label for="name">{{'yourName' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" [appAutofocus]="email !== ''">
<small class="form-text text-muted">{{'yourNameDesc' | i18n}}</small>
</div>
<div class="form-group">
<label for="masterPassword">{{'masterPass' | i18n}}</label>
<div class="d-flex">
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}" name="MasterPassword" class="text-monospace form-control"
[(ngModel)]="masterPassword" required appInputVerbatim>
<button type="button" class="ml-1 btn btn-link" title="{{'toggleVisibility' | i18n}}" (click)="togglePassword(false)">
<i class="fa fa-lg" [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
</div>
<small class="form-text text-muted">{{'masterPassDesc' | i18n}}</small>
</div>
<div class="form-group">
<label for="masterPasswordRetype">{{'reTypeMasterPass' | i18n}}</label>
<div class="d-flex">
<input id="masterPasswordRetype" type="{{showPassword ? 'text' : 'password'}}" name="MasterPasswordRetype" class="text-monospace form-control"
[(ngModel)]="confirmMasterPassword" required appInputVerbatim>
<button type="button" class="ml-1 btn btn-link" title="{{'toggleVisibility' | i18n}}" (click)="togglePassword(true)">
<i class="fa fa-lg" [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
</div>
</div>
<div class="form-group">
<label for="hint">{{'masterPassHint' | i18n}}</label>
<input id="hint" class="form-control" type="text" name="Hint" [(ngModel)]="hint">
<small class="form-text text-muted">{{'masterPassHintDesc' | i18n}}</small>
</div>
<hr>
<div class="d-flex mb-2">
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
<span>{{'submit' | i18n}}</span>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
</button>
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{'cancel' | i18n}}
</a>
</div>
<small class="text-muted" *ngIf="showTerms">
{{'submitAgreePolicies' | i18n}}
<a href="https://bitwarden.com/terms/" target="_blank" rel="noopener">{{'termsOfService' | i18n}}</a>,
<a href="https://bitwarden.com/privacy/" target="_blank" rel="noopener">{{'privacyPolicy' | i18n}}</a>
</small>
<div class="layout" [ngClass]="['layout', layout]">
<header class="header" *ngIf="layout === 'enterprise2'">
<div class="container">
<div class="row">
<div class="col-7">
<img alt="Bitwarden" class="logo mb-2" src="../../images/register-layout/logo-horizontal-white.png">
</div>
</div>
</div>
</div>
</form>
</header>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
<div class="row">
<div class="col-7" *ngIf="layout">
<div class="mt-5">
<div *ngIf="layout === 'enterprise2'">
<h2>Companies globally trust Bitwarden for password management.</h2>
<p>Start your 7-day free trial!</p>
<p class="highlight">Quickly deploy your <b>organization</b></p>
<p>Use Bitwarden across all platforms</p>
<p>Collaborate and share securely</p>
<figure>
<figcaption>
<cite>
<img src="../../images/register-layout/wired-logo.png" alt="Wired">
</cite>
</figcaption>
<blockquote>
"Bitwarden has become a popular choice among open-source software advocates. After using it for a
few months, I can see why." - February 2020
</blockquote>
</figure>
</div>
<div *ngIf="layout === 'enterprise3'">
<p>Enterprise 3 layout</p>
</div>
<div *ngIf="layout === 'enterprise4'">
<p>Enterprise 4 layout</p>
</div>
</div>
</div>
<div [ngClass]="{'col-5': layout, 'col-12': !layout}">
<div class="row justify-content-md-center mt-5">
<div [ngClass]="{'col-5': !layout, 'col-12': layout}">
<p class="lead text-center mb-4" *ngIf="!layout">{{'createAccount' | i18n}}</p>
<div class="card d-block">
<div class="card-body">
<app-callout title="{{'createOrganizationStep1' | i18n}}" type="info" icon="fa-thumb-tack"
*ngIf="showCreateOrgMessage">
{{'createOrganizationCreatePersonalAccount' | i18n}}
</app-callout>
<div class="form-group">
<label for="email">{{'emailAddress' | i18n}}</label>
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email"
required [appAutofocus]="email === ''" inputmode="email" appInputVerbatim="false">
<small class="form-text text-muted">{{'emailAddressDesc' | i18n}}</small>
</div>
<div class="form-group">
<label for="name">{{'yourName' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name"
[appAutofocus]="email !== ''">
<small class="form-text text-muted">{{'yourNameDesc' | i18n}}</small>
</div>
<div class="form-group">
<app-callout type="info" *ngIf="enforcedPolicyOptions">
{{'masterPasswordPolicyInEffect' | i18n}}
<ul class="mb-0">
<li *ngIf="enforcedPolicyOptions?.minComplexity > 0">
{{'policyInEffectMinComplexity' | i18n : getPasswordScoreAlertDisplay()}}
</li>
<li *ngIf="enforcedPolicyOptions?.minLength > 0">
{{'policyInEffectMinLength' | i18n : enforcedPolicyOptions?.minLength.toString()}}
</li>
<li *ngIf="enforcedPolicyOptions?.requireUpper">
{{'policyInEffectUppercase' | i18n}}</li>
<li *ngIf="enforcedPolicyOptions?.requireLower">
{{'policyInEffectLowercase' | i18n}}</li>
<li *ngIf="enforcedPolicyOptions?.requireNumbers">
{{'policyInEffectNumbers' | i18n}}</li>
<li *ngIf="enforcedPolicyOptions?.requireSpecial">
{{'policyInEffectSpecial' | i18n : '!@#$%^&*'}}</li>
</ul>
</app-callout>
<label for="masterPassword">{{'masterPass' | i18n}}</label>
<div class="d-flex">
<div class="w-100">
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
name="MasterPassword" class="text-monospace form-control mb-1"
[(ngModel)]="masterPassword" (input)="updatePasswordStrength()" required
appInputVerbatim>
<app-password-strength [score]="masterPasswordScore" [showText]="true">
</app-password-strength>
</div>
<div>
<button type="button" class="ml-1 btn btn-link"
appA11yTitle="{{'toggleVisibility' | i18n}}"
(click)="togglePassword(false)">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
<div class="progress-bar invisible"></div>
</div>
</div>
<small class="form-text text-muted">{{'masterPassDesc' | i18n}}</small>
</div>
<div class="form-group">
<label for="masterPasswordRetype">{{'reTypeMasterPass' | i18n}}</label>
<div class="d-flex">
<input id="masterPasswordRetype" type="{{showPassword ? 'text' : 'password'}}"
name="MasterPasswordRetype" class="text-monospace form-control"
[(ngModel)]="confirmMasterPassword" required appInputVerbatim>
<button type="button" class="ml-1 btn btn-link"
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword(true)">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
</div>
</div>
<div class="form-group">
<label for="hint">{{'masterPassHint' | i18n}}</label>
<input id="hint" class="form-control" type="text" name="Hint" [(ngModel)]="hint">
<small class="form-text text-muted">{{'masterPassHintDesc' | i18n}}</small>
</div>
<hr>
<div class="d-flex mb-2">
<button type="submit" class="btn btn-primary btn-block btn-submit"
[disabled]="form.loading">
<span>{{'submit' | i18n}}</span>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"
aria-hidden="true"></i>
</button>
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{'cancel' | i18n}}
</a>
</div>
<small class="text-muted" *ngIf="showTerms">
{{'submitAgreePolicies' | i18n}}
<a href="https://bitwarden.com/terms/" target="_blank"
rel="noopener">{{'termsOfService' | i18n}}</a>,
<a href="https://bitwarden.com/privacy/" target="_blank"
rel="noopener">{{'privacyPolicy' | i18n}}</a>
</small>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>

View File

@@ -8,11 +8,19 @@ import { ApiService } from 'jslib/abstractions/api.service';
import { AuthService } from 'jslib/abstractions/auth.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { PolicyService } from 'jslib/abstractions/policy.service';
import { StateService } from 'jslib/abstractions/state.service';
import { RegisterComponent as BaseRegisterComponent } from 'jslib/angular/components/register.component';
import { MasterPasswordPolicyOptions } from 'jslib/models/domain/masterPasswordPolicyOptions';
import { Policy } from 'jslib/models/domain/policy';
import { PolicyData } from 'jslib/models/data/policyData';
import { ReferenceEventRequest } from 'jslib/models/request/referenceEventRequest';
@Component({
selector: 'app-register',
templateUrl: 'register.component.html',
@@ -20,17 +28,44 @@ import { RegisterComponent as BaseRegisterComponent } from 'jslib/angular/compon
export class RegisterComponent extends BaseRegisterComponent {
showCreateOrgMessage = false;
showTerms = true;
layout = '';
enforcedPolicyOptions: MasterPasswordPolicyOptions;
private policies: Policy[];
constructor(authService: AuthService, router: Router,
i18nService: I18nService, cryptoService: CryptoService,
apiService: ApiService, private route: ActivatedRoute,
stateService: StateService, platformUtilsService: PlatformUtilsService) {
super(authService, router, i18nService, cryptoService, apiService, stateService, platformUtilsService);
stateService: StateService, platformUtilsService: PlatformUtilsService,
passwordGenerationService: PasswordGenerationService, private policyService: PolicyService) {
super(authService, router, i18nService, cryptoService, apiService, stateService, platformUtilsService,
passwordGenerationService);
this.showTerms = !platformUtilsService.isSelfHost();
}
ngOnInit() {
this.route.queryParams.subscribe((qParams) => {
getPasswordScoreAlertDisplay() {
if (this.enforcedPolicyOptions == null) {
return '';
}
let str: string;
switch (this.enforcedPolicyOptions.minComplexity) {
case 4:
str = this.i18nService.t('strong');
break;
case 3:
str = this.i18nService.t('good');
break;
default:
str = this.i18nService.t('weak');
break;
}
return str + ' (' + this.enforcedPolicyOptions.minComplexity + ')';
}
async ngOnInit() {
const queryParamsSub = this.route.queryParams.subscribe((qParams) => {
this.referenceData = new ReferenceEventRequest();
if (qParams.email != null && qParams.email.indexOf('@') > -1) {
this.email = qParams.email;
}
@@ -38,9 +73,51 @@ export class RegisterComponent extends BaseRegisterComponent {
this.stateService.save('loginRedirect', { route: '/settings/premium' });
} else if (qParams.org != null) {
this.showCreateOrgMessage = true;
this.referenceData.flow = qParams.org;
this.stateService.save('loginRedirect',
{ route: '/settings/create-organization', qParams: { plan: qParams.org } });
}
if (qParams.layout != null) {
this.layout = this.referenceData.layout = qParams.layout;
}
if (qParams.reference != null) {
this.referenceData.id = qParams.reference;
} else {
this.referenceData.id = ('; ' + document.cookie).split('; reference=').pop().split(';').shift();
}
if (this.referenceData.id === '') {
this.referenceData.id = null;
}
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();
}
});
const invite = await this.stateService.get<any>('orgInvitation');
if (invite != null) {
try {
const policies = await this.apiService.getPoliciesByToken(invite.organizationId, invite.token,
invite.email, invite.organizationUserId);
if (policies.data != null) {
const policiesData = policies.data.map((p) => new PolicyData(p));
this.policies = policiesData.map((p) => new Policy(p));
}
} catch { }
}
if (this.policies != null) {
this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(this.policies);
}
}
async submit() {
if (this.enforcedPolicyOptions != null &&
!this.policyService.evaluateMasterPassword(this.masterPasswordScore, this.masterPassword,
this.enforcedPolicyOptions)) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPasswordPolicyRequirementsNotMet'));
return;
}
await super.submit();
}
}

View File

@@ -0,0 +1,85 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate autocomplete="off">
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{'setMasterPassword' | i18n}}</p>
<div class="card d-block">
<div class="card-body text-center" *ngIf="syncLoading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
{{'loading' | i18n}}
</div>
<div class="card-body" *ngIf="!syncLoading">
<app-callout type="info">{{'ssoCompleteRegistration' | i18n}}</app-callout>
<div class="form-group">
<app-callout type="info" *ngIf="enforcedPolicyOptions">
{{'masterPasswordPolicyInEffect' | i18n}}
<ul class="mb-0">
<li *ngIf="enforcedPolicyOptions?.minComplexity > 0">
{{'policyInEffectMinComplexity' | i18n : getPasswordScoreAlertDisplay()}}
</li>
<li *ngIf="enforcedPolicyOptions?.minLength > 0">
{{'policyInEffectMinLength' | i18n : enforcedPolicyOptions?.minLength.toString()}}
</li>
<li *ngIf="enforcedPolicyOptions?.requireUpper">
{{'policyInEffectUppercase' | i18n}}</li>
<li *ngIf="enforcedPolicyOptions?.requireLower">
{{'policyInEffectLowercase' | i18n}}</li>
<li *ngIf="enforcedPolicyOptions?.requireNumbers">
{{'policyInEffectNumbers' | i18n}}</li>
<li *ngIf="enforcedPolicyOptions?.requireSpecial">
{{'policyInEffectSpecial' | i18n : '!@#$%^&*'}}</li>
</ul>
</app-callout>
<label for="masterPassword">{{'masterPass' | i18n}}</label>
<div class="d-flex">
<div class="w-100">
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
name="MasterPasswordHash" class="text-monospace form-control mb-1"
[(ngModel)]="masterPassword" (input)="updatePasswordStrength()" required
appInputVerbatim>
<app-password-strength [score]="masterPasswordScore" [showText]="true">
</app-password-strength>
</div>
<div>
<button type="button" class="ml-1 btn btn-link"
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword(false)">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
<div class="progress-bar invisible"></div>
</div>
</div>
<small class="form-text text-muted">{{'masterPassDesc' | i18n}}</small>
</div>
<div class="form-group">
<label for="masterPasswordRetype">{{'reTypeMasterPass' | i18n}}</label>
<div class="d-flex">
<input id="masterPasswordRetype" type="{{showPassword ? 'text' : 'password'}}"
name="MasterPasswordRetype" class="text-monospace form-control"
[(ngModel)]="masterPasswordRetype" required appInputVerbatim>
<button type="button" class="ml-1 btn btn-link" appA11yTitle="{{'toggleVisibility' | i18n}}"
(click)="togglePassword(true)">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
</div>
</div>
<div class="form-group">
<label for="hint">{{'masterPassHint' | i18n}}</label>
<input id="hint" class="form-control" type="text" name="Hint" [(ngModel)]="hint">
<small class="form-text text-muted">{{'masterPassHintDesc' | i18n}}</small>
</div>
<hr>
<div class="d-flex">
<button type="submit" class="btn btn-primary btn-block 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 btn-block ml-2 mt-0" (click)="logOut()">
{{'logOut' | i18n}}
</button>
</div>
</div>
</div>
</div>
</div>
</form>

View File

@@ -0,0 +1,31 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { PolicyService } from 'jslib/abstractions/policy.service';
import { SyncService } from 'jslib/abstractions/sync.service';
import { UserService } from 'jslib/abstractions/user.service';
import {
SetPasswordComponent as BaseSetPasswordComponent,
} from 'jslib/angular/components/set-password.component';
@Component({
selector: 'app-set-password',
templateUrl: 'set-password.component.html',
})
export class SetPasswordComponent extends BaseSetPasswordComponent {
constructor(apiService: ApiService, i18nService: I18nService,
cryptoService: CryptoService, messagingService: MessagingService,
userService: UserService, passwordGenerationService: PasswordGenerationService,
platformUtilsService: PlatformUtilsService, policyService: PolicyService, router: Router,
syncService: SyncService) {
super(i18nService, cryptoService, messagingService, userService, passwordGenerationService,
platformUtilsService, policyService, router, apiService, syncService);
}
}

View File

@@ -0,0 +1,33 @@
<form #form (ngSubmit)="submit()" class="container" [appApiAction]="initiateSsoFormPromise" ngNativeValidate>
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<img src="../../images/logo-dark@2x.png" class="logo mb-2" alt="Bitwarden">
<div class="card d-block mt-4">
<div class="card-body" *ngIf="loggingIn">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
{{'loading' | i18n}}
</div>
<div class="card-body" *ngIf="!loggingIn">
<p>{{'ssoLogInWithOrgIdentifier' | i18n}}</p>
<div class="form-group">
<label for="identifier">{{'organizationIdentifier' | i18n}}</label>
<input id="identifier" class="form-control" type="text" name="Identifier"
[(ngModel)]="identifier" required appAutofocus>
</div>
<hr>
<div class="d-flex">
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
<span>
<i class="fa fa-sign-in" aria-hidden="true"></i> {{'logIn' | i18n}}
</span>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{'cancel' | i18n}}
</a>
</div>
</div>
</div>
</div>
</div>
</form>

View File

@@ -0,0 +1,58 @@
import { Component } from '@angular/core';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { ApiService } from 'jslib/abstractions/api.service';
import { AuthService } from 'jslib/abstractions/auth.service';
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StateService } from 'jslib/abstractions/state.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { SsoComponent as BaseSsoComponent } from 'jslib/angular/components/sso.component';
const IdentifierStorageKey = 'ssoOrgIdentifier';
@Component({
selector: 'app-sso',
templateUrl: 'sso.component.html',
})
export class SsoComponent extends BaseSsoComponent {
constructor(authService: AuthService, router: Router,
i18nService: I18nService, route: ActivatedRoute,
storageService: StorageService, stateService: StateService,
platformUtilsService: PlatformUtilsService, apiService: ApiService,
cryptoFunctionService: CryptoFunctionService,
passwordGenerationService: PasswordGenerationService) {
super(authService, router, i18nService, route, storageService, stateService, platformUtilsService,
apiService, cryptoFunctionService, passwordGenerationService);
this.redirectUri = window.location.origin + '/sso-connector.html';
this.clientId = 'web';
}
async ngOnInit() {
super.ngOnInit();
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
if (qParams.identifier != null) {
this.identifier = qParams.identifier;
} else {
const storedIdentifier = await this.storageService.get<string>(IdentifierStorageKey);
if (storedIdentifier != null) {
this.identifier = storedIdentifier;
}
}
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();
}
});
}
async submit() {
await this.storageService.save(IdentifierStorageKey, this.identifier);
super.submit();
}
}

View File

@@ -1,14 +1,15 @@
<div class="modal fade">
<div class="modal-dialog">
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="twoStepOptionsTitle">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">{{'twoStepOptions' | i18n}}</h2>
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
<h2 class="modal-title" id="twoStepOptionsTitle">{{'twoStepOptions' | i18n}}</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="list-group list-group-flush">
<a href="#" appStopClick *ngFor="let p of providers" (click)="choose(p)" class="list-group-item list-group-item-action">
<a href="#" appStopClick *ngFor="let p of providers" (click)="choose(p)"
class="list-group-item list-group-item-action">
<img [src]="'images/two-factor/' + p.type + '.png'" alt="" class="pull-right">
<h3>{{p.name}}</h3>
{{p.description}}

View File

@@ -1,19 +1,24 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate autocomplete="off">
<div class="row justify-content-md-center mt-5">
<div class="col-5" [ngClass]="{'col-9': selectedProviderType === providerType.Duo || selectedProviderType === providerType.OrganizationDuo}">
<div class="col-5"
[ngClass]="{'col-9': selectedProviderType === providerType.Duo || selectedProviderType === providerType.OrganizationDuo}">
<p class="lead text-center mb-4">{{title}}</p>
<div class="card d-block">
<div class="card-body">
<ng-container *ngIf="selectedProviderType === providerType.Email || selectedProviderType === providerType.Authenticator">
<p *ngIf="selectedProviderType === providerType.Authenticator">{{'enterVerificationCodeApp' | i18n}}</p>
<ng-container
*ngIf="selectedProviderType === providerType.Email || selectedProviderType === providerType.Authenticator">
<p *ngIf="selectedProviderType === providerType.Authenticator">
{{'enterVerificationCodeApp' | i18n}}</p>
<p *ngIf="selectedProviderType === providerType.Email">
{{'enterVerificationCodeEmail' | i18n : twoFactorEmail}}
</p>
<div class="form-group">
<label for="code" class="sr-only">{{'verificationCode' | i18n}}</label>
<input id="code" type="text" name="Code" class="form-control" [(ngModel)]="token" required appAutofocus inputmode="tel" appInputVerbatim>
<input id="code" type="text" name="Code" class="form-control" [(ngModel)]="token" required
appAutofocus inputmode="tel" appInputVerbatim>
<small class="form-text" *ngIf="selectedProviderType === providerType.Email">
<a href="#" appStopClick (click)="sendEmail(true)" [appApiAction]="emailPromise" *ngIf="selectedProviderType === providerType.Email">
<a href="#" appStopClick (click)="sendEmail(true)" [appApiAction]="emailPromise"
*ngIf="selectedProviderType === providerType.Email">
{{'sendVerificationCodeEmailAgain' | i18n}}
</a>
</small>
@@ -24,13 +29,15 @@
<img src="../../images/yubikey.jpg" class="rounded img-fluid mb-3" alt="">
<div class="form-group">
<label for="code" class="sr-only">{{'verificationCode' | i18n}}</label>
<input id="code" type="password" name="Code" class="form-control" [(ngModel)]="token" required appAutofocus appInputVerbatim
autocomplete="new-password">
<input id="code" type="password" name="Code" class="form-control" [(ngModel)]="token"
required appAutofocus appInputVerbatim autocomplete="new-password">
</div>
</ng-container>
<ng-container *ngIf="selectedProviderType === providerType.U2f">
<p class="text-center" *ngIf="!u2fReady">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"
aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</p>
<ng-container *ngIf="u2fReady">
<p class="text-center">{{'insertU2f' | i18n}}</p>
@@ -43,9 +50,11 @@
<iframe id="duo_iframe"></iframe>
</div>
</ng-container>
<i class="fa fa-spinner text-muted fa-spin pull-right" title="{{'loading' | i18n}}" *ngIf="form.loading && selectedProviderType === providerType.U2f"></i>
<i class="fa fa-spinner text-muted fa-spin pull-right" title="{{'loading' | i18n}}"
*ngIf="form.loading && selectedProviderType === providerType.U2f" aria-hidden="true"></i>
<div class="form-check" *ngIf="selectedProviderType != null">
<input id="remember" type="checkbox" name="Remember" class="form-check-input" [(ngModel)]="remember">
<input id="remember" type="checkbox" name="Remember" class="form-check-input"
[(ngModel)]="remember">
<label for="remember" class="form-check-label">{{'rememberMe' | i18n}}</label>
</div>
<ng-container *ngIf="selectedProviderType == null">
@@ -54,12 +63,13 @@
</ng-container>
<hr>
<div class="d-flex mb-3">
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading" *ngIf="selectedProviderType != null && selectedProviderType !== providerType.Duo &&
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading"
*ngIf="selectedProviderType != null && selectedProviderType !== providerType.Duo &&
selectedProviderType !== providerType.OrganizationDuo && selectedProviderType !== providerType.U2f">
<span>
<i class="fa fa-sign-in"></i> {{'continue' | i18n}}
<i class="fa fa-sign-in" aria-hidden="true"></i> {{'continue' | i18n}}
</span>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{'cancel' | i18n}}

View File

@@ -19,6 +19,7 @@ import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StateService } from 'jslib/abstractions/state.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { TwoFactorComponent as BaseTwoFactorComponent } from 'jslib/angular/components/two-factor.component';
@@ -27,13 +28,15 @@ import { TwoFactorComponent as BaseTwoFactorComponent } from 'jslib/angular/comp
templateUrl: 'two-factor.component.html',
})
export class TwoFactorComponent extends BaseTwoFactorComponent {
@ViewChild('twoFactorOptions', { read: ViewContainerRef }) twoFactorOptionsModal: ViewContainerRef;
@ViewChild('twoFactorOptions', { read: ViewContainerRef, static: true }) twoFactorOptionsModal: ViewContainerRef;
constructor(authService: AuthService, router: Router,
i18nService: I18nService, apiService: ApiService,
platformUtilsService: PlatformUtilsService, private stateService: StateService,
environmentService: EnvironmentService, private componentFactoryResolver: ComponentFactoryResolver) {
super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService);
platformUtilsService: PlatformUtilsService, stateService: StateService,
environmentService: EnvironmentService, private componentFactoryResolver: ComponentFactoryResolver,
storageService: StorageService) {
super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService,
stateService, storageService);
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
}

View File

@@ -2,7 +2,8 @@
<div>
<img 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}}"></i>
<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>

View File

@@ -13,7 +13,7 @@
<div class="d-flex">
<button type="submit" class="btn btn-danger btn-block btn-submit" [disabled]="form.loading">
<span>{{'deleteAccount' | i18n}}</span>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{'cancel' | i18n}}

View File

@@ -15,6 +15,8 @@ import { LoginComponent } from './accounts/login.component';
import { RecoverDeleteComponent } from './accounts/recover-delete.component';
import { RecoverTwoFactorComponent } from './accounts/recover-two-factor.component';
import { RegisterComponent } from './accounts/register.component';
import { SetPasswordComponent } from './accounts/set-password.component';
import { SsoComponent } from './accounts/sso.component';
import { TwoFactorComponent } from './accounts/two-factor.component';
import { VerifyEmailTokenComponent } from './accounts/verify-email-token.component';
import { VerifyRecoverDeleteComponent } from './accounts/verify-recover-delete.component';
@@ -24,17 +26,34 @@ import { EventsComponent as OrgEventsComponent } from './organizations/manage/ev
import { GroupsComponent as OrgGroupsComponent } from './organizations/manage/groups.component';
import { ManageComponent as OrgManageComponent } from './organizations/manage/manage.component';
import { PeopleComponent as OrgPeopleComponent } from './organizations/manage/people.component';
import { PoliciesComponent as OrgPoliciesComponent } from './organizations/manage/policies.component';
import { AccountComponent as OrgAccountComponent } from './organizations/settings/account.component';
import { OrganizationBillingComponent } from './organizations/settings/organization-billing.component';
import { OrganizationSubscriptionComponent } from './organizations/settings/organization-subscription.component';
import { SettingsComponent as OrgSettingsComponent } from './organizations/settings/settings.component';
import {
TwoFactorSetupComponent as OrgTwoFactorSetupComponent,
} from './organizations/settings/two-factor-setup.component';
import { ExportComponent as OrgExportComponent } from './organizations/tools/export.component';
import {
ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent,
} from './organizations/tools/exposed-passwords-report.component';
import { ImportComponent as OrgImportComponent } from './organizations/tools/import.component';
import {
InactiveTwoFactorReportComponent as OrgInactiveTwoFactorReportComponent,
} from './organizations/tools/inactive-two-factor-report.component';
import {
ReusedPasswordsReportComponent as OrgReusedPasswordsReportComponent,
} from './organizations/tools/reused-passwords-report.component';
import { ToolsComponent as OrgToolsComponent } from './organizations/tools/tools.component';
import {
UnsecuredWebsitesReportComponent as OrgUnsecuredWebsitesReportComponent,
} from './organizations/tools/unsecured-websites-report.component';
import {
WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent,
} from './organizations/tools/weak-passwords-report.component';
import { VaultComponent as OrgVaultComponent } from './organizations/vault/vault.component';
@@ -47,12 +66,18 @@ import { PremiumComponent } from './settings/premium.component';
import { SettingsComponent } from './settings/settings.component';
import { TwoFactorSetupComponent } from './settings/two-factor-setup.component';
import { UserBillingComponent } from './settings/user-billing.component';
import { UserSubscriptionComponent } from './settings/user-subscription.component';
import { BreachReportComponent } from './tools/breach-report.component';
import { ExportComponent } from './tools/export.component';
import { ExposedPasswordsReportComponent } from './tools/exposed-passwords-report.component';
import { ImportComponent } from './tools/import.component';
import { InactiveTwoFactorReportComponent } from './tools/inactive-two-factor-report.component';
import { PasswordGeneratorComponent } from './tools/password-generator.component';
import { ReusedPasswordsReportComponent } from './tools/reused-passwords-report.component';
import { ToolsComponent } from './tools/tools.component';
import { UnsecuredWebsitesReportComponent } from './tools/unsecured-websites-report.component';
import { WeakPasswordsReportComponent } from './tools/weak-passwords-report.component';
import { VaultComponent } from './vault/vault.component';
@@ -76,6 +101,15 @@ const routes: Routes = [
canActivate: [UnauthGuardService],
data: { titleId: 'createAccount' },
},
{
path: 'sso', component: SsoComponent,
canActivate: [UnauthGuardService],
data: { titleId: 'enterpriseSingleSignOn' },
},
{
path: 'set-password', component: SetPasswordComponent,
data: { titleId: 'setMasterPassword' },
},
{
path: 'hint', component: HintComponent,
canActivate: [UnauthGuardService],
@@ -125,7 +159,12 @@ const routes: Routes = [
{ path: 'domain-rules', component: DomainRulesComponent, data: { titleId: 'domainRules' } },
{ path: 'two-factor', component: TwoFactorSetupComponent, data: { titleId: 'twoStepLogin' } },
{ path: 'premium', component: PremiumComponent, data: { titleId: 'goPremium' } },
{ path: 'billing', component: UserBillingComponent, data: { titleId: 'billingAndLicensing' } },
{ path: 'billing', component: UserBillingComponent, data: { titleId: 'billing' } },
{
path: 'subscription',
component: UserSubscriptionComponent,
data: { titleId: 'premiumMembership' },
},
{ path: 'organizations', component: OrganizationsComponent, data: { titleId: 'organizations' } },
{
path: 'create-organization',
@@ -148,6 +187,31 @@ const routes: Routes = [
data: { titleId: 'passwordGenerator' },
},
{ path: 'breach-report', component: BreachReportComponent, data: { titleId: 'dataBreachReport' } },
{
path: 'reused-passwords-report',
component: ReusedPasswordsReportComponent,
data: { titleId: 'reusedPasswordsReport' },
},
{
path: 'unsecured-websites-report',
component: UnsecuredWebsitesReportComponent,
data: { titleId: 'unsecuredWebsitesReport' },
},
{
path: 'weak-passwords-report',
component: WeakPasswordsReportComponent,
data: { titleId: 'weakPasswordsReport' },
},
{
path: 'exposed-passwords-report',
component: ExposedPasswordsReportComponent,
data: { titleId: 'exposedPasswordsReport' },
},
{
path: 'inactive-two-factor-report',
component: InactiveTwoFactorReportComponent,
data: { titleId: 'inactive2faReport' },
},
],
},
],
@@ -168,19 +232,51 @@ const routes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: 'import' },
{ path: 'import', component: OrgImportComponent, data: { titleId: 'importData' } },
{ path: 'export', component: OrgExportComponent, data: { titleId: 'exportVault' } },
{
path: 'exposed-passwords-report',
component: OrgExposedPasswordsReportComponent,
data: { titleId: 'exposedPasswordsReport' },
},
{
path: 'inactive-two-factor-report',
component: OrgInactiveTwoFactorReportComponent,
data: { titleId: 'inactive2faReport' },
},
{
path: 'reused-passwords-report',
component: OrgReusedPasswordsReportComponent,
data: { titleId: 'reusedPasswordsReport' },
},
{
path: 'unsecured-websites-report',
component: OrgUnsecuredWebsitesReportComponent,
data: { titleId: 'unsecuredWebsitesReport' },
},
{
path: 'weak-passwords-report',
component: OrgWeakPasswordsReportComponent,
data: { titleId: 'weakPasswordsReport' },
},
],
},
{
path: 'manage',
component: OrgManageComponent,
canActivate: [OrganizationTypeGuardService],
data: { allowedTypes: [OrganizationUserType.Owner, OrganizationUserType.Admin] },
data: {
allowedTypes: [
OrganizationUserType.Owner,
OrganizationUserType.Admin,
OrganizationUserType.Manager,
],
},
children: [
{ path: '', pathMatch: 'full', redirectTo: 'people' },
{ path: 'collections', component: OrgManageCollectionsComponent, data: { titleId: 'collections' } },
{ path: 'events', component: OrgEventsComponent, data: { titleId: 'eventLogs' } },
{ path: 'groups', component: OrgGroupsComponent, data: { titleId: 'groups' } },
{ path: 'people', component: OrgPeopleComponent, data: { titleId: 'people' } },
{ path: 'policies', component: OrgPoliciesComponent, data: { titleId: 'policies' } },
],
},
{
@@ -195,7 +291,12 @@ const routes: Routes = [
{
path: 'billing',
component: OrganizationBillingComponent,
data: { titleId: 'billingAndLicensing' },
data: { titleId: 'billing' },
},
{
path: 'subscription',
component: OrganizationSubscriptionComponent,
data: { titleId: 'subscription' },
},
],
},

View File

@@ -1,2 +1,2 @@
<toaster-container [toasterconfig]="toasterConfig"></toaster-container>
<toaster-container [toasterconfig]="toasterConfig" aria-live="polite"></toaster-container>
<router-outlet></router-outlet>

View File

@@ -1,6 +1,5 @@
import * as jq from 'jquery';
import * as _swal from 'sweetalert';
import { SweetAlert } from 'sweetalert/typings/core';
import Swal from 'sweetalert2/src/sweetalert2.js';
import {
BodyOutputType,
@@ -33,25 +32,26 @@ import { AuthService } from 'jslib/abstractions/auth.service';
import { CipherService } from 'jslib/abstractions/cipher.service';
import { CollectionService } from 'jslib/abstractions/collection.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { EventService } from 'jslib/abstractions/event.service';
import { FolderService } from 'jslib/abstractions/folder.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { LockService } from 'jslib/abstractions/lock.service';
import { NotificationsService } from 'jslib/abstractions/notifications.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { PolicyService } from 'jslib/abstractions/policy.service';
import { SearchService } from 'jslib/abstractions/search.service';
import { SettingsService } from 'jslib/abstractions/settings.service';
import { StateService } from 'jslib/abstractions/state.service';
import { SyncService } from 'jslib/abstractions/sync.service';
import { TokenService } from 'jslib/abstractions/token.service';
import { UserService } from 'jslib/abstractions/user.service';
import { VaultTimeoutService } from 'jslib/abstractions/vaultTimeout.service';
import { ConstantsService } from 'jslib/services/constants.service';
import { RouterService } from './services/router.service';
const BroadcasterSubscriptionId = 'AppComponent';
// Hack due to Angular 5.2 bug
const swal: SweetAlert = _swal as any;
const IdleTimeout = 60000 * 10; // 10 minutes
@Component({
@@ -78,10 +78,12 @@ export class AppComponent implements OnDestroy, OnInit {
private authService: AuthService, private router: Router, private analytics: Angulartics2,
private toasterService: ToasterService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private ngZone: NgZone,
private lockService: LockService, private storageService: StorageService,
private vaultTimeoutService: VaultTimeoutService, private storageService: StorageService,
private cryptoService: CryptoService, private collectionService: CollectionService,
private sanitizer: DomSanitizer, private searchService: SearchService,
private notificationsService: NotificationsService) { }
private notificationsService: NotificationsService, private routerService: RouterService,
private stateService: StateService, private eventService: EventService,
private policyService: PolicyService) { }
ngOnInit() {
this.ngZone.runOutsideAngular(() => {
@@ -101,16 +103,22 @@ export class AppComponent implements OnDestroy, OnInit {
case 'unlocked':
this.notificationsService.updateConnection(false);
break;
case 'authBlocked':
this.router.navigate(['/']);
break;
case 'logout':
this.logOut(!!message.expired);
break;
case 'lockVault':
await this.lockService.lock();
await this.vaultTimeoutService.lock();
break;
case 'locked':
this.notificationsService.updateConnection(false);
this.router.navigate(['lock']);
break;
case 'lockedUrl':
window.setTimeout(() => this.routerService.setPreviousUrl(message.url), 500);
break;
case 'syncStarted':
break;
case 'syncCompleted':
@@ -140,6 +148,9 @@ export class AppComponent implements OnDestroy, OnInit {
properties: { label: message.label },
});
break;
case 'setFullWidth':
this.setFullWidth();
break;
default:
break;
}
@@ -154,10 +165,12 @@ export class AppComponent implements OnDestroy, OnInit {
}
if (document.querySelector('.swal-modal') != null) {
swal.close(undefined);
Swal.close(undefined);
}
}
});
this.setFullWidth();
}
ngOnDestroy() {
@@ -165,9 +178,11 @@ export class AppComponent implements OnDestroy, OnInit {
}
private async logOut(expired: boolean) {
await this.eventService.uploadEvents();
const userId = await this.userService.getUserId();
await Promise.all([
this.eventService.clearEvents(),
this.syncService.setLastSync(new Date(0)),
this.tokenService.clearToken(),
this.cryptoService.clearKeys(),
@@ -176,7 +191,9 @@ export class AppComponent implements OnDestroy, OnInit {
this.cipherService.clear(userId),
this.folderService.clear(userId),
this.collectionService.clear(userId),
this.policyService.clear(userId),
this.passwordGenerationService.clear(),
this.stateService.purge(),
]);
this.searchService.clearIndex();
@@ -186,6 +203,8 @@ export class AppComponent implements OnDestroy, OnInit {
this.toasterService.popAsync('warning', this.i18nService.t('loggedOut'),
this.i18nService.t('loginExpired'));
}
Swal.close();
this.router.navigate(['/']);
});
}
@@ -250,4 +269,13 @@ export class AppComponent implements OnDestroy, OnInit {
this.notificationsService.reconnectFromActivity();
}
}
private async setFullWidth() {
const enableFullWidth = await this.storageService.get<boolean>('enableFullWidth');
if (enableFullWidth) {
document.body.classList.add('full-width');
} else {
document.body.classList.remove('full-width');
}
}
}

View File

@@ -3,9 +3,11 @@ import 'core-js';
import { ToasterModule } from 'angular2-toaster';
import { Angulartics2Module } from 'angulartics2';
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { AppRoutingModule } from './app-routing.module';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
@@ -17,7 +19,7 @@ import { AppComponent } from './app.component';
import { ModalComponent } from './modal.component';
import { AvatarComponent } from './components/avatar.component';
import { CalloutComponent } from './components/callout.component';
import { PasswordStrengthComponent } from './components/password-strength.component';
import { FooterComponent } from './layouts/footer.component';
import { FrontendLayoutComponent } from './layouts/frontend-layout.component';
@@ -32,6 +34,8 @@ import { LoginComponent } from './accounts/login.component';
import { RecoverDeleteComponent } from './accounts/recover-delete.component';
import { RecoverTwoFactorComponent } from './accounts/recover-two-factor.component';
import { RegisterComponent } from './accounts/register.component';
import { SetPasswordComponent } from './accounts/set-password.component';
import { SsoComponent } from './accounts/sso.component';
import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component';
import { TwoFactorComponent } from './accounts/two-factor.component';
import { VerifyEmailTokenComponent } from './accounts/verify-email-token.component';
@@ -48,21 +52,44 @@ import { GroupAddEditComponent as OrgGroupAddEditComponent } from './organizatio
import { GroupsComponent as OrgGroupsComponent } from './organizations/manage/groups.component';
import { ManageComponent as OrgManageComponent } from './organizations/manage/manage.component';
import { PeopleComponent as OrgPeopleComponent } from './organizations/manage/people.component';
import { PoliciesComponent as OrgPoliciesComponent } from './organizations/manage/policies.component';
import { PolicyEditComponent as OrgPolicyEditComponent } from './organizations/manage/policy-edit.component';
import { UserAddEditComponent as OrgUserAddEditComponent } from './organizations/manage/user-add-edit.component';
import { UserConfirmComponent as OrgUserConfirmComponent } from './organizations/manage/user-confirm.component';
import { UserGroupsComponent as OrgUserGroupsComponent } from './organizations/manage/user-groups.component';
import { AccountComponent as OrgAccountComponent } from './organizations/settings/account.component';
import { AdjustSeatsComponent } from './organizations/settings/adjust-seats.component';
import { ApiKeyComponent as OrgApiKeyComponent } from './organizations/settings/api-key.component';
import { ChangePlanComponent } from './organizations/settings/change-plan.component';
import { DeleteOrganizationComponent } from './organizations/settings/delete-organization.component';
import { DownloadLicenseComponent } from './organizations/settings/download-license.component';
import { OrganizationBillingComponent } from './organizations/settings/organization-billing.component';
import { OrganizationSubscriptionComponent } from './organizations/settings/organization-subscription.component';
import { RotateApiKeyComponent as OrgRotateApiKeyComponent } from './organizations/settings/rotate-api-key.component';
import { SettingsComponent as OrgSettingComponent } from './organizations/settings/settings.component';
import {
TwoFactorSetupComponent as OrgTwoFactorSetupComponent,
} from './organizations/settings/two-factor-setup.component';
import { ExportComponent as OrgExportComponent } from './organizations/tools/export.component';
import {
ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent,
} from './organizations/tools/exposed-passwords-report.component';
import { ImportComponent as OrgImportComponent } from './organizations/tools/import.component';
import {
InactiveTwoFactorReportComponent as OrgInactiveTwoFactorReportComponent,
} from './organizations/tools/inactive-two-factor-report.component';
import {
ReusedPasswordsReportComponent as OrgReusedPasswordsReportComponent,
} from './organizations/tools/reused-passwords-report.component';
import { ToolsComponent as OrgToolsComponent } from './organizations/tools/tools.component';
import {
UnsecuredWebsitesReportComponent as OrgUnsecuredWebsitesReportComponent,
} from './organizations/tools/unsecured-websites-report.component';
import {
WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent,
} from './organizations/tools/weak-passwords-report.component';
import { AddEditComponent as OrgAddEditComponent } from './organizations/vault/add-edit.component';
import { AttachmentsComponent as OrgAttachmentsComponent } from './organizations/vault/attachments.component';
@@ -72,6 +99,7 @@ import { GroupingsComponent as OrgGroupingsComponent } from './organizations/vau
import { VaultComponent as OrgVaultComponent } from './organizations/vault/vault.component';
import { AccountComponent } from './settings/account.component';
import { AddCreditComponent } from './settings/add-credit.component';
import { AdjustPaymentComponent } from './settings/adjust-payment.component';
import { AdjustStorageComponent } from './settings/adjust-storage.component';
import { ChangeEmailComponent } from './settings/change-email.component';
@@ -81,13 +109,16 @@ import { CreateOrganizationComponent } from './settings/create-organization.comp
import { DeauthorizeSessionsComponent } from './settings/deauthorize-sessions.component';
import { DeleteAccountComponent } from './settings/delete-account.component';
import { DomainRulesComponent } from './settings/domain-rules.component';
import { LinkSsoComponent } from './settings/link-sso.component';
import { OptionsComponent } from './settings/options.component';
import { OrganizationPlansComponent } from './settings/organization-plans.component';
import { OrganizationsComponent } from './settings/organizations.component';
import { PaymentComponent } from './settings/payment.component';
import { PremiumComponent } from './settings/premium.component';
import { ProfileComponent } from './settings/profile.component';
import { PurgeVaultComponent } from './settings/purge-vault.component';
import { SettingsComponent } from './settings/settings.component';
import { TaxInfoComponent } from './settings/tax-info.component';
import { TwoFactorAuthenticatorComponent } from './settings/two-factor-authenticator.component';
import { TwoFactorDuoComponent } from './settings/two-factor-duo.component';
import { TwoFactorEmailComponent } from './settings/two-factor-email.component';
@@ -99,19 +130,27 @@ import { TwoFactorYubiKeyComponent } from './settings/two-factor-yubikey.compone
import { UpdateKeyComponent } from './settings/update-key.component';
import { UpdateLicenseComponent } from './settings/update-license.component';
import { UserBillingComponent } from './settings/user-billing.component';
import { UserSubscriptionComponent } from './settings/user-subscription.component';
import { VerifyEmailComponent } from './settings/verify-email.component';
import { BreachReportComponent } from './tools/breach-report.component';
import { ExportComponent } from './tools/export.component';
import { ExposedPasswordsReportComponent } from './tools/exposed-passwords-report.component';
import { ImportComponent } from './tools/import.component';
import { InactiveTwoFactorReportComponent } from './tools/inactive-two-factor-report.component';
import { PasswordGeneratorHistoryComponent } from './tools/password-generator-history.component';
import { PasswordGeneratorComponent } from './tools/password-generator.component';
import { ReusedPasswordsReportComponent } from './tools/reused-passwords-report.component';
import { ToolsComponent } from './tools/tools.component';
import { UnsecuredWebsitesReportComponent } from './tools/unsecured-websites-report.component';
import { WeakPasswordsReportComponent } from './tools/weak-passwords-report.component';
import { AddEditComponent } from './vault/add-edit.component';
import { AttachmentsComponent } from './vault/attachments.component';
import { BulkActionsComponent } from './vault/bulk-actions.component';
import { BulkDeleteComponent } from './vault/bulk-delete.component';
import { BulkMoveComponent } from './vault/bulk-move.component';
import { BulkRestoreComponent } from './vault/bulk-restore.component';
import { BulkShareComponent } from './vault/bulk-share.component';
import { CiphersComponent } from './vault/ciphers.component';
import { CollectionsComponent } from './vault/collections.component';
@@ -120,30 +159,42 @@ import { GroupingsComponent } from './vault/groupings.component';
import { ShareComponent } from './vault/share.component';
import { VaultComponent } from './vault/vault.component';
import { CalloutComponent } from 'jslib/angular/components/callout.component';
import { IconComponent } from 'jslib/angular/components/icon.component';
import { A11yTitleDirective } from 'jslib/angular/directives/a11y-title.directive';
import { ApiActionDirective } from 'jslib/angular/directives/api-action.directive';
import { AutofocusDirective } from 'jslib/angular/directives/autofocus.directive';
import { BlurClickDirective } from 'jslib/angular/directives/blur-click.directive';
import { BoxRowDirective } from 'jslib/angular/directives/box-row.directive';
import { FallbackSrcDirective } from 'jslib/angular/directives/fallback-src.directive';
import { InputVerbatimDirective } from 'jslib/angular/directives/input-verbatim.directive';
import { SelectCopyDirective } from 'jslib/angular/directives/select-copy.directive';
import { StopClickDirective } from 'jslib/angular/directives/stop-click.directive';
import { StopPropDirective } from 'jslib/angular/directives/stop-prop.directive';
import { TrueFalseValueDirective } from 'jslib/angular/directives/true-false-value.directive';
import { ColorPasswordPipe } from 'jslib/angular/pipes/color-password.pipe';
import { I18nPipe } from 'jslib/angular/pipes/i18n.pipe';
import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
import { SearchPipe } from 'jslib/angular/pipes/search.pipe';
import { registerLocaleData } from '@angular/common';
import localeCa from '@angular/common/locales/ca';
import localeCs from '@angular/common/locales/cs';
import localeDa from '@angular/common/locales/da';
import localeDe from '@angular/common/locales/de';
import localeEl from '@angular/common/locales/el';
import localeEnGb from '@angular/common/locales/en-GB';
import localeEs from '@angular/common/locales/es';
import localeEt from '@angular/common/locales/et';
import localeFr from '@angular/common/locales/fr';
import localeHe from '@angular/common/locales/he';
import localeIt from '@angular/common/locales/it';
import localeJa from '@angular/common/locales/ja';
import localeKo from '@angular/common/locales/ko';
import localeLv from '@angular/common/locales/lv';
import localeMl from '@angular/common/locales/ml';
import localeNb from '@angular/common/locales/nb';
import localeNl from '@angular/common/locales/nl';
import localePl from '@angular/common/locales/pl';
@@ -152,15 +203,25 @@ import localePtPt from '@angular/common/locales/pt-PT';
import localeRu from '@angular/common/locales/ru';
import localeSk from '@angular/common/locales/sk';
import localeSv from '@angular/common/locales/sv';
import localeUk from '@angular/common/locales/uk';
import localeZhCn from '@angular/common/locales/zh-Hans';
import localeZhTw from '@angular/common/locales/zh-Hant';
registerLocaleData(localeCa, 'ca');
registerLocaleData(localeCs, 'cs');
registerLocaleData(localeDa, 'da');
registerLocaleData(localeDe, 'de');
registerLocaleData(localeEl, 'el');
registerLocaleData(localeEnGb, 'en-GB');
registerLocaleData(localeEs, 'es');
registerLocaleData(localeEt, 'et');
registerLocaleData(localeFr, 'fr');
registerLocaleData(localeHe, 'he');
registerLocaleData(localeIt, 'it');
registerLocaleData(localeJa, 'ja');
registerLocaleData(localeKo, 'ko');
registerLocaleData(localeLv, 'lv');
registerLocaleData(localeMl, 'ml');
registerLocaleData(localeNb, 'nb');
registerLocaleData(localeNl, 'nl');
registerLocaleData(localePl, 'pl');
@@ -169,7 +230,9 @@ registerLocaleData(localePtPt, 'pt-PT');
registerLocaleData(localeRu, 'ru');
registerLocaleData(localeSk, 'sk');
registerLocaleData(localeSv, 'sv');
registerLocaleData(localeUk, 'uk');
registerLocaleData(localeZhCn, 'zh-CN');
registerLocaleData(localeZhTw, 'zh-TW');
@NgModule({
imports: [
@@ -178,16 +241,21 @@ registerLocaleData(localeZhCn, 'zh-CN');
FormsModule,
AppRoutingModule,
ServicesModule,
Angulartics2Module.forRoot([Angulartics2GoogleAnalytics], {
Angulartics2Module.forRoot({
pageTracking: {
clearQueryParams: true,
},
}),
ToasterModule.forRoot(),
InfiniteScrollModule,
DragDropModule,
],
declarations: [
A11yTitleDirective,
AcceptOrganizationComponent,
AccountComponent,
SetPasswordComponent,
AddCreditComponent,
AddEditComponent,
AdjustPaymentComponent,
AdjustSeatsComponent,
@@ -200,31 +268,39 @@ registerLocaleData(localeZhCn, 'zh-CN');
BlurClickDirective,
BoxRowDirective,
BreachReportComponent,
BulkActionsComponent,
BulkDeleteComponent,
BulkMoveComponent,
BulkRestoreComponent,
BulkShareComponent,
CalloutComponent,
ChangeEmailComponent,
ChangeKdfComponent,
ChangePasswordComponent,
ChangePlanComponent,
CiphersComponent,
CollectionsComponent,
ColorPasswordPipe,
CreateOrganizationComponent,
DeauthorizeSessionsComponent,
DeleteAccountComponent,
DeleteOrganizationComponent,
DomainRulesComponent,
DownloadLicenseComponent,
ExportComponent,
ExposedPasswordsReportComponent,
FallbackSrcDirective,
FolderAddEditComponent,
FooterComponent,
FrontendLayoutComponent,
GroupingsComponent,
HintComponent,
IconComponent,
I18nPipe,
IconComponent,
ImportComponent,
InactiveTwoFactorReportComponent,
InputVerbatimDirective,
LinkSsoComponent,
LockComponent,
LoginComponent,
ModalComponent,
@@ -232,7 +308,10 @@ registerLocaleData(localeZhCn, 'zh-CN');
OptionsComponent,
OrgAccountComponent,
OrgAddEditComponent,
OrgApiKeyComponent,
OrganizationBillingComponent,
OrganizationPlansComponent,
OrganizationSubscriptionComponent,
OrgAttachmentsComponent,
OrgCiphersComponent,
OrgCollectionAddEditComponent,
@@ -241,23 +320,33 @@ registerLocaleData(localeZhCn, 'zh-CN');
OrgEntityUsersComponent,
OrgEventsComponent,
OrgExportComponent,
OrgExposedPasswordsReportComponent,
OrgImportComponent,
OrgInactiveTwoFactorReportComponent,
OrgGroupAddEditComponent,
OrgGroupingsComponent,
OrgGroupsComponent,
OrgManageCollectionsComponent,
OrgManageComponent,
OrgPeopleComponent,
OrgPolicyEditComponent,
OrgPoliciesComponent,
OrgReusedPasswordsReportComponent,
OrgRotateApiKeyComponent,
OrgSettingComponent,
OrgToolsComponent,
OrgTwoFactorSetupComponent,
OrgUserAddEditComponent,
OrgUserConfirmComponent,
OrgUserGroupsComponent,
OrganizationsComponent,
OrganizationLayoutComponent,
OrgUnsecuredWebsitesReportComponent,
OrgVaultComponent,
OrgWeakPasswordsReportComponent,
PasswordGeneratorComponent,
PasswordGeneratorHistoryComponent,
PasswordStrengthComponent,
PaymentComponent,
PremiumComponent,
ProfileComponent,
@@ -265,12 +354,16 @@ registerLocaleData(localeZhCn, 'zh-CN');
RecoverDeleteComponent,
RecoverTwoFactorComponent,
RegisterComponent,
ReusedPasswordsReportComponent,
SearchCiphersPipe,
SearchPipe,
SelectCopyDirective,
SettingsComponent,
ShareComponent,
SsoComponent,
StopClickDirective,
StopPropDirective,
TaxInfoComponent,
ToolsComponent,
TrueFalseValueDirective,
TwoFactorAuthenticatorComponent,
@@ -283,20 +376,25 @@ registerLocaleData(localeZhCn, 'zh-CN');
TwoFactorU2fComponent,
TwoFactorVerifyComponent,
TwoFactorYubiKeyComponent,
UnsecuredWebsitesReportComponent,
UpdateKeyComponent,
UpdateLicenseComponent,
UserBillingComponent,
UserLayoutComponent,
UserSubscriptionComponent,
VaultComponent,
VerifyEmailComponent,
VerifyEmailTokenComponent,
VerifyRecoverDeleteComponent,
WeakPasswordsReportComponent,
],
entryComponents: [
AddEditComponent,
AttachmentsComponent,
BulkActionsComponent,
BulkDeleteComponent,
BulkMoveComponent,
BulkRestoreComponent,
BulkShareComponent,
CollectionsComponent,
DeauthorizeSessionsComponent,
@@ -305,13 +403,17 @@ registerLocaleData(localeZhCn, 'zh-CN');
FolderAddEditComponent,
ModalComponent,
OrgAddEditComponent,
OrgApiKeyComponent,
OrgAttachmentsComponent,
OrgCollectionAddEditComponent,
OrgCollectionsComponent,
OrgEntityEventsComponent,
OrgEntityUsersComponent,
OrgGroupAddEditComponent,
OrgPolicyEditComponent,
OrgRotateApiKeyComponent,
OrgUserAddEditComponent,
OrgUserConfirmComponent,
OrgUserGroupsComponent,
PasswordGeneratorHistoryComponent,
PurgeVaultComponent,

View File

@@ -1,7 +0,0 @@
<div class="callout callout-{{calloutStyle}}" role="alert">
<h3 class="callout-heading" *ngIf="title">
<i class="fa {{icon}}" *ngIf="icon"></i>
{{title}}
</h3>
<ng-content></ng-content>
</div>

View File

@@ -1,53 +0,0 @@
import {
Component,
Input,
OnInit,
} from '@angular/core';
import { I18nService } from 'jslib/abstractions/i18n.service';
@Component({
selector: 'app-callout',
templateUrl: 'callout.component.html',
})
export class CalloutComponent implements OnInit {
@Input() type = 'info';
@Input() icon: string;
@Input() title: string;
calloutStyle: string;
constructor(private i18nService: I18nService) { }
ngOnInit() {
this.calloutStyle = this.type;
if (this.type === 'warning' || this.type === 'danger') {
if (this.type === 'danger') {
this.calloutStyle = 'danger';
}
if (this.title === undefined) {
this.title = this.i18nService.t('warning');
}
if (this.icon === undefined) {
this.icon = 'fa-warning';
}
} else if (this.type === 'error') {
this.calloutStyle = 'danger';
if (this.title === undefined) {
this.title = this.i18nService.t('error');
}
if (this.icon === undefined) {
this.icon = 'fa-bolt';
}
} else if (this.type === 'tip') {
this.calloutStyle = 'success';
if (this.title === undefined) {
this.title = this.i18nService.t('tip');
}
if (this.icon === undefined) {
this.icon = 'fa-lightbulb-o';
}
}
}
}

View File

@@ -0,0 +1,8 @@
<div class="progress">
<div class="progress-bar {{color}}" role="progressbar" [ngStyle]="{width: (scoreWidth + '%')}"
attr.aria-valuenow="{{scoreWidth}}" aria-valuemin="0" aria-valuemax="100">
<ng-container *ngIf="showText && text">
{{text}}
</ng-container>
</div>
</div>

View File

@@ -0,0 +1,44 @@
import {
Component,
Input,
OnChanges,
} from '@angular/core';
import { I18nService } from 'jslib/abstractions/i18n.service';
@Component({
selector: 'app-password-strength',
templateUrl: 'password-strength.component.html',
})
export class PasswordStrengthComponent implements OnChanges {
@Input() score?: number;
@Input() showText = false;
scoreWidth = 0;
color = 'bg-danger';
text: string;
constructor(private i18nService: I18nService) { }
ngOnChanges(): void {
this.scoreWidth = this.score == null ? 0 : (this.score + 1) * 20;
switch (this.score) {
case 4:
this.color = 'bg-success';
this.text = this.i18nService.t('strong');
break;
case 3:
this.color = 'bg-primary';
this.text = this.i18nService.t('good');
break;
case 2:
this.color = 'bg-warning';
this.text = this.i18nService.t('weak');
break;
default:
this.color = 'bg-danger';
this.text = this.score != null ? this.i18nService.t('weak') : null;
break;
}
}
}

View File

@@ -1,7 +1,7 @@
<div class="container footer text-muted">
<div class="row">
<div class="col">
&copy; {{year}}, 8bit Solutions LLC
&copy; {{year}}, Bitwarden Inc.
</div>
<div class="col text-center"></div>
<div class="col text-right">

View File

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

View File

@@ -12,10 +12,12 @@ import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
})
export class FrontendLayoutComponent implements OnInit, OnDestroy {
version: string;
year: string = '2015';
constructor(private platformUtilsService: PlatformUtilsService) { }
ngOnInit() {
this.year = new Date().getFullYear().toString();
this.version = this.platformUtilsService.getApplicationVersion();
document.body.classList.add('layout_frontend');
}

View File

@@ -1,7 +1,7 @@
<nav class="navbar navbar-expand navbar-dark bg-primary" [ngClass]="{'bg-secondary-alt': selfHosted}">
<div class="container">
<a class="navbar-brand" routerLink="/" title="{{'pageTitle' | i18n : 'Bitwarden'}}">
<i class="fa fa-shield"></i>
<a class="navbar-brand" routerLink="/" appA11yTitle="{{'pageTitle' | i18n : 'Bitwarden'}}">
<i class="fa fa-shield" aria-hidden="true"></i>
</a>
<div class="collapse navbar-collapse">
<ul class="navbar-nav">
@@ -18,8 +18,9 @@
</div>
<ul class="navbar-nav flex-row ml-md-auto d-none d-md-flex">
<li class="nav-item dropdown">
<a class="nav-item nav-link dropdown-toggle" href="#" id="nav-profile" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-user-circle fa-lg"></i>
<a class="nav-item nav-link dropdown-toggle" href="#" id="nav-profile" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<i class="fa fa-user-circle fa-lg" aria-hidden="true"></i>
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="nav-profile">
<div class="dropdown-item-text d-flex align-items-center" *ngIf="name" appStopProp>
@@ -31,24 +32,24 @@
</div>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" routerLink="/settings/account">
<i class="fa fa-fw fa-user"></i>
<i class="fa fa-fw fa-user" aria-hidden="true"></i>
{{'myAccount' | i18n}}
</a>
<a class="dropdown-item" href="https://help.bitwarden.com" target="_blank" rel="noopener">
<i class="fa fa-fw fa-question-circle"></i>
<i class="fa fa-fw fa-question-circle" aria-hidden="true"></i>
{{'getHelp' | i18n}}
</a>
<a class="dropdown-item" href="https://bitwarden.com#download" target="_blank" rel="noopener">
<i class="fa fa-fw fa-download"></i>
<a class="dropdown-item" href="https://bitwarden.com/download/" target="_blank" rel="noopener">
<i class="fa fa-fw fa-download" aria-hidden="true"></i>
{{'getApps' | i18n}}
</a>
<div class="dropdown-divider"></div>
<button type="button" class="dropdown-item" (click)="lock()">
<i class="fa fa-fw fa-lock"></i>
<i class="fa fa-fw fa-lock" aria-hidden="true"></i>
{{'lockNow' | i18n}}
</button>
<button type="button" class="dropdown-item" (click)="logOut()">
<i class="fa fa-fw fa-sign-out"></i>
<i class="fa fa-fw fa-sign-out" aria-hidden="true"></i>
{{'logOut' | i18n}}
</button>
</div>

View File

@@ -1,45 +1,56 @@
<app-navbar></app-navbar>
<div class="org-nav" *ngIf="organization">
<div class="container d-flex flex-column">
<div class="my-auto d-flex align-items-center pl-1">
<app-avatar [data]="organization.name" size="45" [circle]="true"></app-avatar>
<div class="org-name ml-3">
<span>{{organization.name}}</span>
<small class="text-muted">{{'organization' | i18n}}</small>
</div>
<div class="ml-auto card border-danger text-danger bg-transparent" *ngIf="!organization.enabled">
<div class="card-body py-2">
<i class="fa fa-exclamation-triangle"></i>
{{'organizationIsDisabled' | i18n}}
<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]="organization.name" size="45" [circle]="true"></app-avatar>
<div class="org-name ml-3">
<span>{{organization.name}}</span>
<small class="text-muted">{{'organization' | i18n}}</small>
</div>
<div class="ml-3 card border-danger text-danger bg-transparent" *ngIf="!organization.enabled">
<div class="card-body py-2">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
{{'organizationIsDisabled' | i18n}}
</div>
</div>
</div>
<ul class="nav nav-tabs" *ngIf="organization.isManager">
<li class="nav-item">
<a class="nav-link" routerLink="vault" routerLinkActive="active">
<i class="fa fa-lock" aria-hidden="true"></i>
{{'vault' | i18n}}
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="manage" routerLinkActive="active">
<i class="fa fa-sliders" aria-hidden="true"></i>
{{'manage' | i18n}}
</a>
</li>
<li class="nav-item" *ngIf="organization.isAdmin">
<a class="nav-link" routerLink="tools" routerLinkActive="active">
<i class="fa fa-wrench" aria-hidden="true"></i>
{{'tools' | i18n}}
</a>
</li>
<li class="nav-item" *ngIf="organization.isOwner">
<a class="nav-link" routerLink="settings" routerLinkActive="active">
<i class="fa fa-cogs" aria-hidden="true"></i>
{{'settings' | i18n}}
</a>
</li>
</ul>
</div>
<div class="ml-auto d-flex align-items-center">
<button class="btn btn-primary" (click)="goToEnterprisePortal()" #enterpriseBtn
[appApiAction]="enterpriseTokenPromise" *ngIf="organization.useBusinessPortal">
<i class="fa fa-bank fa-fw" [hidden]="enterpriseBtn.loading" aria-hidden="true"></i>
<i class="fa fa-spinner fa-spin fa-fw" [hidden]="!enterpriseBtn.loading" title="{{'loading' | i18n}}"
aria-hidden="true"></i>
{{'businessPortal' | i18n}} →
</button>
</div>
<ul class="nav nav-tabs" *ngIf="organization.isAdmin">
<li class="nav-item">
<a class="nav-link" routerLink="vault" routerLinkActive="active">
<i class="fa fa-lock"></i>
{{'vault' | i18n}}
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="manage" routerLinkActive="active">
<i class="fa fa-sliders"></i>
{{'manage' | i18n}}
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="tools" routerLinkActive="active">
<i class="fa fa-wrench"></i>
{{'tools' | i18n}}
</a>
</li>
<li class="nav-item" *ngIf="organization.isOwner">
<a class="nav-link" routerLink="settings" routerLinkActive="active">
<i class="fa fa-cogs"></i>
{{'settings' | i18n}}
</a>
</li>
</ul>
</div>
</div>
<router-outlet></router-outlet>

View File

@@ -1,33 +1,83 @@
import {
Component,
NgZone,
OnDestroy,
OnInit,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
import { ApiService } from 'jslib/abstractions/api.service';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { UserService } from 'jslib/abstractions/user.service';
import { Organization } from 'jslib/models/domain/organization';
const BroadcasterSubscriptionId = 'OrganizationLayoutComponent';
@Component({
selector: 'app-organization-layout',
templateUrl: 'organization-layout.component.html',
})
export class OrganizationLayoutComponent implements OnInit {
export class OrganizationLayoutComponent implements OnInit, OnDestroy {
organization: Organization;
enterpriseTokenPromise: Promise<any>;
private organizationId: string;
private enterpriseUrl: string;
constructor(private route: ActivatedRoute, private userService: UserService) { }
constructor(private route: ActivatedRoute, private userService: UserService,
private broadcasterService: BroadcasterService, private ngZone: NgZone,
private apiService: ApiService, private platformUtilsService: PlatformUtilsService,
private environmentService: EnvironmentService) { }
ngOnInit() {
this.enterpriseUrl = 'https://portal.bitwarden.com';
if (this.environmentService.enterpriseUrl != null) {
this.enterpriseUrl = this.environmentService.enterpriseUrl;
} else if (this.environmentService.baseUrl != null) {
this.enterpriseUrl = this.environmentService.baseUrl + '/portal';
}
document.body.classList.remove('layout_frontend');
this.route.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
await this.load();
});
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case 'updatedOrgLicense':
await this.load();
break;
}
});
});
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
async load() {
this.organization = await this.userService.getOrganization(this.organizationId);
}
async goToEnterprisePortal() {
if (this.enterpriseTokenPromise != null) {
return;
}
try {
this.enterpriseTokenPromise = this.apiService.getEnterprisePortalSignInToken();
const token = await this.enterpriseTokenPromise;
if (token != null) {
const userId = await this.userService.getUserId();
this.platformUtilsService.launchUri(this.enterpriseUrl + '/login?userId=' + userId +
'&token=' + (window as any).encodeURIComponent(token) + '&organizationId=' + this.organization.id);
}
} catch { }
this.enterpriseTokenPromise = null;
}
}

View File

@@ -10,6 +10,8 @@ import {
import { ModalComponent as BaseModalComponent } from 'jslib/angular/components/modal.component';
import { Utils } from 'jslib/misc/utils';
import { MessagingService } from 'jslib/abstractions/messaging.service';
@Component({
selector: 'app-modal',
template: `<ng-template #container></ng-template>`,
@@ -17,18 +19,22 @@ import { Utils } from 'jslib/misc/utils';
export class ModalComponent extends BaseModalComponent {
el: any = null;
constructor(componentFactoryResolver: ComponentFactoryResolver) {
super(componentFactoryResolver);
constructor(componentFactoryResolver: ComponentFactoryResolver, messagingService: MessagingService) {
super(componentFactoryResolver, messagingService);
}
ngOnDestroy() { /* Nothing */ }
show<T>(type: Type<T>, parentContainer: ViewContainerRef, fade: boolean = true): T {
show<T>(type: Type<T>, parentContainer: ViewContainerRef, fade: boolean = true,
setComponentParameters: (component: T) => void = null): T {
this.parentContainer = parentContainer;
this.fade = fade;
const factory = this.componentFactoryResolver.resolveComponentFactory<T>(type);
const componentRef = this.container.createComponent<T>(factory);
if (setComponentParameters != null) {
setComponentParameters(componentRef.instance);
}
const modals = Array.from(document.querySelectorAll('.modal'));
if (modals.length > 0) {
@@ -37,18 +43,22 @@ export class ModalComponent extends BaseModalComponent {
this.el.on('show.bs.modal', () => {
this.onShow.emit();
this.messagingService.send('modalShow');
});
this.el.on('shown.bs.modal', () => {
this.onShown.emit();
this.messagingService.send('modalShown');
if (!Utils.isMobileBrowser) {
this.el.find('*[appAutoFocus]').focus();
}
});
this.el.on('hide.bs.modal', () => {
this.onClose.emit();
this.messagingService.send('modalClose');
});
this.el.on('hidden.bs.modal', () => {
this.onClosed.emit();
this.messagingService.send('modalClosed');
if (this.parentContainer != null) {
this.parentContainer.clear();
}

View File

@@ -1,19 +1,26 @@
<div class="modal fade">
<div class="modal-dialog">
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="collectionAddEditTitle">
<div class="modal-dialog" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title">{{title}}</h2>
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
<h2 class="modal-title" id="collectionAddEditTitle">{{title}}</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}}"></i>
<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">
<div class="form-group">
<label for="name">{{'name' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required
appAutofocus>
</div>
<div class="form-group">
<label for="externalId">{{'externalId' | i18n}}</label>
<input id="externalId" class="form-control" type="text" name="ExternalId" [(ngModel)]="externalId">
<small class="form-text text-muted">{{'externalIdDesc' | i18n}}</small>
</div>
<ng-container *ngIf="accessGroups">
<h3 class="mt-4 d-flex mb-0">
@@ -35,22 +42,31 @@
<tr>
<th>&nbsp;</th>
<th>{{'name' | i18n}}</th>
<th width="100" class="text-center">{{'hidePasswords' | i18n}}</th>
<th width="100" class="text-center">{{'readOnly' | i18n}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let g of groups; let i = index">
<td class="table-list-checkbox" (click)="check(g)">
<input type="checkbox" [(ngModel)]="g.checked" name="Groups[{{i}}].Checked" [disabled]="g.accessAll" appStopProp>
<input type="checkbox" [(ngModel)]="g.checked" name="Groups[{{i}}].Checked"
[disabled]="g.accessAll" appStopProp>
</td>
<td (click)="check(g)">
<span appStopProp>
{{g.name}}
<i class="fa fa-th text-muted fa-fw" *ngIf="g.accessAll" title="This group can access all items"></i>
</span>
{{g.name}}
<ng-container *ngIf="g.accessAll">
<i class="fa fa-th text-muted fa-fw" title="{{'groupAccessAllItems' | i18n}}"
aria-hidden="true"></i>
<span class="sr-only">{{'groupAccessAllItems' | i18n}}</span>
</ng-container>
</td>
<td class="text-center">
<input type="checkbox" [(ngModel)]="g.readOnly" name="Groups[{{i}}].ReadOnly" [disabled]="!g.checked || g.accessAll">
<input type="checkbox" [(ngModel)]="g.hidePasswords"
name="Groups[{{i}}].HidePasswords" [disabled]="!g.checked || g.accessAll">
</td>
<td class="text-center">
<input type="checkbox" [(ngModel)]="g.readOnly" name="Groups[{{i}}].ReadOnly"
[disabled]="!g.checked || g.accessAll">
</td>
</tr>
</tbody>
@@ -59,15 +75,18 @@
</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}}"></i>
<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>
<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" title="{{'delete' | i18n}}" *ngIf="editMode"
[disabled]="deleteBtn.loading" [appApiAction]="deletePromise">
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading"></i>
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading" title="{{'loading' | i18n}}"></i>
<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>

View File

@@ -38,6 +38,7 @@ export class CollectionAddEditComponent implements OnInit {
accessGroups: boolean = false;
title: string;
name: string;
externalId: string;
groups: GroupResponse[] = [];
formPromise: Promise<any>;
deletePromise: Promise<any>;
@@ -65,12 +66,14 @@ export class CollectionAddEditComponent implements OnInit {
try {
const collection = await this.apiService.getCollectionDetails(this.organizationId, this.collectionId);
this.name = await this.cryptoService.decryptToUtf8(new CipherString(collection.name), this.orgKey);
this.externalId = collection.externalId;
if (collection.groups != null && this.groups.length > 0) {
collection.groups.forEach((s) => {
const group = this.groups.filter((g) => !g.accessAll && g.id === s.id);
if (group != null && group.length > 0) {
(group[0] as any).checked = true;
(group[0] as any).readOnly = s.readOnly;
(group[0] as any).hidePasswords = s.hidePasswords;
}
});
}
@@ -95,6 +98,7 @@ export class CollectionAddEditComponent implements OnInit {
(g as any).checked = select == null ? !(g as any).checked : select;
if (!(g as any).checked) {
(g as any).readOnly = false;
(g as any).hidePasswords = false;
}
}
@@ -109,8 +113,9 @@ export class CollectionAddEditComponent implements OnInit {
const request = new CollectionRequest();
request.name = (await this.cryptoService.encrypt(this.name, this.orgKey)).encryptedString;
request.externalId = this.externalId;
request.groups = this.groups.filter((g) => (g as any).checked && !g.accessAll)
.map((g) => new SelectionReadOnlyRequest(g.id, !!(g as any).readOnly));
.map((g) => new SelectionReadOnlyRequest(g.id, !!(g as any).readOnly, !!(g as any).hidePasswords));
try {
if (this.editMode) {

View File

@@ -3,18 +3,24 @@
<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">
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
[(ngModel)]="searchText">
</div>
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="add()">
<i class="fa fa-plus fa-fw"></i>
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
{{'newCollection' | i18n}}
</button>
</div>
</div>
<i class="fa fa-spinner fa-spin text-muted" *ngIf="loading"></i>
<ng-container *ngIf="!loading && (collections | search:searchText:'name':'id') as searchedCollections">
<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>
</ng-container>
<ng-container
*ngIf="!loading && (isPaging() ? pagedCollections : collections | search:searchText:'name':'id') as searchedCollections">
<p *ngIf="!searchedCollections.length">{{'noCollectionsInList' | i18n}}</p>
<table class="table table-hover table-list" *ngIf="searchedCollections.length">
<table class="table table-hover table-list" *ngIf="searchedCollections.length" infiniteScroll
[infiniteScrollDistance]="1" [infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
<tbody>
<tr *ngFor="let c of searchedCollections">
<td>
@@ -22,16 +28,17 @@
</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">
<i class="fa fa-cog fa-lg"></i>
<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)="users(c)">
<i class="fa fa-fw fa-users"></i>
<i class="fa fa-fw fa-users" aria-hidden="true"></i>
{{'users' | i18n}}
</a>
<a class="dropdown-item text-danger" href="#" appStopClick (click)="delete(c)">
<i class="fa fa-fw fa-trash-o"></i>
<i class="fa fa-fw fa-trash-o" aria-hidden="true"></i>
{{'delete' | i18n}}
</a>
</div>

View File

@@ -14,10 +14,16 @@ import { ApiService } from 'jslib/abstractions/api.service';
import { CollectionService } from 'jslib/abstractions/collection.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SearchService } from 'jslib/abstractions/search.service';
import { UserService } from 'jslib/abstractions/user.service';
import { CollectionData } from 'jslib/models/data/collectionData';
import { Collection } from 'jslib/models/domain/collection';
import { CollectionDetailsResponse } from 'jslib/models/response/collectionResponse';
import {
CollectionDetailsResponse,
CollectionResponse,
} from 'jslib/models/response/collectionResponse';
import { ListResponse } from 'jslib/models/response/listResponse';
import { CollectionView } from 'jslib/models/view/collectionView';
import { ModalComponent } from '../../modal.component';
@@ -29,39 +35,72 @@ import { EntityUsersComponent } from './entity-users.component';
templateUrl: 'collections.component.html',
})
export class CollectionsComponent implements OnInit {
@ViewChild('addEdit', { read: ViewContainerRef }) addEditModalRef: ViewContainerRef;
@ViewChild('usersTemplate', { read: ViewContainerRef }) usersModalRef: ViewContainerRef;
@ViewChild('addEdit', { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
@ViewChild('usersTemplate', { read: ViewContainerRef, static: true }) usersModalRef: ViewContainerRef;
loading = true;
organizationId: string;
collections: CollectionView[];
pagedCollections: CollectionView[];
searchText: string;
protected didScroll = false;
protected pageSize = 100;
private pagedCollectionsCount = 0;
private modal: ModalComponent = null;
constructor(private apiService: ApiService, private route: ActivatedRoute,
private collectionService: CollectionService, private componentFactoryResolver: ComponentFactoryResolver,
private analytics: Angulartics2, private toasterService: ToasterService,
private i18nService: I18nService, private platformUtilsService: PlatformUtilsService) { }
private i18nService: I18nService, private platformUtilsService: PlatformUtilsService,
private userService: UserService, private searchService: SearchService) { }
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
await this.load();
this.route.queryParams.subscribe(async (qParams) => {
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
this.searchText = qParams.search;
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();
}
});
});
}
async load() {
const response = await this.apiService.getCollections(this.organizationId);
const collections = response.data.map((r) =>
const organization = await this.userService.getOrganization(this.organizationId);
let response: ListResponse<CollectionResponse>;
if (organization.isAdmin) {
response = await this.apiService.getCollections(this.organizationId);
} else {
response = await this.apiService.getUserCollections();
}
const collections = response.data.filter((c) => c.organizationId === this.organizationId).map((r) =>
new Collection(new CollectionData(r as CollectionDetailsResponse)));
this.collections = await this.collectionService.decryptMany(collections);
this.resetPaging();
this.loading = false;
}
loadMore() {
if (!this.collections || this.collections.length <= this.pageSize) {
return;
}
const pagedLength = this.pagedCollections.length;
let pagedSize = this.pageSize;
if (pagedLength === 0 && this.pagedCollectionsCount > this.pageSize) {
pagedSize = this.pagedCollectionsCount;
}
if (this.collections.length > pagedLength) {
this.pagedCollections =
this.pagedCollections.concat(this.collections.slice(pagedLength, pagedLength + pagedSize));
}
this.pagedCollectionsCount = this.pagedCollections.length;
this.didScroll = this.pagedCollections.length > this.pageSize;
}
edit(collection: CollectionView) {
if (this.modal != null) {
this.modal.close();
@@ -123,15 +162,37 @@ export class CollectionsComponent implements OnInit {
childComponent.entityId = collection.id;
childComponent.entityName = collection.name;
childComponent.onEditedUsers.subscribe(() => {
this.load();
this.modal.close();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
async resetPaging() {
this.pagedCollections = [];
this.loadMore();
}
isSearching() {
return this.searchService.isSearchable(this.searchText);
}
isPaging() {
const searching = this.isSearching();
if (searching && this.didScroll) {
this.resetPaging();
}
return !searching && this.collections && this.collections.length > this.pageSize;
}
private removeCollection(collection: CollectionView) {
const index = this.collections.indexOf(collection);
if (index > -1) {
this.collections.splice(index, 1);
this.resetPaging();
}
}
}

View File

@@ -1,32 +1,35 @@
<div class="modal fade">
<div class="modal-dialog modal-lg">
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="eventLogsTitle">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">
<h2 class="modal-title" id="eventLogsTitle">
{{'eventLogs' | i18n}}
<small class="text-muted" *ngIf="name">{{name}}</small>
</h2>
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" *ngIf="!loaded">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
<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="loaded">
<div class="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">
<input type="datetime-local" class="form-control form-control-sm" id="start"
placeholder="{{'startDate' | i18n}}" [(ngModel)]="start" placeholder="YYYY-MM-DDTHH:MM">
<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">
<input type="datetime-local" class="form-control form-control-sm" id="end"
placeholder="{{'endDate' | i18n}}" [(ngModel)]="end" placeholder="YYYY-MM-DDTHH:MM">
</div>
<button #refreshBtn [appApiAction]="refreshPromise" type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="loadEvents(true)"
<button #refreshBtn [appApiAction]="refreshPromise" type="button"
class="btn btn-sm btn-outline-primary ml-3" (click)="loadEvents(true)"
[disabled]="loaded && refreshBtn.loading">
<i class="fa fa-refresh fa-fw" [ngClass]="{'fa-spin': loaded && refreshBtn.loading}"></i>
<i class="fa fa-refresh fa-fw" [ngClass]="{'fa-spin': loaded && refreshBtn.loading}"
aria-hidden="true"></i>
{{'refresh' | i18n}}
</button>
</div>
@@ -49,18 +52,20 @@
<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}}"></i>
<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 *ngIf="showUser">
<span title="{{e.userEmail}}">{{e.userName}}</span>
<span appA11yTitle="{{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}}"></i>
<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>
</div>

View File

@@ -1,57 +1,113 @@
<div class="modal fade">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="userAccessTitle">
<div class="modal-dialog modal-lg" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title">
<h2 class="modal-title" id="userAccessTitle">
{{'userAccess' | i18n}}
<small>{{entityName}}</small>
</h2>
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
<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}}"></i>
<div class="modal-body" *ngIf="loading || !users">
<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="!users || !users.length">
<div class="modal-body"
*ngIf="!loading && users && (users | search:searchText:'name':'email':'id') as searchedUsers">
<div class="d-flex">
<div class="mr-3">
<label class="sr-only" for="search">{{'search' | i18n}}</label>
<input type="search" class="form-control form-control-sm" id="search"
placeholder="{{'search' | i18n}}" name="SearchText" [(ngModel)]="searchText">
</div>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary" [ngClass]="{active: !showSelected}"
(click)="filterSelected(false)">
{{'all' | i18n}}
</button>
<button type="button" class="btn btn-outline-secondary" [ngClass]="{active: showSelected}"
(click)="filterSelected(true)">
{{'selected' | i18n}}
<span class="badge badge-pill badge-info" *ngIf="selectedCount">{{selectedCount}}</span>
</button>
</div>
</div>
<ng-container *ngIf="!searchedUsers.length">
<hr>
{{'noUsersInList' | i18n}}
</ng-container>
<table class="table table-hover table-list mb-0" *ngIf="users && users.length">
<tbody>
<tr *ngFor="let u of users">
<td width="30">
<app-avatar [data]="u.name || u.email" [email]="u.email" size="25" [circle]="true" [fontSize]="14"></app-avatar>
</td>
<td>
{{u.email}}
<span class="badge badge-secondary" *ngIf="u.status === organizationUserStatusType.Invited">{{'invited' | i18n}}</span>
<span class="badge badge-warning" *ngIf="u.status === organizationUserStatusType.Accepted">{{'accepted' | i18n}}</span>
<small class="text-muted d-block" *ngIf="u.name">{{u.name}}</small>
</td>
<td *ngIf="entity === 'collection'">
<i class="fa fa-th" *ngIf="u.accessAll" title="{{'userAccessAllItems' | i18n}}"></i>
<i class="fa fa-eye" *ngIf="u.readOnly" title="{{'readOnly' | i18n}}"></i>
</td>
<td>
<span *ngIf="u.type === organizationUserType.Owner">{{'owner' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.Admin">{{'admin' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.User">{{'user' | i18n}}</span>
</td>
<td class="table-list-options wider">
<button type="button" class="btn btn-sm btn-outline-danger btn-submit" (click)="remove(u)" #removeBtn [disabled]="removeBtn.loading"
[appApiAction]="actionPromise" *ngIf="entity !== 'collection' || !u.accessAll">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
<span>{{'remove' | i18n}}</span>
</button>
</td>
</tr>
</tbody>
</table>
<ng-container *ngIf="searchedUsers.length">
<table class="table table-hover table-list mb-0">
<thead>
<tr>
<th>&nbsp;</th>
<th>&nbsp;</th>
<th>{{'name' | i18n}}</th>
<th *ngIf="entity === 'collection'">&nbsp;</th>
<th>{{'userType' | i18n}}</th>
<th width="100" class="text-center" *ngIf="entity === 'collection'">{{'hidePasswords' |
i18n}}</th>
<th width="100" class="text-center" *ngIf="entity === 'collection'">{{'readOnly' |
i18n}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let u of searchedUsers">
<td class="table-list-checkbox" (click)="check(u)">
<input type="checkbox" [(ngModel)]="u.checked" name="{{u.id.substr(0,8)}}_Checked"
[disabled]="entity === 'collection' && u.accessAll"
(change)="selectedChanged(u)" appStopProp>
</td>
<td width="30" (click)="check(u)">
<app-avatar [data]="u.name || u.email" [email]="u.email" size="25" [circle]="true"
[fontSize]="14"></app-avatar>
</td>
<td>
{{u.email}}
<span class="badge badge-secondary"
*ngIf="u.status === organizationUserStatusType.Invited">{{'invited'
| i18n}}</span>
<span class="badge badge-warning"
*ngIf="u.status === organizationUserStatusType.Accepted">{{'accepted'
| i18n}}</span>
<small class="text-muted d-block" *ngIf="u.name">{{u.name}}</small>
</td>
<td *ngIf="entity === 'collection'">
<ng-container *ngIf="u.accessAll">
<i class="fa fa-th" title="{{'userAccessAllItems' | i18n}}"
aria-hidden="true"></i>
<span class="sr-only">{{'userAccessAllItems' | i18n}}</span>
</ng-container>
</td>
<td>
<span *ngIf="u.type === organizationUserType.Owner">{{'owner' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.Admin">{{'admin' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.Manager">{{'manager' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.User">{{'user' | i18n}}</span>
</td>
<td class="text-center" *ngIf="entity === 'collection'">
<input type="checkbox" [(ngModel)]="u.hidePasswords"
name="{{u.id.substr(0,8)}}_HidePasswords"
[disabled]="u.accessAll || !u.checked">
</td>
<td class="text-center" *ngIf="entity === 'collection'">
<input type="checkbox" [(ngModel)]="u.readOnly" name="{{u.id.substr(0,8)}}_ReadOnly"
[disabled]="u.accessAll || !u.checked">
</td>
</tr>
</tbody>
</table>
</ng-container>
</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">{{'close' | i18n}}</button>
</div>
</div>
</form>
</div>
</div>

View File

@@ -11,10 +11,11 @@ import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { OrganizationUserStatusType } from 'jslib/enums/organizationUserStatusType';
import { OrganizationUserType } from 'jslib/enums/organizationUserType';
import { SelectionReadOnlyRequest } from 'jslib/models/request/selectionReadOnlyRequest';
import { OrganizationUserUserDetailsResponse } from 'jslib/models/response/organizationUserResponse';
import { Utils } from 'jslib/misc/utils';
@@ -27,68 +28,112 @@ export class EntityUsersComponent implements OnInit {
@Input() entityId: string;
@Input() entityName: string;
@Input() organizationId: string;
@Output() onRemovedUser = new EventEmitter();
@Output() onEditedUsers = new EventEmitter();
organizationUserType = OrganizationUserType;
organizationUserStatusType = OrganizationUserStatusType;
showSelected = false;
loading = true;
users: any[] = [];
actionPromise: Promise<any>;
formPromise: Promise<any>;
selectedCount = 0;
searchText: string;
private allUsers: OrganizationUserUserDetailsResponse[] = [];
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private platformUtilsService: PlatformUtilsService) { }
private analytics: Angulartics2, private toasterService: ToasterService) { }
async ngOnInit() {
await this.loadUsers();
this.loading = false;
}
async loadUsers() {
let users: any[] = [];
if (this.entity === 'group') {
const response = await this.apiService.getGroupUsers(this.organizationId, this.entityId);
users = response.data.map((r) => r);
} else if (this.entity === 'collection') {
const response = await this.apiService.getCollectionUsers(this.organizationId, this.entityId);
users = response.data.map((r) => r);
get users() {
if (this.showSelected) {
return this.allUsers.filter((u) => (u as any).checked);
} else {
return this.allUsers;
}
users.sort(Utils.getSortFunction(this.i18nService, 'email'));
this.users = users;
}
async remove(user: any) {
if (this.actionPromise != null || (this.entity === 'collection' && user.accessAll)) {
async loadUsers() {
const users = await this.apiService.getOrganizationUsers(this.organizationId);
this.allUsers = users.data.map((r) => r).sort(Utils.getSortFunction(this.i18nService, 'email'));
if (this.entity === 'group') {
const response = await this.apiService.getGroupUsers(this.organizationId, this.entityId);
if (response != null && users.data.length > 0) {
response.forEach((s) => {
const user = users.data.filter((u) => u.id === s);
if (user != null && user.length > 0) {
(user[0] as any).checked = true;
}
});
}
} else if (this.entity === 'collection') {
const response = await this.apiService.getCollectionUsers(this.organizationId, this.entityId);
if (response != null && users.data.length > 0) {
response.forEach((s) => {
const user = users.data.filter((u) => !u.accessAll && u.id === s.id);
if (user != null && user.length > 0) {
(user[0] as any).checked = true;
(user[0] as any).readOnly = s.readOnly;
(user[0] as any).hidePasswords = s.hidePasswords;
}
});
}
}
this.allUsers.forEach((u) => {
if (this.entity === 'collection' && u.accessAll) {
(u as any).checked = true;
}
if ((u as any).checked) {
this.selectedCount++;
}
});
}
check(u: OrganizationUserUserDetailsResponse) {
if (this.entity === 'collection' && u.accessAll) {
return;
}
(u as any).checked = !(u as any).checked;
this.selectedChanged(u);
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('removeUserConfirmation'), user.email,
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
selectedChanged(u: OrganizationUserUserDetailsResponse) {
if ((u as any).checked) {
this.selectedCount++;
} else {
if (this.entity === 'collection') {
(u as any).readOnly = false;
(u as any).hidePasswords = false;
}
this.selectedCount--;
}
}
filterSelected(showSelected: boolean) {
this.showSelected = showSelected;
}
async submit() {
try {
if (this.entity === 'group') {
this.actionPromise = this.apiService.deleteGroupUser(this.organizationId, this.entityId,
user.organizationUserId);
await this.actionPromise;
this.analytics.eventTrack.next({ action: 'Removed User From Group' });
} else if (this.entity === 'collection') {
this.actionPromise = this.apiService.deleteCollectionUser(this.organizationId, this.entityId,
user.organizationUserId);
await this.actionPromise;
this.analytics.eventTrack.next({ action: 'Removed User From Collection' });
}
this.toasterService.popAsync('success', null, this.i18nService.t('removedUserId', user.email));
this.onRemovedUser.emit();
const index = this.users.indexOf(user);
if (index > -1) {
this.users.splice(index, 1);
const selections = this.users.filter((u) => (u as any).checked).map((u) => u.id);
this.formPromise = this.apiService.putGroupUsers(this.organizationId, this.entityId, selections);
} else {
const selections = this.users.filter((u) => (u as any).checked && !u.accessAll)
.map((u) => new SelectionReadOnlyRequest(u.id, !!(u as any).readOnly, !!(u as any).hidePasswords));
this.formPromise = this.apiService.putCollectionUsers(this.organizationId, this.entityId, selections);
}
await this.formPromise;
this.analytics.eventTrack.next({
action: this.entity === 'group' ? 'Edited Group Users' : 'Edited Collection Users',
});
this.toasterService.popAsync('success', null, this.i18nService.t('updatedUsers'));
this.onEditedUsers.emit();
} catch { }
}
}

View File

@@ -3,21 +3,24 @@
<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">
<input type="datetime-local" class="form-control form-control-sm" id="start"
placeholder="{{'startDate' | i18n}}" [(ngModel)]="start" placeholder="YYYY-MM-DDTHH:MM">
<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">
<input type="datetime-local" class="form-control form-control-sm" id="end"
placeholder="{{'endDate' | i18n}}" [(ngModel)]="end" placeholder="YYYY-MM-DDTHH:MM">
</div>
<button #refreshBtn [appApiAction]="refreshPromise" type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="loadEvents(true)"
[disabled]="loaded && refreshBtn.loading">
<i class="fa fa-refresh fa-fw" [ngClass]="{'fa-spin': loaded && refreshBtn.loading}"></i>
<button #refreshBtn [appApiAction]="refreshPromise" type="button" class="btn btn-sm btn-outline-primary ml-3"
(click)="loadEvents(true)" [disabled]="loaded && refreshBtn.loading">
<i class="fa fa-refresh fa-fw" aria-hidden="true" [ngClass]="{'fa-spin': loaded && refreshBtn.loading}"></i>
{{'refresh' | i18n}}
</button>
</div>
</div>
<i class="fa fa-spinner fa-spin text-muted" *ngIf="!loaded" title="{{'loading' | i18n}}"></i>
<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>
</ng-container>
<ng-container *ngIf="loaded">
<p *ngIf="!events || !events.length">{{'noEventsInList' | i18n}}</p>
<table class="table table-hover" *ngIf="events && events.length">
@@ -35,7 +38,8 @@
<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}}"></i>
<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>
@@ -44,9 +48,9 @@
</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}}"></i>
<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>
</ng-container>

View File

@@ -1,14 +1,15 @@
<div class="modal fade">
<div class="modal-dialog">
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="groupAddEditTitle">
<div class="modal-dialog modal-lg" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title">{{title}}</h2>
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
<h2 class="modal-title" id="groupAddEditTitle">{{title}}</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}}"></i>
<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">
<div class="form-group">
@@ -18,11 +19,15 @@
<div class="form-group">
<label for="externalId">{{'externalId' | i18n}}</label>
<input id="externalId" class="form-control" type="text" name="ExternalId" [(ngModel)]="externalId">
<small class="form-text text-muted">{{'externalIdGroupDesc' | i18n}}</small>
<small class="form-text text-muted">{{'externalIdDesc' | i18n}}</small>
</div>
<h3 class="mt-4 d-flex">
<div class="mb-2">
{{'accessControl' | i18n}}
<a target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}"
href="https://bitwarden.com/help/article/user-types-access-control/#access-control">
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</div>
<div class="ml-auto" *ngIf="access === 'selected' && collections && collections.length">
<button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
@@ -35,13 +40,15 @@
</h3>
<div class="form-group" [ngClass]="{'mb-0': access !== 'selected'}">
<div class="form-check">
<input class="form-check-input" type="radio" name="access" id="accessAll" value="all" [(ngModel)]="access">
<input class="form-check-input" type="radio" name="access" id="accessAll" value="all"
[(ngModel)]="access">
<label class="form-check-label" for="accessAll">
{{'groupAccessAllItems' | i18n}}
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="access" id="accessSelected" value="selected" [(ngModel)]="access">
<input class="form-check-input" type="radio" name="access" id="accessSelected" value="selected"
[(ngModel)]="access">
<label class="form-check-label" for="accessSelected">
{{'groupAccessSelectedCollections' | i18n}}
</label>
@@ -56,19 +63,26 @@
<tr>
<th>&nbsp;</th>
<th>{{'name' | i18n}}</th>
<th width="100" class="text-center">{{'hidePasswords' | i18n}}</th>
<th width="100" class="text-center">{{'readOnly' | i18n}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let c of collections; let i = index">
<td class="table-list-checkbox" (click)="check(c)">
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked" appStopProp>
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked"
appStopProp>
</td>
<td (click)="check(c)">
<span appStopProp>{{c.name}}</span>
{{c.name}}
</td>
<td class="text-center">
<input type="checkbox" [(ngModel)]="c.readOnly" name="Collection[{{i}}].ReadOnly" [disabled]="!c.checked">
<input type="checkbox" [(ngModel)]="c.hidePasswords"
name="Collection[{{i}}].HidePasswords" [disabled]="!c.checked">
</td>
<td class="text-center">
<input type="checkbox" [(ngModel)]="c.readOnly" name="Collection[{{i}}].ReadOnly"
[disabled]="!c.checked">
</td>
</tr>
</tbody>
@@ -77,15 +91,18 @@
</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}}"></i>
<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>
<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" title="{{'delete' | i18n}}" *ngIf="editMode"
[disabled]="deleteBtn.loading" [appApiAction]="deletePromise">
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading"></i>
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading" title="{{'loading' | i18n}}"></i>
<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" aria-hidden="true"
title="{{'loading' | i18n}}"></i>
</button>
</div>
</div>

View File

@@ -63,6 +63,7 @@ export class GroupAddEditComponent implements OnInit {
if (collection != null && collection.length > 0) {
(collection[0] as any).checked = true;
collection[0].readOnly = s.readOnly;
collection[0].hidePasswords = s.hidePasswords;
}
});
}
@@ -99,7 +100,7 @@ export class GroupAddEditComponent implements OnInit {
request.accessAll = this.access === 'all';
if (!request.accessAll) {
request.collections = this.collections.filter((c) => (c as any).checked)
.map((c) => new SelectionReadOnlyRequest(c.id, !!c.readOnly));
.map((c) => new SelectionReadOnlyRequest(c.id, !!c.readOnly, !!c.hidePasswords));
}
try {

View File

@@ -3,18 +3,23 @@
<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">
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
[(ngModel)]="searchText">
</div>
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="add()">
<i class="fa fa-plus fa-fw"></i>
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
{{'newGroup' | i18n}}
</button>
</div>
</div>
<i class="fa fa-spinner fa-spin text-muted" *ngIf="loading" title="{{'loading' | i18n}}"></i>
<ng-container *ngIf="!loading && (groups | search:searchText:'name':'id') as searchedGroups">
<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>
</ng-container>
<ng-container *ngIf="!loading && (isPaging() ? pagedGroups : groups | search:searchText:'name':'id') as searchedGroups">
<p *ngIf="!searchedGroups.length">{{'noGroupsInList' | i18n}}</p>
<table class="table table-hover table-list" *ngIf="searchedGroups.length">
<table class="table table-hover table-list" *ngIf="searchedGroups.length" infiniteScroll
[infiniteScrollDistance]="1" [infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
<tbody>
<tr *ngFor="let g of searchedGroups">
<td>
@@ -22,16 +27,17 @@
</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">
<i class="fa fa-cog fa-lg"></i>
<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)="users(g)">
<i class="fa fa-fw fa-users"></i>
<i class="fa fa-fw fa-users" aria-hidden="true"></i>
{{'users' | i18n}}
</a>
<a class="dropdown-item text-danger" href="#" appStopClick (click)="delete(g)">
<i class="fa fa-fw fa-trash-o"></i>
<i class="fa fa-fw fa-trash-o" aria-hidden="true"></i>
{{'delete' | i18n}}
</a>
</div>

View File

@@ -16,6 +16,7 @@ import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SearchService } from 'jslib/abstractions/search.service';
import { UserService } from 'jslib/abstractions/user.service';
import { GroupResponse } from 'jslib/models/response/groupResponse';
@@ -31,21 +32,26 @@ import { GroupAddEditComponent } from './group-add-edit.component';
templateUrl: 'groups.component.html',
})
export class GroupsComponent implements OnInit {
@ViewChild('addEdit', { read: ViewContainerRef }) addEditModalRef: ViewContainerRef;
@ViewChild('usersTemplate', { read: ViewContainerRef }) usersModalRef: ViewContainerRef;
@ViewChild('addEdit', { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
@ViewChild('usersTemplate', { read: ViewContainerRef, static: true }) usersModalRef: ViewContainerRef;
loading = true;
organizationId: string;
groups: GroupResponse[];
pagedGroups: GroupResponse[];
searchText: string;
protected didScroll = false;
protected pageSize = 100;
private pagedGroupsCount = 0;
private modal: ModalComponent = null;
constructor(private apiService: ApiService, private route: ActivatedRoute,
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
private analytics: Angulartics2, private toasterService: ToasterService,
private platformUtilsService: PlatformUtilsService, private userService: UserService,
private router: Router) { }
private router: Router, private searchService: SearchService) { }
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
@@ -56,8 +62,11 @@ export class GroupsComponent implements OnInit {
return;
}
await this.load();
this.route.queryParams.subscribe(async (qParams) => {
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
this.searchText = qParams.search;
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();
}
});
});
}
@@ -67,9 +76,26 @@ export class GroupsComponent implements OnInit {
const groups = response.data != null && response.data.length > 0 ? response.data : [];
groups.sort(Utils.getSortFunction(this.i18nService, 'name'));
this.groups = groups;
this.resetPaging();
this.loading = false;
}
loadMore() {
if (!this.groups || this.groups.length <= this.pageSize) {
return;
}
const pagedLength = this.pagedGroups.length;
let pagedSize = this.pageSize;
if (pagedLength === 0 && this.pagedGroupsCount > this.pageSize) {
pagedSize = this.pagedGroupsCount;
}
if (this.groups.length > pagedLength) {
this.pagedGroups = this.pagedGroups.concat(this.groups.slice(pagedLength, pagedLength + pagedSize));
}
this.pagedGroupsCount = this.pagedGroups.length;
this.didScroll = this.pagedGroups.length > this.pageSize;
}
edit(group: GroupResponse) {
if (this.modal != null) {
this.modal.close();
@@ -131,15 +157,36 @@ export class GroupsComponent implements OnInit {
childComponent.entityId = group.id;
childComponent.entityName = group.name;
childComponent.onEditedUsers.subscribe(() => {
this.modal.close();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
async resetPaging() {
this.pagedGroups = [];
this.loadMore();
}
isSearching() {
return this.searchService.isSearchable(this.searchText);
}
isPaging() {
const searching = this.isSearching();
if (searching && this.didScroll) {
this.resetPaging();
}
return !searching && this.groups && this.groups.length > this.pageSize;
}
private removeGroup(group: GroupResponse) {
const index = this.groups.indexOf(group);
if (index > -1) {
this.groups.splice(index, 1);
this.resetPaging();
}
}
}

View File

@@ -1,19 +1,26 @@
<div class="container page-content">
<div class="row">
<div class="col-3">
<div class="card">
<div class="card" *ngIf="organization">
<div class="card-header">{{'manage' | i18n}}</div>
<div class="list-group list-group-flush">
<a routerLink="people" class="list-group-item" routerLinkActive="active">
<a routerLink="people" class="list-group-item" routerLinkActive="active"
*ngIf="organization.isAdmin">
{{'people' | i18n}}
</a>
<a routerLink="collections" class="list-group-item" routerLinkActive="active">
{{'collections' | i18n}}
</a>
<a routerLink="groups" class="list-group-item" routerLinkActive="active" *ngIf="accessGroups">
<a routerLink="groups" class="list-group-item" routerLinkActive="active"
*ngIf="organization.isAdmin && accessGroups">
{{'groups' | i18n}}
</a>
<a routerLink="events" class="list-group-item" routerLinkActive="active" *ngIf="accessEvents">
<a routerLink="policies" class="list-group-item" routerLinkActive="active"
*ngIf="organization.isAdmin && accessPolicies">
{{'policies' | i18n}}
</a>
<a routerLink="events" class="list-group-item" routerLinkActive="active"
*ngIf="organization.isAdmin && accessEvents">
{{'eventLogs' | i18n}}
</a>
</div>

View File

@@ -6,11 +6,15 @@ import { ActivatedRoute } from '@angular/router';
import { UserService } from 'jslib/abstractions/user.service';
import { Organization } from 'jslib/models/domain/organization';
@Component({
selector: 'app-org-manage',
templateUrl: 'manage.component.html',
})
export class ManageComponent implements OnInit {
organization: Organization;
accessPolicies = false;
accessGroups = false;
accessEvents = false;
@@ -18,9 +22,10 @@ export class ManageComponent implements OnInit {
ngOnInit() {
this.route.parent.params.subscribe(async (params) => {
const organization = await this.userService.getOrganization(params.organizationId);
this.accessEvents = organization.useEvents;
this.accessGroups = organization.useGroups;
this.organization = await this.userService.getOrganization(params.organizationId);
this.accessPolicies = this.organization.usePolicies;
this.accessEvents = this.organization.useEvents;
this.accessGroups = this.organization.useGroups;
});
}
}

View File

@@ -2,15 +2,19 @@
<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)">
<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 == organizationUserStatusType.Invited}"
<button type="button" class="btn btn-outline-secondary"
[ngClass]="{active: status == organizationUserStatusType.Invited}"
(click)="filter(organizationUserStatusType.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 == organizationUserStatusType.Accepted}"
<button type="button" class="btn btn-outline-secondary"
[ngClass]="{active: status == organizationUserStatusType.Accepted}"
(click)="filter(organizationUserStatusType.Accepted)">
{{'accepted' | i18n}}
<span class="badge badge-pill badge-warning" *ngIf="acceptedCount">{{acceptedCount}}</span>
@@ -18,62 +22,83 @@
</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">
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
[(ngModel)]="searchText">
</div>
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="invite()">
<i class="fa fa-plus fa-fw"></i>
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
{{'inviteUser' | i18n}}
</button>
</div>
</div>
<i class="fa fa-spinner fa-spin text-muted" *ngIf="loading" title="{{'loading' | i18n}}"></i>
<ng-container *ngIf="!loading && (users | search:searchText:'name':'email':'id') as searchedUsers">
<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>
</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">
{{'usersNeedConfirmed' | i18n}}
</app-callout>
<table class="table table-hover table-list">
<table class="table table-hover table-list" infiniteScroll [infiniteScrollDistance]="1"
[infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
<tbody>
<tr *ngFor="let u of searchedUsers">
<td width="30">
<app-avatar [data]="u.name || u.email" [email]="u.email" size="25" [circle]="true" [fontSize]="14"></app-avatar>
<app-avatar [data]="u.name || u.email" [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 === organizationUserStatusType.Invited">{{'invited' | i18n}}</span>
<span class="badge badge-warning" *ngIf="u.status === organizationUserStatusType.Accepted">{{'accepted' | i18n}}</span>
<span class="badge badge-secondary"
*ngIf="u.status === organizationUserStatusType.Invited">{{'invited' | i18n}}</span>
<span class="badge badge-warning"
*ngIf="u.status === organizationUserStatusType.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 === organizationUserType.Owner">{{'owner' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.Admin">{{'admin' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.Manager">{{'manager' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.User">{{'user' | 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">
<i class="fa fa-cog fa-lg"></i>
<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 === organizationUserStatusType.Invited">
<i class="fa fa-fw fa-envelope-o"></i>
<a class="dropdown-item" href="#" appStopClick (click)="reinvite(u)"
*ngIf="u.status === organizationUserStatusType.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 === organizationUserStatusType.Accepted">
<i class="fa fa-fw fa-check"></i>
<a class="dropdown-item text-success" href="#" appStopClick (click)="confirm(u)"
*ngIf="u.status === organizationUserStatusType.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"></i>
<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 === organizationUserStatusType.Confirmed">
<i class="fa fa-fw fa-file-text-o"></i>
<a class="dropdown-item" href="#" appStopClick (click)="events(u)"
*ngIf="accessEvents && u.status === organizationUserStatusType.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"></i>
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
{{'remove' | i18n}}
</a>
</div>
@@ -87,3 +112,4 @@
<ng-template #addEdit></ng-template>
<ng-template #groupsTemplate></ng-template>
<ng-template #eventsTemplate></ng-template>
<ng-template #confirmTemplate></ng-template>

View File

@@ -5,15 +5,22 @@ import {
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ConstantsService } from 'jslib/services/constants.service';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SearchService } from 'jslib/abstractions/search.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { UserService } from 'jslib/abstractions/user.service';
import { OrganizationUserConfirmRequest } from 'jslib/models/request/organizationUserConfirmRequest';
@@ -28,6 +35,7 @@ import { Utils } from 'jslib/misc/utils';
import { ModalComponent } from '../../modal.component';
import { EntityEventsComponent } from './entity-events.component';
import { UserAddEditComponent } from './user-add-edit.component';
import { UserConfirmComponent } from './user-confirm.component';
import { UserGroupsComponent } from './user-groups.component';
@Component({
@@ -35,13 +43,15 @@ import { UserGroupsComponent } from './user-groups.component';
templateUrl: 'people.component.html',
})
export class PeopleComponent implements OnInit {
@ViewChild('addEdit', { read: ViewContainerRef }) addEditModalRef: ViewContainerRef;
@ViewChild('groupsTemplate', { read: ViewContainerRef }) groupsModalRef: ViewContainerRef;
@ViewChild('eventsTemplate', { read: ViewContainerRef }) eventsModalRef: 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('confirmTemplate', { read: ViewContainerRef, static: true }) confirmModalRef: ViewContainerRef;
loading = true;
organizationId: string;
users: OrganizationUserUserDetailsResponse[];
pagedUsers: OrganizationUserUserDetailsResponse[];
searchText: string;
status: OrganizationUserStatusType = null;
statusMap = new Map<OrganizationUserStatusType, OrganizationUserUserDetailsResponse[]>();
@@ -51,6 +61,10 @@ export class PeopleComponent implements OnInit {
accessEvents = false;
accessGroups = false;
protected didScroll = false;
protected pageSize = 100;
private pagedUsersCount = 0;
private modal: ModalComponent = null;
private allUsers: OrganizationUserUserDetailsResponse[];
@@ -58,17 +72,22 @@ export class PeopleComponent implements OnInit {
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
private platformUtilsService: PlatformUtilsService, private analytics: Angulartics2,
private toasterService: ToasterService, private cryptoService: CryptoService,
private userService: UserService) { }
private userService: UserService, private router: Router,
private storageService: StorageService, private searchService: SearchService) { }
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
const organization = await this.userService.getOrganization(this.organizationId);
if (!organization.isAdmin) {
this.router.navigate(['../collections'], { relativeTo: this.route });
return;
}
this.accessEvents = organization.useEvents;
this.accessGroups = organization.useGroups;
await this.load();
this.route.queryParams.subscribe(async (qParams) => {
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);
@@ -76,6 +95,9 @@ export class PeopleComponent implements OnInit {
this.events(user[0]);
}
}
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();
}
});
});
}
@@ -103,6 +125,27 @@ export class PeopleComponent implements OnInit {
} else {
this.users = this.allUsers;
}
this.resetPaging();
}
loadMore() {
if (!this.users || this.users.length <= this.pageSize) {
return;
}
const pagedLength = this.pagedUsers.length;
let pagedSize = this.pageSize;
if (pagedLength === 0 && this.pagedUsersCount > this.pageSize) {
pagedSize = this.pagedUsersCount;
}
if (this.users.length > pagedLength) {
this.pagedUsers = this.pagedUsers.concat(this.users.slice(pagedLength, pagedLength + pagedSize));
}
this.pagedUsersCount = this.pagedUsers.length;
this.didScroll = this.pagedUsers.length > this.pageSize;
}
get allCount() {
return this.allUsers != null ? this.allUsers.length : 0;
}
get invitedCount() {
@@ -206,17 +249,48 @@ export class PeopleComponent implements OnInit {
}
async confirm(user: OrganizationUserUserDetailsResponse) {
function updateUser(self: PeopleComponent) {
user.status = OrganizationUserStatusType.Confirmed;
const mapIndex = self.statusMap.get(OrganizationUserStatusType.Accepted).indexOf(user);
if (mapIndex > -1) {
self.statusMap.get(OrganizationUserStatusType.Accepted).splice(mapIndex, 1);
self.statusMap.get(OrganizationUserStatusType.Confirmed).push(user);
}
}
if (this.actionPromise != null) {
return;
}
const autoConfirm = await this.storageService.get<boolean>(ConstantsService.autoConfirmFingerprints);
if (autoConfirm == null || !autoConfirm) {
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.confirmModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<UserConfirmComponent>(
UserConfirmComponent, this.confirmModalRef);
childComponent.name = user != null ? user.name || user.email : null;
childComponent.organizationId = this.organizationId;
childComponent.organizationUserId = user != null ? user.id : null;
childComponent.userId = user != null ? user.userId : null;
childComponent.onConfirmedUser.subscribe(() => {
this.modal.close();
updateUser(this);
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
return;
}
this.actionPromise = this.doConfirmation(user);
await this.actionPromise;
user.status = OrganizationUserStatusType.Confirmed;
const mapIndex = this.statusMap.get(OrganizationUserStatusType.Accepted).indexOf(user);
if (mapIndex > -1) {
this.statusMap.get(OrganizationUserStatusType.Accepted).splice(mapIndex, 1);
this.statusMap.get(OrganizationUserStatusType.Confirmed).push(user);
}
updateUser(this);
this.analytics.eventTrack.next({ action: 'Confirmed User' });
this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenConfirmed', user.name || user.email));
this.actionPromise = null;
@@ -243,10 +317,32 @@ export class PeopleComponent implements OnInit {
});
}
async resetPaging() {
this.pagedUsers = [];
this.loadMore();
}
isSearching() {
return this.searchService.isSearchable(this.searchText);
}
isPaging() {
const searching = this.isSearching();
if (searching && this.didScroll) {
this.resetPaging();
}
return !searching && this.users && this.users.length > this.pageSize;
}
private async doConfirmation(user: OrganizationUserUserDetailsResponse) {
const orgKey = await this.cryptoService.getOrgKey(this.organizationId);
const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId);
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
try {
// tslint:disable-next-line
console.log('User\'s fingerprint: ' +
(await this.cryptoService.getFingerprint(user.userId, publicKey.buffer)).join('-'));
} catch { }
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer);
const request = new OrganizationUserConfirmRequest();
request.key = key.encryptedString;
@@ -257,6 +353,7 @@ export class PeopleComponent implements OnInit {
let index = this.users.indexOf(user);
if (index > -1) {
this.users.splice(index, 1);
this.resetPaging();
}
if (this.statusMap.has(OrganizationUserStatusType.Accepted)) {
index = this.statusMap.get(OrganizationUserStatusType.Accepted).indexOf(user);

View File

@@ -0,0 +1,19 @@
<div class="page-header d-flex">
<h1>{{'policies' | i18n}}</h1>
</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>
</ng-container>
<table class="table table-hover table-list" *ngIf="!loading">
<tbody>
<tr *ngFor="let p of policies">
<td>
<a href="#" appStopClick (click)="edit(p)">{{p.name}}</a>
<span class="badge badge-success" *ngIf="p.enabled">{{'enabled' | i18n}}</span>
<small class="text-muted d-block">{{p.description}}</small>
</td>
</tr>
</tbody>
</table>
<ng-template #editTemplate></ng-template>

View File

@@ -0,0 +1,114 @@
import {
Component,
ComponentFactoryResolver,
OnInit,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { PolicyType } from 'jslib/enums/policyType';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { UserService } from 'jslib/abstractions/user.service';
import { PolicyResponse } from 'jslib/models/response/policyResponse';
import { ModalComponent } from '../../modal.component';
import { PolicyEditComponent } from './policy-edit.component';
@Component({
selector: 'app-org-policies',
templateUrl: 'policies.component.html',
})
export class PoliciesComponent implements OnInit {
@ViewChild('editTemplate', { read: ViewContainerRef, static: true }) editModalRef: ViewContainerRef;
loading = true;
organizationId: string;
policies: any[];
private modal: ModalComponent = null;
private orgPolicies: PolicyResponse[];
private policiesEnabledMap: Map<PolicyType, boolean> = new Map<PolicyType, boolean>();
constructor(private apiService: ApiService, private route: ActivatedRoute,
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
private platformUtilsService: PlatformUtilsService, private userService: UserService,
private router: Router) {
this.policies = [
{
name: i18nService.t('twoStepLogin'),
description: i18nService.t('twoStepLoginPolicyDesc'),
type: PolicyType.TwoFactorAuthentication,
enabled: false,
},
{
name: i18nService.t('masterPass'),
description: i18nService.t('masterPassPolicyDesc'),
type: PolicyType.MasterPassword,
enabled: false,
},
{
name: i18nService.t('passwordGenerator'),
description: i18nService.t('passwordGeneratorPolicyDesc'),
type: PolicyType.PasswordGenerator,
enabled: false,
},
];
}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
const organization = await this.userService.getOrganization(this.organizationId);
if (organization == null || !organization.usePolicies) {
this.router.navigate(['/organizations', this.organizationId]);
return;
}
await this.load();
});
}
async load() {
const response = await this.apiService.getPolicies(this.organizationId);
this.orgPolicies = response.data != null && response.data.length > 0 ? response.data : [];
this.orgPolicies.forEach((op) => {
this.policiesEnabledMap.set(op.type, op.enabled);
});
this.policies.forEach((p) => {
p.enabled = this.policiesEnabledMap.has(p.type) && this.policiesEnabledMap.get(p.type);
});
this.loading = false;
}
edit(p: any) {
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.editModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<PolicyEditComponent>(
PolicyEditComponent, this.editModalRef);
childComponent.name = p.name;
childComponent.description = p.description;
childComponent.type = p.type;
childComponent.organizationId = this.organizationId;
childComponent.onSavedPolicy.subscribe(() => {
this.modal.close();
this.load();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
}

View File

@@ -0,0 +1,143 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="policiesEditTitle">
<div class="modal-dialog" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title" id="policiesEditTitle">{{'editPolicy' | i18n}} - {{name}}</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">
<p>{{description}}</p>
<app-callout type="warning" *ngIf="type === policyType.TwoFactorAuthentication"
title="{{'warning' | i18n}}" icon="fa-warning">
{{'twoStepLoginPolicyWarning' | i18n}}
</app-callout>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="enabled" [(ngModel)]="enabled"
name="Enabled">
<label class="form-check-label" for="enabled">{{'enabled' | i18n}}</label>
</div>
</div>
<ng-container *ngIf="type === policyType.MasterPassword">
<div class="row">
<div class="col-6 form-group">
<label for="masterPassMinComplexity">{{'minComplexityScore' | i18n}}</label>
<select id="masterPassMinComplexity" name="MasterPassMinComplexity"
[(ngModel)]="masterPassMinComplexity" class="form-control">
<option *ngFor="let o of passwordScores" [ngValue]="o.value">{{o.name}}</option>
</select>
</div>
<div class="col-6 form-group">
<label for="masterPassMinLength">{{'minLength' | i18n}}</label>
<input id="masterPassMinLength" class="form-control" type="number" min="8"
name="MasterPassMinLength" [(ngModel)]="masterPassMinLength">
</div>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="masterPassRequireUpper"
[(ngModel)]="masterPassRequireUpper" name="MasterPassRequireUpper">
<label class="form-check-label" for="masterPassRequireUpper">A-Z</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="masterPassRequireLower"
[(ngModel)]="masterPassRequireLower" name="MasterPassRequireLower">
<label class="form-check-label" for="masterPassRequireLower">a-z</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="masterPassRequireNumbers"
[(ngModel)]="masterPassRequireNumbers" name="MasterPassRequireNumbers">
<label class="form-check-label" for="masterPassRequireNumbers">0-9</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="masterPassRequireSpecial"
[(ngModel)]="masterPassRequireSpecial" name="MasterPassRequireSpecial">
<label class="form-check-label" for="masterPassRequireSpecial">!@#$%^&amp;*</label>
</div>
</ng-container>
<ng-container *ngIf="type === policyType.PasswordGenerator">
<div class="row">
<div class="col-6 form-group mb-0">
<label for="passGenDefaultType">{{'defaultType' | i18n}}</label>
<select id="passGenDefaultType" name="PassGenDefaultType" [(ngModel)]="passGenDefaultType"
class="form-control">
<option *ngFor="let o of defaultTypes" [ngValue]="o.value">{{o.name}}</option>
</select>
</div>
</div>
<h3 class="mt-4">{{'password' | i18n}}</h3>
<div class="row">
<div class="col-6 form-group">
<label for="passGenMinLength">{{'minLength' | i18n}}</label>
<input id="passGenMinLength" class="form-control" type="number" name="PassGenMinLength"
min="5" max="128" [(ngModel)]="passGenMinLength">
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="passGenMinNumbers">{{'minNumbers' | i18n}}</label>
<input id="passGenMinNumbers" class="form-control" type="number" name="PassGenMinNumbers"
min="0" max="9" [(ngModel)]="passGenMinNumbers">
</div>
<div class="col-6 form-group">
<label for="passGenMinSpecial">{{'minSpecial' | i18n}}</label>
<input id="passGenMinSpecial" class="form-control" type="number" name="PassGenMinSpecial"
min="0" max="9" [(ngModel)]="passGenMinSpecial">
</div>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="passGenUseUpper"
[(ngModel)]="passGenUseUpper" name="PassGenUseUpper">
<label class="form-check-label" for="passGenUseUpper">A-Z</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="passGenUseLower"
[(ngModel)]="passGenUseLower" name="PassGenUseLower">
<label class="form-check-label" for="passGenUseLower">a-z</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="passGenUseNumbers"
[(ngModel)]="passGenUseNumbers" name="PassGenUseNumbers">
<label class="form-check-label" for="passGenUseNumbers">0-9</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="passGenUseSpecial"
[(ngModel)]="passGenUseSpecial" name="PassGenUseSpecial">
<label class="form-check-label" for="passGenUseSpecial">!@#$%^&amp;*</label>
</div>
<h3 class="mt-4">{{'passphrase' | i18n}}</h3>
<div class="row">
<div class="col-6 form-group">
<label for="passGenMinNumberWords">{{'minimumNumberOfWords' | i18n}}</label>
<input id="passGenMinNumberWords" class="form-control" type="number"
name="PassGenMinNumberWords" min="3" max="20" [(ngModel)]="passGenMinNumberWords">
</div>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="passGenCapitalize"
[(ngModel)]="passGenCapitalize" name="PassGenCapitalize">
<label class="form-check-label" for="passGenCapitalize">{{'capitalize' | i18n}}</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="passGenIncludeNumber"
[(ngModel)]="passGenIncludeNumber" name="PassGenIncludeNumber">
<label class="form-check-label" for="passGenIncludeNumber">{{'includeNumber' | i18n}}</label>
</div>
</ng-container>
</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>
</form>
</div>
</div>

View File

@@ -0,0 +1,171 @@
import {
Component,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PolicyType } from 'jslib/enums/policyType';
import { PolicyRequest } from 'jslib/models/request/policyRequest';
import { PolicyResponse } from 'jslib/models/response/policyResponse';
@Component({
selector: 'app-policy-edit',
templateUrl: 'policy-edit.component.html',
})
export class PolicyEditComponent implements OnInit {
@Input() name: string;
@Input() description: string;
@Input() type: PolicyType;
@Input() organizationId: string;
@Output() onSavedPolicy = new EventEmitter();
policyType = PolicyType;
loading = true;
enabled = false;
formPromise: Promise<any>;
passwordScores: any[];
defaultTypes: any[];
// Master password
masterPassMinComplexity?: number = null;
masterPassMinLength?: number;
masterPassRequireUpper?: number;
masterPassRequireLower?: number;
masterPassRequireNumbers?: number;
masterPassRequireSpecial?: number;
// Password generator
passGenDefaultType?: string;
passGenMinLength?: number;
passGenUseUpper?: boolean;
passGenUseLower?: boolean;
passGenUseNumbers?: boolean;
passGenUseSpecial?: boolean;
passGenMinNumbers?: number;
passGenMinSpecial?: number;
passGenMinNumberWords?: number;
passGenCapitalize?: boolean;
passGenIncludeNumber?: boolean;
private policy: PolicyResponse;
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService) {
this.passwordScores = [
{ name: '-- ' + i18nService.t('select') + ' --', value: null },
{ name: i18nService.t('weak') + ' (0)', value: 0 },
{ name: i18nService.t('weak') + ' (1)', value: 1 },
{ name: i18nService.t('weak') + ' (2)', value: 2 },
{ name: i18nService.t('good') + ' (3)', value: 3 },
{ name: i18nService.t('strong') + ' (4)', value: 4 },
];
this.defaultTypes = [
{ name: i18nService.t('userPreference'), value: null },
{ name: i18nService.t('password'), value: 'password' },
{ name: i18nService.t('passphrase'), value: 'passphrase' },
];
}
async ngOnInit() {
await this.load();
this.loading = false;
}
async load() {
try {
this.policy = await this.apiService.getPolicy(this.organizationId, this.type);
if (this.policy != null) {
this.enabled = this.policy.enabled;
if (this.policy.data != null) {
switch (this.type) {
case PolicyType.PasswordGenerator:
this.passGenDefaultType = this.policy.data.defaultType;
this.passGenMinLength = this.policy.data.minLength;
this.passGenUseUpper = this.policy.data.useUpper;
this.passGenUseLower = this.policy.data.useLower;
this.passGenUseNumbers = this.policy.data.useNumbers;
this.passGenUseSpecial = this.policy.data.useSpecial;
this.passGenMinNumbers = this.policy.data.minNumbers;
this.passGenMinSpecial = this.policy.data.minSpecial;
this.passGenMinNumberWords = this.policy.data.minNumberWords;
this.passGenCapitalize = this.policy.data.capitalize;
this.passGenIncludeNumber = this.policy.data.includeNumber;
break;
case PolicyType.MasterPassword:
this.masterPassMinComplexity = this.policy.data.minComplexity;
this.masterPassMinLength = this.policy.data.minLength;
this.masterPassRequireUpper = this.policy.data.requireUpper;
this.masterPassRequireLower = this.policy.data.requireLower;
this.masterPassRequireNumbers = this.policy.data.requireNumbers;
this.masterPassRequireSpecial = this.policy.data.requireSpecial;
break;
default:
break;
}
}
}
} catch (e) {
if (e.statusCode === 404) {
this.enabled = false;
} else {
throw e;
}
}
}
async submit() {
const request = new PolicyRequest();
request.enabled = this.enabled;
request.type = this.type;
request.data = null;
switch (this.type) {
case PolicyType.PasswordGenerator:
request.data = {
defaultType: this.passGenDefaultType,
minLength: this.passGenMinLength || null,
useUpper: this.passGenUseUpper,
useLower: this.passGenUseLower,
useNumbers: this.passGenUseNumbers,
useSpecial: this.passGenUseSpecial,
minNumbers: this.passGenMinNumbers || null,
minSpecial: this.passGenMinSpecial || null,
minNumberWords: this.passGenMinNumberWords || null,
capitalize: this.passGenCapitalize,
includeNumber: this.passGenIncludeNumber,
};
break;
case PolicyType.MasterPassword:
request.data = {
minComplexity: this.masterPassMinComplexity || null,
minLength: this.masterPassMinLength || null,
requireUpper: this.masterPassRequireUpper,
requireLower: this.masterPassRequireLower,
requireNumbers: this.masterPassRequireNumbers,
requireSpecial: this.masterPassRequireSpecial,
};
break;
default:
break;
}
try {
this.formPromise = this.apiService.putPolicy(this.organizationId, this.type, request);
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Edited Policy' });
this.toasterService.popAsync('success', null, this.i18nService.t('editedPolicyId', this.name));
this.onSavedPolicy.emit();
} catch { }
}
}

View File

@@ -1,44 +1,63 @@
<div class="modal fade">
<div class="modal-dialog" [ngClass]="{'modal-lg': !editMode}">
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
<div class="modal-dialog modal-lg" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title">
<h2 class="modal-title" id="userAddEditTitle">
{{title}}
<small class="text-muted" *ngIf="name">{{name}}</small>
</h2>
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
<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}}"></i>
<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>{{'inviteUserDesc' | 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>
<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}}</h3>
<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="userTypeUser" [value]="organizationUserType.User" [(ngModel)]="type">
<input class="form-check-input" type="radio" name="userType" id="userTypeUser"
[value]="organizationUserType.User" [(ngModel)]="type">
<label class="form-check-label" for="userTypeUser">
{{'user' | i18n}}
<small>{{'userDesc' | i18n}}</small>
</label>
</div>
<div class="form-check mt-2 form-check-block">
<input class="form-check-input" type="radio" name="userType" id="userTypeAdmin" [value]="organizationUserType.Admin" [(ngModel)]="type">
<input class="form-check-input" type="radio" name="userType" id="userTypeManager"
[value]="organizationUserType.Manager" [(ngModel)]="type">
<label class="form-check-label" for="userTypeManager">
{{'manager' | i18n}}
<small>{{'managerDesc' | i18n}}</small>
</label>
</div>
<div class="form-check mt-2 form-check-block">
<input class="form-check-input" type="radio" name="userType" id="userTypeAdmin"
[value]="organizationUserType.Admin" [(ngModel)]="type">
<label class="form-check-label" for="userTypeAdmin">
{{'admin' | i18n}}
<small>{{'adminDesc' | i18n}}</small>
</label>
</div>
<div class="form-check mt-2 form-check-block">
<input class="form-check-input" type="radio" name="userType" id="userTypeOwner" [value]="organizationUserType.Owner" [(ngModel)]="type">
<input class="form-check-input" type="radio" name="userType" id="userTypeOwner"
[value]="organizationUserType.Owner" [(ngModel)]="type">
<label class="form-check-label" for="userTypeOwner">
{{'owner' | i18n}}
<small>{{'ownerDesc' | i18n}}</small>
@@ -47,6 +66,10 @@
<h3 class="mt-4 d-flex">
<div class="mb-2">
{{'accessControl' | i18n}}
<a target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}"
href="https://bitwarden.com/help/article/user-types-access-control/#access-control">
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</div>
<div class="ml-auto" *ngIf="access === 'selected' && collections && collections.length">
<button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
@@ -59,13 +82,15 @@
</h3>
<div class="form-group" [ngClass]="{'mb-0': access !== 'selected'}">
<div class="form-check">
<input class="form-check-input" type="radio" name="access" id="accessAll" value="all" [(ngModel)]="access">
<input class="form-check-input" type="radio" name="access" id="accessAll" value="all"
[(ngModel)]="access">
<label class="form-check-label" for="accessAll">
{{'userAccessAllItems' | i18n}}
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="access" id="accessSelected" value="selected" [(ngModel)]="access">
<input class="form-check-input" type="radio" name="access" id="accessSelected" value="selected"
[(ngModel)]="access">
<label class="form-check-label" for="accessSelected">
{{'userAccessSelectedCollections' | i18n}}
</label>
@@ -80,19 +105,26 @@
<tr>
<th>&nbsp;</th>
<th>{{'name' | i18n}}</th>
<th width="100" class="text-center">{{'hidePasswords' | i18n}}</th>
<th width="100" class="text-center">{{'readOnly' | i18n}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let c of collections; let i = index">
<td class="table-list-checkbox" (click)="check(c)">
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked" appStopProp>
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked"
appStopProp>
</td>
<td (click)="check(c)">
<span appStopProp>{{c.name}}</span>
{{c.name}}
</td>
<td class="text-center">
<input type="checkbox" [(ngModel)]="c.readOnly" name="Collection[{{i}}].ReadOnly" [disabled]="!c.checked">
<input type="checkbox" [(ngModel)]="c.hidePasswords"
name="Collection[{{i}}].HidePasswords" [disabled]="!c.checked">
</td>
<td class="text-center">
<input type="checkbox" [(ngModel)]="c.readOnly" name="Collection[{{i}}].ReadOnly"
[disabled]="!c.checked">
</td>
</tr>
</tbody>
@@ -101,15 +133,18 @@
</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}}"></i>
<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>
<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" title="{{'delete' | i18n}}" *ngIf="editMode"
[disabled]="deleteBtn.loading" [appApiAction]="deletePromise">
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading"></i>
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading" title="{{'loading' | i18n}}"></i>
<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>

View File

@@ -67,6 +67,7 @@ export class UserAddEditComponent implements OnInit {
if (collection != null && collection.length > 0) {
(collection[0] as any).checked = true;
collection[0].readOnly = s.readOnly;
collection[0].hidePasswords = s.hidePasswords;
}
});
}
@@ -100,7 +101,7 @@ export class UserAddEditComponent implements OnInit {
let collections: SelectionReadOnlyRequest[] = null;
if (this.access !== 'all') {
collections = this.collections.filter((c) => (c as any).checked)
.map((c) => new SelectionReadOnlyRequest(c.id, !!c.readOnly));
.map((c) => new SelectionReadOnlyRequest(c.id, !!c.readOnly, !!c.hidePasswords));
}
try {

View File

@@ -0,0 +1,38 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="confirmUserTitle">
<div class="modal-dialog" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-header">
<h2 class="modal-title" id="confirmUserTitle">
{{'confirmUser' | i18n}}
<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">
<p>
{{'fingerprintEnsureIntegrityVerify' | i18n}}
<a href="https://help.bitwarden.com/article/fingerprint-phrase/" target="_blank" rel="noopener">
{{'learnMore' | i18n}}</a>
</p>
<p><code>{{fingerprint}}</code></p>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="dontAskAgain" name="DontAskAgain"
[(ngModel)]="dontAskAgain">
<label class="form-check-label" for="dontAskAgain">
{{'dontAskFingerprintAgain' | i18n}}
</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>{{'confirm' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary"
data-dismiss="modal">{{'cancel' | i18n}}</button>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,84 @@
import {
Component,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ConstantsService } from 'jslib/services/constants.service';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { OrganizationUserConfirmRequest } from 'jslib/models/request/organizationUserConfirmRequest';
import { Utils } from 'jslib/misc/utils';
@Component({
selector: 'app-user-confirm',
templateUrl: 'user-confirm.component.html',
})
export class UserConfirmComponent implements OnInit {
@Input() name: string;
@Input() userId: string;
@Input() organizationUserId: string;
@Input() organizationId: string;
@Output() onConfirmedUser = new EventEmitter();
dontAskAgain = false;
loading = true;
fingerprint: string;
formPromise: Promise<any>;
private publicKey: Uint8Array = null;
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private cryptoService: CryptoService, private storageService: StorageService) { }
async ngOnInit() {
try {
const publicKeyResponse = await this.apiService.getUserPublicKey(this.userId);
if (publicKeyResponse != null) {
this.publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
const fingerprint = await this.cryptoService.getFingerprint(this.userId, this.publicKey.buffer);
if (fingerprint != null) {
this.fingerprint = fingerprint.join('-');
}
}
} catch { }
this.loading = false;
}
async submit() {
if (this.loading) {
return;
}
if (this.dontAskAgain) {
await this.storageService.save(ConstantsService.autoConfirmFingerprints, true);
}
try {
this.formPromise = this.doConfirmation();
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Confirmed User' });
this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenConfirmed', this.name));
this.onConfirmedUser.emit();
} catch { }
}
private async doConfirmation() {
const orgKey = await this.cryptoService.getOrgKey(this.organizationId);
const key = await this.cryptoService.rsaEncrypt(orgKey.key, this.publicKey.buffer);
const request = new OrganizationUserConfirmRequest();
request.key = key.encryptedString;
await this.apiService.postOrganizationUserConfirm(this.organizationId, this.organizationUserId, request);
}
}

View File

@@ -1,17 +1,18 @@
<div class="modal fade">
<div class="modal-dialog">
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="groupAccessTitle">
<div class="modal-dialog" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-header">
<h2 class="modal-title">
<h2 class="modal-title" id="groupAccessTitle">
{{'groupAccess' | i18n}}
<small class="text-muted" *ngIf="name">{{name}}</small>
</h2>
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
<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}}"></i>
<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">
<p>{{'groupAccessUserDesc' | i18n}}</p>
@@ -25,7 +26,7 @@
<input type="checkbox" [(ngModel)]="g.checked" name="Groups[{{i}}].Checked" appStopProp>
</td>
<td (click)="check(g)">
<span appStopProp>{{g.name}}</span>
{{g.name}}
</td>
</tr>
</tbody>
@@ -33,10 +34,11 @@
</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}}"></i>
<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>
<button type="button" class="btn btn-outline-secondary"
data-dismiss="modal">{{'cancel' | i18n}}</button>
</div>
</form>
</div>

View File

@@ -2,22 +2,31 @@
<h1>{{'myOrganization' | i18n}}</h1>
</div>
<div *ngIf="loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</div>
<form *ngIf="org && !loading" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="name">{{'organizationName' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="org.name">
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="org.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)]="org.billingEmail">
<input id="billingEmail" class="form-control" type="text" name="BillingEmail"
[(ngModel)]="org.billingEmail" [disabled]="selfHosted">
</div>
<div class="form-group">
<label for="businessName">{{'businessName' | i18n}}</label>
<input id="businessName" class="form-control" type="text" name="BusinessName" [(ngModel)]="org.businessName">
<input id="businessName" class="form-control" type="text" name="BusinessName"
[(ngModel)]="org.businessName" [disabled]="selfHosted">
</div>
<div class="form-group">
<label for="identifier">{{'identifier' | i18n}}</label>
<input id="identifier" class="form-control" type="text" name="Identifier"
[(ngModel)]="org.identifier">
</div>
</div>
<div class="col-6">
@@ -25,26 +34,50 @@
</div>
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'save' | i18n}}</span>
</button>
</form>
<ng-container *ngIf="canUseApi">
<div class="secondary-header border-0 mb-0">
<h1>{{'apiKey' | i18n}}</h1>
</div>
<p>
{{'apiKeyDesc' | i18n}}
<a href="https://docs.bitwarden.com" target="_blank" rel="noopener">
{{'learnMore' | i18n}}
</a>
</p>
<button type="button" class="btn btn-outline-secondary" (click)="viewApiKey()">{{'viewApiKey' | i18n}}</button>
<button type="button" class="btn btn-outline-secondary" (click)="rotateApiKey()">{{'rotateApiKey' | i18n}}</button>
</ng-container>
<div class="secondary-header border-0 mb-0">
<h1>{{'taxInformation' | i18n}}</h1>
</div>
<p>{{'taxInformationDesc' | i18n}}</p>
<a href="https://bitwarden.com/contact/" target="_blank" rel="noopener" class="btn btn-outline-secondary">
{{'contactSupport' | i18n}}
</a>
<div *ngIf="!org || 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>
<form *ngIf="org && !loading" #formTax (ngSubmit)="submitTaxInfo()" [appApiAction]="taxFormPromise" ngNativeValidate>
<app-tax-info></app-tax-info>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="formTax.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'save' | i18n}}</span>
</button>
</form>
<div class="secondary-header text-danger border-0 mb-0">
<h1>{{'dangerZone' | i18n}}</h1>
</div>
<div class="card border-danger">
<div class="card-body">
<p>{{'dangerZoneDesc' | i18n}}</p>
<button type="button" class="btn btn-outline-danger" (click)="deleteOrganization()">{{'deleteOrganization' | i18n}}</button>
<button type="button" class="btn btn-outline-danger"
(click)="deleteOrganization()">{{'deleteOrganization' | i18n}}</button>
<button type="button" class="btn btn-outline-danger" (click)="purgeVault()">{{'purgeVault' | i18n}}</button>
</div>
</div>
<ng-template #deleteOrganizationTemplate></ng-template>
<ng-template #purgeOrganizationTemplate></ng-template>
<ng-template #apiKeyTemplate></ng-template>
<ng-template #rotateApiKeyTemplate></ng-template>

View File

@@ -11,6 +11,7 @@ import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SyncService } from 'jslib/abstractions/sync.service';
import { OrganizationUpdateRequest } from 'jslib/models/request/organizationUpdateRequest';
@@ -18,19 +19,28 @@ import { OrganizationResponse } from 'jslib/models/response/organizationResponse
import { ModalComponent } from '../../modal.component';
import { PurgeVaultComponent } from '../../settings/purge-vault.component';
import { TaxInfoComponent } from '../../settings/tax-info.component';
import { ApiKeyComponent } from './api-key.component';
import { DeleteOrganizationComponent } from './delete-organization.component';
import { RotateApiKeyComponent } from './rotate-api-key.component';
@Component({
selector: 'app-org-account',
templateUrl: 'account.component.html',
})
export class AccountComponent {
@ViewChild('deleteOrganizationTemplate', { read: ViewContainerRef }) deleteModalRef: ViewContainerRef;
@ViewChild('purgeOrganizationTemplate', { read: ViewContainerRef }) purgeModalRef: ViewContainerRef;
@ViewChild('deleteOrganizationTemplate', { read: ViewContainerRef, static: true }) deleteModalRef: ViewContainerRef;
@ViewChild('purgeOrganizationTemplate', { read: ViewContainerRef, static: true }) purgeModalRef: ViewContainerRef;
@ViewChild('apiKeyTemplate', { read: ViewContainerRef, static: true }) apiKeyModalRef: ViewContainerRef;
@ViewChild('rotateApiKeyTemplate', { read: ViewContainerRef, static: true }) rotateApiKeyModalRef: ViewContainerRef;
@ViewChild(TaxInfoComponent) taxInfo: TaxInfoComponent;
selfHosted = false;
loading = true;
canUseApi = false;
org: OrganizationResponse;
formPromise: Promise<any>;
taxFormPromise: Promise<any>;
private organizationId: string;
private modal: ModalComponent = null;
@@ -38,13 +48,16 @@ export class AccountComponent {
constructor(private componentFactoryResolver: ComponentFactoryResolver,
private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private route: ActivatedRoute, private syncService: SyncService) { }
private route: ActivatedRoute, private syncService: SyncService,
private platformUtilsService: PlatformUtilsService) { }
async ngOnInit() {
this.selfHosted = this.platformUtilsService.isSelfHost();
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
try {
this.org = await this.apiService.getOrganization(this.organizationId);
this.canUseApi = this.org.useApi;
} catch { }
});
this.loading = false;
@@ -56,6 +69,7 @@ export class AccountComponent {
request.name = this.org.name;
request.businessName = this.org.businessName;
request.billingEmail = this.org.billingEmail;
request.identifier = this.org.identifier;
this.formPromise = this.apiService.putOrganization(this.organizationId, request).then(() => {
return this.syncService.fullSync(true);
});
@@ -65,6 +79,13 @@ export class AccountComponent {
} catch { }
}
async submitTaxInfo() {
this.taxFormPromise = this.taxInfo.submitTaxInfo();
await this.taxFormPromise;
this.analytics.eventTrack.next({ action: 'Updated Organization Tax Info' });
this.toasterService.popAsync('success', null, this.i18nService.t('taxInfoUpdated'));
}
deleteOrganization() {
if (this.modal != null) {
this.modal.close();
@@ -95,4 +116,34 @@ export class AccountComponent {
this.modal = null;
});
}
viewApiKey() {
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.apiKeyModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<ApiKeyComponent>(ApiKeyComponent, this.apiKeyModalRef);
childComponent.organizationId = this.organizationId;
this.modal.onClosed.subscribe(async () => {
this.modal = null;
});
}
rotateApiKey() {
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.rotateApiKeyModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<RotateApiKeyComponent>(RotateApiKeyComponent, this.rotateApiKeyModalRef);
childComponent.organizationId = this.organizationId;
this.modal.onClosed.subscribe(async () => {
this.modal = null;
});
}
}

View File

@@ -1,11 +1,13 @@
<form #form class="card" (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="card-body">
<button type="button" class="close" appA11yTitle="{{'cancel' | i18n}}" (click)="cancel()"><span
aria-hidden="true">&times;</span></button>
<h3 class="card-body-header">{{(add ? 'addSeats' : 'removeSeats') | i18n}}</h3>
<div class="row">
<div class="form-group col-6">
<label for="seatAdjustment">{{(add ? 'seatsToAdd' : 'seatsToRemove') | i18n}}</label>
<input id="seatAdjustment" class="form-control" type="number" name="SeatAdjustment" [(ngModel)]="seatAdjustment" min="0"
step="1" required>
<input id="seatAdjustment" class="form-control" type="number" name="SeatAdjustment"
[(ngModel)]="seatAdjustment" min="0" step="1" required>
</div>
</div>
<div *ngIf="add" class="mb-3">
@@ -13,7 +15,7 @@
| currency:'$'}} /{{interval | i18n}}
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
<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()">
@@ -24,3 +26,4 @@
</small>
</div>
</form>
<app-payment [showMethods]="false"></app-payment>

View File

@@ -3,8 +3,14 @@ import {
EventEmitter,
Input,
Output,
ViewChild,
} from '@angular/core';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
@@ -13,6 +19,8 @@ import { I18nService } from 'jslib/abstractions/i18n.service';
import { SeatRequest } from 'jslib/models/request/seatRequest';
import { PaymentComponent } from '../../settings/payment.component';
@Component({
selector: 'app-adjust-seats',
templateUrl: 'adjust-seats.component.html',
@@ -25,11 +33,14 @@ export class AdjustSeatsComponent {
@Output() onAdjusted = new EventEmitter<number>();
@Output() onCanceled = new EventEmitter();
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
seatAdjustment = 0;
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService) { }
private analytics: Angulartics2, private toasterService: ToasterService,
private router: Router, private activatedRoute: ActivatedRoute) { }
async submit() {
try {
@@ -39,12 +50,32 @@ export class AdjustSeatsComponent {
request.seatAdjustment *= -1;
}
this.formPromise = this.apiService.postOrganizationSeat(this.organizationId, request);
let paymentFailed = false;
const action = async () => {
const result = await this.apiService.postOrganizationSeat(this.organizationId, request);
if (result != null && result.paymentIntentClientSecret != null) {
try {
await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null);
} catch {
paymentFailed = true;
}
}
};
this.formPromise = action();
await this.formPromise;
this.analytics.eventTrack.next({ action: this.add ? 'Added Seats' : 'Removed Seats' });
this.toasterService.popAsync('success', null,
this.i18nService.t('adjustedSeats', request.seatAdjustment.toString()));
this.onAdjusted.emit(this.seatAdjustment);
if (paymentFailed) {
this.toasterService.popAsync({
body: this.i18nService.t('couldNotChargeCardPayInvoice'),
type: 'warning',
timeout: 10000,
});
this.router.navigate(['../billing'], { relativeTo: this.activatedRoute });
} else {
this.toasterService.popAsync('success', null,
this.i18nService.t('adjustedSeats', request.seatAdjustment.toString()));
}
} catch { }
}

View File

@@ -0,0 +1,48 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="apiKeyTitle">
<div class="modal-dialog" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title" id="apiKeyTitle">{{'apiKey' | i18n}}</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>{{'apiKeyDesc' | i18n}}</p>
<ng-container *ngIf="!clientSecret">
<label for="masterPassword">{{'masterPass' | i18n}}</label>
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control"
[(ngModel)]="masterPassword" required appAutofocus appInputVerbatim>
</ng-container>
<app-callout type="warning" *ngIf="clientSecret">{{'apiKeyWarning' | i18n}}</app-callout>
<app-callout type="info" title="{{'oauth2ClientCredentials' | i18n}}" icon="fa-key"
*ngIf="clientSecret">
<p class="mb-1">
<strong>client_id:</strong><br>
<code>{{clientId}}</code>
</p>
<p class="mb-1">
<strong>client_secret:</strong><br>
<code>{{clientSecret}}</code>
</p>
<p class="mb-1">
<strong>scope:</strong><br>
<code>{{scope}}</code>
</p>
<p class="mb-0">
<strong>grant_type:</strong><br>
<code>client_credentials</code>
</p>
</app-callout>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"
*ngIf="!clientSecret">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'viewApiKey' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,50 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PasswordVerificationRequest } from 'jslib/models/request/passwordVerificationRequest';
import { ApiKeyResponse } from 'jslib/models/response/apiKeyResponse';
@Component({
selector: 'app-api-key',
templateUrl: 'api-key.component.html',
})
export class ApiKeyComponent {
organizationId: string;
masterPassword: string;
formPromise: Promise<ApiKeyResponse>;
clientId: string;
clientSecret: string;
scope: string;
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private cryptoService: CryptoService, private router: Router) { }
async submit() {
if (this.masterPassword == null || this.masterPassword === '') {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPassRequired'));
return;
}
const request = new PasswordVerificationRequest();
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
try {
this.formPromise = this.apiService.postOrganizationApiKey(this.organizationId, request);
const response = await this.formPromise;
this.clientSecret = response.apiKey;
this.clientId = 'organization.' + this.organizationId;
this.scope = 'api.organization';
this.analytics.eventTrack.next({ action: 'Viewed Organization API Key' });
} catch { }
}
}

View File

@@ -0,0 +1,11 @@
<div class="card card-org-plans">
<div class="card-body">
<button type="button" class="close" appA11yTitle="{{'cancel' | i18n}}" (click)="cancel()"><span
aria-hidden="true">&times;</span></button>
<h2 class="card-body-header">{{'changeBillingPlan' | i18n}}</h2>
<p class="mb-0">{{'changeBillingPlanUpgrade' | i18n}}</p>
<app-organization-plans [showFree]="false" [showCancel]="true" [plan]="defaultUpgradePlan"
[product]="defaultUpgradeProduct" [organizationId]="organizationId" (onCanceled)="cancel()">
</app-organization-plans>
</div>
</div>

View File

@@ -0,0 +1,39 @@
import {
Component,
EventEmitter,
Input,
Output,
} from '@angular/core';
import { ApiService } from 'jslib/abstractions/api.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { PlanType } from 'jslib/enums/planType';
import { ProductType } from 'jslib/enums/productType';
@Component({
selector: 'app-change-plan',
templateUrl: 'change-plan.component.html',
})
export class ChangePlanComponent {
@Input() organizationId: string;
@Output() onChanged = new EventEmitter();
@Output() onCanceled = new EventEmitter();
formPromise: Promise<any>;
defaultUpgradePlan: PlanType = PlanType.FamiliesAnnually;
defaultUpgradeProduct: ProductType = ProductType.Families;
constructor(private apiService: ApiService, private platformUtilsService: PlatformUtilsService) { }
async submit() {
try {
this.platformUtilsService.eventTrack('Changed Plan');
this.onChanged.emit();
} catch { }
}
cancel() {
this.onCanceled.emit();
}
}

View File

@@ -1,9 +1,9 @@
<div class="modal fade">
<div class="modal-dialog">
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="deleteOrganizationTitle">
<div class="modal-dialog" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title">{{'deleteOrganization' | i18n}}</h2>
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
<h2 class="modal-title" id="deleteOrganizationTitle">{{'deleteOrganization' | i18n}}</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
@@ -11,12 +11,12 @@
<p>{{'deleteOrganizationDesc' | i18n}}</p>
<app-callout type="warning">{{'deleteOrganizationWarning' | i18n}}</app-callout>
<label for="masterPassword">{{'masterPass' | i18n}}</label>
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control" [(ngModel)]="masterPassword" required
appAutofocus appInputVerbatim>
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control"
[(ngModel)]="masterPassword" required appAutofocus appInputVerbatim>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-danger btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'deleteOrganization' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>

View File

@@ -0,0 +1,27 @@
<form #form class="card" (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="card-body">
<button type="button" class="close" appA11yTitle="{{'cancel' | i18n}}" (click)="cancel()"><span
aria-hidden="true">&times;</span></button>
<h3 class="card-body-header">{{'downloadLicense' | i18n}}</h3>
<div class="row">
<div class="form-group col-6">
<div class="d-flex">
<label for="installationId">{{'enterInstallationId' | i18n}}</label>
<a class="ml-auto" target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}"
href="https://help.bitwarden.com/article/licensing-on-premise/#organization-account-sharing">
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</div>
<input id="installationId" class="form-control" type="text" name="InstallationId"
[(ngModel)]="installationId" required>
</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>{{'submit' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()">
{{'cancel' | i18n}}
</button>
</div>
</form>

View File

@@ -0,0 +1,43 @@
import {
Component,
EventEmitter,
Input,
Output,
} from '@angular/core';
import { ApiService } from 'jslib/abstractions/api.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
@Component({
selector: 'app-download-license',
templateUrl: 'download-license.component.html',
})
export class DownloadLicenseComponent {
@Input() organizationId: string;
@Output() onDownloaded = new EventEmitter();
@Output() onCanceled = new EventEmitter();
installationId: string;
formPromise: Promise<any>;
constructor(private apiService: ApiService, private platformUtilsService: PlatformUtilsService) { }
async submit() {
if (this.installationId == null || this.installationId === '') {
return;
}
try {
this.formPromise = this.apiService.getOrganizationLicense(this.organizationId, this.installationId);
const license = await this.formPromise;
const licenseString = JSON.stringify(license, null, 2);
this.platformUtilsService.saveFile(window, licenseString, null, 'bitwarden_organization_license.json');
this.platformUtilsService.eventTrack('Downloaded License');
this.onDownloaded.emit();
} catch { }
}
cancel() {
this.onCanceled.emit();
}
}

View File

@@ -1,195 +0,0 @@
<div class="page-header">
<h1>
{{'billingAndLicensing' | i18n}}
<small>
<i class="fa fa-spinner fa-spin text-muted" *ngIf="firstLoaded && loading" title="{{'loading' | i18n}}"></i>
</small>
</h1>
</div>
<i class="fa fa-spinner fa-spin text-muted" *ngIf="!firstLoaded && loading" title="{{'loading' | i18n}}"></i>
<ng-container *ngIf="billing">
<app-callout type="warning" title="{{'canceled' | i18n}}" *ngIf="subscription && subscription.cancelled">{{'subscriptionCanceled' | i18n}}</app-callout>
<app-callout type="warning" title="{{'pendingCancellation' | i18n}}" *ngIf="subscriptionMarkedForCancel">
<p>{{'subscriptionPendingCanceled' | i18n}}</p>
<button #reinstateBtn type="button" class="btn btn-outline-secondary btn-submit" (click)="reinstate()" [appApiAction]="reinstatePromise"
[disabled]="reinstateBtn.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
<span>{{'reinstateSubscription' | i18n}}</span>
</button>
</app-callout>
<dl *ngIf="selfHosted">
<dt>{{'billingPlan' | i18n}}</dt>
<dd>{{billing.plan}}</dd>
<dt>{{'expiration' | i18n}}</dt>
<dd *ngIf="billing.expiration">
{{billing.expiration | date:'mediumDate'}}
<span *ngIf="isExpired" class="text-danger ml-2">
<i class="fa fa-exclamation-triangle"></i>
{{'licenseIsExpired' | i18n}}
</span>
</dd>
<dd *ngIf="!billing.expiration">{{'neverExpires' | i18n}}</dd>
</dl>
<div class="row" *ngIf="!selfHosted">
<div class="col-4">
<dl>
<dt>{{'billingPlan' | i18n}}</dt>
<dd>{{billing.plan}}</dd>
<ng-container *ngIf="subscription">
<dt>{{'status' | i18n}}</dt>
<dd>
<span class="text-capitalize">{{subscription.status || '-'}}</span>
<span class="badge badge-warning" *ngIf="subscriptionMarkedForCancel">{{'pendingCancellation' | i18n}}</span>
</dd>
<dt>{{'nextCharge' | i18n}}</dt>
<dd>{{nextInvoice ? ((nextInvoice.date | date: 'mediumDate') + ', ' + (nextInvoice.amount | currency:'$'))
: '-'}}
</dd>
</ng-container>
</dl>
</div>
<div class="col-8" *ngIf="subscription">
<strong class="d-block mb-1">{{'details' | i18n}}</strong>
<table class="table">
<tbody>
<tr *ngFor="let i of subscription.items">
<td>
{{i.name}} {{i.quantity > 1 ? '&times;' + i.quantity : ''}} @ {{i.amount | currency:'$'}}
</td>
<td>
{{(i.quantity * i.amount) | currency:'$'}} /{{i.interval | i18n}}
</td>
</tr>
</tbody>
</table>
</div>
</div>
<ng-container *ngIf="selfHosted">
<div>
<button type="button" class="btn btn-outline-secondary" (click)="updateLicense()">
{{'updateLicense' | i18n}}
</button>
<a href="https://vault.bitwarden.com" target="_blank" rel="noopener" class="btn btn-outline-secondary">
{{'manageSubscription' | i18n}}
</a>
</div>
<div class="card mt-3" *ngIf="showUpdateLicense">
<div class="card-body">
<h3 class="card-body-header">{{'updateLicense' | i18n}}</h3>
<app-update-license [organizationId]="organizationId" (onUpdated)="closeUpdateLicense(true)" (onCanceled)="closeUpdateLicense(false)"></app-update-license>
</div>
</div>
</ng-container>
<ng-container *ngIf="!selfHosted">
<div class="d-flex">
<button type="button" class="btn btn-outline-secondary" (click)="changePlan()">
{{'changeBillingPlan' | i18n}}
</button>
<button type="button" class="btn btn-outline-secondary btn-submit ml-1" #licenseBtn [appApiAction]="licensePromise" [disabled]="licenseBtn.loading"
(click)="downloadLicense()" *ngIf="canDownloadLicense">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
<span>{{'downloadLicense' | i18n}}</span>
</button>
<button #cancelBtn type="button" class="btn btn-outline-danger btn-submit ml-auto" (click)="cancel()" [appApiAction]="cancelPromise"
[disabled]="cancelBtn.loading" *ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
<span>{{'cancelSubscription' | i18n}}</span>
</button>
</div>
<h2 class="spaced-header">{{'userSeats' | i18n}}</h2>
<p>{{'subscriptionUserSeats' | i18n : billing.seats}}</p>
<ng-container *ngIf="subscription && canAdjustSeats">
<div class="mt-3">
<div class="d-flex" *ngIf="!showAdjustSeats">
<button type="button" class="btn btn-outline-secondary" (click)="adjustSeats(true)">
{{'addSeats' | i18n}}
</button>
<button type="button" class="btn btn-outline-secondary ml-1" (click)="adjustSeats(false)">
{{'removeSeats' | i18n}}
</button>
</div>
<app-adjust-seats [seatPrice]="seatPrice" [add]="adjustSeatsAdd" [organizationId]="organizationId" [interval]="billingInterval"
(onAdjusted)="closeSeats(true)" (onCanceled)="closeSeats(false)" *ngIf="showAdjustSeats"></app-adjust-seats>
</div>
</ng-container>
<h2 class="spaced-header">{{'storage' | i18n}}</h2>
<p>{{'subscriptionStorage' | i18n : billing.maxStorageGb || 0 : billing.storageName || '0 MB'}}</p>
<div class="progress">
<div class="progress-bar bg-success" role="progressbar" [ngStyle]="{width: storageProgressWidth + '%' }" [attr.aria-valuenow]="storagePercentage"
aria-valuemin="0" aria-valuemax="100">{{(storagePercentage / 100) | percent}}</div>
</div>
<ng-container *ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel && paymentSource">
<div class="mt-3">
<div class="d-flex" *ngIf="!showAdjustStorage">
<button type="button" class="btn btn-outline-secondary" (click)="adjustStorage(true)">
{{'addStorage' | i18n}}
</button>
<button type="button" class="btn btn-outline-secondary ml-1" (click)="adjustStorage(false)">
{{'removeStorage' | i18n}}
</button>
</div>
<app-adjust-storage [storageGbPrice]="storageGbPrice" [add]="adjustStorageAdd" [organizationId]="organizationId" [interval]="billingInterval"
(onAdjusted)="closeStorage(true)" (onCanceled)="closeStorage(false)" *ngIf="showAdjustStorage"></app-adjust-storage>
</div>
</ng-container>
<h2 class="spaced-header">{{'paymentMethod' | i18n}}</h2>
<p *ngIf="!paymentSource">{{'noPaymentMethod' | i18n}}</p>
<ng-container *ngIf="paymentSource">
<app-callout type="warning" title="{{'verifyBankAccount' | i18n}}" *ngIf="paymentSource.type === paymentMethodType.BankAccount && paymentSource.needsVerification">
<p>{{'verifyBankAccountDesc' | i18n}} {{'verifyBankAccountFailureWarning' | i18n}}</p>
<form #verifyForm class="form-inline" (ngSubmit)="verifyBank()" [appApiAction]="verifyBankPromise" ngNativeValidate>
<label class="sr-only" for="verifyAmount1">{{'amount' | i18n : '1'}}</label>
<div class="input-group mr-2">
<div class="input-group-prepend">
<div class="input-group-text">$0.</div>
</div>
<input type="number" class="form-control" id="verifyAmount1" placeholder="xx" name="Amount1" [(ngModel)]="verifyAmount1"
min="1" max="99" step="1" required>
</div>
<label class="sr-only" for="verifyAmount2">{{'amount' | i18n : '2'}}</label>
<div class="input-group mr-2">
<div class="input-group-prepend">
<div class="input-group-text">$0.</div>
</div>
<input type="number" class="form-control" id="verifyAmount2" placeholder="xx" name="Amount2" [(ngModel)]="verifyAmount2"
min="1" max="99" step="1" required>
</div>
<button type="submit" class="btn btn-outline-primary btn-submit" [disabled]="verifyForm.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
<span>{{'verifyBankAccount' | i18n}}</span>
</button>
</form>
</app-callout>
<p>
<i class="fa fa-fw" [ngClass]="{'fa-credit-card': paymentSource.type === paymentMethodType.Card,
'fa-university': paymentSource.type === paymentMethodType.BankAccount,
'fa-paypal text-primary': paymentSource.type === paymentMethodType.PayPal}"></i>
{{paymentSource.description}}
</p>
</ng-container>
<button type="button" class="btn btn-outline-secondary" (click)="changePayment()" *ngIf="!showAdjustPayment">
{{(paymentSource ? 'changePaymentMethod' : 'addPaymentMethod') | i18n}}
</button>
<app-adjust-payment [currentType]="paymentSource != null ? paymentSource.type : null" [organizationId]="organizationId" (onAdjusted)="closePayment(true)"
(onCanceled)="closePayment(false)" *ngIf="showAdjustPayment">
</app-adjust-payment>
<h2 class="spaced-header">{{'charges' | i18n}}</h2>
<p *ngIf="!charges || !charges.length">{{'noCharges' | i18n}}</p>
<table class="table mb-2" *ngIf="charges && charges.length">
<tbody>
<tr *ngFor="let c of charges">
<td>
<a href="#" appStopClick (click)="viewInvoice(c)" title="{{'invoice' | i18n}}">
<i class="fa fa-file-pdf-o"></i>
</a>
</td>
<td>{{c.createdDate | date:'mediumDate'}}</td>
<td>{{c.paymentSource ? c.paymentSource.description : '-'}}</td>
<td class="text-capitalize">{{c.status}}</td>
<td [ngClass]="{'text-strike':c.refunded}" title="{{(c.refunded ? 'refunded' : '') | i18n}}">{{c.amount | currency:'$'}}</td>
</tr>
</tbody>
</table>
<small class="text-muted">* {{'chargesStatement' | i18n : 'BITWARDEN'}}</small>
</ng-container>
</ng-container>

View File

@@ -7,49 +7,21 @@ import { ActivatedRoute } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { VerifyBankRequest } from 'jslib/models/request/verifyBankRequest';
import { BillingChargeResponse } from 'jslib/models/response/billingResponse';
import { OrganizationBillingResponse } from 'jslib/models/response/organizationBillingResponse';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { TokenService } from 'jslib/abstractions/token.service';
import { PaymentMethodType } from 'jslib/enums/paymentMethodType';
import { PlanType } from 'jslib/enums/planType';
import { UserBillingComponent } from '../../settings/user-billing.component';
@Component({
selector: 'app-org-billing',
templateUrl: 'organization-billing.component.html',
templateUrl: '../../settings/user-billing.component.html',
})
export class OrganizationBillingComponent implements OnInit {
loading = false;
firstLoaded = false;
organizationId: string;
adjustSeatsAdd = true;
showAdjustSeats = false;
adjustStorageAdd = true;
showAdjustStorage = false;
showAdjustPayment = false;
showUpdateLicense = false;
billing: OrganizationBillingResponse;
paymentMethodType = PaymentMethodType;
selfHosted = false;
verifyAmount1: number;
verifyAmount2: number;
cancelPromise: Promise<any>;
reinstatePromise: Promise<any>;
licensePromise: Promise<any>;
verifyBankPromise: Promise<any>;
constructor(private tokenService: TokenService, private apiService: ApiService,
private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private route: ActivatedRoute) {
this.selfHosted = platformUtilsService.isSelfHost();
export class OrganizationBillingComponent extends UserBillingComponent implements OnInit {
constructor(apiService: ApiService, i18nService: I18nService,
analytics: Angulartics2, toasterService: ToasterService,
private route: ActivatedRoute, platformUtilsService: PlatformUtilsService) {
super(apiService, i18nService, analytics, toasterService, platformUtilsService);
}
async ngOnInit() {
@@ -59,222 +31,4 @@ export class OrganizationBillingComponent implements OnInit {
this.firstLoaded = true;
});
}
async load() {
if (this.loading) {
return;
}
this.loading = true;
this.billing = await this.apiService.getOrganizationBilling(this.organizationId);
this.loading = false;
}
async reinstate() {
if (this.loading) {
return;
}
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('reinstateConfirmation'),
this.i18nService.t('reinstateSubscription'), this.i18nService.t('yes'), this.i18nService.t('cancel'));
if (!confirmed) {
return;
}
try {
this.reinstatePromise = this.apiService.postOrganizationReinstate(this.organizationId);
await this.reinstatePromise;
this.analytics.eventTrack.next({ action: 'Reinstated Plan' });
this.toasterService.popAsync('success', null, this.i18nService.t('reinstated'));
this.load();
} catch { }
}
async cancel() {
if (this.loading) {
return;
}
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('cancelConfirmation'),
this.i18nService.t('cancelSubscription'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return;
}
try {
this.cancelPromise = this.apiService.postOrganizationCancel(this.organizationId);
await this.cancelPromise;
this.analytics.eventTrack.next({ action: 'Canceled Plan' });
this.toasterService.popAsync('success', null, this.i18nService.t('canceledSubscription'));
this.load();
} catch { }
}
async changePlan() {
const contactSupport = await this.platformUtilsService.showDialog(this.i18nService.t('changeBillingPlanDesc'),
this.i18nService.t('changeBillingPlan'), this.i18nService.t('contactSupport'), this.i18nService.t('close'));
if (contactSupport) {
this.platformUtilsService.launchUri('https://bitwarden.com/contact');
}
}
async downloadLicense() {
if (this.loading) {
return;
}
const installationId = window.prompt(this.i18nService.t('enterInstallationId'));
if (installationId == null || installationId === '') {
return;
}
try {
this.licensePromise = this.apiService.getOrganizationLicense(this.organizationId, installationId);
const license = await this.licensePromise;
const licenseString = JSON.stringify(license, null, 2);
this.platformUtilsService.saveFile(window, licenseString, null, 'bitwarden_organization_license.json');
} catch { }
}
updateLicense() {
if (this.loading) {
return;
}
this.showUpdateLicense = true;
}
async verifyBank() {
if (this.loading) {
return;
}
try {
const request = new VerifyBankRequest();
request.amount1 = this.verifyAmount1;
request.amount2 = this.verifyAmount2;
this.verifyBankPromise = this.apiService.postOrganizationVerifyBank(this.organizationId, request);
await this.verifyBankPromise;
this.analytics.eventTrack.next({ action: 'Verified Bank Account' });
this.toasterService.popAsync('success', null, this.i18nService.t('verifiedBankAccount'));
this.load();
} catch { }
}
closeUpdateLicense(load: boolean) {
this.showUpdateLicense = false;
if (load) {
this.load();
}
}
adjustSeats(add: boolean) {
this.adjustSeatsAdd = add;
this.showAdjustSeats = true;
}
closeSeats(load: boolean) {
this.showAdjustSeats = false;
if (load) {
this.load();
}
}
adjustStorage(add: boolean) {
this.adjustStorageAdd = add;
this.showAdjustStorage = true;
}
closeStorage(load: boolean) {
this.showAdjustStorage = false;
if (load) {
this.load();
}
}
changePayment() {
this.showAdjustPayment = true;
}
closePayment(load: boolean) {
this.showAdjustPayment = false;
if (load) {
this.load();
}
}
async viewInvoice(charge: BillingChargeResponse) {
const token = await this.tokenService.getToken();
const url = this.apiService.apiBaseUrl + '/organizations/' + this.organizationId +
'/billing-invoice/' + charge.invoiceId + '?access_token=' + token;
this.platformUtilsService.launchUri(url);
}
get isExpired() {
return this.billing != null && this.billing.expiration != null &&
new Date(this.billing.expiration) < new Date();
}
get subscriptionMarkedForCancel() {
return this.subscription != null && !this.subscription.cancelled && this.subscription.cancelAtEndDate;
}
get subscription() {
return this.billing != null ? this.billing.subscription : null;
}
get nextInvoice() {
return this.billing != null ? this.billing.upcomingInvoice : null;
}
get paymentSource() {
return this.billing != null ? this.billing.paymentSource : null;
}
get charges() {
return this.billing != null ? this.billing.charges : null;
}
get storagePercentage() {
return this.billing != null && this.billing.maxStorageGb ?
+(100 * (this.billing.storageGb / this.billing.maxStorageGb)).toFixed(2) : 0;
}
get storageProgressWidth() {
return this.storagePercentage < 5 ? 5 : 0;
}
get billingInterval() {
const monthly = this.billing.planType === PlanType.EnterpriseMonthly ||
this.billing.planType === PlanType.TeamsMonthly;
return monthly ? 'month' : 'year';
}
get storageGbPrice() {
return this.billingInterval === 'month' ? 0.5 : 4;
}
get seatPrice() {
switch (this.billing.planType) {
case PlanType.EnterpriseMonthly:
return 4;
case PlanType.EnterpriseAnnually:
return 36;
case PlanType.TeamsMonthly:
return 2.5;
case PlanType.TeamsAnnually:
return 24;
default:
return 0;
}
}
get canAdjustSeats() {
return this.billing.planType === PlanType.EnterpriseMonthly ||
this.billing.planType === PlanType.EnterpriseAnnually ||
this.billing.planType === PlanType.TeamsMonthly || this.billing.planType === PlanType.TeamsAnnually;
}
get canDownloadLicense() {
return (this.billing.planType !== PlanType.Free && this.subscription == null) ||
(this.subscription != null && !this.subscription.cancelled);
}
}

View File

@@ -0,0 +1,154 @@
<div class="page-header">
<h1>
{{'subscription' | i18n}}
<small *ngIf="firstLoaded && loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</small>
</h1>
</div>
<ng-container *ngIf="!firstLoaded && loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</ng-container>
<ng-container *ngIf="sub">
<app-callout type="warning" title="{{'canceled' | i18n}}" *ngIf="subscription && subscription.cancelled">
{{'subscriptionCanceled' | i18n}}</app-callout>
<app-callout type="warning" title="{{'pendingCancellation' | i18n}}" *ngIf="subscriptionMarkedForCancel">
<p>{{'subscriptionPendingCanceled' | i18n}}</p>
<button #reinstateBtn type="button" class="btn btn-outline-secondary btn-submit" (click)="reinstate()"
[appApiAction]="reinstatePromise" [disabled]="reinstateBtn.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'reinstateSubscription' | i18n}}</span>
</button>
</app-callout>
<dl *ngIf="selfHosted">
<dt>{{'billingPlan' | i18n}}</dt>
<dd>{{sub.plan.name}}</dd>
<dt>{{'expiration' | i18n}}</dt>
<dd *ngIf="sub.expiration">
{{sub.expiration | date:'mediumDate'}}
<span *ngIf="isExpired" class="text-danger ml-2">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
{{'licenseIsExpired' | i18n}}
</span>
</dd>
<dd *ngIf="!sub.expiration">{{'neverExpires' | i18n}}</dd>
</dl>
<div class="row" *ngIf="!selfHosted">
<div class="col-4">
<dl>
<dt>{{'billingPlan' | i18n}}</dt>
<dd>{{sub.plan.name}}</dd>
<ng-container *ngIf="subscription">
<dt>{{'status' | i18n}}</dt>
<dd>
<span class="text-capitalize">{{subscription.status || '-'}}</span>
<span class="badge badge-warning"
*ngIf="subscriptionMarkedForCancel">{{'pendingCancellation' | i18n}}</span>
</dd>
<dt>{{'nextCharge' | i18n}}</dt>
<dd>{{nextInvoice ? ((nextInvoice.date | date: 'mediumDate') + ', ' + (nextInvoice.amount | currency:'$'))
: '-'}}
</dd>
</ng-container>
</dl>
</div>
<div class="col-8" *ngIf="subscription">
<strong class="d-block mb-1">{{'details' | i18n}}</strong>
<table class="table">
<tbody>
<tr *ngFor="let i of subscription.items">
<td>
{{i.name}} {{i.quantity > 1 ? '&times;' + i.quantity : ''}} @ {{i.amount | currency:'$'}}
</td>
<td>
{{(i.quantity * i.amount) | currency:'$'}} /{{i.interval | i18n}}
</td>
</tr>
</tbody>
</table>
</div>
</div>
<ng-container *ngIf="selfHosted">
<div>
<button type="button" class="btn btn-outline-secondary" (click)="updateLicense()">
{{'updateLicense' | i18n}}
</button>
<a href="https://vault.bitwarden.com" target="_blank" rel="noopener" class="btn btn-outline-secondary">
{{'manageSubscription' | i18n}}
</a>
</div>
<div class="card mt-3" *ngIf="showUpdateLicense">
<div class="card-body">
<button type="button" class="close" appA11yTitle="{{'cancel' | i18n}}"
(click)="closeUpdateLicense(false)"><span aria-hidden="true">&times;</span></button>
<h3 class="card-body-header">{{'updateLicense' | i18n}}</h3>
<app-update-license [organizationId]="organizationId" (onUpdated)="closeUpdateLicense(true)"
(onCanceled)="closeUpdateLicense(false)"></app-update-license>
</div>
</div>
</ng-container>
<ng-container *ngIf="!selfHosted">
<div class="d-flex">
<button type="button" class="btn btn-outline-secondary" (click)="changePlan()" *ngIf="!showChangePlan">
{{'changeBillingPlan' | i18n}}
</button>
<button type="button" class="btn btn-outline-secondary ml-1" (click)="downloadLicense()"
*ngIf="canDownloadLicense" [disabled]="showDownloadLicense">
{{'downloadLicense' | i18n}}
</button>
<button #cancelBtn type="button" class="btn btn-outline-danger btn-submit ml-auto" (click)="cancel()"
[appApiAction]="cancelPromise" [disabled]="cancelBtn.loading"
*ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'cancelSubscription' | i18n}}</span>
</button>
</div>
<app-change-plan [organizationId]="organizationId" (onChanged)="closeChangePlan(true)"
(onCanceled)="closeChangePlan(false)" *ngIf="showChangePlan"></app-change-plan>
<div class="mt-3" *ngIf="showDownloadLicense">
<app-download-license [organizationId]="organizationId" (onDownloaded)="closeDownloadLicense()"
(onCanceled)="closeDownloadLicense()"></app-download-license>
</div>
<h2 class="spaced-header">{{'userSeats' | i18n}}</h2>
<p>{{'subscriptionUserSeats' | i18n : sub.seats}}</p>
<ng-container *ngIf="subscription && canAdjustSeats && !subscription.cancelled && !subscriptionMarkedForCancel">
<div class="mt-3">
<div class="d-flex" *ngIf="!showAdjustSeats">
<button type="button" class="btn btn-outline-secondary" (click)="adjustSeats(true)">
{{'addSeats' | i18n}}
</button>
<button type="button" class="btn btn-outline-secondary ml-1" (click)="adjustSeats(false)">
{{'removeSeats' | i18n}}
</button>
</div>
<app-adjust-seats [seatPrice]="seatPrice" [add]="adjustSeatsAdd" [organizationId]="organizationId"
[interval]="billingInterval" (onAdjusted)="closeSeats(true)" (onCanceled)="closeSeats(false)"
*ngIf="showAdjustSeats"></app-adjust-seats>
</div>
</ng-container>
<h2 class="spaced-header">{{'storage' | i18n}}</h2>
<p>{{'subscriptionStorage' | i18n : sub.maxStorageGb || 0 : sub.storageName || '0 MB'}}</p>
<div class="progress">
<div class="progress-bar bg-success" role="progressbar" [ngStyle]="{width: storageProgressWidth + '%' }"
[attr.aria-valuenow]="storagePercentage" aria-valuemin="0" aria-valuemax="100">
{{(storagePercentage / 100) | percent}}</div>
</div>
<ng-container *ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel">
<div class="mt-3">
<div class="d-flex" *ngIf="!showAdjustStorage">
<button type="button" class="btn btn-outline-secondary" (click)="adjustStorage(true)">
{{'addStorage' | i18n}}
</button>
<button type="button" class="btn btn-outline-secondary ml-1" (click)="adjustStorage(false)">
{{'removeStorage' | i18n}}
</button>
</div>
<app-adjust-storage [storageGbPrice]="storageGbPrice" [add]="adjustStorageAdd"
[organizationId]="organizationId" [interval]="billingInterval" (onAdjusted)="closeStorage(true)"
(onCanceled)="closeStorage(false)" *ngIf="showAdjustStorage"></app-adjust-storage>
</div>
</ng-container>
</ng-container>
</ng-container>

View File

@@ -0,0 +1,214 @@
import {
Component,
OnInit,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { OrganizationSubscriptionResponse } from 'jslib/models/response/organizationSubscriptionResponse';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { PlanType } from 'jslib/enums/planType';
@Component({
selector: 'app-org-subscription',
templateUrl: 'organization-subscription.component.html',
})
export class OrganizationSubscriptionComponent implements OnInit {
loading = false;
firstLoaded = false;
organizationId: string;
adjustSeatsAdd = true;
showAdjustSeats = false;
adjustStorageAdd = true;
showAdjustStorage = false;
showUpdateLicense = false;
showDownloadLicense = false;
showChangePlan = false;
sub: OrganizationSubscriptionResponse;
selfHosted = false;
cancelPromise: Promise<any>;
reinstatePromise: Promise<any>;
constructor(private apiService: ApiService, private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService, private analytics: Angulartics2,
private toasterService: ToasterService, private messagingService: MessagingService,
private route: ActivatedRoute) {
this.selfHosted = platformUtilsService.isSelfHost();
}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
await this.load();
this.firstLoaded = true;
});
}
async load() {
if (this.loading) {
return;
}
this.loading = true;
this.sub = await this.apiService.getOrganizationSubscription(this.organizationId);
this.loading = false;
}
async reinstate() {
if (this.loading) {
return;
}
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('reinstateConfirmation'),
this.i18nService.t('reinstateSubscription'), this.i18nService.t('yes'), this.i18nService.t('cancel'));
if (!confirmed) {
return;
}
try {
this.reinstatePromise = this.apiService.postOrganizationReinstate(this.organizationId);
await this.reinstatePromise;
this.analytics.eventTrack.next({ action: 'Reinstated Plan' });
this.toasterService.popAsync('success', null, this.i18nService.t('reinstated'));
this.load();
} catch { }
}
async cancel() {
if (this.loading) {
return;
}
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('cancelConfirmation'),
this.i18nService.t('cancelSubscription'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return;
}
try {
this.cancelPromise = this.apiService.postOrganizationCancel(this.organizationId);
await this.cancelPromise;
this.analytics.eventTrack.next({ action: 'Canceled Plan' });
this.toasterService.popAsync('success', null, this.i18nService.t('canceledSubscription'));
this.load();
} catch { }
}
async changePlan() {
if (this.subscription == null && this.sub.planType === PlanType.Free) {
this.showChangePlan = !this.showChangePlan;
return;
}
const contactSupport = await this.platformUtilsService.showDialog(this.i18nService.t('changeBillingPlanDesc'),
this.i18nService.t('changeBillingPlan'), this.i18nService.t('contactSupport'), this.i18nService.t('close'));
if (contactSupport) {
this.platformUtilsService.launchUri('https://bitwarden.com/contact');
}
}
closeChangePlan(changed: boolean) {
this.showChangePlan = false;
}
downloadLicense() {
this.showDownloadLicense = !this.showDownloadLicense;
}
closeDownloadLicense() {
this.showDownloadLicense = false;
}
updateLicense() {
if (this.loading) {
return;
}
this.showUpdateLicense = true;
}
closeUpdateLicense(updated: boolean) {
this.showUpdateLicense = false;
if (updated) {
this.load();
this.messagingService.send('updatedOrgLicense');
}
}
adjustSeats(add: boolean) {
this.adjustSeatsAdd = add;
this.showAdjustSeats = true;
}
closeSeats(load: boolean) {
this.showAdjustSeats = false;
if (load) {
this.load();
}
}
adjustStorage(add: boolean) {
this.adjustStorageAdd = add;
this.showAdjustStorage = true;
}
closeStorage(load: boolean) {
this.showAdjustStorage = false;
if (load) {
this.load();
}
}
get isExpired() {
return this.sub != null && this.sub.expiration != null &&
new Date(this.sub.expiration) < new Date();
}
get subscriptionMarkedForCancel() {
return this.subscription != null && !this.subscription.cancelled && this.subscription.cancelAtEndDate;
}
get subscription() {
return this.sub != null ? this.sub.subscription : null;
}
get nextInvoice() {
return this.sub != null ? this.sub.upcomingInvoice : null;
}
get storagePercentage() {
return this.sub != null && this.sub.maxStorageGb ?
+(100 * (this.sub.storageGb / this.sub.maxStorageGb)).toFixed(2) : 0;
}
get storageProgressWidth() {
return this.storagePercentage < 5 ? 5 : 0;
}
get billingInterval() {
const monthly = !this.sub.plan.isAnnual;
return monthly ? 'month' : 'year';
}
get storageGbPrice() {
return this.sub.plan.additionalStoragePricePerGb;
}
get seatPrice() {
return this.sub.plan.seatPrice;
}
get canAdjustSeats() {
return this.sub.plan.hasAdditionalSeatsOption;
}
get canDownloadLicense() {
return (this.sub.planType !== PlanType.Free && this.subscription == null) ||
(this.subscription != null && !this.subscription.cancelled);
}
}

View File

@@ -0,0 +1,48 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="rotateKeyTitle">
<div class="modal-dialog" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title" id="rotateKeyTitle">{{'rotateApiKey' | i18n}}</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>{{'apiKeyRotateDesc' | i18n}}</p>
<ng-container *ngIf="!clientSecret">
<label for="masterPassword">{{'masterPass' | i18n}}</label>
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control"
[(ngModel)]="masterPassword" required appAutofocus appInputVerbatim>
</ng-container>
<app-callout type="warning" *ngIf="clientSecret">{{'apiKeyWarning' | i18n}}</app-callout>
<app-callout type="info" title="{{'oauth2ClientCredentials' | i18n}}" icon="fa-key"
*ngIf="clientSecret">
<p class="mb-1">
<strong>client_id:</strong><br>
<code>{{clientId}}</code>
</p>
<p class="mb-1">
<strong>client_secret:</strong><br>
<code>{{clientSecret}}</code>
</p>
<p class="mb-1">
<strong>scope:</strong><br>
<code>{{scope}}</code>
</p>
<p class="mb-0">
<strong>grant_type:</strong><br>
<code>client_credentials</code>
</p>
</app-callout>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"
*ngIf="!clientSecret">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'rotateApiKey' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,50 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PasswordVerificationRequest } from 'jslib/models/request/passwordVerificationRequest';
import { ApiKeyResponse } from 'jslib/models/response/apiKeyResponse';
@Component({
selector: 'app-rotate-api-key',
templateUrl: 'rotate-api-key.component.html',
})
export class RotateApiKeyComponent {
organizationId: string;
masterPassword: string;
formPromise: Promise<ApiKeyResponse>;
clientId: string;
clientSecret: string;
scope: string;
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private cryptoService: CryptoService, private router: Router) { }
async submit() {
if (this.masterPassword == null || this.masterPassword === '') {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPassRequired'));
return;
}
const request = new PasswordVerificationRequest();
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
try {
this.formPromise = this.apiService.postOrganizationRotateApiKey(this.organizationId, request);
const response = await this.formPromise;
this.clientSecret = response.apiKey;
this.clientId = 'organization.' + this.organizationId;
this.scope = 'api.organization';
this.analytics.eventTrack.next({ action: 'Rotated Organization API Key' });
} catch { }
}
}

View File

@@ -7,8 +7,11 @@
<a routerLink="account" class="list-group-item" routerLinkActive="active">
{{'myOrganization' | i18n}}
</a>
<a routerLink="billing" class="list-group-item" routerLinkActive="active">
{{'billingAndLicensing' | i18n}}
<a routerLink="subscription" class="list-group-item" routerLinkActive="active">
{{'subscription' | i18n}}
</a>
<a routerLink="billing" class="list-group-item" routerLinkActive="active" *ngIf="!selfHosted">
{{'billing' | i18n}}
</a>
<a routerLink="two-factor" class="list-group-item" routerLinkActive="active" *ngIf="access2fa">
{{'twoStepLogin' | i18n}}

View File

@@ -1,6 +1,7 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { UserService } from 'jslib/abstractions/user.service';
@Component({
@@ -9,11 +10,14 @@ import { UserService } from 'jslib/abstractions/user.service';
})
export class SettingsComponent {
access2fa = false;
selfHosted: boolean;
constructor(private route: ActivatedRoute, private userService: UserService) { }
constructor(private route: ActivatedRoute, private userService: UserService,
private platformUtilsService: PlatformUtilsService) { }
ngOnInit() {
this.route.parent.params.subscribe(async (params) => {
this.selfHosted = await this.platformUtilsService.isSelfHost();
const organization = await this.userService.getOrganization(params.organizationId);
this.access2fa = organization.use2fa;
});

View File

@@ -6,6 +6,7 @@ import { ActivatedRoute } from '@angular/router';
import { ApiService } from 'jslib/abstractions/api.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PolicyService } from 'jslib/abstractions/policy.service';
import { UserService } from 'jslib/abstractions/user.service';
import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType';
@@ -20,8 +21,8 @@ import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from '../../se
export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent {
constructor(apiService: ApiService, userService: UserService,
componentFactoryResolver: ComponentFactoryResolver, messagingService: MessagingService,
private route: ActivatedRoute) {
super(apiService, userService, componentFactoryResolver, messagingService);
policyService: PolicyService, private route: ActivatedRoute) {
super(apiService, userService, componentFactoryResolver, messagingService, policyService);
}
async ngOnInit() {

View File

@@ -2,12 +2,15 @@ import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { EventService } from 'jslib/abstractions/event.service';
import { ExportService } from 'jslib/abstractions/export.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { ExportComponent as BaseExportComponent } from '../../tools/export.component';
import { EventType } from 'jslib/enums/eventType';
@Component({
selector: 'app-org-export',
templateUrl: '../../tools/export.component.html',
@@ -17,8 +20,8 @@ export class ExportComponent extends BaseExportComponent {
constructor(cryptoService: CryptoService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, exportService: ExportService,
private route: ActivatedRoute) {
super(cryptoService, i18nService, platformUtilsService, exportService);
eventService: EventService, private route: ActivatedRoute) {
super(cryptoService, i18nService, platformUtilsService, exportService, eventService);
}
ngOnInit() {
@@ -28,10 +31,15 @@ export class ExportComponent extends BaseExportComponent {
}
getExportData() {
return this.exportService.getOrganizationExport(this.organizationId, 'csv');
return this.exportService.getOrganizationExport(this.organizationId, this.format);
}
getFileName() {
return super.getFileName('org');
}
async collectEvent(): Promise<any> {
// TODO
// await this.eventService.collect(EventType.Organization_ClientExportedVault);
}
}

View File

@@ -0,0 +1,39 @@
import {
Component,
ComponentFactoryResolver,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { AuditService } from 'jslib/abstractions/audit.service';
import { CipherService } from 'jslib/abstractions/cipher.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { UserService } from 'jslib/abstractions/user.service';
import {
ExposedPasswordsReportComponent as BaseExposedPasswordsReportComponent,
} from '../../tools/exposed-passwords-report.component';
import { CipherView } from 'jslib/models/view/cipherView';
@Component({
selector: 'app-exposed-passwords-report',
templateUrl: '../../tools/exposed-passwords-report.component.html',
})
export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportComponent {
constructor(cipherService: CipherService, auditService: AuditService,
componentFactoryResolver: ComponentFactoryResolver, messagingService: MessagingService,
userService: UserService, private route: ActivatedRoute) {
super(cipherService, auditService, componentFactoryResolver, messagingService, userService);
}
ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.organization = await this.userService.getOrganization(params.organizationId);
super.ngOnInit();
});
}
getAllCiphers(): Promise<CipherView[]> {
return this.cipherService.getAllFromApiForOrganization(this.organization.id);
}
}

View File

@@ -0,0 +1,38 @@
import {
Component,
ComponentFactoryResolver,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { CipherService } from 'jslib/abstractions/cipher.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { UserService } from 'jslib/abstractions/user.service';
import {
InactiveTwoFactorReportComponent as BaseInactiveTwoFactorReportComponent,
} from '../../tools/inactive-two-factor-report.component';
import { CipherView } from 'jslib/models/view/cipherView';
@Component({
selector: 'app-inactive-two-factor-report',
templateUrl: '../../tools/inactive-two-factor-report.component.html',
})
export class InactiveTwoFactorReportComponent extends BaseInactiveTwoFactorReportComponent {
constructor(cipherService: CipherService, componentFactoryResolver: ComponentFactoryResolver,
messagingService: MessagingService, userService: UserService,
private route: ActivatedRoute) {
super(cipherService, componentFactoryResolver, messagingService, userService);
}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.organization = await this.userService.getOrganization(params.organizationId);
await super.ngOnInit();
});
}
getAllCiphers(): Promise<CipherView[]> {
return this.cipherService.getAllFromApiForOrganization(this.organization.id);
}
}

View File

@@ -0,0 +1,38 @@
import {
Component,
ComponentFactoryResolver,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { CipherService } from 'jslib/abstractions/cipher.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { UserService } from 'jslib/abstractions/user.service';
import { CipherView } from 'jslib/models/view/cipherView';
import {
ReusedPasswordsReportComponent as BaseReusedPasswordsReportComponent,
} from '../../tools/reused-passwords-report.component';
@Component({
selector: 'app-reused-passwords-report',
templateUrl: '../../tools/reused-passwords-report.component.html',
})
export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportComponent {
constructor(cipherService: CipherService, componentFactoryResolver: ComponentFactoryResolver,
messagingService: MessagingService, userService: UserService,
private route: ActivatedRoute) {
super(cipherService, componentFactoryResolver, messagingService, userService);
}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.organization = await this.userService.getOrganization(params.organizationId);
await super.ngOnInit();
});
}
getAllCiphers(): Promise<CipherView[]> {
return this.cipherService.getAllFromApiForOrganization(this.organization.id);
}
}

View File

@@ -1,7 +1,7 @@
<div class="container page-content">
<div class="row">
<div class="col-3">
<div class="card">
<div class="card mb-4">
<div class="card-header">{{'tools' | i18n}}</div>
<div class="list-group list-group-flush">
<a routerLink="import" class="list-group-item" routerLinkActive="active">
@@ -12,6 +12,34 @@
</a>
</div>
</div>
<div class="card">
<div class="card-header d-flex">
{{'reports' | i18n}}
<div class="ml-auto">
<a href="#" appStopClick class="badge badge-primary" *ngIf="!accessReports"
(click)="upgradeOrganization()">
{{'upgrade' | i18n}}
</a>
</div>
</div>
<div class="list-group list-group-flush">
<a routerLink="exposed-passwords-report" class="list-group-item" routerLinkActive="active">
{{'exposedPasswordsReport' | i18n}}
</a>
<a routerLink="reused-passwords-report" class="list-group-item" routerLinkActive="active">
{{'reusedPasswordsReport' | i18n}}
</a>
<a routerLink="weak-passwords-report" class="list-group-item" routerLinkActive="active">
{{'weakPasswordsReport' | i18n}}
</a>
<a routerLink="unsecured-websites-report" class="list-group-item" routerLinkActive="active">
{{'unsecuredWebsitesReport' | i18n}}
</a>
<a routerLink="inactive-two-factor-report" class="list-group-item" routerLinkActive="active">
{{'inactive2faReport' | i18n}}
</a>
</div>
</div>
</div>
<div class="col-9">
<router-outlet></router-outlet>

View File

@@ -1,7 +1,32 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Organization } from 'jslib/models/domain/organization';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { UserService } from 'jslib/abstractions/user.service';
@Component({
selector: 'app-org-tools',
templateUrl: 'tools.component.html',
})
export class ToolsComponent { }
export class ToolsComponent {
organization: Organization;
accessReports = false;
constructor(private route: ActivatedRoute, private userService: UserService,
private messagingService: MessagingService) { }
ngOnInit() {
this.route.parent.params.subscribe(async (params) => {
this.organization = await this.userService.getOrganization(params.organizationId);
// TODO: Maybe we want to just make sure they are not on a free plan? Just compare useTotp for now
// since all paid plans include useTotp
this.accessReports = this.organization.useTotp;
});
}
upgradeOrganization() {
this.messagingService.send('upgradeOrganization', { organizationId: this.organization.id });
}
}

View File

@@ -0,0 +1,38 @@
import {
Component,
ComponentFactoryResolver,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { CipherService } from 'jslib/abstractions/cipher.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { UserService } from 'jslib/abstractions/user.service';
import {
UnsecuredWebsitesReportComponent as BaseUnsecuredWebsitesReportComponent,
} from '../../tools/unsecured-websites-report.component';
import { CipherView } from 'jslib/models/view/cipherView';
@Component({
selector: 'app-unsecured-websites-report',
templateUrl: '../../tools/unsecured-websites-report.component.html',
})
export class UnsecuredWebsitesReportComponent extends BaseUnsecuredWebsitesReportComponent {
constructor(cipherService: CipherService, componentFactoryResolver: ComponentFactoryResolver,
messagingService: MessagingService, userService: UserService,
private route: ActivatedRoute) {
super(cipherService, componentFactoryResolver, messagingService, userService);
}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.organization = await this.userService.getOrganization(params.organizationId);
await super.ngOnInit();
});
}
getAllCiphers(): Promise<CipherView[]> {
return this.cipherService.getAllFromApiForOrganization(this.organization.id);
}
}

View File

@@ -0,0 +1,39 @@
import {
Component,
ComponentFactoryResolver,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { CipherService } from 'jslib/abstractions/cipher.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { UserService } from 'jslib/abstractions/user.service';
import { CipherView } from 'jslib/models/view/cipherView';
import {
WeakPasswordsReportComponent as BaseWeakPasswordsReportComponent,
} from '../../tools/weak-passwords-report.component';
@Component({
selector: 'app-weak-passwords-report',
templateUrl: '../../tools/weak-passwords-report.component.html',
})
export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportComponent {
constructor(cipherService: CipherService, passwordGenerationService: PasswordGenerationService,
componentFactoryResolver: ComponentFactoryResolver, messagingService: MessagingService,
userService: UserService, private route: ActivatedRoute) {
super(cipherService, passwordGenerationService, componentFactoryResolver, messagingService, userService);
}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.organization = await this.userService.getOrganization(params.organizationId);
await super.ngOnInit();
});
}
getAllCiphers(): Promise<CipherView[]> {
return this.cipherService.getAllFromApiForOrganization(this.organization.id);
}
}

View File

@@ -1,11 +1,10 @@
import {
Component,
OnInit,
} from '@angular/core';
import { Component } from '@angular/core';
import { ApiService } from 'jslib/abstractions/api.service';
import { AuditService } from 'jslib/abstractions/audit.service';
import { CipherService } from 'jslib/abstractions/cipher.service';
import { CollectionService } from 'jslib/abstractions/collection.service';
import { EventService } from 'jslib/abstractions/event.service';
import { FolderService } from 'jslib/abstractions/folder.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
@@ -18,6 +17,7 @@ import { UserService } from 'jslib/abstractions/user.service';
import { CipherData } from 'jslib/models/data/cipherData';
import { Cipher } from 'jslib/models/domain/cipher';
import { Organization } from 'jslib/models/domain/organization';
import { CipherCreateRequest } from 'jslib/models/request/cipherCreateRequest';
import { CipherRequest } from 'jslib/models/request/cipherRequest';
import { AddEditComponent as BaseAddEditComponent } from '../../vault/add-edit.component';
@@ -26,18 +26,38 @@ import { AddEditComponent as BaseAddEditComponent } from '../../vault/add-edit.c
selector: 'app-org-vault-add-edit',
templateUrl: '../../vault/add-edit.component.html',
})
export class AddEditComponent extends BaseAddEditComponent implements OnInit {
export class AddEditComponent extends BaseAddEditComponent {
organization: Organization;
originalCipher: Cipher = null;
constructor(cipherService: CipherService, folderService: FolderService,
i18nService: I18nService, platformUtilsService: PlatformUtilsService,
auditService: AuditService, stateService: StateService,
userService: UserService, totpService: TotpService,
passwordGenerationService: PasswordGenerationService, private apiService: ApiService,
messagingService: MessagingService) {
userService: UserService, collectionService: CollectionService,
totpService: TotpService, passwordGenerationService: PasswordGenerationService,
private apiService: ApiService, messagingService: MessagingService,
eventService: EventService) {
super(cipherService, folderService, i18nService, platformUtilsService, auditService, stateService,
userService, totpService, passwordGenerationService, messagingService);
userService, collectionService, totpService, passwordGenerationService, messagingService,
eventService);
}
protected allowOwnershipAssignment() {
if (this.ownershipOptions != null && this.ownershipOptions.length > 1) {
if (this.organization != null) {
return this.cloneMode && this.organization.isAdmin;
} else {
return !this.editMode || this.cloneMode;
}
}
return false;
}
protected loadCollections() {
if (!this.organization.isAdmin) {
return super.loadCollections();
}
return Promise.resolve(this.collections);
}
protected async loadCipher() {
@@ -51,9 +71,6 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit {
}
protected encryptCipher() {
if (!this.editMode) {
this.cipher.organizationId = this.organization.id;
}
if (!this.organization.isAdmin) {
return super.encryptCipher();
}
@@ -61,13 +78,14 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit {
}
protected async saveCipher(cipher: Cipher) {
if (!this.organization.isAdmin) {
if (!this.organization.isAdmin || cipher.organizationId == null) {
return super.saveCipher(cipher);
}
const request = new CipherRequest(cipher);
if (this.editMode) {
if (this.editMode && !this.cloneMode) {
const request = new CipherRequest(cipher);
return this.apiService.putCipherAdmin(this.cipherId, request);
} else {
const request = new CipherCreateRequest(cipher);
return this.apiService.postCipherAdmin(request);
}
}
@@ -76,6 +94,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit {
if (!this.organization.isAdmin) {
return super.deleteCipher();
}
return this.apiService.deleteCipherAdmin(this.cipherId);
return this.cipher.isDeleted ? this.apiService.deleteCipherAdmin(this.cipherId)
: this.apiService.putDeleteCipherAdmin(this.cipherId);
}
}

View File

@@ -11,6 +11,8 @@ import { CipherData } from 'jslib/models/data/cipherData';
import { Cipher } from 'jslib/models/domain/cipher';
import { Organization } from 'jslib/models/domain/organization';
import { AttachmentView } from 'jslib/models/view/attachmentView';
import { AttachmentsComponent as BaseAttachmentsComponent } from '../../vault/attachments.component';
@Component({
@@ -26,6 +28,12 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
super(cipherService, i18nService, cryptoService, userService, platformUtilsService);
}
protected async reupload(attachment: AttachmentView) {
if (this.organization.isAdmin && this.showFixOldAttachments(attachment)) {
await super.reuploadCipherAttachment(attachment, true);
}
}
protected async loadCipher() {
if (!this.organization.isAdmin) {
return await super.loadCipher();
@@ -44,4 +52,8 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
}
return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId);
}
protected showFixOldAttachments(attachment: AttachmentView) {
return attachment.key == null && this.organization.isAdmin;
}
}

View File

@@ -9,12 +9,11 @@ import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CipherService } from 'jslib/abstractions/cipher.service';
import { EventService } from 'jslib/abstractions/event.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SearchService } from 'jslib/abstractions/search.service';
import { CipherData } from 'jslib/models/data/cipherData';
import { Cipher } from 'jslib/models/domain/cipher';
import { Organization } from 'jslib/models/domain/organization';
import { CipherView } from 'jslib/models/view/cipherView';
@@ -35,31 +34,18 @@ export class CiphersComponent extends BaseCiphersComponent {
constructor(searchService: SearchService, analytics: Angulartics2,
toasterService: ToasterService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, cipherService: CipherService,
private apiService: ApiService) {
super(searchService, analytics, toasterService, i18nService, platformUtilsService, cipherService);
private apiService: ApiService, eventService: EventService) {
super(searchService, analytics, toasterService, i18nService, platformUtilsService,
cipherService, eventService);
}
async load(filter: (cipher: CipherView) => boolean = null) {
if (!this.organization.isAdmin) {
await super.load();
await super.load(filter, this.deleted);
return;
}
this.accessEvents = this.organization.useEvents;
const ciphers = await this.apiService.getCiphersOrganization(this.organization.id);
if (ciphers != null && ciphers.data != null && ciphers.data.length) {
const decCiphers: CipherView[] = [];
const promises: any[] = [];
ciphers.data.forEach((r) => {
const data = new CipherData(r);
const cipher = new Cipher(data);
promises.push(cipher.decrypt().then((c) => decCiphers.push(c)));
});
await Promise.all(promises);
decCiphers.sort(this.cipherService.getLocaleSortingFunction());
this.allCiphers = decCiphers;
} else {
this.allCiphers = [];
}
this.allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organization.id);
this.applyFilter(filter);
this.loaded = true;
}
@@ -73,24 +59,27 @@ export class CiphersComponent extends BaseCiphersComponent {
}
}
search(timeout: number = null) {
async search(timeout: number = null) {
if (!this.organization.isAdmin) {
return super.search(timeout);
}
this.searchPending = false;
let filteredCiphers = this.allCiphers;
if (this.filter != null) {
filteredCiphers = filteredCiphers.filter(this.filter);
}
if (this.searchText == null || this.searchText.trim().length < 2) {
this.ciphers = filteredCiphers;
} else {
this.ciphers = this.searchService.searchCiphersBasic(filteredCiphers, this.searchText);
}
}
checkCipher(c: CipherView) {
// do nothing
if (this.searchText == null || this.searchText.trim().length < 2) {
this.ciphers = filteredCiphers.filter((c) => {
if (c.isDeleted !== this.deleted) {
return false;
}
return this.filter == null || this.filter(c);
});
} else {
if (this.filter != null) {
filteredCiphers = filteredCiphers.filter(this.filter);
}
this.ciphers = this.searchService.searchCiphersBasic(filteredCiphers, this.searchText, this.deleted);
}
await this.resetPaging();
}
events(c: CipherView) {
@@ -99,8 +88,12 @@ export class CiphersComponent extends BaseCiphersComponent {
protected deleteCipher(id: string) {
if (!this.organization.isAdmin) {
return super.deleteCipher(id);
return super.deleteCipher(id, this.deleted);
}
return this.apiService.deleteCipherAdmin(id);
return this.deleted ? this.apiService.deleteCipherAdmin(id) : this.apiService.putDeleteCipherAdmin(id);
}
protected showFixOldAttachments(c: CipherView) {
return this.organization.isAdmin && c.hasOldAttachments;
}
}

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