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

Compare commits

...

231 Commits

Author SHA1 Message Date
Kyle Spearrin
6b9c90f99b update jslib 2018-12-14 17:20:31 -05:00
Kyle Spearrin
4050bc1da8 accessReports typo 2018-12-14 14:50:15 -05:00
Kyle Spearrin
4bb9051136 show reports with upgrade message 2018-12-14 14:48:12 -05:00
Kyle Spearrin
ceca4fbe53 add more org reports 2018-12-14 14:42:04 -05:00
Kyle Spearrin
9b7c0288d4 inactive 2fa report for orgs 2018-12-14 14:22:30 -05:00
Kyle Spearrin
392a90c02c exposed passwords report for orgs 2018-12-14 13:56:01 -05:00
Kyle Spearrin
7a58f6d967 make sure routerService is newed 2018-12-14 11:15:50 -05:00
Kyle Spearrin
35d1e51f9b update jslib 2018-12-13 14:38:03 -05:00
Kyle Spearrin
9729a4c724 enpass json importer 2018-12-13 14:34:43 -05:00
Kyle Spearrin
f13713a055 update jslib 2018-12-13 10:59:05 -05:00
Kyle Spearrin
31655f7832 userInputs for strength check based on username 2018-12-12 19:37:27 -05:00
Kyle Spearrin
fb4bb81595 dashlane json importer 2018-12-12 17:06:22 -05:00
Kyle Spearrin
31cb6916c6 null check 2018-12-12 15:01:23 -05:00
Kyle Spearrin
61d37615af New translations messages.json (Portuguese) (#308) 2018-12-12 12:57:01 -05:00
Kyle Spearrin
eaa7701696 New translations messages.json (Portuguese) (#307) 2018-12-12 12:49:10 -05:00
Kyle Spearrin
d6cff8e0b0 Merge branch 'master' of github.com:bitwarden/web 2018-12-12 12:46:20 -05:00
Kyle Spearrin
8ba761b33c add missing "that" 2018-12-12 12:46:17 -05:00
Kyle Spearrin
05e7e452df New Crowdin translations (#306)
* New translations messages.json (Chinese Simplified)

* New translations messages.json (Korean)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Japanese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

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

* New translations messages.json (Danish)

* New translations messages.json (Dutch)

* New translations messages.json (French)

* New translations messages.json (Italian)

* New translations messages.json (Japanese)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Polish)

* New translations messages.json (Portuguese)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Russian)

* New translations messages.json (Slovak)

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

* New translations messages.json (Korean)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Japanese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

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

* New translations messages.json (Korean)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Japanese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

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

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Danish)

* New translations messages.json (Dutch)

* New translations messages.json (Estonian)

* New translations messages.json (Finnish)

* New translations messages.json (French)

* New translations messages.json (Japanese)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Polish)

* New translations messages.json (Portuguese)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Russian)

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

* New translations messages.json (Korean)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Japanese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

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

* New translations messages.json (Korean)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Japanese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

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

* New translations messages.json (Estonian)

* New translations messages.json (Finnish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Russian)

* New translations messages.json (Slovak)

* New translations messages.json (Turkish)
2018-10-16 08:58:37 -04:00
Kyle Spearrin
a0864f5f67 update jslib 2018-10-16 08:57:19 -04:00
Kyle Spearrin
6e9f71f942 move getDomain to jslib 2018-10-13 23:26:38 -04:00
Kyle Spearrin
65211372df New Crowdin translations (#275)
* New translations messages.json (Chinese Simplified)

* New translations messages.json (Spanish)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Italian)

* New translations messages.json (French)

* New translations messages.json (Estonian)

* New translations messages.json (Danish)

* New translations messages.json (Ukrainian)
2018-10-11 21:35:24 -04:00
Kyle Spearrin
2ca8d8817a update jslib 2018-10-11 20:59:55 -04:00
Kyle Spearrin
ec266ea657 update jslib 2018-10-10 17:52:29 -04:00
Kyle Spearrin
d117aa5139 update yubiKeyDesc for 5 series 2018-10-10 12:30:03 -04:00
Kyle Spearrin
4534b7d4dc Merge branch 'master' of github.com:bitwarden/web 2018-10-09 18:03:25 -04:00
Kyle Spearrin
707fe01d77 update signalr 2018-10-09 18:03:23 -04:00
Kyle Spearrin
0e09ba0dd5 New Crowdin translations (#273)
* New translations messages.json (Chinese Simplified)

* New translations messages.json (Korean)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Japanese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Ukrainian)
2018-10-09 16:07:41 -04:00
Kyle Spearrin
989560f23c renamed event to updated2fa 2018-10-09 16:01:00 -04:00
Kyle Spearrin
844a9f934f New Crowdin translations (#272)
* New translations messages.json (Chinese Simplified)

* New translations messages.json (Korean)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Japanese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Ukrainian)
2018-10-09 09:21:52 -04:00
Kyle Spearrin
b5348c593a bump version 2018-10-08 23:11:38 -04:00
Kyle Spearrin
7f809ba541 inline redios 2018-10-08 22:42:32 -04:00
Kyle Spearrin
f9058fcddc pass gen fixes. word sep option 2018-10-08 22:06:15 -04:00
Kyle Spearrin
05c9957fd2 passphrase cleanup 2018-10-08 17:55:07 -04:00
Martin Trigaux
675739d24f Adapt the interface to generate passphrase too (#267) 2018-10-08 17:27:25 -04:00
Kyle Spearrin
10be0867ad New Crowdin translations (#270)
* New translations messages.json (Chinese Simplified)

* New translations messages.json (Korean)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Japanese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Ukrainian)
2018-10-08 16:05:16 -04:00
Kyle Spearrin
d2a4b85bdd update passman instructions 2018-10-08 15:54:58 -04:00
ServiusHack
782061ac5e Add instructions for passman (#269) 2018-10-08 15:51:59 -04:00
Kyle Spearrin
8d98e9e6f9 add back proper isDev check 2018-10-08 14:26:10 -04:00
Kyle Spearrin
4aa75e9376 support for setup of multiple u2f keys 2018-10-08 14:23:30 -04:00
Kyle Spearrin
c6d6eecb43 update jslib 2018-10-05 13:57:41 -04:00
Kyle Spearrin
1d6d7b8aa8 dont await void methods 2018-10-04 11:58:19 -04:00
Kyle Spearrin
68ed8e51bd convert analytics and toaster to platform utils 2018-10-03 10:33:04 -04:00
Kyle Spearrin
d4dd962193 update jslib 2018-10-02 09:22:49 -04:00
Kyle Spearrin
7dfb70eb8e purge org vault 2018-09-25 09:12:24 -04:00
Kyle Spearrin
53675eeba7 stop prop on checkbox clicks 2018-09-24 17:45:35 -04:00
Kyle Spearrin
6399973bfa nojekyll 2018-09-22 16:04:09 -04:00
Kyle Spearrin
f1384f5dc1 passpack importer 2018-09-21 13:54:17 -04:00
Kyle Spearrin
027cad9e52 switch to webpack-dev-server 2018-09-18 11:59:03 -04:00
Kyle Spearrin
dffcff48a0 New Crowdin translations (#263)
* New translations messages.json (Czech)

* New translations messages.json (Finnish)

* New translations messages.json (Slovak)
2018-09-17 15:33:14 -04:00
Kyle Spearrin
0391f31b3a update jslib 2018-09-17 14:38:27 -04:00
Kyle Spearrin
eb7b0ba92f update jslib 2018-09-14 09:41:37 -04:00
Kyle Spearrin
3a136e1464 remove tax information 2018-09-13 16:24:02 -04:00
Kyle Spearrin
8792bcabcb preserveWhitespaces 2018-09-13 11:57:28 -04:00
Kyle Spearrin
c362fc4677 Revert "remove swal hack"
This reverts commit 2d6b4f1216.
2018-09-13 11:28:52 -04:00
Kyle Spearrin
f471fe62ea Revert "remove swal hack again"
This reverts commit f19aa96f3e.
2018-09-13 11:28:31 -04:00
Kyle Spearrin
f19aa96f3e remove swal hack again 2018-09-12 15:18:37 -04:00
Kyle Spearrin
2d6b4f1216 remove swal hack 2018-09-12 15:16:02 -04:00
Kyle Spearrin
22a8f766c7 fix adjust seat pricing 2018-09-12 12:26:07 -04:00
Kyle Spearrin
86bc6fa807 remove trailing comma 2018-09-12 00:33:18 -04:00
Kyle Spearrin
14b094cfe0 update jslib 2018-09-11 22:48:51 -04:00
Kyle Spearrin
d90b36bd33 update package lock file 2018-09-11 22:48:30 -04:00
Kyle Spearrin
18b800ff7a fix node refs in tsconfig 2018-09-11 22:41:43 -04:00
Kyle Spearrin
7ab56a9616 prebuild:prod task 2018-09-11 17:49:43 -04:00
Kyle Spearrin
6f8352033b package lock updates 2018-09-11 17:41:56 -04:00
Daniel
188ac5051a Removed requirement to load JavaScript from js.braintreegateway.com (#259)
* Removed requirement to load JavaScript from js.braintreegateway.com

* Moved braintree-web-drop-in from a devDependencies to dependencies per code review.
2018-09-11 17:40:56 -04:00
Kyle Spearrin
335e0dd575 update angular and libs 2018-09-11 17:30:44 -04:00
Kyle Spearrin
67b187f884 bump version is lock file 2018-09-11 09:36:31 -04:00
Kyle Spearrin
c2d262ea1d New Crowdin translations (#258)
* New translations messages.json (Chinese Simplified)

* New translations messages.json (Danish)

* New translations messages.json (Dutch)

* New translations messages.json (Estonian)

* New translations messages.json (French)

* New translations messages.json (German)

* New translations messages.json (Hungarian)

* New translations messages.json (Korean)

* New translations messages.json (Polish)

* New translations messages.json (Portuguese)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Russian)

* New translations messages.json (Spanish)

* New translations messages.json (Ukrainian)
2018-09-11 09:26:00 -04:00
Kyle Spearrin
8683465d70 bump version 2018-09-11 09:01:03 -04:00
Kyle Spearrin
8c9705eec0 null check collection ids filter 2018-09-11 08:46:04 -04:00
Kyle Spearrin
d14a8bc301 update jslib 2018-09-10 10:38:32 -04:00
Kyle Spearrin
72aceedab4 update jslib 2018-09-10 10:04:56 -04:00
Kyle Spearrin
7c5ee1bd00 make sure org key exists for collection add/edit 2018-09-10 08:25:52 -04:00
Kyle Spearrin
26aa79db1a trim email also 2018-09-08 08:13:47 -04:00
Kyle Spearrin
2629aaf368 Merge branch 'master' of github.com:bitwarden/web 2018-09-03 21:51:44 -04:00
Kyle Spearrin
3973ebc00f update lunr 2018-09-03 21:51:40 -04:00
Kyle Spearrin
6cddb5f3ba New Crowdin translations (#255)
* New translations messages.json (Chinese Simplified)

* New translations messages.json (Korean)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Japanese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Estonian)

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Ukrainian)
2018-09-01 08:24:36 -04:00
Kyle Spearrin
7c55da8cc6 users get premium on enterprise 2018-09-01 08:22:36 -04:00
Kyle Spearrin
0c9f122719 premium access already notice 2018-08-31 17:42:19 -04:00
Kyle Spearrin
1d941baff1 canAccessPremium check on enabled check 2018-08-31 17:28:36 -04:00
Kyle Spearrin
e5226d7ffc check key and premium after sync 2018-08-31 17:23:36 -04:00
Kyle Spearrin
aa3d69cb94 update jslib 2018-08-30 21:48:24 -04:00
Kyle Spearrin
e68d386d3d save length options on input blur 2018-08-30 08:06:40 -04:00
Kyle Spearrin
b322f20c81 fix attachments deps 2018-08-29 09:31:20 -04:00
Kyle Spearrin
7d7a9f3dc6 attachments accessible if can access premium 2018-08-29 09:22:28 -04:00
Kyle Spearrin
977a5e868f Merge branch 'master' of github.com:bitwarden/web 2018-08-28 23:18:02 -04:00
Kyle Spearrin
41ff511165 user canAccessPremium checks 2018-08-28 23:17:58 -04:00
Kyle Spearrin
aadbb970b6 New Crowdin translations (#253)
* New translations messages.json (Chinese Simplified)

* New translations messages.json (Korean)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Japanese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Estonian)

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Ukrainian)
2018-08-28 12:53:14 -04:00
Kyle Spearrin
1873ce41b6 support for logout notification 2018-08-28 08:38:25 -04:00
Kyle Spearrin
ff51e4cc36 update jslib 2018-08-28 00:27:30 -04:00
Kyle Spearrin
73b87f2e97 update dropin to 1.12.0 2018-08-28 00:27:24 -04:00
Kyle Spearrin
1444c99458 change KDF 2018-08-27 22:40:03 -04:00
Kyle Spearrin
85c3056223 remakeEncKey when changing password/email 2018-08-27 19:09:26 -04:00
Kyle Spearrin
4a815b0bdf duplicate node_env check 2018-08-25 08:48:07 -04:00
Kyle Spearrin
b7525e1e7e messagepack protocolf or signalr 2018-08-23 21:45:06 -04:00
Kyle Spearrin
c3f64fe9c4 update jslib 2018-08-23 08:56:45 -04:00
Kyle Spearrin
34f6bc2403 10 minute idle timeout 2018-08-22 22:56:00 -04:00
Kyle Spearrin
9ecec972ca local notifications URL 2018-08-22 22:43:40 -04:00
Kyle Spearrin
80febf97d3 idle reconnects for notifications 2018-08-22 22:37:55 -04:00
Kyle Spearrin
71073874eb update jslib 2018-08-22 16:02:45 -04:00
Kyle Spearrin
f3dfeac125 update jslib 2018-08-22 08:54:22 -04:00
Kyle Spearrin
f12e73519e New Crowdin translations (#249)
* New translations messages.json (Danish)

* New translations messages.json (Dutch)

* New translations messages.json (Estonian)

* New translations messages.json (French)

* New translations messages.json (Italian)

* New translations messages.json (Spanish)
2018-08-21 23:27:57 -04:00
Kyle Spearrin
19f7dda4cc add nl language 2018-08-21 23:23:30 -04:00
Kyle Spearrin
221397b159 add et language 2018-08-21 16:01:47 -04:00
Kyle Spearrin
91766ecea3 word break-all on card lists 2018-08-21 15:48:37 -04:00
Kyle Spearrin
191be134f9 update some packages 2018-08-21 15:31:29 -04:00
Kyle Spearrin
56d279ae1e npm audit fix 2018-08-21 15:19:50 -04:00
Kyle Spearrin
3e61464dac explicitly use https://notifications.bitwarden.com 2018-08-21 13:58:45 -04:00
Kyle Spearrin
85ca10dbb3 notification service implementation 2018-08-20 22:21:13 -04:00
Kyle Spearrin
eaf08c45d9 dont use clipboard writeText api 2018-08-20 09:20:24 -04:00
Kyle Spearrin
bcb44e8cf7 fix copying 2018-08-17 12:25:21 -04:00
Kyle Spearrin
d215e0716e update jslib 2018-08-17 11:09:11 -04:00
Kyle Spearrin
f635162832 New Crowdin translations (#247)
* New translations messages.json (Dutch)

* New translations messages.json (Estonian)
2018-08-16 15:17:55 -04:00
Kyle Spearrin
c892480086 wrap cipher list details for long names 2018-08-16 08:46:49 -04:00
Kyle Spearrin
ea49d17c47 set search text before load 2018-08-15 22:26:39 -04:00
Kyle Spearrin
39c32b0e62 pass original cipher if admin. resolves #245 2018-08-15 11:47:47 -04:00
Kyle Spearrin
5cdfa35a76 update jslib 2018-08-15 09:01:37 -04:00
Kyle Spearrin
147b3ff993 prelogin kdf info 2018-08-14 15:14:04 -04:00
Kyle Spearrin
662c229de1 add italian language to web vault 2018-08-14 10:42:54 -04:00
Kyle Spearrin
c90cb2ae6e New Crowdin translations (#244)
* New translations messages.json (Chinese Traditional)

* New translations messages.json (Danish)

* New translations messages.json (Dutch)

* New translations messages.json (French)

* New translations messages.json (German)

* New translations messages.json (Italian)

* New translations messages.json (Polish)

* New translations messages.json (Slovak)

* New translations messages.json (Spanish)

* New translations messages.json (Swedish)
2018-08-14 10:40:14 -04:00
Kyle Spearrin
864d070656 bump version 2018-08-14 10:36:45 -04:00
Kyle Spearrin
e8ac2b561a apply filters on org vault 2018-08-13 16:38:21 -04:00
Kyle Spearrin
c71a432ce4 update jslib 2018-08-13 16:27:35 -04:00
Kyle Spearrin
e3ca470a6a implement search service 2018-08-13 16:27:17 -04:00
146 changed files with 27127 additions and 7349 deletions

3
.gitattributes vendored Normal file
View File

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

View File

@@ -24,12 +24,13 @@ function webfonts() {
.pipe(gulp.dest(paths.cssDir));
};
function version() {
function version(cb) {
fs.writeFileSync(paths.build + 'version.json', '{"version":"' + package.version + '"}');
cb();
}
gulp.task('clean', clean);
gulp.task('webfonts', ['clean'], webfonts);
gulp.task('prebuild', ['webfonts']);
gulp.task('version', version);
gulp.task('postdist', ['version']);
exports.clean = clean;
exports.webfonts = gulp.series(clean, webfonts);
exports.prebuild = gulp.series(clean, webfonts);
exports.version = version;
exports.postdist = version;

2
jslib

Submodule jslib updated: 4ca7a9709e...27566c3fd5

7697
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,91 +1,95 @@
{
"name": "bitwarden-web",
"version": "2.1.1",
"version": "2.7.0",
"scripts": {
"sub:init": "git submodule update --init --recursive",
"sub:update": "git submodule update --remote",
"sub:pull": "git submodule foreach git pull",
"postinstall": "npm run sub:init",
"build": "gulp prebuild && webpack --config webpack.config.js",
"build:watch": "gulp prebuild && webpack-serve --config webpack.config.js",
"build:prod": "gulp prebuild && cross-env NODE_ENV=production webpack --config webpack.config.js",
"build:prod:watch": "gulp prebuild && cross-env NODE_ENV=production webpack-serve --config webpack.config.js",
"build:selfhost": "gulp prebuild && cross-env SELF_HOST=true webpack-serve --config webpack.config.js",
"build:selfhost:watch": "gulp prebuild && cross-env SELF_HOST=true webpack-serve --config webpack.config.js",
"build:selfhost:prod": "gulp prebuild && cross-env SELF_HOST=true NODE_ENV=production webpack --config webpack.config.js",
"build:selfhost:prod:watch": "gulp prebuild && cross-env SELF_HOST=true NODE_ENV=production webpack-serve --config webpack.config.js",
"build": "gulp prebuild && webpack",
"build:watch": "gulp prebuild && webpack-dev-server",
"build:prod": "gulp prebuild && cross-env NODE_ENV=production webpack",
"build:prod:watch": "gulp prebuild && cross-env NODE_ENV=production webpack-dev-server",
"build:selfhost": "gulp prebuild && cross-env SELF_HOST=true webpack-dev-server",
"build:selfhost:watch": "gulp prebuild && cross-env SELF_HOST=true webpack-dev-server",
"build:selfhost:prod": "gulp prebuild && cross-env SELF_HOST=true NODE_ENV=production webpack",
"build:selfhost:prod:watch": "gulp prebuild && cross-env SELF_HOST=true NODE_ENV=production webpack-dev-server",
"clean:l10n": "git push origin --delete l10n_master",
"dist": "npm run build:prod && gulp postdist",
"dist:selfhost": "npm run build:selfhost:prod && gulp postdist",
"deploy": "npm run dist && gh-pages -d build",
"deploy:preview": "npm run dist && gh-pages -d build -r git@github.com:kspearrin/web-preview.git",
"deploy:dev": "npm run dist && gh-pages -d build -r git@github.com:kspearrin/bitwarden-web-dev.git",
"lint": "tslint src/**/*.ts || true",
"lint:fix": "tslint src/**/*.ts --fix"
},
"devDependencies": {
"@angular/compiler-cli": "5.2.0",
"@ngtools/webpack": "1.10.2",
"@types/jquery": "^3.3.2",
"@types/lunr": "2.1.5",
"@types/node": "8.0.19",
"@types/node-forge": "0.6.10",
"@types/papaparse": "4.1.33",
"@angular/compiler-cli": "^6.1.7",
"@ngtools/webpack": "^6.2.1",
"@types/jquery": "^3.3.6",
"@types/lunr": "^2.1.6",
"@types/node-forge": "^0.7.5",
"@types/papaparse": "^4.5.3",
"@types/webcrypto": "^0.0.28",
"@types/webpack": "^4.4.11",
"@types/zxcvbn": "^4.4.0",
"angular2-template-loader": "^0.6.2",
"clean-webpack-plugin": "^0.1.19",
"copy-webpack-plugin": "^4.5.1",
"cross-env": "^5.1.4",
"css-loader": "^0.28.11",
"copy-webpack-plugin": "^4.5.2",
"cross-env": "^5.2.0",
"css-loader": "^1.0.0",
"del": "^3.0.0",
"extract-text-webpack-plugin": "next",
"file-loader": "^1.1.11",
"file-loader": "^2.0.0",
"gh-pages": "^1.2.0",
"gulp": "^3.9.1",
"gulp": "^4.0.0",
"gulp-google-webfonts": "^2.0.0",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"node-sass": "^4.9.2",
"sass-loader": "^7.0.2",
"style-loader": "^0.21.0",
"ts-loader": "^4.3.1",
"tslint": "^5.10.0",
"node-sass": "^4.9.3",
"sass-loader": "^7.1.0",
"style-loader": "^0.23.0",
"ts-loader": "^5.1.0",
"tslint": "^5.11.0",
"tslint-loader": "^3.6.0",
"typescript": "^2.7.2",
"webpack": "^4.10.2",
"webpack-cli": "^3.0.2",
"webpack-serve": "^1.0.2"
"webpack": "^4.18.0",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.8"
},
"dependencies": {
"@angular/animations": "5.2.0",
"@angular/common": "5.2.0",
"@angular/compiler": "5.2.0",
"@angular/core": "5.2.0",
"@angular/forms": "5.2.0",
"@angular/http": "5.2.0",
"@angular/platform-browser": "5.2.0",
"@angular/platform-browser-dynamic": "5.2.0",
"@angular/router": "5.2.0",
"@angular/upgrade": "5.2.0",
"angular2-toaster": "4.0.2",
"angulartics2": "5.0.1",
"bootstrap": "4.1.1",
"core-js": "2.4.1",
"@angular/animations": "6.1.7",
"@angular/common": "6.1.7",
"@angular/compiler": "6.1.7",
"@angular/core": "6.1.7",
"@angular/forms": "6.1.7",
"@angular/http": "6.1.7",
"@angular/platform-browser": "6.1.7",
"@angular/platform-browser-dynamic": "6.1.7",
"@angular/router": "6.1.7",
"@angular/upgrade": "6.1.7",
"@aspnet/signalr": "1.0.4",
"@aspnet/signalr-protocol-msgpack": "1.0.4",
"angular2-toaster": "6.1.0",
"angulartics2": "6.3.0",
"big-integer": "1.6.36",
"bootstrap": "4.1.3",
"braintree-web-drop-in": "1.13.0",
"core-js": "2.5.7",
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git",
"font-awesome": "4.7.0",
"jquery": "3.3.1",
"lunr": "2.1.6",
"mousetrap": "1.6.1",
"ngx-infinite-scroll": "0.8.4",
"node-forge": "0.7.1",
"papaparse": "4.3.5",
"popper.js": "1.14.3",
"lunr": "2.3.3",
"ngx-infinite-scroll": "6.0.1",
"node-forge": "0.7.6",
"papaparse": "4.6.0",
"popper.js": "1.14.4",
"qrious": "4.0.2",
"rxjs": "5.5.6",
"rxjs": "6.3.2",
"sweetalert": "2.1.0",
"tldjs": "2.0.0",
"web-animations-js": "2.3.1",
"webcrypto-shim": "0.1.4",
"whatwg-fetch": "^2.0.4",
"zone.js": "0.8.19"
"whatwg-fetch": "3.0.0",
"zone.js": "0.8.26",
"zxcvbn": "4.4.2"
}
}

0
src/.nojekyll Normal file
View File

View File

@@ -1,11 +1,9 @@
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 { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { HintComponent as BaseHintComponent } from 'jslib/angular/components/hint.component';
@@ -14,9 +12,8 @@ import { HintComponent as BaseHintComponent } from 'jslib/angular/components/hin
templateUrl: 'hint.component.html',
})
export class HintComponent extends BaseHintComponent {
constructor(router: Router, analytics: Angulartics2,
toasterService: ToasterService, i18nService: I18nService,
apiService: ApiService) {
super(router, analytics, toasterService, i18nService, apiService);
constructor(router: Router, i18nService: I18nService,
apiService: ApiService, platformUtilsService: PlatformUtilsService) {
super(router, i18nService, apiService, platformUtilsService);
}
}

View File

@@ -4,9 +4,6 @@ import {
} from '@angular/core';
import { Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
@@ -22,13 +19,11 @@ import { LockComponent as BaseLockComponent } from 'jslib/angular/components/loc
templateUrl: 'lock.component.html',
})
export class LockComponent extends BaseLockComponent implements OnInit {
constructor(router: Router, analytics: Angulartics2,
toasterService: ToasterService, i18nService: I18nService,
constructor(router: Router, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, messagingService: MessagingService,
userService: UserService, cryptoService: CryptoService,
private routerService: RouterService) {
super(router, analytics, toasterService, i18nService, platformUtilsService,
messagingService, userService, cryptoService);
super(router, i18nService, platformUtilsService, messagingService, userService, cryptoService);
}
async ngOnInit() {

View File

@@ -4,11 +4,9 @@ import {
Router,
} from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { AuthService } from 'jslib/abstractions/auth.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StateService } from 'jslib/abstractions/state.service';
import { StorageService } from 'jslib/abstractions/storage.service';
@@ -20,10 +18,10 @@ import { LoginComponent as BaseLoginComponent } from 'jslib/angular/components/l
})
export class LoginComponent extends BaseLoginComponent {
constructor(authService: AuthService, router: Router,
analytics: Angulartics2, toasterService: ToasterService,
i18nService: I18nService, private route: ActivatedRoute,
storageService: StorageService, private stateService: StateService) {
super(authService, router, analytics, toasterService, i18nService, storageService);
storageService: StorageService, private stateService: StateService,
platformUtilsService: PlatformUtilsService) {
super(authService, router, platformUtilsService, i18nService, storageService);
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
}

View File

@@ -25,7 +25,7 @@ export class RecoverDeleteComponent {
async submit() {
try {
const request = new DeleteRecoverRequest();
request.email = this.email.toLowerCase();
request.email = this.email.trim().toLowerCase();
this.formPromise = this.apiService.postAccountRecoverDelete(request);
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Started Delete Recovery' });

View File

@@ -5,6 +5,7 @@ import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { AuthService } from 'jslib/abstractions/auth.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
@@ -22,15 +23,15 @@ export class RecoverTwoFactorComponent {
constructor(private router: Router, private apiService: ApiService,
private analytics: Angulartics2, private toasterService: ToasterService,
private i18nService: I18nService, private cryptoService: CryptoService) {
}
private i18nService: I18nService, private cryptoService: CryptoService,
private authService: AuthService) { }
async submit() {
try {
const request = new TwoFactorRecoveryRequest();
request.recoveryCode = this.recoveryCode.replace(/\s/g, '').toLowerCase();
request.email = this.email.toLowerCase();
const key = await this.cryptoService.makeKey(this.masterPassword, request.email);
request.email = this.email.trim().toLowerCase();
const key = await this.authService.makePreloginKey(this.masterPassword, request.email);
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, key);
this.formPromise = this.apiService.postTwoFactorRecover(request);
await this.formPromise;

View File

@@ -21,11 +21,17 @@
<div class="form-group">
<label for="masterPassword">{{'masterPass' | i18n}}</label>
<div class="d-flex">
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}" name="MasterPassword" class="text-monospace form-control"
[(ngModel)]="masterPassword" required appInputVerbatim>
<button type="button" class="ml-1 btn btn-link" title="{{'toggleVisibility' | i18n}}" (click)="togglePassword(false)">
<i class="fa fa-lg" [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
<div 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" title="{{'toggleVisibility' | i18n}}" (click)="togglePassword(false)">
<i class="fa fa-lg" [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>

View File

@@ -4,13 +4,11 @@ import {
Router,
} from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { AuthService } from 'jslib/abstractions/auth.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StateService } from 'jslib/abstractions/state.service';
@@ -25,11 +23,12 @@ export class RegisterComponent extends BaseRegisterComponent {
showTerms = true;
constructor(authService: AuthService, router: Router,
analytics: Angulartics2, toasterService: ToasterService,
i18nService: I18nService, cryptoService: CryptoService,
apiService: ApiService, private route: ActivatedRoute,
stateService: StateService, platformUtilsService: PlatformUtilsService) {
super(authService, router, analytics, toasterService, i18nService, cryptoService, apiService, stateService);
stateService: StateService, platformUtilsService: PlatformUtilsService,
passwordGenerationService: PasswordGenerationService) {
super(authService, router, i18nService, cryptoService, apiService, stateService, platformUtilsService,
passwordGenerationService);
this.showTerms = !platformUtilsService.isSelfHost();
}

View File

@@ -1,9 +1,6 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { AuthService } from 'jslib/abstractions/auth.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
@@ -18,8 +15,7 @@ import {
})
export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent {
constructor(authService: AuthService, router: Router,
analytics: Angulartics2, toasterService: ToasterService,
i18nService: I18nService, platformUtilsService: PlatformUtilsService) {
super(authService, router, analytics, toasterService, i18nService, platformUtilsService, window);
super(authService, router, i18nService, platformUtilsService, window);
}
}

View File

@@ -7,9 +7,6 @@ import {
import { Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { TwoFactorOptionsComponent } from './two-factor-options.component';
import { ModalComponent } from '../modal.component';
@@ -33,12 +30,10 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
@ViewChild('twoFactorOptions', { read: ViewContainerRef }) twoFactorOptionsModal: ViewContainerRef;
constructor(authService: AuthService, router: Router,
analytics: Angulartics2, toasterService: ToasterService,
i18nService: I18nService, apiService: ApiService,
platformUtilsService: PlatformUtilsService, private stateService: StateService,
environmentService: EnvironmentService, private componentFactoryResolver: ComponentFactoryResolver) {
super(authService, router, analytics, toasterService, i18nService, apiService,
platformUtilsService, window, environmentService);
super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService);
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
}

View File

@@ -33,8 +33,23 @@ import {
} from './organizations/settings/two-factor-setup.component';
import { ExportComponent as OrgExportComponent } from './organizations/tools/export.component';
import {
ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent,
} from './organizations/tools/exposed-passwords-report.component';
import { ImportComponent as OrgImportComponent } from './organizations/tools/import.component';
import {
InactiveTwoFactorReportComponent as OrgInactiveTwoFactorReportComponent,
} from './organizations/tools/inactive-two-factor-report.component';
import {
ReusedPasswordsReportComponent as OrgReusedPasswordsReportComponent,
} from './organizations/tools/reused-passwords-report.component';
import { ToolsComponent as OrgToolsComponent } from './organizations/tools/tools.component';
import {
UnsecuredWebsitesReportComponent as OrgUnsecuredWebsitesReportComponent,
} from './organizations/tools/unsecured-websites-report.component';
import {
WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent,
} from './organizations/tools/weak-passwords-report.component';
import { VaultComponent as OrgVaultComponent } from './organizations/vault/vault.component';
@@ -50,9 +65,14 @@ import { UserBillingComponent } from './settings/user-billing.component';
import { BreachReportComponent } from './tools/breach-report.component';
import { ExportComponent } from './tools/export.component';
import { ExposedPasswordsReportComponent } from './tools/exposed-passwords-report.component';
import { ImportComponent } from './tools/import.component';
import { InactiveTwoFactorReportComponent } from './tools/inactive-two-factor-report.component';
import { PasswordGeneratorComponent } from './tools/password-generator.component';
import { ReusedPasswordsReportComponent } from './tools/reused-passwords-report.component';
import { ToolsComponent } from './tools/tools.component';
import { UnsecuredWebsitesReportComponent } from './tools/unsecured-websites-report.component';
import { WeakPasswordsReportComponent } from './tools/weak-passwords-report.component';
import { VaultComponent } from './vault/vault.component';
@@ -148,6 +168,31 @@ const routes: Routes = [
data: { titleId: 'passwordGenerator' },
},
{ path: 'breach-report', component: BreachReportComponent, data: { titleId: 'dataBreachReport' } },
{
path: 'reused-passwords-report',
component: ReusedPasswordsReportComponent,
data: { titleId: 'reusedPasswordsReport' },
},
{
path: 'unsecured-websites-report',
component: UnsecuredWebsitesReportComponent,
data: { titleId: 'unsecuredWebsitesReport' },
},
{
path: 'weak-passwords-report',
component: WeakPasswordsReportComponent,
data: { titleId: 'weakPasswordsReport' },
},
{
path: 'exposed-passwords-report',
component: ExposedPasswordsReportComponent,
data: { titleId: 'exposedPasswordsReport' },
},
{
path: 'inactive-two-factor-report',
component: InactiveTwoFactorReportComponent,
data: { titleId: 'inactive2faReport' },
},
],
},
],
@@ -168,13 +213,44 @@ const routes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: 'import' },
{ path: 'import', component: OrgImportComponent, data: { titleId: 'importData' } },
{ path: 'export', component: OrgExportComponent, data: { titleId: 'exportVault' } },
{
path: 'exposed-passwords-report',
component: OrgExposedPasswordsReportComponent,
data: { titleId: 'exposedPasswordsReport' },
},
{
path: 'inactive-two-factor-report',
component: OrgInactiveTwoFactorReportComponent,
data: { titleId: 'inactive2faReport' },
},
{
path: 'reused-passwords-report',
component: OrgReusedPasswordsReportComponent,
data: { titleId: 'reusedPasswordsReport' },
},
{
path: 'unsecured-websites-report',
component: OrgUnsecuredWebsitesReportComponent,
data: { titleId: 'unsecuredWebsitesReport' },
},
{
path: 'weak-passwords-report',
component: OrgWeakPasswordsReportComponent,
data: { titleId: 'weakPasswordsReport' },
},
],
},
{
path: 'manage',
component: OrgManageComponent,
canActivate: [OrganizationTypeGuardService],
data: { allowedTypes: [OrganizationUserType.Owner, OrganizationUserType.Admin] },
data: {
allowedTypes: [
OrganizationUserType.Owner,
OrganizationUserType.Admin,
OrganizationUserType.Manager,
],
},
children: [
{ path: '', pathMatch: 'full', redirectTo: 'people' },
{ path: 'collections', component: OrgManageCollectionsComponent, data: { titleId: 'collections' } },

View File

@@ -3,7 +3,10 @@ import * as _swal from 'sweetalert';
import { SweetAlert } from 'sweetalert/typings/core';
import {
BodyOutputType,
Toast,
ToasterConfig,
ToasterContainerComponent,
ToasterService,
} from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
@@ -14,7 +17,9 @@ import {
NgZone,
OnDestroy,
OnInit,
SecurityContext,
} from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import {
NavigationEnd,
Router,
@@ -31,8 +36,10 @@ import { CryptoService } from 'jslib/abstractions/crypto.service';
import { FolderService } from 'jslib/abstractions/folder.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { LockService } from 'jslib/abstractions/lock.service';
import { NotificationsService } from 'jslib/abstractions/notifications.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SearchService } from 'jslib/abstractions/search.service';
import { SettingsService } from 'jslib/abstractions/settings.service';
import { SyncService } from 'jslib/abstractions/sync.service';
import { TokenService } from 'jslib/abstractions/token.service';
@@ -45,6 +52,7 @@ import { RouterService } from './services/router.service';
const BroadcasterSubscriptionId = 'AppComponent';
// Hack due to Angular 5.2 bug
const swal: SweetAlert = _swal as any;
const IdleTimeout = 60000 * 10; // 10 minutes
@Component({
selector: 'app-root',
@@ -59,6 +67,8 @@ export class AppComponent implements OnDestroy, OnInit {
});
private lastActivity: number = null;
private idleTimer: number = null;
private isIdle = false;
constructor(private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics,
private broadcasterService: BroadcasterService, private userService: UserService,
@@ -70,7 +80,8 @@ export class AppComponent implements OnDestroy, OnInit {
private platformUtilsService: PlatformUtilsService, private ngZone: NgZone,
private lockService: LockService, private storageService: StorageService,
private cryptoService: CryptoService, private collectionService: CollectionService,
private routerService: RouterService) { }
private sanitizer: DomSanitizer, private searchService: SearchService,
private notificationsService: NotificationsService, private routerService: RouterService) { }
ngOnInit() {
this.ngZone.runOutsideAngular(() => {
@@ -86,8 +97,9 @@ export class AppComponent implements OnDestroy, OnInit {
this.ngZone.run(async () => {
switch (message.command) {
case 'loggedIn':
case 'unlocked':
case 'loggedOut':
case 'unlocked':
this.notificationsService.updateConnection(false);
break;
case 'logout':
this.logOut(!!message.expired);
@@ -96,6 +108,7 @@ export class AppComponent implements OnDestroy, OnInit {
await this.lockService.lock();
break;
case 'locked':
this.notificationsService.updateConnection(false);
this.router.navigate(['lock']);
break;
case 'syncStarted':
@@ -118,6 +131,15 @@ export class AppComponent implements OnDestroy, OnInit {
this.router.navigate(['settings/premium']);
}
break;
case 'showToast':
this.showToast(message);
break;
case 'analyticsEventTrack':
this.analytics.eventTrack.next({
action: message.action,
properties: { label: message.label },
});
break;
default:
break;
}
@@ -157,6 +179,7 @@ export class AppComponent implements OnDestroy, OnInit {
this.passwordGenerationService.clear(),
]);
this.searchService.clearIndex();
this.authService.logOut(async () => {
this.analytics.eventTrack.next({ action: 'Logged Out' });
if (expired) {
@@ -175,5 +198,56 @@ export class AppComponent implements OnDestroy, OnInit {
this.lastActivity = now;
this.storageService.save(ConstantsService.lastActiveKey, now);
// Idle states
if (this.isIdle) {
this.isIdle = false;
this.idleStateChanged();
}
if (this.idleTimer != null) {
window.clearTimeout(this.idleTimer);
this.idleTimer = null;
}
this.idleTimer = window.setTimeout(() => {
if (!this.isIdle) {
this.isIdle = true;
this.idleStateChanged();
}
}, IdleTimeout);
}
private showToast(msg: any) {
const toast: Toast = {
type: msg.type,
title: msg.title,
};
if (typeof (msg.text) === 'string') {
toast.body = msg.text;
} else if (msg.text.length === 1) {
toast.body = msg.text[0];
} else {
let message = '';
msg.text.forEach((t: string) =>
message += ('<p>' + this.sanitizer.sanitize(SecurityContext.HTML, t) + '</p>'));
toast.body = message;
toast.bodyOutputType = BodyOutputType.TrustedHtml;
}
if (msg.options != null) {
if (msg.options.trustedHtml === true) {
toast.bodyOutputType = BodyOutputType.TrustedHtml;
}
if (msg.options.timeout != null && msg.options.timeout > 0) {
toast.timeout = msg.options.timeout;
}
}
this.toasterService.popAsync(toast);
}
private idleStateChanged() {
if (this.isIdle) {
this.notificationsService.disconnectFromInactivity();
} else {
this.notificationsService.reconnectFromActivity();
}
}
}

View File

@@ -18,6 +18,7 @@ import { ModalComponent } from './modal.component';
import { AvatarComponent } from './components/avatar.component';
import { CalloutComponent } from './components/callout.component';
import { PasswordStrengthComponent } from './components/password-strength.component';
import { FooterComponent } from './layouts/footer.component';
import { FrontendLayoutComponent } from './layouts/frontend-layout.component';
@@ -49,6 +50,7 @@ import { GroupsComponent as OrgGroupsComponent } from './organizations/manage/gr
import { ManageComponent as OrgManageComponent } from './organizations/manage/manage.component';
import { PeopleComponent as OrgPeopleComponent } from './organizations/manage/people.component';
import { UserAddEditComponent as OrgUserAddEditComponent } from './organizations/manage/user-add-edit.component';
import { UserConfirmComponent as OrgUserConfirmComponent } from './organizations/manage/user-confirm.component';
import { UserGroupsComponent as OrgUserGroupsComponent } from './organizations/manage/user-groups.component';
import { AccountComponent as OrgAccountComponent } from './organizations/settings/account.component';
@@ -61,8 +63,23 @@ import {
} from './organizations/settings/two-factor-setup.component';
import { ExportComponent as OrgExportComponent } from './organizations/tools/export.component';
import {
ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent,
} from './organizations/tools/exposed-passwords-report.component';
import { ImportComponent as OrgImportComponent } from './organizations/tools/import.component';
import {
InactiveTwoFactorReportComponent as OrgInactiveTwoFactorReportComponent,
} from './organizations/tools/inactive-two-factor-report.component';
import {
ReusedPasswordsReportComponent as OrgReusedPasswordsReportComponent,
} from './organizations/tools/reused-passwords-report.component';
import { ToolsComponent as OrgToolsComponent } from './organizations/tools/tools.component';
import {
UnsecuredWebsitesReportComponent as OrgUnsecuredWebsitesReportComponent,
} from './organizations/tools/unsecured-websites-report.component';
import {
WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent,
} from './organizations/tools/weak-passwords-report.component';
import { AddEditComponent as OrgAddEditComponent } from './organizations/vault/add-edit.component';
import { AttachmentsComponent as OrgAttachmentsComponent } from './organizations/vault/attachments.component';
@@ -75,6 +92,7 @@ import { AccountComponent } from './settings/account.component';
import { AdjustPaymentComponent } from './settings/adjust-payment.component';
import { AdjustStorageComponent } from './settings/adjust-storage.component';
import { ChangeEmailComponent } from './settings/change-email.component';
import { ChangeKdfComponent } from './settings/change-kdf.component';
import { ChangePasswordComponent } from './settings/change-password.component';
import { CreateOrganizationComponent } from './settings/create-organization.component';
import { DeauthorizeSessionsComponent } from './settings/deauthorize-sessions.component';
@@ -102,10 +120,15 @@ import { VerifyEmailComponent } from './settings/verify-email.component';
import { BreachReportComponent } from './tools/breach-report.component';
import { ExportComponent } from './tools/export.component';
import { ExposedPasswordsReportComponent } from './tools/exposed-passwords-report.component';
import { ImportComponent } from './tools/import.component';
import { InactiveTwoFactorReportComponent } from './tools/inactive-two-factor-report.component';
import { PasswordGeneratorHistoryComponent } from './tools/password-generator-history.component';
import { PasswordGeneratorComponent } from './tools/password-generator.component';
import { ReusedPasswordsReportComponent } from './tools/reused-passwords-report.component';
import { ToolsComponent } from './tools/tools.component';
import { UnsecuredWebsitesReportComponent } from './tools/unsecured-websites-report.component';
import { WeakPasswordsReportComponent } from './tools/weak-passwords-report.component';
import { AddEditComponent } from './vault/add-edit.component';
import { AttachmentsComponent } from './vault/attachments.component';
@@ -131,6 +154,7 @@ import { StopClickDirective } from 'jslib/angular/directives/stop-click.directiv
import { StopPropDirective } from 'jslib/angular/directives/stop-prop.directive';
import { TrueFalseValueDirective } from 'jslib/angular/directives/true-false-value.directive';
import { ColorPasswordPipe } from 'jslib/angular/pipes/color-password.pipe';
import { I18nPipe } from 'jslib/angular/pipes/i18n.pipe';
import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
import { SearchPipe } from 'jslib/angular/pipes/search.pipe';
@@ -140,8 +164,11 @@ import localeCs from '@angular/common/locales/cs';
import localeDa from '@angular/common/locales/da';
import localeDe from '@angular/common/locales/de';
import localeEs from '@angular/common/locales/es';
import localeEt from '@angular/common/locales/et';
import localeFr from '@angular/common/locales/fr';
import localeIt from '@angular/common/locales/it';
import localeNb from '@angular/common/locales/nb';
import localeNl from '@angular/common/locales/nl';
import localePl from '@angular/common/locales/pl';
import localePtBr from '@angular/common/locales/pt';
import localePtPt from '@angular/common/locales/pt-PT';
@@ -154,8 +181,11 @@ registerLocaleData(localeCs, 'cs');
registerLocaleData(localeDa, 'da');
registerLocaleData(localeDe, 'de');
registerLocaleData(localeEs, 'es');
registerLocaleData(localeEt, 'et');
registerLocaleData(localeFr, 'fr');
registerLocaleData(localeIt, 'it');
registerLocaleData(localeNb, 'nb');
registerLocaleData(localeNl, 'nl');
registerLocaleData(localePl, 'pl');
registerLocaleData(localePtBr, 'pt-BR');
registerLocaleData(localePtPt, 'pt-PT');
@@ -176,7 +206,7 @@ registerLocaleData(localeZhCn, 'zh-CN');
clearQueryParams: true,
},
}),
ToasterModule,
ToasterModule.forRoot(),
],
declarations: [
AcceptOrganizationComponent,
@@ -198,24 +228,28 @@ registerLocaleData(localeZhCn, 'zh-CN');
BulkShareComponent,
CalloutComponent,
ChangeEmailComponent,
ChangeKdfComponent,
ChangePasswordComponent,
CiphersComponent,
CollectionsComponent,
ColorPasswordPipe,
CreateOrganizationComponent,
DeauthorizeSessionsComponent,
DeleteAccountComponent,
DeleteOrganizationComponent,
DomainRulesComponent,
ExportComponent,
ExposedPasswordsReportComponent,
FallbackSrcDirective,
FolderAddEditComponent,
FooterComponent,
FrontendLayoutComponent,
GroupingsComponent,
HintComponent,
IconComponent,
I18nPipe,
IconComponent,
ImportComponent,
InactiveTwoFactorReportComponent,
InputVerbatimDirective,
LockComponent,
LoginComponent,
@@ -233,23 +267,30 @@ registerLocaleData(localeZhCn, 'zh-CN');
OrgEntityUsersComponent,
OrgEventsComponent,
OrgExportComponent,
OrgExposedPasswordsReportComponent,
OrgImportComponent,
OrgInactiveTwoFactorReportComponent,
OrgGroupAddEditComponent,
OrgGroupingsComponent,
OrgGroupsComponent,
OrgManageCollectionsComponent,
OrgManageComponent,
OrgPeopleComponent,
OrgReusedPasswordsReportComponent,
OrgSettingComponent,
OrgToolsComponent,
OrgTwoFactorSetupComponent,
OrgUserAddEditComponent,
OrgUserConfirmComponent,
OrgUserGroupsComponent,
OrganizationsComponent,
OrganizationLayoutComponent,
OrgUnsecuredWebsitesReportComponent,
OrgVaultComponent,
OrgWeakPasswordsReportComponent,
PasswordGeneratorComponent,
PasswordGeneratorHistoryComponent,
PasswordStrengthComponent,
PaymentComponent,
PremiumComponent,
ProfileComponent,
@@ -257,6 +298,7 @@ registerLocaleData(localeZhCn, 'zh-CN');
RecoverDeleteComponent,
RecoverTwoFactorComponent,
RegisterComponent,
ReusedPasswordsReportComponent,
SearchCiphersPipe,
SearchPipe,
SettingsComponent,
@@ -275,6 +317,7 @@ registerLocaleData(localeZhCn, 'zh-CN');
TwoFactorU2fComponent,
TwoFactorVerifyComponent,
TwoFactorYubiKeyComponent,
UnsecuredWebsitesReportComponent,
UpdateKeyComponent,
UpdateLicenseComponent,
UserBillingComponent,
@@ -283,6 +326,7 @@ registerLocaleData(localeZhCn, 'zh-CN');
VerifyEmailComponent,
VerifyEmailTokenComponent,
VerifyRecoverDeleteComponent,
WeakPasswordsReportComponent,
],
entryComponents: [
AddEditComponent,
@@ -304,6 +348,7 @@ registerLocaleData(localeZhCn, 'zh-CN');
OrgEntityUsersComponent,
OrgGroupAddEditComponent,
OrgUserAddEditComponent,
OrgUserConfirmComponent,
OrgUserGroupsComponent,
PasswordGeneratorHistoryComponent,
PurgeVaultComponent,

View File

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

View File

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

View File

@@ -14,7 +14,7 @@
</div>
</div>
</div>
<ul class="nav nav-tabs" *ngIf="organization.isAdmin">
<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"></i>
@@ -27,7 +27,7 @@
{{'manage' | i18n}}
</a>
</li>
<li class="nav-item">
<li class="nav-item" *ngIf="organization.isAdmin">
<a class="nav-link" routerLink="tools" routerLinkActive="active">
<i class="fa fa-wrench"></i>
{{'tools' | i18n}}

View File

@@ -14,4 +14,4 @@ if (process.env.ENV === 'production') {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule);
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });

View File

@@ -41,13 +41,11 @@
<tbody>
<tr *ngFor="let g of groups; let i = index">
<td class="table-list-checkbox" (click)="check(g)">
<input type="checkbox" [(ngModel)]="g.checked" name="Groups[{{i}}].Checked" [disabled]="g.accessAll">
<input type="checkbox" [(ngModel)]="g.checked" name="Groups[{{i}}].Checked" [disabled]="g.accessAll" appStopProp>
</td>
<td (click)="check(g)">
<span appStopProp>
{{g.name}}
<i class="fa fa-th text-muted fa-fw" *ngIf="g.accessAll" title="This group can access all items"></i>
</span>
{{g.name}}
<i class="fa fa-th text-muted fa-fw" *ngIf="g.accessAll" title="This group can access all items"></i>
</td>
<td class="text-center">
<input type="checkbox" [(ngModel)]="g.readOnly" name="Groups[{{i}}].ReadOnly" [disabled]="!g.checked || g.accessAll">
@@ -64,8 +62,8 @@
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'cancel' | i18n}}</button>
<div class="ml-auto">
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger" title="{{'delete' | i18n}}" *ngIf="editMode"
[disabled]="deleteBtn.loading" [appApiAction]="deletePromise">
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger" title="{{'delete' | i18n}}"
*ngIf="editMode" [disabled]="deleteBtn.loading" [appApiAction]="deletePromise">
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading"></i>
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading" title="{{'loading' | i18n}}"></i>
</button>

View File

@@ -103,6 +103,10 @@ export class CollectionAddEditComponent implements OnInit {
}
async submit() {
if (this.orgKey == null) {
throw new Error('No encryption key for this organization.');
}
const request = new CollectionRequest();
request.name = (await this.cryptoService.encrypt(this.name, this.orgKey)).encryptedString;
request.groups = this.groups.filter((g) => (g as any).checked && !g.accessAll)

View File

@@ -14,10 +14,15 @@ 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 { UserService } from 'jslib/abstractions/user.service';
import { CollectionData } from 'jslib/models/data/collectionData';
import { Collection } from 'jslib/models/domain/collection';
import { CollectionDetailsResponse } from 'jslib/models/response/collectionResponse';
import {
CollectionDetailsResponse,
CollectionResponse,
} from 'jslib/models/response/collectionResponse';
import { ListResponse } from 'jslib/models/response/listResponse';
import { CollectionView } from 'jslib/models/view/collectionView';
import { ModalComponent } from '../../modal.component';
@@ -42,7 +47,8 @@ export class CollectionsComponent implements OnInit {
constructor(private apiService: ApiService, private route: ActivatedRoute,
private collectionService: CollectionService, private componentFactoryResolver: ComponentFactoryResolver,
private analytics: Angulartics2, private toasterService: ToasterService,
private i18nService: I18nService, private platformUtilsService: PlatformUtilsService) { }
private i18nService: I18nService, private platformUtilsService: PlatformUtilsService,
private userService: UserService) { }
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
@@ -55,8 +61,14 @@ export class CollectionsComponent implements OnInit {
}
async load() {
const response = await this.apiService.getCollections(this.organizationId);
const collections = response.data.map((r) =>
const organization = await this.userService.getOrganization(this.organizationId);
let response: ListResponse<CollectionResponse>;
if (organization.isAdmin) {
response = await this.apiService.getCollections(this.organizationId);
} else {
response = await this.apiService.getUserCollections();
}
const collections = response.data.filter((c) => c.organizationId === this.organizationId).map((r) =>
new Collection(new CollectionData(r as CollectionDetailsResponse)));
this.collections = await this.collectionService.decryptMany(collections);
this.loading = false;
@@ -123,6 +135,10 @@ export class CollectionsComponent implements OnInit {
childComponent.entityId = collection.id;
childComponent.entityName = collection.name;
childComponent.onEditedUsers.subscribe(() => {
this.load();
this.modal.close();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});

View File

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

View File

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

View File

@@ -62,10 +62,10 @@
<tbody>
<tr *ngFor="let c of collections; let i = index">
<td class="table-list-checkbox" (click)="check(c)">
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked">
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked" appStopProp>
</td>
<td (click)="check(c)">
<span appStopProp>{{c.name}}</span>
{{c.name}}
</td>
<td class="text-center">
<input type="checkbox" [(ngModel)]="c.readOnly" name="Collection[{{i}}].ReadOnly" [disabled]="!c.checked">

View File

@@ -131,6 +131,9 @@ export class GroupsComponent implements OnInit {
childComponent.entityId = group.id;
childComponent.entityName = group.name;
childComponent.onEditedUsers.subscribe(() => {
this.modal.close();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});

View File

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

View File

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

View File

@@ -48,6 +48,7 @@
<td>
<span *ngIf="u.type === organizationUserType.Owner">{{'owner' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.Admin">{{'admin' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.Manager">{{'manager' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.User">{{'user' | i18n}}</span>
</td>
<td class="table-list-options">
@@ -87,3 +88,4 @@
<ng-template #addEdit></ng-template>
<ng-template #groupsTemplate></ng-template>
<ng-template #eventsTemplate></ng-template>
<ng-template #confirmTemplate></ng-template>

View File

@@ -5,15 +5,21 @@ import {
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ConstantsService } from 'jslib/services/constants.service';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { UserService } from 'jslib/abstractions/user.service';
import { OrganizationUserConfirmRequest } from 'jslib/models/request/organizationUserConfirmRequest';
@@ -28,6 +34,7 @@ import { Utils } from 'jslib/misc/utils';
import { ModalComponent } from '../../modal.component';
import { EntityEventsComponent } from './entity-events.component';
import { UserAddEditComponent } from './user-add-edit.component';
import { UserConfirmComponent } from './user-confirm.component';
import { UserGroupsComponent } from './user-groups.component';
@Component({
@@ -38,6 +45,7 @@ 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;
loading = true;
organizationId: string;
@@ -58,12 +66,17 @@ export class PeopleComponent implements OnInit {
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
private platformUtilsService: PlatformUtilsService, private analytics: Angulartics2,
private toasterService: ToasterService, private cryptoService: CryptoService,
private userService: UserService) { }
private userService: UserService, private router: Router,
private storageService: StorageService) { }
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
const organization = await this.userService.getOrganization(this.organizationId);
if (!organization.isAdmin) {
this.router.navigate(['../collections'], { relativeTo: this.route });
return;
}
this.accessEvents = organization.useEvents;
this.accessGroups = organization.useGroups;
await this.load();
@@ -206,17 +219,48 @@ export class PeopleComponent implements OnInit {
}
async confirm(user: OrganizationUserUserDetailsResponse) {
function updateUser(self: PeopleComponent) {
user.status = OrganizationUserStatusType.Confirmed;
const mapIndex = self.statusMap.get(OrganizationUserStatusType.Accepted).indexOf(user);
if (mapIndex > -1) {
self.statusMap.get(OrganizationUserStatusType.Accepted).splice(mapIndex, 1);
self.statusMap.get(OrganizationUserStatusType.Confirmed).push(user);
}
}
if (this.actionPromise != null) {
return;
}
const autoConfirm = await this.storageService.get<boolean>(ConstantsService.autoConfirmFingerprints);
if (autoConfirm == null || !autoConfirm) {
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.groupsModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<UserConfirmComponent>(
UserConfirmComponent, this.confirmModalRef);
childComponent.name = user != null ? user.name || user.email : null;
childComponent.organizationId = this.organizationId;
childComponent.organizationUserId = user != null ? user.id : null;
childComponent.userId = user != null ? user.userId : null;
childComponent.onConfirmedUser.subscribe(() => {
this.modal.close();
updateUser(this);
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
return;
}
this.actionPromise = this.doConfirmation(user);
await this.actionPromise;
user.status = OrganizationUserStatusType.Confirmed;
const mapIndex = this.statusMap.get(OrganizationUserStatusType.Accepted).indexOf(user);
if (mapIndex > -1) {
this.statusMap.get(OrganizationUserStatusType.Accepted).splice(mapIndex, 1);
this.statusMap.get(OrganizationUserStatusType.Confirmed).push(user);
}
updateUser(this);
this.analytics.eventTrack.next({ action: 'Confirmed User' });
this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenConfirmed', user.name || user.email));
this.actionPromise = null;
@@ -247,6 +291,11 @@ export class PeopleComponent implements OnInit {
const orgKey = await this.cryptoService.getOrgKey(this.organizationId);
const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId);
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
try {
// tslint:disable-next-line
console.log('User\'s fingerprint: ' +
(await this.cryptoService.getFingerprint(user.userId, publicKey.buffer)).join('-'));
} catch { }
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer);
const request = new OrganizationUserConfirmRequest();
request.key = key.encryptedString;

View File

@@ -30,6 +30,13 @@
<small>{{'userDesc' | i18n}}</small>
</label>
</div>
<div class="form-check mt-2 form-check-block">
<input class="form-check-input" type="radio" name="userType" id="userTypeManager" [value]="organizationUserType.Manager" [(ngModel)]="type">
<label class="form-check-label" for="userTypeManager">
{{'manager' | i18n}}
<small>{{'managerDesc' | i18n}}</small>
</label>
</div>
<div class="form-check mt-2 form-check-block">
<input class="form-check-input" type="radio" name="userType" id="userTypeAdmin" [value]="organizationUserType.Admin" [(ngModel)]="type">
<label class="form-check-label" for="userTypeAdmin">
@@ -86,10 +93,10 @@
<tbody>
<tr *ngFor="let c of collections; let i = index">
<td class="table-list-checkbox" (click)="check(c)">
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked">
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked" appStopProp>
</td>
<td (click)="check(c)">
<span appStopProp>{{c.name}}</span>
{{c.name}}
</td>
<td class="text-center">
<input type="checkbox" [(ngModel)]="c.readOnly" name="Collection[{{i}}].ReadOnly" [disabled]="!c.checked">

View File

@@ -0,0 +1,36 @@
<div class="modal fade">
<div class="modal-dialog">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-header">
<h2 class="modal-title">
{{'confirmUser' | i18n}}
<small class="text-muted" *ngIf="name">{{name}}</small>
</h2>
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'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}}"></i>
<span>{{'confirm' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'cancel' | i18n}}</button>
</div>
</form>
</div>
</div>

View File

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

View File

@@ -22,10 +22,10 @@
<tbody>
<tr *ngFor="let g of groups; let i = index">
<td class="table-list-checkbox" (click)="check(g)">
<input type="checkbox" [(ngModel)]="g.checked" name="Groups[{{i}}].Checked">
<input type="checkbox" [(ngModel)]="g.checked" name="Groups[{{i}}].Checked" appStopProp>
</td>
<td (click)="check(g)">
<span appStopProp>{{g.name}}</span>
{{g.name}}
</td>
</tr>
</tbody>

View File

@@ -32,13 +32,6 @@
<div class="secondary-header border-0 mb-0">
<h1>{{'taxInformation' | i18n}}</h1>
</div>
<div class="mb-3" *ngIf="org && (org.businessAddress1 || org.businessTaxNumber)">
<div>{{org.businessAddress1}}</div>
<div>{{org.businessAddress2}}</div>
<div>{{org.businessAddress3}}</div>
<div>{{org.businessCountry}}</div>
<div>{{org.businessTaxNumber}}</div>
</div>
<p>{{'taxInformationDesc' | i18n}}</p>
<a href="https://bitwarden.com/contact/" target="_blank" rel="noopener" class="btn btn-outline-secondary">
{{'contactSupport' | i18n}}
@@ -50,6 +43,8 @@
<div class="card-body">
<p>{{'dangerZoneDesc' | i18n}}</p>
<button type="button" class="btn btn-outline-danger" (click)="deleteOrganization()">{{'deleteOrganization' | i18n}}</button>
<button type="button" class="btn btn-outline-danger" (click)="purgeVault()">{{'purgeVault' | i18n}}</button>
</div>
</div>
<ng-template #deleteOrganizationTemplate></ng-template>
<ng-template #purgeOrganizationTemplate></ng-template>

View File

@@ -17,6 +17,7 @@ import { OrganizationUpdateRequest } from 'jslib/models/request/organizationUpda
import { OrganizationResponse } from 'jslib/models/response/organizationResponse';
import { ModalComponent } from '../../modal.component';
import { PurgeVaultComponent } from '../../settings/purge-vault.component';
import { DeleteOrganizationComponent } from './delete-organization.component';
@Component({
@@ -25,6 +26,7 @@ import { DeleteOrganizationComponent } from './delete-organization.component';
})
export class AccountComponent {
@ViewChild('deleteOrganizationTemplate', { read: ViewContainerRef }) deleteModalRef: ViewContainerRef;
@ViewChild('purgeOrganizationTemplate', { read: ViewContainerRef }) purgeModalRef: ViewContainerRef;
loading = true;
org: OrganizationResponse;
@@ -78,4 +80,19 @@ export class AccountComponent {
this.modal = null;
});
}
purgeVault() {
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.purgeModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<PurgeVaultComponent>(PurgeVaultComponent, this.purgeModalRef);
childComponent.organizationId = this.organizationId;
this.modal.onClosed.subscribe(async () => {
this.modal = null;
});
}
}

View File

@@ -53,6 +53,6 @@ export class AdjustSeatsComponent {
}
get adjustedSeatTotal(): number {
return this.seatAdjustment * this.seatAdjustment;
return this.seatAdjustment * this.seatPrice;
}
}

View File

@@ -209,7 +209,8 @@ export class OrganizationBillingComponent implements OnInit {
}
get isExpired() {
return this.billing != null && this.billing.expiration != null && this.billing.expiration < new Date();
return this.billing != null && this.billing.expiration != null &&
new Date(this.billing.expiration) < new Date();
}
get subscriptionMarkedForCancel() {
@@ -256,11 +257,11 @@ export class OrganizationBillingComponent implements OnInit {
case PlanType.EnterpriseMonthly:
return 4;
case PlanType.EnterpriseAnnually:
return 3;
return 36;
case PlanType.TeamsMonthly:
return 2.5;
case PlanType.TeamsAnnually:
return 2;
return 24;
default:
return 0;
}

View File

@@ -6,7 +6,7 @@ import { ActivatedRoute } from '@angular/router';
import { ApiService } from 'jslib/abstractions/api.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { TokenService } from 'jslib/abstractions/token.service';
import { UserService } from 'jslib/abstractions/user.service';
import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType';
@@ -18,10 +18,10 @@ import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from '../../se
templateUrl: '../../settings/two-factor-setup.component.html',
})
export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent {
constructor(apiService: ApiService, tokenService: TokenService,
constructor(apiService: ApiService, userService: UserService,
componentFactoryResolver: ComponentFactoryResolver, messagingService: MessagingService,
private route: ActivatedRoute) {
super(apiService, tokenService, componentFactoryResolver, messagingService);
super(apiService, userService, componentFactoryResolver, messagingService);
}
async ngOnInit() {

View File

@@ -1,6 +1,3 @@
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@@ -8,7 +5,6 @@ import { CryptoService } from 'jslib/abstractions/crypto.service';
import { ExportService } from 'jslib/abstractions/export.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { UserService } from 'jslib/abstractions/user.service';
import { ExportComponent as BaseExportComponent } from '../../tools/export.component';
@@ -19,12 +15,10 @@ import { ExportComponent as BaseExportComponent } from '../../tools/export.compo
export class ExportComponent extends BaseExportComponent {
organizationId: string;
constructor(analytics: Angulartics2, toasterService: ToasterService,
cryptoService: CryptoService, userService: UserService,
i18nService: I18nService, platformUtilsService: PlatformUtilsService,
exportService: ExportService, private route: ActivatedRoute) {
super(analytics, toasterService, cryptoService, userService, i18nService, platformUtilsService,
exportService);
constructor(cryptoService: CryptoService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, exportService: ExportService,
private route: ActivatedRoute) {
super(cryptoService, i18nService, platformUtilsService, exportService);
}
ngOnInit() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,26 +1,22 @@
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 { AuditService } from 'jslib/abstractions/audit.service';
import { CipherService } from 'jslib/abstractions/cipher.service';
import { CollectionService } from 'jslib/abstractions/collection.service';
import { FolderService } from 'jslib/abstractions/folder.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 { StateService } from 'jslib/abstractions/state.service';
import { TokenService } from 'jslib/abstractions/token.service';
import { TotpService } from 'jslib/abstractions/totp.service';
import { UserService } from 'jslib/abstractions/user.service';
import { CipherData } from 'jslib/models/data/cipherData';
import { Cipher } from 'jslib/models/domain/cipher';
import { Organization } from 'jslib/models/domain/organization';
import { CipherCreateRequest } from 'jslib/models/request/cipherCreateRequest';
import { CipherRequest } from 'jslib/models/request/cipherRequest';
import { AddEditComponent as BaseAddEditComponent } from '../../vault/add-edit.component';
@@ -29,19 +25,26 @@ import { AddEditComponent as BaseAddEditComponent } from '../../vault/add-edit.c
selector: 'app-org-vault-add-edit',
templateUrl: '../../vault/add-edit.component.html',
})
export class AddEditComponent extends BaseAddEditComponent implements OnInit {
export class AddEditComponent extends BaseAddEditComponent {
organization: Organization;
originalCipher: Cipher = null;
constructor(cipherService: CipherService, folderService: FolderService,
i18nService: I18nService, platformUtilsService: PlatformUtilsService,
analytics: Angulartics2, toasterService: ToasterService,
auditService: AuditService, stateService: StateService,
tokenService: TokenService, totpService: TotpService,
passwordGenerationService: PasswordGenerationService, private apiService: ApiService,
userService: UserService, collectionService: CollectionService,
totpService: TotpService, passwordGenerationService: PasswordGenerationService,
private apiService: ApiService,
messagingService: MessagingService) {
super(cipherService, folderService, i18nService, platformUtilsService, analytics,
toasterService, auditService, stateService, tokenService, totpService, passwordGenerationService,
messagingService);
super(cipherService, folderService, i18nService, platformUtilsService, auditService, stateService,
userService, collectionService, totpService, passwordGenerationService, messagingService);
}
protected loadCollections() {
if (!this.organization.isAdmin) {
return super.loadCollections();
}
return Promise.resolve(this.collections);
}
protected async loadCipher() {
@@ -49,24 +52,27 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit {
return await super.loadCipher();
}
const response = await this.apiService.getCipherAdmin(this.cipherId);
return new Cipher(new CipherData(response));
const data = new CipherData(response);
this.originalCipher = new Cipher(data);
return new Cipher(data);
}
protected encryptCipher() {
if (!this.editMode) {
this.cipher.organizationId = this.organization.id;
if (!this.organization.isAdmin) {
return super.encryptCipher();
}
return super.encryptCipher();
return this.cipherService.encrypt(this.cipher, null, this.originalCipher);
}
protected async saveCipher(cipher: Cipher) {
if (!this.organization.isAdmin) {
return super.saveCipher(cipher);
}
const request = new CipherRequest(cipher);
if (this.editMode) {
const request = new CipherRequest(cipher);
return this.apiService.putCipherAdmin(this.cipherId, request);
} else {
const request = new CipherCreateRequest(cipher);
return this.apiService.postCipherAdmin(request);
}
}

View File

@@ -1,19 +1,18 @@
import { Component } from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CipherService } from 'jslib/abstractions/cipher.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { TokenService } from 'jslib/abstractions/token.service';
import { UserService } from 'jslib/abstractions/user.service';
import { CipherData } from 'jslib/models/data/cipherData';
import { Cipher } from 'jslib/models/domain/cipher';
import { Organization } from 'jslib/models/domain/organization';
import { AttachmentView } from 'jslib/models/view/attachmentView';
import { AttachmentsComponent as BaseAttachmentsComponent } from '../../vault/attachments.component';
@Component({
@@ -23,12 +22,16 @@ import { AttachmentsComponent as BaseAttachmentsComponent } from '../../vault/at
export class AttachmentsComponent extends BaseAttachmentsComponent {
organization: Organization;
constructor(cipherService: CipherService, analytics: Angulartics2,
toasterService: ToasterService, i18nService: I18nService,
cryptoService: CryptoService, tokenService: TokenService,
constructor(cipherService: CipherService, i18nService: I18nService,
cryptoService: CryptoService, userService: UserService,
platformUtilsService: PlatformUtilsService, private apiService: ApiService) {
super(cipherService, analytics, toasterService, i18nService, cryptoService, tokenService,
platformUtilsService);
super(cipherService, i18nService, cryptoService, userService, platformUtilsService);
}
protected async reupload(attachment: AttachmentView) {
if (this.organization.isAdmin && this.showFixOldAttachments(attachment)) {
await super.reuploadCipherAttachment(attachment, true);
}
}
protected async loadCipher() {
@@ -49,4 +52,8 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
}
return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId);
}
protected showFixOldAttachments(attachment: AttachmentView) {
return attachment.key == null && this.organization.isAdmin;
}
}

View File

@@ -11,6 +11,7 @@ import { ApiService } from 'jslib/abstractions/api.service';
import { CipherService } from 'jslib/abstractions/cipher.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SearchService } from 'jslib/abstractions/search.service';
import { CipherData } from 'jslib/models/data/cipherData';
import { Cipher } from 'jslib/models/domain/cipher';
@@ -29,43 +30,48 @@ export class CiphersComponent extends BaseCiphersComponent {
organization: Organization;
accessEvents = false;
constructor(cipherService: CipherService, analytics: Angulartics2,
protected allCiphers: CipherView[] = [];
constructor(searchService: SearchService, analytics: Angulartics2,
toasterService: ToasterService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, private apiService: ApiService) {
super(cipherService, analytics, toasterService, i18nService, platformUtilsService);
platformUtilsService: PlatformUtilsService, cipherService: CipherService,
private apiService: ApiService) {
super(searchService, analytics, toasterService, i18nService, platformUtilsService, cipherService);
}
async load(filter: (cipher: CipherView) => boolean = null) {
if (!this.organization.isAdmin) {
await super.load();
await super.load(filter);
return;
}
this.accessEvents = this.organization.useEvents;
const ciphers = await this.apiService.getCiphersOrganization(this.organization.id);
if (ciphers != null && ciphers.data != null && ciphers.data.length) {
const decCiphers: CipherView[] = [];
const promises: any[] = [];
ciphers.data.forEach((r) => {
const data = new CipherData(r);
const cipher = new Cipher(data);
promises.push(cipher.decrypt().then((c) => decCiphers.push(c)));
});
await Promise.all(promises);
decCiphers.sort(this.cipherService.getLocaleSortingFunction());
this.allCiphers = decCiphers;
} else {
this.allCiphers = [];
}
this.allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organization.id);
this.applyFilter(filter);
this.loaded = true;
}
applyFilter(filter: (cipher: CipherView) => boolean = null) {
async applyFilter(filter: (cipher: CipherView) => boolean = null) {
if (this.organization.isAdmin) {
super.applyFilter(filter);
await super.applyFilter(filter);
} else {
const f = (c: CipherView) => c.organizationId === this.organization.id && (filter == null || filter(c));
super.applyFilter(f);
await super.applyFilter(f);
}
}
search(timeout: number = null) {
if (!this.organization.isAdmin) {
return super.search(timeout);
}
this.searchPending = false;
let filteredCiphers = this.allCiphers;
if (this.filter != null) {
filteredCiphers = filteredCiphers.filter(this.filter);
}
if (this.searchText == null || this.searchText.trim().length < 2) {
this.ciphers = filteredCiphers;
} else {
this.ciphers = this.searchService.searchCiphersBasic(filteredCiphers, this.searchText);
}
}
@@ -83,4 +89,8 @@ export class CiphersComponent extends BaseCiphersComponent {
}
return this.apiService.deleteCipherAdmin(id);
}
protected showFixOldAttachments(c: CipherView) {
return this.organization.isAdmin && c.hasOldAttachments;
}
}

View File

@@ -1,12 +1,10 @@
import { Component } from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CipherService } from 'jslib/abstractions/cipher.service';
import { CollectionService } from 'jslib/abstractions/collection.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { CipherData } from 'jslib/models/data/cipherData';
import { Cipher } from 'jslib/models/domain/cipher';
@@ -22,10 +20,10 @@ import { CollectionsComponent as BaseCollectionsComponent } from '../../vault/co
export class CollectionsComponent extends BaseCollectionsComponent {
organization: Organization;
constructor(collectionService: CollectionService, analytics: Angulartics2,
toasterService: ToasterService, i18nService: I18nService,
cipherService: CipherService, private apiService: ApiService) {
super(collectionService, analytics, toasterService, i18nService, cipherService);
constructor(collectionService: CollectionService, platformUtilsService: PlatformUtilsService,
i18nService: I18nService, cipherService: CipherService,
private apiService: ApiService) {
super(collectionService, platformUtilsService, i18nService, cipherService);
}
protected async loadCipher() {

View File

@@ -4,6 +4,8 @@ import { ApiService } from 'jslib/abstractions/api.service';
import { CollectionService } from 'jslib/abstractions/collection.service';
import { FolderService } from 'jslib/abstractions/folder.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { UserService } from 'jslib/abstractions/user.service';
import { CollectionData } from 'jslib/models/data/collectionData';
import { Collection } from 'jslib/models/domain/collection';
@@ -21,8 +23,9 @@ export class GroupingsComponent extends BaseGroupingsComponent {
organization: Organization;
constructor(collectionService: CollectionService, folderService: FolderService,
storageService: StorageService, userService: UserService,
private apiService: ApiService, private i18nService: I18nService) {
super(collectionService, folderService);
super(collectionService, folderService, storageService, userService);
}
async loadCollections() {
@@ -30,6 +33,7 @@ export class GroupingsComponent extends BaseGroupingsComponent {
await super.loadCollections(this.organization.id);
return;
}
const collections = await this.apiService.getCollections(this.organization.id);
if (collections != null && collections.data != null && collections.data.length) {
const collectionDomains = collections.data.map((r) =>
@@ -45,5 +49,14 @@ export class GroupingsComponent extends BaseGroupingsComponent {
unassignedCollection.organizationId = this.organization.id;
unassignedCollection.readOnly = true;
this.collections.push(unassignedCollection);
this.nestedCollections = await this.collectionService.getAllNested(this.collections);
}
collapse(grouping: CollectionView) {
super.collapse(grouping, 'org_');
}
isCollapsed(grouping: CollectionView) {
return super.isCollapsed(grouping, 'org_');
}
}

View File

@@ -13,7 +13,7 @@
<i *ngIf="actionSpinner.loading" class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
</small>
</h1>
<button type="button" class="btn btn-outline-primary btn-sm ml-auto" (click)="addCipher()" *ngIf="showAdd">
<button type="button" class="btn btn-outline-primary btn-sm ml-auto" (click)="addCipher()">
<i class="fa fa-plus fa-fw"></i>{{'addItem' | i18n}}
</button>
</div>

View File

@@ -1,7 +1,9 @@
import { Location } from '@angular/common';
import {
ChangeDetectorRef,
Component,
ComponentFactoryResolver,
NgZone,
OnDestroy,
OnInit,
ViewChild,
ViewContainerRef,
@@ -16,6 +18,8 @@ import { MessagingService } from 'jslib/abstractions/messaging.service';
import { SyncService } from 'jslib/abstractions/sync.service';
import { UserService } from 'jslib/abstractions/user.service';
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
import { Organization } from 'jslib/models/domain/organization';
import { CipherView } from 'jslib/models/view/cipherView';
@@ -30,11 +34,13 @@ import { CiphersComponent } from './ciphers.component';
import { CollectionsComponent } from './collections.component';
import { GroupingsComponent } from './groupings.component';
const BroadcasterSubscriptionId = 'OrgVaultComponent';
@Component({
selector: 'app-org-vault',
templateUrl: 'vault.component.html',
})
export class VaultComponent implements OnInit {
export class VaultComponent implements OnInit, OnDestroy {
@ViewChild(GroupingsComponent) groupingsComponent: GroupingsComponent;
@ViewChild(CiphersComponent) ciphersComponent: CiphersComponent;
@ViewChild('attachments', { read: ViewContainerRef }) attachmentsModalRef: ViewContainerRef;
@@ -45,25 +51,40 @@ export class VaultComponent implements OnInit {
organization: Organization;
collectionId: string;
type: CipherType;
showAdd = true;
private modal: ModalComponent = null;
constructor(private route: ActivatedRoute, private userService: UserService,
private location: Location, private router: Router,
private router: Router, private changeDetectorRef: ChangeDetectorRef,
private syncService: SyncService, private i18nService: I18nService,
private componentFactoryResolver: ComponentFactoryResolver, private messagingService: MessagingService) { }
private componentFactoryResolver: ComponentFactoryResolver, private messagingService: MessagingService,
private broadcasterService: BroadcasterService, private ngZone: NgZone) { }
ngOnInit() {
this.route.parent.params.subscribe(async (params) => {
this.organization = await this.userService.getOrganization(params.organizationId);
this.showAdd = this.organization.isAdmin;
this.groupingsComponent.organization = this.organization;
this.ciphersComponent.organization = this.organization;
this.route.queryParams.subscribe(async (qParams) => {
this.ciphersComponent.searchText = this.groupingsComponent.searchText = qParams.search;
if (!this.organization.isAdmin) {
await this.syncService.fullSync(false);
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case 'syncCompleted':
if (message.successfully) {
await Promise.all([
this.groupingsComponent.load(),
this.ciphersComponent.refresh(),
]);
this.changeDetectorRef.detectChanges();
}
break;
}
});
});
}
await this.groupingsComponent.load();
@@ -84,7 +105,6 @@ export class VaultComponent implements OnInit {
}
}
this.ciphersComponent.searchText = this.groupingsComponent.searchText = qParams.search;
if (qParams.viewEvents != null) {
const cipher = this.ciphersComponent.ciphers.filter((c) => c.id === qParams.viewEvents);
if (cipher.length > 0) {
@@ -95,6 +115,10 @@ export class VaultComponent implements OnInit {
});
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
async clearGroupingFilters() {
this.ciphersComponent.showAddNew = true;
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchVault');
@@ -118,13 +142,13 @@ export class VaultComponent implements OnInit {
}
async filterCollection(collectionId: string, load = false) {
this.ciphersComponent.showAddNew = false;
this.ciphersComponent.showAddNew = true;
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchCollection');
const filter = (c: CipherView) => {
if (collectionId === 'unassigned') {
return c.collectionIds == null || c.collectionIds.length === 0;
} else {
return c.collectionIds.indexOf(collectionId) > -1;
return c.collectionIds != null && c.collectionIds.indexOf(collectionId) > -1;
}
};
if (load) {
@@ -139,6 +163,7 @@ export class VaultComponent implements OnInit {
filterSearchText(searchText: string) {
this.ciphersComponent.searchText = searchText;
this.ciphersComponent.search(200);
}
editCipherAttachments(cipher: CipherView) {
@@ -197,7 +222,14 @@ export class VaultComponent implements OnInit {
addCipher() {
const component = this.editCipher(null);
component.organizationId = this.organization.id;
component.type = this.type;
if (this.organization.isAdmin) {
component.collections = this.groupingsComponent.collections.filter((c) => !c.readOnly);
}
if (this.collectionId != null) {
component.collectionIds = [this.collectionId];
}
}
editCipher(cipher: CipherView) {
@@ -261,8 +293,10 @@ export class VaultComponent implements OnInit {
};
}
const url = this.router.createUrlTree(['organizations', this.organization.id, 'vault'],
{ queryParams: queryParams }).toString();
this.location.go(url);
this.router.navigate([], {
relativeTo: this.route,
queryParams: queryParams,
replaceUrl: true,
});
}
}

View File

@@ -47,8 +47,8 @@ export class EventService {
case EventType.User_ChangedPassword:
msg = this.i18nService.t('changedPassword');
break;
case EventType.User_Enabled2fa:
msg = this.i18nService.t('enabled2fa');
case EventType.User_Updated2fa:
msg = this.i18nService.t('enabledUpdated2fa');
break;
case EventType.User_Disabled2fa:
msg = this.i18nService.t('disabled2fa');
@@ -124,6 +124,9 @@ export class EventService {
case EventType.Organization_Updated:
msg = this.i18nService.t('editedOrgSettings');
break;
case EventType.Organization_PurgedVault:
msg = this.i18nService.t('purgedOrganizationVault');
break;
default:
break;
}

View File

@@ -38,7 +38,9 @@ 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 { SearchService } from 'jslib/services/search.service';
import { SettingsService } from 'jslib/services/settings.service';
import { StateService } from 'jslib/services/state.service';
import { SyncService } from 'jslib/services/sync.service';
@@ -63,10 +65,12 @@ import { ImportService as ImportServiceAbstraction } from 'jslib/abstractions/im
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';
import {
PasswordGenerationService as PasswordGenerationServiceAbstraction,
} from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from 'jslib/abstractions/platformUtils.service';
import { SearchService as SearchServiceAbstraction } from 'jslib/abstractions/search.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';
@@ -79,7 +83,7 @@ const i18nService = new I18nService(window.navigator.language, 'locales');
const stateService = new StateService();
const broadcasterService = new BroadcasterService();
const messagingService = new BroadcasterMessagingService(broadcasterService);
const platformUtilsService = new WebPlatformUtilsService(i18nService);
const platformUtilsService = new WebPlatformUtilsService(i18nService, messagingService);
const storageService: StorageServiceAbstraction = new HtmlStorageService(platformUtilsService);
const secureStorageService: StorageServiceAbstraction = new MemoryStorageService();
const cryptoFunctionService: CryptoFunctionServiceAbstraction = new WebCryptoFunctionService(window,
@@ -90,26 +94,30 @@ const tokenService = new TokenService(storageService);
const appIdService = new AppIdService(storageService);
const apiService = new ApiService(tokenService, platformUtilsService,
async (expired: boolean) => messagingService.send('logout', { expired: expired }));
const environmentService = new EnvironmentService(apiService, storageService);
const userService = new UserService(tokenService, storageService);
const settingsService = new SettingsService(userService, storageService);
export let searchService: SearchService = null;
const cipherService = new CipherService(cryptoService, userService, settingsService,
apiService, storageService, i18nService, platformUtilsService);
apiService, storageService, i18nService, () => searchService);
const folderService = new FolderService(cryptoService, userService, apiService, storageService,
i18nService, cipherService);
const collectionService = new CollectionService(cryptoService, userService, storageService, i18nService);
searchService = new SearchService(cipherService, platformUtilsService);
const lockService = new LockService(cipherService, folderService, collectionService,
cryptoService, platformUtilsService, storageService, messagingService, null);
cryptoService, platformUtilsService, storageService, messagingService, searchService, null);
const syncService = new SyncService(userService, apiService, settingsService,
folderService, cipherService, cryptoService, collectionService, storageService, messagingService,
async (expired: boolean) => messagingService.send('logout', { expired: expired }));
const passwordGenerationService = new PasswordGenerationService(cryptoService, storageService);
const totpService = new TotpService(storageService, cryptoFunctionService);
const containerService = new ContainerService(cryptoService, platformUtilsService);
const containerService = new ContainerService(cryptoService);
const authService = new AuthService(cryptoService, apiService,
userService, tokenService, appIdService, i18nService, platformUtilsService, messagingService);
const exportService = new ExportService(folderService, cipherService, apiService);
const importService = new ImportService(cipherService, folderService, apiService, i18nService, collectionService);
const notificationsService = new NotificationsService(userService, syncService, appIdService,
apiService, cryptoService, async () => messagingService.send('logout', { expired: true }));
const environmentService = new EnvironmentService(apiService, storageService, notificationsService);
const auditService = new AuditService(cryptoFunctionService, apiService);
const analytics = new Analytics(window, () => platformUtilsService.isDev() || platformUtilsService.isSelfHost(),
@@ -122,8 +130,11 @@ export function initFactory(): Function {
const isDev = platformUtilsService.isDev();
if (!isDev && platformUtilsService.isSelfHost()) {
environmentService.baseUrl = window.location.origin;
} else {
environmentService.notificationsUrl = isDev ? 'http://localhost:61840' :
'https://notifications.bitwarden.com'; // window.location.origin + '/notifications';
}
await apiService.setUrls({
apiService.setUrls({
base: isDev ? null : window.location.origin,
api: isDev ? 'http://localhost:4000' : null,
identity: isDev ? 'http://localhost:33656' : null,
@@ -135,11 +146,12 @@ export function initFactory(): Function {
// api: 'https://api.bitwarden.com',
// identity: 'https://identity.bitwarden.com',
});
setTimeout(() => notificationsService.init(environmentService), 3000);
lockService.init(true);
const locale = await storageService.get<string>(ConstantsService.localeKey);
await i18nService.init(locale);
await authService.init();
authService.init();
const htmlEl = window.document.documentElement;
htmlEl.classList.add('locale_' + i18nService.translationLocale);
let theme = await storageService.get<string>(ConstantsService.themeKey);
@@ -188,7 +200,9 @@ export function initFactory(): Function {
{ provide: StorageServiceAbstraction, useValue: storageService },
{ provide: StateServiceAbstraction, useValue: stateService },
{ provide: ExportServiceAbstraction, useValue: exportService },
{ provide: SearchServiceAbstraction, useValue: searchService },
{ provide: ImportServiceAbstraction, useValue: importService },
{ provide: NotificationsServiceAbstraction, useValue: notificationsService },
{ provide: CryptoFunctionServiceAbstraction, useValue: cryptoFunctionService },
{
provide: APP_INITIALIZER,

View File

@@ -10,6 +10,10 @@
<h1>{{'changeMasterPassword' | i18n}}</h1>
</div>
<app-change-password></app-change-password>
<div class="secondary-header">
<h1>{{'encKeySettings' | i18n}}</h1>
</div>
<app-change-kdf></app-change-kdf>
<div class="secondary-header text-danger border-0 mb-0">
<h1>{{'dangerZone' | i18n}}</h1>
</div>

View File

@@ -9,6 +9,7 @@ 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 { UserService } from 'jslib/abstractions/user.service';
import { EmailRequest } from 'jslib/models/request/emailRequest';
import { EmailTokenRequest } from 'jslib/models/request/emailTokenRequest';
@@ -27,7 +28,8 @@ export class ChangeEmailComponent {
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private cryptoService: CryptoService, private messagingService: MessagingService) { }
private cryptoService: CryptoService, private messagingService: MessagingService,
private userService: UserService) { }
async submit() {
const hasEncKey = await this.cryptoService.hasEncKey();
@@ -36,7 +38,7 @@ export class ChangeEmailComponent {
return;
}
this.newEmail = this.newEmail.toLowerCase();
this.newEmail = this.newEmail.trim().toLowerCase();
if (!this.tokenSent) {
const request = new EmailTokenRequest();
request.newEmail = this.newEmail;
@@ -51,11 +53,12 @@ export class ChangeEmailComponent {
request.token = this.token;
request.newEmail = this.newEmail;
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
const newKey = await this.cryptoService.makeKey(this.masterPassword, this.newEmail);
const kdf = await this.userService.getKdf();
const kdfIterations = await this.userService.getKdfIterations();
const newKey = await this.cryptoService.makeKey(this.masterPassword, this.newEmail, kdf, kdfIterations);
request.newMasterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, newKey);
const encKey = await this.cryptoService.getEncKey();
const newEncKey = await this.cryptoService.encrypt(encKey.key, newKey);
request.key = newEncKey.encryptedString;
const newEncKey = await this.cryptoService.remakeEncKey(newKey);
request.key = newEncKey[1].encryptedString;
try {
this.formPromise = this.apiService.postEmail(request);
await this.formPromise;

View File

@@ -0,0 +1,47 @@
<app-callout type="warning">{{'loggedOutWarning' | i18n}}</app-callout>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate autocomplete="off">
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="kdfMasterPassword">{{'masterPass' | i18n}}</label>
<input id="kdfMasterPassword" type="password" name="MasterPasswordHash" class="form-control" [(ngModel)]="masterPassword"
required appInputVerbatim>
</div>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="form-group mb-0">
<label for="kdf">{{'kdfAlgorithm' | i18n}}</label>
<a class="ml-auto" href="https://en.wikipedia.org/wiki/Key_derivation_function" target="_blank" rel="noopener" title="{{'learnMore' | i18n}}">
<i class="fa fa-question-circle-o"></i>
</a>
<select id="kdf" name="Kdf" [(ngModel)]="kdf" class="form-control" required>
<option *ngFor="let o of kdfOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
</div>
</div>
<div class="col-6">
<div class="form-group mb-0">
<label for="kdfIterations">{{'kdfIterations' | i18n}}</label>
<a class="ml-auto" href="https://en.wikipedia.org/wiki/PBKDF2" target="_blank" rel="noopener" title="{{'learnMore' | i18n}}">
<i class="fa fa-question-circle-o"></i>
</a>
<input id="kdfIterations" type="number" min="5000" max="1000000" name="KdfIterations" class="form-control" [(ngModel)]="kdfIterations"
required>
</div>
</div>
<div class="col-12">
<div class="form-group">
<div class="small form-text text-muted">
<p>{{'kdfIterationsDesc' | i18n : (100000 | number)}}</p>
<strong>{{'warning' | i18n}}</strong>: {{'kdfIterationsWarning' | i18n : (50000 | number)}}
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
<span>{{'changeKdf' | i18n}}</span>
</button>
</form>

View File

@@ -0,0 +1,69 @@
import {
Component,
OnInit,
} from '@angular/core';
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 { MessagingService } from 'jslib/abstractions/messaging.service';
import { UserService } from 'jslib/abstractions/user.service';
import { KdfRequest } from 'jslib/models/request/kdfRequest';
import { KdfType } from 'jslib/enums/kdfType';
@Component({
selector: 'app-change-kdf',
templateUrl: 'change-kdf.component.html',
})
export class ChangeKdfComponent implements OnInit {
masterPassword: string;
kdfIterations: number;
kdf = KdfType.PBKDF2_SHA256;
kdfOptions: any[] = [];
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private cryptoService: CryptoService, private messagingService: MessagingService,
private userService: UserService) {
this.kdfOptions = [
{ name: 'PBKDF2 SHA-256', value: KdfType.PBKDF2_SHA256 },
];
}
async ngOnInit() {
this.kdf = await this.userService.getKdf();
this.kdfIterations = await this.userService.getKdfIterations();
}
async submit() {
const hasEncKey = await this.cryptoService.hasEncKey();
if (!hasEncKey) {
this.toasterService.popAsync('error', null, this.i18nService.t('updateKey'));
return;
}
const request = new KdfRequest();
request.kdf = this.kdf;
request.kdfIterations = this.kdfIterations;
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
const email = await this.userService.getEmail();
const newKey = await this.cryptoService.makeKey(this.masterPassword, email, this.kdf, this.kdfIterations);
request.newMasterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, newKey);
const newEncKey = await this.cryptoService.remakeEncKey(newKey);
request.key = newEncKey[1].encryptedString;
try {
this.formPromise = this.apiService.postAccountKdf(request);
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Changed KDF' });
this.toasterService.popAsync('success', this.i18nService.t('encKeySettingsChanged'),
this.i18nService.t('logBackIn'));
this.messagingService.send('logout');
} catch { }
}
}

View File

@@ -4,21 +4,42 @@
<div class="col-6">
<div class="form-group">
<label for="currentMasterPassword">{{'currentMasterPass' | i18n}}</label>
<input id="currentMasterPassword" type="password" name="MasterPasswordHash" class="form-control" [(ngModel)]="currentMasterPassword"
required appInputVerbatim>
<input id="currentMasterPassword" type="password" name="MasterPasswordHash" class="form-control"
[(ngModel)]="currentMasterPassword" required appInputVerbatim>
</div>
</div>
</div>
<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" [(ngModel)]="newMasterPassword"
required appInputVerbatim autocomplete="new-password">
<input id="newMasterPassword" type="password" name="NewMasterPasswordHash" class="form-control mb-1"
[(ngModel)]="newMasterPassword" (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 autocomplete="new-password">
<input id="confirmNewMasterPassword" type="password" name="ConfirmNewMasterPasswordHash" class="form-control"
[(ngModel)]="confirmNewMasterPassword" required appInputVerbatim autocomplete="new-password">
</div>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="rotateEncKey" name="RotateEncKey" [(ngModel)]="rotateEncKey"
(change)="rotateEncKeyClicked()">
<label class="form-check-label" for="rotateEncKey">
{{'rotateAccountEncKey' | i18n}}
</label>
<a href="https://help.bitwarden.com/article/change-your-master-password/#rotating-your-accounts-encryption-key"
target="_blank" rel="noopener" title="{{'learnMore' | i18n}}">
<i class="fa fa-question-circle-o"></i>
</a>
</div>
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
<span>{{'changeMasterPassword' | i18n}}</span>

View File

@@ -1,31 +1,55 @@
import {
Component,
OnInit,
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CipherService } from 'jslib/abstractions/cipher.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { FolderService } from 'jslib/abstractions/folder.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 { SyncService } from 'jslib/abstractions/sync.service';
import { UserService } from 'jslib/abstractions/user.service';
import { CipherString } from 'jslib/models/domain/cipherString';
import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
import { CipherWithIdRequest } from 'jslib/models/request/cipherWithIdRequest';
import { FolderWithIdRequest } from 'jslib/models/request/folderWithIdRequest';
import { PasswordRequest } from 'jslib/models/request/passwordRequest';
import { UpdateKeyRequest } from 'jslib/models/request/updateKeyRequest';
@Component({
selector: 'app-change-password',
templateUrl: 'change-password.component.html',
})
export class ChangePasswordComponent {
export class ChangePasswordComponent implements OnInit {
currentMasterPassword: string;
newMasterPassword: string;
confirmNewMasterPassword: string;
formPromise: Promise<any>;
masterPasswordScore: number;
rotateEncKey = false;
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 userService: UserService, private passwordGenerationService: PasswordGenerationService,
private platformUtilsService: PlatformUtilsService, private folderService: FolderService,
private cipherService: CipherService, private syncService: SyncService) { }
async ngOnInit() {
this.email = await this.userService.getEmail();
}
async submit() {
const hasEncKey = await this.cryptoService.hasEncKey();
@@ -51,16 +75,39 @@ export class ChangePasswordComponent {
return;
}
const strengthResult = this.passwordGenerationService.passwordStrength(this.newMasterPassword,
this.getPasswordStrengthUserInput());
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 newKey = await this.cryptoService.makeKey(this.newMasterPassword, email);
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 encKey = await this.cryptoService.getEncKey();
const newEncKey = await this.cryptoService.encrypt(encKey.key, newKey);
request.key = newEncKey.encryptedString;
const newEncKey = await this.cryptoService.remakeEncKey(newKey);
request.key = newEncKey[1].encryptedString;
try {
this.formPromise = this.apiService.postPassword(request);
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'),
@@ -68,4 +115,93 @@ export class ChangePasswordComponent {
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);
}
async rotateEncKeyClicked() {
if (this.rotateEncKey) {
const ciphers = await this.cipherService.getAllDecrypted();
let hasOldAttachments = false;
if (ciphers != null) {
for (let i = 0; i < ciphers.length; i++) {
if (ciphers[i].organizationId == null && ciphers[i].hasOldAttachments) {
hasOldAttachments = true;
break;
}
}
}
if (hasOldAttachments) {
const learnMore = await this.platformUtilsService.showDialog(
this.i18nService.t('oldAttachmentsNeedFixDesc'), null,
this.i18nService.t('learnMore'), this.i18nService.t('close'), 'warning');
if (learnMore) {
this.platformUtilsService.launchUri(
'https://help.bitwarden.com/article/attachments/#fixing-old-attachments');
}
this.rotateEncKey = false;
return;
}
const result = await this.platformUtilsService.showDialog(
this.i18nService.t('updateEncryptionKeyWarning') + ' ' +
this.i18nService.t('rotateEncKeyConfirmation'), this.i18nService.t('rotateEncKeyTitle'),
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!result) {
this.rotateEncKey = false;
}
}
}
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]/));
}
return userInput;
}
private async updateKey(key: SymmetricCryptoKey, masterPasswordHash: string) {
const encKey = await this.cryptoService.makeEncKey(key);
const privateKey = await this.cryptoService.getPrivateKey();
let encPrivateKey: CipherString = null;
if (privateKey != null) {
encPrivateKey = await this.cryptoService.encrypt(privateKey, encKey[0]);
}
const request = new UpdateKeyRequest();
request.privateKey = encPrivateKey != null ? encPrivateKey.encryptedString : null;
request.key = encKey[1].encryptedString;
request.masterPasswordHash = masterPasswordHash;
const folders = await this.folderService.getAllDecrypted();
for (let i = 0; i < folders.length; i++) {
if (folders[i].id == null) {
continue;
}
const folder = await this.folderService.encrypt(folders[i], encKey[0]);
request.folders.push(new FolderWithIdRequest(folder));
}
const ciphers = await this.cipherService.getAllDecrypted();
for (let i = 0; i < ciphers.length; i++) {
if (ciphers[i].organizationId != null) {
continue;
}
const cipher = await this.cipherService.encrypt(ciphers[i], encKey[0]);
request.ciphers.push(new CipherWithIdRequest(cipher));
}
await this.apiService.postAccountKey(request);
}
}

View File

@@ -89,6 +89,7 @@
<small>• {{'trackAuditLogs' | i18n}}</small>
<small>• {{'syncUsersFromDirectory' | i18n}}</small>
<small>• {{'onPremHostingOptional' | i18n}}</small>
<small>• {{'usersGetPremium' | i18n}}</small>
<small>• {{'priorityCustomerSupport' | i18n}}</small>
<small>• {{'xDayFreeTrial' | i18n : '7'}}</small>
<span>{{'costPerUser' | i18n : (3 | currency:'$')}} /{{'month' | i18n}}</span>
@@ -120,7 +121,16 @@
<label for="additionalStorage">{{'additionalStorageGb' | i18n}}</label>
<input id="additionalStorage" class="form-control" type="number" name="AdditionalStorageGb" [(ngModel)]="additionalStorage"
min="0" max="99" step="1" placeholder="{{'additionalStorageGbDesc' | i18n}}">
<small class="text-muted form-text">{{'additionalStorageDesc' | i18n : '1 GB' : (storageGb.price | currency:'$')}}</small>
<small class="text-muted form-text">{{'additionalStorageIntervalDesc' | i18n : '1 GB' : (storageGb.price | currency:'$') : ('month' | i18n)}}</small>
</div>
</div>
<div class="row">
<div class="form-group col-6" *ngIf="plans[plan].canBuyPremiumAccessAddon">
<div class="form-check">
<input id="premiumAccess" class="form-check-input" type="checkbox" name="PremiumAccessAddon" [(ngModel)]="premiumAccessAddon">
<label for="premiumAccess" class="form-check-label bold">{{'premiumAccess' | i18n}}</label>
</div>
<small class="text-muted form-text">{{'premiumAccessDesc' | i18n : (3.33 | currency:'$') : ('month' | i18n)}}</small>
</div>
</div>
<h2 class="spaced-header">{{'summary' | i18n}}</h2>
@@ -142,6 +152,10 @@
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} &times; {{storageGb.price | currency:'$'}} &times;12 {{'monthAbbr'
| i18n}} = {{additionalStorageTotal(true) | currency:'$'}} /{{'year' | i18n}}
</small>
<small *ngIf="plans[plan].canBuyPremiumAccessAddon && premiumAccessAddon">
{{'premiumAccess' | i18n}}:
{{3.33 | currency:'$'}} &times;12 {{'monthAbbr' | i18n}} = {{40 | currency:'$'}} /{{'year' | i18n}}
</small>
</label>
</div>
<div class="form-check form-check-block" *ngIf="plans[plan].monthlySeatPrice">

View File

@@ -31,6 +31,7 @@ export class CreateOrganizationComponent implements OnInit {
selfHosted = false;
ownedBusiness = false;
premiumAccessAddon = false;
storageGbPriceMonthly = 0.33;
additionalStorage = 0;
additionalSeats = 0;
@@ -58,6 +59,7 @@ export class CreateOrganizationComponent implements OnInit {
baseSeats: 5,
noAdditionalSeats: true,
annualPlanType: PlanType.FamiliesAnnually,
canBuyPremiumAccessAddon: true,
},
teams: {
basePrice: 5,
@@ -144,6 +146,8 @@ export class CreateOrganizationComponent implements OnInit {
request.businessName = this.ownedBusiness ? this.businessName : null;
request.additionalSeats = this.additionalSeats;
request.additionalStorageGb = this.additionalStorage;
request.premiumAccessAddon = this.plans[this.plan].canBuyPremiumAccessAddon &&
this.premiumAccessAddon;
request.country = this.paymentComponent.getCountry();
if (this.interval === 'month') {
request.planType = this.plans[this.plan].monthPlanType;
@@ -170,6 +174,10 @@ export class CreateOrganizationComponent implements OnInit {
}
changedPlan() {
if (!this.plans[this.plan].canBuyPremiumAccessAddon) {
this.premiumAccessAddon = false;
}
if (this.plans[this.plan].monthPlanType == null) {
this.interval = 'year';
}
@@ -217,8 +225,18 @@ export class CreateOrganizationComponent implements OnInit {
}
}
premiumAccessTotal(annual: boolean): number {
if (this.plans[this.plan].canBuyPremiumAccessAddon && this.premiumAccessAddon) {
if (annual) {
return 40;
}
}
return 0;
}
get total(): number {
const annual = this.interval === 'year';
return this.baseTotal(annual) + this.seatTotal(annual) + this.additionalStorageTotal(annual);
return this.baseTotal(annual) + this.seatTotal(annual) + this.additionalStorageTotal(annual) +
this.premiumAccessTotal(annual);
}
}

View File

@@ -55,7 +55,7 @@ export class PaymentComponent implements OnInit {
this.platformUtilsService.isDev() ? Keys.stripeTest : Keys.stripeLive);
};
this.btScript = window.document.createElement('script');
this.btScript.src = 'https://js.braintreegateway.com/web/dropin/1.4.0/js/dropin.min.js';
this.btScript.src = 'scripts/dropin.js';
this.btScript.async = true;
this.cardExpMonthOptions = [

View File

@@ -1,6 +1,9 @@
<div class="page-header">
<h1>{{'goPremium' | i18n}}</h1>
</div>
<app-callout type="info" *ngIf="canAccessPremium" title="{{'youHavePremiumAccess' | i18n}}" icon="fa-star">
{{'alreadyPremiumFromOrg' | i18n}}
</app-callout>
<app-callout type="success">
<p>{{'premiumUpgradeUnlockFeatures' | i18n}}</p>
<ul class="fa-ul">
@@ -12,6 +15,10 @@
<i class="fa fa-check text-success fa-li"></i>
{{'premiumSignUpTwoStep' | i18n}}
</li>
<li>
<i class="fa fa-check text-success fa-li"></i>
{{'premiumSignUpReports' | i18n}}
</li>
<li>
<i class="fa fa-check text-success fa-li"></i>
{{'premiumSignUpTotp' | i18n}}
@@ -52,7 +59,7 @@
<label for="additionalStorage">{{'additionalStorageGb' | i18n}}</label>
<input id="additionalStorage" class="form-control" type="number" name="AdditionalStorageGb" [(ngModel)]="additionalStorage"
min="0" max="99" step="1" placeholder="{{'additionalStorageGbDesc' | i18n}}">
<small class="text-muted form-text">{{'additionalStorageDesc' | i18n : '1 GB' : (storageGbPrice | currency:'$')}}</small>
<small class="text-muted form-text">{{'additionalStorageIntervalDesc' | i18n : '1 GB' : (storageGbPrice | currency:'$') : ('year' | i18n)}}</small>
</div>
</div>
<h2 class="spaced-header">{{'summary' | i18n}}</h2>

View File

@@ -14,6 +14,7 @@ import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SyncService } from 'jslib/abstractions/sync.service';
import { TokenService } from 'jslib/abstractions/token.service';
import { UserService } from 'jslib/abstractions/user.service';
import { PaymentComponent } from './payment.component';
@@ -24,6 +25,7 @@ import { PaymentComponent } from './payment.component';
export class PremiumComponent implements OnInit {
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
canAccessPremium = false;
selfHosted = false;
premiumPrice = 10;
storageGbPrice = 4;
@@ -35,11 +37,12 @@ export class PremiumComponent implements OnInit {
private analytics: Angulartics2, private toasterService: ToasterService,
platformUtilsService: PlatformUtilsService, private tokenService: TokenService,
private router: Router, private messagingService: MessagingService,
private syncService: SyncService) {
private syncService: SyncService, private userService: UserService) {
this.selfHosted = platformUtilsService.isSelfHost();
}
async ngOnInit() {
this.canAccessPremium = await this.userService.canAccessPremium();
const premium = await this.tokenService.getPremium();
if (premium) {
this.router.navigate(['/settings/billing']);

View File

@@ -18,7 +18,17 @@
</div>
</div>
<div class="col-6">
<app-avatar data="{{profile.name || profile.email}}" [email]="profile.email" dynamic="true" size="75" fontSize="35"></app-avatar>
<div class="mb-3">
<app-avatar data="{{profile.name || profile.email}}" [email]="profile.email" dynamic="true" size="75"
fontSize="35"></app-avatar>
</div>
<hr>
<p *ngIf="fingerprint">
{{'yourAccountsFingerprint' | i18n}}:
<a href="https://help.bitwarden.com/article/fingerprint-phrase/" target="_blank" rel="noopener" title="{{'learnMore' | i18n}}">
<i class="fa fa-question-circle-o"></i></a><br>
<code>{{fingerprint}}</code>
</p>
</div>
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">

View File

@@ -7,7 +7,9 @@ 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 { UserService } from 'jslib/abstractions/user.service';
import { UpdateProfileRequest } from 'jslib/models/request/updateProfileRequest';
@@ -20,15 +22,21 @@ import { ProfileResponse } from 'jslib/models/response/profileResponse';
export class ProfileComponent implements OnInit {
loading = true;
profile: ProfileResponse;
fingerprint: string;
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService) { }
private analytics: Angulartics2, private toasterService: ToasterService,
private userService: UserService, private cryptoService: CryptoService) { }
async ngOnInit() {
this.profile = await this.apiService.getProfile();
this.loading = false;
const fingerprint = await this.cryptoService.getFingerprint(await this.userService.getUserId());
if (fingerprint != null) {
this.fingerprint = fingerprint.join('-');
}
}
async submit() {

View File

@@ -8,7 +8,7 @@
</button>
</div>
<div class="modal-body">
<p>{{'purgeVaultDesc' | i18n}}</p>
<p>{{(organizationId ? 'purgeOrgVaultDesc' : 'purgeVaultDesc') | i18n}}</p>
<app-callout type="warning">{{'purgeVaultWarning' | i18n}}</app-callout>
<label for="masterPassword">{{'masterPass' | i18n}}</label>
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control" [(ngModel)]="masterPassword" required

View File

@@ -1,4 +1,7 @@
import { Component } from '@angular/core';
import {
Component,
Input,
} from '@angular/core';
import { Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
@@ -15,6 +18,8 @@ import { PasswordVerificationRequest } from 'jslib/models/request/passwordVerifi
templateUrl: 'purge-vault.component.html',
})
export class PurgeVaultComponent {
@Input() organizationId?: string = null;
masterPassword: string;
formPromise: Promise<any>;
@@ -32,11 +37,17 @@ export class PurgeVaultComponent {
const request = new PasswordVerificationRequest();
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
try {
this.formPromise = this.apiService.postPurgeCiphers(request);
this.formPromise = this.apiService.postPurgeCiphers(request, this.organizationId);
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Purged Vault' });
this.analytics.eventTrack.next({
action: this.organizationId != null ? 'Purged Organization Vault' : 'Purged Vault',
});
this.toasterService.popAsync('success', null, this.i18nService.t('vaultPurged'));
this.router.navigate(['vault']);
if (this.organizationId != null) {
this.router.navigate(['organizations', this.organizationId, 'vault']);
} else {
this.router.navigate(['vault']);
}
} catch { }
}
}

View File

@@ -21,15 +21,15 @@
<div class="mx-4">
<h3 class="mb-0">
{{p.name}}
<i class="fa fa-check text-success fa-fw" *ngIf="p.enabled" title="{{'enabled' | i18n}}"></i>
<a href="#" appStopClick class="badge badge-primary" *ngIf="!premium && p.premium" (click)="premiumRequired()">
<i class="fa fa-check text-success fa-fw" *ngIf="p.enabled && canAccessPremium" title="{{'enabled' | i18n}}"></i>
<a href="#" appStopClick class="badge badge-primary" *ngIf="!canAccessPremium && p.premium" (click)="premiumRequired()">
{{'premium' | i18n}}
</a>
</h3>
{{p.description}}
</div>
<div class="ml-auto">
<button type="button" class="btn btn-outline-secondary btn-sm" [disabled]="!premium && p.premium" (click)="manage(p.type)">
<button type="button" class="btn btn-outline-secondary btn-sm" [disabled]="!canAccessPremium && p.premium" (click)="manage(p.type)">
{{'manage' | i18n}}
</button>
</div>

View File

@@ -9,7 +9,7 @@ import {
import { ApiService } from 'jslib/abstractions/api.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { TokenService } from 'jslib/abstractions/token.service';
import { UserService } from 'jslib/abstractions/user.service';
import { TwoFactorProviders } from 'jslib/services/auth.service';
@@ -38,16 +38,16 @@ export class TwoFactorSetupComponent implements OnInit {
organizationId: string;
providers: any[] = [];
premium: boolean;
canAccessPremium: boolean;
loading = true;
private modal: ModalComponent = null;
constructor(protected apiService: ApiService, protected tokenService: TokenService,
constructor(protected apiService: ApiService, protected userService: UserService,
protected componentFactoryResolver: ComponentFactoryResolver, protected messagingService: MessagingService) { }
async ngOnInit() {
this.premium = this.tokenService.getPremium();
this.canAccessPremium = await this.userService.canAccessPremium();
for (const key in TwoFactorProviders) {
if (!TwoFactorProviders.hasOwnProperty(key)) {
@@ -128,8 +128,7 @@ export class TwoFactorSetupComponent implements OnInit {
}
async premiumRequired() {
const premium = await this.tokenService.getPremium();
if (!premium) {
if (!this.canAccessPremium) {
this.messagingService.send('premiumRequired');
return;
}

View File

@@ -1,5 +1,5 @@
<div class="modal fade">
<div class="modal-dialog">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">
@@ -23,43 +23,66 @@
<li>{{'twoFactorU2fSupportWeb' | i18n}}</li>
</ul>
</app-callout>
<ng-container *ngIf="!enabled">
<img src="../../images/two-factor/4.png" class="float-right ml-5" alt="">
<p>{{'twoFactorU2fAdd' | i18n}}:</p>
<ol>
<li>{{'twoFactorU2fPlugIn' | i18n}}</li>
<li>{{'twoFactorU2fTouchButton' | i18n}}</li>
</ol>
<hr>
<div class="text-center">
<ng-container *ngIf="u2fListening">
<p>
<i class="fa fa-spinner fa-spin fa-2x text-muted"></i>
</p>
{{'twoFactorU2fWaiting' | i18n}}...
</ng-container>
<ng-container *ngIf="u2fResponse">
<p>
<i class="fa fa-check-circle fa-2x text-success"></i>
</p>
{{'twoFactorU2fClickEnable' | i18n}}
</ng-container>
<ng-container *ngIf="u2fError">
<p>
<i class="fa fa-warning fa-2x text-danger"></i>
</p>
{{'twoFactorU2fProblemReading' | i18n}}
<img src="../../images/two-factor/4.png" class="float-right ml-5" alt="">
<ul class="fa-ul">
<li *ngFor="let k of keys; let i = index" #removeKeyBtn [appApiAction]="k.removePromise">
<i class="fa-li fa fa-key"></i>
<strong *ngIf="!k.configured || !k.name">{{'u2fkeyX' | i18n : i + 1}}</strong>
<strong *ngIf="k.configured && k.name">{{k.name}}</strong>
<i class="fa fa-fw" [ngClass]="{'fa-check text-success': !k.compromised, 'fa-exclamation-triangle text-warning': k.compromised}"
*ngIf="k.configured && !removeKeyBtn.loading" title="{{(k.compromised ? 'keyCompromised' : 'enabled') | i18n}}"></i>
<ng-container *ngIf="keysConfiguredCount > 1 && k.configured">
<i class="fa fa-spin fa-spinner text-muted fa-fw" title="{{'loading' | i18n}}" *ngIf="removeKeyBtn.loading"></i>
-
<a href="#" appStopClick (click)="remove(k)">{{'remove' | i18n}}</a>
</ng-container>
</li>
</ul>
<hr>
<p>{{'twoFactorU2fAdd' | i18n}}:</p>
<ol>
<li>{{'twoFactorU2fGiveName' | i18n}}</li>
<li>{{'twoFactorU2fPlugInReadKey' | i18n}}</li>
<li>{{'twoFactorU2fTouchButton' | i18n}}</li>
<li>{{'twoFactorU2fSaveForm' | i18n}}</li>
</ol>
<div class="row">
<div class="form-group col-6">
<label for="name">{{'name' | i18n}}</label>
<input id="name" type="text" name="Name" class="form-control" [(ngModel)]="name" [disabled]="!keyIdAvailable">
</div>
</div>
<button type="button" (click)="readKey()" class="btn btn-outline-secondary mr-2" [disabled]="readKeyBtn.loading || u2fListening || !keyIdAvailable"
#readKeyBtn [appApiAction]="challengePromise">
{{'readKey' | i18n}}
</button>
<ng-container *ngIf="readKeyBtn.loading">
<i class="fa fa-spinner fa-spin text-muted"></i>
</ng-container>
<ng-container *ngIf="!readKeyBtn.loading">
<ng-container *ngIf="u2fListening">
<i class="fa fa-spinner fa-spin text-muted"></i>
{{'twoFactorU2fWaiting' | i18n}}...
</ng-container>
<ng-container *ngIf="u2fResponse">
<i class="fa fa-check-circle text-success"></i>
{{'twoFactorU2fClickSave' | i18n}}
</ng-container>
<ng-container *ngIf="u2fError">
<i class="fa fa-warning text-danger"></i>
{{'twoFactorU2fProblemReadingTryAgain' | i18n}}
</ng-container>
</ng-container>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary" [disabled]="form.loading || (!enabled && !u2fResponse)">
<button type="submit" class="btn btn-primary" [disabled]="form.loading || !u2fResponse">
<i class="fa fa-spinner fa-spin" *ngIf="form.loading" title="{{'loading' | i18n}}"></i>
<ng-container *ngIf="!form.loading">
<span *ngIf="!enabled">{{'enable' | i18n}}</span>
<span *ngIf="enabled">{{'disable' | i18n}}</span>
</ng-container>
<span *ngIf="!form.loading">{{'save' | i18n}}</span>
</button>
<button #disableBtn type="button" class="btn btn-outline-secondary btn-submit" [appApiAction]="disablePromise"
[disabled]="disableBtn.loading" (click)="disable()" *ngIf="enabled">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
<span>{{'disableAllKeys' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
</div>

View File

@@ -1,5 +1,6 @@
import {
Component,
NgZone,
OnDestroy,
OnInit,
} from '@angular/core';
@@ -12,6 +13,9 @@ import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType';
import { PasswordVerificationRequest } from 'jslib/models/request/passwordVerificationRequest';
import { UpdateTwoFactorU2fDeleteRequest } from 'jslib/models/request/updateTwoFactorU2fDeleteRequest';
import { UpdateTwoFactorU2fRequest } from 'jslib/models/request/updateTwoFactorU2fRequest';
import {
ChallengeResponse,
@@ -26,18 +30,21 @@ import { TwoFactorBaseComponent } from './two-factor-base.component';
})
export class TwoFactorU2fComponent extends TwoFactorBaseComponent implements OnInit, OnDestroy {
type = TwoFactorProviderType.U2f;
u2fChallenge: ChallengeResponse;
name: string;
keys: any[];
keyIdAvailable: number = null;
keysConfiguredCount = 0;
u2fError: boolean;
u2fListening: boolean;
u2fResponse: string;
challengePromise: Promise<ChallengeResponse>;
formPromise: Promise<any>;
private closed = false;
private u2fScript: HTMLScriptElement;
constructor(apiService: ApiService, i18nService: I18nService,
analytics: Angulartics2, toasterService: ToasterService,
platformUtilsService: PlatformUtilsService) {
platformUtilsService: PlatformUtilsService, private ngZone: NgZone) {
super(apiService, i18nService, analytics, toasterService, platformUtilsService);
this.u2fScript = window.document.createElement('script');
this.u2fScript.src = 'scripts/u2f.js';
@@ -49,28 +56,24 @@ export class TwoFactorU2fComponent extends TwoFactorBaseComponent implements OnI
}
ngOnDestroy() {
this.closed = true;
window.document.body.removeChild(this.u2fScript);
}
auth(authResponse: any) {
super.auth(authResponse);
this.processResponse(authResponse.response);
this.readDevice();
}
submit() {
if (this.enabled) {
return super.disable(this.formPromise);
} else {
return this.enable();
if (this.u2fResponse == null || this.keyIdAvailable == null) {
// Should never happen.
return Promise.reject();
}
}
protected enable() {
const request = new UpdateTwoFactorU2fRequest();
request.masterPasswordHash = this.masterPasswordHash;
request.deviceResponse = this.u2fResponse;
request.id = this.keyIdAvailable;
request.name = this.name;
return super.enable(async () => {
this.formPromise = this.apiService.putTwoFactorU2f(request);
@@ -79,38 +82,97 @@ export class TwoFactorU2fComponent extends TwoFactorBaseComponent implements OnI
});
}
private readDevice() {
if (this.closed || this.enabled) {
disable() {
return super.disable(this.formPromise);
}
async remove(key: any) {
if (this.keysConfiguredCount <= 1 || key.removePromise != null) {
return;
}
const name = key.name != null ? key.name : this.i18nService.t('u2fkeyX', key.id);
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('removeU2fConfirmation'), name,
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return;
}
const request = new UpdateTwoFactorU2fDeleteRequest();
request.id = key.id;
request.masterPasswordHash = this.masterPasswordHash;
try {
key.removePromise = this.apiService.deleteTwoFactorU2f(request);
const response = await key.removePromise;
key.removePromise = null;
await this.processResponse(response);
} catch { }
}
async readKey() {
if (this.keyIdAvailable == null) {
return;
}
const request = new PasswordVerificationRequest();
request.masterPasswordHash = this.masterPasswordHash;
try {
this.challengePromise = this.apiService.getTwoFactorU2fChallenge(request);
const challenge = await this.challengePromise;
this.readDevice(challenge);
} catch { }
}
private readDevice(u2fChallenge: ChallengeResponse) {
// tslint:disable-next-line
console.log('listening for key...');
this.resetU2f(true);
(window as any).u2f.register(u2fChallenge.appId, [{
version: u2fChallenge.version,
challenge: u2fChallenge.challenge,
}], [], (data: any) => {
this.ngZone.run(() => {
this.u2fListening = false;
if (data.errorCode) {
this.u2fError = true;
// tslint:disable-next-line
console.log('error: ' + data.errorCode);
return;
}
this.u2fResponse = JSON.stringify(data);
});
}, 15);
}
private resetU2f(listening = false) {
this.u2fResponse = null;
this.u2fError = false;
this.u2fListening = true;
(window as any).u2f.register(this.u2fChallenge.appId, [{
version: this.u2fChallenge.version,
challenge: this.u2fChallenge.challenge,
}], [], (data: any) => {
this.u2fListening = false;
if (data.errorCode === 5) {
this.readDevice();
return;
} else if (data.errorCode) {
this.u2fError = true;
// tslint:disable-next-line
console.log('error: ' + data.errorCode);
return;
}
this.u2fResponse = JSON.stringify(data);
}, 10);
this.u2fListening = listening;
}
private processResponse(response: TwoFactorU2fResponse) {
this.u2fChallenge = response.challenge;
this.resetU2f();
this.keys = [];
this.keyIdAvailable = null;
this.name = null;
this.keysConfiguredCount = 0;
for (let i = 1; i <= 5; i++) {
if (response.keys != null) {
const key = response.keys.filter((k) => k.id === i);
if (key.length > 0) {
this.keysConfiguredCount++;
this.keys.push({
id: i, name: key[0].name,
configured: true,
compromised: key[0].compromised,
removePromise: null,
});
continue;
}
}
this.keys.push({ id: i, name: null, configured: false, compromised: false, removePromise: null });
if (this.keyIdAvailable == null) {
this.keyIdAvailable = i;
}
}
this.enabled = response.enabled;
}
}

View File

@@ -51,8 +51,7 @@ export class UserBillingComponent implements OnInit {
return;
}
const premium = this.tokenService.getPremium();
if (premium) {
if (this.tokenService.getPremium()) {
this.loading = true;
this.billing = await this.apiService.getUserBilling();
} else {

View File

@@ -28,7 +28,7 @@
<li *ngFor="let a of breachedAccounts" class="list-group-item min-height-fix">
<div class="row">
<div class="col-2 text-center">
<img [src]="'https://haveibeenpwned.com/Content/Images/PwnedLogos/' + a.name + '.' + a.logoType" alt="" class="img-fluid">
<img [src]="a.logoPath" alt="" class="img-fluid">
</div>
<div class="col-7">
<h3 class="text-lg">{{a.title}}</h3>

View File

@@ -0,0 +1,95 @@
import {
ComponentFactoryResolver,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { CipherView } from 'jslib/models/view/cipherView';
import { Organization } from 'jslib/models/domain/organization';
import { ModalComponent } from '../modal.component';
import { AddEditComponent as OrgAddEditComponent } from '../organizations/vault/add-edit.component';
import { AddEditComponent } from '../vault/add-edit.component';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { UserService } from 'jslib/abstractions/user.service';
export class CipherReportComponent {
@ViewChild('cipherAddEdit', { read: ViewContainerRef }) cipherAddEditModalRef: ViewContainerRef;
loading = false;
hasLoaded = false;
ciphers: CipherView[] = [];
organization: Organization;
private modal: ModalComponent = null;
constructor(private componentFactoryResolver: ComponentFactoryResolver, protected userService: UserService,
protected messagingService: MessagingService, public requiresPaid: boolean) { }
async load() {
this.loading = true;
await this.setCiphers();
this.loading = false;
this.hasLoaded = true;
}
selectCipher(cipher: CipherView) {
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.cipherAddEditModalRef.createComponent(factory).instance;
let childComponent: OrgAddEditComponent | AddEditComponent;
if (this.organization != null) {
childComponent = this.modal.show<OrgAddEditComponent>(OrgAddEditComponent, this.cipherAddEditModalRef);
(childComponent as OrgAddEditComponent).organization = this.organization;
} else {
childComponent = this.modal.show<AddEditComponent>(AddEditComponent, this.cipherAddEditModalRef);
}
childComponent.cipherId = cipher == null ? null : cipher.id;
if (this.organization != null) {
childComponent.organizationId = this.organization.id;
}
childComponent.onSavedCipher.subscribe(async (c: CipherView) => {
this.modal.close();
await this.load();
});
childComponent.onDeletedCipher.subscribe(async (c: CipherView) => {
this.modal.close();
await this.load();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
return childComponent;
}
protected async checkAccess(): Promise<boolean> {
if (this.organization != null) {
// TODO: Maybe we want to just make sure they are not on a free plan? Just compare use2fa for now
// since all paid plans include use2fa
if (this.requiresPaid && !this.organization.use2fa) {
this.messagingService.send('upgradeOrganization', { organizationId: this.organization.id });
return false;
}
} else {
const accessPremium = await this.userService.canAccessPremium();
if (this.requiresPaid && !accessPremium) {
this.messagingService.send('premiumRequired');
this.loading = false;
return false;
}
}
return true;
}
protected async setCiphers() {
this.ciphers = [];
}
}

View File

@@ -1,13 +1,9 @@
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { Component } from '@angular/core';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { ExportService } from 'jslib/abstractions/export.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { UserService } from 'jslib/abstractions/user.service';
import { ExportComponent as BaseExportComponent } from 'jslib/angular/components/export.component';
@@ -16,17 +12,14 @@ import { ExportComponent as BaseExportComponent } from 'jslib/angular/components
templateUrl: 'export.component.html',
})
export class ExportComponent extends BaseExportComponent {
constructor(analytics: Angulartics2, toasterService: ToasterService,
cryptoService: CryptoService, userService: UserService,
i18nService: I18nService, platformUtilsService: PlatformUtilsService,
exportService: ExportService) {
super(analytics, toasterService, cryptoService, userService, i18nService, platformUtilsService,
exportService, window);
constructor(cryptoService: CryptoService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, exportService: ExportService) {
super(cryptoService, i18nService, platformUtilsService, exportService, window);
}
protected saved() {
super.saved();
this.masterPassword = null;
this.toasterService.popAsync('success', null, this.i18nService.t('exportSuccess'));
this.platformUtilsService.showToast('success', null, this.i18nService.t('exportSuccess'));
}
}

View File

@@ -0,0 +1,40 @@
<div class="page-header">
<h1>{{'exposedPasswordsReport' | i18n}}</h1>
</div>
<p>{{'exposedPasswordsReportDesc' | i18n}}</p>
<button type="button" class="btn btn-primary btn-submit" [disabled]="loading" (click)="load()">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
<span>{{'checkExposedPasswords' | i18n}}</span>
</button>
<div class="mt-4" *ngIf="hasLoaded">
<app-callout type="success" title="{{'goodNews' | i18n}}" *ngIf="!ciphers.length">
{{'noExposedPasswords' | i18n}}
</app-callout>
<ng-container *ngIf="ciphers.length">
<app-callout type="danger" title="{{'exposedPasswordsFound' | i18n}}">
{{'exposedPasswordsFoundDesc' | i18n : (ciphers.length | number)}}
</app-callout>
<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>
<i class="fa fa-share-alt" *ngIf="!organization && c.organizationId" title="{{'shared' | i18n}}"></i>
<i class="fa fa-paperclip" title="{{'attachments' | i18n}}" *ngIf="c.hasAttachments"></i>
<br>
<small>{{c.subTitle}}</small>
</td>
<td class="text-right">
<span class="badge badge-warning">
{{'exposedXTimes' | i18n : (exposedPasswordMap.get(c.id) | number)}}
</span>
</td>
</tr>
</tbody>
</table>
</ng-container>
</div>
<ng-template #cipherAddEdit></ng-template>

View File

@@ -0,0 +1,64 @@
import {
Component,
ComponentFactoryResolver,
OnInit,
} from '@angular/core';
import { AuditService } from 'jslib/abstractions/audit.service';
import { CipherService } from 'jslib/abstractions/cipher.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { UserService } from 'jslib/abstractions/user.service';
import { CipherView } from 'jslib/models/view/cipherView';
import { CipherType } from 'jslib/enums/cipherType';
import { CipherReportComponent } from './cipher-report.component';
@Component({
selector: 'app-exposed-passwords-report',
templateUrl: 'exposed-passwords-report.component.html',
})
export class ExposedPasswordsReportComponent extends CipherReportComponent implements OnInit {
exposedPasswordMap = new Map<string, number>();
constructor(protected cipherService: CipherService, protected auditService: AuditService,
componentFactoryResolver: ComponentFactoryResolver, messagingService: MessagingService,
userService: UserService) {
super(componentFactoryResolver, userService, messagingService, true);
}
ngOnInit() {
this.checkAccess();
}
async load() {
if (await this.checkAccess()) {
super.load();
}
}
async setCiphers() {
const allCiphers = await this.getAllCiphers();
const exposedPasswordCiphers: CipherView[] = [];
const promises: Array<Promise<void>> = [];
allCiphers.forEach((c) => {
if (c.type !== CipherType.Login || c.login.password == null || c.login.password === '') {
return;
}
const promise = this.auditService.passwordLeaked(c.login.password).then((exposedCount) => {
if (exposedCount > 0) {
exposedPasswordCiphers.push(c);
this.exposedPasswordMap.set(c.id, exposedCount);
}
});
promises.push(promise);
});
await Promise.all(promises);
this.ciphers = exposedPasswordCiphers;
}
protected getAllCiphers(): Promise<CipherView[]> {
return this.cipherService.getAllDecrypted();
}
}

View File

@@ -84,13 +84,17 @@
and save the CSV file. Note that the importer only supports files exported while Enpass is set to the English
language, so adjust your settings accordingly.
</ng-container>
<ng-container *ngIf="format === 'enpassjson'">
Using the Enpass 6 desktop application, click the menu button and navigate to "File" &rarr; "Export".
Select the ".json" file format option and save the JSON file.
</ng-container>
<ng-container *ngIf="format === 'pwsafexml'">
Using the Password Safe desktop application, navigate to "File" &rarr; "Export To" &rarr; "XML format..." and save the XML
file.
</ng-container>
<ng-container *ngIf="format === 'dashlanecsv'">
Using the Dashlane desktop application, navigate to "File" &rarr; "Export" &rarr; "Unsecured archive (readable) in CSV format"
and save the CSV file.
<ng-container *ngIf="format === 'dashlanejson'">
Using the Dashlane desktop application, navigate to "File" &rarr; "Export" &rarr; "Unsecured archive (readable) in JSON format"
and save the JSON file.
</ng-container>
<ng-container *ngIf="format === 'msecurecsv'">
Using the mSecure desktop application, navigate to "File" &rarr; "Export" &rarr; "CSV File..." and save the CSV file.
@@ -153,6 +157,15 @@
check all of the fields, change the "Output format" to "CSV", and then click the "Start" button to save the CSV
file.
</ng-container>
<ng-container *ngIf="format === 'passpackcsv'">
Log into the Passpack website vault and navigate to "Settings" &rarr; "Export", then click the "Download" button to save
the CSV file.
</ng-container>
<ng-container *ngIf="format === 'passmanjson'">
Open your Passman vault and click on "Settings" in the bottom left corner. In the "Settings" window switch to the
"Export credentials" tab and choose "JSON" as the export type. Enter your vault's passphrase and click the "Export"
button to save the JSON file.
</ng-container>
</app-callout>
<div class="row">
<div class="col-6">

View File

@@ -129,7 +129,7 @@ export class ImportComponent implements OnInit {
reader.onload = (evt) => {
if (this.format === 'lastpasscsv' && file.type === 'text/html') {
const parser = new DOMParser();
const doc = parser.parseFromString(evt.target.result, 'text/html');
const doc = parser.parseFromString((evt.target as any).result, 'text/html');
const pre = doc.querySelector('pre');
if (pre != null) {
resolve(pre.textContent);
@@ -139,7 +139,7 @@ export class ImportComponent implements OnInit {
return;
}
resolve(evt.target.result);
resolve((evt.target as any).result);
};
reader.onerror = () => {
reject();

View File

@@ -0,0 +1,44 @@
<div class="page-header">
<h1>
{{'inactive2faReport' | i18n}}
<small *ngIf="hasLoaded && loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
</small>
</h1>
</div>
<p>{{'inactive2faReportDesc' | i18n}}</p>
<div *ngIf="!hasLoaded && loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
</div>
<div class="mt-4" *ngIf="hasLoaded">
<app-callout type="success" title="{{'goodNews' | i18n}}" *ngIf="!ciphers.length">
{{'noInactive2fa' | i18n}}
</app-callout>
<ng-container *ngIf="ciphers.length">
<app-callout type="danger" title="{{'inactive2faFound' | i18n}}">
{{'inactive2faFoundDesc' | i18n : (ciphers.length | number)}}
</app-callout>
<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>
<i class="fa fa-share-alt" *ngIf="!organization && c.organizationId" title="{{'shared' | i18n}}"></i>
<i class="fa fa-paperclip" title="{{'attachments' | i18n}}" *ngIf="c.hasAttachments"></i>
<br>
<small>{{c.subTitle}}</small>
</td>
<td class="text-right">
<a class="badge badge-primary" href="{{cipherDocs.get(c.id)}}" target="_blank" rel="noopener"
*ngIf="cipherDocs.has(c.id)">
{{'instructions' | i18n}}</a>
</td>
</tr>
</tbody>
</table>
</ng-container>
</div>
<ng-template #cipherAddEdit></ng-template>

View File

@@ -0,0 +1,101 @@
import {
Component,
ComponentFactoryResolver,
OnInit,
} from '@angular/core';
import { CipherService } from 'jslib/abstractions/cipher.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { UserService } from 'jslib/abstractions/user.service';
import { CipherView } from 'jslib/models/view/cipherView';
import { CipherType } from 'jslib/enums/cipherType';
import { Utils } from 'jslib/misc/utils';
import { CipherReportComponent } from './cipher-report.component';
@Component({
selector: 'app-inactive-two-factor-report',
templateUrl: 'inactive-two-factor-report.component.html',
})
export class InactiveTwoFactorReportComponent extends CipherReportComponent implements OnInit {
services = new Map<string, string>();
cipherDocs = new Map<string, string>();
constructor(protected cipherService: CipherService, componentFactoryResolver: ComponentFactoryResolver,
messagingService: MessagingService, userService: UserService) {
super(componentFactoryResolver, userService, messagingService, true);
}
async ngOnInit() {
if (await this.checkAccess()) {
await super.load();
}
}
async setCiphers() {
try {
await this.load2fa();
} catch { }
if (this.services.size > 0) {
const allCiphers = await this.getAllCiphers();
const inactive2faCiphers: CipherView[] = [];
const promises: Array<Promise<void>> = [];
const docs = new Map<string, string>();
allCiphers.forEach((c) => {
if (c.type !== CipherType.Login || (c.login.totp != null && c.login.totp !== '') || !c.login.hasUris) {
return;
}
for (let i = 0; i < c.login.uris.length; i++) {
const u = c.login.uris[i];
if (u.uri != null && u.uri !== '') {
const hostname = Utils.getHostname(u.uri);
if (hostname != null && this.services.has(hostname)) {
if (this.services.get(hostname) != null) {
docs.set(c.id, this.services.get(hostname));
}
inactive2faCiphers.push(c);
break;
}
}
}
});
await Promise.all(promises);
this.ciphers = inactive2faCiphers;
this.cipherDocs = docs;
}
}
protected getAllCiphers(): Promise<CipherView[]> {
return this.cipherService.getAllDecrypted();
}
private async load2fa() {
if (this.services.size > 0) {
return;
}
const response = await fetch(new Request('https://twofactorauth.org/data.json'));
if (response.status !== 200) {
throw new Error();
}
const responseJson = await response.json();
for (const categoryName in responseJson) {
if (responseJson.hasOwnProperty(categoryName)) {
const category = responseJson[categoryName];
for (const serviceName in category) {
if (category.hasOwnProperty(serviceName)) {
const service = category[serviceName];
if (service.tfa && service.url != null) {
const hostname = Utils.getHostname(service.url);
if (hostname != null) {
this.services.set(hostname, service.doc);
}
}
}
}
}
}
}
}

View File

@@ -10,7 +10,7 @@
<ul class="list-group list-group-flush" *ngIf="history.length">
<li class="list-group-item d-flex" *ngFor="let h of history">
<div>
<div class="password">{{h.password}}</div>
<div class="text-monospace password-wrapper" [innerHTML]="h.password | colorPassword"></div>
<small class="text-muted">{{h.date | date:'medium'}}</small>
</div>
<div class="ml-auto">

View File

@@ -1,6 +1,3 @@
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { Component } from '@angular/core';
import { I18nService } from 'jslib/abstractions/i18n.service';
@@ -16,9 +13,8 @@ import {
templateUrl: 'password-generator-history.component.html',
})
export class PasswordGeneratorHistoryComponent extends BasePasswordGeneratorHistoryComponent {
constructor(passwordGenerationService: PasswordGenerationService, analytics: Angulartics2,
platformUtilsService: PlatformUtilsService, i18nService: I18nService,
toasterService: ToasterService) {
super(passwordGenerationService, analytics, platformUtilsService, i18nService, toasterService, window);
constructor(passwordGenerationService: PasswordGenerationService, platformUtilsService: PlatformUtilsService,
i18nService: I18nService) {
super(passwordGenerationService, platformUtilsService, i18nService, window);
}
}

View File

@@ -2,46 +2,71 @@
<h1>{{'passwordGenerator' | i18n}}</h1>
</div>
<div class="card card-password bg-light my-4">
<div class="card-body">
{{password}}
</div>
</div>
<div class="row">
<div class="form-group col-4">
<label for="length">{{'length' | i18n}}</label>
<input id="length" class="form-control" type="number" min="5" max="128" [(ngModel)]="options.length" (input)="saveOptions()">
</div>
<div class="form-group col-4">
<label for="min-number">{{'minNumbers' | i18n}}</label>
<input id="min-number" class="form-control" type="number" min="0" max="9" (input)="saveOptions()" [(ngModel)]="options.minNumber">
</div>
<div class="form-group col-4">
<label for="min-special">{{'minSpecial' | i18n}}</label>
<input id="min-special" class="form-control" type="number" min="0" max="9" (input)="saveOptions()" [(ngModel)]="options.minSpecial">
</div>
<div class="card-body" [innerHTML]="password | colorPassword"></div>
</div>
<div class="form-group">
<div class="form-check">
<input id="uppercase" class="form-check-input" type="checkbox" (change)="saveOptions()" [(ngModel)]="options.uppercase">
<label for="uppercase" class="form-check-label">A-Z</label>
<div class="form-check form-check-inline">
<input id="generate-password" name="type" value="password" class="form-check-input" type="radio" (change)="saveOptions()"
[(ngModel)]="options.type">
<label for="generate-password" class="form-check-label">{{'password' | i18n}}</label>
</div>
<div class="form-check">
<input id="lowercase" class="form-check-input" type="checkbox" (change)="saveOptions()" [(ngModel)]="options.lowercase">
<label for="lowercase" class="form-check-label">a-z</label>
</div>
<div class="form-check">
<input id="numbers" class="form-check-input" type="checkbox" (change)="saveOptions()" [(ngModel)]="options.number">
<label for="numbers" class="form-check-label">0-9</label>
</div>
<div class="form-check">
<input id="special" class="form-check-input" type="checkbox" (change)="saveOptions()" [(ngModel)]="options.special">
<label for="special" class="form-check-label">!@#$%^&amp;*</label>
</div>
<div class="form-check">
<input id="ambiguous" class="form-check-input" type="checkbox" (change)="saveOptions()" [(ngModel)]="avoidAmbiguous">
<label for="ambiguous" class="form-check-label">{{'ambiguous' | i18n}}</label>
<div class="form-check form-check-inline">
<input id="generate-passphrase" name="type" value="passphrase" class="form-check-input" type="radio" (change)="saveOptions()"
[(ngModel)]="options.type">
<label for="generate-passphrase" class="form-check-label">{{'passphrase' | i18n}}</label>
</div>
</div>
<div class="row" *ngIf="options.type === 'passphrase'">
<div class="form-group col-4">
<label for="num-words">{{'numWords' | i18n}}</label>
<input id="num-words" class="form-control" type="number" min="3" max="20" [(ngModel)]="options.numWords" (blur)="saveOptions()">
</div>
<div class="form-group col-4">
<label for="word-separator">{{'wordSeparator' | i18n}}</label>
<input id="word-separator" class="form-control" type="text" maxlength="1" [(ngModel)]="options.wordSeparator"
(blur)="saveOptions()">
</div>
</div>
<ng-container *ngIf="options.type === 'password'">
<div class="row">
<div class="form-group col-4">
<label for="length">{{'length' | i18n}}</label>
<input id="length" class="form-control" type="number" min="5" max="128" [(ngModel)]="options.length" (blur)="saveOptions()">
</div>
<div class="form-group col-4">
<label for="min-number">{{'minNumbers' | i18n}}</label>
<input id="min-number" class="form-control" type="number" min="0" max="9" (input)="saveOptions()"
[(ngModel)]="options.minNumber">
</div>
<div class="form-group col-4">
<label for="min-special">{{'minSpecial' | i18n}}</label>
<input id="min-special" class="form-control" type="number" min="0" max="9" (input)="saveOptions()"
[(ngModel)]="options.minSpecial">
</div>
</div>
<div class="form-group">
<div class="form-check">
<input id="uppercase" class="form-check-input" type="checkbox" (change)="saveOptions()" [(ngModel)]="options.uppercase">
<label for="uppercase" class="form-check-label">A-Z</label>
</div>
<div class="form-check">
<input id="lowercase" class="form-check-input" type="checkbox" (change)="saveOptions()" [(ngModel)]="options.lowercase">
<label for="lowercase" class="form-check-label">a-z</label>
</div>
<div class="form-check">
<input id="numbers" class="form-check-input" type="checkbox" (change)="saveOptions()" [(ngModel)]="options.number">
<label for="numbers" class="form-check-label">0-9</label>
</div>
<div class="form-check">
<input id="special" class="form-check-input" type="checkbox" (change)="saveOptions()" [(ngModel)]="options.special">
<label for="special" class="form-check-label">!@#$%^&amp;*</label>
</div>
<div class="form-check">
<input id="ambiguous" class="form-check-input" type="checkbox" (change)="saveOptions()" [(ngModel)]="avoidAmbiguous">
<label for="ambiguous" class="form-check-label">{{'ambiguous' | i18n}}</label>
</div>
</div>
</ng-container>
<div class="d-flex">
<div>
<button type="button" class="btn btn-primary" (click)="regenerate()">

View File

@@ -1,6 +1,3 @@
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import {
Component,
ComponentFactoryResolver,
@@ -28,10 +25,9 @@ export class PasswordGeneratorComponent extends BasePasswordGeneratorComponent {
private modal: ModalComponent = null;
constructor(passwordGenerationService: PasswordGenerationService, analytics: Angulartics2,
platformUtilsService: PlatformUtilsService, i18nService: I18nService,
toasterService: ToasterService, private componentFactoryResolver: ComponentFactoryResolver) {
super(passwordGenerationService, analytics, platformUtilsService, i18nService, toasterService, window);
constructor(passwordGenerationService: PasswordGenerationService, platformUtilsService: PlatformUtilsService,
i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver) {
super(passwordGenerationService, platformUtilsService, i18nService, window);
}
history() {

View File

@@ -0,0 +1,44 @@
<div class="page-header">
<h1>
{{'reusedPasswordsReport' | i18n}}
<small *ngIf="hasLoaded && loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
</small>
</h1>
</div>
<p>{{'reusedPasswordsReportDesc' | i18n}}</p>
<div *ngIf="!hasLoaded && loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
</div>
<div class="mt-4" *ngIf="hasLoaded">
<app-callout type="success" title="{{'goodNews' | i18n}}" *ngIf="!ciphers.length">
{{'noReusedPasswords' | i18n}}
</app-callout>
<ng-container *ngIf="ciphers.length">
<app-callout type="danger" title="{{'reusedPasswordsFound' | i18n}}">
{{'reusedPasswordsFoundDesc' | i18n : (ciphers.length | number)}}
</app-callout>
<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>
<i class="fa fa-share-alt" *ngIf="!organization && c.organizationId" title="{{'shared' | i18n}}"></i>
<i class="fa fa-paperclip" title="{{'attachments' | i18n}}" *ngIf="c.hasAttachments"></i>
<br>
<small>{{c.subTitle}}</small>
</td>
<td class="text-right">
<span class="badge badge-warning">
{{'reusedXTimes' | i18n : passwordUseMap.get(c.login.password)}}
</span>
</td>
</tr>
</tbody>
</table>
</ng-container>
</div>
<ng-template #cipherAddEdit></ng-template>

View File

@@ -0,0 +1,58 @@
import {
Component,
ComponentFactoryResolver,
OnInit,
} from '@angular/core';
import { CipherService } from 'jslib/abstractions/cipher.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { UserService } from 'jslib/abstractions/user.service';
import { CipherView } from 'jslib/models/view/cipherView';
import { CipherType } from 'jslib/enums/cipherType';
import { CipherReportComponent } from './cipher-report.component';
@Component({
selector: 'app-reused-passwords-report',
templateUrl: 'reused-passwords-report.component.html',
})
export class ReusedPasswordsReportComponent extends CipherReportComponent implements OnInit {
passwordUseMap: Map<string, number>;
constructor(protected cipherService: CipherService, componentFactoryResolver: ComponentFactoryResolver,
messagingService: MessagingService, userService: UserService) {
super(componentFactoryResolver, userService, messagingService, true);
}
async ngOnInit() {
if (await this.checkAccess()) {
await super.load();
}
}
async setCiphers() {
const allCiphers = await this.getAllCiphers();
const ciphersWithPasswords: CipherView[] = [];
this.passwordUseMap = new Map<string, number>();
allCiphers.forEach((c) => {
if (c.type !== CipherType.Login || c.login.password == null || c.login.password === '') {
return;
}
ciphersWithPasswords.push(c);
if (this.passwordUseMap.has(c.login.password)) {
this.passwordUseMap.set(c.login.password, this.passwordUseMap.get(c.login.password) + 1);
} else {
this.passwordUseMap.set(c.login.password, 1);
}
});
const reusedPasswordCiphers = ciphersWithPasswords.filter((c) =>
this.passwordUseMap.has(c.login.password) && this.passwordUseMap.get(c.login.password) > 1);
this.ciphers = reusedPasswordCiphers;
}
protected getAllCiphers(): Promise<CipherView[]> {
return this.cipherService.getAllDecrypted();
}
}

View File

@@ -16,10 +16,37 @@
</div>
</div>
<div class="card">
<div class="card-header">{{'reports' | i18n}}</div>
<div class="card-header d-flex">
{{'reports' | i18n}}
<div class="ml-auto">
<a href="#" appStopClick class="badge badge-primary" *ngIf="!canAccessPremium" (click)="premiumRequired()">
{{'premium' | i18n}}
</a>
</div>
</div>
<div class="list-group list-group-flush">
<a routerLink="breach-report" class="list-group-item" routerLinkActive="active">
<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>
<a routerLink="breach-report" class="list-group-item d-flex" routerLinkActive="active">
{{'dataBreachReport' | i18n}}
<div class="ml-auto">
<span class="badge badge-success" *ngIf="!canAccessPremium">
{{'free' | i18n | uppercase}}
</span>
</div>
</a>
</div>
</div>

View File

@@ -1,7 +1,28 @@
import { Component } from '@angular/core';
import {
Component,
OnInit,
} from '@angular/core';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { UserService } from 'jslib/abstractions/user.service';
@Component({
selector: 'app-tools',
templateUrl: 'tools.component.html',
})
export class ToolsComponent { }
export class ToolsComponent implements OnInit {
canAccessPremium = false;
constructor(private userService: UserService, private messagingService: MessagingService) { }
async ngOnInit() {
this.canAccessPremium = await this.userService.canAccessPremium();
}
premiumRequired() {
if (!this.canAccessPremium) {
this.messagingService.send('premiumRequired');
return;
}
}
}

View File

@@ -0,0 +1,39 @@
<div class="page-header">
<h1>
{{'unsecuredWebsitesReport' | i18n}}
<small *ngIf="hasLoaded && loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
</small>
</h1>
</div>
<p>{{'unsecuredWebsitesReportDesc' | i18n}}</p>
<div *ngIf="!hasLoaded && loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
</div>
<div class="mt-4" *ngIf="hasLoaded">
<app-callout type="success" title="{{'goodNews' | i18n}}" *ngIf="!ciphers.length">
{{'noUnsecuredWebsites' | i18n}}
</app-callout>
<ng-container *ngIf="ciphers.length">
<app-callout type="danger" title="{{'unsecuredWebsitesFound' | i18n}}">
{{'unsecuredWebsitesFoundDesc' | i18n : (ciphers.length | number)}}
</app-callout>
<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>
<i class="fa fa-share-alt" *ngIf="!organization && c.organizationId" title="{{'shared' | i18n}}"></i>
<i class="fa fa-paperclip" title="{{'attachments' | i18n}}" *ngIf="c.hasAttachments"></i>
<br>
<small>{{c.subTitle}}</small>
</td>
</tr>
</tbody>
</table>
</ng-container>
</div>
<ng-template #cipherAddEdit></ng-template>

View File

@@ -0,0 +1,47 @@
import {
Component,
ComponentFactoryResolver,
OnInit,
} from '@angular/core';
import { CipherService } from 'jslib/abstractions/cipher.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { UserService } from 'jslib/abstractions/user.service';
import { CipherType } from 'jslib/enums/cipherType';
import { CipherView } from 'jslib/models/view/cipherView';
import { CipherReportComponent } from './cipher-report.component';
@Component({
selector: 'app-unsecured-websites-report',
templateUrl: 'unsecured-websites-report.component.html',
})
export class UnsecuredWebsitesReportComponent extends CipherReportComponent implements OnInit {
constructor(protected cipherService: CipherService, componentFactoryResolver: ComponentFactoryResolver,
messagingService: MessagingService, userService: UserService) {
super(componentFactoryResolver, userService, messagingService, true);
}
async ngOnInit() {
if (await this.checkAccess()) {
await super.load();
}
}
async setCiphers() {
const allCiphers = await this.getAllCiphers();
const unsecuredCiphers = allCiphers.filter((c) => {
if (c.type !== CipherType.Login || !c.login.hasUris) {
return false;
}
return c.login.uris.find((u) => u.uri != null && u.uri.indexOf('http://') === 0) != null;
});
this.ciphers = unsecuredCiphers;
}
protected getAllCiphers(): Promise<CipherView[]> {
return this.cipherService.getAllDecrypted();
}
}

View File

@@ -0,0 +1,44 @@
<div class="page-header">
<h1>
{{'weakPasswordsReport' | i18n}}
<small *ngIf="hasLoaded && loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
</small>
</h1>
</div>
<p>{{'weakPasswordsReportDesc' | i18n}}</p>
<div *ngIf="!hasLoaded && loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
</div>
<div class="mt-4" *ngIf="hasLoaded">
<app-callout type="success" title="{{'goodNews' | i18n}}" *ngIf="!ciphers.length">
{{'noWeakPasswords' | i18n}}
</app-callout>
<ng-container *ngIf="ciphers.length">
<app-callout type="danger" title="{{'weakPasswordsFound' | i18n}}">
{{'weakPasswordsFoundDesc' | i18n : (ciphers.length | number)}}
</app-callout>
<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>
<i class="fa fa-share-alt" *ngIf="!organization && c.organizationId" title="{{'shared' | i18n}}"></i>
<i class="fa fa-paperclip" title="{{'attachments' | i18n}}" *ngIf="c.hasAttachments"></i>
<br>
<small>{{c.subTitle}}</small>
</td>
<td class="text-right">
<span class="badge badge-{{passwordStrengthMap.get(c.id)[1]}}">
{{passwordStrengthMap.get(c.id)[0] | i18n}}
</span>
</td>
</tr>
</tbody>
</table>
</ng-container>
</div>
<ng-template #cipherAddEdit></ng-template>

View File

@@ -0,0 +1,90 @@
import {
Component,
ComponentFactoryResolver,
OnInit,
} from '@angular/core';
import { CipherService } from 'jslib/abstractions/cipher.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { UserService } from 'jslib/abstractions/user.service';
import { CipherView } from 'jslib/models/view/cipherView';
import { CipherType } from 'jslib/enums/cipherType';
import { CipherReportComponent } from './cipher-report.component';
@Component({
selector: 'app-weak-passwords-report',
templateUrl: 'weak-passwords-report.component.html',
})
export class WeakPasswordsReportComponent extends CipherReportComponent implements OnInit {
passwordStrengthMap = new Map<string, [string, string]>();
private passwordStrengthCache = new Map<string, number>();
constructor(protected cipherService: CipherService, protected passwordGenerationService: PasswordGenerationService,
componentFactoryResolver: ComponentFactoryResolver, messagingService: MessagingService,
userService: UserService) {
super(componentFactoryResolver, userService, messagingService, true);
}
async ngOnInit() {
if (await this.checkAccess()) {
await super.load();
}
}
async setCiphers() {
const allCiphers = await this.getAllCiphers();
const weakPasswordCiphers: CipherView[] = [];
allCiphers.forEach((c) => {
if (c.type !== CipherType.Login || c.login.password == null || c.login.password === '') {
return;
}
const hasUsername = c.login.username != null && c.login.username.trim() !== '';
const cacheKey = c.login.password + '_____' + (hasUsername ? c.login.username : '');
if (!this.passwordStrengthCache.has(cacheKey)) {
let userInput: string[] = [];
if (hasUsername) {
const atPosition = c.login.username.indexOf('@');
if (atPosition > -1) {
userInput = userInput.concat(
c.login.username.substr(0, atPosition).trim().toLowerCase().split(/[^A-Za-z0-9]/))
.filter((i) => i.length >= 3);
} else {
userInput = c.login.username.trim().toLowerCase().split(/[^A-Za-z0-9]/)
.filter((i) => i.length >= 3);
}
}
const result = this.passwordGenerationService.passwordStrength(c.login.password,
userInput.length > 0 ? userInput : null);
this.passwordStrengthCache.set(cacheKey, result.score);
}
const score = this.passwordStrengthCache.get(cacheKey);
if (score != null && score <= 3) {
this.passwordStrengthMap.set(c.id, this.scoreKey(score));
weakPasswordCiphers.push(c);
}
});
this.ciphers = weakPasswordCiphers;
}
protected getAllCiphers(): Promise<CipherView[]> {
return this.cipherService.getAllDecrypted();
}
private scoreKey(score: number): [string, string] {
switch (score) {
case 4:
return ['strong', 'success'];
case 3:
return ['good', 'primary'];
case 2:
return ['weak', 'warning'];
default:
return ['veryWeak', 'danger'];
}
}
}

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