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

Compare commits

...

299 Commits

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

* New translations messages.json (Korean)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Japanese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

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

* New translations messages.json (Danish)

* New translations messages.json (Dutch)

* New translations messages.json (French)

* New translations messages.json (Italian)

* New translations messages.json (Japanese)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Polish)

* New translations messages.json (Portuguese)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Russian)

* New translations messages.json (Slovak)

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

* New translations messages.json (Korean)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Japanese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

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

* New translations messages.json (Korean)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Japanese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

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

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Danish)

* New translations messages.json (Dutch)

* New translations messages.json (Estonian)

* New translations messages.json (Finnish)

* New translations messages.json (French)

* New translations messages.json (Japanese)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Polish)

* New translations messages.json (Portuguese)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Russian)

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

* New translations messages.json (Korean)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Japanese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

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

* New translations messages.json (Korean)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Japanese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

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

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Danish)

* New translations messages.json (French)

* New translations messages.json (German)

* 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 (Swedish)
2018-08-07 15:43:30 -04:00
Kyle Spearrin
85db64ed70 webVault param 2018-08-07 15:04:30 -04:00
Kyle Spearrin
d72ba456ae cacheWeb arg 2018-08-07 14:31:16 -04:00
Kyle Spearrin
b4a0913994 dont need to copy settings file anymore 2018-08-07 14:01:47 -04:00
Kyle Spearrin
6fb8a9ef2f add polish language 2018-08-06 17:42:59 -04:00
Kyle Spearrin
4e58708f12 New Crowdin translations (#238)
* New translations messages.json (Chinese Simplified)

* New translations messages.json (Norwegian Bokmal)

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

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Japanese)

* New translations messages.json (Italian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Estonian)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Ukrainian)
2018-08-06 17:41:26 -04:00
Kyle Spearrin
32876367c2 update status maps when user status changes 2018-08-06 17:37:54 -04:00
Kyle Spearrin
226aa0b3ba show message that users needs to be confirmed 2018-08-06 17:18:33 -04:00
Kyle Spearrin
c37359cdfd use jslib import service 2018-08-06 11:40:12 -04:00
Kyle Spearrin
d2d89c252a add new languages 2018-08-06 08:58:50 -04:00
Kyle Spearrin
b76f1d3c8c New Crowdin translations (#237)
* New translations messages.json (Chinese Simplified)

* New translations messages.json (Norwegian Bokmal)

* 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 (Chinese Traditional)

* New translations messages.json (Japanese)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Ukrainian)
2018-08-06 08:57:14 -04:00
Kyle Spearrin
9ff34a12a2 fix save button on collection management 2018-08-03 21:09:22 -04:00
Kyle Spearrin
c5f6ce3fe5 no org message on share 2018-08-03 21:07:24 -04:00
Kyle Spearrin
965d556ac9 fix grammatical error 2018-08-03 17:13:33 -04:00
Kyle Spearrin
e52d3f21d1 standardize locale name 2018-08-02 09:35:44 -04:00
Kyle Spearrin
00576053d0 toaster paragraph formatting 2018-08-02 08:46:33 -04:00
Kyle Spearrin
1f040efb7f set viewport width 2018-08-01 22:02:32 -04:00
Kyle Spearrin
3b5e9775c5 New Crowdin translations (#235)
* New translations messages.json (Chinese Simplified)

* New translations messages.json (Norwegian Bokmal)

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

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Japanese)

* New translations messages.json (Italian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Estonian)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Ukrainian)
2018-08-01 17:03:43 -04:00
Kyle Spearrin
ea0653460e update jslib 2018-08-01 16:55:36 -04:00
Kyle Spearrin
6ed80eb6c9 org disabled and license expired warnings 2018-08-01 16:51:25 -04:00
Kyle Spearrin
12bdd87705 free desc 2018-08-01 15:51:46 -04:00
Kyle Spearrin
9bdf7ff174 remove ps1 script 2018-08-01 10:53:02 -04:00
Kyle Spearrin
2ab2f757a0 is mobile browser checks on autofocus 2018-08-01 09:13:55 -04:00
Kyle Spearrin
f830de8d13 New Crowdin translations (#234)
* New translations messages.json (Chinese Simplified)

* New translations messages.json (Norwegian Bokmal)

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

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Japanese)

* New translations messages.json (Italian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Estonian)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Ukrainian)
2018-08-01 07:58:30 -04:00
Kyle Spearrin
7eb329db6b add french translations support 2018-08-01 07:35:33 -04:00
Kyle Spearrin
fecb980c9b update jslib 2018-07-31 23:49:40 -04:00
Kyle Spearrin
600218cf7b support for otpauth 2018-07-31 11:26:04 -04:00
Kyle Spearrin
8eb48e4311 rename to "enable gravatars" 2018-07-31 00:02:09 -04:00
Kyle Spearrin
ac33d2f37c add support for gravatars 2018-07-30 23:52:04 -04:00
Kyle Spearrin
bfc462cbec fix navbar text breaks 2018-07-30 22:53:08 -04:00
Kyle Spearrin
8b8bd88adf control when password history shows 2018-07-30 22:02:01 -04:00
Kyle Spearrin
bc768b773b bump version 2018-07-30 21:24:26 -04:00
Kyle Spearrin
950e5a57b1 additional language support 2018-07-30 21:14:34 -04:00
Kyle Spearrin
52a4fc8e93 clean l10n script 2018-07-30 17:10:35 -04:00
Kyle Spearrin
dd5c840b61 contributing doc 2018-07-30 17:06:57 -04:00
Kyle Spearrin
337c01daf8 select, not select in 2018-07-30 16:57:58 -04:00
Kyle Spearrin
5a9da2a97a New Crowdin translations (#232)
* New translations messages.json (Chinese Simplified)

* New translations messages.json (Norwegian Bokmal)

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

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Japanese)

* New translations messages.json (Italian)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Estonian)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Ukrainian)
2018-07-30 16:55:06 -04:00
Kyle Spearrin
38c1a0f383 clean l10n command 2018-07-30 16:52:00 -04:00
Kyle Spearrin
2b1718945f update paymentCharged messages 2018-07-30 16:47:25 -04:00
Kyle Spearrin
a6319a3865 update jslib 2018-07-30 11:00:23 -04:00
Kyle Spearrin
35241307d3 update jslib 2018-07-30 10:05:44 -04:00
Kyle Spearrin
ed8aaa5505 view password history 2018-07-30 08:48:48 -04:00
Kyle Spearrin
a25f6dee73 check group access on collection edit page 2018-07-30 08:12:47 -04:00
Kyle Spearrin
9aa8699617 disable autocomplete of password fields 2018-07-30 08:00:23 -04:00
Kyle Spearrin
94f671ca6b show reivison dates for item edit 2018-07-27 23:38:12 -04:00
Kyle Spearrin
7c9016bf4a never option in dev mode 2018-07-27 22:52:27 -04:00
Kyle Spearrin
98d18fb097 only show add button if admin 2018-07-27 22:39:51 -04:00
Kyle Spearrin
dc69887628 fixes for non-admin actions on org vault list 2018-07-27 22:36:25 -04:00
Kyle Spearrin
7c882ed40c catch error 2018-07-27 22:08:31 -04:00
Kyle Spearrin
c8909beedd uncheck all on destroy 2018-07-27 22:05:03 -04:00
Kyle Spearrin
15fc4e5f2d filter ciphers before doing select all, resolves #227 2018-07-27 21:59:49 -04:00
Kyle Spearrin
229b9cf40a update jslib 2018-07-27 21:52:49 -04:00
Kyle Spearrin
17ae441c22 dont set default lock option in dev 2018-07-27 21:52:36 -04:00
Kyle Spearrin
9a70c0e8a7 update jslib 2018-07-27 17:48:32 -04:00
Kyle Spearrin
2a41fee1ff on browser refresh 2018-07-27 15:11:57 -04:00
Kyle Spearrin
f385c3773c allow users to change lock options, default to 15 min 2018-07-27 15:08:59 -04:00
Kyle Spearrin
4b4e847816 Update README.md 2018-07-26 10:03:55 -04:00
Kyle Spearrin
a70c4c250c Update README.md 2018-07-26 10:03:22 -04:00
Kyle Spearrin
950e75e7e3 Update README.md 2018-07-26 10:02:24 -04:00
Kyle Spearrin
2cbf2006c6 Update README.md 2018-07-26 10:02:04 -04:00
Kyle Spearrin
14c4095a4e Update README.md 2018-07-26 09:54:36 -04:00
166 changed files with 36625 additions and 13259 deletions

3
.gitattributes vendored Normal file
View File

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

13
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,13 @@
Code contributions are welcome! Please commit any pull requests against the `master` branch.
# Localization (l10n)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/bitwarden-web/localized.svg)](https://crowdin.com/project/bitwarden-web)
We use a translation tool called [Crowdin](https://crowdin.com) to help manage our localization efforts across many different languages.
If you are interested in helping translate the Bitwarden web vault into another language (or make a translation correction), please register an account at Crowdin and join our project here: https://crowdin.com/project/bitwarden-web
If the language that you are interested in translating is not already listed, create a new account on Crowdin, join the project, and contact the project owner (https://crowdin.com/profile/kspearrin).
You can read Crowdin's getting started guide for translators here: https://support.crowdin.com/crowdin-intro/

View File

@@ -1,18 +1,31 @@
[![appveyor build](https://ci.appveyor.com/api/projects/status/github/bitwarden/web?branch=master&svg=true)](https://ci.appveyor.com/project/bitwarden/web) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/bitwarden-web/localized.svg)](https://crowdin.com/project/bitwarden-web) [![DockerHub](https://img.shields.io/docker/pulls/bitwarden/web.svg)](https://hub.docker.com/u/bitwarden/) [![Join the chat at https://gitter.im/bitwarden/Lobby](https://badges.gitter.im/bitwarden/Lobby.svg)](https://gitter.im/bitwarden/Lobby)
<p align="center">
<img src="https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/web-vault-macbook.png" alt="" width="600" height="358" />
</p>
<p align="center">
The Bitwarden web project is an Angular application that powers the web vault (https://vault.bitwarden.com/).
</p>
<p align="center">
<a href="https://ci.appveyor.com/project/bitwarden/web/branch/master" target="_blank">
<img src="https://ci.appveyor.com/api/projects/status/github/bitwarden/web?branch=master&svg=true" alt="appveyor build" />
</a>
<a href="https://crowdin.com/project/bitwarden-web" target="_blank">
<img src="https://d322cqt584bo4o.cloudfront.net/bitwarden-web/localized.svg" alt="Crowdin" />
</a>
<a href="https://hub.docker.com/u/bitwarden/" target="_blank">
<img src="https://img.shields.io/docker/pulls/bitwarden/web.svg" alt="DockerHub" />
</a>
<a href="https://gitter.im/bitwarden/Lobby" target="_blank">
<img src="https://badges.gitter.im/bitwarden/Lobby.svg" alt="gitter chat" />
</a>
</p>
# Bitwarden Web Vault
## Build/Run
The Bitwarden web project is an Angular application that powers the web vault (https://vault.bitwarden.com/).
<img src="https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/web-vault-macbook.png" alt="" />
# Build/Run
**Requirements**
### Requirements
- [Node.js](https://nodejs.org) v8.11 or greater
**Run the app**
### Run the app
```
npm install
@@ -40,7 +53,7 @@ await apiService.setUrls({
```
# Contribute
## Contribute
Code contributions are welcome! Please commit any pull requests against the `master` branch.

View File

@@ -1,13 +0,0 @@
$dir = Split-Path -Parent $MyInvocation.MyCommand.Path
echo "`n# Building Web"
echo "`nBuilding app"
echo "npm version $(npm --version)"
npm install
npm run sub:update
npm run dist:selfhost
echo "`nBuilding docker image"
docker --version
docker build -t bitwarden/web $dir\.

View File

@@ -30,10 +30,9 @@ mkhomedir_helper $USERNAME
# The rest...
chown -R $USERNAME:$GROUPNAME /etc/bitwarden
cp /etc/bitwarden/web/settings.js /app/js/settings.js
cp /etc/bitwarden/web/app-id.json /app/app-id.json
chown -R $USERNAME:$GROUPNAME /app
chown -R $USERNAME:$GROUPNAME /bitwarden_server
exec gosu $USERNAME:$GROUPNAME dotnet /bitwarden_server/Server.dll \
/contentRoot=/app /webRoot=. /serveUnknown=false
/contentRoot=/app /webRoot=. /serveUnknown=false /webVault=true

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: 9df96a3288...27566c3fd5

11100
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,90 +1,95 @@
{
"name": "bitwarden-web",
"version": "2.0.0",
"version": "2.7.1",
"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.7.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

@@ -1,4 +1,4 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate autocomplete="off">
<div class="row justify-content-md-center mt-5">
<div class="col-5" [ngClass]="{'col-9': selectedProviderType === providerType.Duo || selectedProviderType === providerType.OrganizationDuo}">
<p class="lead text-center mb-4">{{title}}</p>
@@ -24,7 +24,8 @@
<img src="../../images/yubikey.jpg" class="rounded img-fluid mb-3" alt="">
<div class="form-group">
<label for="code" class="sr-only">{{'verificationCode' | i18n}}</label>
<input id="code" type="password" name="Code" class="form-control" [(ngModel)]="token" required appAutofocus appInputVerbatim>
<input id="code" type="password" name="Code" class="form-control" [(ngModel)]="token" required appAutofocus appInputVerbatim
autocomplete="new-password">
</div>
</ng-container>
<ng-container *ngIf="selectedProviderType === providerType.U2f">

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,16 +154,45 @@ 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';
import { registerLocaleData } from '@angular/common';
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';
import localeRu from '@angular/common/locales/ru';
import localeSk from '@angular/common/locales/sk';
import localeSv from '@angular/common/locales/sv';
import localeZhCn from '@angular/common/locales/zh-Hans';
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');
registerLocaleData(localeRu, 'ru');
registerLocaleData(localeSk, 'sk');
registerLocaleData(localeSv, 'sv');
registerLocaleData(localeZhCn, 'zh-CN');
@NgModule({
imports: [
@@ -154,7 +206,7 @@ registerLocaleData(localeSk, 'sk');
clearQueryParams: true,
},
}),
ToasterModule,
ToasterModule.forRoot(),
],
declarations: [
AcceptOrganizationComponent,
@@ -176,24 +228,28 @@ registerLocaleData(localeSk, 'sk');
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,
@@ -211,23 +267,30 @@ registerLocaleData(localeSk, 'sk');
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,
@@ -235,6 +298,7 @@ registerLocaleData(localeSk, 'sk');
RecoverDeleteComponent,
RecoverTwoFactorComponent,
RegisterComponent,
ReusedPasswordsReportComponent,
SearchCiphersPipe,
SearchPipe,
SettingsComponent,
@@ -253,6 +317,7 @@ registerLocaleData(localeSk, 'sk');
TwoFactorU2fComponent,
TwoFactorVerifyComponent,
TwoFactorYubiKeyComponent,
UnsecuredWebsitesReportComponent,
UpdateKeyComponent,
UpdateLicenseComponent,
UserBillingComponent,
@@ -261,6 +326,7 @@ registerLocaleData(localeSk, 'sk');
VerifyEmailComponent,
VerifyEmailTokenComponent,
VerifyRecoverDeleteComponent,
WeakPasswordsReportComponent,
],
entryComponents: [
AddEditComponent,
@@ -282,6 +348,7 @@ registerLocaleData(localeSk, 'sk');
OrgEntityUsersComponent,
OrgGroupAddEditComponent,
OrgUserAddEditComponent,
OrgUserConfirmComponent,
OrgUserGroupsComponent,
PasswordGeneratorHistoryComponent,
PurgeVaultComponent,

View File

@@ -6,6 +6,11 @@ import {
} from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
import { StateService } from 'jslib/abstractions/state.service';
import { Utils } from 'jslib/misc/utils';
@Component({
selector: 'app-avatar',
template: '<img [src]="sanitizer.bypassSecurityTrustResourceUrl(src)" title="{{data}}" ' +
@@ -13,8 +18,8 @@ import { DomSanitizer } from '@angular/platform-browser';
})
export class AvatarComponent implements OnChanges, OnInit {
@Input() data: string;
@Input() width = 45;
@Input() height = 45;
@Input() email: string;
@Input() size = 45;
@Input() charCount = 2;
@Input() textColor = '#ffffff';
@Input() fontSize = 20;
@@ -24,7 +29,8 @@ export class AvatarComponent implements OnChanges, OnInit {
src: string;
constructor(public sanitizer: DomSanitizer) { }
constructor(public sanitizer: DomSanitizer, private cryptoFunctionService: CryptoFunctionService,
private stateService: StateService) { }
ngOnInit() {
if (!this.dynamic) {
@@ -38,24 +44,31 @@ export class AvatarComponent implements OnChanges, OnInit {
}
}
private generate() {
let chars: string = null;
const upperData = this.data.toUpperCase();
private async generate() {
const enableGravatars = await this.stateService.get<boolean>('enableGravatars');
if (enableGravatars && this.email != null) {
const hashBytes = await this.cryptoFunctionService.hash(this.email.toLowerCase().trim(), 'md5');
const hash = Utils.fromBufferToHex(hashBytes).toLowerCase();
this.src = 'https://www.gravatar.com/avatar/' + hash + '?s=' + this.size + '&r=pg&d=retro';
} else {
let chars: string = null;
const upperData = this.data.toUpperCase();
if (this.charCount > 1) {
chars = this.getFirstLetters(upperData, this.charCount);
}
if (chars == null) {
chars = upperData.substr(0, this.charCount);
}
if (this.charCount > 1) {
chars = this.getFirstLetters(upperData, this.charCount);
}
if (chars == null) {
chars = upperData.substr(0, this.charCount);
}
const charObj = this.getCharText(chars);
const color = this.stringToColor(upperData);
const svg = this.getSvg(this.width, this.height, color);
svg.appendChild(charObj);
const html = window.document.createElement('div').appendChild(svg).outerHTML;
const svgHtml = window.btoa(unescape(encodeURIComponent(html)));
this.src = 'data:image/svg+xml;base64,' + svgHtml;
const charObj = this.getCharText(chars);
const color = this.stringToColor(upperData);
const svg = this.getSvg(this.size, color);
svg.appendChild(charObj);
const html = window.document.createElement('div').appendChild(svg).outerHTML;
const svgHtml = window.btoa(unescape(encodeURIComponent(html)));
this.src = 'data:image/svg+xml;base64,' + svgHtml;
}
}
private stringToColor(str: string): string {
@@ -85,15 +98,15 @@ export class AvatarComponent implements OnChanges, OnInit {
return null;
}
private getSvg(width: number, height: number, color: string): HTMLElement {
private getSvg(size: number, color: string): HTMLElement {
const svgTag = window.document.createElement('svg');
svgTag.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
svgTag.setAttribute('pointer-events', 'none');
svgTag.setAttribute('width', width.toString());
svgTag.setAttribute('height', height.toString());
svgTag.setAttribute('width', size.toString());
svgTag.setAttribute('height', size.toString());
svgTag.style.backgroundColor = color;
svgTag.style.width = width + 'px';
svgTag.style.height = height + 'px';
svgTag.style.width = size + 'px';
svgTag.style.height = size + 'px';
return svgTag;
}

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

@@ -23,9 +23,9 @@
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="nav-profile">
<div class="dropdown-item-text d-flex align-items-center" *ngIf="name" appStopProp>
<app-avatar [data]="name" width="25" height="25" fontSize="14" [circle]="true"></app-avatar>
<app-avatar [data]="name" [email]="email" size="25" fontSize="14" [circle]="true"></app-avatar>
<div class="ml-2 overflow-hidden">
{{'loggedInAs' | i18n}}
<span>{{'loggedInAs' | i18n}}</span>
<small class="text-muted">{{name}}</small>
</div>
</div>

View File

@@ -14,6 +14,7 @@ import { TokenService } from 'jslib/abstractions/token.service';
export class NavbarComponent implements OnInit {
selfHosted = false;
name: string;
email: string;
constructor(private messagingService: MessagingService, private platformUtilsService: PlatformUtilsService,
private tokenService: TokenService) {
@@ -22,8 +23,9 @@ export class NavbarComponent implements OnInit {
async ngOnInit() {
this.name = await this.tokenService.getName();
this.email = await this.tokenService.getEmail();
if (this.name == null || this.name.trim() === '') {
this.name = await this.tokenService.getEmail();
this.name = this.email;
}
}

View File

@@ -2,13 +2,19 @@
<div class="org-nav" *ngIf="organization">
<div class="container d-flex flex-column">
<div class="my-auto d-flex align-items-center pl-1">
<app-avatar [data]="organization.name" [width]="45" [height]="45" [circle]="true"></app-avatar>
<app-avatar [data]="organization.name" size="45" [circle]="true"></app-avatar>
<div class="org-name ml-3">
<span>{{organization.name}}</span>
<small class="text-muted">{{'organization' | i18n}}</small>
</div>
<div class="ml-auto card border-danger text-danger bg-transparent" *ngIf="!organization.enabled">
<div class="card-body py-2">
<i class="fa fa-exclamation-triangle"></i>
{{'organizationIsDisabled' | i18n}}
</div>
</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>
@@ -21,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

@@ -8,6 +8,7 @@ import {
} from '@angular/core';
import { ModalComponent as BaseModalComponent } from 'jslib/angular/components/modal.component';
import { Utils } from 'jslib/misc/utils';
@Component({
selector: 'app-modal',
@@ -39,7 +40,9 @@ export class ModalComponent extends BaseModalComponent {
});
this.el.on('shown.bs.modal', () => {
this.onShown.emit();
this.el.find('*[appAutoFocus]').focus();
if (!Utils.isMobileBrowser) {
this.el.find('*[appAutoFocus]').focus();
}
});
this.el.on('hide.bs.modal', () => {
this.onClose.emit();

View File

@@ -15,45 +15,45 @@
<label for="name">{{'name' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required>
</div>
<h3 class="mt-4 d-flex mb-0">
{{'groupAccess' | i18n}}
<div class="ml-auto" *ngIf="groups && groups.length">
<button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
{{'selectAll' | i18n}}
</button>
<button type="button" (click)="selectAll(false)" class="btn btn-link btn-sm py-0">
{{'unselectAll' | i18n}}
</button>
<ng-container *ngIf="accessGroups">
<h3 class="mt-4 d-flex mb-0">
{{'groupAccess' | i18n}}
<div class="ml-auto" *ngIf="groups && groups.length">
<button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
{{'selectAll' | i18n}}
</button>
<button type="button" (click)="selectAll(false)" class="btn btn-link btn-sm py-0">
{{'unselectAll' | i18n}}
</button>
</div>
</h3>
<div *ngIf="!groups || !groups.length">
{{'noGroupsInList' | i18n}}
</div>
</h3>
<div *ngIf="!groups || !groups.length">
{{'noGroupsInList' | i18n}}
</div>
<table class="table table-hover table-list mb-0" *ngIf="groups && groups.length">
<thead>
<tr>
<th>&nbsp;</th>
<th>{{'name' | i18n}}</th>
<th width="100" class="text-center">{{'readOnly' | i18n}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let g of groups; let i = index">
<td class="table-list-checkbox" (click)="check(g)">
<input type="checkbox" [(ngModel)]="g.checked" name="Groups[{{i}}].Checked" [disabled]="g.accessAll">
</td>
<td (click)="check(g)">
<span appStopProp>
<table class="table table-hover table-list mb-0" *ngIf="groups && groups.length">
<thead>
<tr>
<th>&nbsp;</th>
<th>{{'name' | i18n}}</th>
<th width="100" class="text-center">{{'readOnly' | i18n}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let g of groups; let i = index">
<td class="table-list-checkbox" (click)="check(g)">
<input type="checkbox" [(ngModel)]="g.checked" name="Groups[{{i}}].Checked" [disabled]="g.accessAll" appStopProp>
</td>
<td (click)="check(g)">
{{g.name}}
<i class="fa fa-th text-muted fa-fw" *ngIf="g.accessAll" title="This group can access all items"></i>
</span>
</td>
<td class="text-center">
<input type="checkbox" [(ngModel)]="g.readOnly" name="Groups[{{i}}].ReadOnly" [disabled]="!g.checked || g.accessAll">
</td>
</tr>
</tbody>
</table>
</td>
<td class="text-center">
<input type="checkbox" [(ngModel)]="g.readOnly" name="Groups[{{i}}].ReadOnly" [disabled]="!g.checked || g.accessAll">
</td>
</tr>
</tbody>
</table>
</ng-container>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
@@ -62,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

@@ -13,6 +13,7 @@ import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { UserService } from 'jslib/abstractions/user.service';
import { CipherString } from 'jslib/models/domain/cipherString';
import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
@@ -34,6 +35,7 @@ export class CollectionAddEditComponent implements OnInit {
loading = true;
editMode: boolean = false;
accessGroups: boolean = false;
title: string;
name: string;
groups: GroupResponse[] = [];
@@ -44,12 +46,17 @@ export class CollectionAddEditComponent implements OnInit {
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private platformUtilsService: PlatformUtilsService, private cryptoService: CryptoService) { }
private platformUtilsService: PlatformUtilsService, private cryptoService: CryptoService,
private userService: UserService) { }
async ngOnInit() {
const organization = await this.userService.getOrganization(this.organizationId);
this.accessGroups = organization.useGroups;
this.editMode = this.loading = this.collectionId != null;
const groupsResponse = await this.apiService.getGroups(this.organizationId);
this.groups = groupsResponse.data.map((r) => r).sort(Utils.getSortFunction(this.i18nService, 'name'));
if (this.accessGroups) {
const groupsResponse = await this.apiService.getGroups(this.organizationId);
this.groups = groupsResponse.data.map((r) => r).sort(Utils.getSortFunction(this.i18nService, 'name'));
}
this.orgKey = await this.cryptoService.getOrgKey(this.organizationId);
if (this.editMode) {
@@ -58,7 +65,7 @@ export class CollectionAddEditComponent implements OnInit {
try {
const collection = await this.apiService.getCollectionDetails(this.organizationId, this.collectionId);
this.name = await this.cryptoService.decryptToUtf8(new CipherString(collection.name), this.orgKey);
if (collection.groups != null && this.groups != null) {
if (collection.groups != null && this.groups.length > 0) {
collection.groups.forEach((s) => {
const group = this.groups.filter((g) => !g.accessAll && g.id === s.id);
if (group != null && group.length > 0) {
@@ -96,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" width="25" height="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

@@ -29,56 +29,63 @@
<i class="fa fa-spinner fa-spin text-muted" *ngIf="loading" title="{{'loading' | i18n}}"></i>
<ng-container *ngIf="!loading && (users | search:searchText:'name':'email':'id') as searchedUsers">
<p *ngIf="!searchedUsers.length">{{'noUsersInList' | i18n}}</p>
<table class="table table-hover table-list" *ngIf="searchedUsers.length">
<tbody>
<tr *ngFor="let u of searchedUsers">
<td width="30">
<app-avatar [data]="u.name || u.email" width="25" height="25" [circle]="true" [fontSize]="14"></app-avatar>
</td>
<td>
<a href="#" appStopClick (click)="edit(u)">{{u.email}}</a>
<span class="badge badge-secondary" *ngIf="u.status === organizationUserStatusType.Invited">{{'invited' | i18n}}</span>
<span class="badge badge-warning" *ngIf="u.status === organizationUserStatusType.Accepted">{{'accepted' | i18n}}</span>
<small class="text-muted d-block" *ngIf="u.name">{{u.name}}</small>
</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">
<div class="dropdown" appListDropdown>
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-cog fa-lg"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="#" appStopClick (click)="reinvite(u)" *ngIf="u.status === organizationUserStatusType.Invited">
<i class="fa fa-fw fa-envelope-o"></i>
{{'resendInvitation' | i18n}}
</a>
<a class="dropdown-item text-success" href="#" appStopClick (click)="confirm(u)" *ngIf="u.status === organizationUserStatusType.Accepted">
<i class="fa fa-fw fa-check"></i>
{{'confirm' | i18n}}
</a>
<a class="dropdown-item" href="#" appStopClick (click)="groups(u)" *ngIf="accessGroups">
<i class="fa fa-fw fa-sitemap"></i>
{{'groups' | i18n}}
</a>
<a class="dropdown-item" href="#" appStopClick (click)="events(u)" *ngIf="accessEvents && u.status === organizationUserStatusType.Confirmed">
<i class="fa fa-fw fa-file-text-o"></i>
{{'eventLogs' | i18n}}
</a>
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(u)">
<i class="fa fa-fw fa-remove"></i>
{{'remove' | i18n}}
</a>
<ng-container *ngIf="searchedUsers.length">
<app-callout type="info" title="{{'confirmUsers' | i18n}}" icon="fa-check-circle" *ngIf="showConfirmUsers">
{{'usersNeedConfirmed' | i18n}}
</app-callout>
<table class="table table-hover table-list">
<tbody>
<tr *ngFor="let u of searchedUsers">
<td width="30">
<app-avatar [data]="u.name || u.email" [email]="u.email" size="25" [circle]="true" [fontSize]="14"></app-avatar>
</td>
<td>
<a href="#" appStopClick (click)="edit(u)">{{u.email}}</a>
<span class="badge badge-secondary" *ngIf="u.status === organizationUserStatusType.Invited">{{'invited' | i18n}}</span>
<span class="badge badge-warning" *ngIf="u.status === organizationUserStatusType.Accepted">{{'accepted' | i18n}}</span>
<small class="text-muted d-block" *ngIf="u.name">{{u.name}}</small>
</td>
<td>
<span *ngIf="u.type === organizationUserType.Owner">{{'owner' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.Admin">{{'admin' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.Manager">{{'manager' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.User">{{'user' | i18n}}</span>
</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown>
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-cog fa-lg"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="#" appStopClick (click)="reinvite(u)" *ngIf="u.status === organizationUserStatusType.Invited">
<i class="fa fa-fw fa-envelope-o"></i>
{{'resendInvitation' | i18n}}
</a>
<a class="dropdown-item text-success" href="#" appStopClick (click)="confirm(u)" *ngIf="u.status === organizationUserStatusType.Accepted">
<i class="fa fa-fw fa-check"></i>
{{'confirm' | i18n}}
</a>
<a class="dropdown-item" href="#" appStopClick (click)="groups(u)" *ngIf="accessGroups">
<i class="fa fa-fw fa-sitemap"></i>
{{'groups' | i18n}}
</a>
<a class="dropdown-item" href="#" appStopClick (click)="events(u)" *ngIf="accessEvents && u.status === organizationUserStatusType.Confirmed">
<i class="fa fa-fw fa-file-text-o"></i>
{{'eventLogs' | i18n}}
</a>
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(u)">
<i class="fa fa-fw fa-remove"></i>
{{'remove' | i18n}}
</a>
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</ng-container>
</ng-container>
<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();
@@ -115,6 +128,16 @@ export class PeopleComponent implements OnInit {
this.statusMap.get(OrganizationUserStatusType.Accepted).length : 0;
}
get confirmedCount() {
return this.statusMap.has(OrganizationUserStatusType.Confirmed) ?
this.statusMap.get(OrganizationUserStatusType.Confirmed).length : 0;
}
get showConfirmUsers(): boolean {
return this.allUsers != null && this.statusMap != null && this.allUsers.length > 1 &&
this.confirmedCount > 0 && this.confirmedCount < 3 && this.acceptedCount > 0;
}
edit(user: OrganizationUserUserDetailsResponse) {
if (this.modal != null) {
this.modal.close();
@@ -196,12 +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;
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;
@@ -232,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;
@@ -239,9 +303,27 @@ export class PeopleComponent implements OnInit {
}
private removeUser(user: OrganizationUserUserDetailsResponse) {
const index = this.users.indexOf(user);
let index = this.users.indexOf(user);
if (index > -1) {
this.users.splice(index, 1);
}
if (this.statusMap.has(OrganizationUserStatusType.Accepted)) {
index = this.statusMap.get(OrganizationUserStatusType.Accepted).indexOf(user);
if (index > -1) {
this.statusMap.get(OrganizationUserStatusType.Accepted).splice(index, 1);
}
}
if (this.statusMap.has(OrganizationUserStatusType.Invited)) {
index = this.statusMap.get(OrganizationUserStatusType.Invited).indexOf(user);
if (index > -1) {
this.statusMap.get(OrganizationUserStatusType.Invited).splice(index, 1);
}
}
if (this.statusMap.has(OrganizationUserStatusType.Confirmed)) {
index = this.statusMap.get(OrganizationUserStatusType.Confirmed).indexOf(user);
if (index > -1) {
this.statusMap.get(OrganizationUserStatusType.Confirmed).splice(index, 1);
}
}
}
}

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

@@ -21,7 +21,7 @@
</div>
</div>
<div class="col-6">
<app-avatar data="{{org.name}}" dynamic="true" width="75" height="75" fontSize="35"></app-avatar>
<app-avatar data="{{org.name}}" dynamic="true" size="75" fontSize="35"></app-avatar>
</div>
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
@@ -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

@@ -21,7 +21,13 @@
<dt>{{'billingPlan' | i18n}}</dt>
<dd>{{billing.plan}}</dd>
<dt>{{'expiration' | i18n}}</dt>
<dd *ngIf="billing.expiration">{{billing.expiration | date:'mediumDate'}}</dd>
<dd *ngIf="billing.expiration">
{{billing.expiration | date:'mediumDate'}}
<span *ngIf="isExpired" class="text-danger ml-2">
<i class="fa fa-exclamation-triangle"></i>
{{'licenseIsExpired' | i18n}}
</span>
</dd>
<dd *ngIf="!billing.expiration">{{'neverExpires' | i18n}}</dd>
</dl>
<div class="row" *ngIf="!selfHosted">

View File

@@ -208,6 +208,11 @@ export class OrganizationBillingComponent implements OnInit {
this.platformUtilsService.launchUri(url);
}
get isExpired() {
return this.billing != null && this.billing.expiration != null &&
new Date(this.billing.expiration) < new Date();
}
get subscriptionMarkedForCancel() {
return this.subscription != null && !this.subscription.cancelled && this.subscription.cancelAtEndDate;
}
@@ -252,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

@@ -7,20 +7,8 @@ import {
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 { FolderService } from 'jslib/abstractions/folder.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { Importer } from 'jslib/importers/importer';
import { CipherRequest } from 'jslib/models/request/cipherRequest';
import { CollectionRequest } from 'jslib/models/request/collectionRequest';
import { ImportOrganizationCiphersRequest } from 'jslib/models/request/importOrganizationCiphersRequest';
import { KvpRequest } from 'jslib/models/request/kvpRequest';
import { ImportResult } from 'jslib/models/domain/importResult';
import { ImportService } from 'jslib/abstractions/import.service';
import { ImportComponent as BaseImportComponent } from '../../tools/import.component';
@@ -29,14 +17,10 @@ import { ImportComponent as BaseImportComponent } from '../../tools/import.compo
templateUrl: '../../tools/import.component.html',
})
export class ImportComponent extends BaseImportComponent {
organizationId: string;
constructor(i18nService: I18nService, analytics: Angulartics2,
toasterService: ToasterService, cipherService: CipherService,
folderService: FolderService, apiService: ApiService,
router: Router, private collectionService: CollectionService,
private route: ActivatedRoute) {
super(i18nService, analytics, toasterService, cipherService, folderService, apiService, router);
toasterService: ToasterService, importService: ImportService,
router: Router, private route: ActivatedRoute) {
super(i18nService, analytics, toasterService, importService, router);
}
ngOnInit() {
@@ -46,33 +30,4 @@ export class ImportComponent extends BaseImportComponent {
super.ngOnInit();
});
}
protected async postImport(importResult: ImportResult) {
const request = new ImportOrganizationCiphersRequest();
for (let i = 0; i < importResult.ciphers.length; i++) {
importResult.ciphers[i].organizationId = this.organizationId;
const c = await this.cipherService.encrypt(importResult.ciphers[i]);
request.ciphers.push(new CipherRequest(c));
}
if (importResult.collections != null) {
for (let i = 0; i < importResult.collections.length; i++) {
importResult.collections[i].organizationId = this.organizationId;
const c = await this.collectionService.encrypt(importResult.collections[i]);
request.collections.push(new CollectionRequest(c));
}
}
if (importResult.collectionRelationships != null) {
importResult.collectionRelationships.forEach((r) =>
request.collectionRelationships.push(new KvpRequest(r[0], r[1])));
}
return await this.apiService.postImportOrganizationCiphers(this.organizationId, request);
}
protected getImporter(): Importer {
const importer = super.getImporter();
if (importer != null) {
importer.organization = true;
}
return importer;
}
}

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 useTotp for now
// since all paid plans include useTotp
this.accessReports = this.organization.useTotp;
});
}
upgradeOrganization() {
this.messagingService.send('upgradeOrganization', { organizationId: this.organization.id });
}
}

View File

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

View File

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

View File

@@ -1,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);
}
}
@@ -76,4 +82,15 @@ export class CiphersComponent extends BaseCiphersComponent {
events(c: CipherView) {
this.onEventsClicked.emit(c);
}
protected deleteCipher(id: string) {
if (!this.organization.isAdmin) {
return super.deleteCipher(id);
}
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() {
@@ -51,7 +49,11 @@ export class CollectionsComponent extends BaseCollectionsComponent {
}
protected saveCollections() {
const request = new CipherCollectionsRequest(this.cipherDomain.collectionIds);
return this.apiService.putCipherCollectionsAdmin(this.cipherId, request);
if (this.organization.isAdmin) {
const request = new CipherCollectionsRequest(this.cipherDomain.collectionIds);
return this.apiService.putCipherCollectionsAdmin(this.cipherId, request);
} else {
return super.saveCollections();
}
}
}

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

@@ -7,7 +7,12 @@
</div>
<div class="col-9">
<div class="page-header d-flex">
<h1>{{'vault' | i18n}}</h1>
<h1>
{{'vault' | i18n}}
<small #actionSpinner [appApiAction]="ciphersComponent.actionPromise">
<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()">
<i class="fa fa-plus fa-fw"></i>{{'addItem' | i18n}}
</button>

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;
@@ -49,9 +55,10 @@ export class VaultComponent implements OnInit {
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) => {
@@ -60,8 +67,24 @@ export class VaultComponent implements OnInit {
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();
@@ -82,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) {
@@ -93,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');
@@ -116,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) {
@@ -137,6 +163,7 @@ export class VaultComponent implements OnInit {
filterSearchText(searchText: string) {
this.ciphersComponent.searchText = searchText;
this.ciphersComponent.search(200);
}
editCipherAttachments(cipher: CipherView) {
@@ -195,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) {
@@ -259,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

@@ -5,11 +5,15 @@ import {
Router,
} from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { UserService } from 'jslib/abstractions/user.service';
@Injectable()
export class OrganizationGuardService implements CanActivate {
constructor(private userService: UserService, private router: Router) { }
constructor(private userService: UserService, private router: Router,
private toasterService: ToasterService, private i18nService: I18nService) { }
async canActivate(route: ActivatedRouteSnapshot) {
const org = await this.userService.getOrganization(route.params.organizationId);
@@ -17,6 +21,11 @@ export class OrganizationGuardService implements CanActivate {
this.router.navigate(['/']);
return false;
}
if (!org.isOwner && !org.enabled) {
this.toasterService.popAsync('error', null, this.i18nService.t('organizationIsDisabled'));
this.router.navigate(['/']);
return false;
}
return true;
}

View File

@@ -36,8 +36,11 @@ import { CryptoService } from 'jslib/services/crypto.service';
import { EnvironmentService } from 'jslib/services/environment.service';
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';
@@ -58,13 +61,16 @@ import { EnvironmentService as EnvironmentServiceAbstraction } from 'jslib/abstr
import { ExportService as ExportServiceAbstraction } from 'jslib/abstractions/export.service';
import { FolderService as FolderServiceAbstraction } from 'jslib/abstractions/folder.service';
import { I18nService as I18nServiceAbstraction } from 'jslib/abstractions/i18n.service';
import { ImportService as ImportServiceAbstraction } from 'jslib/abstractions/import.service';
import { LockService as LockServiceAbstraction } from 'jslib/abstractions/lock.service';
import { LogService as LogServiceAbstraction } from 'jslib/abstractions/log.service';
import { MessagingService as MessagingServiceAbstraction } from 'jslib/abstractions/messaging.service';
import { NotificationsService as NotificationsServiceAbstraction } from 'jslib/abstractions/notifications.service';
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';
@@ -77,8 +83,8 @@ 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 storageService: StorageServiceAbstraction = new HtmlStorageService();
const platformUtilsService = new WebPlatformUtilsService(i18nService, messagingService);
const storageService: StorageServiceAbstraction = new HtmlStorageService(platformUtilsService);
const secureStorageService: StorageServiceAbstraction = new MemoryStorageService();
const cryptoFunctionService: CryptoFunctionServiceAbstraction = new WebCryptoFunctionService(window,
platformUtilsService);
@@ -88,25 +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(),
@@ -115,11 +126,15 @@ containerService.attachToWindow(window);
export function initFactory(): Function {
return async () => {
await (storageService as HtmlStorageService).init();
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,
@@ -131,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);
@@ -145,6 +161,7 @@ export function initFactory(): Function {
htmlEl.classList.add('theme_' + theme);
stateService.save(ConstantsService.disableFaviconKey,
await storageService.get<boolean>(ConstantsService.disableFaviconKey));
stateService.save('enableGravatars', await storageService.get<boolean>('enableGravatars'));
};
}
@@ -183,6 +200,10 @@ 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,
useFactory: initFactory,

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

@@ -1,24 +1,45 @@
<app-callout type="warning">{{'loggedOutWarning' | i18n}}</app-callout>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate autocomplete="off">
<div class="row">
<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>
<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>
<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;
@@ -161,10 +165,8 @@ export class CreateOrganizationComponent implements OnInit {
}
async finalize(orgId: string) {
await Promise.all([
this.apiService.refreshIdentityToken(),
this.syncService.fullSync(true),
]);
await this.apiService.refreshIdentityToken();
await this.syncService.fullSync(true);
this.analytics.eventTrack.next({ action: 'Created Organization' });
this.toasterService.popAsync('success', this.i18nService.t('organizationCreated'),
this.i18nService.t('organizationReadyToGo'));
@@ -172,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';
}
@@ -219,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

@@ -3,6 +3,17 @@
</div>
<p>{{'optionsDesc' | i18n}}</p>
<form (ngSubmit)="submit()" ngNativeValidate>
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="lockOption">{{'lockOptions' | i18n}}</label>
<select id="lockOption" name="LockOption" [(ngModel)]="lockOption" class="form-control">
<option *ngFor="let o of lockOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
<small class="form-text text-muted">{{'lockOptionsDesc' | i18n}}</small>
</div>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="form-group">
@@ -31,6 +42,18 @@
</div>
<small class="form-text text-muted">{{'disableIconsDesc' | i18n}}</small>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="enableGravatars" name="enableGravatars" [(ngModel)]="enableGravatars">
<label class="form-check-label" for="enableGravatars">
{{'enableGravatars' | i18n}}
</label>
<a href="https://gravatar.com/" target="_blank" rel="noopener" title="{{'learnMore' | i18n}}">
<i class="fa fa-question-circle-o"></i>
</a>
</div>
<small class="form-text text-muted">{{'enableGravatarsDesc' | i18n}}</small>
</div>
<button type="submit" class="btn btn-primary">
{{'save' | i18n}}
</button>

View File

@@ -7,6 +7,8 @@ import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { LockService } from 'jslib/abstractions/lock.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StateService } from 'jslib/abstractions/state.service';
import { StorageService } from 'jslib/abstractions/storage.service';
@@ -19,15 +21,32 @@ import { Utils } from 'jslib/misc/utils';
templateUrl: 'options.component.html',
})
export class OptionsComponent implements OnInit {
lockOption: number = null;
disableIcons: boolean;
enableGravatars: boolean;
locale: string;
lockOptions: any[];
localeOptions: any[];
private startingLocale: string;
constructor(private storageService: StorageService, private stateService: StateService,
private analytics: Angulartics2, private i18nService: I18nService,
private toasterService: ToasterService) {
private toasterService: ToasterService, private lockService: LockService,
private platformUtilsService: PlatformUtilsService) {
this.lockOptions = [
{ name: i18nService.t('oneMinute'), value: 1 },
{ name: i18nService.t('fiveMinutes'), value: 5 },
{ name: i18nService.t('fifteenMinutes'), value: 15 },
{ name: i18nService.t('thirtyMinutes'), value: 30 },
{ name: i18nService.t('oneHour'), value: 60 },
{ name: i18nService.t('fourHours'), value: 240 },
{ name: i18nService.t('onRefresh'), value: -1 },
];
if (this.platformUtilsService.isDev()) {
this.lockOptions.push({ name: i18nService.t('never'), value: null });
}
const localeOptions: any[] = [];
i18nService.supportedTranslationLocales.forEach((locale) => {
localeOptions.push({ name: locale, value: locale });
@@ -38,13 +57,18 @@ export class OptionsComponent implements OnInit {
}
async ngOnInit() {
this.lockOption = await this.storageService.get<number>(ConstantsService.lockOptionKey);
this.disableIcons = await this.storageService.get<boolean>(ConstantsService.disableFaviconKey);
this.enableGravatars = await this.storageService.get<boolean>('enableGravatars');
this.locale = this.startingLocale = await this.storageService.get<string>(ConstantsService.localeKey);
}
async submit() {
await this.lockService.setLockOption(this.lockOption != null ? this.lockOption : null);
await this.storageService.save(ConstantsService.disableFaviconKey, this.disableIcons);
await this.stateService.save(ConstantsService.disableFaviconKey, this.disableIcons);
await this.storageService.save('enableGravatars', this.enableGravatars);
await this.stateService.save('enableGravatars', this.enableGravatars);
await this.storageService.save(ConstantsService.localeKey, this.locale);
this.analytics.eventTrack.next({ action: 'Saved Options' });
if (this.locale !== this.startingLocale) {

View File

@@ -7,6 +7,7 @@
<li *ngFor="let o of organizations">
<a [routerLink]="['/organizations', o.id]" class="text-body">
<i class="fa-li fa fa-caret-right"></i> {{o.name}}
<i *ngIf="!o.enabled" class="fa fa-exclamation-triangle text-danger" title="{{'organizationIsDisabled' | i18n}}"></i>
</a>
</li>
</ul>
@@ -43,10 +44,11 @@
<tbody>
<tr *ngFor="let o of organizations">
<td width="30">
<app-avatar [data]="o.name" width="25" height="25" [circle]="true" [fontSize]="14"></app-avatar>
<app-avatar [data]="o.name" size="25" [circle]="true" [fontSize]="14"></app-avatar>
</td>
<td>
<a href="#" [routerLink]="['/organizations', o.id]">{{o.name}}</a>
<i *ngIf="!o.enabled" class="fa fa-exclamation-triangle text-danger" title="{{'organizationIsDisabled' | i18n}}"></i>
</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown>

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']);
@@ -87,10 +90,8 @@ export class PremiumComponent implements OnInit {
}
async finalizePremium() {
await Promise.all([
this.apiService.refreshIdentityToken(),
this.syncService.fullSync(true),
]);
await this.apiService.refreshIdentityToken();
await this.syncService.fullSync(true);
this.analytics.eventTrack.next({ action: 'Signed Up Premium' });
this.toasterService.popAsync('success', null, this.i18nService.t('premiumUpdated'));
this.messagingService.send('purchasedPremium');

View File

@@ -18,7 +18,17 @@
</div>
</div>
<div class="col-6">
<app-avatar data="{{profile.name || profile.email}}" dynamic="true" width="75" height="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

@@ -12,7 +12,7 @@
</div>
<app-two-factor-verify [organizationId]="organizationId" [type]="type" (onAuthed)="auth($event)" *ngIf="!authed">
</app-two-factor-verify>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="authed">
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="authed" autocomplete="off">
<div class="modal-body">
<ng-container *ngIf="enabled">
<app-callout type="success" title="{{'enabled' | i18n}}" icon="fa-check-circle">
@@ -34,7 +34,7 @@
</div>
<div class="form-group">
<label for="skey">{{'twoFactorDuoSecretKey' | i18n}}</label>
<input id="skey" type="password" name="SecretKey" class="form-control" [(ngModel)]="skey" required appInputVerbatim>
<input id="skey" type="password" name="SecretKey" class="form-control" [(ngModel)]="skey" required appInputVerbatim autocomplete="new-password">
</div>
<div class="form-group">
<label for="host">{{'twoFactorDuoApiHostname' | i18n}}</label>

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

@@ -12,7 +12,7 @@
</div>
<app-two-factor-verify [organizationId]="organizationId" [type]="type" (onAuthed)="auth($event)" *ngIf="!authed">
</app-two-factor-verify>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="authed">
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="authed" autocomplete="off">
<div class="modal-body">
<app-callout type="success" title="{{'enabled' | i18n}}" icon="fa-check-circle" *ngIf="enabled">
{{'twoStepLoginProviderEnabled' | i18n}}
@@ -36,8 +36,8 @@
<div class="row">
<div class="form-group col-6" *ngFor="let k of keys; let i = index">
<label for="key{{i + 1}}">{{'yubikeyX' | i18n : i + 1}}</label>
<input id="key{{i + 1}}" type="text" name="Key{{i + 1}}" class="form-control" [(ngModel)]="k.key" *ngIf="!k.existingKey"
appInputVerbatim>
<input id="key{{i + 1}}" type="password" name="Key{{i + 1}}" class="form-control" [(ngModel)]="k.key" *ngIf="!k.existingKey"
appInputVerbatim autocomplete="new-password">
<div class="d-flex" *ngIf="k.existingKey">
<span class="mr-2">{{k.existingKey}}</span>
<button type="button" class="btn btn-link text-danger ml-auto" (click)="remove(k)" title="{{'remove' | i18n}}">

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 useTotp for now
// since all paid plans include useTotp
if (this.requiresPaid && !this.organization.useTotp) {
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>

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