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

Compare commits

...

227 Commits

Author SHA1 Message Date
Chad Scharf
9326a9a4a7 version bump v2.18.1 2021-01-25 17:40:41 -05:00
Addison Beck
6b3b1a901d Lunr search bug (#810)
* changed hrtime library

* updated jslib
2021-01-25 16:57:43 -05:00
Kyle Spearrin
41d0880bd5 New translations messages.json (Polish) (#809) 2021-01-25 16:53:07 -05:00
Kyle Spearrin
3e66a7162c New Crowdin updates (#807)
* New translations messages.json (Romanian)

* New translations messages.json (Polish)

* New translations messages.json (Malayalam)

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

* New translations messages.json (Russian)

* New translations messages.json (Dutch)

* New translations messages.json (French)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

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

* New translations messages.json (Spanish)

* New translations messages.json (English, India)
2021-01-25 16:52:58 -05:00
Thomas Rittson
c93e39566f Add branded 404 page to replace Github Pages 404 (#798) 2021-01-25 16:52:35 -05:00
Oscar Hinton
6af405b89d Update emergency access user-access link (#797)
The help link for user-access incorrectly linked to the wrong page. Changed to the correct link.
2021-01-25 16:52:13 -05:00
vachan-maker
f9a2ec138e Update learn more link for Emergency Access (#796) 2021-01-25 16:52:04 -05:00
Matt Gibson
ddab1a3cee Fix context copy buttons work only with TOTP present (#794) 2021-01-25 16:51:55 -05:00
Oscar Hinton
f24836b0a5 Fix emergency access confirm not working with two-factor enabled (#792) 2021-01-25 16:51:45 -05:00
Kyle Spearrin
1429cb3f76 New Crowdin updates (#786)
* 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 (Slovenian)

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

* New translations messages.json (French)

* New translations messages.json (Greek)

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

* New translations messages.json (Finnish)

* New translations messages.json (Russian)

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

* New translations messages.json (English, India)
2021-01-19 21:51:17 -05:00
Thomas Rittson
13cbba3e99 Fix typo in selectedPlanInterval (#785) 2021-01-20 09:25:01 +10:00
Chad Scharf
73d24162a0 disable send (#784) 2021-01-19 17:03:34 -05:00
Chad Scharf
4964ebd31e Version bump to 2.18.0 (#783) 2021-01-19 16:18:52 -05:00
Kyle Spearrin
5a8198a878 fixes to plan/pricing page (#776) 2021-01-15 14:46:25 -05:00
Addison Beck
03aa806af6 fixed various Permissions UI issues (#775) 2021-01-14 18:08:26 -05:00
Addison Beck
023bf0474c Showed sales tax amount on Go Premium screen (#774) 2021-01-14 17:53:46 -05:00
Oscar Hinton
2e22ca9216 Remove grantee email from accept-emergency component (#773) 2021-01-14 20:22:49 +01:00
Addison Beck
5a540bba9e Fixed trailing comma lint warnings (#772)
* Fixed trailing comma lint warnings

* Specified options on tslint comma rule
2021-01-13 15:34:06 -05:00
Vincent Salucci
2047a6378b [Policy] Update Personal Ownership checkbox description (#767)
* Initial commit of enabled checkbox description update

* Updated requested changes

* Fixed merge conflict
2021-01-12 17:13:59 -06:00
Addison Beck
dc87510a7a Implemented Custom role and permissions (#750)
* Implemented Custom role and permissions

* converted Permissions interface into a class

* fixed a merge issue

* updated jslib

* code review cleanup for Permissions

* trailing commas
2021-01-12 15:31:22 -05:00
Khánh Hoàng
c3f4c6c03b Fix #759 for Print Code button in Two-step Login (#760) 2021-01-12 12:33:56 -05:00
Matt Gibson
e8b72477c9 Update jslib (#770) 2021-01-08 12:14:20 -06:00
Kyle Spearrin
862874c2ae ui updates for send add/edit component (#768)
* ui updates for send add/edit component

* move messaging.service import
2021-01-07 17:13:25 -05:00
Vincent Salucci
4d2d686078 [Policy] Personal Ownership banner (#764)
* Updated banner position and message

* updated capitalization
2021-01-06 10:01:34 -06:00
Kyle Spearrin
6d458646fa add predefined time frames for delete and expire (#765) 2021-01-05 14:45:23 -05:00
Matt Gibson
a1345488d0 Update jslib (#763) 2021-01-04 11:38:16 -06:00
Kyle Spearrin
c43012a5f2 send improvements and bug fixes (#757)
* send improvements and bug fixes

* update jslib

* update jslib

* update jslib

* update jslib ref

* Hide match uri overflow (#758)

match descriptions are overflowing in german and causing the uri delete
button to overflow off of the cipher view modal

* update jslib

* jslib ref

* update jslib

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
2021-01-04 10:57:53 -05:00
Kyle Spearrin
577cab24c4 update jslib ref 2021-01-04 10:55:58 -05:00
Vincent Salucci
5c0a77aec8 update jslib (48144a7) (#755) 2020-12-30 20:35:21 -06:00
Matt Gibson
a0904b14ed Hide match uri overflow (#758)
match descriptions are overflowing in german and causing the uri delete
button to overflow off of the cipher view modal
2020-12-30 17:12:01 -06:00
Kyle Spearrin
6774ae0ef3 warning dialog is now handled in base component (#751) 2020-12-22 16:37:50 -05:00
Oscar Hinton
5a76ca4676 Fix linting errors (#749)
* Fix linting errors

* Added back the form promise
2020-12-22 11:28:58 -05:00
Oscar Hinton
3c5a972bc9 Add support for Emergency Access (#707)
* Add support for Emergency Access

* Cleanup & Bugfix

* Apply suggestions from code review

Co-authored-by: Addison Beck <addisonbeck1@gmail.com>

* Cleanup some more imports

* Restrict emergency access invite to premium users

* Restrict editing existing emergency accesses to premium account.

* Handle changes in jslib

* Add some info messages for when you haven't been granted or invited emergency contacts

* Resolve review comments

* Update jslib

Co-authored-by: Addison Beck <addisonbeck1@gmail.com>
2020-12-22 10:57:44 -05:00
Joseph Flinn
54b68ac543 fixing incorrect secret name (#747) 2020-12-21 12:58:51 -08:00
Joseph Flinn
ff378f05fe Moving appveyor to actions (#746)
* initial build testing

* fixing the release event

* fixing typo

* adding windows build

* fixing yaml

* fixing yaml again...

* fixing the windows build
2020-12-21 09:46:36 -08:00
Chad Scharf
f207aa3a9d show caret or icon, not just "blank" space (#745) 2020-12-21 10:59:07 -05:00
Vincent Salucci
7b43dcb6a1 [Policy] Single Org dependency chain (#739)
* Initial commit of Single Org downstream policy checks

* Moved comments
2020-12-17 14:20:45 -06:00
Matt Gibson
c487cf3284 Add message for missing event type (#740)
* Add message for missing event type

* update jslib reference
2020-12-16 18:29:19 -06:00
Chad Scharf
c2e1d325f2 update jslib and fix webPlatformUtils (#741) 2020-12-16 16:40:10 -05:00
Mithilesh Zavar
f090e8febf Password History Overflow (#743) 2020-12-16 15:47:37 -05:00
Wyatt Childers
087c84bcfb Only show carats for items with children (#410)
* Only show carats for folders with children

* Only show carats for collections with children
2020-12-15 16:32:49 -05:00
Mithilesh Zavar
a457c83242 Attachments with long file names go beyond the window. #695 (#702) 2020-12-15 16:03:17 -05:00
Matt Gibson
bcd8963e8b Add totp copy to clipboard button to cipher view (#737)
* Add totp copy to clipboard button to cipher view

* Align totp copy privs with cipher view

* Enforce TOTP as premium feature

* Update jslib reference
2020-12-15 10:25:52 -06:00
Matt Gibson
1464e0fbe8 Add ConsoleLogService dependency from jslib (#735)
* Pre-emptively add new jslib dependency

* Add consoleLogService dependency definition

* Update jslib

* PR Review

Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
2020-12-14 12:27:32 -06:00
Kyle Spearrin
ec2b048289 enable send 2020-12-11 16:43:28 -05:00
Joshua McCauley
04811c934f Updated readme to note how to run the app against production (#706)
* Noted upper limit of Node.js verion support for the application: the SCSS dependency v4.13.1 only supports Node.js up to v13.9.0.
Added note for npm commands for running the application against local APIs versus production. The correct npm command for running against production was found here https://github.com/bitwarden/web/issues/666.
Added more lines to the services.module.ts example to better reflect the actual file.

* Added CORS common issue and solution to README.md

* Changed Node.js version notes for real this time.
2020-12-10 12:08:31 -05:00
Vincent Salucci
f84ee30b9d Fixed casing for error message (#734) 2020-12-09 13:30:55 -06:00
Vincent Salucci
218caa28b0 [Policy] Personal Ownership (#722)
* Initial commit of personal ownership policy

* Added event handling for modifying policies

* I didn't save the merge conflict fix...

* Removed unused import

* Updated jslib (dcbd09e -> 2d62e10)
2020-12-08 13:24:59 -06:00
Matt Gibson
a8af807650 Add help text for jslib's mac importer (#731)
Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
2020-12-08 09:16:50 -06:00
vachan-maker
826170507e Updated Firefox Import Instructions (#728) 2020-12-07 16:51:53 -05:00
Kyle Spearrin
c37979e48d update getImporter signature pass org id (#730) 2020-12-07 13:02:56 -05:00
Addison Beck
7ebb046cd8 updated jslib (#727) 2020-12-04 12:39:41 -05:00
Addison Beck
7c4d0a15dd Implemented tax collection for subscriptions (#723)
* Implemented tax collection for subscriptions

* Cleanup for Sales Tax

* Code review fixes for Tax Rate implementation

* Code review fixes for Tax Rate implementation
2020-12-04 12:05:44 -05:00
Kyle Spearrin
512b9e0a92 encrypted json export option for user and orgs (#726)
* encrypted json export option for user and orgs

* move org id to base export component
2020-12-04 09:58:26 -05:00
Kyle Spearrin
5e95a8565c disable send 2020-12-02 15:23:54 -05:00
Kyle Spearrin
e83d0f2a9d bump version 2020-12-02 15:16:39 -05:00
Kyle Spearrin
eaebbcf6c8 New Crowdin updates (#725)
* 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-12-02 15:13:29 -05:00
Matt Gibson
7df5ed9b35 Terser minimizer requires option to include maps (#721)
Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
2020-11-30 11:14:21 -06:00
Matt Gibson
6b66f14319 Update web sso content to indicate window OK to close (#720)
* Update web sso content to indicate window OK to close

This is done after the authResult handoff message is delivered to the
extension. It is not possible to close the window from javascript as
closing a window is limited to the script that opened it.

If we maintain a reference to the web window, it should be possible to
subscribe to the authResult message and close the web windows from the
browser.

* Use i18n for close tab message

* delete cookie after it is used

Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
2020-11-25 15:57:11 -06:00
Chad Scharf
2db1684b3c Exclude deleted items from any/all reports (#700) 2020-11-24 12:36:40 -05:00
Matt Gibson
4625b44703 WIP: dirty fix to SSO web vs browser redirect logic split (#719)
* WIP: dirty fix to SSO web vs browser redirect logic split

* Use includes for clientId identification

routing determination more robust to future state string changes

Co-authored-by: Addison Beck <abeck@bitwarden.com>

Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
Co-authored-by: Addison Beck <abeck@bitwarden.com>
2020-11-24 11:29:04 -06:00
Chad Scharf
0356ecc17b update jslib (#717) 2020-11-23 17:36:17 -05:00
Oscar Hinton
1e7c27fba1 Change supportsSecureStorage to false (#716) 2020-11-23 15:56:22 -05:00
Vincent Salucci
03f575f66f [Bug] Update 2fa navigate action to pass along Org Identifier (#714)
* Add identifer in 2fa navigate action

* Update jslib (6563dcc -> d9d13bb)

* fixed breaking changes from jslib update
2020-11-23 09:12:12 -06:00
Matt Gibson
82b36c1b70 Use mobile's trash message for item delete (#710)
Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
2020-11-19 11:38:53 -06:00
Kyle Spearrin
6878ab51fb send service implementation (#708)
* send service implementation

* update jslib
2020-11-18 15:18:13 -05:00
Chad Scharf
8662033979 re-enable send (#709) 2020-11-18 12:44:21 -05:00
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
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
218 changed files with 58520 additions and 6006 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
@@ -13,3 +13,6 @@ insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 4
[*.{ts}]
quote_type = single

168
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,168 @@
name: build
on:
push:
branches-ignore:
- 'l10n_master'
- 'gh-pages'
release:
types:
- published
jobs:
cloc:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v2
- name: Set up cloc
run: |
sudo apt update
sudo apt -y install cloc
- name: Print lines of code
run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
ubuntu:
runs-on: ubuntu-latest
steps:
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: '10.x'
- name: Print environment
run: |
whoami
node --version
npm --version
gulp --version
docker --version
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
env:
GITHUB_REF: ${{ github.ref }}
GITHUB_EVENT: ${{ github.event_name }}
- name: Log into docker
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
run: echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
- name: Setup Docker Trust
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
run: |
mkdir -p ~/.docker/trust/private
echo "${{ secrets.DOCKER_DELEGATION_KEY }}" > ~/.docker/trust/private/$DOCKER_DELEGATION_KEY_ID.key
echo "${{ secrets.DOCKER_REPO_WEB_KEY }}" > ~/.docker/trust/private/$DOCKER_WEB_KEY_ID.key
env:
DOCKER_DELEGATION_KEY_ID: "5702b22123e058cbd96a7a43000cb981ae98ef3f2f4aa34138ab3dc1d011e446"
DOCKER_WEB_KEY_ID: "0f88641697187f42a31b584897cd4edfe80360a5209122d9e7f71af17a6422e4"
- name: Checkout repo
uses: actions/checkout@v2
- name: Restore
run: dotnet tool restore
- name: Build
run: |
chmod +x ./build.sh
./build.sh
- name: Tag dev
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
run: ./build.sh tag dev
- name: Tag beta
if: github.event_name == 'release'
run: ./build.sh tag beta
- name: Tag version
if: github.event_name == 'release'
run: ./build.sh tag $($env:RELEASE_TAG_NAME.trimStart('v'))
shell: pwsh
env:
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
- name: List docker images
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
run: docker images
- name: Push dev images
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
run: ./build.sh push dev
env:
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE }}
- name: Push beta images
if: github.event_name == 'release'
run: ./build.sh push beta
env:
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE }}
- name: Push latest images
if: github.event_name == 'release'
run: ./build.sh push latest
env:
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE }}
- name: Push version images
if: github.event_name == 'release'
run: ./build.sh push $($env:RELEASE_TAG_NAME.trimStart('v'))
shell: pwsh
env:
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE }}
- name: Log out of docker
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
run: docker logout
windows:
runs-on: windows-latest
steps:
- name: Set up NuGet
uses: nuget/setup-nuget@v1
with:
nuget-version: 'latest'
- name: Set up MSBuild
uses: microsoft/setup-msbuild@v1
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: '10.x'
- name: Print environment
run: |
nuget help
msbuild -version
dotnet --info
node --version
npm --version
Write-Output "GitHub ref: $env:GITHUB_REF"
Write-Output "GitHub event: $env:GITHUB_EVENT"
shell: pwsh
env:
GITHUB_REF: ${{ github.ref }}
GITHUB_EVENT: ${{ github.event_name }}
- name: Checkout repo
uses: actions/checkout@v2
- name: npm install
run: npm install
- name: npm build
run: npm run build:prod

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

@@ -27,6 +27,8 @@
### Run the app
For local development, run the app with:
```
npm install
npm run build:watch
@@ -39,6 +41,7 @@ await apiService.setUrls({
base: isDev ? null : window.location.origin,
api: isDev ? 'http://mylocalapi' : null,
identity: isDev ? 'http://mylocalidentity' : null,
events: isDev ? 'http://mylocalevents' : null,
});
```
@@ -49,9 +52,31 @@ await apiService.setUrls({
base: null,
api: 'https://api.bitwarden.com',
identity: 'https://identity.bitwarden.com',
events: 'https://events.bitwarden.com',
});
```
And note to run the app with:
```
npm install
npm run build:prod:watch
```
## Common Issues:
### CORS
If you run the frontend and receive a notification after attempting to login that says:
```
An error has occurred.
NetworkError when attempting to fetch resource.
```
And in the console:
```
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://api.bitwarden.com/accounts/prelogin. (Reason: CORS header Access-Control-Allow-Origin missing).
```
This means that you are having a CORS header issue. This can be mitigated by using a CORS header changing extension in your browser such as [this one.](https://mybrowseraddon.com/access-control-allow-origin.html)
## Contribute

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: 31a257407b...9ddec9baf8

2735
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.2",
"version": "2.18.1",
"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,45 @@
"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",
"browser-hrtime": "^1.1.8",
"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 +91,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"
}
}

35
src/404.css Normal file
View File

@@ -0,0 +1,35 @@
html, body, .row {
height: 100%;
-webkit-font-smoothing: antialiased;
}
h2 {
font-size: 25px;
margin-bottom: 12.5px;
font-weight: 500;
line-height: 1.1;
}
.brand {
font-size: 23px;
line-height: 25px;
color: #fff;
font-family: "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
}
.banner {
background-color: #175DDC;
height: 56px;
}
.content {
padding-top: 20px;
padding-bottom: 20px;
padding-left: 15px;
padding-right: 15px;
}
.footer {
padding: 40px 0 40px 0;
border-top: 1px solid #dee2e6;
}

54
src/404.html Normal file
View File

@@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
<link rel="stylesheet" crossorigin="anonymous"
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
integrity="sha256-eZrrJcwDc/3uDhsdt61sL2oOBY362qM3lon1gyExkL0= sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN sha512-SfTiTlX6kk+qitfevl/7LibUOeJWlt9rbyDn92a1DqWOw9vWG2MFoays0sgObmWazO5BQPiFucnnEAjpAB+/Sw==">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700,300italic,400italic,600italic"
rel="stylesheet" type="text/css">
<link href="/404.css" rel="stylesheet" type="text/css">
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/favicon-16x16.png">
<link rel="mask-icon" href="/images/icons/safari-pinned-tab.svg" color="#175DDC">
<link rel="manifest" href="/manifest.json">
<title>Page not found!</title>
<meta name="description" content="404 Page Not Found">
</head>
<body>
<div class="banner">
<div class="container inner banner">
<div class="row align-items-center">
<div class="col brand">
<i class="fa fa-shield"></i>&nbsp;
<strong>bit</strong>warden</span>
</div>
</div>
</div>
</div>
<div class="container inner content">
<h2>Page not found!</h2>
<p>Sorry, but the page you were looking for could not be found.</p>
<p>
<a href="/">
<img src="/images/404.png" class="img-fluid" alt="404 image" width="80%"/>
</a>
</p>
<p>You can <a href="/">return to the web vault</a>, check our <a href="https://status.bitwarden.com/">status page</a>
or <a href="https://bitwarden.com/contact/">contact us</a>.</p>
</div>
<div class="container footer text-muted content">
© Copyright 2021 Bitwarden, Inc.
</div>
</body>
</html>

View File

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

View File

@@ -0,0 +1,93 @@
import {
Component,
OnInit,
} from '@angular/core';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import {
Toast,
ToasterService,
} from 'angular2-toaster';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { StateService } from 'jslib/abstractions/state.service';
import { UserService } from 'jslib/abstractions/user.service';
import { EmergencyAccessAcceptRequest } from 'jslib/models/request/emergencyAccessAcceptRequest';
@Component({
selector: 'app-accept-emergency',
templateUrl: 'accept-emergency.component.html',
})
export class AcceptEmergencyComponent implements OnInit {
loading = true;
authed = false;
name: string;
email: string;
actionPromise: Promise<any>;
constructor(private router: Router, private toasterService: ToasterService,
private i18nService: I18nService, private route: ActivatedRoute,
private apiService: ApiService, private userService: UserService,
private stateService: StateService) { }
ngOnInit() {
let fired = false;
this.route.queryParams.subscribe(async (qParams) => {
if (fired) {
return;
}
fired = true;
await this.stateService.remove('emergencyInvitation');
let error = qParams.id == null || qParams.name == null || qParams.email == null || qParams.token == null;
let errorMessage: string = null;
if (!error) {
this.authed = await this.userService.isAuthenticated();
if (this.authed) {
const request = new EmergencyAccessAcceptRequest();
request.token = qParams.token;
try {
this.actionPromise = this.apiService.postEmergencyAccessAccept(qParams.id, request);
await this.actionPromise;
const toast: Toast = {
type: 'success',
title: this.i18nService.t('inviteAccepted'),
body: this.i18nService.t('emergencyInviteAcceptedDesc'),
timeout: 10000,
};
this.toasterService.popAsync(toast);
this.router.navigate(['/vault']);
} catch (e) {
error = true;
errorMessage = e.message;
}
} else {
await this.stateService.save('emergencyInvitation', qParams);
this.email = qParams.email;
this.name = qParams.name;
if (this.name != null) {
// Fix URL encoding of space issue with Angular
this.name = this.name.replace(/\+/g, ' ');
}
}
}
if (error) {
const toast: Toast = {
type: 'error',
title: null,
body: errorMessage != null ? this.i18nService.t('emergencyInviteAcceptFailedShort', errorMessage) :
this.i18nService.t('emergencyInviteAcceptFailed'),
timeout: 10000,
};
this.toasterService.popAsync(toast);
this.router.navigate(['/']);
}
this.loading = false;
});
}
}

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;
}
@@ -44,9 +52,12 @@ export class LoginComponent extends BaseLoginComponent {
}
async goAfterLogIn() {
const invite = await this.stateService.get<any>('orgInvitation');
if (invite != null) {
this.router.navigate(['accept-organization'], { queryParams: invite });
const orgInvite = await this.stateService.get<any>('orgInvitation');
const emergencyInvite = await this.stateService.get<any>('emergencyInvitation');
if (orgInvite != null) {
this.router.navigate(['accept-organization'], { queryParams: orgInvite });
} else if (emergencyInvite != null) {
this.router.navigate(['accept-emergency'], { queryParams: emergencyInvite });
} else {
const loginRedirect = await this.stateService.get<any>('loginRedirect');
if (loginRedirect != null) {

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,61 @@
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);
if (this.clientId === 'browser') {
document.cookie = `ssoHandOffMessage=${this.i18nService.t('ssoHandOff')};SameSite=strict`;
}
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;
}
@@ -57,16 +60,23 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
}
async goAfterLogIn() {
const invite = await this.stateService.get<any>('orgInvitation');
if (invite != null) {
this.router.navigate(['accept-organization'], { queryParams: invite });
const orgInvite = await this.stateService.get<any>('orgInvitation');
const emergencyInvite = await this.stateService.get<any>('emergencyInvitation');
if (orgInvite != null) {
this.router.navigate(['accept-organization'], { queryParams: orgInvite });
} else if (emergencyInvite != null) {
this.router.navigate(['accept-emergency'], { queryParams: emergencyInvite });
} else {
const loginRedirect = await this.stateService.get<any>('loginRedirect');
if (loginRedirect != null) {
this.router.navigate([loginRedirect.route], { queryParams: loginRedirect.qParams });
await this.stateService.remove('loginRedirect');
} else {
this.router.navigate([this.successRoute]);
this.router.navigate([this.successRoute], {
queryParams: {
identifier: this.identifier,
},
});
}
}
}

View File

@@ -8,6 +8,7 @@ import { FrontendLayoutComponent } from './layouts/frontend-layout.component';
import { OrganizationLayoutComponent } from './layouts/organization-layout.component';
import { UserLayoutComponent } from './layouts/user-layout.component';
import { AcceptEmergencyComponent } from './accounts/accept-emergency.component';
import { AcceptOrganizationComponent } from './accounts/accept-organization.component';
import { HintComponent } from './accounts/hint.component';
import { LockComponent } from './accounts/lock.component';
@@ -15,6 +16,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 +58,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';
@@ -85,7 +91,10 @@ import { UnauthGuardService } from './services/unauth-guard.service';
import { AuthGuardService } from 'jslib/angular/services/auth-guard.service';
import { OrganizationUserType } from 'jslib/enums/organizationUserType';
import { Permissions } from 'jslib/enums/permissions';
import { EmergencyAccessViewComponent } from './settings/emergency-access-view.component';
import { EmergencyAccessComponent } from './settings/emergency-access.component';
const routes: Routes = [
{
@@ -99,6 +108,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],
@@ -111,6 +129,11 @@ const routes: Routes = [
component: AcceptOrganizationComponent,
data: { titleId: 'joinOrganization' },
},
{
path: 'accept-emergency',
component: AcceptEmergencyComponent,
data: { titleId: 'acceptEmergency' },
},
{ path: 'recover', pathMatch: 'full', redirectTo: 'recover-2fa' },
{
path: 'recover-2fa',
@@ -130,6 +153,11 @@ const routes: Routes = [
canActivate: [UnauthGuardService],
data: { titleId: 'deleteAccount' },
},
/*{
path: 'send/:sendId/:key',
component: AccessComponent,
data: { title: 'Bitwarden Send' },
},*/
],
},
{
@@ -138,6 +166,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,
@@ -160,6 +189,21 @@ const routes: Routes = [
component: CreateOrganizationComponent,
data: { titleId: 'newOrganization' },
},
{
path: 'emergency-access',
children: [
{
path: '',
component: EmergencyAccessComponent,
data: { titleId: 'emergencyAccess'},
},
{
path: ':id',
component: EmergencyAccessViewComponent,
data: { titleId: 'emergencyAccess'},
},
],
},
],
},
{
@@ -216,35 +260,75 @@ const routes: Routes = [
path: 'tools',
component: OrgToolsComponent,
canActivate: [OrganizationTypeGuardService],
data: { allowedTypes: [OrganizationUserType.Owner, OrganizationUserType.Admin] },
data: { permissions: [Permissions.AccessImportExport, Permissions.AccessReports] },
children: [
{ path: '', pathMatch: 'full', redirectTo: 'import' },
{ path: 'import', component: OrgImportComponent, data: { titleId: 'importData' } },
{ path: 'export', component: OrgExportComponent, data: { titleId: 'exportVault' } },
{
path: '',
pathMatch: 'full',
redirectTo: 'import',
},
{
path: 'import',
component: OrgImportComponent,
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'importData',
permissions: [Permissions.AccessImportExport],
},
},
{
path: 'export',
component: OrgExportComponent,
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'exportVault',
permissions: [Permissions.AccessImportExport],
},
},
{
path: 'exposed-passwords-report',
component: OrgExposedPasswordsReportComponent,
data: { titleId: 'exposedPasswordsReport' },
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'exposedPasswordsReport',
permissions: [Permissions.AccessReports],
},
},
{
path: 'inactive-two-factor-report',
component: OrgInactiveTwoFactorReportComponent,
data: { titleId: 'inactive2faReport' },
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'inactive2faReport',
permissions: [Permissions.AccessReports],
},
},
{
path: 'reused-passwords-report',
component: OrgReusedPasswordsReportComponent,
data: { titleId: 'reusedPasswordsReport' },
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'reusedPasswordsReport',
permissions: [Permissions.AccessReports],
},
},
{
path: 'unsecured-websites-report',
component: OrgUnsecuredWebsitesReportComponent,
data: { titleId: 'unsecuredWebsitesReport' },
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'unsecuredWebsitesReport',
permissions: [Permissions.AccessReports],
},
},
{
path: 'weak-passwords-report',
component: OrgWeakPasswordsReportComponent,
data: { titleId: 'weakPasswordsReport' },
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'weakPasswordsReport',
permissions: [Permissions.AccessReports],
},
},
],
},
@@ -253,26 +337,73 @@ const routes: Routes = [
component: OrgManageComponent,
canActivate: [OrganizationTypeGuardService],
data: {
allowedTypes: [
OrganizationUserType.Owner,
OrganizationUserType.Admin,
OrganizationUserType.Manager,
permissions: [
Permissions.ManageAssignedCollections,
Permissions.ManageAllCollections,
Permissions.AccessEventLogs,
Permissions.ManageGroups,
Permissions.ManageUsers,
Permissions.ManagePolicies,
],
},
children: [
{ path: '', pathMatch: 'full', redirectTo: 'people' },
{ path: 'collections', component: OrgManageCollectionsComponent, data: { titleId: 'collections' } },
{ path: 'events', component: OrgEventsComponent, data: { titleId: 'eventLogs' } },
{ path: 'groups', component: OrgGroupsComponent, data: { titleId: 'groups' } },
{ path: 'people', component: OrgPeopleComponent, data: { titleId: 'people' } },
{ path: 'policies', component: OrgPoliciesComponent, data: { titleId: 'policies' } },
{
path: '',
pathMatch: 'full',
redirectTo: 'people',
},
{
path: 'collections',
component: OrgManageCollectionsComponent,
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'collections',
permissions: [Permissions.ManageAssignedCollections, Permissions.ManageAllCollections],
},
},
{
path: 'events',
component: OrgEventsComponent,
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'eventLogs',
permissions: [Permissions.AccessEventLogs],
},
},
{
path: 'groups',
component: OrgGroupsComponent,
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'groups',
permissions: [Permissions.ManageGroups],
},
},
{
path: 'people',
component: OrgPeopleComponent,
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'people',
permissions: [Permissions.ManageUsers],
},
},
{
path: 'policies',
component: OrgPoliciesComponent,
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'policies',
permissions: [Permissions.ManagePolicies],
},
},
],
},
{
path: 'settings',
component: OrgSettingsComponent,
canActivate: [OrganizationTypeGuardService],
data: { allowedTypes: [OrganizationUserType.Owner] },
data: { permissions: [Permissions.ManageOrganization] },
children: [
{ path: '', pathMatch: 'full', redirectTo: 'account' },
{ path: 'account', component: OrgAccountComponent, data: { titleId: 'myOrganization' } },
@@ -297,6 +428,7 @@ const routes: Routes = [
@NgModule({
imports: [RouterModule.forRoot(routes, {
useHash: true,
paramsInheritanceStrategy: 'always',
/*enableTracing: true,*/
})],
exports: [RouterModule],

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

@@ -27,6 +27,7 @@ import { NavbarComponent } from './layouts/navbar.component';
import { OrganizationLayoutComponent } from './layouts/organization-layout.component';
import { UserLayoutComponent } from './layouts/user-layout.component';
import { AcceptEmergencyComponent } from './accounts/accept-emergency.component';
import { AcceptOrganizationComponent } from './accounts/accept-organization.component';
import { HintComponent } from './accounts/hint.component';
import { LockComponent } from './accounts/lock.component';
@@ -34,6 +35,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 +61,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 +97,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 +113,13 @@ 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 { EmergencyAccessAddEditComponent } from './settings/emergency-access-add-edit.component';
import { EmergencyAccessComponent } from './settings/emergency-access.component';
import { EmergencyAccessConfirmComponent } from './settings/emergency-access-confirm.component';
import { EmergencyAccessTakeoverComponent } from './settings/emergency-access-takeover.component';
import { EmergencyAccessViewComponent } from './settings/emergency-access-view.component';
import { EmergencyAddEditComponent } from './settings/emergency-add-edit.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 +128,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 +157,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 +189,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 +206,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 +224,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 +233,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 +254,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
FormsModule,
AppRoutingModule,
ServicesModule,
Angulartics2Module.forRoot([Angulartics2GoogleAnalytics], {
Angulartics2Module.forRoot({
pageTracking: {
clearQueryParams: true,
},
@@ -240,14 +265,18 @@ registerLocaleData(localeZhTw, 'zh-TW');
],
declarations: [
A11yTitleDirective,
AcceptEmergencyComponent,
AccessComponent,
AcceptOrganizationComponent,
AccountComponent,
SetPasswordComponent,
AddCreditComponent,
AddEditComponent,
AdjustPaymentComponent,
AdjustSeatsComponent,
AdjustStorageComponent,
ApiActionDirective,
ApiKeyComponent,
AppComponent,
AttachmentsComponent,
AutofocusDirective,
@@ -255,8 +284,10 @@ registerLocaleData(localeZhTw, 'zh-TW');
BlurClickDirective,
BoxRowDirective,
BreachReportComponent,
BulkActionsComponent,
BulkDeleteComponent,
BulkMoveComponent,
BulkRestoreComponent,
BulkShareComponent,
CalloutComponent,
ChangeEmailComponent,
@@ -272,6 +303,12 @@ registerLocaleData(localeZhTw, 'zh-TW');
DeleteOrganizationComponent,
DomainRulesComponent,
DownloadLicenseComponent,
EmergencyAccessAddEditComponent,
EmergencyAccessComponent,
EmergencyAccessConfirmComponent,
EmergencyAccessTakeoverComponent,
EmergencyAccessViewComponent,
EmergencyAddEditComponent,
ExportComponent,
ExposedPasswordsReportComponent,
FallbackSrcDirective,
@@ -285,6 +322,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
ImportComponent,
InactiveTwoFactorReportComponent,
InputVerbatimDirective,
LinkSsoComponent,
LockComponent,
LoginComponent,
ModalComponent,
@@ -292,7 +330,6 @@ registerLocaleData(localeZhTw, 'zh-TW');
OptionsComponent,
OrgAccountComponent,
OrgAddEditComponent,
OrgApiKeyComponent,
OrganizationBillingComponent,
OrganizationPlansComponent,
OrganizationSubscriptionComponent,
@@ -316,7 +353,6 @@ registerLocaleData(localeZhTw, 'zh-TW');
OrgPolicyEditComponent,
OrgPoliciesComponent,
OrgReusedPasswordsReportComponent,
OrgRotateApiKeyComponent,
OrgSettingComponent,
OrgToolsComponent,
OrgTwoFactorSetupComponent,
@@ -342,10 +378,14 @@ registerLocaleData(localeZhTw, 'zh-TW');
SearchCiphersPipe,
SearchPipe,
SelectCopyDirective,
SendAddEditComponent,
SendComponent,
SettingsComponent,
ShareComponent,
SsoComponent,
StopClickDirective,
StopPropDirective,
TaxInfoComponent,
ToolsComponent,
TrueFalseValueDirective,
TwoFactorAuthenticatorComponent,
@@ -372,18 +412,24 @@ registerLocaleData(localeZhTw, 'zh-TW');
],
entryComponents: [
AddEditComponent,
ApiKeyComponent,
AttachmentsComponent,
BulkActionsComponent,
BulkDeleteComponent,
BulkMoveComponent,
BulkRestoreComponent,
BulkShareComponent,
CollectionsComponent,
DeauthorizeSessionsComponent,
DeleteAccountComponent,
DeleteOrganizationComponent,
EmergencyAccessAddEditComponent,
EmergencyAccessConfirmComponent,
EmergencyAccessTakeoverComponent,
EmergencyAddEditComponent,
FolderAddEditComponent,
ModalComponent,
OrgAddEditComponent,
OrgApiKeyComponent,
OrgAttachmentsComponent,
OrgCollectionAddEditComponent,
OrgCollectionsComponent,
@@ -391,12 +437,12 @@ registerLocaleData(localeZhTw, 'zh-TW');
OrgEntityUsersComponent,
OrgGroupAddEditComponent,
OrgPolicyEditComponent,
OrgRotateApiKeyComponent,
OrgUserAddEditComponent,
OrgUserConfirmComponent,
OrgUserGroupsComponent,
PasswordGeneratorHistoryComponent,
PurgeVaultComponent,
SendAddEditComponent,
ShareComponent,
TwoFactorAuthenticatorComponent,
TwoFactorDuoComponent,
@@ -407,7 +453,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="showMenuBar">
<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" *ngIf="showManageTab">
<a class="nav-link" [routerLink]="manageRoute" routerLinkActive="active">
<i class="fa fa-sliders" aria-hidden="true"></i>
{{'manage' | i18n}}
</a>
</li>
<li class="nav-item" *ngIf="showToolsTab">
<a class="nav-link" [routerLink]="toolsRoute" 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)="goToBusinessPortal()" #businessBtn
[appApiAction]="businessTokenPromise" *ngIf="showBusinessPortalButton">
<i class="fa fa-bank fa-fw" [hidden]="businessBtn.loading" aria-hidden="true"></i>
<i class="fa fa-spinner fa-spin fa-fw" [hidden]="!businessBtn.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;
businessTokenPromise: Promise<any>;
private organizationId: string;
private businessUrl: 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.businessUrl = 'https://portal.bitwarden.com';
if (this.environmentService.enterpriseUrl != null) {
this.businessUrl = this.environmentService.enterpriseUrl;
} else if (this.environmentService.baseUrl != null) {
this.businessUrl = 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,69 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
async load() {
this.organization = await this.userService.getOrganization(this.organizationId);
}
async goToBusinessPortal() {
if (this.businessTokenPromise != null) {
return;
}
try {
this.businessTokenPromise = this.apiService.getEnterprisePortalSignInToken();
const token = await this.businessTokenPromise;
if (token != null) {
const userId = await this.userService.getUserId();
this.platformUtilsService.launchUri(this.businessUrl + '/login?userId=' + userId +
'&token=' + (window as any).encodeURIComponent(token) + '&organizationId=' + this.organization.id);
}
} catch { }
this.businessTokenPromise = null;
}
get showMenuBar() {
return this.showManageTab || this.showToolsTab || this.organization.isOwner;
}
get showManageTab(): boolean {
return this.organization.canManageUsers ||
this.organization.canManageAssignedCollections ||
this.organization.canManageAllCollections ||
this.organization.canManageGroups ||
this.organization.canManagePolicies ||
this.organization.canAccessEventLogs;
}
get showToolsTab(): boolean {
return this.organization.canAccessImportExport || this.organization.canAccessReports;
}
get showBusinessPortalButton(): boolean {
return this.organization.useBusinessPortal && this.organization.canAccessBusinessPortal;
}
get toolsRoute(): string {
return this.organization.canAccessImportExport ?
'tools/import' :
'tools/exposed-passwords-report';
}
get manageRoute(): string {
let route: string;
switch (true) {
case this.organization.canManageUsers:
route = 'manage/people';
break;
case this.organization.canManageAssignedCollections || this.organization.canManageAllCollections:
route = 'manage/collections';
break;
case this.organization.canManageGroups:
route = 'manage/groups';
break;
case this.organization.canManagePolicies:
route = 'manage/policies';
break;
case this.organization.canAccessEventLogs:
route = 'manage/events';
break;
}
return route;
}
}

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) => {
@@ -66,7 +72,7 @@ export class CollectionsComponent implements OnInit {
async load() {
const organization = await this.userService.getOrganization(this.organizationId);
let response: ListResponse<CollectionResponse>;
if (organization.isAdmin) {
if (organization.canManageAllCollections) {
response = await this.apiService.getCollections(this.organizationId);
} else {
response = await this.apiService.getUserCollections();
@@ -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>
@@ -84,6 +86,12 @@
<span *ngIf="u.type === organizationUserType.Admin">{{'admin' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.Manager">{{'manager' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.User">{{'user' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.Custom">{{'custom' | 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"

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

@@ -5,22 +5,23 @@
<div class="card-header">{{'manage' | i18n}}</div>
<div class="list-group list-group-flush">
<a routerLink="people" class="list-group-item" routerLinkActive="active"
*ngIf="organization.isAdmin">
*ngIf="organization.canManageUsers">
{{'people' | i18n}}
</a>
<a routerLink="collections" class="list-group-item" routerLinkActive="active">
<a routerLink="collections" class="list-group-item" routerLinkActive="active"
*ngIf="organization.canManageAssignedCollections || organization.canManageAllCollections">
{{'collections' | i18n}}
</a>
<a routerLink="groups" class="list-group-item" routerLinkActive="active"
*ngIf="organization.isAdmin && accessGroups">
*ngIf="organization.canManageGroups && accessGroups">
{{'groups' | i18n}}
</a>
<a routerLink="policies" class="list-group-item" routerLinkActive="active"
*ngIf="organization.isAdmin && accessPolicies">
*ngIf="organization.canManagePolicies && accessPolicies">
{{'policies' | i18n}}
</a>
<a routerLink="events" class="list-group-item" routerLinkActive="active"
*ngIf="organization.isAdmin && accessEvents">
*ngIf="organization.canAccessEventLogs && accessEvents">
{{'eventLogs' | i18n}}
</a>
</div>

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">
@@ -67,6 +69,7 @@
<span *ngIf="u.type === organizationUserType.Admin">{{'admin' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.Manager">{{'manager' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.User">{{'user' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.Custom">{{'custom' | i18n}}</span>
</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown>

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,13 +73,13 @@ 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) => {
this.organizationId = params.organizationId;
const organization = await this.userService.getOrganization(this.organizationId);
if (!organization.isAdmin) {
if (!organization.canManageUsers) {
this.router.navigate(['../collections'], { relativeTo: this.route });
return;
}
@@ -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 *ngIf="userCanAccessBusinessPortal" [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,12 +29,18 @@ 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>;
userCanAccessBusinessPortal = false;
private enterpriseUrl: string;
private modal: ModalComponent = null;
private orgPolicies: PolicyResponse[];
private policiesEnabledMap: Map<PolicyType, boolean> = new Map<PolicyType, boolean>();
@@ -41,28 +48,7 @@ export class PoliciesComponent implements OnInit {
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 +58,83 @@ export class PoliciesComponent implements OnInit {
this.router.navigate(['/organizations', this.organizationId]);
return;
}
this.userCanAccessBusinessPortal = organization.canAccessBusinessPortal;
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,
},
{
name: this.i18nService.t('personalOwnership'),
description: this.i18nService.t('personalOwnershipPolicyDesc'),
type: PolicyType.PersonalOwnership,
enabled: false,
display: true,
},
];
await this.load();
// Handle policies component launch from Event message
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
if (qParams.policyId != null) {
const policyIdFromEvents: string = qParams.policyId;
for (const orgPolicy of this.orgPolicies) {
if (orgPolicy.id === policyIdFromEvents) {
for (let i = 0; i < this.policies.length; i++) {
if (this.policies[i].type === orgPolicy.type) {
this.edit(this.policies[i]);
break;
}
}
break;
}
}
}
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();
}
});
});
// 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 +163,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 +173,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,11 +17,26 @@
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>
<app-callout type="warning" *ngIf="type === policyType.PersonalOwnership">
{{'personalOwnershipExemption' | i18n}}
</app-callout>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="enabled" [(ngModel)]="enabled"
name="Enabled">
<label class="form-check-label" for="enabled">{{'enabled' | i18n}}</label>
<label class="form-check-label" for="enabled">{{checkboxDesc}}</label>
</div>
</div>
<ng-container *ngIf="type === policyType.MasterPassword">

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,85 @@ 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 { }
}
}
get checkboxDesc(): string {
return this.type === PolicyType.PersonalOwnership ? this.i18nService.t('personalOwnershipCheckboxDesc') :
this.i18nService.t('enabled');
}
private preValidate(): boolean {
switch (this.type) {
case PolicyType.RequireSso:
// Don't need prevalidation checks if submitting to disable
if (!this.enabled) {
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;
case PolicyType.SingleOrg:
// Don't need prevalidation checks if submitting to enable
if (this.enabled) {
return true;
}
// If RequireSso Policy is enabled prevent submittal
if (this.policiesEnabledMap.has(PolicyType.RequireSso)
&& this.policiesEnabledMap.get(PolicyType.RequireSso)) {
this.toasterService.popAsync('error', null, this.i18nService.t('disableRequireSsoError'));
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">
@@ -56,9 +63,132 @@
<small>{{'ownerDesc' | i18n}}</small>
</label>
</div>
<div class="form-check mt-2 form-check-block">
<input class="form-check-input" type="radio" name="userType" id="userTypeCustom"
[value]="organizationUserType.Custom" [(ngModel)]="type">
<label class="form-check-label" for="userTypeCustom">
{{'custom' | i18n}}
<small>{{'customDesc' | i18n}}</small>
</label>
</div>
<ng-container *ngIf="customUserTypeSelected">
<h3 class="mt-4 d-flex">
{{'permissions' | i18n}}
</h3>
<div class="row">
<div class="col-6">
<div class="mb-3">
<label class="font-weight-bold mb-0">Manager Permissions</label>
<hr class="my-0 mr-2" />
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="manageAssignedCollections"
id="manageAssignedCollections"
[(ngModel)]="permissions.manageAssignedCollections">
<label class="form-check-label font-weight-normal"
for="manageAssignedCollections">
{{'manageAssignedCollections' | i18n}}
</label>
</div>
</div>
</div>
</div>
<div class="col-6">
<div class="mb-3">
<label class="font-weight-bold mb-0">Admin Permissions</label>
<hr class="my-0 mr-2" />
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="accessBusinessPortal"
id="accessBusinessPortal" [(ngModel)]="permissions.accessBusinessPortal">
<label class="form-check-label font-weight-normal" for="accessBusinessPortal">
{{'accessBusinessPortal' | i18n}}
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="accessEventLogs"
id="accessEventLogs" [(ngModel)]="permissions.accessEventLogs">
<label class="form-check-label font-weight-normal" for="accessEventLogs">
{{'accessEventLogs' | i18n}}
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="accessImportExport"
id="accessImportExport" [(ngModel)]="permissions.accessImportExport">
<label class="form-check-label font-weight-normal" for="accessImportExport">
{{'accessImportExport' | i18n}}
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="accessReports"
id="accessReports" [(ngModel)]="permissions.accessReports">
<label class="form-check-label font-weight-normal" for="accessReports">
{{'accessReports' | i18n}}
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="manageAllCollections"
id="manageAllCollections" [(ngModel)]="permissions.manageAllCollections">
<label class="form-check-label font-weight-normal" for="manageAllCollections">
{{'manageAllCollections' | i18n}}
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="manageGroups"
id="manageGroups" [(ngModel)]="permissions.manageGroups">
<label class="form-check-label font-weight-normal" for="manageGroups">
{{'manageGroups' | i18n}}
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="manageSso"
id="managePolicies" [(ngModel)]="permissions.manageSso">
<label class="form-check-label font-weight-normal" for="manageSso">
{{'manageSso' | i18n}}
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="managePolicies"
id="managePolicies" [(ngModel)]="permissions.managePolicies">
<label class="form-check-label font-weight-normal" for="managePolicies">
{{'managePolicies' | i18n}}
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="manageUsers"
id="manageUsers" [(ngModel)]="permissions.manageUsers">
<label class="form-check-label font-weight-normal" for="manageUsers">
{{'manageUsers' | i18n}}
</label>
</div>
</div>
</div>
</div>
</div>
</ng-container>
<h3 class="mt-4 d-flex">
<div class="mb-2">
<div class="mb-3">
{{'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 +224,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 +237,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

@@ -23,6 +23,7 @@ import { CollectionDetailsResponse } from 'jslib/models/response/collectionRespo
import { CollectionView } from 'jslib/models/view/collectionView';
import { OrganizationUserType } from 'jslib/enums/organizationUserType';
import { PermissionsApi } from 'jslib/models/api/permissionsApi';
@Component({
selector: 'app-user-add-edit',
@@ -40,12 +41,18 @@ export class UserAddEditComponent implements OnInit {
title: string;
emails: string;
type: OrganizationUserType = OrganizationUserType.User;
permissions = new PermissionsApi();
showCustom = false;
access: 'all' | 'selected' = 'selected';
collections: CollectionView[] = [];
formPromise: Promise<any>;
deletePromise: Promise<any>;
organizationUserType = OrganizationUserType;
get customUserTypeSelected(): boolean {
return this.type === OrganizationUserType.Custom;
}
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private collectionService: CollectionService, private platformUtilsService: PlatformUtilsService) { }
@@ -61,12 +68,16 @@ export class UserAddEditComponent implements OnInit {
const user = await this.apiService.getOrganizationUser(this.organizationId, this.organizationUserId);
this.access = user.accessAll ? 'all' : 'selected';
this.type = user.type;
if (user.type === OrganizationUserType.Custom) {
this.permissions = user.permissions;
}
if (user.collections != null && this.collections != null) {
user.collections.forEach((s) => {
const collection = this.collections.filter((c) => c.id === s.id);
if (collection != null && collection.length > 0) {
(collection[0] as any).checked = true;
collection[0].readOnly = s.readOnly;
collection[0].hidePasswords = s.hidePasswords;
}
});
}
@@ -96,11 +107,45 @@ export class UserAddEditComponent implements OnInit {
this.collections.forEach((c) => this.check(c, select));
}
setRequestPermissions(p: PermissionsApi, clearPermissions: boolean) {
p.accessBusinessPortal = clearPermissions ?
false :
this.permissions.accessBusinessPortal;
p.accessEventLogs = this.permissions.accessEventLogs = clearPermissions ?
false :
this.permissions.accessEventLogs;
p.accessImportExport = clearPermissions ?
false :
this.permissions.accessImportExport;
p.accessReports = clearPermissions ?
false :
this.permissions.accessReports;
p.manageAllCollections = clearPermissions ?
false :
this.permissions.manageAllCollections;
p.manageAssignedCollections = clearPermissions ?
false :
this.permissions.manageAssignedCollections;
p.manageGroups = clearPermissions ?
false :
this.permissions.manageGroups;
p.manageSso = clearPermissions ?
false :
this.permissions.manageSso;
p.managePolicies = clearPermissions ?
false :
this.permissions.managePolicies;
p.manageUsers = clearPermissions ?
false :
this.permissions.manageUsers;
return p;
}
async submit() {
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 {
@@ -109,6 +154,7 @@ export class UserAddEditComponent implements OnInit {
request.accessAll = this.access === 'all';
request.type = this.type;
request.collections = collections;
request.permissions = this.setRequestPermissions(request.permissions ?? new PermissionsApi(), request.type !== OrganizationUserType.Custom);
this.formPromise = this.apiService.putOrganizationUser(this.organizationId, this.organizationUserId,
request);
} else {
@@ -116,6 +162,7 @@ export class UserAddEditComponent implements OnInit {
request.emails = this.emails.trim().split(/\s*,\s*/);
request.accessAll = this.access === 'all';
request.type = this.type;
request.permissions = this.setRequestPermissions(request.permissions ?? new PermissionsApi(), request.type !== OrganizationUserType.Custom);
request.collections = collections;
this.formPromise = this.apiService.postOrganizationUserInvite(this.organizationId, request);
}
@@ -147,4 +194,5 @@ export class UserAddEditComponent implements OnInit {
this.onDeletedUser.emit();
} catch { }
}
}

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

@@ -16,8 +16,6 @@ import { EventType } from 'jslib/enums/eventType';
templateUrl: '../../tools/export.component.html',
})
export class ExportComponent extends BaseExportComponent {
organizationId: string;
constructor(cryptoService: CryptoService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, exportService: ExportService,
eventService: EventService, private route: ActivatedRoute) {

View File

@@ -14,12 +14,15 @@ import {
} from '../../tools/exposed-passwords-report.component';
import { CipherView } from 'jslib/models/view/cipherView';
import { Cipher } from 'jslib/models/domain/cipher';
@Component({
selector: 'app-exposed-passwords-report',
templateUrl: '../../tools/exposed-passwords-report.component.html',
})
export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportComponent {
manageableCiphers: Cipher[];
constructor(cipherService: CipherService, auditService: AuditService,
componentFactoryResolver: ComponentFactoryResolver, messagingService: MessagingService,
userService: UserService, private route: ActivatedRoute) {
@@ -29,6 +32,7 @@ export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportC
ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.organization = await this.userService.getOrganization(params.organizationId);
this.manageableCiphers = await this.cipherService.getAll();
super.ngOnInit();
});
}
@@ -36,4 +40,8 @@ export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportC
getAllCiphers(): Promise<CipherView[]> {
return this.cipherService.getAllFromApiForOrganization(this.organization.id);
}
canManageCipher(c: CipherView): boolean {
return this.manageableCiphers.some(x => x.id === c.id);
}
}

View File

@@ -8,6 +8,8 @@ import { CipherService } from 'jslib/abstractions/cipher.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { UserService } from 'jslib/abstractions/user.service';
import { Cipher } from 'jslib/models/domain/cipher';
import { CipherView } from 'jslib/models/view/cipherView';
import {
@@ -19,6 +21,8 @@ import {
templateUrl: '../../tools/reused-passwords-report.component.html',
})
export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportComponent {
manageableCiphers: Cipher[];
constructor(cipherService: CipherService, componentFactoryResolver: ComponentFactoryResolver,
messagingService: MessagingService, userService: UserService,
private route: ActivatedRoute) {
@@ -28,6 +32,7 @@ export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportCom
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.organization = await this.userService.getOrganization(params.organizationId);
this.manageableCiphers = await this.cipherService.getAll();
await super.ngOnInit();
});
}
@@ -35,4 +40,8 @@ export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportCom
getAllCiphers(): Promise<CipherView[]> {
return this.cipherService.getAllFromApiForOrganization(this.organization.id);
}
canManageCipher(c: CipherView): boolean {
return this.manageableCiphers.some(x => x.id === c.id);
}
}

View File

@@ -1,48 +1,54 @@
<div class="container page-content">
<div class="row">
<div class="col-3">
<div class="card mb-4">
<div class="card-header">{{'tools' | i18n}}</div>
<div class="list-group list-group-flush">
<a routerLink="import" class="list-group-item" routerLinkActive="active">
{{'importData' | i18n}}
</a>
<a routerLink="export" class="list-group-item" routerLinkActive="active">
{{'exportVault' | i18n}}
</a>
</div>
</div>
<div class="card">
<div class="card-header d-flex">
{{'reports' | i18n}}
<div class="ml-auto">
<a href="#" appStopClick class="badge badge-primary" *ngIf="!accessReports"
(click)="upgradeOrganization()">
{{'upgrade' | i18n}}
<ng-container *ngIf="loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</ng-container>
<ng-container *ngIf="!loading">
<div class="row">
<div class="col-3">
<div class="card mb-4" *ngIf="organization.canAccessImportExport">
<div class="card-header">{{'tools' | i18n}}</div>
<div class="list-group list-group-flush">
<a routerLink="import" class="list-group-item" routerLinkActive="active">
{{'importData' | i18n}}
</a>
<a routerLink="export" class="list-group-item" routerLinkActive="active">
{{'exportVault' | i18n}}
</a>
</div>
</div>
<div class="list-group list-group-flush">
<a routerLink="exposed-passwords-report" class="list-group-item" routerLinkActive="active">
{{'exposedPasswordsReport' | i18n}}
</a>
<a routerLink="reused-passwords-report" class="list-group-item" routerLinkActive="active">
{{'reusedPasswordsReport' | i18n}}
</a>
<a routerLink="weak-passwords-report" class="list-group-item" routerLinkActive="active">
{{'weakPasswordsReport' | i18n}}
</a>
<a routerLink="unsecured-websites-report" class="list-group-item" routerLinkActive="active">
{{'unsecuredWebsitesReport' | i18n}}
</a>
<a routerLink="inactive-two-factor-report" class="list-group-item" routerLinkActive="active">
{{'inactive2faReport' | i18n}}
</a>
<div class="card" *ngIf="organization.canAccessReports">
<div class="card-header d-flex">
{{'reports' | i18n}}
<div class="ml-auto">
<a href="#" appStopClick class="badge badge-primary" *ngIf="!accessReports"
(click)="upgradeOrganization()">
{{'upgrade' | i18n}}
</a>
</div>
</div>
<div class="list-group list-group-flush">
<a routerLink="exposed-passwords-report" class="list-group-item" routerLinkActive="active">
{{'exposedPasswordsReport' | i18n}}
</a>
<a routerLink="reused-passwords-report" class="list-group-item" routerLinkActive="active">
{{'reusedPasswordsReport' | i18n}}
</a>
<a routerLink="weak-passwords-report" class="list-group-item" routerLinkActive="active">
{{'weakPasswordsReport' | i18n}}
</a>
<a routerLink="unsecured-websites-report" class="list-group-item" routerLinkActive="active">
{{'unsecuredWebsitesReport' | i18n}}
</a>
<a routerLink="inactive-two-factor-report" class="list-group-item" routerLinkActive="active">
{{'inactive2faReport' | i18n}}
</a>
</div>
</div>
</div>
<div class="col-9">
<router-outlet></router-outlet>
</div>
</div>
<div class="col-9">
<router-outlet></router-outlet>
</div>
</div>
</ng-container>
</div>

View File

@@ -13,6 +13,7 @@ import { UserService } from 'jslib/abstractions/user.service';
export class ToolsComponent {
organization: Organization;
accessReports = false;
loading = true;
constructor(private route: ActivatedRoute, private userService: UserService,
private messagingService: MessagingService) { }
@@ -23,6 +24,7 @@ export class ToolsComponent {
// TODO: Maybe we want to just make sure they are not on a free plan? Just compare useTotp for now
// since all paid plans include useTotp
this.accessReports = this.organization.useTotp;
this.loading = false;
});
}

View File

@@ -9,6 +9,8 @@ import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { UserService } from 'jslib/abstractions/user.service';
import { Cipher } from 'jslib/models/domain/cipher';
import { CipherView } from 'jslib/models/view/cipherView';
import {
@@ -20,6 +22,8 @@ import {
templateUrl: '../../tools/weak-passwords-report.component.html',
})
export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportComponent {
manageableCiphers: Cipher[];
constructor(cipherService: CipherService, passwordGenerationService: PasswordGenerationService,
componentFactoryResolver: ComponentFactoryResolver, messagingService: MessagingService,
userService: UserService, private route: ActivatedRoute) {
@@ -29,6 +33,7 @@ export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportCompone
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.organization = await this.userService.getOrganization(params.organizationId);
this.manageableCiphers = await this.cipherService.getAll();
await super.ngOnInit();
});
}
@@ -36,4 +41,8 @@ export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportCompone
getAllCiphers(): Promise<CipherView[]> {
return this.cipherService.getAllFromApiForOrganization(this.organization.id);
}
canManageCipher(c: CipherView): boolean {
return this.manageableCiphers.some(x => x.id === c.id);
}
}

View File

@@ -10,6 +10,7 @@ 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 { StateService } from 'jslib/abstractions/state.service';
import { TotpService } from 'jslib/abstractions/totp.service';
import { UserService } from 'jslib/abstractions/user.service';
@@ -36,16 +37,16 @@ export class AddEditComponent extends BaseAddEditComponent {
userService: UserService, collectionService: CollectionService,
totpService: TotpService, passwordGenerationService: PasswordGenerationService,
private apiService: ApiService, messagingService: MessagingService,
eventService: EventService) {
eventService: EventService, policyService: PolicyService) {
super(cipherService, folderService, i18nService, platformUtilsService, auditService, stateService,
userService, collectionService, totpService, passwordGenerationService, messagingService,
eventService);
eventService, policyService);
}
protected allowOwnershipAssignment() {
if (this.ownershipOptions != null && this.ownershipOptions.length > 1) {
if (this.ownershipOptions != null && (this.ownershipOptions.length > 1 || !this.allowPersonal)) {
if (this.organization != null) {
return this.cloneMode && this.organization.isAdmin;
return this.cloneMode && this.organization.canManageAllCollections;
} else {
return !this.editMode || this.cloneMode;
}
@@ -54,14 +55,14 @@ export class AddEditComponent extends BaseAddEditComponent {
}
protected loadCollections() {
if (!this.organization.isAdmin) {
if (!this.organization.canManageAllCollections) {
return super.loadCollections();
}
return Promise.resolve(this.collections);
}
protected async loadCipher() {
if (!this.organization.isAdmin) {
if (!this.organization.canManageAllCollections) {
return await super.loadCipher();
}
const response = await this.apiService.getCipherAdmin(this.cipherId);
@@ -71,14 +72,14 @@ export class AddEditComponent extends BaseAddEditComponent {
}
protected encryptCipher() {
if (!this.organization.isAdmin) {
if (!this.organization.canManageAllCollections) {
return super.encryptCipher();
}
return this.cipherService.encrypt(this.cipher, null, this.originalCipher);
}
protected async saveCipher(cipher: Cipher) {
if (!this.organization.isAdmin || cipher.organizationId == null) {
if (!this.organization.canManageAllCollections || cipher.organizationId == null) {
return super.saveCipher(cipher);
}
if (this.editMode && !this.cloneMode) {
@@ -91,9 +92,10 @@ export class AddEditComponent extends BaseAddEditComponent {
}
protected async deleteCipher() {
if (!this.organization.isAdmin) {
if (!this.organization.canManageAllCollections) {
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

@@ -29,13 +29,13 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
}
protected async reupload(attachment: AttachmentView) {
if (this.organization.isAdmin && this.showFixOldAttachments(attachment)) {
if (this.organization.canManageAllCollections && this.showFixOldAttachments(attachment)) {
await super.reuploadCipherAttachment(attachment, true);
}
}
protected async loadCipher() {
if (!this.organization.isAdmin) {
if (!this.organization.canManageAllCollections) {
return await super.loadCipher();
}
const response = await this.apiService.getCipherAdmin(this.cipherId);
@@ -43,17 +43,17 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
}
protected saveCipherAttachment(file: File) {
return this.cipherService.saveAttachmentWithServer(this.cipherDomain, file, this.organization.isAdmin);
return this.cipherService.saveAttachmentWithServer(this.cipherDomain, file, this.organization.canManageAllCollections);
}
protected deleteCipherAttachment(attachmentId: string) {
if (!this.organization.isAdmin) {
if (!this.organization.canManageAllCollections) {
return super.deleteCipherAttachment(attachmentId);
}
return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId);
}
protected showFixOldAttachments(attachment: AttachmentView) {
return attachment.key == null && this.organization.isAdmin;
return attachment.key == null && this.organization.canManageAllCollections;
}
}

View File

@@ -13,6 +13,8 @@ import { EventService } from 'jslib/abstractions/event.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SearchService } from 'jslib/abstractions/search.service';
import { TotpService } from 'jslib/abstractions/totp.service';
import { UserService } from 'jslib/abstractions/user.service';
import { Organization } from 'jslib/models/domain/organization';
import { CipherView } from 'jslib/models/view/cipherView';
@@ -34,14 +36,14 @@ export class CiphersComponent extends BaseCiphersComponent {
constructor(searchService: SearchService, analytics: Angulartics2,
toasterService: ToasterService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, cipherService: CipherService,
private apiService: ApiService, eventService: EventService) {
private apiService: ApiService, eventService: EventService, totpService: TotpService, userService: UserService) {
super(searchService, analytics, toasterService, i18nService, platformUtilsService,
cipherService, eventService);
cipherService, eventService, totpService, userService);
}
async load(filter: (cipher: CipherView) => boolean = null) {
if (!this.organization.isAdmin) {
await super.load(filter);
if (!this.organization.canManageAllCollections) {
await super.load(filter, this.deleted);
return;
}
this.accessEvents = this.organization.useEvents;
@@ -51,7 +53,7 @@ export class CiphersComponent extends BaseCiphersComponent {
}
async applyFilter(filter: (cipher: CipherView) => boolean = null) {
if (this.organization.isAdmin) {
if (this.organization.canManageAllCollections) {
await super.applyFilter(filter);
} else {
const f = (c: CipherView) => c.organizationId === this.organization.id && (filter == null || filter(c));
@@ -60,38 +62,40 @@ export class CiphersComponent extends BaseCiphersComponent {
}
async search(timeout: number = null) {
if (!this.organization.isAdmin) {
if (!this.organization.canManageAllCollections) {
return super.search(timeout);
}
this.searchPending = false;
let filteredCiphers = this.allCiphers;
if (this.filter != null) {
filteredCiphers = filteredCiphers.filter(this.filter);
}
if (this.searchText == null || this.searchText.trim().length < 2) {
this.ciphers = filteredCiphers;
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);
if (!this.organization.canManageAllCollections) {
return super.deleteCipher(id, this.deleted);
}
return this.apiService.deleteCipherAdmin(id);
return this.deleted ? this.apiService.deleteCipherAdmin(id) : this.apiService.putDeleteCipherAdmin(id);
}
protected showFixOldAttachments(c: CipherView) {
return this.organization.isAdmin && c.hasOldAttachments;
return this.organization.canManageAllCollections && c.hasOldAttachments;
}
}

View File

@@ -28,7 +28,7 @@ export class CollectionsComponent extends BaseCollectionsComponent {
}
protected async loadCipher() {
if (!this.organization.isAdmin) {
if (!this.organization.canManageAllCollections) {
return await super.loadCipher();
}
const response = await this.apiService.getCipherAdmin(this.cipherId);
@@ -36,21 +36,21 @@ export class CollectionsComponent extends BaseCollectionsComponent {
}
protected loadCipherCollections() {
if (!this.organization.isAdmin) {
if (!this.organization.canManageAllCollections) {
return super.loadCipherCollections();
}
return this.collectionIds;
}
protected loadCollections() {
if (!this.organization.isAdmin) {
if (!this.organization.canManageAllCollections) {
return super.loadCollections();
}
return Promise.resolve(this.collections);
}
protected saveCollections() {
if (this.organization.isAdmin) {
if (this.organization.canManageAllCollections) {
const request = new CipherCollectionsRequest(this.cipherDomain.collectionIds);
return this.apiService.putCipherCollectionsAdmin(this.cipherId, request);
} else {

View File

@@ -29,7 +29,7 @@ export class GroupingsComponent extends BaseGroupingsComponent {
}
async loadCollections() {
if (!this.organization.isAdmin) {
if (!this.organization.canManageAllCollections) {
await super.loadCollections(this.organization.id);
return;
}

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,14 +62,14 @@ 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;
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
this.ciphersComponent.searchText = this.groupingsComponent.searchText = qParams.search;
if (!this.organization.isAdmin) {
if (!this.organization.canManageAllCollections) {
await this.syncService.fullSync(false);
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
this.ngZone.run(async () => {
@@ -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);
@@ -208,7 +233,7 @@ export class VaultComponent implements OnInit, OnDestroy {
this.modal = this.collectionsModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<CollectionsComponent>(CollectionsComponent, this.collectionsModalRef);
if (this.organization.isAdmin) {
if (this.organization.canManageAllCollections) {
childComponent.collectionIds = cipher.collectionIds;
childComponent.collections = this.groupingsComponent.collections.filter((c) => !c.readOnly);
}
@@ -228,7 +253,7 @@ export class VaultComponent implements OnInit, OnDestroy {
const component = this.editCipher(null);
component.organizationId = this.organization.id;
component.type = this.type;
if (this.organization.isAdmin) {
if (this.organization.canManageAllCollections) {
component.collections = this.groupingsComponent.collections.filter((c) => !c.readOnly);
}
if (this.collectionId != null) {
@@ -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;
@@ -267,7 +296,7 @@ export class VaultComponent implements OnInit, OnDestroy {
const component = this.editCipher(cipher);
component.cloneMode = true;
component.organizationId = this.organization.id;
if (this.organization.isAdmin) {
if (this.organization.canManageAllCollections) {
component.collections = this.groupingsComponent.collections.filter((c) => !c.readOnly);
}
// Regardless of Admin state, the collection Ids need to passed manually as they are not assigned value
@@ -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,65 @@
<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 && unavailable">
{{'sendAccessUnavailable' | i18n}}
</div>
<div class="card-body" *ngIf="!loading && error">
{{'unexpectedError' | i18n}}
</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></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,143 @@
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;
unavailable = false;
error = 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;
}
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() {
this.unavailable = false;
this.error = false;
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;
} else if (e.statusCode === 404) {
this.unavailable = true;
} else {
this.error = true;
}
}
}
this.loading = false;
}
}

View File

@@ -0,0 +1,159 @@
<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
(change)='typeChanged()'>
<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">{{'textHiddenByDefault' | 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-5">{{'options' | i18n}}</h3>
<div class="row">
<div class="col-6 form-group">
<label for="deletionDate">{{'deletionDate' | i18n}}</label>
<div *ngIf="!editMode">
<select id="deletionDate" name="DeletionDateSelect" [(ngModel)]="deletionDateSelect"
class="form-control" required>
<option *ngFor="let o of deletionDateOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
<input id="deletionDateCustom" class="form-control mt-1" type="datetime-local"
name="DeletionDate" [(ngModel)]="deletionDate" required *ngIf="deletionDateSelect === 0"
placeholder="MM/DD/YYYY HH:MM AM/PM">
</div>
<div *ngIf="editMode">
<input id="deletionDate" class="form-control" type="datetime-local" name="DeletionDate"
[(ngModel)]="deletionDate" required placeholder="MM/DD/YYYY HH:MM AM/PM">
</div>
<div class="form-text text-muted small">{{'deletionDateDesc' | i18n}}</div>
</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" *ngIf="editMode">
{{'clear' | i18n}}
</a>
</div>
<div *ngIf="!editMode">
<select id="expirationDate" name="ExpirationDateSelect" [(ngModel)]="expirationDateSelect"
class="form-control" required>
<option *ngFor="let o of expirationDateOptions" [ngValue]="o.value">{{o.name}}
</option>
</select>
<input id="expirationDateCustom" class="form-control mt-1" type="datetime-local"
name="ExpirationDate" [(ngModel)]="expirationDate" required
*ngIf="expirationDateSelect === 0" placeholder="MM/DD/YYYY HH:MM AM/PM">
</div>
<div *ngIf="editMode">
<input id="expirationDate" class="form-control" type="datetime-local" name="ExpirationDate"
[(ngModel)]="expirationDate" placeholder="MM/DD/YYYY HH:MM AM/PM">
</div>
<div class="form-text text-muted small">{{'expirationDateDesc' | i18n}}</div>
</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" min="1">
<div class="form-text text-muted small">{{'maxAccessCountDesc' | i18n}}</div>
</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 class="form-text text-muted small">{{'sendPasswordDesc' | i18n}}</div>
</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 class="form-text text-muted small">{{'sendNotesDesc' | i18n}}</div>
</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">{{'disableThisSend' | i18n}}</label>
</div>
</div>
<h3 class="mt-5" *ngIf="link">{{'share' | i18n}}</h3>
<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,236 @@
import { DatePipe } from '@angular/common';
import {
EventEmitter,
Input,
Output,
} from '@angular/core';
import { Component } from '@angular/core';
import { SendType } from 'jslib/enums/sendType';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SendService } from 'jslib/abstractions/send.service';
import { UserService } from 'jslib/abstractions/user.service';
import { SendFileView } from 'jslib/models/view/sendFileView';
import { SendTextView } from 'jslib/models/view/sendTextView';
import { SendView } from 'jslib/models/view/sendView';
import { Send } from 'jslib/models/domain/send';
@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[];
deletionDateOptions: any[];
expirationDateOptions: any[];
deletionDateSelect = 168;
expirationDateSelect: number = null;
canAccessPremium = true;
premiumRequiredAlertShown = false;
constructor(private i18nService: I18nService, private platformUtilsService: PlatformUtilsService,
private environmentService: EnvironmentService, private datePipe: DatePipe,
private sendService: SendService, private userService: UserService,
private messagingService: MessagingService) {
this.typeOptions = [
{ name: i18nService.t('sendTypeFile'), value: SendType.File },
{ name: i18nService.t('sendTypeText'), value: SendType.Text },
];
this.deletionDateOptions = this.expirationDateOptions = [
{ name: i18nService.t('oneHour'), value: 1 },
{ name: i18nService.t('oneDay'), value: 24 },
{ name: i18nService.t('days', '2'), value: 48 },
{ name: i18nService.t('days', '3'), value: 72 },
{ name: i18nService.t('days', '7'), value: 168 },
{ name: i18nService.t('days', '30'), value: 720 },
{ name: i18nService.t('custom'), value: 0 },
];
this.expirationDateOptions = [
{ name: i18nService.t('never'), value: null },
].concat([...this.deletionDateOptions]);
}
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');
}
this.canAccessPremium = await this.userService.canAccessPremium();
if (!this.canAccessPremium) {
this.type = SendType.Text;
}
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.dateToString(this.send.deletionDate);
this.expirationDate = this.dateToString(this.send.expirationDate);
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;
}
}
if (!this.editMode) {
const now = new Date();
if (this.deletionDateSelect > 0) {
const d = new Date();
d.setHours(now.getHours() + this.deletionDateSelect);
this.deletionDate = this.dateToString(d);
}
if (this.expirationDateSelect != null && this.expirationDateSelect > 0) {
const d = new Date();
d.setHours(now.getHours() + this.expirationDateSelect);
this.expirationDate = this.dateToString(d);
}
}
const encSend = await this.encryptSend(file);
try {
this.formPromise = this.sendService.saveWithServer(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.sendService.deleteWithServer(this.send.id);
await this.deletePromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedSend'));
await this.load();
this.onDeletedSend.emit(this.send);
} catch { }
}
typeChanged() {
if (!this.canAccessPremium && this.send.type === SendType.File && !this.premiumRequiredAlertShown) {
this.premiumRequiredAlertShown = true;
this.messagingService.send('premiumRequired');
}
}
protected async loadSend(): Promise<Send> {
return this.sendService.get(this.sendId);
}
protected async encryptSend(file: File): Promise<[Send, ArrayBuffer]> {
const sendData = await this.sendService.encrypt(this.send, file, this.password, null);
// Parse dates
try {
sendData[0].deletionDate = this.deletionDate == null ? null : new Date(this.deletionDate);
} catch {
sendData[0].deletionDate = null;
}
try {
sendData[0].expirationDate = this.expirationDate == null ? null : new Date(this.expirationDate);
} catch {
sendData[0].expirationDate = null;
}
return sendData;
}
protected dateToString(d: Date) {
return d == null ? null : this.datePipe.transform(d, 'yyyy-MM-ddTHH:mm');
}
}

View File

@@ -0,0 +1,130 @@
<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>
<span appStopClick class="badge badge-secondary" *ngIf="s.disabled">
{{'disabled' | i18n}}
</span>
<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>
<ng-container *ngIf="s.maxAccessCountReached">
<i class="fa fa-ban" appStopProp title="{{'maxAccessCountReached' | i18n}}"
aria-hidden="true"></i>
<span class="sr-only">{{'maxAccessCountReached' | i18n}}</span>
</ng-container>
<ng-container *ngIf="s.expired">
<i class="fa fa-clock-o" appStopProp title="{{'expired' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'expired' | i18n}}</span>
</ng-container>
<ng-container *ngIf="s.pendingDelete">
<i class="fa fa-trash" appStopProp title="{{'pendingDeletion' | i18n}}"
aria-hidden="true"></i>
<span class="sr-only">{{'pendingDeletion' | 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,207 @@
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 { SendService } from 'jslib/abstractions/send.service';
@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 sendService: SendService,
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 sends = await this.sendService.getAllDecrypted();
this.sends = sends;
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.sendService.removePasswordWithServer(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.sendService.deleteWithServer(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;
@@ -147,6 +153,8 @@ export class EventService {
case EventType.OrganizationUser_UpdatedGroups:
msg = this.i18nService.t('editedGroupsForUser', this.formatOrgUserId(ev));
break;
case EventType.OrganizationUser_UnlinkedSso:
msg = this.i18nService.t('unlinkedSsoUser', this.formatOrgUserId(ev));
// Org
case EventType.Organization_Updated:
msg = this.i18nService.t('editedOrgSettings');
@@ -159,6 +167,11 @@ export class EventService {
msg = this.i18nService.t('exportedOrganizationVault');
break;
*/
// Policies
case EventType.Policy_Updated:
msg = this.i18nService.t('modifiedPolicy', this.formatPolicyId(ev));
break;
default:
break;
}
@@ -245,6 +258,13 @@ export class EventService {
return a.outerHTML;
}
private formatPolicyId(ev: EventResponse) {
const shortId = this.getShortId(ev.policyId);
const a = this.makeAnchor(shortId);
a.setAttribute('href', '#/organizations/' + ev.organizationId + '/manage/policies?policyId=' + ev.policyId);
return a.outerHTML;
}
private makeAnchor(shortId: string) {
const a = document.createElement('a');
a.title = this.i18nService.t('view');

View File

@@ -7,20 +7,32 @@ import {
import { UserService } from 'jslib/abstractions/user.service';
import { OrganizationUserType } from 'jslib/enums/organizationUserType';
import { Permissions } from 'jslib/enums/permissions';
@Injectable()
export class OrganizationTypeGuardService implements CanActivate {
constructor(private userService: UserService, private router: Router) { }
async canActivate(route: ActivatedRouteSnapshot) {
const org = await this.userService.getOrganization(route.parent.params.organizationId);
const allowedTypes = route.data == null ? null : route.data.allowedTypes as OrganizationUserType[];
if (allowedTypes == null || allowedTypes.indexOf(org.type) === -1) {
this.router.navigate(['/organizations', org.id]);
return false;
const org = await this.userService.getOrganization(route.params.organizationId);
const permissions = route.data == null ? null : route.data.permissions as Permissions[];
if (
(permissions.indexOf(Permissions.AccessBusinessPortal) !== -1 && org.canAccessBusinessPortal) ||
(permissions.indexOf(Permissions.AccessEventLogs) !== -1 && org.canAccessEventLogs) ||
(permissions.indexOf(Permissions.AccessImportExport) !== -1 && org.canAccessImportExport) ||
(permissions.indexOf(Permissions.AccessReports) !== -1 && org.canAccessReports) ||
(permissions.indexOf(Permissions.ManageAllCollections) !== -1 && org.canManageAllCollections) ||
(permissions.indexOf(Permissions.ManageAssignedCollections) !== -1 && org.canManageAssignedCollections) ||
(permissions.indexOf(Permissions.ManageGroups) !== -1 && org.canManageGroups) ||
(permissions.indexOf(Permissions.ManageOrganization) !== -1 && org.isOwner) ||
(permissions.indexOf(Permissions.ManagePolicies) !== -1 && org.canManagePolicies) ||
(permissions.indexOf(Permissions.ManageUsers) !== -1 && org.canManageUsers)
) {
return true;
}
return true;
this.router.navigate(['/organizations', org.id]);
return false;
}
}

View File

@@ -30,6 +30,7 @@ import { AuditService } from 'jslib/services/audit.service';
import { AuthService } from 'jslib/services/auth.service';
import { CipherService } from 'jslib/services/cipher.service';
import { CollectionService } from 'jslib/services/collection.service';
import { ConsoleLogService } from 'jslib/services/consoleLog.service';
import { ConstantsService } from 'jslib/services/constants.service';
import { ContainerService } from 'jslib/services/container.service';
import { CryptoService } from 'jslib/services/crypto.service';
@@ -38,17 +39,18 @@ 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';
import { SearchService } from 'jslib/services/search.service';
import { SendService } from 'jslib/services/send.service';
import { SettingsService } from 'jslib/services/settings.service';
import { StateService } from 'jslib/services/state.service';
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 +67,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';
@@ -75,6 +76,7 @@ import {
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from 'jslib/abstractions/platformUtils.service';
import { PolicyService as PolicyServiceAbstraction } from 'jslib/abstractions/policy.service';
import { SearchService as SearchServiceAbstraction } from 'jslib/abstractions/search.service';
import { SendService as SendServiceAbstraction } from 'jslib/abstractions/send.service';
import { SettingsService as SettingsServiceAbstraction } from 'jslib/abstractions/settings.service';
import { StateService as StateServiceAbstraction } from 'jslib/abstractions/state.service';
import { StorageService as StorageServiceAbstraction } from 'jslib/abstractions/storage.service';
@@ -82,6 +84,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();
@@ -92,8 +95,10 @@ const storageService: StorageServiceAbstraction = new HtmlStorageService(platfor
const secureStorageService: StorageServiceAbstraction = new MemoryStorageService();
const cryptoFunctionService: CryptoFunctionServiceAbstraction = new WebCryptoFunctionService(window,
platformUtilsService);
const consoleLogService = new ConsoleLogService(false);
const cryptoService = new CryptoService(storageService,
platformUtilsService.isDev() ? storageService : secureStorageService, cryptoFunctionService);
platformUtilsService.isDev() ? storageService : secureStorageService, cryptoFunctionService, platformUtilsService,
consoleLogService);
const tokenService = new TokenService(storageService);
const appIdService = new AppIdService(storageService);
const apiService = new ApiService(tokenService, platformUtilsService,
@@ -106,22 +111,26 @@ 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, consoleLogService);
const policyService = new PolicyService(userService, storageService);
const lockService = new LockService(cipherService, folderService, collectionService,
cryptoService, platformUtilsService, storageService, messagingService, searchService, userService, null);
const sendService = new SendService(cryptoService, userService, apiService, storageService,
i18nService, cryptoFunctionService);
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 }));
sendService, async (expired: boolean) => messagingService.send('logout', { expired: expired }));
const passwordGenerationService = new PasswordGenerationService(cryptoService, storageService, policyService);
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,
consoleLogService);
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 }), consoleLogService);
const environmentService = new EnvironmentService(apiService, storageService, notificationsService);
const auditService = new AuditService(cryptoFunctionService, apiService);
const eventLoggingService = new EventLoggingService(storageService, apiService, userService, cipherService);
@@ -139,6 +148,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 +167,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 +216,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 },
@@ -215,6 +226,7 @@ export function initFactory(): Function {
{ provide: CryptoFunctionServiceAbstraction, useValue: cryptoFunctionService },
{ provide: EventLoggingServiceAbstraction, useValue: eventLoggingService },
{ provide: PolicyServiceAbstraction, useValue: policyService },
{ provide: SendServiceAbstraction, useValue: sendService },
{
provide: APP_INITIALIZER,
useFactory: initFactory,

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,11 +12,18 @@ 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 { EmergencyAccessStatusType } from 'jslib/enums/emergencyAccessStatusType';
import { Utils } from 'jslib/misc/utils';
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';
import { EmergencyAccessUpdateRequest } from 'jslib/models/request/emergencyAccessUpdateRequest';
import { FolderWithIdRequest } from 'jslib/models/request/folderWithIdRequest';
import { PasswordRequest } from 'jslib/models/request/passwordRequest';
import { UpdateKeyRequest } from 'jslib/models/request/updateKeyRequest';
@@ -31,136 +32,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() {
@@ -190,6 +73,7 @@ export class ChangePasswordComponent implements OnInit {
const result = await this.platformUtilsService.showDialog(
this.i18nService.t('updateEncryptionKeyWarning') + ' ' +
this.i18nService.t('updateEncryptionKeyExportWarning') + ' ' +
this.i18nService.t('rotateEncKeyConfirmation'), this.i18nService.t('rotateEncKeyTitle'),
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!result) {
@@ -198,13 +82,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) {
@@ -239,5 +164,32 @@ export class ChangePasswordComponent implements OnInit {
}
await this.apiService.postAccountKey(request);
await this.updateEmergencyAccesses(encKey[0]);
}
private async updateEmergencyAccesses(encKey: SymmetricCryptoKey) {
const emergencyAccess = await this.apiService.getEmergencyAccessTrusted();
const allowedStatuses = [
EmergencyAccessStatusType.Confirmed,
EmergencyAccessStatusType.RecoveryInitiated,
EmergencyAccessStatusType.RecoveryApproved,
];
const filteredAccesses = emergencyAccess.data.filter(d => allowedStatuses.includes(d.status));
for (const details of filteredAccesses) {
const publicKeyResponse = await this.apiService.getUserPublicKey(details.granteeId);
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey.buffer);
const updateRequest = new EmergencyAccessUpdateRequest();
updateRequest.type = details.type;
updateRequest.waitTimeDays = details.waitTimeDays;
updateRequest.keyEncrypted = encryptedKey.encryptedString;
await this.apiService.putEmergencyAccess(details.id, updateRequest);
}
}
}

View File

@@ -5,6 +5,9 @@ import {
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { PlanType } from 'jslib/enums/planType';
import { ProductType } from 'jslib/enums/productType';
import { OrganizationPlansComponent } from './organization-plans.component';
@Component({
@@ -12,14 +15,21 @@ 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) { }
ngOnInit() {
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
if (qParams.plan === 'families' || qParams.plan === 'teams' || qParams.plan === 'enterprise') {
this.orgPlansComponent.plan = qParams.plan;
if (qParams.plan === 'families') {
this.orgPlansComponent.plan = PlanType.FamiliesAnnually;
this.orgPlansComponent.product = ProductType.Families;
} else if (qParams.plan === 'teams') {
this.orgPlansComponent.plan = PlanType.TeamsAnnually;
this.orgPlansComponent.product = ProductType.Teams;
} else if (qParams.plan === 'enterprise') {
this.orgPlansComponent.plan = PlanType.EnterpriseAnnually;
this.orgPlansComponent.product = ProductType.Enterprise;
}
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();

View File

@@ -0,0 +1,76 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
<div class="modal-dialog modal-lg" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title" id="userAddEditTitle">
<span class="badge badge-primary" *ngIf="readOnly">{{'premium' | i18n}}</span>
{{title}}
<small class="text-muted" *ngIf="name">{{name}}</small>
</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" *ngIf="loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</div>
<div class="modal-body" *ngIf="!loading">
<ng-container *ngIf="!editMode">
<p>{{'inviteEmergencyContactDesc' | i18n}}</p>
<div class="form-group mb-4">
<label for="email">{{'email' | i18n}}</label>
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required>
</div>
</ng-container>
<h3>
{{'userAccess' | i18n}}
<a target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}"
href="https://bitwarden.com/help/article/emergency-access/#user-access">
<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="emergencyTypeView"
[value]="emergencyAccessType.View" [(ngModel)]="type">
<label class="form-check-label" for="emergencyTypeView">
{{'view' | i18n}}
<small>{{'viewDesc' | i18n}}</small>
</label>
</div>
<div class="form-check mt-2 form-check-block">
<input class="form-check-input" type="radio" name="userType" id="emergencyTypeTakeover"
[value]="emergencyAccessType.Takeover" [(ngModel)]="type" [disabled]="readOnly">
<label class="form-check-label" for="emergencyTypeTakeover">
{{'takeover' | i18n}}
<small>{{'takeoverDesc' | i18n}}</small>
</label>
</div>
<div class="form-group col-6 mt-4">
<label for="waitTime">{{'waitTime' | i18n}}</label>
<select id="waitTime" name="waitTime" [(ngModel)]="waitTime" class="form-control" [disabled]="readOnly">
<option *ngFor="let o of waitTimes" [ngValue]="o.value">{{o.name}}</option>
</select>
<small class="text-muted">{{'waitTimeDesc' | i18n}}</small>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary" [disabled]="loading || readOnly">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true" *ngIf="loading"></i>
<span *ngIf="!loading">{{'save' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary"
data-dismiss="modal">{{'cancel' | i18n}}</button>
<div class="ml-auto">
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger"
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"
[appApiAction]="deletePromise">
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading" aria-hidden="true"></i>
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"
title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
</div>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,99 @@
import {
Component,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { EmergencyAccessType } from 'jslib/enums/emergencyAccessType';
import { EmergencyAccessInviteRequest } from 'jslib/models/request/emergencyAccessInviteRequest';
import { EmergencyAccessUpdateRequest } from 'jslib/models/request/emergencyAccessUpdateRequest';
@Component({
selector: 'emergency-access-add-edit',
templateUrl: 'emergency-access-add-edit.component.html',
})
export class EmergencyAccessAddEditComponent implements OnInit {
@Input() name: string;
@Input() emergencyAccessId: string;
@Output() onSaved = new EventEmitter();
@Output() onDeleted = new EventEmitter();
loading = true;
readOnly: boolean = false;
editMode: boolean = false;
title: string;
email: string;
type: EmergencyAccessType = EmergencyAccessType.View;
formPromise: Promise<any>;
emergencyAccessType = EmergencyAccessType;
waitTimes: { name: string; value: number; }[];
waitTime: number;
constructor(private apiService: ApiService, private i18nService: I18nService,
private toasterService: ToasterService) { }
async ngOnInit() {
this.editMode = this.loading = this.emergencyAccessId != null;
this.waitTimes = [
{ name: this.i18nService.t('oneDay'), value: 1 },
{ name: this.i18nService.t('days', '2'), value: 2 },
{ name: this.i18nService.t('days', '7'), value: 7 },
{ name: this.i18nService.t('days', '14'), value: 14 },
{ name: this.i18nService.t('days', '30'), value: 30 },
{ name: this.i18nService.t('days', '90'), value: 90 },
];
if (this.editMode) {
this.editMode = true;
this.title = this.i18nService.t('editEmergencyContact');
try {
const emergencyAccess = await this.apiService.getEmergencyAccess(this.emergencyAccessId);
this.type = emergencyAccess.type;
this.waitTime = emergencyAccess.waitTimeDays;
} catch { }
} else {
this.title = this.i18nService.t('inviteEmergencyContact');
this.waitTime = this.waitTimes[2].value;
}
this.loading = false;
}
async submit() {
try {
if (this.editMode) {
const request = new EmergencyAccessUpdateRequest();
request.type = this.type;
request.waitTimeDays = this.waitTime;
this.formPromise = this.apiService.putEmergencyAccess(this.emergencyAccessId, request);
} else {
const request = new EmergencyAccessInviteRequest();
request.email = this.email.trim();
request.type = this.type;
request.waitTimeDays = this.waitTime;
this.formPromise = this.apiService.postEmergencyAccessInvite(request);
}
await this.formPromise;
this.toasterService.popAsync('success', null,
this.i18nService.t(this.editMode ? 'editedUserId' : 'invitedUsers', this.name));
this.onSaved.emit();
} catch { }
}
async delete() {
this.onDeleted.emit();
}
}

View File

@@ -0,0 +1,38 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="confirmUserTitle">
<div class="modal-dialog" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-header">
<h2 class="modal-title" id="confirmUserTitle">
{{'confirmUser' | i18n}}
<small class="text-muted" *ngIf="name">{{name}}</small>
</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>
{{'fingerprintEnsureIntegrityVerify' | i18n}}
<a href="https://help.bitwarden.com/article/fingerprint-phrase/" target="_blank" rel="noopener">
{{'learnMore' | i18n}}</a>
</p>
<p><code>{{fingerprint}}</code></p>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="dontAskAgain" name="DontAskAgain"
[(ngModel)]="dontAskAgain">
<label class="form-check-label" for="dontAskAgain">
{{'dontAskFingerprintAgain' | i18n}}
</label>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'confirm' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary"
data-dismiss="modal">{{'cancel' | i18n}}</button>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,62 @@
import {
Component,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import { ConstantsService } from 'jslib/services/constants.service';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { Utils } from 'jslib/misc/utils';
@Component({
selector: 'emergency-access-confirm',
templateUrl: 'emergency-access-confirm.component.html',
})
export class EmergencyAccessConfirmComponent implements OnInit {
@Input() name: string;
@Input() userId: string;
@Input() emergencyAccessId: string;
@Input() formPromise: Promise<any>;
@Output() onConfirmed = new EventEmitter();
dontAskAgain = false;
loading = true;
fingerprint: string;
constructor(private apiService: ApiService, private cryptoService: CryptoService,
private storageService: StorageService) { }
async ngOnInit() {
try {
const publicKeyResponse = await this.apiService.getUserPublicKey(this.userId);
if (publicKeyResponse != null) {
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
const fingerprint = await this.cryptoService.getFingerprint(this.userId, publicKey.buffer);
if (fingerprint != null) {
this.fingerprint = fingerprint.join('-');
}
}
} catch { }
this.loading = false;
}
async submit() {
if (this.loading) {
return;
}
if (this.dontAskAgain) {
await this.storageService.save(ConstantsService.autoConfirmFingerprints, true);
}
try {
this.onConfirmed.emit();
} catch { }
}
}

View File

@@ -0,0 +1,44 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
<div class="modal-dialog modal-lg" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title" id="userAddEditTitle">
{{'takeover' | i18n}}
<small class="text-muted" *ngIf="name">{{name}}</small>
</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<app-callout type="warning">{{'loggedOutWarning' | i18n}}</app-callout>
<div class="row">
<div class="col-6">
<div class="form-group">
<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="masterPasswordRetype">{{'confirmNewMasterPass' | i18n}}</label>
<input id="masterPasswordRetype" type="password" name="MasterPasswordRetype"
class="form-control" [(ngModel)]="masterPasswordRetype" required appInputVerbatim
autocomplete="new-password">
</div>
</div>
</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>
</form>
</div>
</div>

View File

@@ -0,0 +1,81 @@
import {
Component,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
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 { UserService } from 'jslib/abstractions/user.service';
import { ChangePasswordComponent } from 'jslib/angular/components/change-password.component';
import { KdfType } from 'jslib/enums/kdfType';
import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
import { EmergencyAccessPasswordRequest } from 'jslib/models/request/emergencyAccessPasswordRequest';
@Component({
selector: 'emergency-access-takeover',
templateUrl: 'emergency-access-takeover.component.html',
})
export class EmergencyAccessTakeoverComponent extends ChangePasswordComponent implements OnInit {
@Output() onDone = new EventEmitter();
@Input() emergencyAccessId: string;
@Input() name: string;
@Input() email: string;
@Input() kdf: KdfType;
@Input() kdfIterations: number;
formPromise: Promise<any>;
constructor(i18nService: I18nService, cryptoService: CryptoService,
messagingService: MessagingService, userService: UserService,
passwordGenerationService: PasswordGenerationService,
platformUtilsService: PlatformUtilsService, policyService: PolicyService,
private apiService: ApiService, private toasterService: ToasterService) {
super(i18nService, cryptoService, messagingService, userService, passwordGenerationService,
platformUtilsService, policyService);
}
// tslint:disable-next-line
async ngOnInit() { }
async submit() {
if (!await this.strongPassword()) {
return;
}
const takeoverResponse = await this.apiService.postEmergencyAccessTakeover(this.emergencyAccessId);
const oldKeyBuffer = await this.cryptoService.rsaDecrypt(takeoverResponse.keyEncrypted);
const oldEncKey = new SymmetricCryptoKey(oldKeyBuffer);
if (oldEncKey == null) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), this.i18nService.t('unexpectedError'));
return;
}
const key = await this.cryptoService.makeKey(this.masterPassword, this.email, takeoverResponse.kdf, takeoverResponse.kdfIterations);
const masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, key);
const encKey = await this.cryptoService.remakeEncKey(key, oldEncKey);
const request = new EmergencyAccessPasswordRequest();
request.newMasterPasswordHash = masterPasswordHash;
request.key = encKey[1].encryptedString;
this.apiService.postEmergencyAccessPassword(this.emergencyAccessId, request);
try {
this.onDone.emit();
} catch { }
}
}

View File

@@ -0,0 +1,31 @@
<div class="page-header">
<h1>{{'vault' | i18n}}</h1>
</div>
<div class="mt-4">
<ng-container *ngIf="ciphers.length">
<table class="table table-hover table-list table-ciphers">
<tbody>
<tr *ngFor="let c of ciphers">
<td class="table-list-icon">
<app-vault-icon [cipher]="c"></app-vault-icon>
</td>
<td class="reduced-lh wrap">
<a href="#" appStopClick (click)="selectCipher(c)" title="{{'editItem' | i18n}}">{{c.name}}</a>
<ng-container *ngIf="!organization && c.organizationId">
<i class="fa fa-share-alt" appStopProp title="{{'shared' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'shared' | i18n}}</span>
</ng-container>
<ng-container *ngIf="c.hasAttachments">
<i class="fa fa-paperclip" appStopProp title="{{'attachments' | i18n}}"
aria-hidden="true"></i>
<span class="sr-only">{{'attachments' | i18n}}</span>
</ng-container>
<br>
<small>{{c.subTitle}}</small>
</td>
</tr>
</tbody>
</table>
</ng-container>
</div>
<ng-template #cipherAddEdit></ng-template>

View File

@@ -0,0 +1,94 @@
import {
Component,
ComponentFactoryResolver,
OnInit,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ApiService } from 'jslib/abstractions/api.service';
import { CipherService } from 'jslib/abstractions/cipher.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { CipherData } from 'jslib/models/data';
import { Cipher, SymmetricCryptoKey } from 'jslib/models/domain';
import { EmergencyAccessViewResponse } from 'jslib/models/response/emergencyAccessResponse';
import { CipherView } from 'jslib/models/view/cipherView';
import { ModalComponent } from '../modal.component';
import { EmergencyAddEditComponent } from './emergency-add-edit.component';
@Component({
selector: 'emergency-access-view',
templateUrl: 'emergency-access-view.component.html',
})
export class EmergencyAccessViewComponent implements OnInit {
@ViewChild('cipherAddEdit', { read: ViewContainerRef, static: true }) cipherAddEditModalRef: ViewContainerRef;
id: string;
ciphers: CipherView[] = [];
private modal: ModalComponent = null;
constructor(private cipherService: CipherService, private cryptoService: CryptoService,
private componentFactoryResolver: ComponentFactoryResolver, private router: Router,
private route: ActivatedRoute, private apiService: ApiService) { }
ngOnInit() {
this.route.params.subscribe((qParams) => {
if (qParams.id == null) {
return this.router.navigate(['settings/emergency-access']);
}
this.id = qParams.id;
this.load();
});
}
selectCipher(cipher: CipherView) {
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.cipherAddEditModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<EmergencyAddEditComponent>(EmergencyAddEditComponent, this.cipherAddEditModalRef);
childComponent.cipherId = cipher == null ? null : cipher.id;
childComponent.cipher = cipher;
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
return childComponent;
}
async load() {
const response = await this.apiService.postEmergencyAccessView(this.id);
this.ciphers = await this.getAllCiphers(response);
}
protected async getAllCiphers(response: EmergencyAccessViewResponse): Promise<CipherView[]> {
const ciphers = response.ciphers;
const decCiphers: CipherView[] = [];
const oldKeyBuffer = await this.cryptoService.rsaDecrypt(response.keyEncrypted);
const oldEncKey = new SymmetricCryptoKey(oldKeyBuffer);
const promises: any[] = [];
ciphers.forEach((cipherResponse) => {
const cipherData = new CipherData(cipherResponse);
const cipher = new Cipher(cipherData);
promises.push(cipher.decrypt(oldEncKey).then((c) => decCiphers.push(c)));
});
await Promise.all(promises);
decCiphers.sort(this.cipherService.getLocaleSortingFunction());
return decCiphers;
}
}

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