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

Compare commits

...

164 Commits

Author SHA1 Message Date
Kyle Spearrin
ef61652fba bump version 2020-11-12 22:49:30 -05:00
Kyle Spearrin
933a66b24c disable send 2020-11-12 22:05:50 -05:00
Kyle Spearrin
e2c6a5f8cd New Crowdin updates (#704)
* New translations messages.json (French)

* New translations messages.json (Slovak)

* New translations messages.json (Malayalam)

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

* New translations messages.json (Indonesian)

* 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 (Serbian (Cyrillic))

* New translations messages.json (Russian)

* New translations messages.json (Spanish)

* New translations messages.json (Polish)

* New translations messages.json (Dutch)

* New translations messages.json (Hungarian)

* New translations messages.json (Finnish)

* 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)
2020-11-12 21:44:42 -05:00
Kyle Spearrin
a818e7dd40 New Crowdin updates (#697)
* New translations messages.json (Romanian)

* New translations messages.json (Indonesian)

* 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 (Portuguese, Brazilian)

* New translations messages.json (Croatian)

* New translations messages.json (Slovak)

* 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 (Norwegian Bokmal)

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

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

* New translations messages.json (Russian)

* New translations messages.json (French)

* New translations messages.json (German)

* New translations messages.json (Spanish)

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

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

* New translations messages.json (English, India)
2020-11-10 17:19:12 -05:00
Addison Beck
759dc647e5 Implement User-based API Keys (#688)
* refactored api key modal for multiple key types

* Added support for viewing and rotating user API keys

* Fixed the API key component references in app.module

* Implemented User ApiKey viewing/rotating

* Changed ApiKey grant_type display to client_credentials

* Hopefully put jslib back

* Added new localization strings for user API keys

* Toggled button text based on if viewing or rotating an api key

* updated jslib

* Reverted jslib

* Trying to fix jslib

* Reverted jslib from commit hash

* Reupdated jslib
2020-11-10 16:13:42 -05:00
Matthew
37cf46d581 Add 'Copy Username' button (#691)
This adds a 'Copy Username' button above the 'Copy Password' button in
the dropdown for individual entries in the safe. This matches the
capabilities of the desktop app, where you can right-click on any entry
and get options for both 'copy password' and 'copy username'.
2020-11-10 14:26:38 -05:00
Vincent Salucci
407032114e [Exemption] Updated policy messages (#692)
* Updated mesages // added callout for require sso

* removed unused string

* updated strings - futureproofing
2020-11-10 09:53:57 -06:00
eliykat
94aece134c Docs contrib (#696)
* expand contributing guide

* fix typo
2020-11-10 10:52:09 -05:00
Christian Oliff
7532bf9825 HTTPS link to EditorConfig.org (#694) 2020-11-09 15:30:33 -05:00
Kyle Spearrin
0f4f541b11 some filtering logic for sends (#689) 2020-11-05 14:41:54 -05:00
Kyle Spearrin
07a3d38bef fix compile errors 2020-11-04 16:30:19 -05:00
Kyle Spearrin
e9273ff79a Send initial implementation (#687)
* send work

* Bump version to 2.16.2 (#668)

* [SSO] New User Provision flow jslib update (f30d6f8 -> d84d6da) (#672)

* Update jslib (f30d6f8 -> d84d6da)

* Updated imports/constructor to super

* OnlyOrg Policy (#669)

* added localization strings needed for the OnlyOrg policy

* added deprecation warning to policies page

* allowed OnlyOrg policy configuration

* blocked creating new orgs if already in an org with OnlyOrg enabled

* code review cleanup for onlyOrg

* removed a blank line

* code review cleanup for onlyOrg

* send listing actions

* updates

* access id

* update jslib

* re-work key and password derivation

* update jslib

* makeSendKey

* update access path

* store max access count

* update jslib

* l10n work

* l10n for access page

* l10n and cleanup

* fix l10n

Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>
Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
Co-authored-by: Addison Beck <abeck@bitwarden.com>
2020-11-04 14:49:08 -05:00
Vincent Salucci
1aa708aed4 [GDPR] Adjusted TOS/Privacy acceptance (#684)
* initial commit for GDPR terms/privacy acceptance

* updated styling/formatting

* Fixed line break in blockquote

* removed unused submit message

* Removed variables/logic now found in superclass

* update jslib (76c0964 -> 5e50aa1)
2020-11-02 16:33:15 -06:00
Addison Beck
ebe5a6030e Only org to single org (#680)
* change OnlyOrg references to SingleOrg

* updated jslib

* change OnlyOrg references to SingleOrg

* missed a reference to OnlyOrg in messages
2020-10-27 10:28:57 -04:00
Chad Scharf
f6946085d8 Use properly transpiled SweetAlert2 lib (#682) 2020-10-26 17:52:01 -04:00
Vincent Salucci
beebe7c98b [Require SSO] Enterprise policy adjustment (#676)
* Commits for policies/edit/strings

* more initial commits of policy/edit/strings

* More changes for require sso

* Updated strings to match policy string patterns

* Updated false enable on error

* Removed sso prevalidate prereq // adjusted callout

* Updated policy array creation and added display value
2020-10-26 11:56:02 -05:00
Addison Beck
a51331d6b2 OnlyOrg Policy (#669)
* added localization strings needed for the OnlyOrg policy

* added deprecation warning to policies page

* allowed OnlyOrg policy configuration

* blocked creating new orgs if already in an org with OnlyOrg enabled

* code review cleanup for onlyOrg

* removed a blank line

* code review cleanup for onlyOrg
2020-10-16 15:36:06 -04:00
Vincent Salucci
b7b970e654 [SSO] New User Provision flow jslib update (f30d6f8 -> d84d6da) (#672)
* Update jslib (f30d6f8 -> d84d6da)

* Updated imports/constructor to super
2020-10-14 11:13:13 -05:00
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
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
174 changed files with 40820 additions and 5633 deletions

View File

@@ -1,4 +1,4 @@
# EditorConfig is awesome: http://EditorConfig.org
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true

View File

@@ -1,4 +1,32 @@
Code contributions are welcome! Please commit any pull requests against the `master` branch.
# How to Contribute
Contributions of all kinds are welcome!
Please visit our [Community Forums](https://community.bitwarden.com/) for general community discussion and the development roadmap.
Here is how you can get involved:
* **Request a new feature:** Go to the [Feature Requests category](https://community.bitwarden.com/c/feature-requests/) of the Community Forums. Please search existing feature requests before making a new one
* **Write code for a new feature:** Make a new post in the [Github Contributions category](https://community.bitwarden.com/c/github-contributions/) of the Community Forums. Include a description of your proposed contribution, screeshots, and links to any relevant feature requests. This helps get feedback from the community and Bitwarden team members before you start writing code
* **Report a bug or submit a bugfix:** Use Github issues and pull requests
* **Write documentation:** Submit a pull request to the [Bitwarden help repository](https://github.com/bitwarden/help)
* **Help other users:** Go to the [User-to-User Support category](https://community.bitwarden.com/c/support/) on the Community Forums
* **Translate:** See the localization (l10n) section below
## Contributor Agreement
Please sign the [Contributor Agreement](https://cla-assistant.io/bitwarden/web) if you intend on contributing to any Github repository. Pull requests cannot be accepted and merged unless the author has signed the Contributor Agreement.
## Pull Request Guidelines
* use `npm run lint` and fix any linting suggestions before submitting a pull request
* commit any pull requests against the `master` branch
* include a link to your Community Forums post
# Localization (l10n)

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

View File

@@ -9,3 +9,4 @@ files:
zh-CN: zh_CN
zh-TW: zh_TW
en-GB: en_GB
en-IN: en_IN

2
jslib

Submodule jslib updated: 0a30c7eb1e...79b856cb6e

2730
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,8 @@
{
"name": "bitwarden-web",
"version": "2.13.1",
"version": "2.17.0",
"license": "GPL-3.0",
"repository": "https://github.com/bitwarden/web",
"scripts": {
"sub:init": "git submodule update --init --recursive",
"sub:update": "git submodule update --remote",
@@ -26,10 +28,11 @@
"lint:fix": "tslint src/**/*.ts --fix"
},
"devDependencies": {
"@angular/compiler-cli": "^7.2.11",
"@ngtools/webpack": "^7.2.2",
"@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",
@@ -41,45 +44,44 @@
"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": "^4.0.0",
"gulp-google-webfonts": "^2.0.0",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.9.0",
"node-sass": "^4.13.1",
"sass-loader": "^7.1.0",
"style-loader": "^0.23.0",
"terser-webpack-plugin": "^1.2.3",
"ts-loader": "^5.3.3",
"tslint": "^5.12.1",
"ts-loader": "^7.0.5",
"tslint": "^6.1.3",
"tslint-loader": "^3.5.4",
"typescript": "3.2.4",
"typescript": "3.8.3",
"webpack": "^4.29.0",
"webpack-cli": "^3.2.1",
"webpack-dev-server": "^3.1.14"
},
"dependencies": {
"@angular/animations": "7.2.1",
"@angular/cdk": "7.2.1",
"@angular/common": "7.2.1",
"@angular/compiler": "7.2.1",
"@angular/core": "7.2.1",
"@angular/forms": "7.2.1",
"@angular/platform-browser": "7.2.1",
"@angular/platform-browser-dynamic": "7.2.1",
"@angular/router": "7.2.1",
"@angular/upgrade": "7.2.1",
"@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": "6.1.0",
"angulartics2": "6.3.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.6.2",
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git",
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git#410a9186cc34663c4913b17d6528067cd3331f1d",
"font-awesome": "4.7.0",
"jquery": "3.4.1",
"lunr": "2.3.3",
@@ -88,12 +90,13 @@
"papaparse": "4.6.0",
"popper.js": "1.14.4",
"qrious": "4.0.2",
"rxjs": "6.3.3",
"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.28",
"zone.js": "0.10.3",
"zxcvbn": "4.4.2"
}
}

View File

@@ -1,4 +1,4 @@
<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">
@@ -25,9 +25,11 @@
</div>
<hr>
<div class="d-flex">
<button type="submit" class="btn btn-primary btn-block">
<i class="fa fa-unlock-alt" aria-hidden="true"></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,15 +1,16 @@
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 { LockService } from 'jslib/abstractions/lock.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';
@@ -23,11 +24,11 @@ export class LockComponent extends BaseLockComponent {
constructor(router: Router, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, messagingService: MessagingService,
userService: UserService, cryptoService: CryptoService,
storageService: StorageService, lockService: LockService,
storageService: StorageService, vaultTimeoutService: VaultTimeoutService,
environmentService: EnvironmentService, private routerService: RouterService,
stateService: StateService) {
stateService: StateService, apiService: ApiService) {
super(router, i18nService, platformUtilsService, messagingService, userService, cryptoService,
storageService, lockService, environmentService, stateService);
storageService, vaultTimeoutService, environmentService, stateService, apiService);
}
async ngOnInit() {

View File

@@ -44,6 +44,11 @@
<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>
</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';
@@ -20,8 +23,13 @@ export class LoginComponent extends BaseLoginComponent {
constructor(authService: AuthService, router: Router,
i18nService: I18nService, private route: ActivatedRoute,
storageService: StorageService, stateService: StateService,
platformUtilsService: PlatformUtilsService) {
super(authService, router, platformUtilsService, i18nService, storageService, stateService);
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
passwordGenerationService: PasswordGenerationService, cryptoFunctionService: CryptoFunctionService) {
super(authService, router,
platformUtilsService, i18nService,
stateService, environmentService,
passwordGenerationService, cryptoFunctionService,
storageService);
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
}

View File

@@ -1,103 +1,156 @@
<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">
<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 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>
<div class="form-group" *ngIf="showTerms">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="acceptPolicies"
[(ngModel)]="acceptPolicies" name="AcceptPolicies">
<label class="form-check-label small text-muted" for="acceptPolicies">
{{'acceptPolicies' | i18n}}<br>
<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>
</label>
</div>
</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>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>

View File

@@ -19,6 +19,7 @@ import { MasterPasswordPolicyOptions } from 'jslib/models/domain/masterPasswordP
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',
@@ -26,7 +27,7 @@ import { PolicyData } from 'jslib/models/data/policyData';
})
export class RegisterComponent extends BaseRegisterComponent {
showCreateOrgMessage = false;
showTerms = true;
layout = '';
enforcedPolicyOptions: MasterPasswordPolicyOptions;
private policies: Policy[];
@@ -38,7 +39,6 @@ export class RegisterComponent extends BaseRegisterComponent {
passwordGenerationService: PasswordGenerationService, private policyService: PolicyService) {
super(authService, router, i18nService, cryptoService, apiService, stateService, platformUtilsService,
passwordGenerationService);
this.showTerms = !platformUtilsService.isSelfHost();
}
getPasswordScoreAlertDisplay() {
@@ -63,6 +63,7 @@ export class RegisterComponent extends BaseRegisterComponent {
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;
}
@@ -70,9 +71,21 @@ 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();
}

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,34 @@
import { Component } from '@angular/core';
import {
ActivatedRoute,
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, route: ActivatedRoute) {
super(i18nService, cryptoService, messagingService, userService, passwordGenerationService,
platformUtilsService, policyService, router, apiService, syncService, route);
}
}

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

@@ -5,7 +5,10 @@ import {
ViewContainerRef,
} from '@angular/core';
import { Router } from '@angular/router';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { TwoFactorOptionsComponent } from './two-factor-options.component';
@@ -28,15 +31,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, stateService: StateService,
environmentService: EnvironmentService, private componentFactoryResolver: ComponentFactoryResolver,
storageService: StorageService) {
storageService: StorageService, route: ActivatedRoute) {
super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService,
stateService, storageService);
stateService, storageService, route);
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
}

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';
@@ -55,6 +57,9 @@ import {
import { VaultComponent as OrgVaultComponent } from './organizations/vault/vault.component';
import { AccessComponent } from './send/access.component';
import { SendComponent } from './send/send.component';
import { AccountComponent } from './settings/account.component';
import { CreateOrganizationComponent } from './settings/create-organization.component';
import { DomainRulesComponent } from './settings/domain-rules.component';
@@ -99,6 +104,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],
@@ -130,6 +144,11 @@ const routes: Routes = [
canActivate: [UnauthGuardService],
data: { titleId: 'deleteAccount' },
},
{
path: 'send/:sendId/:key',
component: AccessComponent,
data: { title: 'Bitwarden Send' },
},
],
},
{
@@ -138,6 +157,7 @@ const routes: Routes = [
canActivate: [AuthGuardService],
children: [
{ path: 'vault', component: VaultComponent, data: { titleId: 'myVault' } },
// { path: 'sends', component: SendComponent, data: { title: 'Send' } },
{
path: 'settings',
component: SettingsComponent,

View File

@@ -1,5 +1,5 @@
import * as jq from 'jquery';
import Swal from 'sweetalert2/src/sweetalert2.js';
import Swal from 'sweetalert2/dist/sweetalert2.js';
import {
BodyOutputType,
@@ -35,7 +35,6 @@ 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';
@@ -46,6 +45,7 @@ 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';
@@ -78,7 +78,7 @@ 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 routerService: RouterService,
@@ -110,7 +110,7 @@ export class AppComponent implements OnDestroy, OnInit {
this.logOut(!!message.expired);
break;
case 'lockVault':
await this.lockService.lock();
await this.vaultTimeoutService.lock();
break;
case 'locked':
this.notificationsService.updateConnection(false);
@@ -148,6 +148,9 @@ export class AppComponent implements OnDestroy, OnInit {
properties: { label: message.label },
});
break;
case 'setFullWidth':
this.setFullWidth();
break;
default:
break;
}
@@ -166,6 +169,8 @@ export class AppComponent implements OnDestroy, OnInit {
}
}
});
this.setFullWidth();
}
ngOnDestroy() {
@@ -198,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(['/']);
});
}
@@ -262,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

@@ -34,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';
@@ -58,13 +60,11 @@ import { UserGroupsComponent as OrgUserGroupsComponent } from './organizations/m
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,
@@ -96,10 +96,15 @@ import { CollectionsComponent as OrgCollectionsComponent } from './organizations
import { GroupingsComponent as OrgGroupingsComponent } from './organizations/vault/groupings.component';
import { VaultComponent as OrgVaultComponent } from './organizations/vault/vault.component';
import { AccessComponent } from './send/access.component';
import { AddEditComponent as SendAddEditComponent } from './send/add-edit.component';
import { SendComponent } from './send/send.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 { ApiKeyComponent } from './settings/api-key.component';
import { ChangeEmailComponent } from './settings/change-email.component';
import { ChangeKdfComponent } from './settings/change-kdf.component';
import { ChangePasswordComponent } from './settings/change-password.component';
@@ -107,6 +112,7 @@ 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';
@@ -115,6 +121,7 @@ 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';
@@ -143,8 +150,10 @@ import { WeakPasswordsReportComponent } from './tools/weak-passwords-report.comp
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';
@@ -173,11 +182,15 @@ 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 {
registerLocaleData,
DatePipe,
} 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';
@@ -186,6 +199,8 @@ 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';
@@ -202,6 +217,7 @@ 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');
@@ -210,6 +226,8 @@ 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');
@@ -229,7 +247,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
FormsModule,
AppRoutingModule,
ServicesModule,
Angulartics2Module.forRoot([Angulartics2GoogleAnalytics], {
Angulartics2Module.forRoot({
pageTracking: {
clearQueryParams: true,
},
@@ -240,14 +258,17 @@ registerLocaleData(localeZhTw, 'zh-TW');
],
declarations: [
A11yTitleDirective,
AccessComponent,
AcceptOrganizationComponent,
AccountComponent,
SetPasswordComponent,
AddCreditComponent,
AddEditComponent,
AdjustPaymentComponent,
AdjustSeatsComponent,
AdjustStorageComponent,
ApiActionDirective,
ApiKeyComponent,
AppComponent,
AttachmentsComponent,
AutofocusDirective,
@@ -255,8 +276,10 @@ registerLocaleData(localeZhTw, 'zh-TW');
BlurClickDirective,
BoxRowDirective,
BreachReportComponent,
BulkActionsComponent,
BulkDeleteComponent,
BulkMoveComponent,
BulkRestoreComponent,
BulkShareComponent,
CalloutComponent,
ChangeEmailComponent,
@@ -285,6 +308,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
ImportComponent,
InactiveTwoFactorReportComponent,
InputVerbatimDirective,
LinkSsoComponent,
LockComponent,
LoginComponent,
ModalComponent,
@@ -292,7 +316,6 @@ registerLocaleData(localeZhTw, 'zh-TW');
OptionsComponent,
OrgAccountComponent,
OrgAddEditComponent,
OrgApiKeyComponent,
OrganizationBillingComponent,
OrganizationPlansComponent,
OrganizationSubscriptionComponent,
@@ -316,7 +339,6 @@ registerLocaleData(localeZhTw, 'zh-TW');
OrgPolicyEditComponent,
OrgPoliciesComponent,
OrgReusedPasswordsReportComponent,
OrgRotateApiKeyComponent,
OrgSettingComponent,
OrgToolsComponent,
OrgTwoFactorSetupComponent,
@@ -342,10 +364,14 @@ registerLocaleData(localeZhTw, 'zh-TW');
SearchCiphersPipe,
SearchPipe,
SelectCopyDirective,
SendAddEditComponent,
SendComponent,
SettingsComponent,
ShareComponent,
SsoComponent,
StopClickDirective,
StopPropDirective,
TaxInfoComponent,
ToolsComponent,
TrueFalseValueDirective,
TwoFactorAuthenticatorComponent,
@@ -372,9 +398,12 @@ registerLocaleData(localeZhTw, 'zh-TW');
],
entryComponents: [
AddEditComponent,
ApiKeyComponent,
AttachmentsComponent,
BulkActionsComponent,
BulkDeleteComponent,
BulkMoveComponent,
BulkRestoreComponent,
BulkShareComponent,
CollectionsComponent,
DeauthorizeSessionsComponent,
@@ -383,7 +412,6 @@ registerLocaleData(localeZhTw, 'zh-TW');
FolderAddEditComponent,
ModalComponent,
OrgAddEditComponent,
OrgApiKeyComponent,
OrgAttachmentsComponent,
OrgCollectionAddEditComponent,
OrgCollectionsComponent,
@@ -391,12 +419,12 @@ registerLocaleData(localeZhTw, 'zh-TW');
OrgEntityUsersComponent,
OrgGroupAddEditComponent,
OrgPolicyEditComponent,
OrgRotateApiKeyComponent,
OrgUserAddEditComponent,
OrgUserConfirmComponent,
OrgUserGroupsComponent,
PasswordGeneratorHistoryComponent,
PurgeVaultComponent,
SendAddEditComponent,
ShareComponent,
TwoFactorAuthenticatorComponent,
TwoFactorDuoComponent,
@@ -407,7 +435,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
TwoFactorYubiKeyComponent,
UpdateKeyComponent,
],
providers: [],
providers: [DatePipe],
bootstrap: [AppComponent],
})
export class AppModule { }

View File

@@ -8,6 +8,9 @@
<li class="nav-item" routerLinkActive="active">
<a class="nav-link" routerLink="/vault">{{'myVault' | i18n}}</a>
</li>
<!--<li class="nav-item" routerLinkActive="active">
<a class="nav-link" routerLink="/sends">Send</a>
</li>-->
<li class="nav-item" routerLinkActive="active">
<a class="nav-link" routerLink="/tools">{{'tools' | i18n}}</a>
</li>
@@ -39,7 +42,7 @@
<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">
<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>

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" aria-hidden="true"></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.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>
<router-outlet></router-outlet>

View File

@@ -4,10 +4,14 @@ import {
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';
@@ -20,19 +24,28 @@ const BroadcasterSubscriptionId = 'OrganizationLayoutComponent';
})
export class OrganizationLayoutComponent implements OnInit, OnDestroy {
organization: Organization;
enterpriseTokenPromise: Promise<any>;
private organizationId: string;
private enterpriseUrl: string;
constructor(private route: ActivatedRoute, private userService: UserService,
private broadcasterService: BroadcasterService, private ngZone: NgZone) { }
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) {
@@ -51,4 +64,20 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
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

@@ -14,7 +14,8 @@
<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>
@@ -41,6 +42,7 @@
<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>
@@ -58,6 +60,10 @@
<span class="sr-only">{{'groupAccessAllItems' | i18n}}</span>
</ng-container>
</td>
<td class="text-center">
<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">

View File

@@ -73,6 +73,7 @@ export class CollectionAddEditComponent implements OnInit {
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;
}
});
}
@@ -97,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;
}
}
@@ -113,7 +115,7 @@ export class CollectionAddEditComponent implements OnInit {
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

@@ -16,9 +16,11 @@
<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 && (collections | search:searchText:'name':'id') as searchedCollections">
<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>

View File

@@ -14,6 +14,7 @@ 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';
@@ -34,21 +35,26 @@ 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 userService: UserService) { }
private userService: UserService, private searchService: SearchService) { }
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
@@ -74,9 +80,27 @@ export class CollectionsComponent implements OnInit {
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();
@@ -147,10 +171,28 @@ export class CollectionsComponent implements OnInit {
});
}
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

@@ -47,6 +47,8 @@
<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>
@@ -85,6 +87,11 @@
<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">

View File

@@ -78,6 +78,7 @@ export class EntityUsersComponent implements OnInit {
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;
}
});
}
@@ -107,6 +108,7 @@ export class EntityUsersComponent implements OnInit {
} else {
if (this.entity === 'collection') {
(u as any).readOnly = false;
(u as any).hidePasswords = false;
}
this.selectedCount--;
}
@@ -123,7 +125,7 @@ export class EntityUsersComponent implements OnInit {
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));
.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;

View File

@@ -1,5 +1,5 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="groupAddEditTitle">
<div class="modal-dialog" role="document">
<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" id="groupAddEditTitle">{{title}}</h2>
@@ -24,6 +24,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,6 +63,7 @@
<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>
@@ -71,6 +76,10 @@
<td (click)="check(c)">
{{c.name}}
</td>
<td class="text-center">
<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">

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

@@ -16,9 +16,10 @@
<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 && (groups | search:searchText:'name':'id') as searchedGroups">
<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>

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) => {
@@ -70,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();
@@ -142,10 +165,28 @@ export class GroupsComponent implements OnInit {
});
}
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

@@ -35,13 +35,15 @@
<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 && (users | search:searchText:'name':'email':'id') as searchedUsers">
<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">

View File

@@ -19,6 +19,7 @@ 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';
@@ -42,14 +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('confirmTemplate', { read: ViewContainerRef }) confirmModalRef: 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[]>();
@@ -59,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[];
@@ -67,7 +73,7 @@ export class PeopleComponent implements OnInit {
private platformUtilsService: PlatformUtilsService, private analytics: Angulartics2,
private toasterService: ToasterService, private cryptoService: CryptoService,
private userService: UserService, private router: Router,
private storageService: StorageService) { }
private storageService: StorageService, private searchService: SearchService) { }
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
@@ -119,10 +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.length;
return this.allUsers != null ? this.allUsers.length : 0;
}
get invitedCount() {
@@ -294,6 +317,23 @@ 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);
@@ -313,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

@@ -1,3 +1,8 @@
<app-callout [type]="'warning'">
<p>{{'webPoliciesDeprecationWarning' | i18n}}</p>
<button type="button" class="btn btn-outline-secondary"
(click)="goToEnterprisePortal()">{{'businessPortal' | i18n}}</button>
</app-callout>
<div class="page-header d-flex">
<h1>{{'policies' | i18n}}</h1>
</div>
@@ -8,7 +13,7 @@
<table class="table table-hover table-list" *ngIf="!loading">
<tbody>
<tr *ngFor="let p of policies">
<td>
<td *ngIf="p.display">
<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>

View File

@@ -13,6 +13,7 @@ import {
import { PolicyType } from 'jslib/enums/policyType';
import { ApiService } from 'jslib/abstractions/api.service';
import { EnvironmentService } from 'jslib/abstractions';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { UserService } from 'jslib/abstractions/user.service';
@@ -28,41 +29,25 @@ import { PolicyEditComponent } from './policy-edit.component';
templateUrl: 'policies.component.html',
})
export class PoliciesComponent implements OnInit {
@ViewChild('editTemplate', { read: ViewContainerRef }) editModalRef: ViewContainerRef;
@ViewChild('editTemplate', { read: ViewContainerRef, static: true }) editModalRef: ViewContainerRef;
loading = true;
organizationId: string;
policies: any[];
// Remove when removing deprecation warning
enterpriseTokenPromise: Promise<any>;
private enterpriseUrl: string;
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,
},
];
}
private router: Router, private environmentService: EnvironmentService) { }
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
@@ -72,8 +57,53 @@ export class PoliciesComponent implements OnInit {
this.router.navigate(['/organizations', this.organizationId]);
return;
}
this.policies = [
{
name: this.i18nService.t('twoStepLogin'),
description: this.i18nService.t('twoStepLoginPolicyDesc'),
type: PolicyType.TwoFactorAuthentication,
enabled: false,
display: true,
},
{
name: this.i18nService.t('masterPass'),
description: this.i18nService.t('masterPassPolicyDesc'),
type: PolicyType.MasterPassword,
enabled: false,
display: true,
},
{
name: this.i18nService.t('passwordGenerator'),
description: this.i18nService.t('passwordGeneratorPolicyDesc'),
type: PolicyType.PasswordGenerator,
enabled: false,
display: true,
},
{
name: this.i18nService.t('singleOrg'),
description: this.i18nService.t('singleOrgDesc'),
type: PolicyType.SingleOrg,
enabled: false,
display: true,
},
{
name: this.i18nService.t('requireSso'),
description: this.i18nService.t('requireSsoPolicyDesc'),
type: PolicyType.RequireSso,
enabled: false,
display: organization.useSso,
},
];
await this.load();
});
// Remove when removing deprecation warning
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';
}
}
async load() {
@@ -102,6 +132,7 @@ export class PoliciesComponent implements OnInit {
childComponent.description = p.description;
childComponent.type = p.type;
childComponent.organizationId = this.organizationId;
childComponent.policiesEnabledMap = this.policiesEnabledMap;
childComponent.onSavedPolicy.subscribe(() => {
this.modal.close();
this.load();
@@ -111,4 +142,22 @@ export class PoliciesComponent implements OnInit {
this.modal = null;
});
}
// Remove when removing deprecation warning
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.organizationId);
}
} catch { }
this.enterpriseTokenPromise = null;
}
}

View File

@@ -17,6 +17,18 @@
title="{{'warning' | i18n}}" icon="fa-warning">
{{'twoStepLoginPolicyWarning' | i18n}}
</app-callout>
<app-callout type="warning" *ngIf="type === policyType.SingleOrg" title="{{'warning' | i18n}}"
icon="fa-warning">
{{'singleOrgPolicyWarning' | i18n}}
</app-callout>
<ng-container *ngIf="type === policyType.RequireSso">
<app-callout type="tip" title="{{'prerequisite' | i18n}}">
{{'requireSsoPolicyReq' | i18n}}
</app-callout>
<app-callout type="warning">
{{'requireSsoExemption' | i18n}}
</app-callout>
</ng-container>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="enabled" [(ngModel)]="enabled"

View File

@@ -27,6 +27,7 @@ export class PolicyEditComponent implements OnInit {
@Input() description: string;
@Input() type: PolicyType;
@Input() organizationId: string;
@Input() policiesEnabledMap: Map<PolicyType, boolean> = new Map<PolicyType, boolean>();
@Output() onSavedPolicy = new EventEmitter();
policyType = PolicyType;
@@ -127,45 +128,66 @@ export class PolicyEditComponent implements OnInit {
}
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;
if (this.preValidate()) {
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 { }
}
}
private preValidate(): boolean {
switch (this.type) {
case PolicyType.RequireSso:
if (!this.enabled) { // Don't need prevalidation checks if submitting to disable
return true;
}
// Have SingleOrg policy enabled?
if (!(this.policiesEnabledMap.has(PolicyType.SingleOrg)
&& this.policiesEnabledMap.get(PolicyType.SingleOrg))) {
this.toasterService.popAsync('error', null, this.i18nService.t('requireSsoPolicyReqError'));
return false;
}
return true;
default:
return true;
}
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,5 +1,5 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
<div class="modal-dialog" [ngClass]="{'modal-lg': !editMode}" role="document">
<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" id="userAddEditTitle">
@@ -19,11 +19,18 @@
<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">
@@ -59,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">
@@ -94,6 +105,7 @@
<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>
@@ -106,6 +118,10 @@
<td (click)="check(c)">
{{c.name}}
</td>
<td class="text-center">
<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">

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

@@ -10,17 +10,23 @@
<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">
[(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">
[(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">
@@ -49,9 +55,17 @@
<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>

View File

@@ -11,31 +11,35 @@ 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';
import { OrganizationResponse } from 'jslib/models/response/organizationResponse';
import { ModalComponent } from '../../modal.component';
import { ApiKeyComponent } from '../../settings/api-key.component';
import { PurgeVaultComponent } from '../../settings/purge-vault.component';
import { ApiKeyComponent } from './api-key.component';
import { TaxInfoComponent } from '../../settings/tax-info.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('apiKeyTemplate', { read: ViewContainerRef }) apiKeyModalRef: ViewContainerRef;
@ViewChild('rotateApiKeyTemplate', { read: ViewContainerRef }) rotateApiKeyModalRef: 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;
@@ -43,9 +47,11 @@ 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 {
@@ -62,6 +68,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);
});
@@ -71,6 +78,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();
@@ -110,7 +124,14 @@ export class AccountComponent {
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;
childComponent.keyType = 'organization';
childComponent.entityId = this.organizationId;
childComponent.postKey = this.apiService.postOrganizationApiKey.bind(this.apiService);
childComponent.scope = 'api.organization';
childComponent.grantType = 'client_credentials';
childComponent.apiKeyTitle = 'apiKey';
childComponent.apiKeyWarning = 'apiKeyWarning';
childComponent.apiKeyDescription = 'apiKeyDesc';
this.modal.onClosed.subscribe(async () => {
this.modal = null;
@@ -124,8 +145,16 @@ export class AccountComponent {
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;
const childComponent = this.modal.show<ApiKeyComponent>(ApiKeyComponent, this.rotateApiKeyModalRef);
childComponent.keyType = 'organization';
childComponent.isRotation = true;
childComponent.entityId = this.organizationId;
childComponent.postKey = this.apiService.postOrganizationRotateApiKey.bind(this.apiService);
childComponent.scope = 'api.organization';
childComponent.grantType = 'client_credentials';
childComponent.apiKeyTitle = 'apiKey';
childComponent.apiKeyWarning = 'apiKeyWarning';
childComponent.apiKeyDescription = 'apiKeyRotateDesc';
this.modal.onClosed.subscribe(async () => {
this.modal = null;

View File

@@ -33,7 +33,7 @@ export class AdjustSeatsComponent {
@Output() onAdjusted = new EventEmitter<number>();
@Output() onCanceled = new EventEmitter();
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
seatAdjustment = 0;
formPromise: Promise<any>;

View File

@@ -4,8 +4,8 @@
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="families" [organizationId]="organizationId"
(onCanceled)="cancel()">
<app-organization-plans [showFree]="false" [showCancel]="true" [plan]="defaultUpgradePlan"
[product]="defaultUpgradeProduct" [organizationId]="organizationId" (onCanceled)="cancel()">
</app-organization-plans>
</div>
</div>

View File

@@ -8,6 +8,9 @@ import {
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',
@@ -18,6 +21,8 @@ export class ChangePlanComponent {
@Output() onCanceled = new EventEmitter();
formPromise: Promise<any>;
defaultUpgradePlan: PlanType = PlanType.FamiliesAnnually;
defaultUpgradeProduct: ProductType = ProductType.Families;
constructor(private apiService: ApiService, private platformUtilsService: PlatformUtilsService) { }

View File

@@ -24,7 +24,7 @@
</app-callout>
<dl *ngIf="selfHosted">
<dt>{{'billingPlan' | i18n}}</dt>
<dd>{{sub.plan}}</dd>
<dd>{{sub.plan.name}}</dd>
<dt>{{'expiration' | i18n}}</dt>
<dd *ngIf="sub.expiration">
{{sub.expiration | date:'mediumDate'}}
@@ -39,7 +39,7 @@
<div class="col-4">
<dl>
<dt>{{'billingPlan' | i18n}}</dt>
<dd>{{sub.plan}}</dd>
<dd>{{sub.plan.name}}</dd>
<ng-container *ngIf="subscription">
<dt>{{'status' | i18n}}</dt>
<dd>

View File

@@ -13,7 +13,6 @@ 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 { TokenService } from 'jslib/abstractions/token.service';
import { PlanType } from 'jslib/enums/planType';
@@ -38,10 +37,10 @@ export class OrganizationSubscriptionComponent implements OnInit {
cancelPromise: Promise<any>;
reinstatePromise: Promise<any>;
constructor(private tokenService: TokenService, private apiService: ApiService,
private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private messagingService: MessagingService, private route: ActivatedRoute) {
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();
}
@@ -192,34 +191,20 @@ export class OrganizationSubscriptionComponent implements OnInit {
}
get billingInterval() {
const monthly = this.sub.planType === PlanType.EnterpriseMonthly ||
this.sub.planType === PlanType.TeamsMonthly;
const monthly = !this.sub.plan.isAnnual;
return monthly ? 'month' : 'year';
}
get storageGbPrice() {
return this.billingInterval === 'month' ? 0.5 : 4;
return this.sub.plan.additionalStoragePricePerGb;
}
get seatPrice() {
switch (this.sub.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;
}
return this.sub.plan.seatPrice;
}
get canAdjustSeats() {
return this.sub.planType === PlanType.EnterpriseMonthly ||
this.sub.planType === PlanType.EnterpriseAnnually ||
this.sub.planType === PlanType.TeamsMonthly || this.sub.planType === PlanType.TeamsAnnually;
return this.sub.plan.hasAdditionalSeatsOption;
}
get canDownloadLicense() {

View File

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

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

@@ -94,6 +94,7 @@ export class AddEditComponent extends BaseAddEditComponent {
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

@@ -41,7 +41,7 @@ export class CiphersComponent extends BaseCiphersComponent {
async load(filter: (cipher: CipherView) => boolean = null) {
if (!this.organization.isAdmin) {
await super.load(filter);
await super.load(filter, this.deleted);
return;
}
this.accessEvents = this.organization.useEvents;
@@ -65,30 +65,32 @@ export class CiphersComponent extends BaseCiphersComponent {
}
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;
this.ciphers = filteredCiphers.filter((c) => {
if (c.isDeleted !== this.deleted) {
return false;
}
return this.filter == null || this.filter(c);
});
} else {
this.ciphers = this.searchService.searchCiphersBasic(filteredCiphers, this.searchText);
if (this.filter != null) {
filteredCiphers = filteredCiphers.filter(this.filter);
}
this.ciphers = this.searchService.searchCiphersBasic(filteredCiphers, this.searchText, this.deleted);
}
await this.resetPaging();
}
checkCipher(c: CipherView) {
// do nothing
}
events(c: CipherView) {
this.onEventsClicked.emit(c);
}
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) {

View File

@@ -1,9 +1,10 @@
<div class="container page-content">
<div class="row">
<div class="col-3">
<app-org-vault-groupings [showFolders]="false" [showFavorites]="false"
<app-org-vault-groupings [showFolders]="false" [showFavorites]="false" [showTrash]="true"
(onAllClicked)="clearGroupingFilters()" (onCipherTypeClicked)="filterCipherType($event)"
(onCollectionClicked)="filterCollection($event.id)" (onSearchTextChanged)="filterSearchText($event)">
(onCollectionClicked)="filterCollection($event.id)" (onSearchTextChanged)="filterSearchText($event)"
(onTrashClicked)="filterDeleted()">
</app-org-vault-groupings>
</div>
<div class="col-9">
@@ -18,9 +19,15 @@
</ng-container>
</small>
</h1>
<button type="button" class="btn btn-outline-primary btn-sm ml-auto" (click)="addCipher()">
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>{{'addItem' | i18n}}
</button>
<div class="ml-auto d-flex">
<app-vault-bulk-actions [ciphersComponent]="ciphersComponent" [modal]="modal" [deleted]="deleted"
[organization]="organization">
</app-vault-bulk-actions>
<button type="button" class="btn btn-outline-primary btn-sm ml-auto" (click)="addCipher()"
*ngIf="!deleted">
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>{{'addItem' | i18n}}
</button>
</div>
</div>
<app-org-vault-ciphers (onCipherClicked)="editCipher($event)"
(onAttachmentsClicked)="editCipherAttachments($event)" (onAddCipher)="addCipher()"
@@ -33,4 +40,4 @@
<ng-template #attachments></ng-template>
<ng-template #cipherAddEdit></ng-template>
<ng-template #collections></ng-template>
<ng-template #eventsTemplate></ng-template>
<ng-template #eventsTemplate></ng-template>

View File

@@ -41,18 +41,19 @@ const BroadcasterSubscriptionId = 'OrgVaultComponent';
templateUrl: 'vault.component.html',
})
export class VaultComponent implements OnInit, OnDestroy {
@ViewChild(GroupingsComponent) groupingsComponent: GroupingsComponent;
@ViewChild(CiphersComponent) ciphersComponent: CiphersComponent;
@ViewChild('attachments', { read: ViewContainerRef }) attachmentsModalRef: ViewContainerRef;
@ViewChild('cipherAddEdit', { read: ViewContainerRef }) cipherAddEditModalRef: ViewContainerRef;
@ViewChild('collections', { read: ViewContainerRef }) collectionsModalRef: ViewContainerRef;
@ViewChild('eventsTemplate', { read: ViewContainerRef }) eventsModalRef: ViewContainerRef;
@ViewChild(GroupingsComponent, { static: true }) groupingsComponent: GroupingsComponent;
@ViewChild(CiphersComponent, { static: true }) ciphersComponent: CiphersComponent;
@ViewChild('attachments', { read: ViewContainerRef, static: true }) attachmentsModalRef: ViewContainerRef;
@ViewChild('cipherAddEdit', { read: ViewContainerRef, static: true }) cipherAddEditModalRef: ViewContainerRef;
@ViewChild('collections', { read: ViewContainerRef, static: true }) collectionsModalRef: ViewContainerRef;
@ViewChild('eventsTemplate', { read: ViewContainerRef, static: true }) eventsModalRef: ViewContainerRef;
organization: Organization;
collectionId: string;
type: CipherType;
collectionId: string = null;
type: CipherType = null;
deleted: boolean = false;
private modal: ModalComponent = null;
modal: ModalComponent = null;
constructor(private route: ActivatedRoute, private userService: UserService,
private router: Router, private changeDetectorRef: ChangeDetectorRef,
@@ -61,7 +62,7 @@ export class VaultComponent implements OnInit, OnDestroy {
private broadcasterService: BroadcasterService, private ngZone: NgZone) { }
ngOnInit() {
this.route.parent.params.subscribe(async (params) => {
const queryParams = this.route.parent.params.subscribe(async (params) => {
this.organization = await this.userService.getOrganization(params.organizationId);
this.groupingsComponent.organization = this.organization;
this.ciphersComponent.organization = this.organization;
@@ -92,7 +93,10 @@ export class VaultComponent implements OnInit, OnDestroy {
this.groupingsComponent.selectedAll = true;
await this.ciphersComponent.reload();
} else {
if (qParams.type) {
if (qParams.deleted) {
this.groupingsComponent.selectedTrash = true;
await this.filterDeleted(true);
} else if (qParams.type) {
const t = parseInt(qParams.type, null);
this.groupingsComponent.selectedType = t;
await this.filterCipherType(t, true);
@@ -116,6 +120,10 @@ export class VaultComponent implements OnInit, OnDestroy {
queryParamsSub.unsubscribe();
}
});
if (queryParams != null) {
queryParams.unsubscribe();
}
});
}
@@ -125,6 +133,7 @@ export class VaultComponent implements OnInit, OnDestroy {
async clearGroupingFilters() {
this.ciphersComponent.showAddNew = true;
this.ciphersComponent.deleted = false;
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchVault');
await this.ciphersComponent.applyFilter();
this.clearFilters();
@@ -133,6 +142,7 @@ export class VaultComponent implements OnInit, OnDestroy {
async filterCipherType(type: CipherType, load = false) {
this.ciphersComponent.showAddNew = true;
this.ciphersComponent.deleted = false;
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchType');
const filter = (c: CipherView) => c.type === type;
if (load) {
@@ -147,6 +157,7 @@ export class VaultComponent implements OnInit, OnDestroy {
async filterCollection(collectionId: string, load = false) {
this.ciphersComponent.showAddNew = true;
this.ciphersComponent.deleted = false;
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchCollection');
const filter = (c: CipherView) => {
if (collectionId === 'unassigned') {
@@ -165,6 +176,20 @@ export class VaultComponent implements OnInit, OnDestroy {
this.go();
}
async filterDeleted(load: boolean = false) {
this.ciphersComponent.showAddNew = false;
this.ciphersComponent.deleted = true;
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchTrash');
if (load) {
await this.ciphersComponent.reload(null, true);
} else {
await this.ciphersComponent.applyFilter(null);
}
this.clearFilters();
this.deleted = true;
this.go();
}
filterSearchText(searchText: string) {
this.ciphersComponent.searchText = searchText;
this.ciphersComponent.search(200);
@@ -255,6 +280,10 @@ export class VaultComponent implements OnInit, OnDestroy {
this.modal.close();
await this.ciphersComponent.refresh();
});
childComponent.onRestoredCipher.subscribe(async (c: CipherView) => {
this.modal.close();
await this.ciphersComponent.refresh();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
@@ -299,6 +328,7 @@ export class VaultComponent implements OnInit, OnDestroy {
private clearFilters() {
this.collectionId = null;
this.type = null;
this.deleted = false;
}
private go(queryParams: any = null) {
@@ -306,6 +336,7 @@ export class VaultComponent implements OnInit, OnDestroy {
queryParams = {
type: this.type,
collectionId: this.collectionId,
deleted: this.deleted ? true : null,
};
}

View File

@@ -0,0 +1,59 @@
<form #form (ngSubmit)="load()" [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">Bitwarden Send</p>
<div class="card d-block">
<div class="card-body" *ngIf="loading" class="text-center">
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}"
aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</div>
<div class="card-body" *ngIf="!loading && passwordRequired">
<p>{{'sendProtectedPassword' | i18n}}</p>
<p>{{'sendProtectedPasswordDontKnow' | i18n}}</p>
<div class="form-group">
<label for="password">{{'password' | i18n}}</label>
<input id="password" type="password" name="Password" class="text-monospace form-control"
[(ngModel)]="password" required appInputVerbatim appAutofocus>
</div>
<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> {{'continue' | i18n}}
</span>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="card-body" *ngIf="!loading && !passwordRequired && send">
<p class="text-center"><b>{{send.name}}</b></p>
<hr>
<!-- Text -->
<ng-container *ngIf="send.type === sendType.Text">
<app-callout *ngIf="send.text.hidden" type="tip">{{'sendHiddenByDefault' | i18n}}</app-callout>
<div class="form-group">
<textarea id="text" rows="8" name="Text" [(ngModel)]="sendText" class="form-control"
readonly (click)="selectText()"></textarea>
</div>
<button class="btn btn-block btn-link" type="button" (click)="toggleText()"
*ngIf="send.text.hidden">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showText, 'fa-eye-slash': showText}"></i>
{{'toggleVisibility' | i18n}}
</button>
<button class="btn btn-block btn-link" type="button" (click)="copyText()">
<i class="fa fa-copy" aria-hidden="true"></i> {{'copyValue' | i18n}}
</button>
</ng-container>
<!-- File -->
<ng-container *ngIf="send.type === sendType.File">
<p>{{send.file.fileName}}</p>
<button class="btn btn-primary btn-block" type="button" (click)="download()">
<i class="fa fa-download" aria-hidden="true"></i>
{{'downloadFile' | i18n}} ({{send.file.sizeName}})</button>
</ng-container>
</div>
</div>
</div>
</div>
</form>

View File

@@ -0,0 +1,139 @@
import {
Component,
OnInit,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { Utils } from 'jslib/misc/utils';
import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
import { SendAccess } from 'jslib/models/domain/sendAccess';
import { SendAccessView } from 'jslib/models/view/sendAccessView';
import { SendType } from 'jslib/enums/sendType';
import { SendAccessRequest } from 'jslib/models/request/sendAccessRequest';
import { ErrorResponse } from 'jslib/models/response/errorResponse';
import { SendAccessResponse } from 'jslib/models/response/sendAccessResponse';
@Component({
selector: 'app-send-access',
templateUrl: 'access.component.html',
})
export class AccessComponent implements OnInit {
send: SendAccessView;
sendType = SendType;
downloading = false;
loading = true;
passwordRequired = false;
formPromise: Promise<SendAccessResponse>;
password: string;
showText = false;
private id: string;
private key: string;
private decKey: SymmetricCryptoKey;
constructor(private i18nService: I18nService, private cryptoFunctionService: CryptoFunctionService,
private apiService: ApiService, private platformUtilsService: PlatformUtilsService,
private route: ActivatedRoute, private cryptoService: CryptoService) {
}
get sendText() {
if (this.send == null || this.send.text == null) {
return null;
}
return this.showText ? this.send.text.text : this.send.text.maskedText;
}
ngOnInit() {
this.route.params.subscribe(async (params) => {
this.id = params.sendId;
this.key = params.key;
if (this.key == null || this.id == null) {
return;
}
await this.load();
});
}
async download() {
if (this.send == null || this.decKey == null) {
return;
}
if (this.downloading) {
return;
}
this.downloading = true;
const response = await fetch(new Request(this.send.file.url, { cache: 'no-store' }));
if (response.status !== 200) {
this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred'));
this.downloading = false;
return;
}
try {
const buf = await response.arrayBuffer();
const decBuf = await this.cryptoService.decryptFromBytes(buf, this.decKey);
this.platformUtilsService.saveFile(window, decBuf, null, this.send.file.fileName);
} catch (e) {
this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred'));
}
this.downloading = false;
}
selectText() {
(document.getElementById('text') as HTMLInputElement).select();
}
copyText() {
this.platformUtilsService.copyToClipboard(this.send.text.text);
this.platformUtilsService.showToast('success', null,
this.i18nService.t('valueCopied', this.i18nService.t('sendTypeText')));
}
toggleText() {
this.showText = !this.showText;
}
async load() {
const keyArray = Utils.fromUrlB64ToArray(this.key);
const accessRequest = new SendAccessRequest();
if (this.password != null) {
const passwordHash = await this.cryptoFunctionService.pbkdf2(this.password, keyArray, 'sha256', 100000);
accessRequest.password = Utils.fromBufferToB64(passwordHash);
}
try {
let sendResponse: SendAccessResponse = null;
if (this.loading) {
sendResponse = await this.apiService.postSendAccess(this.id, accessRequest);
} else {
this.formPromise = this.apiService.postSendAccess(this.id, accessRequest);
sendResponse = await this.formPromise;
}
this.passwordRequired = false;
const sendAccess = new SendAccess(sendResponse);
this.decKey = await this.cryptoService.makeSendKey(keyArray);
this.send = await sendAccess.decrypt(this.decKey);
this.showText = this.send.text != null ? !this.send.text.hidden : true;
} catch (e) {
if (e instanceof ErrorResponse) {
if (e.statusCode === 401) {
this.passwordRequired = true;
}
}
}
this.loading = false;
}
}

View File

@@ -0,0 +1,129 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="sendAddEditTitle">
<div class="modal-dialog modal-lg" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate
autocomplete="off">
<div class="modal-header">
<h2 class="modal-title" id="sendAddEditTitle">{{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="send">
<div class="row" *ngIf="!editMode">
<div class="col-6 form-group">
<label for="type">{{'whatTypeOfSend' | i18n}}</label>
<select id="type" name="Type" [(ngModel)]="send.type" class="form-control" appAutofocus>
<option *ngFor="let o of typeOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="name">{{'name' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="send.name" required>
</div>
</div>
<!-- Text -->
<ng-container *ngIf="send.type === sendType.Text">
<div class="form-group">
<label for="text">{{'sendTypeText' | i18n}}</label>
<textarea id="text" name="Text.Text" rows="6" [(ngModel)]="send.text.text"
class="form-control"></textarea>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" [(ngModel)]="send.text.hidden"
id="text-hidden" name="Text.Hidden">
<label class="form-check-label" for="text-hidden">{{'cfTypeHidden' | i18n}}</label>
</div>
</div>
</ng-container>
<!-- File -->
<ng-container *ngIf="send.type === sendType.File">
<div class="form-group">
<div *ngIf="editMode">
<strong class="d-block">{{'file' | i18n}}</strong>
{{send.file.fileName}} ({{send.file.sizeName}})
</div>
<div *ngIf="!editMode">
<label for="file">{{'file' | i18n}}</label>
<input type="file" id="file" class="form-control-file" name="file" required>
<small class="form-text text-muted">{{'maxFileSize' | i18n}}</small>
</div>
</div>
</ng-container>
<h3 class="mt-4">{{'options' | i18n}}</h3>
<div class="row">
<div class="col-6 form-group">
<label for="deletionDate">{{'deletionDate' | i18n}}</label>
<input id="deletionDate" class="form-control" type="datetime-local" name="DeletionDate"
[(ngModel)]="deletionDate" required>
</div>
<div class="col-6 form-group">
<div class="d-flex">
<label for="expirationDate">{{'expirationDate' | i18n}}</label>
<a href="#" appStopClick (click)="clearExpiration()" class="ml-auto">
{{'clear' | i18n}}
</a>
</div>
<input id="expirationDate" class="form-control" type="datetime-local" name="ExpirationDate"
[(ngModel)]="expirationDate">
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="maxAccessCount">{{'maxAccessCount' | i18n}}</label>
<input id="maxAccessCount" class="form-control" type="number" name="MaxAccessCount"
[(ngModel)]="send.maxAccessCount">
</div>
<div class="col-6 form-group" *ngIf="editMode">
<label for="accessCount">{{'currentAccessCount' | i18n}}</label>
<input id="accessCount" class="form-control" type="number" name="AccessCount" readonly
[(ngModel)]="send.accessCount">
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="password" *ngIf="!hasPassword">{{'password' | i18n}}</label>
<label for="password" *ngIf="hasPassword">{{'newPassword' | i18n}}</label>
<input id="password" class="form-control" type="password" name="Password"
[(ngModel)]="password">
</div>
</div>
<div class="form-group">
<label for="notes">{{'notes' | i18n}}</label>
<textarea id="notes" name="Notes" rows="6" [(ngModel)]="send.notes" class="form-control"></textarea>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" [(ngModel)]="send.disabled" id="disabled"
name="Disabled">
<label class="form-check-label" for="disabled">{{'disabled' | i18n}}</label>
</div>
</div>
<div class="form-group" *ngIf="link">
<label for="link">{{'sendLink' | i18n}}</label>
<input type="text" readonly id="link" name="Link" [(ngModel)]="link" class="form-control">
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'save' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{'cancel' | i18n}}
</button>
<div class="ml-auto" *ngIf="send">
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger"
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"
[appApiAction]="deletePromise">
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading" aria-hidden="true"></i>
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"
title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
</div>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,276 @@
import { DatePipe } from '@angular/common';
import {
EventEmitter,
Input,
Output,
} from '@angular/core';
import { Component } from '@angular/core';
import { SendType } from 'jslib/enums/sendType';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SendView } from 'jslib/models/view/sendView';
import { SendFileView } from 'jslib/models/view/sendFileView';
import { SendTextView } from 'jslib/models/view/sendTextView';
import { Send } from 'jslib/models/domain/send';
import { SendFile } from 'jslib/models/domain/sendFile';
import { SendText } from 'jslib/models/domain/sendText';
import { SendData } from 'jslib/models/data/sendData';
import { SendRequest } from 'jslib/models/request/sendRequest';
import { Utils } from 'jslib/misc/utils';
@Component({
selector: 'app-send-add-edit',
templateUrl: 'add-edit.component.html',
})
export class AddEditComponent {
@Input() sendId: string;
@Input() type: SendType;
@Output() onSavedSend = new EventEmitter<SendView>();
@Output() onDeletedSend = new EventEmitter<SendView>();
@Output() onCancelled = new EventEmitter<SendView>();
editMode: boolean = false;
send: SendView;
link: string;
title: string;
deletionDate: string;
expirationDate: string;
hasPassword: boolean;
password: string;
formPromise: Promise<any>;
deletePromise: Promise<any>;
sendType = SendType;
typeOptions: any[];
constructor(private i18nService: I18nService, private platformUtilsService: PlatformUtilsService,
private apiService: ApiService, private cryptoService: CryptoService,
private cryptoFunctionService: CryptoFunctionService, private environmentService: EnvironmentService,
private datePipe: DatePipe) {
this.typeOptions = [
{ name: i18nService.t('sendTypeFile'), value: SendType.File },
{ name: i18nService.t('sendTypeText'), value: SendType.Text },
];
}
async ngOnInit() {
await this.load();
}
async load() {
this.editMode = this.sendId != null;
if (this.editMode) {
this.editMode = true;
this.title = this.i18nService.t('editSend');
} else {
this.title = this.i18nService.t('createSend');
}
if (this.send == null) {
if (this.editMode) {
const send = await this.loadSend();
this.send = await send.decrypt();
} else {
this.send = new SendView();
this.send.type = this.type == null ? SendType.File : this.type;
this.send.file = new SendFileView();
this.send.text = new SendTextView();
this.send.deletionDate = new Date();
this.send.deletionDate.setDate(this.send.deletionDate.getDate() + 7);
}
}
this.hasPassword = this.send.password != null && this.send.password.trim() !== '';
// Parse dates
this.deletionDate = this.send.deletionDate == null ? null :
this.datePipe.transform(this.send.deletionDate, 'yyyy-MM-ddTHH:mm');
this.expirationDate = this.send.expirationDate == null ? null :
this.datePipe.transform(this.send.expirationDate, 'yyyy-MM-ddTHH:mm');
if (this.editMode) {
let webVaultUrl = this.environmentService.getWebVaultUrl();
if (webVaultUrl == null) {
webVaultUrl = 'https://vault.bitwarden.com';
}
this.link = webVaultUrl + '/#/send/' + this.send.accessId + '/' + this.send.urlB64Key;
}
}
async submit(): Promise<boolean> {
if (this.send.name == null || this.send.name === '') {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('nameRequired'));
return false;
}
let file: File = null;
if (this.send.type === SendType.File && !this.editMode) {
const fileEl = document.getElementById('file') as HTMLInputElement;
const files = fileEl.files;
if (files == null || files.length === 0) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('selectFile'));
return;
}
file = files[0];
if (file.size > 104857600) { // 100 MB
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('maxFileSize'));
return;
}
}
const encSend = await this.encryptSend(file);
try {
this.formPromise = this.saveSend(encSend);
await this.formPromise;
this.send.id = encSend[0].id;
this.platformUtilsService.showToast('success', null,
this.i18nService.t(this.editMode ? 'editedSend' : 'createdSend'));
this.onSavedSend.emit(this.send);
return true;
} catch { }
return false;
}
clearExpiration() {
this.expirationDate = null;
}
async delete(): Promise<void> {
if (this.deletePromise != null) {
return;
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('deleteSendConfirmation'),
this.i18nService.t('deleteSend'),
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return;
}
try {
this.deletePromise = this.apiService.deleteSend(this.send.id);
await this.deletePromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedSend'));
await this.load();
this.onDeletedSend.emit(this.send);
} catch { }
}
protected async loadSend(): Promise<Send> {
const response = await this.apiService.getSend(this.sendId);
const data = new SendData(response);
return new Send(data);
}
protected async encryptSend(file: File): Promise<[Send, ArrayBuffer]> {
let fileData: ArrayBuffer = null;
const send = new Send();
send.id = this.send.id;
send.type = this.send.type;
send.disabled = this.send.disabled;
send.maxAccessCount = this.send.maxAccessCount;
if (this.send.key == null) {
this.send.key = await this.cryptoFunctionService.randomBytes(16);
this.send.cryptoKey = await this.cryptoService.makeSendKey(this.send.key);
}
if (this.password != null) {
const passwordHash = await this.cryptoFunctionService.pbkdf2(this.password,
this.send.key, 'sha256', 100000);
send.password = Utils.fromBufferToB64(passwordHash);
}
send.key = await this.cryptoService.encrypt(this.send.key, null);
send.name = await this.cryptoService.encrypt(this.send.name, this.send.cryptoKey);
send.notes = await this.cryptoService.encrypt(this.send.notes, this.send.cryptoKey);
if (send.type === SendType.Text) {
send.text = new SendText();
send.text.text = await this.cryptoService.encrypt(this.send.text.text, this.send.cryptoKey);
send.text.hidden = this.send.text.hidden;
} else if (send.type === SendType.File) {
send.file = new SendFile();
if (file != null) {
fileData = await this.parseFile(send, file);
}
}
// Parse dates
try {
send.deletionDate = this.deletionDate == null ? null : new Date(this.deletionDate);
} catch {
send.deletionDate = null;
}
try {
send.expirationDate = this.expirationDate == null ? null : new Date(this.expirationDate);
} catch {
send.expirationDate = null;
}
return [send, fileData];
}
protected async saveSend(sendData: [Send, ArrayBuffer]) {
const request = new SendRequest(sendData[0]);
if (sendData[0].id == null) {
if (sendData[0].type === SendType.Text) {
await this.apiService.postSend(request);
} else {
const fd = new FormData();
try {
const blob = new Blob([sendData[1]], { type: 'application/octet-stream' });
fd.append('model', JSON.stringify(request));
fd.append('data', blob, sendData[0].file.fileName.encryptedString);
} catch (e) {
if (Utils.isNode && !Utils.isBrowser) {
fd.append('model', JSON.stringify(request));
fd.append('data', Buffer.from(sendData[1]) as any, {
filepath: sendData[0].file.fileName.encryptedString,
contentType: 'application/octet-stream',
} as any);
} else {
throw e;
}
}
await this.apiService.postSendFile(fd);
}
} else {
await this.apiService.putSend(sendData[0].id, request);
}
}
private parseFile(send: Send, file: File): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = async (evt) => {
try {
send.file.fileName = await this.cryptoService.encrypt(file.name, this.send.cryptoKey);
const fileData = await this.cryptoService.encryptToBytes(evt.target.result as ArrayBuffer,
this.send.cryptoKey);
resolve(fileData);
} catch (e) {
reject(e);
}
};
reader.onerror = (evt) => {
reject('Error reading file.');
};
});
}
}

View File

@@ -0,0 +1,113 @@
<div class="container page-content">
<div class="row">
<div class="col-3 groupings">
<div class="card vault-filters">
<div class="card-header d-flex">
{{'filters' | i18n}}
</div>
<div class="card-body">
<input type="search" placeholder="{{searchPlaceholder || ('searchSends' | i18n)}}" id="search"
class="form-control" [(ngModel)]="searchText" (input)="searchTextChanged()" autocomplete="off"
appAutofocus>
<ul class="fa-ul card-ul">
<li [ngClass]="{active: selectedAll}">
<a href="#" appStopClick (click)="selectAll()">
<i class="fa-li fa fa-fw fa-th"></i>{{'allSends' | i18n}}
</a>
</li>
</ul>
<h3>{{'types' | i18n}}</h3>
<ul class="fa-ul card-ul">
<li [ngClass]="{active: selectedType === sendType.Text}">
<a href="#" appStopClick (click)="selectType(sendType.Text)">
<i class="fa-li fa fa-fw fa-file-text-o"></i>{{'sendTypeText' | i18n}}
</a>
</li>
<li [ngClass]="{active: selectedType === sendType.File}">
<a href="#" appStopClick (click)="selectType(sendType.File)">
<i class="fa-li fa fa-fw fa-file-o"></i>{{'sendTypeFile' | i18n}}
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="col-9">
<div class="page-header d-flex">
<h1>
Send
<small #actionSpinner [appApiAction]="actionPromise">
<ng-container *ngIf="actionSpinner.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>
</small>
</h1>
<div class="ml-auto d-flex">
<button type="button" class="btn btn-outline-primary btn-sm" (click)="addSend()">
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>{{'createSend' | i18n}}
</button>
</div>
</div>
<!--Listing Table-->
<table class="table table-hover table-list" *ngIf="filteredSends && filteredSends.length">
<tbody>
<tr *ngFor="let s of filteredSends">
<td class="table-list-icon">
<div class="icon" aria-hidden="true">
<i class="fa fa-fw fa-lg fa-file-o" *ngIf="s.type == sendType.File"></i>
<i class="fa fa-fw fa-lg fa-file-text-o" *ngIf="s.type == sendType.Text"></i>
</div>
</td>
<td class="reduced-lh wrap">
<a href="#" appStopClick appStopProp (click)="editSend(s)">{{s.name}}</a>
<ng-container *ngIf="s.password">
<i class="fa fa-key" appStopProp title="{{'password' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'password' | i18n}}</span>
</ng-container>
<br>
<small appStopProp>{{s.deletionDate | date:'medium'}}</small>
</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown>
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
id="dropdownMenuButton" 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" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" href="#" appStopClick (click)="copy(s)">
<i class="fa fa-fw fa-copy" aria-hidden="true"></i>
{{'copySendLink' | i18n}}
</a>
<a class="dropdown-item" href="#" appStopClick (click)="removePassword(s)"
*ngIf="s.password">
<i class="fa fa-fw fa-undo" aria-hidden="true"></i>
{{'removePassword' | i18n}}
</a>
<a class="dropdown-item text-danger" href="#" appStopClick (click)="delete(s)">
<i class="fa fa-fw fa-trash-o" aria-hidden="true"></i>
{{'delete' | i18n}}
</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<div class="no-items" *ngIf="filteredSends && !filteredSends.length">
<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>{{'noSendsInList' | i18n}}</p>
<button (click)="addSend()" class="btn btn-outline-primary">
<i class="fa fa-plus fa-fw"></i>{{'createSend' | i18n}}</button>
</ng-container>
</div>
</div>
</div>
</div>
<ng-template #sendAddEdit></ng-template>

View File

@@ -0,0 +1,221 @@
import {
Component,
ComponentFactoryResolver,
OnInit,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { SendType } from 'jslib/enums/sendType';
import { SendView } from 'jslib/models/view/sendView';
import { AddEditComponent } from './add-edit.component';
import { ModalComponent } from '../modal.component';
import { ApiService } from 'jslib/abstractions/api.service';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { UserService } from 'jslib/abstractions/user.service';
import { SendData } from 'jslib/models/data/sendData';
import { Send } from 'jslib/models/domain/send';
@Component({
selector: 'app-send',
templateUrl: 'send.component.html',
})
export class SendComponent implements OnInit {
@ViewChild('sendAddEdit', { read: ViewContainerRef, static: true }) sendAddEditModalRef: ViewContainerRef;
sendType = SendType;
loaded = false;
loading = true;
refreshing = false;
expired: boolean = false;
type: SendType = null;
sends: SendView[] = [];
filteredSends: SendView[] = [];
searchText: string;
selectedType: SendType;
selectedAll: boolean;
searchPlaceholder: string;
filter: (cipher: SendView) => boolean;
searchPending = false;
modal: ModalComponent = null;
actionPromise: any;
private searchTimeout: any;
constructor(private apiService: ApiService, private userService: UserService,
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
private platformUtilsService: PlatformUtilsService, private environmentService: EnvironmentService) { }
async ngOnInit() {
await this.load();
}
async load(filter: (send: SendView) => boolean = null) {
this.loading = true;
const userId = await this.userService.getUserId();
const sends = await this.apiService.getSends();
const sendsArr: SendView[] = [];
if (sends != null && sends.data != null) {
for (const res of sends.data) {
const data = new SendData(res, userId);
const send = new Send(data);
const view = await send.decrypt();
sendsArr.push(view);
}
}
this.sends = sendsArr;
this.selectAll();
this.loading = false;
this.loaded = true;
}
async reload(filter: (send: SendView) => boolean = null) {
this.loaded = false;
this.sends = [];
await this.load(filter);
}
async refresh() {
try {
this.refreshing = true;
await this.reload(this.filter);
} finally {
this.refreshing = false;
}
}
async applyFilter(filter: (send: SendView) => boolean = null) {
this.filter = filter;
await this.search(null);
}
async search(timeout: number = null) {
this.searchPending = false;
if (this.searchTimeout != null) {
clearTimeout(this.searchTimeout);
}
if (timeout == null) {
this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s));
return;
}
this.searchPending = true;
this.searchTimeout = setTimeout(async () => {
this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s));
this.searchPending = false;
}, timeout);
}
addSend() {
const component = this.editSend(null);
component.type = this.type;
}
editSend(send: SendView) {
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.sendAddEditModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<AddEditComponent>(
AddEditComponent, this.sendAddEditModalRef);
childComponent.sendId = send == null ? null : send.id;
childComponent.onSavedSend.subscribe(async (s: SendView) => {
this.modal.close();
await this.load();
});
childComponent.onDeletedSend.subscribe(async (s: SendView) => {
this.modal.close();
await this.load();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
return childComponent;
}
async removePassword(s: SendView): Promise<boolean> {
if (this.actionPromise != null || s.password == null) {
return;
}
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('removePasswordConfirmation'),
this.i18nService.t('removePassword'),
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
try {
this.actionPromise = this.apiService.putSendRemovePassword(s.id);
await this.actionPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('removedPassword'));
await this.load();
} catch { }
this.actionPromise = null;
}
async delete(s: SendView): Promise<boolean> {
if (this.actionPromise != null) {
return false;
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('deleteSendConfirmation'),
this.i18nService.t('deleteSend'),
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
try {
this.actionPromise = this.apiService.deleteSend(s.id);
await this.actionPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedSend'));
await this.load();
} catch { }
this.actionPromise = null;
return true;
}
copy(s: SendView) {
let webVaultUrl = this.environmentService.getWebVaultUrl();
if (webVaultUrl == null) {
webVaultUrl = 'https://vault.bitwarden.com';
}
const link = webVaultUrl + '/#/send/' + s.accessId + '/' + s.urlB64Key;
this.platformUtilsService.copyToClipboard(link);
this.platformUtilsService.showToast('success', null,
this.i18nService.t('valueCopied', this.i18nService.t('sendLink')));
}
searchTextChanged() {
this.search(200);
}
selectAll() {
this.clearSelections();
this.selectedAll = true;
this.applyFilter(null);
}
selectType(type: SendType) {
this.clearSelections();
this.selectedType = type;
this.applyFilter((s) => s.type === type);
}
clearSelections() {
this.selectedAll = false;
this.selectedType = null;
}
}

View File

@@ -73,8 +73,14 @@ export class EventService {
msg = this.i18nService.t('editedItemId', this.formatCipherId(ev, options));
break;
case EventType.Cipher_Deleted:
msg = this.i18nService.t('permanentlyDeletedItemId', this.formatCipherId(ev, options));
break;
case EventType.Cipher_SoftDeleted:
msg = this.i18nService.t('deletedItemId', this.formatCipherId(ev, options));
break;
case EventType.Cipher_Restored:
msg = this.i18nService.t('restoredItemId', this.formatCipherId(ev, options));
break;
case EventType.Cipher_AttachmentCreated:
msg = this.i18nService.t('createdAttachmentForItem', this.formatCipherId(ev, options));
break;

View File

@@ -38,7 +38,6 @@ import { EventService as EventLoggingService } from 'jslib/services/event.servic
import { ExportService } from 'jslib/services/export.service';
import { FolderService } from 'jslib/services/folder.service';
import { ImportService } from 'jslib/services/import.service';
import { LockService } from 'jslib/services/lock.service';
import { NotificationsService } from 'jslib/services/notifications.service';
import { PasswordGenerationService } from 'jslib/services/passwordGeneration.service';
import { PolicyService } from 'jslib/services/policy.service';
@@ -49,6 +48,7 @@ import { SyncService } from 'jslib/services/sync.service';
import { TokenService } from 'jslib/services/token.service';
import { TotpService } from 'jslib/services/totp.service';
import { UserService } from 'jslib/services/user.service';
import { VaultTimeoutService } from 'jslib/services/vaultTimeout.service';
import { WebCryptoFunctionService } from 'jslib/services/webCryptoFunction.service';
import { ApiService as ApiServiceAbstraction } from 'jslib/abstractions/api.service';
@@ -65,7 +65,6 @@ import { ExportService as ExportServiceAbstraction } from 'jslib/abstractions/ex
import { FolderService as FolderServiceAbstraction } from 'jslib/abstractions/folder.service';
import { I18nService as I18nServiceAbstraction } from 'jslib/abstractions/i18n.service';
import { ImportService as ImportServiceAbstraction } from 'jslib/abstractions/import.service';
import { LockService as LockServiceAbstraction } from 'jslib/abstractions/lock.service';
import { LogService as LogServiceAbstraction } from 'jslib/abstractions/log.service';
import { MessagingService as MessagingServiceAbstraction } from 'jslib/abstractions/messaging.service';
import { NotificationsService as NotificationsServiceAbstraction } from 'jslib/abstractions/notifications.service';
@@ -82,6 +81,7 @@ import { SyncService as SyncServiceAbstraction } from 'jslib/abstractions/sync.s
import { TokenService as TokenServiceAbstraction } from 'jslib/abstractions/token.service';
import { TotpService as TotpServiceAbstraction } from 'jslib/abstractions/totp.service';
import { UserService as UserServiceAbstraction } from 'jslib/abstractions/user.service';
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from 'jslib/abstractions/vaultTimeout.service';
const i18nService = new I18nService(window.navigator.language, 'locales');
const stateService = new StateService();
@@ -106,10 +106,11 @@ const cipherService = new CipherService(cryptoService, userService, settingsServ
const folderService = new FolderService(cryptoService, userService, apiService, storageService,
i18nService, cipherService);
const collectionService = new CollectionService(cryptoService, userService, storageService, i18nService);
searchService = new SearchService(cipherService, platformUtilsService);
searchService = new SearchService(cipherService);
const policyService = new PolicyService(userService, storageService);
const lockService = new LockService(cipherService, folderService, collectionService,
cryptoService, platformUtilsService, storageService, messagingService, searchService, userService, null);
const vaultTimeoutService = new VaultTimeoutService(cipherService, folderService, collectionService,
cryptoService, platformUtilsService, storageService, messagingService, searchService, userService, tokenService,
null, async () => messagingService.send('logout', { expired: false }));
const syncService = new SyncService(userService, apiService, settingsService,
folderService, cipherService, cryptoService, collectionService, storageService, messagingService, policyService,
async (expired: boolean) => messagingService.send('logout', { expired: expired }));
@@ -117,11 +118,11 @@ const passwordGenerationService = new PasswordGenerationService(cryptoService, s
const totpService = new TotpService(storageService, cryptoFunctionService);
const containerService = new ContainerService(cryptoService);
const authService = new AuthService(cryptoService, apiService,
userService, tokenService, appIdService, i18nService, platformUtilsService, messagingService);
userService, tokenService, appIdService, i18nService, platformUtilsService, messagingService, vaultTimeoutService);
const exportService = new ExportService(folderService, cipherService, apiService);
const importService = new ImportService(cipherService, folderService, apiService, i18nService, collectionService);
const notificationsService = new NotificationsService(userService, syncService, appIdService,
apiService, lockService, async () => messagingService.send('logout', { expired: true }));
apiService, vaultTimeoutService, async () => messagingService.send('logout', { expired: true }));
const environmentService = new EnvironmentService(apiService, storageService, notificationsService);
const auditService = new AuditService(cryptoFunctionService, apiService);
const eventLoggingService = new EventLoggingService(storageService, apiService, userService, cipherService);
@@ -139,6 +140,8 @@ export function initFactory(): Function {
} else {
environmentService.notificationsUrl = isDev ? 'http://localhost:61840' :
'https://notifications.bitwarden.com'; // window.location.origin + '/notifications';
environmentService.enterpriseUrl = isDev ? 'http://localhost:52313' :
'https://portal.bitwarden.com'; // window.location.origin + '/portal';
}
apiService.setUrls({
base: isDev ? null : window.location.origin,
@@ -156,7 +159,7 @@ export function initFactory(): Function {
});
setTimeout(() => notificationsService.init(environmentService), 3000);
lockService.init(true);
vaultTimeoutService.init(true);
const locale = await storageService.get<string>(ConstantsService.localeKey);
await i18nService.init(locale);
eventLoggingService.init(true);
@@ -205,7 +208,7 @@ export function initFactory(): Function {
{ provide: MessagingServiceAbstraction, useValue: messagingService },
{ provide: BroadcasterService, useValue: broadcasterService },
{ provide: SettingsServiceAbstraction, useValue: settingsService },
{ provide: LockServiceAbstraction, useValue: lockService },
{ provide: VaultTimeoutServiceAbstraction, useValue: vaultTimeoutService },
{ provide: StorageServiceAbstraction, useValue: storageService },
{ provide: StateServiceAbstraction, useValue: stateService },
{ provide: ExportServiceAbstraction, useValue: exportService },

View File

@@ -4,18 +4,18 @@ import {
Router,
} from '@angular/router';
import { LockService } from 'jslib/abstractions/lock.service';
import { UserService } from 'jslib/abstractions/user.service';
import { VaultTimeoutService } from 'jslib/abstractions/vaultTimeout.service';
@Injectable()
export class UnauthGuardService implements CanActivate {
constructor(private lockService: LockService, private userService: UserService,
constructor(private vaultTimeoutService: VaultTimeoutService, private userService: UserService,
private router: Router) { }
async canActivate() {
const isAuthed = await this.userService.isAuthenticated();
if (isAuthed) {
const locked = await this.lockService.isLocked();
const locked = await this.vaultTimeoutService.isLocked();
if (locked) {
this.router.navigate(['lock']);
} else {

View File

@@ -14,6 +14,14 @@
<h1>{{'encKeySettings' | i18n}}</h1>
</div>
<app-change-kdf></app-change-kdf>
<div class="secondary-header border-0 mb-0">
<h1>{{'apiKey' | i18n}}</h1>
</div>
<p>
{{'userApiKeyDesc' | i18n}}
</p>
<button type="button" class="btn btn-outline-secondary" (click)="viewUserApiKey()">{{'viewApiKey' | i18n}}</button>
<button type="button" class="btn btn-outline-secondary" (click)="rotateUserApiKey()">{{'rotateApiKey' | i18n}}</button>
<div class="secondary-header text-danger border-0 mb-0">
<h1>{{'dangerZone' | i18n}}</h1>
</div>
@@ -30,3 +38,5 @@
<ng-template #deauthorizeSessionsTemplate></ng-template>
<ng-template #purgeVaultTemplate></ng-template>
<ng-template #deleteAccountTemplate></ng-template>
<ng-template #viewUserApiKeyTemplate></ng-template>
<ng-template #rotateUserApiKeyTemplate></ng-template>

View File

@@ -6,22 +6,29 @@ import {
} from '@angular/core';
import { ModalComponent } from '../modal.component';
import { ApiKeyComponent } from './api-key.component';
import { DeauthorizeSessionsComponent } from './deauthorize-sessions.component';
import { DeleteAccountComponent } from './delete-account.component';
import { PurgeVaultComponent } from './purge-vault.component';
import { ApiService } from 'jslib/abstractions/api.service';
import { UserService } from 'jslib/abstractions/user.service';
@Component({
selector: 'app-account',
templateUrl: 'account.component.html',
})
export class AccountComponent {
@ViewChild('deauthorizeSessionsTemplate', { read: ViewContainerRef }) deauthModalRef: ViewContainerRef;
@ViewChild('purgeVaultTemplate', { read: ViewContainerRef }) purgeModalRef: ViewContainerRef;
@ViewChild('deleteAccountTemplate', { read: ViewContainerRef }) deleteModalRef: ViewContainerRef;
@ViewChild('deauthorizeSessionsTemplate', { read: ViewContainerRef, static: true }) deauthModalRef: ViewContainerRef;
@ViewChild('purgeVaultTemplate', { read: ViewContainerRef, static: true }) purgeModalRef: ViewContainerRef;
@ViewChild('deleteAccountTemplate', { read: ViewContainerRef, static: true }) deleteModalRef: ViewContainerRef;
@ViewChild('viewUserApiKeyTemplate', { read: ViewContainerRef, static: true }) viewUserApiKeyModalRef: ViewContainerRef;
@ViewChild('rotateUserApiKeyTemplate', { read: ViewContainerRef, static: true }) rotateUserApiKeyModalRef: ViewContainerRef;
private modal: ModalComponent = null;
constructor(private componentFactoryResolver: ComponentFactoryResolver) { }
constructor(private componentFactoryResolver: ComponentFactoryResolver, private apiService: ApiService,
private userService: UserService) { }
deauthorizeSessions() {
if (this.modal != null) {
@@ -64,4 +71,49 @@ export class AccountComponent {
this.modal = null;
});
}
async viewUserApiKey() {
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.viewUserApiKeyModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<ApiKeyComponent>(ApiKeyComponent, this.viewUserApiKeyModalRef);
childComponent.keyType = 'user';
childComponent.entityId = await this.userService.getUserId();
childComponent.postKey = this.apiService.postUserApiKey.bind(this.apiService);
childComponent.scope = 'api';
childComponent.grantType = 'client_credentials';
childComponent.apiKeyTitle = 'apiKey';
childComponent.apiKeyWarning = 'userApiKeyWarning';
childComponent.apiKeyDescription = 'userApiKeyDesc';
this.modal.onClosed.subscribe(async () => {
this.modal = null;
});
}
async rotateUserApiKey() {
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.rotateUserApiKeyModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<ApiKeyComponent>(ApiKeyComponent, this.rotateUserApiKeyModalRef);
childComponent.keyType = 'user';
childComponent.isRotation = true;
childComponent.entityId = await this.userService.getUserId();
childComponent.postKey = this.apiService.postUserRotateApiKey.bind(this.apiService);
childComponent.scope = 'api';
childComponent.grantType = 'client_credentials';
childComponent.apiKeyTitle = 'apiKey';
childComponent.apiKeyWarning = 'userApiKeyWarning';
childComponent.apiKeyDescription = 'apiKeyRotateDesc';
this.modal.onClosed.subscribe(async () => {
this.modal = null;
});
}
}

View File

@@ -33,7 +33,7 @@ export class AddCreditComponent implements OnInit {
@Output() onAdded = new EventEmitter();
@Output() onCanceled = new EventEmitter();
@ViewChild('ppButtonForm', { read: ElementRef }) ppButtonFormRef: ElementRef;
@ViewChild('ppButtonForm', { read: ElementRef, static: true }) ppButtonFormRef: ElementRef;
paymentMethodType = PaymentMethodType;
ppButtonFormAction = WebConstants.paypal.buttonActionProduction;

View File

@@ -4,6 +4,7 @@
aria-hidden="true">&times;</span></button>
<h3 class="card-body-header">{{(currentType != null ? 'changePaymentMethod' : 'addPaymentMethod') | i18n}}</h3>
<app-payment [hideBank]="!organizationId" [hideCredit]="true"></app-payment>
<app-tax-info (onCountryChanged)="changeCountry()"></app-tax-info>
<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>

View File

@@ -17,13 +17,15 @@ import { PaymentRequest } from 'jslib/models/request/paymentRequest';
import { PaymentMethodType } from 'jslib/enums/paymentMethodType';
import { PaymentComponent } from './payment.component';
import { TaxInfoComponent } from './tax-info.component';
@Component({
selector: 'app-adjust-payment',
templateUrl: 'adjust-payment.component.html',
})
export class AdjustPaymentComponent {
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
@ViewChild(TaxInfoComponent, { static: true }) taxInfoComponent: TaxInfoComponent;
@Input() currentType?: PaymentMethodType;
@Input() organizationId: string;
@@ -42,9 +44,17 @@ export class AdjustPaymentComponent {
this.formPromise = this.paymentComponent.createPaymentToken().then((result) => {
request.paymentToken = result[0];
request.paymentMethodType = result[1];
request.postalCode = this.taxInfoComponent.taxInfo.postalCode;
request.country = this.taxInfoComponent.taxInfo.country;
if (this.organizationId == null) {
return this.apiService.postAccountPayment(request);
} else {
request.taxId = this.taxInfoComponent.taxInfo.taxId;
request.state = this.taxInfoComponent.taxInfo.state;
request.line1 = this.taxInfoComponent.taxInfo.line1;
request.line2 = this.taxInfoComponent.taxInfo.line2;
request.city = this.taxInfoComponent.taxInfo.city;
request.state = this.taxInfoComponent.taxInfo.state;
return this.apiService.postOrganizationPayment(this.organizationId, request);
}
});
@@ -60,4 +70,16 @@ export class AdjustPaymentComponent {
cancel() {
this.onCanceled.emit();
}
changeCountry() {
if (this.taxInfoComponent.taxInfo.country === 'US') {
this.paymentComponent.hideBank = !this.organizationId;
} else {
this.paymentComponent.hideBank = true;
if (this.paymentComponent.method === PaymentMethodType.BankAccount) {
this.paymentComponent.method = PaymentMethodType.Card;
this.paymentComponent.changeMethod();
}
}
}
}

View File

@@ -35,7 +35,7 @@ export class AdjustStorageComponent {
@Output() onAdjusted = new EventEmitter<number>();
@Output() onCanceled = new EventEmitter();
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
storageAdjustment = 0;
formPromise: Promise<any>;

View File

@@ -2,19 +2,19 @@
<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>
<h2 class="modal-title" id="apiKeyTitle">{{apiKeyTitle | 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>
<p>{{apiKeyDescription | 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="warning" *ngIf="clientSecret">{{apiKeyWarning | i18n}}</app-callout>
<app-callout type="info" title="{{'oauth2ClientCredentials' | i18n}}" icon="fa-key"
*ngIf="clientSecret">
<p class="mb-1">
@@ -31,7 +31,7 @@
</p>
<p class="mb-0">
<strong>grant_type:</strong><br>
<code>client_credentials</code>
<code>{{grantType}}</code>
</p>
</app-callout>
</div>
@@ -39,7 +39,7 @@
<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>
<span>{{(isRotation ? 'rotateApiKey' : 'viewApiKey') | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
</div>

View File

@@ -1,10 +1,8 @@
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';
@@ -17,17 +15,23 @@ import { ApiKeyResponse } from 'jslib/models/response/apiKeyResponse';
templateUrl: 'api-key.component.html',
})
export class ApiKeyComponent {
organizationId: string;
keyType: string;
isRotation: boolean;
postKey: (entityId: string, request: PasswordVerificationRequest) => Promise<ApiKeyResponse>;
entityId: string;
scope: string;
grantType: string;
apiKeyTitle: string;
apiKeyWarning: string;
apiKeyDescription: 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) { }
constructor(private i18nService: I18nService, private analytics: Angulartics2,
private toasterService: ToasterService, private cryptoService: CryptoService) { }
async submit() {
if (this.masterPassword == null || this.masterPassword === '') {
@@ -39,12 +43,11 @@ export class ApiKeyComponent {
const request = new PasswordVerificationRequest();
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
try {
this.formPromise = this.apiService.postOrganizationApiKey(this.organizationId, request);
this.formPromise = this.postKey(this.entityId, 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' });
this.clientId = `${this.keyType}.${this.entityId}`;
this.analytics.eventTrack.next({ action: `Viewed ${this.keyType} API Key` });
} catch { }
}
}

View File

@@ -28,18 +28,18 @@
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="newMasterPassword">{{'newMasterPass' | i18n}}</label>
<input id="newMasterPassword" type="password" name="NewMasterPasswordHash" class="form-control mb-1"
[(ngModel)]="newMasterPassword" (input)="updatePasswordStrength()" required appInputVerbatim
<label for="masterPassword">{{'newMasterPass' | i18n}}</label>
<input id="masterPassword" type="password" name="NewMasterPasswordHash" class="form-control mb-1"
[(ngModel)]="masterPassword" (input)="updatePasswordStrength()" required appInputVerbatim
autocomplete="new-password">
<app-password-strength [score]="masterPasswordScore" [showText]="true"></app-password-strength>
</div>
</div>
<div class="col-6">
<div class="form-group">
<label for="confirmNewMasterPassword">{{'confirmNewMasterPass' | i18n}}</label>
<input id="confirmNewMasterPassword" type="password" name="ConfirmNewMasterPasswordHash"
class="form-control" [(ngModel)]="confirmNewMasterPassword" required appInputVerbatim
<label for="masterPasswordRetype">{{'confirmNewMasterPass' | i18n}}</label>
<input id="masterPasswordRetype" type="password" name="MasterPasswordRetype"
class="form-control" [(ngModel)]="masterPasswordRetype" required appInputVerbatim
autocomplete="new-password">
</div>
</div>

View File

@@ -1,10 +1,4 @@
import {
Component,
OnInit,
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { Component } from '@angular/core';
import { ApiService } from 'jslib/abstractions/api.service';
import { CipherService } from 'jslib/abstractions/cipher.service';
@@ -18,8 +12,11 @@ import { PolicyService } from 'jslib/abstractions/policy.service';
import { SyncService } from 'jslib/abstractions/sync.service';
import { UserService } from 'jslib/abstractions/user.service';
import {
ChangePasswordComponent as BaseChangePasswordComponent,
} from 'jslib/angular/components/change-password.component';
import { CipherString } from 'jslib/models/domain/cipherString';
import { MasterPasswordPolicyOptions } from 'jslib/models/domain/masterPasswordPolicyOptions';
import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
import { CipherWithIdRequest } from 'jslib/models/request/cipherWithIdRequest';
@@ -31,136 +28,18 @@ import { UpdateKeyRequest } from 'jslib/models/request/updateKeyRequest';
selector: 'app-change-password',
templateUrl: 'change-password.component.html',
})
export class ChangePasswordComponent implements OnInit {
currentMasterPassword: string;
newMasterPassword: string;
confirmNewMasterPassword: string;
formPromise: Promise<any>;
masterPasswordScore: number;
export class ChangePasswordComponent extends BaseChangePasswordComponent {
rotateEncKey = false;
enforcedPolicyOptions: MasterPasswordPolicyOptions;
currentMasterPassword: string;
private masterPasswordStrengthTimeout: any;
private email: string;
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private cryptoService: CryptoService, private messagingService: MessagingService,
private userService: UserService, private passwordGenerationService: PasswordGenerationService,
private platformUtilsService: PlatformUtilsService, private folderService: FolderService,
private cipherService: CipherService, private syncService: SyncService,
private policyService: PolicyService) { }
async ngOnInit() {
this.email = await this.userService.getEmail();
this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions();
}
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 submit() {
const hasEncKey = await this.cryptoService.hasEncKey();
if (!hasEncKey) {
this.toasterService.popAsync('error', null, this.i18nService.t('updateKey'));
return;
}
if (this.currentMasterPassword == null || this.currentMasterPassword === '' ||
this.newMasterPassword == null || this.newMasterPassword === '') {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPassRequired'));
return;
}
if (this.newMasterPassword.length < 8) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPassLength'));
return;
}
if (this.newMasterPassword !== this.confirmNewMasterPassword) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPassDoesntMatch'));
return;
}
const strengthResult = this.passwordGenerationService.passwordStrength(this.newMasterPassword,
this.getPasswordStrengthUserInput());
if (this.enforcedPolicyOptions != null &&
!this.policyService.evaluateMasterPassword(
strengthResult.score,
this.newMasterPassword,
this.enforcedPolicyOptions)) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPasswordPolicyRequirementsNotMet'));
return;
}
if (strengthResult != null && strengthResult.score < 3) {
const result = await this.platformUtilsService.showDialog(this.i18nService.t('weakMasterPasswordDesc'),
this.i18nService.t('weakMasterPassword'), this.i18nService.t('yes'), this.i18nService.t('no'),
'warning');
if (!result) {
return;
}
}
if (this.rotateEncKey) {
await this.syncService.fullSync(true);
}
const request = new PasswordRequest();
request.masterPasswordHash = await this.cryptoService.hashPassword(this.currentMasterPassword, null);
const email = await this.userService.getEmail();
const kdf = await this.userService.getKdf();
const kdfIterations = await this.userService.getKdfIterations();
const newKey = await this.cryptoService.makeKey(this.newMasterPassword, email.trim().toLowerCase(),
kdf, kdfIterations);
request.newMasterPasswordHash = await this.cryptoService.hashPassword(this.newMasterPassword, newKey);
const newEncKey = await this.cryptoService.remakeEncKey(newKey);
request.key = newEncKey[1].encryptedString;
try {
if (this.rotateEncKey) {
this.formPromise = this.apiService.postPassword(request).then(() => {
return this.updateKey(newKey, request.newMasterPasswordHash);
});
} else {
this.formPromise = this.apiService.postPassword(request);
}
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Changed Password' });
this.toasterService.popAsync('success', this.i18nService.t('masterPasswordChanged'),
this.i18nService.t('logBackIn'));
this.messagingService.send('logout');
} catch { }
}
updatePasswordStrength() {
if (this.masterPasswordStrengthTimeout != null) {
clearTimeout(this.masterPasswordStrengthTimeout);
}
this.masterPasswordStrengthTimeout = setTimeout(() => {
const strengthResult = this.passwordGenerationService.passwordStrength(this.newMasterPassword,
this.getPasswordStrengthUserInput());
this.masterPasswordScore = strengthResult == null ? null : strengthResult.score;
}, 300);
constructor(i18nService: I18nService,
cryptoService: CryptoService, messagingService: MessagingService,
userService: UserService, passwordGenerationService: PasswordGenerationService,
platformUtilsService: PlatformUtilsService, policyService: PolicyService,
private folderService: FolderService, private cipherService: CipherService,
private syncService: SyncService, private apiService: ApiService, ) {
super(i18nService, cryptoService, messagingService, userService, passwordGenerationService,
platformUtilsService, policyService);
}
async rotateEncKeyClicked() {
@@ -198,13 +77,54 @@ export class ChangePasswordComponent implements OnInit {
}
}
private getPasswordStrengthUserInput() {
let userInput: string[] = [];
const atPosition = this.email.indexOf('@');
if (atPosition > -1) {
userInput = userInput.concat(this.email.substr(0, atPosition).trim().toLowerCase().split(/[^A-Za-z0-9]/));
async submit() {
const hasEncKey = await this.cryptoService.hasEncKey();
if (!hasEncKey) {
this.platformUtilsService.showToast('error', null, this.i18nService.t('updateKey'));
return;
}
await super.submit();
}
async setupSubmitActions() {
if (this.currentMasterPassword == null || this.currentMasterPassword === '') {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPassRequired'));
return false;
}
if (this.rotateEncKey) {
await this.syncService.fullSync(true);
}
return super.setupSubmitActions();
}
async performSubmitActions(newMasterPasswordHash: string, newKey: SymmetricCryptoKey,
newEncKey: [SymmetricCryptoKey, CipherString]) {
const request = new PasswordRequest();
request.masterPasswordHash = await this.cryptoService.hashPassword(this.currentMasterPassword, null);
request.newMasterPasswordHash = newMasterPasswordHash;
request.key = newEncKey[1].encryptedString;
try {
if (this.rotateEncKey) {
this.formPromise = this.apiService.postPassword(request).then(() => {
return this.updateKey(newKey, request.newMasterPasswordHash);
});
} else {
this.formPromise = this.apiService.postPassword(request);
}
await this.formPromise;
this.platformUtilsService.showToast('success', this.i18nService.t('masterPasswordChanged'),
this.i18nService.t('logBackIn'));
this.messagingService.send('logout');
} catch {
this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred'));
}
return userInput;
}
private async updateKey(key: SymmetricCryptoKey, masterPasswordHash: string) {

View File

@@ -12,7 +12,7 @@ import { OrganizationPlansComponent } from './organization-plans.component';
templateUrl: 'create-organization.component.html',
})
export class CreateOrganizationComponent implements OnInit {
@ViewChild(OrganizationPlansComponent) orgPlansComponent: OrganizationPlansComponent;
@ViewChild(OrganizationPlansComponent, { static: true }) orgPlansComponent: OrganizationPlansComponent;
constructor(private route: ActivatedRoute) { }

View File

@@ -0,0 +1,4 @@
<a class="dropdown-item" href="#" appStopClick (click)="submit(returnUri, true)">
<i class="fa fa-fw fa-link" aria-hidden="true"></i>
{{'linkSso' | i18n}}
</a>

View File

@@ -0,0 +1,48 @@
import {
AfterContentInit,
Component,
Input,
} 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 } from 'jslib/angular/components/sso.component';
import { Organization } from 'jslib/models/domain/organization';
@Component({
selector: 'app-link-sso',
templateUrl: 'link-sso.component.html',
})
export class LinkSsoComponent extends SsoComponent implements AfterContentInit {
@Input() organization: Organization;
returnUri: string = '/settings/organizations'
constructor(platformUtilsService: PlatformUtilsService, i18nService: I18nService,
apiService: ApiService, authService: AuthService,
router: Router, route: ActivatedRoute,
cryptoFunctionService: CryptoFunctionService, passwordGenerationService: PasswordGenerationService,
storageService: StorageService, stateService: StateService) {
super(authService, router,
i18nService, route,
storageService, stateService,
platformUtilsService, apiService,
cryptoFunctionService, passwordGenerationService);
this.returnUri = '/settings/organizations';
this.redirectUri = window.location.origin + '/sso-connector.html';
this.clientId = 'web';
}
async ngAfterContentInit() {
this.identifier = this.organization.identifier;
}
}

View File

@@ -6,14 +6,33 @@
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="lockOption">{{'lockOptions' | i18n}}</label>
<select id="lockOption" name="LockOption" [(ngModel)]="lockOption" class="form-control">
<option *ngFor="let o of lockOptions" [ngValue]="o.value">{{o.name}}</option>
<label for="vaultTimeout">{{'vaultTimeout' | i18n}}</label>
<select id="vaultTimeout" name="VaultTimeout" [(ngModel)]="vaultTimeout" class="form-control">
<option *ngFor="let o of vaultTimeouts" [ngValue]="o.value">{{o.name}}</option>
</select>
<small class="form-text text-muted">{{'lockOptionsDesc' | i18n}}</small>
<small class="form-text text-muted">{{'vaultTimeoutDesc' | i18n}}</small>
</div>
</div>
</div>
<div class="form-group">
<label>{{'vaultTimeoutAction' | i18n}}</label>
<div class="form-check form-check-block">
<input class="form-check-input" type="radio" name="vaultTimeoutAction" id="vaultTimeoutActionLock"
value="lock" [(ngModel)]="vaultTimeoutAction">
<label class="form-check-label" for="vaultTimeoutActionLock">
{{'lock' | i18n}}
<small>{{'vaultTimeoutActionLockDesc' | i18n}}</small>
</label>
</div>
<div class="form-check mt-2 form-check-block">
<input class="form-check-input" type="radio" name="vaultTimeoutAction" id="vaultTimeoutActionLogOut"
value="logOut" [(ngModel)]="vaultTimeoutAction" (ngModelChange)="vaultTimeoutActionChanged($event)">
<label class="form-check-label" for="vaultTimeoutActionLogOut">
{{'logOut' | i18n}}
<small>{{'vaultTimeoutActionLogOutDesc' | i18n}}</small>
</label>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="form-group">
@@ -57,6 +76,16 @@
</a>
</div>
<small class="form-text text-muted">{{'enableGravatarsDesc' | i18n}}</small>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="enableFullWidth" name="enableFullWidth"
[(ngModel)]="enableFullWidth">
<label class="form-check-label" for="enableFullWidth">
{{'enableFullWidth' | i18n}}
</label>
</div>
<small class="form-text text-muted">{{'enableFullWidthDesc' | i18n}}</small>
</div>
<button type="submit" class="btn btn-primary">
{{'save' | i18n}}

View File

@@ -7,10 +7,11 @@ import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { LockService } from 'jslib/abstractions/lock.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 { VaultTimeoutService } from 'jslib/abstractions/vaultTimeout.service';
import { ConstantsService } from 'jslib/services/constants.service';
@@ -21,20 +22,22 @@ import { Utils } from 'jslib/misc/utils';
templateUrl: 'options.component.html',
})
export class OptionsComponent implements OnInit {
lockOption: number = null;
vaultTimeout: number = null;
vaultTimeoutAction: string = 'lock';
disableIcons: boolean;
enableGravatars: boolean;
enableFullWidth: boolean;
locale: string;
lockOptions: any[];
vaultTimeouts: any[];
localeOptions: any[];
private startingLocale: string;
constructor(private storageService: StorageService, private stateService: StateService,
private analytics: Angulartics2, private i18nService: I18nService,
private toasterService: ToasterService, private lockService: LockService,
private platformUtilsService: PlatformUtilsService) {
this.lockOptions = [
private toasterService: ToasterService, private vaultTimeoutService: VaultTimeoutService,
private platformUtilsService: PlatformUtilsService, private messagingService: MessagingService) {
this.vaultTimeouts = [
{ name: i18nService.t('oneMinute'), value: 1 },
{ name: i18nService.t('fiveMinutes'), value: 5 },
{ name: i18nService.t('fifteenMinutes'), value: 15 },
@@ -44,7 +47,7 @@ export class OptionsComponent implements OnInit {
{ name: i18nService.t('onRefresh'), value: -1 },
];
if (this.platformUtilsService.isDev()) {
this.lockOptions.push({ name: i18nService.t('never'), value: null });
this.vaultTimeouts.push({ name: i18nService.t('never'), value: null });
}
const localeOptions: any[] = [];
@@ -61,18 +64,23 @@ export class OptionsComponent implements OnInit {
}
async ngOnInit() {
this.lockOption = await this.storageService.get<number>(ConstantsService.lockOptionKey);
this.vaultTimeout = await this.storageService.get<number>(ConstantsService.vaultTimeoutKey);
this.vaultTimeoutAction = await this.storageService.get<string>(ConstantsService.vaultTimeoutActionKey);
this.disableIcons = await this.storageService.get<boolean>(ConstantsService.disableFaviconKey);
this.enableGravatars = await this.storageService.get<boolean>('enableGravatars');
this.enableFullWidth = await this.storageService.get<boolean>('enableFullWidth');
this.locale = this.startingLocale = await this.storageService.get<string>(ConstantsService.localeKey);
}
async submit() {
await this.lockService.setLockOption(this.lockOption != null ? this.lockOption : null);
await this.vaultTimeoutService.setVaultTimeoutOptions(this.vaultTimeout != null ? this.vaultTimeout : null,
this.vaultTimeoutAction);
await this.storageService.save(ConstantsService.disableFaviconKey, this.disableIcons);
await this.stateService.save(ConstantsService.disableFaviconKey, this.disableIcons);
await this.storageService.save('enableGravatars', this.enableGravatars);
await this.stateService.save('enableGravatars', this.enableGravatars);
await this.storageService.save('enableFullWidth', this.enableFullWidth);
this.messagingService.send('setFullWidth');
await this.storageService.save(ConstantsService.localeKey, this.locale);
this.analytics.eventTrack.next({ action: 'Saved Options' });
if (this.locale !== this.startingLocale) {
@@ -81,4 +89,18 @@ export class OptionsComponent implements OnInit {
this.toasterService.popAsync('success', null, this.i18nService.t('optionsUpdated'));
}
}
async vaultTimeoutActionChanged(newValue: string) {
if (newValue === 'logOut') {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('vaultTimeoutLogOutConfirmation'),
this.i18nService.t('vaultTimeoutLogOutConfirmationTitle'),
this.i18nService.t('yes'), this.i18nService.t('cancel'), 'warning');
if (!confirmed) {
this.vaultTimeoutAction = 'lock';
return;
}
}
this.vaultTimeoutAction = newValue;
}
}

View File

@@ -1,3 +1,7 @@
<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="createOrganization && selfHosted">
<p>{{'uploadLicenseFileOrg' | i18n}}</p>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
@@ -13,7 +17,8 @@
</button>
</form>
</ng-container>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="!selfHosted">
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate
*ngIf="!loading && !selfHosted && this.plans">
<h2 class="mt-5">{{'generalInformation' | i18n}}</h2>
<div class="row" *ngIf="createOrganization">
<div class="form-group col-6">
@@ -38,69 +43,61 @@
</div>
</div>
<h2 class="mt-5">{{'chooseYourPlan' | i18n}}</h2>
<div class="form-check form-check-block" *ngIf="!ownedBusiness && showFree">
<input class="form-check-input" type="radio" name="PlanType" id="planFree" value="free" [(ngModel)]="plan"
(change)="changedPlan()">
<label class="form-check-label" for="planFree">
{{'planNameFree' | i18n}}
<small class="mb-1">{{'planDescFree' | i18n : '1'}}</small>
<small>• {{'limitedUsers' | i18n : '2'}}</small>
<small>• {{'limitedCollections' | i18n : '2'}}</small>
<span>{{'freeForever' | i18n}}</span>
<div *ngFor="let selectableProduct of selectableProducts" class="form-check form-check-block">
<input class="form-check-input" type="radio" name="product" id="product{{selectableProduct.product}}"
[value]="selectableProduct.product" [(ngModel)]="product" (change)="changedProduct()">
<label class="form-check-label" for="product{{selectableProduct.product}}">
{{ selectableProduct.nameLocalizationKey | i18n}}
<small class="mb-1">{{ selectableProduct.descriptionLocalizationKey | i18n : '1'}}</small>
<ng-container *ngIf="selectableProduct.product === productTypes.Enterprise; else fullFeatureList">
<small>• {{'includeAllTeamsFeatures' | i18n}}</small>
<small *ngIf="selectableProduct.hasSelfHost">{{'onPremHostingOptional' | i18n}}</small>
<small *ngIf="selectableProduct.hasSso">• {{'includeSsoAuthentication' | i18n}}</small>
<small *ngIf="selectableProduct.hasPolicies">• {{'includeEnterprisePolicies' | i18n}}</small>
</ng-container>
<ng-template #fullFeatureList>
<small *ngIf="selectableProduct.product == productTypes.Free">
{{'limitedUsers' | i18n : selectableProduct.maxUsers }}</small>
<small *ngIf="selectableProduct.product != productTypes.Free && selectableProduct.maxUsers">
{{'addShareLimitedUsers' | i18n : selectableProduct.maxUsers}}</small>
<small *ngIf="!selectableProduct.maxUsers">
{{'addShareUnlimitedUsers' | i18n}}</small>
<small *ngIf="selectableProduct.maxCollections">
{{'limitedCollections' | i18n : selectableProduct.maxCollections }}</small>
<small *ngIf="selectableProduct.maxAdditionalSeats">
{{'addShareLimitedUsers' | i18n : selectableProduct.maxAdditionalSeats }}</small>
<small *ngIf="!selectableProduct.maxCollections">• {{'createUnlimitedCollections' | i18n}}</small>
<small *ngIf="selectableProduct.baseStorageGb">
{{'gbEncryptedFileStorage' | i18n : selectableProduct.baseStorageGb + 'GB'}}</small>
<small *ngIf="selectableProduct.hasGroups">• {{'controlAccessWithGroups' | i18n}}</small>
<small *ngIf="selectableProduct.hasApi">• {{'trackAuditLogs' | i18n}}</small>
<small *ngIf="selectableProduct.hasDirectory">• {{'syncUsersFromDirectory' | i18n}}</small>
<small *ngIf="selectableProduct.hasSelfHost">• {{'onPremHostingOptional' | i18n}}</small>
<small *ngIf="selectableProduct.usersGetPremium">• {{'usersGetPremium' | i18n}}</small>
<small *ngIf="selectableProduct.product != productTypes.Free">
{{'priorityCustomerSupport' | i18n}}</small>
<small *ngIf="selectableProduct.trialPeriodDays">
{{'xDayFreeTrial' | i18n : selectableProduct.trialPeriodDays }}
</small>
</ng-template>
<span *ngIf="selectableProduct.product != productTypes.Free">
<ng-container *ngIf="selectableProduct.basePrice">
{{selectableProduct.basePrice / 12 | currency:'$'}} /{{'month' | i18n}},
{{'includesXUsers' | i18n : selectableProduct.baseSeats}}
<ng-container *ngIf="selectableProduct.hasAdditionalSeatsOption">
{{('additionalUsers' | i18n).toLowerCase()}}
{{selectableProduct.seatPrice / 12 | currency:'$'}} /{{'month' | i18n}}
</ng-container>
</ng-container>
</span>
<span *ngIf="!selectableProduct.basePrice && selectableProduct.hasAdditionalSeatsOption">
{{'costPerUser' | i18n : (selectableProduct.seatPrice / 12 | currency:'$')}} /{{'month' | i18n}}
</span>
<span *ngIf="selectableProduct.product == productTypes.Free">{{'freeForever' | i18n}}</span>
</label>
</div>
<div class="form-check form-check-block" *ngIf="!ownedBusiness">
<input class="form-check-input" type="radio" name="PlanType" id="planFamilies" value="families"
[(ngModel)]="plan" (change)="changedPlan()">
<label class="form-check-label" for="planFamilies">
{{'planNameFamilies' | i18n}}
<small class="mb-1">{{'planDescFamilies' | i18n}}</small>
<small>• {{'addShareLimitedUsers' | i18n : '5'}}</small>
<small>• {{'createUnlimitedCollections' | i18n}}</small>
<small>• {{'gbEncryptedFileStorage' | i18n : '1 GB'}}</small>
<small>• {{'onPremHostingOptional' | i18n}}</small>
<small>• {{'priorityCustomerSupport' | i18n}}</small>
<small>• {{'xDayFreeTrial' | i18n : '7'}}</small>
<span>{{1 | currency:'$'}} /{{'month' | i18n}}, {{'includesXUsers' | i18n : 5}}</span>
</label>
</div>
<div class="form-check form-check-block">
<input class="form-check-input" type="radio" name="PlanType" id="planTeams" value="teams" [(ngModel)]="plan"
(change)="changedPlan()">
<label class="form-check-label" for="planTeams">
{{'planNameTeams' | i18n}}
<small class="mb-1">{{'planDescTeams' | i18n}}</small>
<small>• {{'addShareUnlimitedUsers' | i18n}}</small>
<small>• {{'createUnlimitedCollections' | i18n}}</small>
<small>• {{'gbEncryptedFileStorage' | i18n : '1 GB'}}</small>
<small>• {{'priorityCustomerSupport' | i18n}}</small>
<small>• {{'xDayFreeTrial' | i18n : '7'}}</small>
<span>{{5 | currency:'$'}} /{{'month' | i18n}}, {{'includesXUsers' | i18n : 5}},
{{('additionalUsers' | i18n).toLowerCase()}}
{{2 | currency:'$'}} /{{'month' | i18n}}</span>
</label>
</div>
<div class="form-check form-check-block">
<input class="form-check-input" type="radio" name="PlanType" id="planEnterprise" value="enterprise"
[(ngModel)]="plan" (change)="changedPlan()">
<label class="form-check-label" for="planEnterprise">
{{'planNameEnterprise' | i18n}}
<small class="mb-1">{{'planDescEnterprise' | i18n}}</small>
<small>• {{'addShareUnlimitedUsers' | i18n}}</small>
<small>• {{'createUnlimitedCollections' | i18n}}</small>
<small>• {{'gbEncryptedFileStorage' | i18n : '1 GB'}}</small>
<small>• {{'controlAccessWithGroups' | i18n}}</small>
<small>• {{'trackAuditLogs' | i18n}}</small>
<small>• {{'syncUsersFromDirectory' | i18n}}</small>
<small>• {{'onPremHostingOptional' | i18n}}</small>
<small>• {{'usersGetPremium' | i18n}}</small>
<small>• {{'priorityCustomerSupport' | i18n}}</small>
<small>• {{'xDayFreeTrial' | i18n : '7'}}</small>
<span>{{'costPerUser' | i18n : (3 | currency:'$')}} /{{'month' | i18n}}</span>
</label>
</div>
<ng-container *ngIf="!plans[plan].noPayment">
<ng-container *ngIf="!plans[plan].noAdditionalSeats && !plans[plan].baseSeats">
<div *ngIf="product !== productTypes.Free">
<ng-container *ngIf="selectedPlan.hasAdditionalSeatsOption && !selectedPlan.baseSeats">
<h2 class="mt-5">{{'users' | i18n}}</h2>
<div class="row">
<div class="col-6">
@@ -113,13 +110,13 @@
</div>
</ng-container>
<h2 class="mt-5">{{'addons' | i18n}}</h2>
<div class="row" *ngIf="!plans[plan].noAdditionalSeats && plans[plan].baseSeats">
<div class="row" *ngIf="selectedPlan.hasAdditionalSeatsOption && selectedPlan.baseSeats">
<div class="form-group col-6">
<label for="additionalSeats">{{'additionalUserSeats' | i18n}}</label>
<input id="additionalSeats" class="form-control" type="number" name="AdditionalSeats"
[(ngModel)]="additionalSeats" min="0" max="100000" placeholder="{{'userSeatsDesc' | i18n}}">
<small
class="text-muted form-text">{{'userSeatsAdditionalDesc' | i18n : plans[plan].baseSeats : (plans[plan].seatPrice | currency:'$')}}</small>
class="text-muted form-text">{{'userSeatsAdditionalDesc' | i18n : selectedPlan.baseSeats : (seatPriceMonthly(selectedPlan) | currency:'$')}}</small>
</div>
</div>
<div class="row">
@@ -129,11 +126,11 @@
[(ngModel)]="additionalStorage" min="0" max="99" step="1"
placeholder="{{'additionalStorageGbDesc' | i18n}}">
<small
class="text-muted form-text">{{'additionalStorageIntervalDesc' | i18n : '1 GB' : (storageGb.price | currency:'$') : ('month' | i18n)}}</small>
class="text-muted form-text">{{'additionalStorageIntervalDesc' | i18n : '1 GB' : (additionalStoragePriceMonthly(selectedPlan) | currency:'$') : ('month' | i18n)}}</small>
</div>
</div>
<div class="row">
<div class="form-group col-6" *ngIf="plans[plan].canBuyPremiumAccessAddon">
<div class="form-group col-6" *ngIf="selectedPlan.hasPremiumAccessOption">
<div class="form-check">
<input id="premiumAccess" class="form-check-input" type="checkbox" name="PremiumAccessAddon"
[(ngModel)]="premiumAccessAddon">
@@ -144,72 +141,91 @@
</div>
</div>
<h2 class="spaced-header">{{'summary' | i18n}}</h2>
<div class="form-check form-check-block">
<input class="form-check-input" type="radio" name="BillingInterval" id="intervalAnnually" value="year"
[(ngModel)]="interval">
<label class="form-check-label" for="intervalAnnually">
{{'annually' | i18n}}
<small *ngIf="plans[plan].annualBasePrice">
{{'basePrice' | i18n}}: {{plans[plan].basePrice | currency:'$'}} &times;12 {{'monthAbbr' | i18n}} =
{{baseTotal(true) | currency:'$'}}
/{{'year' | i18n}}
</small>
<small *ngIf="!plans[plan].noAdditionalSeats">
<span *ngIf="plans[plan].baseSeats">{{'additionalUsers' | i18n}}:</span>
<span *ngIf="!plans[plan].baseSeats">{{'users' | i18n}}:</span>
{{additionalSeats || 0}} &times; {{plans[plan].seatPrice | currency:'$'}} &times;12
{{'monthAbbr' | i18n}} = {{seatTotal(true)
<div class="form-check form-check-block" *ngFor="let selectablePlan of selectablePlans">
<input class="form-check-input" type="radio" name="BillingInterval" id="interval{{selectablePlan.type}}"
[value]="selectablePlan.type" [(ngModel)]="plan">
<label class="form-check-label" for="interval{{selectablePlan.type}}">
<ng-container *ngIf="selectablePlan.isAnnual">
{{'annually' | i18n}}
<small *ngIf="selectablePlan.basePrice">
{{'basePrice' | i18n}}: {{ selectablePlan.basePrice / 12 | currency:'$'}} &times; 12
{{'monthAbbr' | i18n}}
=
{{selectablePlan.basePrice | currency:'$'}}
/{{'year' | i18n}}
</small>
<small *ngIf="selectablePlan.hasAdditionalSeatsOption">
<span *ngIf="selectablePlan.baseSeats">{{'additionalUsers' | i18n}}:</span>
<span *ngIf="!selectablePlan.baseSeats">{{'users' | i18n}}:</span>
{{additionalSeats || 0}} &times; {{selectablePlan.seatPrice / 12 | currency:'$'}} &times; 12
{{'monthAbbr' | i18n}} = {{seatTotal(selectablePlan)
| currency:'$'}} /{{'year' | i18n}}
</small>
<small>
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} &times;
{{storageGb.price | currency:'$'}} &times;12 {{'monthAbbr'
| i18n}} = {{additionalStorageTotal(true) | currency:'$'}} /{{'year' | i18n}}
</small>
<small *ngIf="plans[plan].canBuyPremiumAccessAddon && premiumAccessAddon">
{{'premiumAccess' | i18n}}:
{{3.33 | currency:'$'}} &times;12 {{'monthAbbr' | i18n}} = {{40 | currency:'$'}} /{{'year' | i18n}}
</small>
</label>
</div>
<div class="form-check form-check-block" *ngIf="plans[plan].monthlySeatPrice">
<input class="form-check-input" type="radio" name="BillingInterval" id="intervalMonthly" value="month"
[(ngModel)]="interval">
<label class="form-check-label" for="intervalMonthly">
{{'monthly' | i18n}}
<small *ngIf="plans[plan].monthlyBasePrice">
{{'basePrice' | i18n}}: {{baseTotal(false) | currency:'$'}} /{{'month' | i18n}}
</small>
<small *ngIf="!plans[plan].noAdditionalSeats">
<span *ngIf="plans[plan].baseSeats">{{'additionalUsers' | i18n}}:</span>
<span *ngIf="!plans[plan].baseSeats">{{'users' | i18n}}:</span>
{{additionalSeats || 0}} &times; {{plans[plan].monthlySeatPrice | currency:'$'}} =
{{seatTotal(false) | currency:'$'}} /{{'month'
| i18n}}
</small>
<small>
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} &times;
{{storageGb.monthlyPrice | currency:'$'}} = {{additionalStorageTotal(false)
</small>
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} &times;
{{selectablePlan.additionalStoragePricePerGb / 12 | currency:'$'}} &times; 12 {{'monthAbbr'
| i18n}} = {{additionalStorageTotal(selectablePlan) | currency:'$'}}
/{{'year' | i18n}}
</small>
<small *ngIf="selectablePlan.hasPremiumAccessOption && premiumAccessAddon">
{{'premiumAccess' | i18n}}:
{{selectablePlan.premiumAccessOptionCost / 12 | currency:'$'}} &times; 12 {{'monthAbbr' | i18n}}
=
{{40 | currency:'$'}}
/{{'year' | i18n}}
</small>
</ng-container>
<ng-container *ngIf="!selectablePlan.isAnnual">
{{'monthly' | i18n}}
<small *ngIf="selectablePlan.basePrice">
{{'basePrice' | i18n}}: {{selectablePlan.basePrice | currency:'$'}} {{'monthAbbr' | i18n}}
=
{{selectablePlan.basePrice | currency:'$'}}
/{{'month' | i18n}}
</small>
<small *ngIf="selectablePlan.hasAdditionalSeatsOption">
<span *ngIf="selectablePlan.baseSeats">{{'additionalUsers' | i18n}}:</span>
<span *ngIf="!selectablePlan.baseSeats">{{'users' | i18n}}:</span>
{{additionalSeats || 0}} &times; {{selectablePlan.seatPrice | currency:'$'}}
{{'monthAbbr' | i18n}} = {{seatTotal(selectablePlan)
| currency:'$'}} /{{'month' | i18n}}
</small>
</small>
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} &times;
{{selectablePlan.additionalStoragePricePerGb | currency:'$'}} {{'monthAbbr'
| i18n}} = {{additionalStorageTotal(selectablePlan) | currency:'$'}}
/{{'month' | i18n}}
</small>
<small *ngIf="selectablePlan.hasPremiumAccessOption && premiumAccessAddon">
{{'premiumAccess' | i18n}}:
{{selectablePlan.premiumAccessOptionCost | currency:'$'}} {{'monthAbbr' | i18n}} =
{{40 | currency:'$'}}
/{{'month' | i18n}}
</small>
</ng-container>
</label>
</div>
<hr class="my-3">
<div class="text-lg">
<strong>{{'total' | i18n}}:</strong> {{total | currency:'USD $'}} /{{interval | i18n}}
<strong>{{'total' | i18n}}:</strong> {{subtotal | currency:'USD $'}} /{{selectedPlanInterval | i18n}}
</div>
<ng-container *ngIf="createOrganization">
<small class="text-muted font-italic">{{'paymentChargedWithTrial' | i18n : (interval | i18n) }}</small>
<small
class="text-muted font-italic">{{'paymentChargedWithTrial' | i18n : (selectedPlanInterval | i18n) }}</small>
<h2 class="spaced-header mb-4">{{'paymentInformation' | i18n}}</h2>
<app-payment [hideCredit]="true"></app-payment>
<app-tax-info (onCountryChanged)="changedCountry()"></app-tax-info>
</ng-container>
<ng-container *ngIf="!createOrganization">
<app-payment [showMethods]="false"></app-payment>
</ng-container>
<small class="text-muted font-italic mt-2 d-block" *ngIf="!createOrganization">
{{'paymentCharged' | i18n : (interval | i18n) }}</small>
</ng-container>
<div [ngClass]="{'mt-4': !createOrganization || plans[plan].noPayment}">
</div>
<div *ngIf="singleOrgPolicyBlock" class="mt-4">
<app-callout [type]="'error'">{{'singleOrgBlockCreateMessage' | i18n}}</app-callout>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'submit' | i18n}}</span>

View File

@@ -2,6 +2,7 @@ import {
Component,
EventEmitter,
Input,
OnInit,
Output,
ViewChild,
} from '@angular/core';
@@ -16,89 +17,196 @@ 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 { PolicyService } from 'jslib/abstractions/policy.service';
import { SyncService } from 'jslib/abstractions/sync.service';
import { PaymentComponent } from './payment.component';
import { TaxInfoComponent } from './tax-info.component';
import { PlanType } from 'jslib/enums/planType';
import { PolicyType } from 'jslib/enums/policyType';
import { ProductType } from 'jslib/enums/productType';
import { OrganizationCreateRequest } from 'jslib/models/request/organizationCreateRequest';
import { OrganizationUpgradeRequest } from 'jslib/models/request/organizationUpgradeRequest';
import { PlanResponse } from 'jslib/models/response/planResponse';
@Component({
selector: 'app-organization-plans',
templateUrl: 'organization-plans.component.html',
})
export class OrganizationPlansComponent {
export class OrganizationPlansComponent implements OnInit {
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@ViewChild(TaxInfoComponent) taxComponent: TaxInfoComponent;
@Input() organizationId: string;
@Input() showFree = true;
@Input() showCancel = false;
@Input() plan = 'free';
@Input() product: ProductType = ProductType.Free;
@Input() plan: PlanType = PlanType.Free;
@Output() onSuccess = new EventEmitter();
@Output() onCanceled = new EventEmitter();
selfHosted = false;
ownedBusiness = false;
premiumAccessAddon = false;
storageGbPriceMonthly = 0.33;
additionalStorage = 0;
additionalSeats = 0;
interval = 'year';
loading: boolean = true;
selfHosted: boolean = false;
ownedBusiness: boolean = false;
premiumAccessAddon: boolean = false;
additionalStorage: number = 0;
additionalSeats: number = 0;
name: string;
billingEmail: string;
businessName: string;
storageGb: any = {
price: 0.33,
monthlyPrice: 0.50,
yearlyPrice: 4,
};
plans: any = {
free: {
basePrice: 0,
noAdditionalSeats: true,
noPayment: true,
},
families: {
basePrice: 1,
annualBasePrice: 12,
baseSeats: 5,
noAdditionalSeats: true,
annualPlanType: PlanType.FamiliesAnnually,
canBuyPremiumAccessAddon: true,
},
teams: {
basePrice: 5,
annualBasePrice: 60,
monthlyBasePrice: 8,
baseSeats: 5,
seatPrice: 2,
annualSeatPrice: 24,
monthlySeatPrice: 2.5,
monthPlanType: PlanType.TeamsMonthly,
annualPlanType: PlanType.TeamsAnnually,
},
enterprise: {
seatPrice: 3,
annualSeatPrice: 36,
monthlySeatPrice: 4,
monthPlanType: PlanType.EnterpriseMonthly,
annualPlanType: PlanType.EnterpriseAnnually,
},
};
productTypes = ProductType;
formPromise: Promise<any>;
singleOrgPolicyBlock: boolean = false;
plans: PlanResponse[];
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
platformUtilsService: PlatformUtilsService, private cryptoService: CryptoService,
private router: Router, private syncService: SyncService) {
private router: Router, private syncService: SyncService,
private policyService: PolicyService) {
this.selfHosted = platformUtilsService.isSelfHost();
}
async ngOnInit() {
if (!this.selfHosted) {
const plans = await this.apiService.getPlans();
this.plans = plans.data;
}
this.loading = false;
}
get createOrganization() {
return this.organizationId == null;
}
get selectedPlan() {
return this.plans.find((plan) => plan.type === this.plan);
}
get selectedPlanInterval() {
return this.selectedPlan.isAnnual
? 'year'
: 'month';
}
get selectableProducts() {
let validPlans = this.plans.filter((plan) => plan.type !== PlanType.Custom);
if (this.ownedBusiness) {
validPlans = validPlans.filter((plan) => plan.canBeUsedByBusiness);
}
if (!this.showFree) {
validPlans = validPlans.filter((plan) => plan.product !== ProductType.Free);
}
validPlans = validPlans
.filter((plan) => !plan.legacyYear
&& !plan.disabled
&& (plan.isAnnual || plan.product === this.productTypes.Free));
return validPlans;
}
get selectablePlans() {
return this.plans.filter((plan) => !plan.legacyYear && !plan.disabled && plan.product === this.product);
}
additionalStoragePriceMonthly(selectedPlan: PlanResponse) {
if (!selectedPlan.isAnnual) {
return selectedPlan.additionalStoragePricePerGb;
}
return selectedPlan.additionalStoragePricePerGb / 12;
}
seatPriceMonthly(selectedPlan: PlanResponse) {
if (!selectedPlan.isAnnual) {
return selectedPlan.seatPrice;
}
return selectedPlan.seatPrice / 12;
}
additionalStorageTotal(plan: PlanResponse): number {
if (!plan.hasAdditionalStorageOption) {
return 0;
}
return plan.additionalStoragePricePerGb * Math.abs(this.additionalStorage || 0);
}
seatTotal(plan: PlanResponse): number {
if (!plan.hasAdditionalSeatsOption) {
return 0;
}
return plan.seatPrice * Math.abs(this.additionalSeats || 0);
}
get subtotal() {
let subTotal = this.selectedPlan.basePrice;
if (this.selectedPlan.hasAdditionalSeatsOption && this.additionalSeats) {
subTotal += this.seatTotal(this.selectedPlan);
}
if (this.selectedPlan.hasAdditionalStorageOption && this.additionalStorage) {
subTotal += this.additionalStorageTotal(this.selectedPlan);
}
if (this.selectedPlan.hasPremiumAccessOption && this.premiumAccessAddon) {
subTotal += this.selectedPlan.premiumAccessOptionPrice;
}
return subTotal;
}
changedProduct() {
this.plan = this.selectablePlans[0].type;
if (!this.selectedPlan.hasPremiumAccessOption) {
this.premiumAccessAddon = false;
}
if (!this.selectedPlan.hasAdditionalStorageOption) {
this.additionalStorage = 0;
}
if (!this.selectedPlan.hasAdditionalSeatsOption) {
this.additionalSeats = 0;
} else if (!this.additionalSeats && !this.selectedPlan.baseSeats &&
this.selectedPlan.hasAdditionalSeatsOption) {
this.additionalSeats = 1;
}
}
changedOwnedBusiness() {
if (!this.ownedBusiness || this.selectedPlan.canBeUsedByBusiness) {
return;
}
this.plan = PlanType.TeamsMonthly;
}
changedCountry() {
this.paymentComponent.hideBank = this.taxComponent.taxInfo.country !== 'US';
// Bank Account payments are only available for US customers
if (this.paymentComponent.hideBank &&
this.paymentComponent.method === PaymentMethodType.BankAccount) {
this.paymentComponent.method = PaymentMethodType.Card;
this.paymentComponent.changeMethod();
}
}
cancel() {
this.onCanceled.emit();
}
async submit() {
if (this.singleOrgPolicyBlock) {
return;
} else {
const policies = await this.policyService.getAll(PolicyType.SingleOrg);
this.singleOrgPolicyBlock = policies.some(policy => policy.enabled);
if (this.singleOrgPolicyBlock) {
return;
}
}
let files: FileList = null;
if (this.createOrganization && this.selfHosted) {
const fileEl = document.getElementById('file') as HTMLInputElement;
@@ -115,7 +223,7 @@ export class OrganizationPlansComponent {
let orgId: string = null;
if (this.createOrganization) {
let tokenResult: [string, PaymentMethodType] = null;
if (!this.selfHosted && this.plan !== 'free') {
if (!this.selfHosted && this.plan !== PlanType.Free) {
tokenResult = await this.paymentComponent.createPaymentToken();
}
const shareKey = await this.cryptoService.makeShareKey();
@@ -138,7 +246,7 @@ export class OrganizationPlansComponent {
request.name = this.name;
request.billingEmail = this.billingEmail;
if (this.plan === 'free') {
if (this.selectedPlan.type === PlanType.Free) {
request.planType = PlanType.Free;
} else {
request.paymentToken = tokenResult[0];
@@ -146,12 +254,17 @@ export class OrganizationPlansComponent {
request.businessName = this.ownedBusiness ? this.businessName : null;
request.additionalSeats = this.additionalSeats;
request.additionalStorageGb = this.additionalStorage;
request.premiumAccessAddon = this.plans[this.plan].canBuyPremiumAccessAddon &&
request.premiumAccessAddon = this.selectedPlan.hasPremiumAccessOption &&
this.premiumAccessAddon;
if (this.interval === 'month') {
request.planType = this.plans[this.plan].monthPlanType;
} else {
request.planType = this.plans[this.plan].annualPlanType;
request.planType = this.selectedPlan.type;
request.billingAddressPostalCode = this.taxComponent.taxInfo.postalCode;
request.billingAddressCountry = this.taxComponent.taxInfo.country;
if (this.taxComponent.taxInfo.includeTaxId) {
request.taxIdNumber = this.taxComponent.taxInfo.taxId;
request.billingAddressLine1 = this.taxComponent.taxInfo.line1;
request.billingAddressLine2 = this.taxComponent.taxInfo.line2;
request.billingAddressCity = this.taxComponent.taxInfo.city;
request.billingAddressState = this.taxComponent.taxInfo.state;
}
}
const response = await this.apiService.postOrganization(request);
@@ -162,13 +275,9 @@ export class OrganizationPlansComponent {
request.businessName = this.ownedBusiness ? this.businessName : null;
request.additionalSeats = this.additionalSeats;
request.additionalStorageGb = this.additionalStorage;
request.premiumAccessAddon = this.plans[this.plan].canBuyPremiumAccessAddon &&
request.premiumAccessAddon = this.selectedPlan.hasPremiumAccessOption &&
this.premiumAccessAddon;
if (this.interval === 'month') {
request.planType = this.plans[this.plan].monthPlanType;
} else {
request.planType = this.plans[this.plan].annualPlanType;
}
request.planType = this.selectedPlan.type;
const result = await this.apiService.postOrganizationUpgrade(this.organizationId, request);
if (!result.success && result.paymentIntentClientSecret != null) {
await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null);
@@ -197,78 +306,4 @@ export class OrganizationPlansComponent {
} catch { }
}
cancel() {
this.onCanceled.emit();
}
changedPlan() {
if (!this.plans[this.plan].canBuyPremiumAccessAddon) {
this.premiumAccessAddon = false;
}
if (this.plans[this.plan].monthPlanType == null) {
this.interval = 'year';
}
if (this.plans[this.plan].noAdditionalSeats) {
this.additionalSeats = 0;
} else if (!this.additionalSeats && !this.plans[this.plan].baseSeats &&
!this.plans[this.plan].noAdditionalSeats) {
this.additionalSeats = 1;
}
}
changedOwnedBusiness() {
if (!this.ownedBusiness || this.plan === 'teams' || this.plan === 'enterprise') {
return;
}
this.plan = 'teams';
}
additionalStorageTotal(annual: boolean): number {
if (annual) {
return Math.abs(this.additionalStorage || 0) * this.storageGb.yearlyPrice;
} else {
return Math.abs(this.additionalStorage || 0) * this.storageGb.monthlyPrice;
}
}
seatTotal(annual: boolean): number {
if (this.plans[this.plan].noAdditionalSeats) {
return 0;
}
if (annual) {
return this.plans[this.plan].annualSeatPrice * Math.abs(this.additionalSeats || 0);
} else {
return this.plans[this.plan].monthlySeatPrice * Math.abs(this.additionalSeats || 0);
}
}
baseTotal(annual: boolean): number {
if (annual) {
return Math.abs(this.plans[this.plan].annualBasePrice || 0);
} else {
return Math.abs(this.plans[this.plan].monthlyBasePrice || 0);
}
}
premiumAccessTotal(annual: boolean): number {
if (this.plans[this.plan].canBuyPremiumAccessAddon && this.premiumAccessAddon) {
if (annual) {
return 40;
}
}
return 0;
}
get total(): number {
const annual = this.interval === 'year';
return this.baseTotal(annual) + this.seatTotal(annual) + this.additionalStorageTotal(annual) +
this.premiumAccessTotal(annual);
}
get createOrganization() {
return this.organizationId == null;
}
}

View File

@@ -74,6 +74,17 @@
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
<ng-container *ngIf="o.useSso && o.identifier">
<a *ngIf="o.ssoBound; else linkSso" class="dropdown-item" href="#" appStopClick
(click)="unlinkSso(o)">
<i class="fa fa-fw fa-chain-broken" aria-hidden="true"></i>
{{'unlinkSso' | i18n}}
</a>
<ng-template #linkSso>
<app-link-sso [organization]="o">
</app-link-sso>
</ng-template>
</ng-container>
<a class="dropdown-item text-danger" href="#" appStopClick (click)="leave(o)">
<i class="fa fa-fw fa-sign-out" aria-hidden="true"></i>
{{'leave' | i18n}}

View File

@@ -35,6 +35,7 @@ export class OrganizationsComponent implements OnInit {
async ngOnInit() {
if (!this.vault) {
await this.syncService.fullSync(true);
await this.load();
}
}
@@ -46,6 +47,25 @@ export class OrganizationsComponent implements OnInit {
this.loaded = true;
}
async unlinkSso(org: Organization) {
const confirmed = await this.platformUtilsService.showDialog(
'Are you sure you want to unlink SSO for this organization?', org.name,
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
try {
this.actionPromise = this.apiService.deleteSsoUser(org.id).then(() => {
return this.syncService.fullSync(true);
});
await this.actionPromise;
this.analytics.eventTrack.next({ action: 'Unlinked SSO' });
this.toasterService.popAsync('success', null, 'Unlinked SSO');
await this.load();
} catch { }
}
async leave(org: Organization) {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('leaveOrganizationConfirmation'), org.name,

View File

@@ -39,13 +39,15 @@
<div id="stripe-card-expiry-element" class="form-control stripe-form-control"></div>
</div>
<div class="form-group col-4">
<label for="stripe-card-cvc-element" class="d-flex">
{{'securityCode' | i18n}}
<div class="d-flex">
<label for="stripe-card-cvc-element">
{{'securityCode' | i18n}}
</label>
<a href="https://www.cvvnumber.com/cvv.html" tabindex="-1" target="_blank" rel="noopener noreferrer"
class="ml-auto" appA11yTitle="{{'learnMore' | i18n}}">
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</label>
</div>
<div id="stripe-card-cvc-element" class="form-control stripe-form-control"></div>
</div>
</div>

View File

@@ -198,8 +198,8 @@ export class PaymentComponent implements OnInit {
return;
}
const handleCardPayment = () => this.showMethods ?
this.stripe.handleCardPayment(clientSecret, this.stripeCardNumberElement) :
this.stripe.handleCardPayment(clientSecret);
this.stripe.handleCardSetup(clientSecret, this.stripeCardNumberElement) :
this.stripe.handleCardSetup(clientSecret);
return handleCardPayment().then(async (result: any) => {
if (result.error) {
reject(result.error.message);

View File

@@ -75,6 +75,7 @@
<small class="text-muted font-italic">{{'paymentChargedAnnually' | i18n}}</small>
<h2 class="spaced-header mb-4">{{'paymentInformation' | i18n}}</h2>
<app-payment [hideBank]="true"></app-payment>
<app-tax-info></app-tax-info>
<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>

View File

@@ -17,6 +17,7 @@ import { TokenService } from 'jslib/abstractions/token.service';
import { UserService } from 'jslib/abstractions/user.service';
import { PaymentComponent } from './payment.component';
import { TaxInfoComponent } from './tax-info.component';
@Component({
selector: 'app-premium',
@@ -24,6 +25,7 @@ import { PaymentComponent } from './payment.component';
})
export class PremiumComponent implements OnInit {
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent;
canAccessPremium = false;
selfHosted = false;
@@ -83,6 +85,8 @@ export class PremiumComponent implements OnInit {
fd.append('paymentToken', result[0]);
}
fd.append('additionalStorageGb', (this.additionalStorage || 0).toString());
fd.append('country', this.taxInfoComponent.taxInfo.country);
fd.append('postalCode', this.taxInfoComponent.taxInfo.postalCode);
return this.apiService.postPremium(fd);
}).then((paymentResponse) => {
if (!paymentResponse.success && paymentResponse.paymentIntentClientSecret != null) {

View File

@@ -0,0 +1,313 @@
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="addressCountry">{{'country' | i18n}}</label>
<select id="addressCountry" class="form-control" [(ngModel)]="taxInfo.country" required name="addressCountry"
autocomplete="country" (change)="changeCountry()">
<option value="">-- Select --</option>
<option value="US">United States</option>
<option value="CN">China</option>
<option value="FR">France</option>
<option value="DE">Germany</option>
<option value="CA">Canada</option>
<option value="GB">United Kingdom</option>
<option value="AU">Australia</option>
<option value="IN">India</option>
<option value="-" disabled></option>
<option value="AF">Afghanistan</option>
<option value="AX">Åland Islands</option>
<option value="AL">Albania</option>
<option value="DZ">Algeria</option>
<option value="AS">American Samoa</option>
<option value="AD">Andorra</option>
<option value="AO">Angola</option>
<option value="AI">Anguilla</option>
<option value="AQ">Antarctica</option>
<option value="AG">Antigua and Barbuda</option>
<option value="AR">Argentina</option>
<option value="AM">Armenia</option>
<option value="AW">Aruba</option>
<option value="AT">Austria</option>
<option value="AZ">Azerbaijan</option>
<option value="BS">Bahamas</option>
<option value="BH">Bahrain</option>
<option value="BD">Bangladesh</option>
<option value="BB">Barbados</option>
<option value="BY">Belarus</option>
<option value="BE">Belgium</option>
<option value="BZ">Belize</option>
<option value="BJ">Benin</option>
<option value="BM">Bermuda</option>
<option value="BT">Bhutan</option>
<option value="BO">Bolivia, Plurinational State of</option>
<option value="BQ">Bonaire, Sint Eustatius and Saba</option>
<option value="BA">Bosnia and Herzegovina</option>
<option value="BW">Botswana</option>
<option value="BV">Bouvet Island</option>
<option value="BR">Brazil</option>
<option value="IO">British Indian Ocean Territory</option>
<option value="BN">Brunei Darussalam</option>
<option value="BG">Bulgaria</option>
<option value="BF">Burkina Faso</option>
<option value="BI">Burundi</option>
<option value="KH">Cambodia</option>
<option value="CM">Cameroon</option>
<option value="CV">Cape Verde</option>
<option value="KY">Cayman Islands</option>
<option value="CF">Central African Republic</option>
<option value="TD">Chad</option>
<option value="CL">Chile</option>
<option value="CX">Christmas Island</option>
<option value="CC">Cocos (Keeling) Islands</option>
<option value="CO">Colombia</option>
<option value="KM">Comoros</option>
<option value="CG">Congo</option>
<option value="CD">Congo, the Democratic Republic of the</option>
<option value="CK">Cook Islands</option>
<option value="CR">Costa Rica</option>
<option value="CI">Côte d'Ivoire</option>
<option value="HR">Croatia</option>
<option value="CU">Cuba</option>
<option value="CW">Curaçao</option>
<option value="CY">Cyprus</option>
<option value="CZ">Czech Republic</option>
<option value="DK">Denmark</option>
<option value="DJ">Djibouti</option>
<option value="DM">Dominica</option>
<option value="DO">Dominican Republic</option>
<option value="EC">Ecuador</option>
<option value="EG">Egypt</option>
<option value="SV">El Salvador</option>
<option value="GQ">Equatorial Guinea</option>
<option value="ER">Eritrea</option>
<option value="EE">Estonia</option>
<option value="ET">Ethiopia</option>
<option value="FK">Falkland Islands (Malvinas)</option>
<option value="FO">Faroe Islands</option>
<option value="FJ">Fiji</option>
<option value="FI">Finland</option>
<option value="GF">French Guiana</option>
<option value="PF">French Polynesia</option>
<option value="TF">French Southern Territories</option>
<option value="GA">Gabon</option>
<option value="GM">Gambia</option>
<option value="GE">Georgia</option>
<option value="GH">Ghana</option>
<option value="GI">Gibraltar</option>
<option value="GR">Greece</option>
<option value="GL">Greenland</option>
<option value="GD">Grenada</option>
<option value="GP">Guadeloupe</option>
<option value="GU">Guam</option>
<option value="GT">Guatemala</option>
<option value="GG">Guernsey</option>
<option value="GN">Guinea</option>
<option value="GW">Guinea-Bissau</option>
<option value="GY">Guyana</option>
<option value="HT">Haiti</option>
<option value="HM">Heard Island and McDonald Islands</option>
<option value="VA">Holy See (Vatican City State)</option>
<option value="HN">Honduras</option>
<option value="HK">Hong Kong</option>
<option value="HU">Hungary</option>
<option value="IS">Iceland</option>
<option value="ID">Indonesia</option>
<option value="IR">Iran, Islamic Republic of</option>
<option value="IQ">Iraq</option>
<option value="IE">Ireland</option>
<option value="IM">Isle of Man</option>
<option value="IL">Israel</option>
<option value="IT">Italy</option>
<option value="JM">Jamaica</option>
<option value="JP">Japan</option>
<option value="JE">Jersey</option>
<option value="JO">Jordan</option>
<option value="KZ">Kazakhstan</option>
<option value="KE">Kenya</option>
<option value="KI">Kiribati</option>
<option value="KP">Korea, Democratic People's Republic of</option>
<option value="KR">Korea, Republic of</option>
<option value="KW">Kuwait</option>
<option value="KG">Kyrgyzstan</option>
<option value="LA">Lao People's Democratic Republic</option>
<option value="LV">Latvia</option>
<option value="LB">Lebanon</option>
<option value="LS">Lesotho</option>
<option value="LR">Liberia</option>
<option value="LY">Libya</option>
<option value="LI">Liechtenstein</option>
<option value="LT">Lithuania</option>
<option value="LU">Luxembourg</option>
<option value="MO">Macao</option>
<option value="MK">Macedonia, the former Yugoslav Republic of</option>
<option value="MG">Madagascar</option>
<option value="MW">Malawi</option>
<option value="MY">Malaysia</option>
<option value="MV">Maldives</option>
<option value="ML">Mali</option>
<option value="MT">Malta</option>
<option value="MH">Marshall Islands</option>
<option value="MQ">Martinique</option>
<option value="MR">Mauritania</option>
<option value="MU">Mauritius</option>
<option value="YT">Mayotte</option>
<option value="MX">Mexico</option>
<option value="FM">Micronesia, Federated States of</option>
<option value="MD">Moldova, Republic of</option>
<option value="MC">Monaco</option>
<option value="MN">Mongolia</option>
<option value="ME">Montenegro</option>
<option value="MS">Montserrat</option>
<option value="MA">Morocco</option>
<option value="MZ">Mozambique</option>
<option value="MM">Myanmar</option>
<option value="NA">Namibia</option>
<option value="NR">Nauru</option>
<option value="NP">Nepal</option>
<option value="NL">Netherlands</option>
<option value="NC">New Caledonia</option>
<option value="NZ">New Zealand</option>
<option value="NI">Nicaragua</option>
<option value="NE">Niger</option>
<option value="NG">Nigeria</option>
<option value="NU">Niue</option>
<option value="NF">Norfolk Island</option>
<option value="MP">Northern Mariana Islands</option>
<option value="NO">Norway</option>
<option value="OM">Oman</option>
<option value="PK">Pakistan</option>
<option value="PW">Palau</option>
<option value="PS">Palestinian Territory, Occupied</option>
<option value="PA">Panama</option>
<option value="PG">Papua New Guinea</option>
<option value="PY">Paraguay</option>
<option value="PE">Peru</option>
<option value="PH">Philippines</option>
<option value="PN">Pitcairn</option>
<option value="PL">Poland</option>
<option value="PT">Portugal</option>
<option value="PR">Puerto Rico</option>
<option value="QA">Qatar</option>
<option value="RE">Réunion</option>
<option value="RO">Romania</option>
<option value="RU">Russian Federation</option>
<option value="RW">Rwanda</option>
<option value="BL">Saint Barthélemy</option>
<option value="SH">Saint Helena, Ascension and Tristan da Cunha</option>
<option value="KN">Saint Kitts and Nevis</option>
<option value="LC">Saint Lucia</option>
<option value="MF">Saint Martin (French part)</option>
<option value="PM">Saint Pierre and Miquelon</option>
<option value="VC">Saint Vincent and the Grenadines</option>
<option value="WS">Samoa</option>
<option value="SM">San Marino</option>
<option value="ST">Sao Tome and Principe</option>
<option value="SA">Saudi Arabia</option>
<option value="SN">Senegal</option>
<option value="RS">Serbia</option>
<option value="SC">Seychelles</option>
<option value="SL">Sierra Leone</option>
<option value="SG">Singapore</option>
<option value="SX">Sint Maarten (Dutch part)</option>
<option value="SK">Slovakia</option>
<option value="SI">Slovenia</option>
<option value="SB">Solomon Islands</option>
<option value="SO">Somalia</option>
<option value="ZA">South Africa</option>
<option value="GS">South Georgia and the South Sandwich Islands</option>
<option value="SS">South Sudan</option>
<option value="ES">Spain</option>
<option value="LK">Sri Lanka</option>
<option value="SD">Sudan</option>
<option value="SR">Suriname</option>
<option value="SJ">Svalbard and Jan Mayen</option>
<option value="SZ">Swaziland</option>
<option value="SE">Sweden</option>
<option value="CH">Switzerland</option>
<option value="SY">Syrian Arab Republic</option>
<option value="TW">Taiwan</option>
<option value="TJ">Tajikistan</option>
<option value="TZ">Tanzania, United Republic of</option>
<option value="TH">Thailand</option>
<option value="TL">Timor-Leste</option>
<option value="TG">Togo</option>
<option value="TK">Tokelau</option>
<option value="TO">Tonga</option>
<option value="TT">Trinidad and Tobago</option>
<option value="TN">Tunisia</option>
<option value="TR">Turkey</option>
<option value="TM">Turkmenistan</option>
<option value="TC">Turks and Caicos Islands</option>
<option value="TV">Tuvalu</option>
<option value="UG">Uganda</option>
<option value="UA">Ukraine</option>
<option value="AE">United Arab Emirates</option>
<option value="UM">United States Minor Outlying Islands</option>
<option value="UY">Uruguay</option>
<option value="UZ">Uzbekistan</option>
<option value="VU">Vanuatu</option>
<option value="VE">Venezuela, Bolivarian Republic of</option>
<option value="VN">Viet Nam</option>
<option value="VG">Virgin Islands, British</option>
<option value="VI">Virgin Islands, U.S.</option>
<option value="WF">Wallis and Futuna</option>
<option value="EH">Western Sahara</option>
<option value="YE">Yemen</option>
<option value="ZM">Zambia</option>
<option value="ZW">Zimbabwe</option>
</select>
</div>
</div>
<div class="col-3">
<div class="form-group">
<label for="addressPostalCode">{{'zipPostalCode' | i18n}}</label>
<input id="addressPostalCode" class="form-control" type="text" name="addressPostalCode"
[(ngModel)]="taxInfo.postalCode" [required]="taxInfo.country === 'US'" autocomplete="postal-code">
</div>
</div>
<div class="col-6" *ngIf="organizationId && taxInfo.country !== 'US'">
<div class="form-group form-check">
<input class="form-check-input" id="addressIncludeTaxId" name="addressIncludeTaxId" type="checkbox"
[(ngModel)]="taxInfo.includeTaxId">
<label class="form-check-label" for="addressIncludeTaxId">{{'includeVAT' | i18n}}</label>
</div>
</div>
</div>
<div class="row" *ngIf="organizationId && taxInfo.includeTaxId">
<div class="col-6">
<div class="form-group">
<label for="taxId">{{'taxIdNumber' | i18n}}</label>
<input id="taxId" class="form-control" type="text" name="taxId" [(ngModel)]="taxInfo.taxId">
</div>
</div>
</div>
<div class="row" *ngIf="organizationId && taxInfo.includeTaxId">
<div class="col-6">
<div class="form-group">
<label for="addressLine1">{{'address1' | i18n}}</label>
<input id="addressLine1" class="form-control" type="text" name="addressLine1"
[(ngModel)]="taxInfo.line1" autocomplete="address-line1">
</div>
</div>
<div class="col-6">
<div class="form-group">
<label for="addressLine2">{{'address2' | i18n}}</label>
<input id="addressLine2" class="form-control" type="text" name="addressLine2"
[(ngModel)]="taxInfo.line2" autocomplete="address-line2">
</div>
</div>
<div class="col-6">
<div class="form-group">
<label for="addressCity">{{'cityTown' | i18n}}</label>
<input id="addressCity" class="form-control" type="text" name="addressCity"
[(ngModel)]="taxInfo.city" autocomplete="address-level2">
</div>
</div>
<div class="col-6">
<div class="form-group">
<label for="addressState">{{'stateProvince' | i18n}}</label>
<input id="addressState" class="form-control" type="text" name="addressState"
[(ngModel)]="taxInfo.state" autocomplete="address-level1">
</div>
</div>
</div>

View File

@@ -0,0 +1,132 @@
import {
Component,
EventEmitter,
Output,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ApiService } from 'jslib/abstractions/api.service';
import { OrganizationTaxInfoUpdateRequest } from 'jslib/models/request/organizationTaxInfoUpdateRequest';
import { TaxInfoUpdateRequest } from 'jslib/models/request/taxInfoUpdateRequest';
@Component({
selector: 'app-tax-info',
templateUrl: 'tax-info.component.html',
})
export class TaxInfoComponent {
@Output() onCountryChanged = new EventEmitter();
loading: boolean = true;
organizationId: string;
taxInfo: any = {
taxId: null,
line1: null,
line2: null,
city: null,
state: null,
postalCode: null,
country: 'US',
includeTaxId: false,
};
private pristine: any = {
taxId: null,
line1: null,
line2: null,
city: null,
state: null,
postalCode: null,
country: 'US',
includeTaxId: false,
};
constructor(private apiService: ApiService, private route: ActivatedRoute) { }
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
if (this.organizationId) {
try {
const taxInfo = await this.apiService.getOrganizationTaxInfo(this.organizationId);
if (taxInfo) {
this.taxInfo.taxId = taxInfo.taxId;
this.taxInfo.state = taxInfo.state;
this.taxInfo.line1 = taxInfo.line1;
this.taxInfo.line2 = taxInfo.line2;
this.taxInfo.city = taxInfo.city;
this.taxInfo.state = taxInfo.state;
this.taxInfo.postalCode = taxInfo.postalCode;
this.taxInfo.country = taxInfo.country || 'US';
this.taxInfo.includeTaxId = this.taxInfo.country !== 'US' && (
!!taxInfo.taxId
|| !!taxInfo.line1
|| !!taxInfo.line2
|| !!taxInfo.city
|| !!taxInfo.state);
}
} catch { }
} else {
const taxInfo = await this.apiService.getTaxInfo();
if (taxInfo) {
this.taxInfo.postalCode = taxInfo.postalCode;
this.taxInfo.country = taxInfo.country || 'US';
}
}
this.pristine = Object.assign({}, this.taxInfo);
// If not the default (US) then trigger onCountryChanged
if (this.taxInfo.country !== 'US') {
this.onCountryChanged.emit();
}
});
this.loading = false;
}
getTaxInfoRequest(): TaxInfoUpdateRequest {
if (this.organizationId) {
const request = new OrganizationTaxInfoUpdateRequest();
request.taxId = this.taxInfo.taxId;
request.state = this.taxInfo.state;
request.line1 = this.taxInfo.line1;
request.line2 = this.taxInfo.line2;
request.city = this.taxInfo.city;
request.state = this.taxInfo.state;
request.postalCode = this.taxInfo.postalCode;
request.country = this.taxInfo.country;
return request;
} else {
const request = new TaxInfoUpdateRequest();
request.postalCode = this.taxInfo.postalCode;
request.country = this.taxInfo.country;
return request;
}
}
submitTaxInfo(): Promise<any> {
if (!this.hasChanged()) {
return new Promise((resolve) => { resolve(); });
}
const request = this.getTaxInfoRequest();
return this.organizationId ? this.apiService.putOrganizationTaxInfo(this.organizationId,
request as OrganizationTaxInfoUpdateRequest) : this.apiService.putTaxInfo(request);
}
changeCountry() {
if (this.taxInfo.country === 'US') {
this.taxInfo.includeTaxId = false;
this.taxInfo.taxId = null;
this.taxInfo.line1 = null;
this.taxInfo.line2 = null;
this.taxInfo.city = null;
this.taxInfo.state = null;
}
this.onCountryChanged.emit();
}
private hasChanged(): boolean {
for (const key in this.taxInfo) {
if (this.pristine.hasOwnProperty(key) && this.pristine[key] !== this.taxInfo[key]) {
return true;
}
}
return false;
}
}

View File

@@ -1,4 +1,5 @@
import {
Directive,
EventEmitter,
Output,
} from '@angular/core';
@@ -13,6 +14,7 @@ import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType';
import { TwoFactorProviderRequest } from 'jslib/models/request/twoFactorProviderRequest';
@Directive()
export abstract class TwoFactorBaseComponent {
@Output() onUpdated = new EventEmitter<boolean>();

View File

@@ -31,12 +31,12 @@ import { TwoFactorYubiKeyComponent } from './two-factor-yubikey.component';
templateUrl: 'two-factor-setup.component.html',
})
export class TwoFactorSetupComponent implements OnInit {
@ViewChild('recoveryTemplate', { read: ViewContainerRef }) recoveryModalRef: ViewContainerRef;
@ViewChild('authenticatorTemplate', { read: ViewContainerRef }) authenticatorModalRef: ViewContainerRef;
@ViewChild('yubikeyTemplate', { read: ViewContainerRef }) yubikeyModalRef: ViewContainerRef;
@ViewChild('u2fTemplate', { read: ViewContainerRef }) u2fModalRef: ViewContainerRef;
@ViewChild('duoTemplate', { read: ViewContainerRef }) duoModalRef: ViewContainerRef;
@ViewChild('emailTemplate', { read: ViewContainerRef }) emailModalRef: ViewContainerRef;
@ViewChild('recoveryTemplate', { read: ViewContainerRef, static: true }) recoveryModalRef: ViewContainerRef;
@ViewChild('authenticatorTemplate', { read: ViewContainerRef, static: true }) authenticatorModalRef: ViewContainerRef;
@ViewChild('yubikeyTemplate', { read: ViewContainerRef, static: true }) yubikeyModalRef: ViewContainerRef;
@ViewChild('u2fTemplate', { read: ViewContainerRef, static: true }) u2fModalRef: ViewContainerRef;
@ViewChild('duoTemplate', { read: ViewContainerRef, static: true }) duoModalRef: ViewContainerRef;
@ViewChild('emailTemplate', { read: ViewContainerRef, static: true }) emailModalRef: ViewContainerRef;
organizationId: string;
providers: any[] = [];

View File

@@ -1,5 +1,6 @@
import {
ComponentFactoryResolver,
Directive,
ViewChild,
ViewContainerRef,
} from '@angular/core';
@@ -15,8 +16,9 @@ import { AddEditComponent } from '../vault/add-edit.component';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { UserService } from 'jslib/abstractions/user.service';
@Directive()
export class CipherReportComponent {
@ViewChild('cipherAddEdit', { read: ViewContainerRef }) cipherAddEditModalRef: ViewContainerRef;
@ViewChild('cipherAddEdit', { read: ViewContainerRef, static: true }) cipherAddEditModalRef: ViewContainerRef;
loading = false;
hasLoaded = false;
@@ -62,6 +64,10 @@ export class CipherReportComponent {
this.modal.close();
await this.load();
});
childComponent.onRestoredCipher.subscribe(async (c: CipherView) => {
this.modal.close();
await this.load();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;

View File

@@ -41,7 +41,7 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple
async setCiphers() {
const allCiphers = await this.getAllCiphers();
const exposedPasswordCiphers: CipherView[] = [];
const promises: Array<Promise<void>> = [];
const promises: Promise<void>[] = [];
allCiphers.forEach((c) => {
if (c.type !== CipherType.Login || c.login.password == null || c.login.password === '') {
return;

View File

@@ -233,6 +233,9 @@
Once syncing of your data is complete, the download icon in the top right corner will turn pink. Click
the download icon and save the CSV file.
</ng-container>
<ng-container *ngIf="format === 'yoticsv'">
From the Yoti browser extension, click on "Settings", then "Export Saved Logins" and save the CSV file.
</ng-container>
</app-callout>
<div class="row">
<div class="col-6">

View File

@@ -42,7 +42,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
if (this.services.size > 0) {
const allCiphers = await this.getAllCiphers();
const inactive2faCiphers: CipherView[] = [];
const promises: Array<Promise<void>> = [];
const promises: Promise<void>[] = [];
const docs = new Map<string, string>();
allCiphers.forEach((c) => {
if (c.type !== CipherType.Login || (c.login.totp != null && c.login.totp !== '') || !c.login.hasUris) {

View File

@@ -17,7 +17,7 @@
<div class="ml-auto">
<button class="btn btn-link" appA11yTitle="{{'copyPassword' | i18n}}"
(click)="copy(h.password)">
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</li>

View File

@@ -21,7 +21,7 @@ import { PasswordGeneratorHistoryComponent } from './password-generator-history.
templateUrl: 'password-generator.component.html',
})
export class PasswordGeneratorComponent extends BasePasswordGeneratorComponent {
@ViewChild('historyTemplate', { read: ViewContainerRef }) historyModalRef: ViewContainerRef;
@ViewChild('historyTemplate', { read: ViewContainerRef, static: true }) historyModalRef: ViewContainerRef;
private modal: ModalComponent = null;

View File

@@ -12,7 +12,8 @@
<div class="row" *ngIf="!editMode">
<div class="col-6 form-group">
<label for="type">{{'whatTypeOfItem' | i18n}}</label>
<select id="type" name="Type" [(ngModel)]="cipher.type" class="form-control">
<select id="type" name="Type" [(ngModel)]="cipher.type" class="form-control"
[disabled]="cipher.isDeleted" appAutofocus>
<option *ngFor="let o of typeOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
</div>
@@ -21,11 +22,12 @@
<div class="col-6 form-group">
<label for="name">{{'name' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="cipher.name"
required>
required [disabled]="cipher.isDeleted">
</div>
<div class="col-6 form-group" *ngIf="!organization">
<label for="folder">{{'folder' | i18n}}</label>
<select id="folder" name="FolderId" [(ngModel)]="cipher.folderId" class="form-control">
<select id="folder" name="FolderId" [(ngModel)]="cipher.folderId" class="form-control"
[disabled]="cipher.isDeleted">
<option *ngFor="let f of folders" [ngValue]="f.id">{{f.name}}</option>
</select>
</div>
@@ -37,12 +39,12 @@
<label for="loginUsername">{{'username' | i18n}}</label>
<div class="input-group">
<input id="loginUsername" class="form-control" type="text" name="Login.Username"
[(ngModel)]="cipher.login.username" appInputVerbatim>
<div class="input-group-append">
[(ngModel)]="cipher.login.username" appInputVerbatim [disabled]="cipher.isDeleted">
<div class="input-group-append" *ngIf="!cipher.isDeleted">
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'copyUsername' | i18n}}"
(click)="copy(cipher.login.username, 'username', 'Username')" tabindex="-1">
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
@@ -50,9 +52,10 @@
<div class="col-6 form-group">
<div class="d-flex">
<label for="loginPassword">{{'password' | i18n}}</label>
<div class="ml-auto d-flex">
<div class="ml-auto d-flex" *ngIf="!cipher.isDeleted">
<a href="#" class="d-block mr-2" appStopClick
appA11yTitle="{{'generatePassword' | i18n}}" (click)="generatePassword()">
appA11yTitle="{{'generatePassword' | i18n}}" (click)="generatePassword()"
*ngIf="cipher.viewPassword">
<i class="fa fa-lg fa-fw fa-refresh" aria-hidden="true"></i>
</a>
<a href="#" class="d-block" #checkPasswordBtn appStopClick
@@ -68,18 +71,20 @@
<div class="input-group">
<input id="loginPassword" class="form-control text-monospace"
type="{{showPassword ? 'text' : 'password'}}" name="Login.Password"
[(ngModel)]="cipher.login.password" appInputVerbatim autocomplete="new-password">
[(ngModel)]="cipher.login.password" appInputVerbatim autocomplete="new-password"
[disabled]="cipher.isDeleted || !cipher.viewPassword">
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword()"
tabindex="-1">
tabindex="-1" [disabled]="!cipher.viewPassword">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'copyPassword' | i18n}}"
(click)="copy(cipher.login.password, 'password', 'Password')" tabindex="-1">
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
(click)="copy(cipher.login.password, 'password', 'Password')" tabindex="-1"
[disabled]="!cipher.viewPassword">
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
@@ -88,8 +93,8 @@
<div class="row">
<div class="col-6 form-group">
<label for="loginTotp">{{'authenticatorKeyTotp' | i18n}}</label>
<input id="loginTotp" type="text" name="Login.Totp" class="form-control text-monospace"
[(ngModel)]="cipher.login.totp" appInputVerbatim>
<input id="loginTotp" type="{{cipher.viewPassword ? 'text' : 'password'}}" name="Login.Totp" class="form-control text-monospace"
[(ngModel)]="cipher.login.totp" appInputVerbatim [disabled]="cipher.isDeleted || !cipher.viewPassword">
</div>
<div class="col-6 form-group totp d-flex align-items-end" [ngClass]="{'low': totpLow}">
<div *ngIf="!cipher.login.totp || !totpCode">
@@ -121,7 +126,7 @@
<button type="button" class="btn btn-link"
appA11yTitle="{{'copyVerificationCode' | i18n}}"
(click)="copy(totpCode, 'verificationCodeTotp', 'TOTP')">
<i class="fa fa-clipboard" aria-hidden="true"></i>
<i class="fa fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
@@ -132,7 +137,7 @@
<label for="loginUri{{i}}">{{'uriPosition' | i18n : (i + 1)}}</label>
<div class="input-group">
<input class="form-control" id="loginUri{{i}}" type="text"
name="Login.Uris[{{i}}].Uri" [(ngModel)]="u.uri"
name="Login.Uris[{{i}}].Uri" [(ngModel)]="u.uri" [disabled]="cipher.isDeleted"
placeholder="{{'ex' | i18n}} https://google.com" appInputVerbatim>
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary"
@@ -143,7 +148,7 @@
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'copyUri' | i18n}}" (click)="copy(u.uri, 'uri', 'URI')"
tabindex="-1">
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
@@ -160,19 +165,20 @@
</div>
<div class="d-flex">
<select class="form-control" id="loginUriMatch{{i}}" name="Login.Uris[{{i}}].Match"
[(ngModel)]="u.match" (change)="loginUriMatchChanged(u)">
[(ngModel)]="u.match" (change)="loginUriMatchChanged(u)"
[disabled]="cipher.isDeleted">
<option *ngFor="let o of uriMatchOptions" [ngValue]="o.value">{{o.name}}
</option>
</select>
<button type="button" class="btn btn-link text-danger ml-2" (click)="removeUri(u)"
appA11yTitle="{{'remove' | i18n}}">
appA11yTitle="{{'remove' | i18n}}" *ngIf="!cipher.isDeleted">
<i class="fa fa-minus-circle fa-lg" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</ng-container>
<a href="#" appStopClick (click)="addUri()" class="d-inline-block mb-3">
<a href="#" appStopClick (click)="addUri()" class="d-inline-block mb-3" *ngIf="!cipher.isDeleted">
<i class="fa fa-plus-circle fa-fw" aria-hidden="true"></i> {{'newUri' | i18n}}
</a>
</ng-container>
@@ -182,12 +188,13 @@
<div class="col-6 form-group">
<label for="cardCardholderName">{{'cardholderName' | i18n}}</label>
<input id="cardCardholderName" class="form-control" type="text"
name="Card.CardCardholderName" [(ngModel)]="cipher.card.cardholderName">
name="Card.CardCardholderName" [(ngModel)]="cipher.card.cardholderName"
[disabled]="cipher.isDeleted">
</div>
<div class="col-6 form-group">
<label for="cardBrand">{{'brand' | i18n}}</label>
<select id="cardBrand" class="form-control" name="Card.Brand"
[(ngModel)]="cipher.card.brand">
[(ngModel)]="cipher.card.brand" [disabled]="cipher.isDeleted">
<option *ngFor="let o of cardBrandOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
</div>
@@ -197,12 +204,12 @@
<label for="cardNumber">{{'number' | i18n}}</label>
<div class="input-group">
<input id="cardNumber" class="form-control" type="text" name="Card.Number"
[(ngModel)]="cipher.card.number" appInputVerbatim>
[(ngModel)]="cipher.card.number" appInputVerbatim [disabled]="cipher.isDeleted">
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'copyNumber' | i18n}}"
(click)="copy(cipher.card.number, 'number', 'Number')" tabindex="-1">
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
@@ -210,14 +217,15 @@
<div class="col form-group">
<label for="cardExpMonth">{{'expirationMonth' | i18n}}</label>
<select id="cardExpMonth" class="form-control" name="Card.ExpMonth"
[(ngModel)]="cipher.card.expMonth">
[(ngModel)]="cipher.card.expMonth" [disabled]="cipher.isDeleted">
<option *ngFor="let o of cardExpMonthOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
</div>
<div class="col form-group">
<label for="cardExpYear">{{'expirationYear' | i18n}}</label>
<input id="cardExpYear" class="form-control" type="text" name="Card.ExpYear"
[(ngModel)]="cipher.card.expYear" placeholder="{{'ex' | i18n}} 2019">
[(ngModel)]="cipher.card.expYear" placeholder="{{'ex' | i18n}} 2019"
[disabled]="cipher.isDeleted">
</div>
</div>
<div class="row">
@@ -226,7 +234,8 @@
<div class="input-group">
<input id="cardCode" class="form-control text-monospace"
type="{{showCardCode ? 'text' : 'password'}}" name="Card.Code"
[(ngModel)]="cipher.card.code" appInputVerbatim autocomplete="new-password">
[(ngModel)]="cipher.card.code" appInputVerbatim autocomplete="new-password"
[disabled]="cipher.isDeleted">
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="toggleCardCode()"
@@ -237,7 +246,7 @@
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'securityCode' | i18n}}"
(click)="copy(cipher.card.code, 'securityCode', 'Security Code')" tabindex="-1">
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
@@ -250,7 +259,7 @@
<div class="col-4 form-group">
<label for="idTitle">{{'title' | i18n}}</label>
<select id="idTitle" class="form-control" name="Identity.Title"
[(ngModel)]="cipher.identity.title">
[(ngModel)]="cipher.identity.title" [disabled]="cipher.isDeleted">
<option *ngFor="let o of identityTitleOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
</div>
@@ -259,107 +268,107 @@
<div class="col-4 form-group">
<label for="idFirstName">{{'firstName' | i18n}}</label>
<input id="idFirstName" class="form-control" type="text" name="Identity.FirstName"
[(ngModel)]="cipher.identity.firstName">
[(ngModel)]="cipher.identity.firstName" [disabled]="cipher.isDeleted">
</div>
<div class="col-4 form-group">
<label for="idMiddleName">{{'middleName' | i18n}}</label>
<input id="idMiddleName" class="form-control" type="text" name="Identity.MiddleName"
[(ngModel)]="cipher.identity.middleName">
[(ngModel)]="cipher.identity.middleName" [disabled]="cipher.isDeleted">
</div>
<div class="col-4 form-group">
<label for="idLastName">{{'lastName' | i18n}}</label>
<input id="idLastName" class="form-control" type="text" name="Identity.LastName"
[(ngModel)]="cipher.identity.lastName">
[(ngModel)]="cipher.identity.lastName" [disabled]="cipher.isDeleted">
</div>
</div>
<div class="row">
<div class="col-4 form-group">
<label for="idUsername">{{'username' | i18n}}</label>
<input id="idUsername" class="form-control" type="text" name="Identity.Username"
[(ngModel)]="cipher.identity.username" appInputVerbatim>
[(ngModel)]="cipher.identity.username" appInputVerbatim [disabled]="cipher.isDeleted">
</div>
<div class="col-4 form-group">
<label for="idCompany">{{'company' | i18n}}</label>
<input id="idCompany" class="form-control" type="text" name="Identity.Company"
[(ngModel)]="cipher.identity.company">
[(ngModel)]="cipher.identity.company" [disabled]="cipher.isDeleted">
</div>
</div>
<div class="row">
<div class="col-4 form-group">
<label for="idSsn">{{'ssn' | i18n}}</label>
<input id="idSsn" class="form-control" type="text" name="Identity.SSN"
[(ngModel)]="cipher.identity.ssn" appInputVerbatim>
[(ngModel)]="cipher.identity.ssn" appInputVerbatim [disabled]="cipher.isDeleted">
</div>
<div class="col-4 form-group">
<label for="idPassportNumber">{{'passportNumber' | i18n}}</label>
<input id="idPassportNumber" class="form-control" type="text" name="Identity.PassportNumber"
[(ngModel)]="cipher.identity.passportNumber" appInputVerbatim>
[(ngModel)]="cipher.identity.passportNumber" appInputVerbatim [disabled]="cipher.isDeleted">
</div>
<div class="col-4 form-group">
<label for="idLicenseNumber">{{'licenseNumber' | i18n}}</label>
<input id="idLicenseNumber" class="form-control" type="text" name="Identity.LicenseNumber"
[(ngModel)]="cipher.identity.licenseNumber" appInputVerbatim>
[(ngModel)]="cipher.identity.licenseNumber" appInputVerbatim [disabled]="cipher.isDeleted">
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="idEmail">{{'email' | i18n}}</label>
<input id="idEmail" class="form-control" type="text" name="Identity.Email"
[(ngModel)]="cipher.identity.email" appInputVerbatim>
[(ngModel)]="cipher.identity.email" appInputVerbatim [disabled]="cipher.isDeleted">
</div>
<div class="col-6 form-group">
<label for="idPhone">{{'phone' | i18n}}</label>
<input id="idPhone" class="form-control" type="text" name="Identity.Phone"
[(ngModel)]="cipher.identity.phone">
[(ngModel)]="cipher.identity.phone" [disabled]="cipher.isDeleted">
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="idAddress1">{{'address1' | i18n}}</label>
<input id="idAddress1" class="form-control" type="text" name="Identity.Address1"
[(ngModel)]="cipher.identity.address1">
[(ngModel)]="cipher.identity.address1" [disabled]="cipher.isDeleted">
</div>
<div class="col-6 form-group">
<label for="idAddress2">{{'address2' | i18n}}</label>
<input id="idAddress2" class="form-control" type="text" name="Identity.Address2"
[(ngModel)]="cipher.identity.address2">
[(ngModel)]="cipher.identity.address2" [disabled]="cipher.isDeleted">
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="idAddress3">{{'address3' | i18n}}</label>
<input id="idAddress3" class="form-control" type="text" name="Identity.Address3"
[(ngModel)]="cipher.identity.address3">
[(ngModel)]="cipher.identity.address3" [disabled]="cipher.isDeleted">
</div>
<div class="col-6 form-group">
<label for="idCity">{{'cityTown' | i18n}}</label>
<input id="idCity" class="form-control" type="text" name="Identity.City"
[(ngModel)]="cipher.identity.city">
[(ngModel)]="cipher.identity.city" [disabled]="cipher.isDeleted">
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="idState">{{'stateProvince' | i18n}}</label>
<input id="idState" class="form-control" type="text" name="Identity.State"
[(ngModel)]="cipher.identity.state">
[(ngModel)]="cipher.identity.state" [disabled]="cipher.isDeleted">
</div>
<div class="col-6 form-group">
<label for="idPostalCode">{{'zipPostalCode' | i18n}}</label>
<input id="idPostalCode" class="form-control" type="text" name="Identity.PostalCode"
[(ngModel)]="cipher.identity.postalCode">
[(ngModel)]="cipher.identity.postalCode" [disabled]="cipher.isDeleted">
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="idCountry">{{'country' | i18n}}</label>
<input id="idCountry" class="form-control" type="text" name="Identity.Country"
[(ngModel)]="cipher.identity.country">
[(ngModel)]="cipher.identity.country" [disabled]="cipher.isDeleted">
</div>
</div>
</ng-container>
<div class="form-group">
<label for="notes">{{'notes' | i18n}}</label>
<textarea id="notes" name="Notes" rows="6" [(ngModel)]="cipher.notes"
<textarea id="notes" name="Notes" rows="6" [(ngModel)]="cipher.notes" [disabled]="cipher.isDeleted"
class="form-control"></textarea>
</div>
<h3 class="mt-4">{{'customFields' | i18n}}</h3>
@@ -374,19 +383,19 @@
</a>
</div>
<input id="fieldName{{i}}" type="text" name="Field.Name{{i}}" [(ngModel)]="f.name"
class="form-control" appInputVerbatim>
class="form-control" appInputVerbatim [disabled]="cipher.isDeleted">
</div>
<div class="col-7 form-group">
<label for="fieldValue{{i}}">{{'value' | i18n}}</label>
<div class="d-flex align-items-center">
<div class="input-group" *ngIf="f.type === fieldType.Text">
<input id="fieldValue{{i}}" class="form-control" type="text" name="Field.Value{{i}}"
[(ngModel)]="f.value" appInputVerbatim>
[(ngModel)]="f.value" appInputVerbatim [disabled]="cipher.isDeleted">
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'copyValue' | i18n}}"
(click)="copy(f.value, 'value', 'Field')" tabindex="-1">
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
@@ -394,11 +403,11 @@
<input id="fieldValue{{i}}" type="{{f.showValue ? 'text' : 'password'}}"
name="Field.Value{{i}}" [(ngModel)]="f.value"
class="form-control text-monospace" appInputVerbatim
autocomplete="new-password">
autocomplete="new-password" [disabled]="cipher.isDeleted || (!cipher.viewPassword && !f.newField)">
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="toggleFieldValue(f)"
tabindex="-1">
tabindex="-1" [disabled]="!cipher.viewPassword && !f.newField">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !f.showValue, 'fa-eye-slash': f.showValue}">
</i>
@@ -406,32 +415,32 @@
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'copyValue' | i18n}}"
(click)="copy(f.value, 'value', f.type === fieldType.Hidden ? 'H_Field' : 'Field')"
tabindex="-1">
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
tabindex="-1" [disabled]="!cipher.viewPassword && !f.newField">
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="flex-fill">
<input id="fieldValue{{i}}" name="Field.Value{{i}}" type="checkbox"
[(ngModel)]="f.value" *ngIf="f.type === fieldType.Boolean" appTrueFalseValue
trueValue="true" falseValue="false">
trueValue="true" falseValue="false" [disabled]="cipher.isDeleted">
</div>
<button type="button" class="btn btn-link text-danger ml-2" (click)="removeField(f)"
appA11yTitle="{{'remove' | i18n}}">
appA11yTitle="{{'remove' | i18n}}" *ngIf="!cipher.isDeleted">
<i class="fa fa-minus-circle fa-lg" aria-hidden="true"></i>
</button>
<button type="button" class="btn btn-link text-muted cursor-move"
appA11yTitle="{{'dragToSort' | i18n}}">
appA11yTitle="{{'dragToSort' | i18n}}" *ngIf="!cipher.isDeleted">
<i class="fa fa-bars fa-lg" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</div>
<a href="#" appStopClick (click)="addField()" class="d-inline-block mb-2">
<a href="#" appStopClick (click)="addField()" class="d-inline-block mb-2" *ngIf="!cipher.isDeleted">
<i class="fa fa-plus-circle fa-fw" aria-hidden="true"></i> {{'newCustomField' | i18n}}
</a>
<div class="row">
<div class="row" *ngIf="!cipher.isDeleted">
<div class="col-5">
<label for="addFieldType" class="sr-only">{{'type' | i18n}}</label>
<select id="addFieldType" class="form-control" name="AddFieldType" [(ngModel)]="addFieldType">
@@ -445,7 +454,8 @@
<div class="col-5">
<label for="organizationId">{{'whoOwnsThisItem' | i18n}}</label>
<select id="organizationId" class="form-control" name="OrganizationId"
[(ngModel)]="cipher.organizationId" (change)="organizationChanged()">
[(ngModel)]="cipher.organizationId" (change)="organizationChanged()"
[disabled]="cipher.isDeleted">
<option *ngFor="let o of ownershipOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
</div>
@@ -459,7 +469,7 @@
<ng-container *ngIf="collections && collections.length">
<div class="form-check" *ngFor="let c of collections; let i = index">
<input class="form-check-input" type="checkbox" [(ngModel)]="c.checked"
id="collection-{{i}}" name="Collection[{{i}}].Checked">
id="collection-{{i}}" name="Collection[{{i}}].Checked" [disabled]="cipher.isDeleted">
<label class="form-check-label" for="collection-{{i}}">{{c.name}}</label>
</div>
</ng-container>
@@ -492,19 +502,20 @@
<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>
<span>{{(cipher?.isDeleted ? 'restore' : 'save') | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{'cancel' | i18n}}
</button>
<div class="ml-auto" *ngIf="cipher">
<button *ngIf="!organization" type="button" (click)="toggleFavorite()" class="btn btn-link"
<button *ngIf="!organization && !cipher.isDeleted" type="button" (click)="toggleFavorite()" class="btn btn-link"
appA11yTitle="{{(cipher.favorite ? 'unfavorite' : 'favorite') | i18n}}">
<i class="fa fa-lg" [ngClass]="{'fa-star': cipher.favorite, 'fa-star-o': !cipher.favorite}"
aria-hidden="true"></i>
</button>
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger"
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode && !cloneMode" [disabled]="deleteBtn.loading"
appA11yTitle="{{(cipher.isDeleted ? 'permanentlyDelete' : 'delete') | i18n}}"
*ngIf="editMode && !cloneMode" [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"
@@ -514,4 +525,4 @@
</div>
</form>
</div>
</div>
</div>

View File

@@ -0,0 +1,38 @@
<div class="dropdown mr-2" appListDropdown>
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" id="bulkActionsButton"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" appA11yTitle="{{'options' | i18n}}">
<i class="fa fa-cog" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="bulkActionsButton">
<button class="dropdown-item" appStopClick (click)="bulkMove()" *ngIf="!deleted && !organization">
<i class="fa fa-fw fa-share" aria-hidden="true"></i>
{{'moveSelected' | i18n}}
</button>
<button class="dropdown-item" appStopClick (click)="bulkShare()" *ngIf="!deleted && !organization">
<i class="fa fa-fw fa-share-alt" aria-hidden="true"></i>
{{'shareSelected' | i18n}}
</button>
<button class="dropdown-item" (click)="bulkRestore()" *ngIf="deleted && !organization">
<i class="fa fa-fw fa-undo" aria-hidden="true"></i>
{{'restoreSelected' | i18n}}
</button>
<button class="dropdown-item text-danger" (click)="bulkDelete()">
<i class="fa fa-fw fa-trash-o" aria-hidden="true"></i>
{{(deleted ? 'permanentlyDeleteSelected' : 'deleteSelected') | i18n}}
</button>
<div class="dropdown-divider"></div>
<button class="dropdown-item" appStopClick (click)="selectAll(true)">
<i class="fa fa-fw fa-check-square-o" aria-hidden="true"></i>
{{'selectAll' | i18n}}
</button>
<button class="dropdown-item" appStopClick (click)="selectAll(false)">
<i class="fa fa-fw fa-minus-square-o" aria-hidden="true"></i>
{{'unselectAll' | i18n}}
</button>
</div>
</div>
<ng-template #bulkDeleteTemplate></ng-template>
<ng-template #bulkRestoreTemplate></ng-template>
<ng-template #bulkMoveTemplate></ng-template>
<ng-template #bulkShareTemplate></ng-template>

View File

@@ -0,0 +1,154 @@
import {
Component,
ComponentFactoryResolver,
Input,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { Organization } from 'jslib/models/domain/organization';
import { ModalComponent } from '../modal.component';
import { BulkDeleteComponent } from './bulk-delete.component';
import { BulkMoveComponent } from './bulk-move.component';
import { BulkRestoreComponent } from './bulk-restore.component';
import { BulkShareComponent } from './bulk-share.component';
import { CiphersComponent } from './ciphers.component';
@Component({
selector: 'app-vault-bulk-actions',
templateUrl: 'bulk-actions.component.html',
})
export class BulkActionsComponent {
@Input() ciphersComponent: CiphersComponent;
@Input() modal: ModalComponent;
@Input() deleted: boolean;
@Input() organization: Organization;
@ViewChild('bulkDeleteTemplate', { read: ViewContainerRef, static: true }) bulkDeleteModalRef: ViewContainerRef;
@ViewChild('bulkRestoreTemplate', { read: ViewContainerRef, static: true }) bulkRestoreModalRef: ViewContainerRef;
@ViewChild('bulkMoveTemplate', { read: ViewContainerRef, static: true }) bulkMoveModalRef: ViewContainerRef;
@ViewChild('bulkShareTemplate', { read: ViewContainerRef, static: true }) bulkShareModalRef: ViewContainerRef;
constructor(private toasterService: ToasterService,
private i18nService: I18nService,
private componentFactoryResolver: ComponentFactoryResolver) { }
bulkDelete() {
const selectedIds = this.ciphersComponent.getSelectedIds();
if (selectedIds.length === 0) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('nothingSelected'));
return;
}
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.bulkDeleteModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<BulkDeleteComponent>(BulkDeleteComponent, this.bulkDeleteModalRef);
childComponent.permanent = this.deleted;
childComponent.cipherIds = selectedIds;
childComponent.organization = this.organization;
childComponent.onDeleted.subscribe(async () => {
this.modal.close();
await this.ciphersComponent.refresh();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
bulkRestore() {
const selectedIds = this.ciphersComponent.getSelectedIds();
if (selectedIds.length === 0) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('nothingSelected'));
return;
}
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.bulkRestoreModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<BulkRestoreComponent>(BulkRestoreComponent, this.bulkRestoreModalRef);
childComponent.cipherIds = selectedIds;
childComponent.onRestored.subscribe(async () => {
this.modal.close();
await this.ciphersComponent.refresh();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
bulkShare() {
const selectedCiphers = this.ciphersComponent.getSelected();
if (selectedCiphers.length === 0) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('nothingSelected'));
return;
}
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.bulkShareModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<BulkShareComponent>(BulkShareComponent, this.bulkShareModalRef);
childComponent.ciphers = selectedCiphers;
childComponent.onShared.subscribe(async () => {
this.modal.close();
await this.ciphersComponent.refresh();
});
this.modal.onClosed.subscribe(async () => {
this.modal = null;
});
}
bulkMove() {
const selectedIds = this.ciphersComponent.getSelectedIds();
if (selectedIds.length === 0) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('nothingSelected'));
return;
}
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.bulkMoveModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<BulkMoveComponent>(BulkMoveComponent, this.bulkMoveModalRef);
childComponent.cipherIds = selectedIds;
childComponent.onMoved.subscribe(async () => {
this.modal.close();
await this.ciphersComponent.refresh();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
selectAll(select: boolean) {
this.ciphersComponent.selectAll(select);
}
}

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