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

Compare commits

...

220 Commits

Author SHA1 Message Date
Kyle Spearrin
192eb7e7c9 bump version 2018-05-31 14:33:00 -04:00
Kyle Spearrin
48a36649fa Merge branch 'master' of github.com:bitwarden/web 2018-05-30 22:36:12 -04:00
Kyle Spearrin
6b83b53401 delete old sln file 2018-05-30 22:36:10 -04:00
Mart124
c953c2b93f Rework service user (#208)
Related to https://github.com/bitwarden/core/pull/292
2018-05-29 16:46:04 -04:00
Kyle Spearrin
7cdce165fb pass token and org user id to registration 2018-05-24 16:51:47 -04:00
Kyle Spearrin
b393064f26 reset folder id for ciphers. resolves #204 2018-05-24 09:21:43 -04:00
Kyle Spearrin
31bf22063e Update README.md 2018-05-12 13:56:19 -04:00
Kyle Spearrin
cd9a43f359 update importer for firefox 2018-05-11 08:04:21 -04:00
Kyle Spearrin
c326e03eb2 null check on subtle 2018-05-05 21:28:34 -04:00
Kyle Spearrin
f0a986aa04 remove old api 2018-04-24 11:47:16 -04:00
Kyle Spearrin
4225da355d publish 2018-04-19 21:21:55 -04:00
Kyle Spearrin
14bac6a744 fix userid comparisons 2018-04-16 16:26:54 -04:00
Kyle Spearrin
05cc9b45e6 bump version 2018-04-16 16:10:53 -04:00
Kyle Spearrin
dba596bf35 fix if when no currentid 2018-04-16 16:08:59 -04:00
Kyle Spearrin
db39d58ea8 remove empty uri on add 2018-04-16 15:17:50 -04:00
Kyle Spearrin
c0f38216ef manage group from entrypoint 2018-04-16 15:17:13 -04:00
Kyle Spearrin
3643222b3c added org duo to 2fa flow 2018-04-03 14:33:00 -04:00
Kyle Spearrin
551217ea38 filter for unassigned collection grouping 2018-04-03 08:35:45 -04:00
Kyle Spearrin
24bf1363ab org 2fa management for duo 2018-04-02 23:19:04 -04:00
Kyle Spearrin
08b2184e12 version bump 2018-04-02 21:22:30 -04:00
Kyle Spearrin
b73161882c make user homedir with helper 2018-04-02 21:12:45 -04:00
Kyle Spearrin
e2186ecd62 Revert "make user home dir"
This reverts commit b407402f3f.
2018-04-02 21:12:02 -04:00
Kyle Spearrin
b407402f3f make user home dir 2018-04-02 19:59:11 -04:00
Kyle Spearrin
8bb4132458 version bump 2018-03-30 10:56:24 -04:00
Kyle Spearrin
443822fd52 step down from host root LUID 2018-03-27 22:56:50 -04:00
Kyle Spearrin
68427fd2de bash 2018-03-27 21:15:16 -04:00
Kyle Spearrin
c3d3369601 proper and syntax for entrypoint conditions 2018-03-27 17:11:48 -04:00
Kyle Spearrin
3c5022d628 upsert bitwarden user 2018-03-27 16:37:50 -04:00
Kyle Spearrin
832ddddc58 gosu 2018-03-27 15:44:25 -04:00
Kyle Spearrin
0fc1415a06 chown deep directories 2018-03-26 14:30:37 -04:00
Kyle Spearrin
1ab408c591 non-root docker 2018-03-26 11:24:09 -04:00
Kyle Spearrin
3160d3f275 disable uglify for now 2018-03-24 20:55:00 -04:00
Kyle Spearrin
d083f1ddc3 version bump and lint fix 2018-03-24 20:49:20 -04:00
Kyle Spearrin
5fbc09b135 cannot create item in collection.
set collection after share.
2018-03-24 20:44:51 -04:00
Kyle Spearrin
6282fabf98 use bitwarden user for docker 2018-03-23 21:21:01 -04:00
Kyle Spearrin
2b528bad97 version json file on dist 2018-03-23 13:04:59 -04:00
Kyle Spearrin
c3be8195fd no edit/del of "no folder" 2018-03-20 15:58:00 -04:00
Kyle Spearrin
39471d0421 loading ciphers false after first chunk 2018-03-19 11:33:52 -04:00
Kyle Spearrin
7a50c0536c loading switches for cipher and groupings 2018-03-19 11:28:23 -04:00
Kyle Spearrin
4ccd9501a8 add back missing select function 2018-03-19 11:14:28 -04:00
Kyle Spearrin
75c05a4a85 version bump in settings 2018-03-17 12:03:07 -04:00
Kyle Spearrin
ca7e12370f version bump 2018-03-17 12:02:00 -04:00
Kyle Spearrin
8bc9dafff2 vault fixes 2018-03-17 12:01:03 -04:00
Kyle Spearrin
dcb0416fd6 re-factor vault listings 2018-03-17 11:42:35 -04:00
Kyle Spearrin
bbb69bba26 Update ISSUE_TEMPLATE.md 2018-03-10 16:36:53 -05:00
Kyle Spearrin
c1838b48ff Create ISSUE_TEMPLATE.md 2018-03-10 09:48:22 -05:00
Kyle Spearrin
d53f40002c totp-col breaks at sm, not md 2018-03-09 23:07:43 -05:00
Kyle Spearrin
866954b180 fix lint issues 2018-03-09 16:42:10 -05:00
Kyle Spearrin
befa9cbf08 version bump 2018-03-09 16:39:17 -05:00
Kyle Spearrin
859f44db43 only perpend http if there is no protocol 2018-03-05 22:15:22 -05:00
Kyle Spearrin
cca9c3c561 get rid of apps page and link to bitwarden.com 2018-03-02 22:42:32 -05:00
Kyle Spearrin
27e68e4c75 multi uri support for import/export 2018-03-02 22:13:53 -05:00
Kyle Spearrin
5c92350ed2 refactor for cipher response. add login uris. 2018-03-02 21:12:26 -05:00
Kyle Spearrin
b94c62d1e5 upadte security md 2018-02-27 23:00:10 -05:00
Kyle Spearrin
de888d8a37 remove pwnedtest 2018-02-27 22:42:39 -05:00
Kyle Spearrin
f8d6816101 Uppercase Bitwarden 2018-02-27 22:41:27 -05:00
Kyle Spearrin
119c6d5817 big-textarea not important 2018-02-27 08:21:26 -05:00
Kyle Spearrin
aaa21daa29 only intercept with headers when api is at start 2018-02-26 23:18:03 -05:00
Kyle Spearrin
10f41bf288 pwned test 2018-02-26 22:52:56 -05:00
Kyle Spearrin
91582691d8 whiteListedDomains for jwt 2018-02-26 13:48:26 -05:00
Kyle Spearrin
463efc2254 use new admin apis for attachments 2018-02-24 14:36:13 -05:00
Kyle Spearrin
0333354271 version bump 2018-02-20 23:34:10 -05:00
Kyle Spearrin
b85f56c681 restore collection ids on edit. resolves #174 2018-02-09 10:39:18 -05:00
Kyle Spearrin
be491be2cd Update organizationBilling.html 2018-02-04 16:00:00 -05:00
Kyle Spearrin
4be4a8115d Update settingsBilling.html 2018-02-04 15:58:41 -05:00
Kyle Spearrin
c0eb499f4d value.type should not be case sensitive 2018-01-26 11:55:57 -05:00
Kyle Spearrin
1b43f3facd check for empty name on SIC importer 2018-01-25 21:22:17 -05:00
Chuck
26d41d3cb9 Change npm to use https for gulp-gh-pages restore. (#168)
When using VS 2017 node.js integration, npm fails because a host key cannot be validated. Switching to https, provides security and no additional configuration to restore the package.
2018-01-23 11:43:51 -05:00
Kyle Spearrin
179765f6e4 use random bytes for each HMAC comparison 2018-01-18 12:07:32 -05:00
Kyle Spearrin
df2e332134 macBuf must exist if key has macKey 2018-01-18 09:03:51 -05:00
Kyle Spearrin
2952f9d158 manifest.json included with dist 2018-01-02 23:54:10 -05:00
Kyle Spearrin
3c9face597 disable autocomplete on duo and yubi setup 2018-01-02 23:38:54 -05:00
Kyle Spearrin
25f2e9c1b7 autocomplete="new-password" to disable autofilling 2018-01-02 22:49:05 -05:00
Kyle Spearrin
a6f8e1b9a3 duo connector moved to its own js file 2018-01-02 13:20:58 -05:00
Kyle Spearrin
d832031cec update cdn libs 2017-12-29 09:45:44 -05:00
Kyle Spearrin
7a1a3ab64d revert uglify removal 2017-12-29 09:28:49 -05:00
Kyle Spearrin
19491a684e additional user/pw field names for roboform 2017-12-29 08:43:07 -05:00
Kyle Spearrin
757224287e disable uglify since it seems to be conflicting 2017-12-29 08:40:37 -05:00
Kyle Spearrin
c9b5426f6f version bump 2017-12-28 12:56:00 -05:00
Kyle Spearrin
bf885c184f lint fixes 2017-12-19 12:15:24 -05:00
Kyle Spearrin
0d2bf4f7a1 update libs 2017-12-19 12:13:33 -05:00
Kyle Spearrin
01ffc68fc2 focus vault search on $viewContentLoaded 2017-12-19 11:30:52 -05:00
Kyle Spearrin
16892239fb cross navigation for event subject ids 2017-12-19 11:14:15 -05:00
Kyle Spearrin
d5765d8814 display app/device info on events 2017-12-18 13:56:38 -05:00
Kyle Spearrin
8d6a96074d send device type header 2017-12-18 13:37:06 -05:00
Kyle Spearrin
f54884eb79 event logs for users. ip address. useEvents checks 2017-12-18 13:17:49 -05:00
Kyle Spearrin
828149b2d6 eventService and cipher event logs page 2017-12-18 11:52:42 -05:00
Kyle Spearrin
501c4fc263 serve CSP from proxy 2017-12-16 23:44:35 -05:00
Kyle Spearrin
1d0b45e17d whiteListedDomains only on dev builds 2017-12-16 23:23:17 -05:00
Kyle Spearrin
a0f7ed68fb content-type doesn't need to be text anymore 2017-12-16 23:14:43 -05:00
Kyle Spearrin
7bd0c17188 switch to fork for gh-pages fix 2017-12-16 23:10:40 -05:00
Kyle Spearrin
1ea9d28523 local api/identity uri paths 2017-12-16 22:08:23 -05:00
Kyle Spearrin
8a3fb92bbe paging 2017-12-15 15:02:27 -05:00
Kyle Spearrin
de3a9b9903 date range filtering 2017-12-15 12:42:21 -05:00
Kyle Spearrin
9834f3d2aa use events check 2017-12-14 18:04:18 -05:00
Kyle Spearrin
ac079b9d88 audit logs icon 2017-12-14 15:24:18 -05:00
Kyle Spearrin
9e96906f32 compute counts on every load scenario 2017-12-14 15:20:18 -05:00
Kyle Spearrin
90c079e743 org events page setup 2017-12-14 15:03:46 -05:00
Kyle Spearrin
4ecf307285 properly flag new folder as type folder
resolves #149
2017-12-09 08:28:52 -05:00
Kyle Spearrin
6cf4c453d9 Update README.md 2017-12-05 11:12:34 -05:00
Philipp Hug
d2899d14c7 vaultAddCipherController.js: secureNote Type is int not string (#144) 2017-12-04 07:59:28 -05:00
Kyle Spearrin
f3b438d514 null ref on keeper import 2017-12-03 21:27:49 -05:00
Kyle Spearrin
2997f694f8 import notes for form fills 2017-11-30 23:45:06 -05:00
Kyle Spearrin
b78ab4db27 import form fill csv for lastpass 2017-11-30 23:40:05 -05:00
Kyle Spearrin
37dddea515 simplify collapse/expand logic 2017-11-30 22:47:16 -05:00
Kyle Spearrin
e307d1e87d init storage 2017-11-29 22:47:21 -05:00
Kyle Spearrin
62e1dbb642 expand/collapse all boxes 2017-11-29 22:43:58 -05:00
Kyle Spearrin
b8a425f530 version bump 2017-11-29 22:12:46 -05:00
Kyle Spearrin
cafb6fa694 not always CSV data 2017-11-28 10:07:21 -05:00
Kyle Spearrin
0482ddea2c store large items in notes for import 2017-11-28 10:02:41 -05:00
Kyle Spearrin
b411176c8d better error message handling 2017-11-28 09:27:44 -05:00
Kyle Spearrin
2f13449cb6 fix null ref 2017-11-22 12:29:30 -05:00
Kyle Spearrin
b0c1b7b683 default password generated is 14 length 2017-11-22 12:28:06 -05:00
Kyle Spearrin
7e8978c7fc single collection icon is a cube 2017-11-22 12:24:21 -05:00
Kyle Spearrin
d58b422bd0 no items in folder/collection 2017-11-22 12:21:55 -05:00
Kyle Spearrin
3563601382 no collections message 2017-11-22 12:17:40 -05:00
Kyle Spearrin
d42e6ca3fd show collection and folder groupings together 2017-11-22 12:08:31 -05:00
Kyle Spearrin
7f0d8c99e3 version bump 2017-11-13 12:31:23 -05:00
Kyle Spearrin
48a67dc2b3 remove amazon app 2017-11-13 12:28:11 -05:00
Kyle Spearrin
8d0b42492d families plan desc 2017-11-08 22:05:53 -05:00
Kyle Spearrin
e4076e95dd lint fix 2017-11-08 22:03:50 -05:00
Kyle Spearrin
30a2b878f6 version bump 2017-11-08 22:02:48 -05:00
Kyle Spearrin
e17f94a67d adjustments for families plan 2017-11-08 13:27:19 -05:00
Kyle Spearrin
4dd60c3844 Merge branch 'master' of github.com:bitwarden/web 2017-11-07 21:06:30 -05:00
Kyle Spearrin
9d76990f24 Org disabled message for self host 2017-11-07 21:06:00 -05:00
Fabio Bonelli
ed3d15f075 Focus by default the vault search input. (#119) 2017-10-30 12:23:54 -04:00
Kyle Spearrin
2c36a2aa96 version bump settings 2017-10-26 22:17:53 -04:00
Kyle Spearrin
16930aa422 version bump 2017-10-26 22:12:42 -04:00
Kyle Spearrin
263f5ba147 monospaced fonts on certain input fields 2017-10-26 11:37:38 -04:00
Kyle Spearrin
6a60c00e22 added note about english for enpass 2017-10-26 11:24:53 -04:00
Kyle Spearrin
f3eaf644b0 purge vault 2017-10-25 21:46:35 -04:00
Kyle Spearrin
a57110b935 lint fixes 2017-10-25 16:01:04 -04:00
Kyle Spearrin
cae8beaa8f default cipher type data objects 2017-10-25 15:45:33 -04:00
Kyle Spearrin
df94d81d07 handle null condition 2017-10-25 12:38:55 -04:00
Kyle Spearrin
f03c22cc07 tax information 2017-10-25 12:21:46 -04:00
Kyle Spearrin
5b31fe37f2 border same as bg 2017-10-25 00:49:49 -04:00
Kyle Spearrin
c60a596995 invoice link for charges 2017-10-25 00:47:07 -04:00
Kyle Spearrin
b52ecd8085 icons url for self hosted instances 2017-10-23 18:11:29 -04:00
Kyle Spearrin
4323341d19 attachments indicator in org vault 2017-10-23 16:23:32 -04:00
Kyle Spearrin
e13992ba27 web vault options 2017-10-23 16:07:41 -04:00
Kyle Spearrin
52a4317d09 add option to disable website icons in web vault 2017-10-23 16:06:55 -04:00
Kyle Spearrin
d53187935b only use icon images if not self hosted 2017-10-23 15:35:46 -04:00
Kyle Spearrin
0d6c96e38b update importers for cipher types & fields 2017-10-23 14:50:19 -04:00
Kyle Spearrin
b0832578a4 handle logins & notes for generic export/import 2017-10-23 12:40:42 -04:00
Kyle Spearrin
805393b4db null check refresh promise 2017-10-19 21:20:32 -04:00
Kyle Spearrin
c3653577c6 fix bug with only showing selected collections 2017-10-19 21:18:45 -04:00
Kyle Spearrin
1eb5a99ba3 make sure uri has . in it before prefixing http 2017-10-18 15:54:42 -04:00
Kyle Spearrin
a035d73545 max-height 2017-10-17 11:27:58 -04:00
Kyle Spearrin
79fc3056a6 re-order car brands 2017-10-12 23:37:05 -04:00
Kyle Spearrin
e44cf6e7ee return error when rejecting 2017-10-12 23:35:58 -04:00
Kyle Spearrin
641c76ae62 overflow y on control-sidebar sections 2017-10-12 22:42:12 -04:00
Kyle Spearrin
1efcd69148 dont hide overflow 2017-10-12 17:18:13 -04:00
Kyle Spearrin
49ee41f7d3 process notes for cards and identity from lastpass 2017-10-12 17:01:34 -04:00
Kyle Spearrin
598c7ea068 update listing when cipher is edited 2017-10-12 15:48:30 -04:00
Kyle Spearrin
001a116c8b generic notes fix 2017-10-12 14:27:45 -04:00
Kyle Spearrin
106e71fe54 import updates
- converted logins to ciphers up to 1password csv
- started secure notes support for lastpasss
2017-10-12 14:24:08 -04:00
Kyle Spearrin
cd93d6cc32 icons for filters 2017-10-12 10:59:01 -04:00
Kyle Spearrin
d63c89bae7 new icon path 2017-10-12 10:23:03 -04:00
Yash Shah
fb3a7733a3 Add semicolon and remove unneeded comma (#108) 2017-10-12 08:29:32 -04:00
Kyle Spearrin
852363cb77 import/export/updatekey fixes for ciphers 2017-10-11 16:41:09 -04:00
Kyle Spearrin
7f6ee21a8e renaming org vault logins to ciphers 2017-10-11 15:54:47 -04:00
Kyle Spearrin
2963516d5c more logins to cipher renames 2017-10-11 09:57:18 -04:00
Kyle Spearrin
1f26ff5c80 round the icons 2017-10-11 09:46:04 -04:00
Kyle Spearrin
de3f310082 renaming logins to ciphers in move 2017-10-11 09:45:52 -04:00
Kyle Spearrin
4af2edafd3 set login icon function 2017-10-11 09:35:59 -04:00
Kyle Spearrin
4de08f2e71 bitwarden vault 2017-10-11 09:26:18 -04:00
Kyle Spearrin
d978e1dfa3 favicon updates 2017-10-10 22:56:04 -04:00
Kyle Spearrin
f828288b84 icons for vault listing 2017-10-10 21:55:58 -04:00
Kyle Spearrin
7a36f13034 convert share from logins to ciphers 2017-10-09 15:54:21 -04:00
Kyle Spearrin
422b48fa36 added additional fields to identity 2017-10-09 11:00:41 -04:00
Kyle Spearrin
fe9e29a057 cipher type icons 2017-10-09 10:42:26 -04:00
Kyle Spearrin
88c302ca2e cipher type forms 2017-10-09 10:06:44 -04:00
Kyle Spearrin
52f3032483 fixes for item filtering in vault 2017-10-09 08:20:58 -04:00
Kyle Spearrin
b13edfeeae adjust height of notes field 2017-10-07 21:57:00 -04:00
Kyle Spearrin
4046339569 filter cipher list by type 2017-10-07 21:48:02 -04:00
Kyle Spearrin
52f4a9d961 show all types in listing 2017-10-07 21:28:15 -04:00
Kyle Spearrin
ca0fb6d66a convert add login to ciphers 2017-10-07 14:20:28 -04:00
Kyle Spearrin
7c93c82d24 shared vault listing conversion to ciphers 2017-10-07 13:45:33 -04:00
Kyle Spearrin
3b71760f9e convert vault listing to ciphers 2017-10-06 22:01:17 -04:00
Kyle Spearrin
c4d2045884 convert edit to generic ciphers 2017-10-06 21:24:04 -04:00
Kyle Spearrin
d28c59544f encrypt/decrypt ciphers 2017-10-06 21:23:14 -04:00
Kyle Spearrin
acff0b19d6 adjusted build script 2017-10-04 16:17:00 -04:00
Kyle Spearrin
94bfcb2865 version bump 2017-10-03 22:29:42 -04:00
Kyle Spearrin
1bb6244337 on alter token header if not self hosted 2017-10-03 22:29:01 -04:00
Kyle Spearrin
a132ec4fd7 export/import custom fields for organizations 2017-10-03 09:46:53 -04:00
Kyle Spearrin
8291fa0ce1 hotfix for safari 2017-10-03 09:29:30 -04:00
Kyle Spearrin
37364ecd7e back to access_token for safari for now 2017-10-03 09:18:19 -04:00
Kyle Spearrin
48d9e626f5 server build is not beta tagged 2017-10-02 21:27:44 -04:00
Kyle Spearrin
f0fbf664d4 versioning and tagging 2017-10-02 16:39:37 -04:00
Kyle Spearrin
7b8b4dc164 adjust text color for light sidebar 2017-10-02 15:35:51 -04:00
Kyle Spearrin
21635dd728 import/export custom fields 2017-10-02 12:37:17 -04:00
Kyle Spearrin
c7802940b1 version bump 2017-09-29 11:44:32 -04:00
Kyle Spearrin
f7b60febe9 Only load u2f-api.js implementation when necessary
Some browsers such as Firefox already provide a window.u2f
implementation. Detect the existing implementation and abort from
u2f-api.js.
2017-09-29 11:22:23 -04:00
Kyle Spearrin
6c93a63c06 import ciphers, no logins 2017-09-28 13:12:39 -04:00
Kyle Spearrin
c44a638644 version bump and lint fixes 2017-09-28 11:16:01 -04:00
Kyle Spearrin
0d3fead0f3 added session activity message 2017-09-27 17:21:27 -04:00
Kyle Spearrin
5ba4b37610 disable autocomplete on various forms 2017-09-27 13:04:03 -04:00
Kyle Spearrin
44a2d071ae update apps 2017-09-21 23:38:48 -04:00
Kyle Spearrin
3b22764368 adjust authenticator qr code 2017-09-21 23:35:42 -04:00
Kyle Spearrin
11336da6df adjust modal sizes 2017-09-21 23:31:16 -04:00
Kyle Spearrin
a0e5591f8e larger modals. sm breakpoints on login add/edit 2017-09-21 23:19:06 -04:00
Kyle Spearrin
e952073c3c new remove button 2017-09-21 23:00:49 -04:00
Kyle Spearrin
9bdd0d116a disable fields when cannot edit 2017-09-21 22:56:31 -04:00
Kyle Spearrin
05c8a39e6d custom fields on all add/edit login pages 2017-09-21 14:27:07 -04:00
Kyle Spearrin
8fa6ff48cf touch-ups on custom field layout 2017-09-21 13:53:54 -04:00
Kyle Spearrin
7a31783ea4 custom fields added to edit login page 2017-09-21 13:21:09 -04:00
Kyle Spearrin
96585b183d subclassing for encrypted login 2017-09-21 10:44:00 -04:00
Kyle Spearrin
f81e7b02dc only delete dist folder contents when cleaned 2017-09-20 23:42:26 -04:00
Kyle Spearrin
f7fbdf2081 move logins to ciphers apis 2017-09-20 16:45:13 -04:00
Kyle Spearrin
06a877c755 style org icon for self host 2017-09-19 22:20:42 -04:00
Kyle Spearrin
30abd52189 lighten sidebar header color 2017-09-19 18:09:39 -04:00
Kyle Spearrin
6af0e62976 light skin for self hosted instances 2017-09-19 17:34:20 -04:00
Kyle Spearrin
84a36a18d6 must verify your email before upgrading to premium 2017-09-18 16:11:30 -04:00
Kyle Spearrin
595cf6c375 use Content-Language header for auth bearer 2017-09-14 10:12:13 -04:00
Kyle Spearrin
4262e2cc1d remove old qs params 2017-09-14 09:34:29 -04:00
Kyle Spearrin
c134986bbf version bump 2017-09-12 22:32:37 -04:00
Kyle Spearrin
d9981e1d71 cleaned providers should be an obj, not array 2017-09-09 12:25:35 -04:00
Kyle Spearrin
2b6d7ec361 org import from lastpass 2017-09-06 10:50:05 -04:00
Kyle Spearrin
aaa91e50b7 org export/import 2017-09-06 09:05:53 -04:00
Kyle Spearrin
ff9030e7af disable autocomplete on verification code input 2017-09-04 23:10:31 -04:00
155 changed files with 16737 additions and 3607 deletions

1
.gitignore vendored
View File

@@ -199,5 +199,4 @@ FakesAssemblies/
*.opt
# Other
project.lock.json
src/js/*.min.js

1
CNAME
View File

@@ -1 +0,0 @@
vault.bitwarden.com

View File

@@ -1,10 +1,15 @@
FROM bitwarden/server
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gosu \
&& rm -rf /var/lib/apt/lists/*
ENV ASPNETCORE_URLS http://+:5000
WORKDIR /app
EXPOSE 5000
COPY ./dist .
EXPOSE 80
COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

5
ISSUE_TEMPLATE.md Normal file
View File

@@ -0,0 +1,5 @@
<!--
Please do not submit feature requests. The [Community Forums][1] has a
section for submitting, voting for, and discussing product feature requests.
[1]: https://community.bitwarden.com
-->

View File

@@ -1,10 +1,10 @@
[![appveyor build](https://ci.appveyor.com/api/projects/status/github/bitwarden/web?branch=master&svg=true)](https://ci.appveyor.com/project/bitwarden/web) [![Join the chat at https://gitter.im/bitwarden/Lobby](https://badges.gitter.im/bitwarden/Lobby.svg)](https://gitter.im/bitwarden/Lobby)
[![appveyor build](https://ci.appveyor.com/api/projects/status/github/bitwarden/web?branch=master&svg=true)](https://ci.appveyor.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)
# bitwarden Web
# Bitwarden Web Vault
The bitwarden Web project is an AngularJS application that powers the web vault (https://vault.bitwarden.com/).
The Bitwarden web project is an AngularJS application that powers the web vault (https://vault.bitwarden.com/).
<img src="https://i.imgur.com/rxrykeX.png" alt="" width="791" height="739" />
<img src="https://i.imgur.com/WB8J2bt.png" alt="" />
# Build/Run

View File

@@ -1,4 +1,4 @@
bitwarden believes that working with security researchers across the globe is crucial to keeping our
Bitwarden believes that working with security researchers across the globe is crucial to keeping our
users safe. If you believe you've found a security issue in our product or service, we encourage you to
notify us. We welcome working with you to resolve the issue promptly. Thanks in advance!
@@ -16,7 +16,7 @@ notify us. We welcome working with you to resolve the issue promptly. Thanks in
# In-scope
- Security issues in any current release of bitwarden. This includes the web vault, browser extension,
- Security issues in any current release of Bitwarden. This includes the web vault, browser extension,
and mobile apps (iOS and Android). Product downloads are available at https://bitwarden.com. Source
code is available at https://github.com/bitwarden.
@@ -24,14 +24,14 @@ notify us. We welcome working with you to resolve the issue promptly. Thanks in
The following bug classes are out-of scope:
- Bugs that are already reported on any of bitwarden's issue trackers (https://github.com/bitwarden),
- Bugs that are already reported on any of Bitwarden's issue trackers (https://github.com/bitwarden),
or that we already know of. Note that some of our issue tracking is private.
- Issues in an upstream software dependency (ex: Xamarin, ASP.NET) which are already reported to the
upstream maintainer.
- Attacks requiring physical access to a user's device.
- Self-XSS
- Issues related to software or protocols not under bitwarden's control
- Vulnerabilities in outdated versions of bitwarden
- Issues related to software or protocols not under Bitwarden's control
- Vulnerabilities in outdated versions of Bitwarden
- Missing security best practices that do not directly lead to a vulnerability
- Issues that do not have any impact on the general public
@@ -39,7 +39,7 @@ While researching, we'd like to ask you to refrain from:
- Denial of service
- Spamming
- Social engineering (including phishing) of bitwarden staff or contractors
- Any physical attempts against bitwarden property or data centers
- Social engineering (including phishing) of Bitwarden staff or contractors
- Any physical attempts against Bitwarden property or data centers
Thank you for helping keep bitwarden and our users safe!
Thank you for helping keep Bitwarden and our users safe!

View File

@@ -1,39 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26228.9
MinimumVisualStudioVersion = 10.0.40219.1
Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "bitwarden-web", ".", "{25BEDEF4-2CAF-445A-807D-63C17FF85694}"
ProjectSection(WebsiteProperties) = preProject
TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.6.1"
Debug.AspNetCompiler.VirtualPath = "/localhost_15509"
Debug.AspNetCompiler.PhysicalPath = "."
Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_15509\"
Debug.AspNetCompiler.Updateable = "true"
Debug.AspNetCompiler.ForceOverwrite = "true"
Debug.AspNetCompiler.FixedNames = "false"
Debug.AspNetCompiler.Debug = "True"
Release.AspNetCompiler.VirtualPath = "/localhost_15509"
Release.AspNetCompiler.PhysicalPath = "."
Release.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_15509\"
Release.AspNetCompiler.Updateable = "true"
Release.AspNetCompiler.ForceOverwrite = "true"
Release.AspNetCompiler.FixedNames = "false"
Release.AspNetCompiler.Debug = "False"
VWDPort = "15509"
SlnRelativePath = "."
DefaultWebSiteLanguage = "Visual C#"
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{25BEDEF4-2CAF-445A-807D-63C17FF85694}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{25BEDEF4-2CAF-445A-807D-63C17FF85694}.Debug|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@@ -4,16 +4,30 @@ set -e
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
echo ""
echo "# Building Web"
echo ""
echo "Building app"
echo "npm version $(npm --version)"
echo "gulp version $(gulp --version)"
npm install
gulp dist:selfHosted
if [ $# -gt 1 -a "$1" == "push" ]
then
TAG=$2
echo "# Pushing Web ($TAG)"
echo ""
docker push bitwarden/web:$TAG
elif [ $# -gt 1 -a "$1" == "tag" ]
then
TAG=$2
echo "Tagging Web as '$TAG'"
docker tag bitwarden/web bitwarden/web:$TAG
else
echo "# Building Web"
echo ""
echo "Building docker image"
docker --version
docker build -t bitwarden/web $DIR/.
echo ""
echo "Building app"
echo "npm version $(npm --version)"
echo "gulp version $(gulp --version)"
npm install
gulp dist:selfHosted
echo ""
echo "Building docker image"
docker --version
docker build -t bitwarden/web $DIR/.
fi

1
dist/.publish vendored Submodule

Submodule dist/.publish added at be3a93a581

View File

@@ -1,5 +1,39 @@
#!/bin/sh
#!/bin/bash
# Setup
GROUPNAME="bitwarden"
USERNAME="bitwarden"
LUID=${LOCAL_UID:-0}
LGID=${LOCAL_GID:-0}
# Step down from host root to well-known nobody/nogroup user
if [ $LUID -eq 0 ]
then
LUID=65534
fi
if [ $LGID -eq 0 ]
then
LGID=65534
fi
# Create user and group
groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||
groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1
useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||
usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1
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
dotnet /bitwarden_server/Server.dll /contentRoot=/app /webRoot=. /serveUnknown=false
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

View File

@@ -12,6 +12,7 @@ var gulp = require('gulp'),
ngAnnotate = require('gulp-ng-annotate'),
preprocess = require('gulp-preprocess'),
runSequence = require('run-sequence'),
jeditor = require("gulp-json-editor"),
merge = require('merge-stream'),
ngConfig = require('gulp-ng-config'),
settings = require('./settings.json'),
@@ -25,7 +26,7 @@ var gulp = require('gulp'),
var paths = {};
paths.dist = './dist/';
paths.webroot = './src/'
paths.webroot = './src/';
paths.js = paths.webroot + 'js/**/*.js';
paths.minJs = paths.webroot + 'js/**/*.min.js';
paths.concatJsDest = paths.webroot + 'js/bw.min.js';
@@ -71,12 +72,13 @@ gulp.task('min:js', ['clean:js'], function () {
'!' + paths.minJs,
'!' + paths.jsDir + 'fallback*.js',
'!' + paths.jsDir + 'u2f-connector.js',
'!' + paths.jsDir + 'duo-connector.js',
'!' + paths.jsDir + 'duo.js',
'!' + paths.jsDir + 'settings.js'
], { base: '.' })
.pipe(preprocess({ context: { cacheTag: randomString, selfHosted: selfHosted } }))
.pipe(concat(paths.concatJsDest))
.pipe(uglify())
//.pipe(uglify())
.pipe(gulp.dest('.'));
});
@@ -291,7 +293,7 @@ gulp.task('browserify:cc', function () {
});
gulp.task('dist:clean', function (cb) {
return rimraf(paths.dist, cb);
return rimraf(paths.dist + '**/*', cb);
});
gulp.task('dist:move', function () {
@@ -304,7 +306,7 @@ gulp.task('dist:move', function () {
src: [
paths.npmDir + 'bootstrap/dist/**/bootstrap.min.js',
paths.npmDir + 'bootstrap/dist/**/bootstrap.min.css',
paths.npmDir + 'bootstrap/dist/**/fonts/**/*',
paths.npmDir + 'bootstrap/dist/**/fonts/**/*'
],
dest: paths.dist + 'lib/bootstrap'
},
@@ -335,6 +337,10 @@ gulp.task('dist:move', function () {
src: paths.jsDir + 'duo.js',
dest: paths.dist + 'js'
},
{
src: paths.jsDir + 'duo-connector.js',
dest: paths.dist + 'js'
},
{
src: paths.jsDir + 'settings.js',
dest: paths.dist + 'js'
@@ -351,6 +357,7 @@ gulp.task('dist:move', function () {
paths.webroot + 'u2f-connector.html',
paths.webroot + 'duo-connector.html',
paths.webroot + 'favicon.ico',
paths.webroot + 'manifest.json',
paths.webroot + 'app-id.json'
],
dest: paths.dist
@@ -389,7 +396,7 @@ gulp.task('dist:js:app', function () {
.pipe(preprocess({ context: { cacheTag: randomString, selfHosted: selfHosted } }))
.pipe(concat(paths.dist + '/js/app.min.js'))
.pipe(ngAnnotate())
.pipe(uglify())
//.pipe(uglify())
.pipe(gulp.dest('.'));
});
@@ -401,7 +408,7 @@ gulp.task('dist:js:fallback', function () {
merge(mainStream)
.pipe(preprocess({ context: { cacheTag: randomString, selfHosted: selfHosted } }))
.pipe(uglify())
//.pipe(uglify())
.pipe(rename({ suffix: '.min' }))
.pipe(gulp.dest(paths.dist + 'js'));
});
@@ -414,7 +421,7 @@ gulp.task('dist:js:u2f', function () {
merge(mainStream)
.pipe(concat(paths.dist + '/js/u2f.min.js'))
.pipe(uglify())
//.pipe(uglify())
.pipe(gulp.dest('.'));
});
@@ -429,7 +436,7 @@ gulp.task('dist:js:lib', function () {
'!' + paths.libDir + 'jquery/**/*'
])
.pipe(concat(paths.dist + '/js/lib.min.js'))
.pipe(uglify())
//.pipe(uglify())
.pipe(gulp.dest('.'));
});
@@ -442,10 +449,16 @@ gulp.task('dist:preprocess', function () {
.pipe(gulp.dest('.'));
});
gulp.task('dist:version', function () {
gulp.src(paths.webroot + 'version.json').pipe(jeditor({
'version': project.version
})).pipe(gulp.dest(paths.dist));
});
gulp.task('dist', ['build'], function (cb) {
return runSequence(
'dist:clean',
['dist:move', 'dist:css', 'dist:js:app', 'dist:js:lib', 'dist:js:fallback', 'dist:js:u2f'],
['dist:move', 'dist:css', 'dist:js:app', 'dist:js:lib', 'dist:js:fallback', 'dist:js:u2f', 'dist:version'],
'dist:preprocess',
cb);
});
@@ -465,7 +478,7 @@ gulp.task('deploy-preview', ['dist'], function () {
return gulp.src(paths.dist + '**/*')
.pipe(ghPages({
cacheDir: paths.dist + '.publish',
remoteUrl: 'git@github.com:kspearrin/bitwarden-web-preview.git'
remoteUrl: 'git@github.com:bitwarden/web-preview.git'
}));
});

9469
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
{
"name": "bitwarden",
"version": "1.15.0",
"version": "1.27.0",
"env": "Production",
"devDependencies": {
"connect": "3.6.3",
"connect": "3.6.5",
"lodash": "4.17.4",
"gulp": "3.9.1",
"gulp-concat": "2.6.1",
@@ -11,41 +11,42 @@
"gulp-less": "3.3.2",
"gulp-rename": "1.2.2",
"gulp-uglify": "3.0.0",
"gulp-gh-pages": "0.5.4",
"gulp-gh-pages": "git+https://github.com/tekd/gulp-gh-pages.git#update-dependency",
"gulp-preprocess": "2.0.0",
"gulp-ng-annotate": "2.0.0",
"gulp-ng-config": "1.4.0",
"gulp-ng-config": "1.5.0",
"gulp-connect": "5.0.0",
"gulp-json-editor": "2.2.2",
"jshint": "2.9.5",
"gulp-jshint": "2.0.4",
"rimraf": "2.6.1",
"run-sequence": "2.1.0",
"rimraf": "2.6.2",
"run-sequence": "2.2.0",
"merge-stream": "1.0.1",
"jquery": "2.2.4",
"jquery": "3.2.1",
"font-awesome": "4.7.0",
"bootstrap": "3.3.7",
"angular": "1.6.6",
"angular-resource": "1.6.6",
"angular-sanitize": "1.6.6",
"angular-ui-bootstrap": "2.5.0",
"angular": "1.6.7",
"angular-resource": "1.6.7",
"angular-sanitize": "1.6.7",
"angular-ui-bootstrap": "2.5.6",
"angular-ui-router": "0.4.2",
"angular-jwt": "0.1.9",
"angular-cookies": "1.6.6",
"angular-cookies": "1.6.7",
"admin-lte": "2.3.11",
"angular-toastr": "2.1.1",
"angular-bootstrap-show-errors": "2.3.0",
"angular-messages": "1.6.6",
"angular-messages": "1.6.7",
"ngstorage": "0.3.11",
"papaparse": "4.3.5",
"papaparse": "4.3.6",
"clipboard": "1.7.1",
"ngclipboard": "1.1.1",
"angulartics": "1.4.0",
"ngclipboard": "1.1.2",
"angulartics": "1.5.0",
"angulartics-google-analytics": "0.4.0",
"node-forge": "0.7.1",
"webpack-stream": "4.0.0",
"angular-stripe": "5.0.0",
"angular-credit-cards": "3.1.6",
"browserify": "14.4.0",
"browserify": "14.5.0",
"vinyl-source-stream": "1.1.0",
"gulp-derequire": "2.1.0",
"exposify": "0.5.0",

View File

@@ -1,11 +1,9 @@
{
"appSettings": {
"apiUri": "https://preview-api.bitwarden.com",
"identityUri": "https://preview-identity.bitwarden.com",
"apiUri": "/api",
"identityUri": "/identity",
"iconsUri": "https://icons.bitwarden.com",
"stripeKey": "pk_test_KPoCfZXu7mznb9uSCPZ2JpTD",
"braintreeKey": "sandbox_r72q8jq6_9pnxkwm75f87sdc2",
"whitelistDomains": [
"preview-api.bitwarden.com"
]
"braintreeKey": "sandbox_r72q8jq6_9pnxkwm75f87sdc2"
}
}

View File

@@ -1,11 +1,9 @@
{
"appSettings": {
"apiUri": "https://api.bitwarden.com",
"identityUri": "https://identity.bitwarden.com",
"apiUri": "/api",
"identityUri": "/identity",
"iconsUri": "https://icons.bitwarden.com",
"stripeKey": "pk_live_bpN0P37nMxrMQkcaHXtAybJk",
"braintreeKey": "production_qfbsv8kc_njj2zjtyngtjmbjd",
"whitelistDomains": [
"api.bitwarden.com"
]
"braintreeKey": "production_qfbsv8kc_njj2zjtyngtjmbjd"
}
}

View File

@@ -2,10 +2,8 @@
"appSettings": {
"apiUri": "http://localhost:4000",
"identityUri": "http://localhost:33656",
"iconsUri": "https://icons.bitwarden.com",
"stripeKey": "pk_test_KPoCfZXu7mznb9uSCPZ2JpTD",
"braintreeKey": "sandbox_r72q8jq6_9pnxkwm75f87sdc2",
"whitelistDomains": [
"localhost"
]
"braintreeKey": "sandbox_r72q8jq6_9pnxkwm75f87sdc2"
}
}

View File

@@ -117,18 +117,18 @@ angular
}
var keys = Object.keys(twoFactorProviders);
var cleanedProviders = [];
for (var i = 0; i < keys.length; i++) {
var provider = $filter('filter')(constants.twoFactorProviderInfo, {
type: keys[i],
active: true,
requiresUsb: false
});
if (provider.length) {
cleanedProviders.push(twoFactorProviders[keys[i]]);
if (!provider.length) {
delete twoFactorProviders[keys[i]];
}
}
return cleanedProviders;
return twoFactorProviders;
}
// ref: https://stackoverflow.com/questions/11381673/detecting-a-mobile-browser
@@ -216,8 +216,9 @@ angular
function init() {
stopU2fCheck = true;
var params;
if ($scope.twoFactorProvider === constants.twoFactorProvider.duo) {
params = $scope.twoFactorProviders[constants.twoFactorProvider.duo];
if ($scope.twoFactorProvider === constants.twoFactorProvider.duo ||
$scope.twoFactorProvider === constants.twoFactorProvider.organizationDuo) {
params = $scope.twoFactorProviders[$scope.twoFactorProvider];
$window.Duo.init({
host: params.Host,

View File

@@ -29,6 +29,13 @@ angular
};
$scope.readOnlyEmail = stateParams.email !== null;
var registerOrgUserId = null;
var registerToken = null;
if(stateParams.returnState && stateParams.returnState.params &&
stateParams.returnState.name === 'frontend.organizationAccept') {
registerOrgUserId = stateParams.returnState.params.organizationUserId || null;
registerToken = stateParams.returnState.params.token || null;
}
$timeout(function () {
if ($scope.model.email) {
@@ -73,7 +80,9 @@ angular
keys: {
publicKey: result.publicKey,
encryptedPrivateKey: result.privateKeyEnc
}
},
token: registerToken,
organizationUserId: registerOrgUserId
};
return apiService.accounts.register(request).$promise;

View File

@@ -6,6 +6,9 @@
$scope.providers = [];
if (providers.hasOwnProperty(constants.twoFactorProvider.organizationDuo)) {
add(constants.twoFactorProvider.organizationDuo);
}
if (providers.hasOwnProperty(constants.twoFactorProvider.authenticator)) {
add(constants.twoFactorProvider.authenticator);
}

View File

@@ -24,7 +24,8 @@
<div class="form-group has-feedback" show-errors>
<label for="code" class="sr-only">Code</label>
<input type="text" id="code" name="Code" class="form-control" placeholder="Verification code"
ng-model="token" required api-field />
ng-model="token" required api-field autocomplete="off" autocorrect="off" autocapitalize="off"
spellcheck="false" />
<span class="fa fa-lock form-control-feedback"></span>
</div>
<div class="row">
@@ -48,7 +49,8 @@
<p class="login-box-msg">
Complete logging in with YubiKey.
</p>
<form name="twoFactorForm" ng-submit="twoFactorForm.$valid && twoFactor(token)" api-form="twoFactorPromise">
<form name="twoFactorForm" ng-submit="twoFactorForm.$valid && twoFactor(token)" api-form="twoFactorPromise"
autocomplete="off">
<div class="callout callout-danger validation-errors" ng-show="twoFactorForm.$errors">
<h4>Errors have occurred</h4>
<ul>
@@ -61,7 +63,8 @@
</p>
<div class="form-group" show-errors>
<label for="code" class="sr-only">Token</label>
<input type="password" id="code" name="Token" class="form-control" ng-model="token" required api-field />
<input type="password" id="code" name="Token" class="form-control" ng-model="token"
autocomplete="new-password" required api-field />
</div>
<div class="row">
<div class="col-xs-7">
@@ -80,11 +83,13 @@
</form>
</div>
<div ng-if="twoFactorProvider === twoFactorProviderConstants.duo">
<div ng-if="twoFactorProvider === twoFactorProviderConstants.duo ||
twoFactorProvider === twoFactorProviderConstants.organizationDuo">
<p class="login-box-msg">
Complete logging in with Duo.
</p>
<form name="twoFactorForm" ng-submit="twoFactorForm.$valid && twoFactor(token)" api-form="twoFactorPromise">
<form name="twoFactorForm" ng-submit="twoFactorForm.$valid && twoFactor(token)" api-form="twoFactorPromise"
autocomplete="off">
<div class="callout callout-danger validation-errors" ng-show="twoFactorForm.$errors">
<h4>Errors have occurred</h4>
<ul>
@@ -115,7 +120,7 @@
<p class="login-box-msg">
Complete logging in with FIDO U2F.
</p>
<form name="twoFactorForm" api-form="twoFactorPromise">
<form name="twoFactorForm" api-form="twoFactorPromise" autocomplete="off">
<div class="callout callout-danger validation-errors" ng-show="twoFactorForm.$errors">
<h4>Errors have occurred</h4>
<ul>

View File

@@ -14,7 +14,7 @@
<p class="text-center"><strong>{{state.params.email}}</strong></p>
<p>
You've been invited to join the organization listed above.
To accept the invitation, you need to log in or create a new bitwarden account.
To accept the invitation, you need to log in or create a new Bitwarden account.
</p>
<hr />
<div class="row">

View File

@@ -3,7 +3,7 @@
<i class="fa fa-shield"></i> <b>bit</b>warden
</div>
<div class="login-box-body">
<p class="login-box-msg">Enter your email address below to recover &amp; delete your bitwarden account.</p>
<p class="login-box-msg">Enter your email address below to recover &amp; delete your Bitwarden account.</p>
<div ng-show="success" class="text-center">
<div class="callout callout-success">
If your account exists ({{model.email}}) we've sent you an email with further instructions.

View File

@@ -12,7 +12,7 @@
This will permanently delete your account. This cannot be undone.
</div>
<p>
You have requested to delete your bitwarden account (<b>{{email}}</b>).
You have requested to delete your Bitwarden account (<b>{{email}}</b>).
Click the button below to confirm and proceed.
</p>
<button ng-click="delete()" class="btn btn-danger btn-block btn-flat">Delete Account</button>

View File

@@ -1,9 +1,13 @@
angular
.module('bit')
.factory('apiInterceptor', function ($injector, $q, toastr) {
.factory('apiInterceptor', function ($injector, $q, toastr, appSettings, utilsService) {
return {
request: function (config) {
if (config.url.indexOf(appSettings.apiUri + '/') === 0) {
config.headers['Device-Type'] = utilsService.getDeviceType();
}
return config;
},
response: function (response) {

View File

@@ -4,20 +4,23 @@ angular
.config(function ($stateProvider, $urlRouterProvider, $httpProvider, jwtInterceptorProvider, jwtOptionsProvider,
$uibTooltipProvider, toastrConfig, $locationProvider, $qProvider, appSettings
// @if !selfHosted
/* jshint ignore:start */
, stripeProvider
/* jshint ignore:end */
// @endif
) {
angular.extend(appSettings, window.bitwardenAppSettings);
$qProvider.errorOnUnhandledRejections(false);
$locationProvider.hashPrefix('');
jwtOptionsProvider.config({
urlParam: 'access_token',
whiteListedDomains: appSettings.whitelistDomains
whiteListedDomains: ['localhost', 'api.bitwarden.com', 'vault.bitwarden.com', 'haveibeenpwned.com']
});
var refreshPromise;
jwtInterceptorProvider.tokenGetter = /*@ngInject*/ function (options, tokenService, authService) {
if (options.url.indexOf(appSettings.apiUri) !== 0) {
if (options.url.indexOf(appSettings.apiUri + '/') !== 0) {
return;
}
@@ -34,7 +37,12 @@ angular
return token;
}
refreshPromise = authService.refreshAccessToken().then(function (newToken) {
var p = authService.refreshAccessToken();
if (!p) {
return;
}
refreshPromise = p.then(function (newToken) {
refreshPromise = null;
return newToken || token;
});
@@ -57,12 +65,6 @@ angular
appendToBody: true
});
if ($httpProvider.defaults.headers.post) {
$httpProvider.defaults.headers.post = {};
}
$httpProvider.defaults.headers.post['Content-Type'] = 'text/plain; charset=utf-8';
// stop IE from caching get requests
if (navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > 0) {
if (!$httpProvider.defaults.headers.get) {
@@ -102,12 +104,6 @@ angular
refreshFromServer: false
}
})
.state('backend.user.shared', {
url: '^/shared',
templateUrl: 'app/vault/views/vaultShared.html',
controller: 'vaultSharedController',
data: { pageTitle: 'Shared' }
})
.state('backend.user.settings', {
url: '^/settings',
templateUrl: 'app/settings/views/settings.html',
@@ -156,12 +152,6 @@ angular
controller: 'reportsBreachController',
data: { pageTitle: 'Data Breach Report' }
})
.state('backend.user.apps', {
url: '^/apps',
templateUrl: 'app/views/apps.html',
controller: 'appsController',
data: { pageTitle: 'Get the Apps' }
})
.state('backend.org', {
templateUrl: 'app/views/organizationLayout.html',
abstract: true
@@ -173,13 +163,13 @@ angular
data: { pageTitle: 'Organization Dashboard' }
})
.state('backend.org.people', {
url: '/organization/:orgId/people',
url: '/organization/:orgId/people?viewEvents&search',
templateUrl: 'app/organization/views/organizationPeople.html',
controller: 'organizationPeopleController',
data: { pageTitle: 'Organization People' }
})
.state('backend.org.collections', {
url: '/organization/:orgId/collections',
url: '/organization/:orgId/collections?search',
templateUrl: 'app/organization/views/organizationCollections.html',
controller: 'organizationCollectionsController',
data: { pageTitle: 'Organization Collections' }
@@ -197,17 +187,26 @@ angular
data: { pageTitle: 'Organization Billing' }
})
.state('backend.org.vault', {
url: '/organization/:orgId/vault',
url: '/organization/:orgId/vault?viewEvents&search',
templateUrl: 'app/organization/views/organizationVault.html',
controller: 'organizationVaultController',
data: { pageTitle: 'Organization Vault' }
data: {
pageTitle: 'Organization Vault',
controlSidebar: true
}
})
.state('backend.org.groups', {
url: '/organization/:orgId/groups',
url: '/organization/:orgId/groups?search',
templateUrl: 'app/organization/views/organizationGroups.html',
controller: 'organizationGroupsController',
data: { pageTitle: 'Organization Groups' }
})
.state('backend.org.events', {
url: '/organization/:orgId/events',
templateUrl: 'app/organization/views/organizationEvents.html',
controller: 'organizationEventsController',
data: { pageTitle: 'Organization Events' }
})
// Frontend
.state('frontend', {
@@ -353,7 +352,7 @@ angular
// user is guaranteed to be authenticated becuase of previous check
if (toState.name.indexOf('backend.org.') > -1 && toParams.orgId) {
// clear vault rootScope when visiting org admin section
$rootScope.vaultLogins = $rootScope.vaultFolders = null;
$rootScope.vaultCiphers = $rootScope.vaultFolders = $rootScope.vaultCollections = null;
authService.getUserProfile().then(function (profile) {
var orgs = profile.organizations;

View File

@@ -26,21 +26,88 @@ angular.module('bit')
duo: 2,
authenticator: 0,
email: 1,
remember: 5
remember: 5,
organizationDuo: 6
},
cipherType: {
login: 1,
secureNote: 2,
card: 3,
identity: 4
},
fieldType: {
text: 0,
hidden: 1,
boolean: 2
},
deviceType: {
android: 0,
ios: 1,
chromeExt: 2,
firefoxExt: 3,
operaExt: 4,
edgeExt: 5,
windowsDesktop: 6,
macOsDesktop: 7,
linuxDesktop: 8,
chrome: 9,
firefox: 10,
opera: 11,
edge: 12,
ie: 13,
unknown: 14,
uwp: 16,
safari: 17,
vivaldi: 18,
vivaldiExt: 19
},
eventType: {
User_LoggedIn: 1000,
User_ChangedPassword: 1001,
User_Enabled2fa: 1002,
User_Disabled2fa: 1003,
User_Recovered2fa: 1004,
User_FailedLogIn: 1005,
User_FailedLogIn2fa: 1006,
Cipher_Created: 1100,
Cipher_Updated: 1101,
Cipher_Deleted: 1102,
Cipher_AttachmentCreated: 1103,
Cipher_AttachmentDeleted: 1104,
Cipher_Shared: 1105,
Cipher_UpdatedCollections: 1106,
Collection_Created: 1300,
Collection_Updated: 1301,
Collection_Deleted: 1302,
Group_Created: 1400,
Group_Updated: 1401,
Group_Deleted: 1402,
OrganizationUser_Invited: 1500,
OrganizationUser_Confirmed: 1501,
OrganizationUser_Updated: 1502,
OrganizationUser_Removed: 1503,
OrganizationUser_UpdatedGroups: 1504,
Organization_Updated: 1600
},
twoFactorProviderInfo: [
{
type: 0,
name: 'Authenticator App',
description: 'Use an authenticator app (such as Authy or Google Authenticator) to generate time-based ' +
'verification codes.',
'verification codes.',
enabled: false,
active: true,
free: true,
image: 'authapp.png',
displayOrder: 0,
priority: 1,
requiresUsb: false
requiresUsb: false,
organization: false
},
{
type: 3,
@@ -51,7 +118,8 @@ angular.module('bit')
image: 'yubico.png',
displayOrder: 1,
priority: 3,
requiresUsb: true
requiresUsb: true,
organization: false
},
{
type: 2,
@@ -62,7 +130,8 @@ angular.module('bit')
image: 'duo.png',
displayOrder: 2,
priority: 2,
requiresUsb: false
requiresUsb: false,
organization: false
},
{
type: 4,
@@ -73,7 +142,8 @@ angular.module('bit')
image: 'fido.png',
displayOrder: 3,
priority: 4,
requiresUsb: true
requiresUsb: true,
organization: false
},
{
type: 1,
@@ -85,7 +155,21 @@ angular.module('bit')
image: 'gmail.png',
displayOrder: 4,
priority: 0,
requiresUsb: false
requiresUsb: false,
organization: false
},
{
type: 6,
name: 'Duo (Organization)',
description: 'Verify with Duo Security for your organization using the Duo Mobile app, SMS, ' +
'phone call, or U2F security key.',
enabled: false,
active: true,
image: 'duo.png',
displayOrder: 1,
priority: 10,
requiresUsb: false,
organization: true
}
],
plans: {
@@ -95,14 +179,12 @@ angular.module('bit')
noPayment: true,
upgradeSortOrder: -1
},
personal: {
families: {
basePrice: 1,
annualBasePrice: 12,
baseSeats: 5,
seatPrice: 1,
annualSeatPrice: 12,
maxAdditionalSeats: 5,
annualPlanType: 'personalAnnually',
noAdditionalSeats: true,
annualPlanType: 'familiesAnnually',
upgradeSortOrder: 1
},
teams: {

View File

@@ -0,0 +1,11 @@
angular
.module('bit.directives')
.directive('fallbackSrc', function () {
return function (scope, element, attrs) {
var el = $(element);
el.bind('error', function (event) {
el.attr('src', attrs.fallbackSrc);
});
};
});

View File

@@ -6,7 +6,7 @@ angular
link: function (scope, element) {
var listener = function (event, toState, toParams, fromState, fromParams) {
// Default title
var title = 'bitwarden Web Vault';
var title = 'Bitwarden Web Vault';
if (toState.data && toState.data.pageTitle) {
title = toState.data.pageTitle + ' - ' + title;
}

View File

@@ -1,6 +0,0 @@
angular
.module('bit.global')
.controller('appsController', function ($scope, $state) {
});

View File

@@ -4,6 +4,7 @@ angular
.controller('mainController', function ($scope, $state, authService, appSettings, toastr, $window, $document,
cryptoService, $uibModal, apiService) {
var vm = this;
vm.skinClass = appSettings.selfHosted ? 'skin-blue-light' : 'skin-blue';
vm.bodyClass = '';
vm.usingControlSidebar = vm.openControlSidebar = false;
vm.searchVaultText = null;
@@ -48,16 +49,16 @@ angular
vm.openControlSidebar = vm.usingControlSidebar && $document.width() > 768;
});
$scope.addLogin = function () {
$scope.$broadcast('vaultAddLogin');
$scope.addCipher = function () {
$scope.$broadcast('vaultAddCipher');
};
$scope.addFolder = function () {
$scope.$broadcast('vaultAddFolder');
};
$scope.addOrganizationLogin = function () {
$scope.$broadcast('organizationVaultAddLogin');
$scope.addOrganizationCipher = function () {
$scope.$broadcast('organizationVaultAddCipher');
};
$scope.addOrganizationCollection = function () {

View File

@@ -1,12 +1,23 @@
angular
.module('bit.global')
.controller('sideNavController', function ($scope, $state, authService, toastr, $analytics, constants) {
.controller('sideNavController', function ($scope, $state, authService, toastr, $analytics, constants, appSettings) {
$scope.$state = $state;
$scope.params = $state.params;
$scope.orgs = [];
$scope.name = '';
if(appSettings.selfHosted) {
$scope.orgIconBgColor = '#ffffff';
$scope.orgIconBorder = '3px solid #a0a0a0';
$scope.orgIconTextColor = '#333333';
}
else {
$scope.orgIconBgColor = '#2c3b41';
$scope.orgIconBorder = '3px solid #1a2226';
$scope.orgIconTextColor = '#ffffff';
}
authService.getUserProfile().then(function (userProfile) {
$scope.name = userProfile.extended && userProfile.extended.name ?
userProfile.extended.name : userProfile.email;
@@ -40,14 +51,6 @@ angular
$state.go('backend.org.dashboard', { orgId: org.id });
};
$scope.searchVault = function () {
$state.go('backend.user.vault');
};
$scope.searchOrganizationVault = function () {
$state.go('backend.org.vault', { orgId: $state.params.orgId });
};
$scope.isOrgOwner = function (org) {
return org && org.type === constants.orgUserType.owner;
};

View File

@@ -4,7 +4,9 @@
.controller('organizationBillingChangePaymentController', function ($scope, $state, $uibModalInstance, apiService,
$analytics, toastr, existingPaymentMethod
// @if !selfHosted
/* jshint ignore:start */
, stripe
/* jshint ignore:end */
// @endif
) {
$analytics.eventTrack('organizationBillingChangePaymentController', { category: 'Modal' });

View File

@@ -2,7 +2,7 @@
.module('bit.organization')
.controller('organizationBillingController', function ($scope, apiService, $state, $uibModal, toastr, $analytics,
appSettings) {
appSettings, tokenService, $window) {
$scope.selfHosted = appSettings.selfHosted;
$scope.charges = [];
$scope.paymentSource = null;
@@ -59,7 +59,7 @@
};
$scope.adjustSeats = function (add) {
if ($scope.selfHosted) {
if ($scope.selfHosted || !$scope.canAdjustSeats) {
return;
}
@@ -208,10 +208,20 @@
});
};
$scope.viewInvoice = function (charge) {
if ($scope.selfHosted) {
return;
}
var url = appSettings.apiUri + '/organizations/' + $state.params.orgId +
'/billing-invoice/' + charge.invoiceId + '?access_token=' + tokenService.getToken();
$window.open(url);
};
function load() {
apiService.organizations.getBilling({ id: $state.params.orgId }, function (org) {
$scope.loading = false;
$scope.noSubscription = org.PlanType === 0;
$scope.canAdjustSeats = org.PlanType > 1;
var i = 0;
$scope.expiration = org.Expiration;

View File

@@ -2,7 +2,7 @@
.module('bit.organization')
.controller('organizationCollectionsController', function ($scope, $state, apiService, $uibModal, cipherService, $filter,
toastr, $analytics) {
toastr, $analytics, $uibModalStack) {
$scope.collections = [];
$scope.loading = true;
$scope.$on('$viewContentLoaded', function () {
@@ -96,6 +96,12 @@
apiService.collections.listOrganization({ orgId: $state.params.orgId }, function (list) {
$scope.collections = cipherService.decryptCollections(list.Data, $state.params.orgId, true);
$scope.loading = false;
if ($state.params.search) {
$uibModalStack.dismissAll();
$scope.filterSearch = $state.params.search;
$('#filterSearch').focus();
}
});
}
});

View File

@@ -1,7 +1,9 @@
angular
.module('bit.organization')
.controller('organizationDashboardController', function ($scope, authService, $state) {
.controller('organizationDashboardController', function ($scope, authService, $state, appSettings) {
$scope.selfHosted = appSettings.selfHosted;
$scope.$on('$viewContentLoaded', function () {
authService.getUserProfile().then(function (userProfile) {
if (!userProfile.organizations) {
@@ -10,4 +12,8 @@
$scope.orgProfile = userProfile.organizations[$state.params.orgId];
});
});
$scope.goBilling = function () {
$state.go('backend.org.billing', { orgId: $state.params.orgId });
};
});

View File

@@ -0,0 +1,100 @@
angular
.module('bit.organization')
.controller('organizationEventsController', function ($scope, $state, apiService, $uibModal, $filter,
toastr, $analytics, constants, eventService, $compile, $sce) {
$scope.events = [];
$scope.orgUsers = [];
$scope.loading = true;
$scope.continuationToken = null;
var defaultFilters = eventService.getDefaultDateFilters();
$scope.filterStart = defaultFilters.start;
$scope.filterEnd = defaultFilters.end;
$scope.$on('$viewContentLoaded', function () {
load();
});
$scope.refresh = function () {
loadEvents(true);
};
$scope.next = function () {
loadEvents(false);
};
var i = 0,
orgUsersUserIdDict = {},
orgUsersIdDict = {};
function load() {
apiService.organizationUsers.list({ orgId: $state.params.orgId }).$promise.then(function (list) {
var users = [];
for (i = 0; i < list.Data.length; i++) {
var user = {
id: list.Data[i].Id,
userId: list.Data[i].UserId,
name: list.Data[i].Name,
email: list.Data[i].Email
};
users.push(user);
var displayName = user.name || user.email;
orgUsersUserIdDict[user.userId] = displayName;
orgUsersIdDict[user.id] = displayName;
}
$scope.orgUsers = users;
return loadEvents(true);
});
}
function loadEvents(clearExisting) {
var filterResult = eventService.formatDateFilters($scope.filterStart, $scope.filterEnd);
if (filterResult.error) {
alert(filterResult.error);
return;
}
if (clearExisting) {
$scope.continuationToken = null;
$scope.events = [];
}
$scope.loading = true;
return apiService.events.listOrganization({
orgId: $state.params.orgId,
start: filterResult.start,
end: filterResult.end,
continuationToken: $scope.continuationToken
}).$promise.then(function (list) {
$scope.continuationToken = list.ContinuationToken;
var events = [];
for (i = 0; i < list.Data.length; i++) {
var userId = list.Data[i].ActingUserId || list.Data[i].UserId;
var eventInfo = eventService.getEventInfo(list.Data[i]);
var htmlMessage = $compile('<span>' + eventInfo.message + '</span>')($scope);
events.push({
message: $sce.trustAsHtml(htmlMessage[0].outerHTML),
appIcon: eventInfo.appIcon,
appName: eventInfo.appName,
userId: userId,
userName: userId ? (orgUsersUserIdDict[userId] || '-') : '-',
date: list.Data[i].Date,
ip: list.Data[i].IpAddress
});
}
if ($scope.events && $scope.events.length > 0) {
$scope.events = $scope.events.concat(events);
}
else {
$scope.events = events;
}
$scope.loading = false;
});
}
});

View File

@@ -2,7 +2,7 @@
.module('bit.organization')
.controller('organizationGroupsController', function ($scope, $state, apiService, $uibModal, $filter,
toastr, $analytics) {
toastr, $analytics, $uibModalStack) {
$scope.groups = [];
$scope.loading = true;
$scope.$on('$viewContentLoaded', function () {
@@ -88,6 +88,12 @@
}
$scope.groups = groups;
$scope.loading = false;
if ($state.params.search) {
$uibModalStack.dismissAll();
$scope.filterSearch = $state.params.search;
$('#filterSearch').focus();
}
});
}
});

View File

@@ -2,9 +2,10 @@
.module('bit.organization')
.controller('organizationPeopleController', function ($scope, $state, $uibModal, cryptoService, apiService, authService,
toastr, $analytics) {
toastr, $analytics, $filter, $uibModalStack) {
$scope.users = [];
$scope.useGroups = false;
$scope.useEvents = false;
$scope.$on('$viewContentLoaded', function () {
loadList();
@@ -13,6 +14,7 @@
if (profile.organizations) {
var org = profile.organizations[$state.params.orgId];
$scope.useGroups = !!org.useGroups;
$scope.useEvents = !!org.useEvents;
}
});
});
@@ -110,6 +112,18 @@
});
};
$scope.events = function (user) {
$uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationPeopleEvents.html',
controller: 'organizationPeopleEventsController',
resolve: {
orgUser: function () { return user; },
orgId: function () { return $state.params.orgId; }
}
});
};
function loadList() {
apiService.organizationUsers.list({ orgId: $state.params.orgId }, function (list) {
var users = [];
@@ -129,6 +143,20 @@
}
$scope.users = users;
if ($state.params.search) {
$uibModalStack.dismissAll();
$scope.filterSearch = $state.params.search;
$('#filterSearch').focus();
}
if ($state.params.viewEvents) {
$uibModalStack.dismissAll();
var eventUser = $filter('filter')($scope.users, { id: $state.params.viewEvents });
if (eventUser && eventUser.length) {
$scope.events(eventUser[0]);
}
}
});
}
});

View File

@@ -0,0 +1,75 @@
angular
.module('bit.organization')
.controller('organizationPeopleEventsController', function ($scope, apiService, $uibModalInstance,
orgUser, $analytics, eventService, orgId, $compile, $sce) {
$analytics.eventTrack('organizationPeopleEventsController', { category: 'Modal' });
$scope.email = orgUser.email;
$scope.events = [];
$scope.loading = true;
$scope.continuationToken = null;
var defaultFilters = eventService.getDefaultDateFilters();
$scope.filterStart = defaultFilters.start;
$scope.filterEnd = defaultFilters.end;
$uibModalInstance.opened.then(function () {
loadEvents(true);
});
$scope.refresh = function () {
loadEvents(true);
};
$scope.next = function () {
loadEvents(false);
};
function loadEvents(clearExisting) {
var filterResult = eventService.formatDateFilters($scope.filterStart, $scope.filterEnd);
if (filterResult.error) {
alert(filterResult.error);
return;
}
if (clearExisting) {
$scope.continuationToken = null;
$scope.events = [];
}
$scope.loading = true;
return apiService.events.listOrganizationUser({
orgId: orgId,
id: orgUser.id,
start: filterResult.start,
end: filterResult.end,
continuationToken: $scope.continuationToken
}).$promise.then(function (list) {
$scope.continuationToken = list.ContinuationToken;
var events = [];
for (var i = 0; i < list.Data.length; i++) {
var eventInfo = eventService.getEventInfo(list.Data[i]);
var htmlMessage = $compile('<span>' + eventInfo.message + '</span>')($scope);
events.push({
message: $sce.trustAsHtml(htmlMessage[0].outerHTML),
appIcon: eventInfo.appIcon,
appName: eventInfo.appName,
date: list.Data[i].Date,
ip: list.Data[i].IpAddress
});
}
if ($scope.events && $scope.events.length > 0) {
$scope.events = $scope.events.concat(events);
}
else {
$scope.events = events;
}
$scope.loading = false;
});
}
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@@ -2,16 +2,47 @@
.module('bit.organization')
.controller('organizationSettingsController', function ($scope, $state, apiService, toastr, authService, $uibModal,
$analytics, appSettings) {
$analytics, appSettings, constants, $filter) {
$scope.selfHosted = appSettings.selfHosted;
$scope.model = {};
$scope.twoStepProviders = $filter('filter')(constants.twoFactorProviderInfo, { organization: true });
$scope.use2fa = false;
$scope.$on('$viewContentLoaded', function () {
apiService.organizations.get({ id: $state.params.orgId }, function (org) {
apiService.organizations.get({ id: $state.params.orgId }).$promise.then(function (org) {
$scope.model = {
name: org.Name,
billingEmail: org.BillingEmail,
businessName: org.BusinessName
businessName: org.BusinessName,
businessAddress1: org.BusinessAddress1,
businessAddress2: org.BusinessAddress2,
businessAddress3: org.BusinessAddress3,
businessCountry: org.BusinessCountry,
businessTaxNumber: org.BusinessTaxNumber
};
$scope.use2fa = org.Use2fa;
if (org.Use2fa) {
return apiService.twoFactor.listOrganization({ orgId: $state.params.orgId }).$promise;
}
else {
return null;
}
}).then(function (response) {
if (!response || !response.Data) {
return;
}
for (var i = 0; i < response.Data.length; i++) {
if (!response.Data[i].Enabled) {
continue;
}
var provider = $filter('filter')($scope.twoStepProviders, { type: response.Data[i].Type });
if (provider.length) {
provider[0].enabled = true;
}
}
});
});
@@ -28,6 +59,22 @@
}).$promise;
};
$scope.import = function () {
$uibModal.open({
animation: true,
templateUrl: 'app/tools/views/toolsImport.html',
controller: 'organizationSettingsImportController'
});
};
$scope.export = function () {
$uibModal.open({
animation: true,
templateUrl: 'app/tools/views/toolsExport.html',
controller: 'organizationSettingsExportController'
});
};
$scope.delete = function () {
$uibModal.open({
animation: true,
@@ -35,4 +82,30 @@
controller: 'organizationDeleteController'
});
};
$scope.edit = function (provider) {
if (provider.type === constants.twoFactorProvider.organizationDuo) {
typeName = 'Duo';
}
else {
return;
}
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsTwoStep' + typeName + '.html',
controller: 'settingsTwoStep' + typeName + 'Controller',
resolve: {
enabled: function () { return provider.enabled; },
orgId: function () { return $state.params.orgId; }
}
});
modal.result.then(function (enabled) {
if (enabled || enabled === false) {
// do not adjust when undefined or null
provider.enabled = enabled;
}
});
};
});

View File

@@ -0,0 +1,155 @@
angular
.module('bit.organization')
.controller('organizationSettingsExportController', function ($scope, apiService, $uibModalInstance, cipherService,
$q, toastr, $analytics, $state, constants) {
$analytics.eventTrack('organizationSettingsExportController', { category: 'Modal' });
$scope.export = function (model) {
$scope.startedExport = true;
var decCiphers = [],
decCollections = [];
var collectionsPromise = apiService.collections.listOrganization({ orgId: $state.params.orgId },
function (collections) {
decCollections = cipherService.decryptCollections(collections.Data, $state.params.orgId, true);
}).$promise;
var ciphersPromise = apiService.ciphers.listOrganizationDetails({ organizationId: $state.params.orgId },
function (ciphers) {
decCiphers = cipherService.decryptCiphers(ciphers.Data);
}).$promise;
$q.all([collectionsPromise, ciphersPromise]).then(function () {
if (!decCiphers.length) {
toastr.error('Nothing to export.', 'Error!');
$scope.close();
return;
}
var i;
var collectionsDict = {};
for (i = 0; i < decCollections.length; i++) {
collectionsDict[decCollections[i].id] = decCollections[i];
}
try {
var exportCiphers = [];
for (i = 0; i < decCiphers.length; i++) {
// only export logins and secure notes
if (decCiphers[i].type !== constants.cipherType.login &&
decCiphers[i].type !== constants.cipherType.secureNote) {
continue;
}
var cipher = {
collections: [],
type: null,
name: decCiphers[i].name,
notes: decCiphers[i].notes,
fields: null,
// Login props
login_uri: null,
login_username: null,
login_password: null,
login_totp: null
};
var j;
if (decCiphers[i].collectionIds) {
for (j = 0; j < decCiphers[i].collectionIds.length; j++) {
if (collectionsDict.hasOwnProperty(decCiphers[i].collectionIds[j])) {
cipher.collections.push(collectionsDict[decCiphers[i].collectionIds[j]].name);
}
}
}
if (decCiphers[i].fields) {
for (j = 0; j < decCiphers[i].fields.length; j++) {
if (!cipher.fields) {
cipher.fields = '';
}
else {
cipher.fields += '\n';
}
cipher.fields += ((decCiphers[i].fields[j].name || '') + ': ' + decCiphers[i].fields[j].value);
}
}
switch (decCiphers[i].type) {
case constants.cipherType.login:
cipher.type = 'login';
cipher.login_uri = null;
cipher.login_username = decCiphers[i].login.username;
cipher.login_password = decCiphers[i].login.password;
cipher.login_totp = decCiphers[i].login.totp;
if (decCiphers[i].login.uris && decCiphers[i].login.uris.length) {
cipher.login_uri = [];
for (j = 0; j < decCiphers[i].login.uris.length; j++) {
cipher.login_uri.push(decCiphers[i].login.uris[j].uri);
}
}
break;
case constants.cipherType.secureNote:
cipher.type = 'note';
break;
default:
continue;
}
exportCiphers.push(cipher);
}
var csvString = Papa.unparse(exportCiphers);
var csvBlob = new Blob([csvString]);
// IE hack. ref http://msdn.microsoft.com/en-us/library/ie/hh779016.aspx
if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveBlob(csvBlob, makeFileName());
}
else {
var a = window.document.createElement('a');
a.href = window.URL.createObjectURL(csvBlob, { type: 'text/plain' });
a.download = makeFileName();
document.body.appendChild(a);
// IE: "Access is denied".
// ref: https://connect.microsoft.com/IE/feedback/details/797361/ie-10-treats-blob-url-as-cross-origin-and-denies-access
a.click();
document.body.removeChild(a);
}
$analytics.eventTrack('Exported Organization Data');
toastr.success('Your data has been exported. Check your browser\'s downloads folder.', 'Success!');
$scope.close();
}
catch (err) {
toastr.error('Something went wrong. Please try again.', 'Error!');
$scope.close();
}
}, function () {
toastr.error('Something went wrong. Please try again.', 'Error!');
$scope.close();
});
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
function makeFileName() {
var now = new Date();
var dateString =
now.getFullYear() + '' + padNumber(now.getMonth() + 1, 2) + '' + padNumber(now.getDate(), 2) +
padNumber(now.getHours(), 2) + '' + padNumber(now.getMinutes(), 2) +
padNumber(now.getSeconds(), 2);
return 'bitwarden_org_export_' + dateString + '.csv';
}
function padNumber(number, width, paddingCharacter) {
paddingCharacter = paddingCharacter || '0';
number = number + '';
return number.length >= width ? number : new Array(width - number.length + 1).join(paddingCharacter) + number;
}
});

View File

@@ -0,0 +1,129 @@
angular
.module('bit.organization')
.controller('organizationSettingsImportController', function ($scope, $state, apiService, $uibModalInstance, cipherService,
toastr, importService, $analytics, $sce, validationService, cryptoService) {
$analytics.eventTrack('organizationSettingsImportController', { category: 'Modal' });
$scope.model = { source: '' };
$scope.source = {};
$scope.splitFeatured = false;
$scope.options = [
{
id: 'bitwardencsv',
name: 'Bitwarden (csv)',
featured: true,
sort: 1,
instructions: $sce.trustAsHtml('Export using the web vault (vault.bitwarden.com). ' +
'Log into the web vault and navigate to your organization\'s admin area. Then to go ' +
'"Settings" > "Tools" > "Export".')
},
{
id: 'lastpass',
name: 'LastPass (csv)',
featured: true,
sort: 2,
instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' +
'<a target="_blank" href="https://help.bitwarden.com/article/import-from-lastpass/">' +
'https://help.bitwarden.com/article/import-from-lastpass/</a>')
}
];
$scope.setSource = function () {
for (var i = 0; i < $scope.options.length; i++) {
if ($scope.options[i].id === $scope.model.source) {
$scope.source = $scope.options[i];
break;
}
}
};
$scope.setSource();
$scope.import = function (model, form) {
if (!model.source || model.source === '') {
validationService.addError(form, 'source', 'Select the format of the import file.', true);
return;
}
var file = document.getElementById('file').files[0];
if (!file && (!model.fileContents || model.fileContents === '')) {
validationService.addError(form, 'file', 'Select the import file or copy/paste the import file contents.', true);
return;
}
$scope.processing = true;
importService.importOrg(model.source, file || model.fileContents, importSuccess, importError);
};
function importSuccess(collections, ciphers, collectionRelationships) {
if (!collections.length && !ciphers.length) {
importError('Nothing was imported.');
return;
}
else if (ciphers.length) {
var halfway = Math.floor(ciphers.length / 2);
var last = ciphers.length - 1;
if (cipherIsBadData(ciphers[0]) && cipherIsBadData(ciphers[halfway]) && cipherIsBadData(ciphers[last])) {
importError('Data is not formatted correctly. Please check your import file and try again.');
return;
}
}
apiService.ciphers.importOrg({ orgId: $state.params.orgId }, {
collections: cipherService.encryptCollections(collections, $state.params.orgId),
ciphers: cipherService.encryptCiphers(ciphers, cryptoService.getOrgKey($state.params.orgId)),
collectionRelationships: collectionRelationships
}, function () {
$uibModalInstance.dismiss('cancel');
$state.go('backend.org.vault', { orgId: $state.params.orgId }).then(function () {
$analytics.eventTrack('Imported Org Data', { label: $scope.model.source });
toastr.success('Data has been successfully imported into your vault.', 'Import Success');
});
}, importError);
}
function cipherIsBadData(cipher) {
return (cipher.name === null || cipher.name === '--') &&
(cipher.login && (cipher.login.password === null || cipher.login.password === ''));
}
function importError(error) {
$analytics.eventTrack('Import Org Data Failed', { label: $scope.model.source });
$uibModalInstance.dismiss('cancel');
if (error) {
var data = error.data;
if (data && data.ValidationErrors) {
var message = '';
for (var key in data.ValidationErrors) {
if (!data.ValidationErrors.hasOwnProperty(key)) {
continue;
}
for (var i = 0; i < data.ValidationErrors[key].length; i++) {
message += (key + ': ' + data.ValidationErrors[key][i] + ' ');
}
}
if (message !== '') {
toastr.error(message);
return;
}
}
else if (data && data.Message) {
toastr.error(data.Message);
return;
}
else {
toastr.error(error);
return;
}
}
toastr.error('Something went wrong. Try again.', 'Oh No!');
}
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@@ -0,0 +1,141 @@
angular
.module('bit.organization')
.controller('organizationVaultAddCipherController', function ($scope, apiService, $uibModalInstance, cryptoService,
cipherService, passwordService, $analytics, authService, orgId, $uibModal, constants, selectedType) {
$analytics.eventTrack('organizationVaultAddCipherController', { category: 'Modal' });
$scope.constants = constants;
$scope.selectedType = selectedType ? selectedType.toString() : constants.cipherType.login.toString();
$scope.cipher = {
type: selectedType || constants.cipherType.login,
login: {
uris: [{
uri: null,
match: null,
matchValue: null
}]
},
identity: {},
card: {},
secureNote: {
type: '0'
}
};
$scope.hideFolders = $scope.hideFavorite = $scope.fromOrg = true;
authService.getUserProfile().then(function (userProfile) {
var orgProfile = userProfile.organizations[orgId];
$scope.useTotp = orgProfile.useTotp;
});
$scope.typeChanged = function () {
$scope.cipher.type = parseInt($scope.selectedType);
};
$scope.savePromise = null;
$scope.save = function () {
$scope.cipher.organizationId = orgId;
var cipher = cipherService.encryptCipher($scope.cipher);
$scope.savePromise = apiService.ciphers.postAdmin(cipher, function (cipherResponse) {
$analytics.eventTrack('Created Organization Cipher');
var decCipher = cipherService.decryptCipherPreview(cipherResponse);
$uibModalInstance.close(decCipher);
}).$promise;
};
$scope.generatePassword = function () {
if (!$scope.cipher.login.password || confirm('Are you sure you want to overwrite the current password?')) {
$analytics.eventTrack('Generated Password From Add');
$scope.cipher.login.password = passwordService.generatePassword({ length: 14, special: true });
}
};
$scope.addUri = function () {
if (!$scope.cipher.login) {
return;
}
if (!$scope.cipher.login.uris) {
$scope.cipher.login.uris = [];
}
$scope.cipher.login.uris.push({
uri: null,
match: null,
matchValue: null
});
};
$scope.removeUri = function (uri) {
if (!$scope.cipher.login || !$scope.cipher.login.uris) {
return;
}
var index = $scope.cipher.login.uris.indexOf(uri);
if (index > -1) {
$scope.cipher.login.uris.splice(index, 1);
}
};
$scope.uriMatchChanged = function (uri) {
if ((!uri.matchValue && uri.matchValue !== 0) || uri.matchValue === '') {
uri.match = null;
}
else {
uri.match = parseInt(uri.matchValue);
}
};
$scope.addField = function () {
if (!$scope.cipher.fields) {
$scope.cipher.fields = [];
}
$scope.cipher.fields.push({
type: constants.fieldType.text.toString(),
name: null,
value: null
});
};
$scope.removeField = function (field) {
var index = $scope.cipher.fields.indexOf(field);
if (index > -1) {
$scope.cipher.fields.splice(index, 1);
}
};
$scope.clipboardSuccess = function (e) {
e.clearSelection();
selectPassword(e);
};
$scope.clipboardError = function (e, password) {
if (password) {
selectPassword(e);
}
alert('Your web browser does not support easy clipboard copying. Copy it manually instead.');
};
function selectPassword(e) {
var target = $(e.trigger).parent().prev();
if (target.attr('type') === 'text') {
target.select();
}
}
$scope.close = function () {
$uibModalInstance.dismiss('close');
};
$scope.showUpgrade = function () {
$uibModal.open({
animation: true,
templateUrl: 'app/views/paidOrgRequired.html',
controller: 'paidOrgRequiredController',
resolve: {
orgId: function () { return orgId; }
}
});
};
});

View File

@@ -1,66 +0,0 @@
angular
.module('bit.organization')
.controller('organizationVaultAddLoginController', function ($scope, apiService, $uibModalInstance, cryptoService,
cipherService, passwordService, $analytics, authService, orgId, $uibModal) {
$analytics.eventTrack('organizationVaultAddLoginController', { category: 'Modal' });
$scope.login = {};
$scope.hideFolders = $scope.hideFavorite = $scope.fromOrg = true;
authService.getUserProfile().then(function (userProfile) {
var orgProfile = userProfile.organizations[orgId];
$scope.useTotp = orgProfile.useTotp;
});
$scope.savePromise = null;
$scope.save = function (model) {
model.organizationId = orgId;
var login = cipherService.encryptLogin(model);
$scope.savePromise = apiService.logins.postAdmin(login, function (loginResponse) {
$analytics.eventTrack('Created Organization Login');
var decLogin = cipherService.decryptLogin(loginResponse);
$uibModalInstance.close(decLogin);
}).$promise;
};
$scope.generatePassword = function () {
if (!$scope.login.password || confirm('Are you sure you want to overwrite the current password?')) {
$analytics.eventTrack('Generated Password From Add');
$scope.login.password = passwordService.generatePassword({ length: 12, special: true });
}
};
$scope.clipboardSuccess = function (e) {
e.clearSelection();
selectPassword(e);
};
$scope.clipboardError = function (e, password) {
if (password) {
selectPassword(e);
}
alert('Your web browser does not support easy clipboard copying. Copy it manually instead.');
};
function selectPassword(e) {
var target = $(e.trigger).parent().prev();
if (target.attr('type') === 'text') {
target.select();
}
}
$scope.close = function () {
$uibModalInstance.dismiss('close');
};
$scope.showUpgrade = function () {
$uibModal.open({
animation: true,
templateUrl: 'app/views/paidOrgRequired.html',
controller: 'paidOrgRequiredController',
resolve: {
orgId: function () { return orgId; }
}
});
};
});

View File

@@ -2,16 +2,16 @@
.module('bit.organization')
.controller('organizationVaultAttachmentsController', function ($scope, apiService, $uibModalInstance, cryptoService,
cipherService, loginId, $analytics, validationService, toastr, $timeout) {
cipherService, cipherId, $analytics, validationService, toastr, $timeout) {
$analytics.eventTrack('organizationVaultAttachmentsController', { category: 'Modal' });
$scope.login = {};
$scope.cipher = {};
$scope.loading = true;
$scope.isPremium = true;
$scope.canUseAttachments = true;
var closing = false;
apiService.logins.getAdmin({ id: loginId }, function (login) {
$scope.login = cipherService.decryptLogin(login);
apiService.ciphers.getAdmin({ id: cipherId }, function (cipher) {
$scope.cipher = cipherService.decryptCipher(cipher);
$scope.loading = false;
}, function () {
$scope.loading = false;
@@ -24,30 +24,26 @@
return;
}
var key = cryptoService.getOrgKey($scope.login.organizationId);
var key = cryptoService.getOrgKey($scope.cipher.organizationId);
$scope.savePromise = cipherService.encryptAttachmentFile(key, files[0]).then(function (encValue) {
var fd = new FormData();
var blob = new Blob([encValue.data], { type: 'application/octet-stream' });
fd.append('data', blob, encValue.fileName);
return apiService.ciphers.postAttachment({ id: loginId }, fd).$promise;
return apiService.ciphers.postAttachmentAdmin({ id: cipherId }, fd).$promise;
}).then(function (response) {
$analytics.eventTrack('Added Attachment');
toastr.success('The attachment has been added.');
closing = true;
$uibModalInstance.close(true);
}, function (err) {
if (err) {
validationService.addError(form, 'file', err, true);
}
else {
validationService.addError(form, 'file', 'Something went wrong.', true);
}
}, function (e) {
var errors = validationService.parseErrors(e);
toastr.error(errors.length ? errors[0] : 'An error occurred.');
});
};
$scope.download = function (attachment) {
attachment.loading = true;
var key = cryptoService.getOrgKey($scope.login.organizationId);
var key = cryptoService.getOrgKey($scope.cipher.organizationId);
cipherService.downloadAndDecryptAttachment(key, attachment, true).then(function (res) {
$timeout(function () {
attachment.loading = false;
@@ -65,12 +61,12 @@
}
attachment.loading = true;
apiService.ciphers.delAttachment({ id: loginId, attachmentId: attachment.id }).$promise.then(function () {
apiService.ciphers.delAttachmentAdmin({ id: cipherId, attachmentId: attachment.id }).$promise.then(function () {
attachment.loading = false;
$analytics.eventTrack('Deleted Organization Attachment');
var index = $scope.login.attachments.indexOf(attachment);
var index = $scope.cipher.attachments.indexOf(attachment);
if (index > -1) {
$scope.login.attachments.splice(index, 1);
$scope.cipher.attachments.splice(index, 1);
}
}, function () {
toastr.error('Cannot delete attachment.');
@@ -89,6 +85,6 @@
e.preventDefault();
closing = true;
$uibModalInstance.close(!!$scope.login.attachments && $scope.login.attachments.length > 0);
$uibModalInstance.close(!!$scope.cipher.attachments && $scope.cipher.attachments.length > 0);
});
});

View File

@@ -1,9 +1,9 @@
angular
.module('bit.organization')
.controller('organizationVaultLoginCollectionsController', function ($scope, apiService, $uibModalInstance, cipherService,
.controller('organizationVaultCipherCollectionsController', function ($scope, apiService, $uibModalInstance, cipherService,
cipher, $analytics, collections) {
$analytics.eventTrack('organizationVaultLoginCollectionsController', { category: 'Modal' });
$analytics.eventTrack('organizationVaultCipherCollectionsController', { category: 'Modal' });
$scope.cipher = {};
$scope.collections = [];
$scope.selectedCollections = {};
@@ -69,7 +69,7 @@
$scope.submitPromise = apiService.ciphers.putCollectionsAdmin({ id: cipher.id }, request)
.$promise.then(function (response) {
$analytics.eventTrack('Edited Login Collections');
$analytics.eventTrack('Edited Cipher Collections');
$uibModalInstance.close({
action: 'collectionsEdit',
collectionIds: request.collectionIds

View File

@@ -0,0 +1,104 @@
angular
.module('bit.organization')
.controller('organizationVaultCipherEventsController', function ($scope, apiService, $uibModalInstance,
cipher, $analytics, eventService) {
$analytics.eventTrack('organizationVaultCipherEventsController', { category: 'Modal' });
$scope.cipher = cipher;
$scope.events = [];
$scope.loading = true;
$scope.continuationToken = null;
var defaultFilters = eventService.getDefaultDateFilters();
$scope.filterStart = defaultFilters.start;
$scope.filterEnd = defaultFilters.end;
$uibModalInstance.opened.then(function () {
load();
});
$scope.refresh = function () {
loadEvents(true);
};
$scope.next = function () {
loadEvents(false);
};
var i = 0,
orgUsersUserIdDict = {},
orgUsersIdDict = {};
function load() {
apiService.organizationUsers.list({ orgId: cipher.organizationId }).$promise.then(function (list) {
var users = [];
for (i = 0; i < list.Data.length; i++) {
var user = {
id: list.Data[i].Id,
userId: list.Data[i].UserId,
name: list.Data[i].Name,
email: list.Data[i].Email
};
users.push(user);
var displayName = user.name || user.email;
orgUsersUserIdDict[user.userId] = displayName;
orgUsersIdDict[user.id] = displayName;
}
$scope.orgUsers = users;
return loadEvents(true);
});
}
function loadEvents(clearExisting) {
var filterResult = eventService.formatDateFilters($scope.filterStart, $scope.filterEnd);
if (filterResult.error) {
alert(filterResult.error);
return;
}
if (clearExisting) {
$scope.continuationToken = null;
$scope.events = [];
}
$scope.loading = true;
return apiService.events.listCipher({
id: cipher.id,
start: filterResult.start,
end: filterResult.end,
continuationToken: $scope.continuationToken
}).$promise.then(function (list) {
$scope.continuationToken = list.ContinuationToken;
var events = [];
for (i = 0; i < list.Data.length; i++) {
var userId = list.Data[i].ActingUserId || list.Data[i].UserId;
var eventInfo = eventService.getEventInfo(list.Data[i], { cipherInfo: false });
events.push({
message: eventInfo.message,
appIcon: eventInfo.appIcon,
appName: eventInfo.appName,
userId: userId,
userName: userId ? (orgUsersUserIdDict[userId] || '-') : '-',
date: list.Data[i].Date,
ip: list.Data[i].IpAddress
});
}
if ($scope.events && $scope.events.length > 0) {
$scope.events = $scope.events.concat(events);
}
else {
$scope.events = events;
}
$scope.loading = false;
});
}
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@@ -2,23 +2,35 @@
.module('bit.organization')
.controller('organizationVaultController', function ($scope, apiService, cipherService, $analytics, $q, $state,
$localStorage, $uibModal, $filter, authService) {
$scope.logins = [];
$localStorage, $uibModal, $filter, authService, $uibModalStack, constants, $timeout) {
$scope.ciphers = [];
$scope.collections = [];
$scope.loading = true;
$scope.useEvents = false;
$scope.constants = constants;
$scope.filter = undefined;
$scope.selectedType = undefined;
$scope.selectedCollection = undefined;
$scope.selectedAll = true;
$scope.selectedTitle = 'All';
$scope.selectedIcon = 'fa-th';
$scope.$on('$viewContentLoaded', function () {
authService.getUserProfile().then(function (profile) {
if (profile.organizations) {
var org = profile.organizations[$state.params.orgId];
$scope.useEvents = !!org.useEvents;
}
});
var collectionPromise = apiService.collections.listOrganization({ orgId: $state.params.orgId }, function (collections) {
var decCollections = [{
id: null,
name: 'Unassigned',
collapsed: $localStorage.collapsedOrgCollections && 'unassigned' in $localStorage.collapsedOrgCollections
name: 'Unassigned'
}];
for (var i = 0; i < collections.Data.length; i++) {
var decCollection = cipherService.decryptCollection(collections.Data[i], null, true);
decCollection.collapsed = $localStorage.collapsedOrgCollections &&
decCollection.id in $localStorage.collapsedOrgCollections;
decCollections.push(decCollection);
}
@@ -27,32 +39,38 @@
var cipherPromise = apiService.ciphers.listOrganizationDetails({ organizationId: $state.params.orgId },
function (ciphers) {
var decLogins = [];
var decCiphers = [];
for (var i = 0; i < ciphers.Data.length; i++) {
if (ciphers.Data[i].Type === 1) {
var decLogin = cipherService.decryptLoginPreview(ciphers.Data[i]);
decLogins.push(decLogin);
}
var decCipher = cipherService.decryptCipherPreview(ciphers.Data[i]);
decCiphers.push(decCipher);
}
$scope.logins = decLogins;
$scope.ciphers = decCiphers;
}).$promise;
$q.all([collectionPromise, cipherPromise]).then(function () {
$scope.loading = false;
});
});
$timeout(function () {
if ($('body').hasClass('control-sidebar-open')) {
$("#search").focus();
}
}, 500);
$scope.filterByCollection = function (collection) {
return function (cipher) {
if (!cipher.collectionIds || !cipher.collectionIds.length) {
return collection.id === null;
if ($state.params.search) {
$uibModalStack.dismissAll();
$scope.searchVaultText = $state.params.search;
}
return cipher.collectionIds.indexOf(collection.id) > -1;
};
};
if ($state.params.viewEvents) {
$uibModalStack.dismissAll();
var cipher = $filter('filter')($scope.ciphers, { id: $state.params.viewEvents });
if (cipher && cipher.length) {
$scope.viewEvents(cipher[0]);
}
}
});
});
$scope.collectionSort = function (item) {
if (!item.id) {
@@ -62,70 +80,60 @@
return item.name.toLowerCase();
};
$scope.collapseExpand = function (collection) {
if (!$localStorage.collapsedOrgCollections) {
$localStorage.collapsedOrgCollections = {};
}
var id = collection.id || 'unassigned';
if (id in $localStorage.collapsedOrgCollections) {
delete $localStorage.collapsedOrgCollections[id];
}
else {
$localStorage.collapsedOrgCollections[id] = true;
}
};
$scope.editLogin = function (login) {
$scope.editCipher = function (cipher) {
var editModel = $uibModal.open({
animation: true,
templateUrl: 'app/vault/views/vaultEditLogin.html',
controller: 'organizationVaultEditLoginController',
templateUrl: 'app/vault/views/vaultEditCipher.html',
controller: 'organizationVaultEditCipherController',
resolve: {
loginId: function () { return login.id; },
cipherId: function () { return cipher.id; },
orgId: function () { return $state.params.orgId; }
}
});
editModel.result.then(function (returnVal) {
var index;
if (returnVal.action === 'edit') {
login.name = returnVal.data.name;
login.username = returnVal.data.username;
index = $scope.ciphers.indexOf(cipher);
if (index > -1) {
returnVal.data.collectionIds = $scope.ciphers[index].collectionIds;
$scope.ciphers[index] = returnVal.data;
}
}
else if (returnVal.action === 'delete') {
var index = $scope.logins.indexOf(login);
index = $scope.ciphers.indexOf(cipher);
if (index > -1) {
$scope.logins.splice(index, 1);
$scope.ciphers.splice(index, 1);
}
}
});
};
$scope.$on('organizationVaultAddLogin', function (event, args) {
$scope.addLogin();
$scope.$on('organizationVaultAddCipher', function (event, args) {
$scope.addCipher();
});
$scope.addLogin = function () {
$scope.addCipher = function () {
var addModel = $uibModal.open({
animation: true,
templateUrl: 'app/vault/views/vaultAddLogin.html',
controller: 'organizationVaultAddLoginController',
templateUrl: 'app/vault/views/vaultAddCipher.html',
controller: 'organizationVaultAddCipherController',
resolve: {
orgId: function () { return $state.params.orgId; }
orgId: function () { return $state.params.orgId; },
selectedType: function () { return $scope.selectedType; }
}
});
addModel.result.then(function (addedLogin) {
$scope.logins.push(addedLogin);
addModel.result.then(function (addedCipher) {
$scope.ciphers.push(addedCipher);
});
};
$scope.editCollections = function (cipher) {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationVaultLoginCollections.html',
controller: 'organizationVaultLoginCollectionsController',
templateUrl: 'app/organization/views/organizationVaultCipherCollections.html',
controller: 'organizationVaultCipherCollectionsController',
resolve: {
cipher: function () { return cipher; },
collections: function () { return $scope.collections; }
@@ -139,9 +147,20 @@
});
};
$scope.attachments = function (login) {
$scope.viewEvents = function (cipher) {
$uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationVaultCipherEvents.html',
controller: 'organizationVaultCipherEventsController',
resolve: {
cipher: function () { return cipher; }
}
});
};
$scope.attachments = function (cipher) {
authService.getUserProfile().then(function (profile) {
return !!profile.organizations[login.organizationId].maxStorageGb;
return !!profile.organizations[cipher.organizationId].maxStorageGb;
}).then(function (useStorage) {
if (!useStorage) {
$uibModal.open({
@@ -149,7 +168,7 @@
templateUrl: 'app/views/paidOrgRequired.html',
controller: 'paidOrgRequiredController',
resolve: {
orgId: function () { return login.organizationId; }
orgId: function () { return cipher.organizationId; }
}
});
return;
@@ -160,49 +179,102 @@
templateUrl: 'app/vault/views/vaultAttachments.html',
controller: 'organizationVaultAttachmentsController',
resolve: {
loginId: function () { return login.id; }
cipherId: function () { return cipher.id; }
}
});
attachmentModel.result.then(function (hasAttachments) {
login.hasAttachments = hasAttachments;
cipher.hasAttachments = hasAttachments;
});
});
};
$scope.removeLogin = function (login, collection) {
if (!confirm('Are you sure you want to remove this login (' + login.name + ') from the ' +
'collection (' + collection.name + ') ?')) {
$scope.deleteCipher = function (cipher) {
if (!confirm('Are you sure you want to delete this item (' + cipher.name + ')?')) {
return;
}
var request = {
collectionIds: []
};
for (var i = 0; i < login.collectionIds.length; i++) {
if (login.collectionIds[i] !== collection.id) {
request.collectionIds.push(login.collectionIds[i]);
apiService.ciphers.delAdmin({ id: cipher.id }, function () {
$analytics.eventTrack('Deleted Cipher');
var index = $scope.ciphers.indexOf(cipher);
if (index > -1) {
$scope.ciphers.splice(index, 1);
}
}
apiService.ciphers.putCollections({ id: login.id }, request).$promise.then(function (response) {
$analytics.eventTrack('Removed Login From Collection');
login.collectionIds = request.collectionIds;
});
};
$scope.deleteLogin = function (login) {
if (!confirm('Are you sure you want to delete this login (' + login.name + ')?')) {
return;
$scope.filterCollection = function (col) {
resetSelected();
$scope.selectedCollection = col;
$scope.selectedIcon = 'fa-cube';
if (col.id) {
$scope.filter = function (c) {
return c.collectionIds && c.collectionIds.indexOf(col.id) > -1;
};
}
else {
$scope.filter = function (c) {
return !c.collectionIds || c.collectionIds.length === 0;
};
}
fixLayout();
};
apiService.ciphers.delAdmin({ id: login.id }, function () {
$analytics.eventTrack('Deleted Login');
var index = $scope.logins.indexOf(login);
if (index > -1) {
$scope.logins.splice(index, 1);
}
});
$scope.filterType = function (t) {
resetSelected();
$scope.selectedType = t;
switch (t) {
case constants.cipherType.login:
$scope.selectedTitle = 'Login';
$scope.selectedIcon = 'fa-globe';
break;
case constants.cipherType.card:
$scope.selectedTitle = 'Card';
$scope.selectedIcon = 'fa-credit-card';
break;
case constants.cipherType.identity:
$scope.selectedTitle = 'Identity';
$scope.selectedIcon = 'fa-id-card-o';
break;
case constants.cipherType.secureNote:
$scope.selectedTitle = 'Secure Note';
$scope.selectedIcon = 'fa-sticky-note-o';
break;
default:
break;
}
$scope.filter = function (c) {
return c.type === t;
};
fixLayout();
};
$scope.filterAll = function () {
resetSelected();
$scope.selectedAll = true;
$scope.selectedTitle = 'All';
$scope.selectedIcon = 'fa-th';
$scope.filter = null;
fixLayout();
};
function resetSelected() {
$scope.selectedCollection = undefined;
$scope.selectedType = undefined;
$scope.selectedAll = false;
}
function fixLayout() {
if ($.AdminLTE && $.AdminLTE.layout) {
$timeout(function () {
$.AdminLTE.layout.fix();
}, 0);
}
}
$scope.cipherFilter = function () {
return function (cipher) {
return !$scope.filter || $scope.filter(cipher);
};
};
});

View File

@@ -0,0 +1,148 @@
angular
.module('bit.organization')
.controller('organizationVaultEditCipherController', function ($scope, apiService, $uibModalInstance, cryptoService,
cipherService, passwordService, cipherId, $analytics, orgId, $uibModal, constants) {
$analytics.eventTrack('organizationVaultEditCipherController', { category: 'Modal' });
$scope.cipher = {};
$scope.hideFolders = $scope.hideFavorite = $scope.fromOrg = true;
$scope.constants = constants;
apiService.ciphers.getAdmin({ id: cipherId }, function (cipher) {
$scope.cipher = cipherService.decryptCipher(cipher);
$scope.useTotp = $scope.cipher.organizationUseTotp;
setUriMatchValues();
});
$scope.save = function (model) {
var cipher = cipherService.encryptCipher(model, $scope.cipher.type);
$scope.savePromise = apiService.ciphers.putAdmin({ id: cipherId }, cipher, function (cipherResponse) {
$analytics.eventTrack('Edited Organization Cipher');
var decCipher = cipherService.decryptCipherPreview(cipherResponse);
$uibModalInstance.close({
action: 'edit',
data: decCipher
});
}).$promise;
};
$scope.generatePassword = function () {
if (!$scope.cipher.login.password || confirm('Are you sure you want to overwrite the current password?')) {
$analytics.eventTrack('Generated Password From Edit');
$scope.cipher.login.password = passwordService.generatePassword({ length: 14, special: true });
}
};
$scope.addUri = function () {
if (!$scope.cipher.login) {
return;
}
if (!$scope.cipher.login.uris) {
$scope.cipher.login.uris = [];
}
$scope.cipher.login.uris.push({
uri: null,
match: null,
matchValue: null
});
};
$scope.removeUri = function (uri) {
if (!$scope.cipher.login || !$scope.cipher.login.uris) {
return;
}
var index = $scope.cipher.login.uris.indexOf(uri);
if (index > -1) {
$scope.cipher.login.uris.splice(index, 1);
}
};
$scope.uriMatchChanged = function (uri) {
if ((!uri.matchValue && uri.matchValue !== 0) || uri.matchValue === '') {
uri.match = null;
}
else {
uri.match = parseInt(uri.matchValue);
}
};
$scope.addField = function () {
if (!$scope.cipher.login.fields) {
$scope.cipher.login.fields = [];
}
$scope.cipher.fields.push({
type: constants.fieldType.text.toString(),
name: null,
value: null
});
};
$scope.removeField = function (field) {
var index = $scope.cipher.fields.indexOf(field);
if (index > -1) {
$scope.cipher.fields.splice(index, 1);
}
};
$scope.clipboardSuccess = function (e) {
e.clearSelection();
selectPassword(e);
};
$scope.clipboardError = function (e, password) {
if (password) {
selectPassword(e);
}
alert('Your web browser does not support easy clipboard copying. Copy it manually instead.');
};
function selectPassword(e) {
var target = $(e.trigger).parent().prev();
if (target.attr('type') === 'text') {
target.select();
}
}
$scope.delete = function () {
if (!confirm('Are you sure you want to delete this item (' + $scope.cipher.name + ')?')) {
return;
}
apiService.ciphers.delAdmin({ id: $scope.cipher.id }, function () {
$analytics.eventTrack('Deleted Organization Cipher From Edit');
$uibModalInstance.close({
action: 'delete',
data: $scope.cipher.id
});
});
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
$scope.showUpgrade = function () {
$uibModal.open({
animation: true,
templateUrl: 'app/views/paidOrgRequired.html',
controller: 'paidOrgRequiredController',
resolve: {
orgId: function () { return orgId; }
}
});
};
function setUriMatchValues() {
if ($scope.cipher.login && $scope.cipher.login.uris) {
for (var i = 0; i < $scope.cipher.login.uris.length; i++) {
$scope.cipher.login.uris[i].matchValue =
$scope.cipher.login.uris[i].match || $scope.cipher.login.uris[i].match === 0 ?
$scope.cipher.login.uris[i].match.toString() : '';
}
}
}
});

View File

@@ -1,81 +0,0 @@
angular
.module('bit.organization')
.controller('organizationVaultEditLoginController', function ($scope, apiService, $uibModalInstance, cryptoService,
cipherService, passwordService, loginId, $analytics, orgId, $uibModal) {
$analytics.eventTrack('organizationVaultEditLoginController', { category: 'Modal' });
$scope.login = {};
$scope.hideFolders = $scope.hideFavorite = $scope.fromOrg = true;
apiService.logins.getAdmin({ id: loginId }, function (login) {
$scope.login = cipherService.decryptLogin(login);
$scope.useTotp = $scope.login.organizationUseTotp;
});
$scope.save = function (model) {
var login = cipherService.encryptLogin(model);
$scope.savePromise = apiService.logins.putAdmin({ id: loginId }, login, function (loginResponse) {
$analytics.eventTrack('Edited Organization Login');
var decLogin = cipherService.decryptLogin(loginResponse);
$uibModalInstance.close({
action: 'edit',
data: decLogin
});
}).$promise;
};
$scope.generatePassword = function () {
if (!$scope.login.password || confirm('Are you sure you want to overwrite the current password?')) {
$analytics.eventTrack('Generated Password From Edit');
$scope.login.password = passwordService.generatePassword({ length: 12, special: true });
}
};
$scope.clipboardSuccess = function (e) {
e.clearSelection();
selectPassword(e);
};
$scope.clipboardError = function (e, password) {
if (password) {
selectPassword(e);
}
alert('Your web browser does not support easy clipboard copying. Copy it manually instead.');
};
function selectPassword(e) {
var target = $(e.trigger).parent().prev();
if (target.attr('type') === 'text') {
target.select();
}
}
$scope.delete = function () {
if (!confirm('Are you sure you want to delete this login (' + $scope.login.name + ')?')) {
return;
}
apiService.ciphers.delAdmin({ id: $scope.login.id }, function () {
$analytics.eventTrack('Deleted Organization Login From Edit');
$uibModalInstance.close({
action: 'delete',
data: $scope.login.id
});
});
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
$scope.showUpgrade = function () {
$uibModal.open({
animation: true,
templateUrl: 'app/views/paidOrgRequired.html',
controller: 'paidOrgRequiredController',
resolve: {
orgId: function () { return orgId; }
}
});
};
});

View File

@@ -116,10 +116,10 @@
Loading...
</div>
<div ng-show="!loading">
You plan currently has a total of <b>{{plan.seats}}</b> seats.
Your plan currently has a total of <b>{{plan.seats}}</b> seats.
</div>
</div>
<div class="box-footer" ng-if="!selfHosted && !noSubscription">
<div class="box-footer" ng-if="!selfHosted && !noSubscription && canAdjustSeats">
<button type="button" class="btn btn-default btn-flat" ng-click="adjustSeats(true)">
Add Seats
</button>
@@ -134,7 +134,7 @@
</div>
<div class="box-body">
<p>
You plan has a total of {{storage.maxGb}} GB of encrypted file storage.
Your plan has a total of {{storage.maxGb}} GB of encrypted file storage.
You are currently using {{storage.currentName}}.
</p>
<div class="progress" style="margin: 0;">
@@ -201,6 +201,11 @@
<table class="table">
<tbody>
<tr ng-repeat="charge in charges">
<td style="width: 30px">
<a href="#" stop-click ng-click="viewInvoice(charge)" title="Invoice">
<i class="fa fa-file-pdf-o"></i>
</a>
</td>
<td style="width: 200px">
{{charge.date | date: 'mediumDate'}}
</td>

View File

@@ -5,7 +5,7 @@
{{add ? 'Add Seats' : 'Remove Seats'}}
</h4>
</div>
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise">
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise" autocomplete="off">
<div class="modal-body">
<div class="callout callout-default" ng-show="add">
<h4><i class="fa fa-dollar"></i> Note About Charges</h4>

View File

@@ -2,7 +2,7 @@
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><i class="fa fa-file-text-o"></i> Change Plan</h4>
</div>
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise">
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise" autocomplete="off">
<div class="modal-body">
You can <a href="https://bitwarden.com/contact/" target="_blank">contact us</a>
if you would like to change your plan. Please ensure that you have an active payment

View File

@@ -5,7 +5,7 @@
Verify Bank Account
</h4>
</div>
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise">
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise" autocomplete="off">
<div class="modal-body">
<p>
Enter the two micro-deposit amounts from your bank account. Both amounts will be less than $1.00 each.

View File

@@ -10,7 +10,7 @@
&nbsp;
<div class="box-filters hidden-xs">
<div class="form-group form-group-sm has-feedback has-feedback-left">
<input type="text" id="search" class="form-control" placeholder="Search collections..."
<input type="text" id="filterSearch" class="form-control" placeholder="Search collections..."
style="width: 200px;" ng-model="filterSearch">
<span class="fa fa-search form-control-feedback text-muted" aria-hidden="true"></span>
</div>

View File

@@ -2,7 +2,7 @@
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><i class="fa fa-cubes"></i> Add New Collection</h4>
</div>
<form name="form" ng-submit="form.$valid && submit(model)" api-form="submitPromise">
<form name="form" ng-submit="form.$valid && submit(model)" api-form="submitPromise" autocomplete="off">
<div class="modal-body">
<div class="callout callout-default">
<h4><i class="fa fa-info-circle"></i> Note</h4>

View File

@@ -2,7 +2,7 @@
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><i class="fa fa-cubes"></i> Edit Collection</h4>
</div>
<form name="form" ng-submit="form.$valid && submit(collection)" api-form="submitPromise">
<form name="form" ng-submit="form.$valid && submit(collection)" api-form="submitPromise" autocomplete="off">
<div class="modal-body">
<div class="callout callout-default">
<h4><i class="fa fa-info-circle"></i> Note</h4>

View File

@@ -7,10 +7,12 @@
<section class="content">
<div class="callout callout-warning" ng-if="!orgProfile.enabled">
<h4><i class="fa fa-warning"></i> Organization Disabled</h4>
<p>
This organization is currently disabled. Users will not see your shared logins or collections.
Contact us if you would like to reinstate this organization.
</p>
<p>This organization is currently disabled. Users will not see your shared logins or collections.</p>
<p ng-if="!selfHosted">Contact us if you would like to reinstate this organization.</p>
<p ng-if="selfHosted">Update your license to reinstate this organization.</p>
<a ng-if="selfHosted" class="btn btn-default btn-flat" href="#" stop-click ng-click="goBilling()">
Billing &amp; Licensing
</a>
<a class="btn btn-default btn-flat" href="https://bitwarden.com/contact/" target="_blank">
Contact Us
</a>
@@ -20,7 +22,7 @@
<h3 class="box-title">Let's Get Started!</h3>
</div>
<div class="box-body">
<p>Dashboard features are coming soon. Get started by inviting users and creating your collections.</p>
<p>Get started by inviting users and creating your collections.</p>
<a class="btn btn-default btn-flat" ui-sref="backend.org.people({orgId: orgProfile.id})">
Invite Users
</a>

View File

@@ -0,0 +1,67 @@
<section class="content-header">
<h1>
Events
<small>audit your organization</small>
</h1>
</section>
<section class="content">
<div class="box">
<div class="box-header with-border">
&nbsp;
<div class="box-filters hidden-xs hidden-sm">
<input type="datetime-local" ng-model="filterStart" required
class="form-control input-sm" style="width:initial;" />
-
<input type="datetime-local" ng-model="filterEnd" required
class="form-control input-sm" style="width:initial;" />
</div>
<div class="box-tools">
<button type="button" class="btn btn-primary btn-sm btn-flat" ng-click="refresh()">
<i class="fa fa-fw fa-refresh" ng-class="{'fa-spin': loading}"></i> Refresh
</button>
</div>
</div>
<div class="box-body" ng-class="{'no-padding': filteredEvents.length}">
<div ng-show="loading && !events.length">
Loading...
</div>
<div ng-show="!loading && !events.length">
<p>There are no events to list.</p>
</div>
<div class="table-responsive" ng-show="events.length">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Timestamp</th>
<th><span class="sr-only">App</span></th>
<th>User</th>
<th>Event</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="event in filteredEvents = (events)">
<td style="width: 210px; min-width: 100px;">
{{event.date | date:'medium'}}
</td>
<td style="width: 20px;" class="text-center">
<i class="text-muted fa fa-lg {{event.appIcon}}" title="{{event.appName}}, {{event.ip}}"></i>
</td>
<td style="width: 150px; min-width: 100px;">
{{event.userName}}
</td>
<td>
<div ng-bind-html="event.message"></div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="box-footer text-center" ng-show="continuationToken">
<button class="btn btn-link btn-block" ng-click="next()" ng-if="!loading">
Load more...
</button>
<i class="fa fa-fw fa-refresh fa-spin text-muted" ng-if="loading"></i>
</div>
</div>
</section>

View File

@@ -10,7 +10,7 @@
&nbsp;
<div class="box-filters hidden-xs">
<div class="form-group form-group-sm has-feedback has-feedback-left">
<input type="text" id="search" class="form-control" placeholder="Search groups..."
<input type="text" id="filterSearch" class="form-control" placeholder="Search groups..."
style="width: 200px;" ng-model="filterSearch">
<span class="fa fa-search form-control-feedback text-muted" aria-hidden="true"></span>
</div>

View File

@@ -2,7 +2,7 @@
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><i class="fa fa-sitemap"></i> Add New Group</h4>
</div>
<form name="form" ng-submit="form.$valid && submit(model)" api-form="submitPromise">
<form name="form" ng-submit="form.$valid && submit(model)" api-form="submitPromise" autocomplete="off">
<div class="modal-body">
<div class="callout callout-default">
<h4><i class="fa fa-info-circle"></i> Note</h4>

View File

@@ -2,7 +2,7 @@
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><i class="fa fa-sitemap"></i> Edit Group</h4>
</div>
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise">
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise" autocomplete="off">
<div class="modal-body">
<div class="callout callout-default">
<h4><i class="fa fa-info-circle"></i> Note</h4>

View File

@@ -10,7 +10,7 @@
&nbsp;
<div class="box-filters hidden-xs">
<div class="form-group form-group-sm has-feedback has-feedback-left">
<input type="text" id="search" class="form-control" placeholder="Search people..."
<input type="text" id="filterSearch" class="form-control" placeholder="Search people..."
style="width: 200px;" ng-model="filterSearch">
<span class="fa fa-search form-control-feedback text-muted" aria-hidden="true"></span>
</div>
@@ -46,6 +46,12 @@
<i class="fa fa-fw fa-sitemap"></i> Groups
</a>
</li>
<li>
<a href="#" stop-click ng-click="events(user)"
ng-if="useEvents && user.status === 2">
<i class="fa fa-fw fa-file-text-o"></i> Event Logs
</a>
</li>
<li ng-show="user.status === 1">
<a href="#" stop-click ng-click="confirm(user)">
<i class="fa fa-fw fa-check"></i> Confirm

View File

@@ -2,7 +2,7 @@
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><i class="fa fa-user"></i> Edit User <small>{{email}}</small></h4>
</div>
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise">
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise" autocomplete="off">
<div class="modal-body">
<div class="callout callout-danger validation-errors" ng-show="form.$errors">
<h4>Errors have occurred</h4>

View File

@@ -0,0 +1,56 @@
<div class="modal-header">
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><i class="fa fa-file-text-o"></i> User Event Logs <small>{{email}}</small></h4>
</div>
<div class="modal-body">
<div class="hidden-xs">
<input type="datetime-local" ng-model="filterStart" required
class="form-control input-sm" style="width:initial; display: inline;" />
-
<input type="datetime-local" ng-model="filterEnd" required
class="form-control input-sm" style="width:initial; display: inline;" />
<button type="button" class="btn btn-primary btn-sm btn-flat" ng-click="refresh()">
<i class="fa fa-fw fa-refresh" ng-class="{'fa-spin': loading}"></i> Refresh
</button>
<hr />
</div>
<div ng-show="loading && !events.length">
Loading...
</div>
<div ng-show="!loading && !events.length">
<p>There are no events to list.</p>
</div>
<div class="table-responsive" ng-show="events.length" style="margin: 0;">
<table class="table table-striped table-hover" style="{{ !continuationToken ? 'margin: 0;' : '' }}">
<thead>
<tr>
<th>Timestamp</th>
<th><span class="sr-only">App</span></th>
<th>Event</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="event in filteredEvents = (events)">
<td style="width: 210px; min-width: 100px;">
{{event.date | date:'medium'}}
</td>
<td style="width: 20px;" class="text-center">
<i class="text-muted fa fa-lg {{event.appIcon}}" title="{{event.appName}}, {{event.ip}}"></i>
</td>
<td>
<div ng-bind-html="event.message"></div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="text-center" ng-show="continuationToken">
<button class="btn btn-link btn-block" ng-click="next()" ng-if="!loading">
Load more...
</button>
<i class="fa fa-fw fa-refresh fa-spin text-muted" ng-if="loading"></i>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
</div>

View File

@@ -2,7 +2,7 @@
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><i class="fa fa-sitemap"></i> Edit User Groups <small>{{orgUser.email}}</small></h4>
</div>
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise">
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise" autocomplete="off">
<div class="modal-body">
<div class="callout callout-danger validation-errors" ng-show="form.$errors">
<h4>Errors have occurred</h4>

View File

@@ -2,11 +2,11 @@
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><i class="fa fa-user"></i> Invite User</h4>
</div>
<form name="inviteForm" ng-submit="inviteForm.$valid && submit(model)" api-form="submitPromise">
<form name="inviteForm" ng-submit="inviteForm.$valid && submit(model)" api-form="submitPromise" autocomplete="off">
<div class="modal-body">
<p>
Invite a new user to your organization by entering their bitwarden account email address below. If they do not have
a bitwarden account already, they will be prompted to create a new account.
Invite a new user to your organization by entering their Bitwarden account email address below. If they do not have
a Bitwarden account already, they will be prompted to create a new account.
</p>
<div class="callout callout-danger validation-errors" ng-show="inviteForm.$errors">
<h4>Errors have occurred</h4>

View File

@@ -9,7 +9,8 @@
<div class="box-header with-border">
<h3 class="box-title">General</h3>
</div>
<form role="form" name="generalForm" ng-submit="generalForm.$valid && generalSave()" api-form="generalPromise">
<form role="form" name="generalForm" ng-submit="generalForm.$valid && generalSave()" api-form="generalPromise"
autocomplete="off">
<div class="box-body">
<div class="row">
<div class="col-sm-9">
@@ -24,15 +25,28 @@
<input type="text" id="name" name="Name" ng-model="model.name" class="form-control"
required api-field ng-readonly="selfHosted" />
</div>
<div class="form-group" show-errors>
<label for="name">Billing Email</label>
<input type="email" id="billingEmail" name="BillingEmail" ng-model="model.billingEmail"
class="form-control" required api-field ng-readonly="selfHosted" />
</div>
<div class="form-group" show-errors>
<label for="name">Business Name</label>
<input type="text" id="businessName" name="BusinessName" ng-model="model.businessName"
class="form-control" api-field ng-readonly="selfHosted" />
</div>
<div class="form-group" show-errors>
<label for="name">Billing Email</label>
<input type="email" id="billingEmail" name="BillingEmail" ng-model="model.billingEmail"
class="form-control" required api-field ng-readonly="selfHosted" />
<div ng-if="!selfHosted">
<hr />
<strong>Tax Information</strong>
<div>{{model.businessAddress1}}</div>
<div>{{model.businessAddress2}}</div>
<div>{{model.businessAddress3}}</div>
<div>{{model.businessCountry}}</div>
<div>{{model.businessTaxNumber}}</div>
<p class="help-block">
Please <a href="https://bitwarden.com/contact/" target="_blank">contact support</a>
to provide (or update) tax information for your invoices.
</p>
</div>
</div>
<div class="col-sm-3 settings-photo">
@@ -49,6 +63,51 @@
</div>
</form>
</div>
<div class="box box-default" ng-if="use2fa">
<div class="box-header with-border">
<h3 class="box-title">Two-step Login Providers</h3>
</div>
<div class="box-body no-padding">
<div class="table-responsive">
<table class="table table-striped table-hover table-vmiddle">
<tbody>
<tr ng-repeat="provider in twoStepProviders | orderBy: 'displayOrder'">
<td style="width: 120px; height: 75px;" align="center">
<a href="#" stop-click ng-click="edit(provider)">
<img alt="{{::provider.name}}" ng-src="{{'images/two-factor/' + provider.image}}" />
</a>
</td>
<td>
<a href="#" stop-click ng-click="edit(provider)">{{::provider.name}}</a>
<div class="text-muted text-sm">{{::provider.description}}</div>
</td>
<td style="width: 100px;" class="text-right">
<span class="label label-full"
ng-class="{ 'label-success': provider.enabled, 'label-default': !provider.enabled }">
{{provider.enabled ? 'Enabled' : 'Disabled'}}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Import/Export</h3>
</div>
<div class="box-body">
<p>
Quickly import logins, collections, and other data. You can also export all of your organization's
vault data in <code>.csv</code> format.
</p>
</div>
<div class="box-footer">
<button class="btn btn-default btn-flat" type="button" ng-click="import()">Import Data</button>
<button class="btn btn-default btn-flat" type="button" ng-click="export()">Export Data</button>
</div>
</div>
<div class="box box-danger">
<div class="box-header with-border">
<h3 class="box-title">Danger Zone</h3>

View File

@@ -5,36 +5,27 @@
<span ng-pluralize
count="collections.length > 0 ? collections.length - 1 : 0"
when="{'1': '{} collection', 'other': '{} collections'}"></span>,
<span ng-pluralize count="logins.length" when="{'1': '{} login', 'other': '{} logins'}"></span>
<span ng-pluralize count="ciphers.length" when="{'1': '{} item', 'other': '{} items'}"></span>
</small>
</h1>
</section>
<section class="content">
<p ng-show="loading && !collections.length">Loading...</p>
<div class="box" ng-class="{'collapsed-box': collection.collapsed}" ng-repeat="collection in collections |
orderBy: collectionSort track by collection.id"
ng-show="collections.length && (!main.searchVaultText || collectionLogins.length)">
<p ng-show="loading">Loading...</p>
<div class="box" ng-show="!loading">
<div class="box-header with-border">
<h3 class="box-title">
<i class="fa" ng-class="{'fa-cubes': collection.id, 'fa-sitemap': !collection.id}"></i>
{{collection.name}}
<small ng-pluralize count="collectionLogins.length" when="{'1': '{} login', 'other': '{} logins'}"></small>
<i class="fa {{selectedIcon}}"></i>
{{selectedCollection ? selectedCollection.name : selectedTitle}}
<small ng-pluralize count="filteredCiphers.length" when="{'1': '{} item', 'other': '{} items'}"></small>
</h3>
<div class="box-tools">
<button type="button" class="btn btn-box-tool" data-widget="collapse" title="Collapse/Expand"
ng-click="collapseExpand(collection)">
<i class="fa" ng-class="{'fa-minus': !collection.collapsed, 'fa-plus': collection.collapsed}"></i>
</button>
</div>
</div>
<div class="box-body" ng-class="{'no-padding': collectionLogins.length}">
<div ng-show="!collectionLogins.length && collection.id">No logins in this collection.</div>
<div ng-show="!collectionLogins.length && !collection.id">No unassigned logins.</div>
<div class="table-responsive" ng-show="collectionLogins.length">
<div class="box-body" ng-class="{'no-padding': filteredCiphers.length}">
<div ng-show="!filteredCiphers.length">No items to list.</div>
<div class="table-responsive" ng-show="filteredCiphers.length">
<table class="table table-striped table-hover table-vmiddle">
<tbody>
<tr ng-repeat="login in collectionLogins = (logins | filter: filterByCollection(collection) |
filter: (main.searchVaultText || '') | orderBy: ['name', 'username']) track by login.id">
<tr ng-repeat="cipher in filteredCiphers = (ciphers | filter: cipherFilter() |
filter: (searchVaultText || '') | orderBy: ['name', 'subTitle']) track by cipher.id">
<td style="width: 70px;">
<div class="btn-group" data-append-to="body">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
@@ -42,37 +33,43 @@
</button>
<ul class="dropdown-menu">
<li>
<a href="#" stop-click ng-click="editLogin(login)">
<a href="#" stop-click ng-click="editCipher(cipher)">
<i class="fa fa-fw fa-pencil"></i> Edit
</a>
</li>
<li>
<a href="#" stop-click ng-click="attachments(login)">
<a href="#" stop-click ng-click="attachments(cipher)">
<i class="fa fa-fw fa-paperclip"></i> Attachments
</a>
</li>
<li>
<a href="#" stop-click ng-click="editCollections(login)">
<a href="#" stop-click ng-click="editCollections(cipher)">
<i class="fa fa-fw fa-cubes"></i> Collections
</a>
</li>
<li>
<a href="#" stop-click ng-click="removeLogin(login, collection)" class="text-red"
ng-if="collection.id">
<i class="fa fa-fw fa-remove"></i> Remove
<a href="#" stop-click ng-click="viewEvents(cipher)" ng-if="useEvents">
<i class="fa fa-fw fa-file-text-o"></i> Event Logs
</a>
</li>
<li>
<a href="#" stop-click ng-click="deleteLogin(login)" class="text-red">
<a href="#" stop-click ng-click="deleteCipher(cipher)" class="text-red">
<i class="fa fa-fw fa-trash"></i> Delete
</a>
</li>
</ul>
</div>
</td>
<td class="vault-icon">
<i class="fa fa-fw fa-lg {{::cipher.icon}}" ng-if="!cipher.meta.image"></i>
<img alt="" ng-if="cipher.meta.image" ng-src="{{cipher.meta.image}}"
fallback-src="images/fa-globe.png" />
</td>
<td>
<a href="#" stop-click ng-click="editLogin(login)">{{login.name}}</a>
<div class="text-sm text-muted">{{login.username}}</div>
<a href="#" stop-click ng-click="editCipher(cipher)">{{cipher.name}}</a>
<i class="fa fa-paperclip text-muted" title="Attachments" ng-if="cipher.hasAttachments"
stop-prop></i>
<div class="text-sm text-muted">{{cipher.subTitle}}</div>
</td>
</tr>
</tbody>
@@ -81,3 +78,65 @@
</div>
</div>
</section>
<aside class="control-sidebar control-sidebar-light">
<div class="tab-content">
<form class="search-form">
<label for="search" class="sr-only">Search</label>
<div class="form-group has-feedback">
<input type="search" id="search" class="form-control" placeholder="Search org vault..."
ng-model="searchVaultText" />
<span class="fa fa-search form-control-feedback" aria-hidden="true"></span>
</div>
</form>
<ul class="control-sidebar-menu">
<li ng-class="{active: selectedAll}">
<a href="#" stop-click ng-click="filterAll()">
<i class="fa fa-th fa-fw"></i> All Items
</a>
</li>
</ul>
<h3 class="control-sidebar-heading">Types</h3>
<div class="control-sidebar-section">
<ul class="control-sidebar-menu">
<li ng-class="{active: constants.cipherType.login === selectedType}">
<a href="#" stop-click ng-click="filterType(constants.cipherType.login)">
<i class="fa fa-globe fa-fw"></i> Login
</a>
</li>
<li ng-class="{active: constants.cipherType.card === selectedType}">
<a href="#" stop-click ng-click="filterType(constants.cipherType.card)">
<i class="fa fa-credit-card fa-fw"></i> Card
</a>
</li>
<li ng-class="{active: constants.cipherType.identity === selectedType}">
<a href="#" stop-click ng-click="filterType(constants.cipherType.identity)">
<i class="fa fa-id-card-o fa-fw"></i> Identity
</a>
</li>
<li ng-class="{active: constants.cipherType.secureNote === selectedType}">
<a href="#" stop-click ng-click="filterType(constants.cipherType.secureNote)">
<i class="fa fa-sticky-note-o fa-fw"></i> Secure Note
</a>
</li>
</ul>
</div>
<h3 class="control-sidebar-heading">Collections</h3>
<div ng-show="loading && !collections.length">
<p>Loading...</p>
</div>
<div ng-show="!loading && !collections.length">
<p>No collections.</p>
</div>
<div class="control-sidebar-section" ng-show="!loading && collections.length">
<ul class="control-sidebar-menu">
<li ng-repeat="collection in collections | orderBy: [collectionSort] track by collection.id"
ng-class="{active: selectedCollection && collection.id === selectedCollection.id}">
<a href="#" stop-click ng-click="filterCollection(collection)">
<i class="fa fa-caret-right fa-fw"></i>
{{collection.name}}
</a>
</li>
</ul>
</div>
</div>
</aside>

View File

@@ -2,9 +2,9 @@
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><i class="fa fa-cubes"></i> Collections <small>{{cipher.name}}</small></h4>
</div>
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise">
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise" autocomplete="off">
<div class="modal-body">
<p>Edit the collections that this login is being shared with.</p>
<p>Edit the collections that this item is being shared with.</p>
<div class="callout callout-danger validation-errors" ng-show="form.$errors">
<h4>Errors have occurred</h4>
<ul>

View File

@@ -0,0 +1,60 @@
<div class="modal-header">
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><i class="fa fa-file-text-o"></i> Event Logs <small>{{cipher.name}}</small></h4>
</div>
<div class="modal-body">
<div class="hidden-xs">
<input type="datetime-local" ng-model="filterStart" required
class="form-control input-sm" style="width:initial; display: inline;" />
-
<input type="datetime-local" ng-model="filterEnd" required
class="form-control input-sm" style="width:initial; display: inline;" />
<button type="button" class="btn btn-primary btn-sm btn-flat" ng-click="refresh()">
<i class="fa fa-fw fa-refresh" ng-class="{'fa-spin': loading}"></i> Refresh
</button>
<hr />
</div>
<div ng-show="loading && !events.length">
Loading...
</div>
<div ng-show="!loading && !events.length">
<p>There are no events to list.</p>
</div>
<div class="table-responsive" ng-show="events.length" style="margin: 0;">
<table class="table table-striped table-hover" style="{{ !continuationToken ? 'margin: 0;' : '' }}">
<thead>
<tr>
<th>Timestamp</th>
<th><span class="sr-only">App</span></th>
<th>User</th>
<th>Event</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="event in filteredEvents = (events)">
<td style="width: 210px; min-width: 100px;">
{{event.date | date:'medium'}}
</td>
<td style="width: 20px;" class="text-center">
<i class="text-muted fa fa-lg {{event.appIcon}}" title="{{event.appName}}, {{event.ip}}"></i>
</td>
<td style="width: 150px; min-width: 100px;">
{{event.userName}}
</td>
<td>
{{event.message}}
</td>
</tr>
</tbody>
</table>
</div>
<div class="text-center" ng-show="continuationToken">
<button class="btn btn-link btn-block" ng-click="next()" ng-if="!loading">
Load more...
</button>
<i class="fa fa-fw fa-refresh fa-spin text-muted" ng-if="loading"></i>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
</div>

View File

@@ -1,22 +1,11 @@
angular
.module('bit.services')
.factory('apiService', function ($resource, tokenService, appSettings, $httpParamSerializer) {
.factory('apiService', function ($resource, tokenService, appSettings, $httpParamSerializer, utilsService) {
var _service = {},
_apiUri = appSettings.apiUri,
_identityUri = appSettings.identityUri;
_service.logins = $resource(_apiUri + '/logins/:id', {}, {
get: { method: 'GET', params: { id: '@id' } },
getAdmin: { url: _apiUri + '/logins/:id/admin', method: 'GET', params: { id: '@id' } },
list: { method: 'GET', params: {} },
post: { method: 'POST', params: {} },
postAdmin: { url: _apiUri + '/logins/admin', method: 'POST', params: {} },
put: { method: 'POST', params: { id: '@id' } },
putAdmin: { url: _apiUri + '/logins/:id/admin', method: 'POST', params: { id: '@id' } },
del: { url: _apiUri + '/logins/:id/delete', method: 'POST', params: { id: '@id' } }
});
_service.folders = $resource(_apiUri + '/folders/:id', {}, {
get: { method: 'GET', params: { id: '@id' } },
list: { method: 'GET', params: {} },
@@ -27,12 +16,16 @@
_service.ciphers = $resource(_apiUri + '/ciphers/:id', {}, {
get: { method: 'GET', params: { id: '@id' } },
getAdmin: { url: _apiUri + '/ciphers/:id/admin', method: 'GET', params: { id: '@id' } },
getDetails: { url: _apiUri + '/ciphers/:id/details', method: 'GET', params: { id: '@id' } },
list: { method: 'GET', params: { includeFolders: false, includeShared: true } },
listDetails: { url: _apiUri + '/ciphers/details', method: 'GET', params: {} },
list: { method: 'GET', params: {} },
listOrganizationDetails: { url: _apiUri + '/ciphers/organization-details', method: 'GET', params: {} },
post: { method: 'POST', params: {} },
postAdmin: { url: _apiUri + '/ciphers/admin', method: 'POST', params: {} },
put: { method: 'POST', params: { id: '@id' } },
putAdmin: { url: _apiUri + '/ciphers/:id/admin', method: 'POST', params: { id: '@id' } },
'import': { url: _apiUri + '/ciphers/import', method: 'POST', params: {} },
favorite: { url: _apiUri + '/ciphers/:id/favorite', method: 'POST', params: { id: '@id' } },
importOrg: { url: _apiUri + '/ciphers/import-organization?organizationId=:orgId', method: 'POST', params: { orgId: '@orgId' } },
putPartial: { url: _apiUri + '/ciphers/:id/partial', method: 'POST', params: { id: '@id' } },
putShare: { url: _apiUri + '/ciphers/:id/share', method: 'POST', params: { id: '@id' } },
putCollections: { url: _apiUri + '/ciphers/:id/collections', method: 'POST', params: { id: '@id' } },
@@ -41,19 +34,27 @@
delAdmin: { url: _apiUri + '/ciphers/:id/delete-admin', method: 'POST', params: { id: '@id' } },
delMany: { url: _apiUri + '/ciphers/delete', method: 'POST' },
moveMany: { url: _apiUri + '/ciphers/move', method: 'POST' },
purge: { url: _apiUri + '/ciphers/purge', method: 'POST' },
postAttachment: {
url: _apiUri + '/ciphers/:id/attachment',
method: 'POST',
headers: { 'Content-Type': undefined },
params: { id: '@id' }
},
postAttachmentAdmin: {
url: _apiUri + '/ciphers/:id/attachment-admin',
method: 'POST',
headers: { 'Content-Type': undefined },
params: { id: '@id' }
},
postShareAttachment: {
url: _apiUri + '/ciphers/:id/attachment/:attachmentId/share?organizationId=:orgId',
method: 'POST',
headers: { 'Content-Type': undefined },
params: { id: '@id', attachmentId: '@attachmentId', orgId: '@orgId' }
},
delAttachment: { url: _apiUri + '/ciphers/:id/attachment/:attachmentId/delete', method: 'POST', params: { id: '@id', attachmentId: '@attachmentId' } }
delAttachment: { url: _apiUri + '/ciphers/:id/attachment/:attachmentId/delete', method: 'POST', params: { id: '@id', attachmentId: '@attachmentId' } },
delAttachmentAdmin: { url: _apiUri + '/ciphers/:id/attachment/:attachmentId/delete-admin', method: 'POST', params: { id: '@id', attachmentId: '@attachmentId' } }
});
_service.organizations = $resource(_apiUri + '/organizations/:id', {}, {
@@ -158,9 +159,11 @@
_service.twoFactor = $resource(_apiUri + '/two-factor', {}, {
list: { method: 'GET', params: {} },
listOrganization: { url: _apiUri + '/organizations/:orgId/two-factor', method: 'GET', params: { orgId: '@orgId' } },
getEmail: { url: _apiUri + '/two-factor/get-email', method: 'POST', params: {} },
getU2f: { url: _apiUri + '/two-factor/get-u2f', method: 'POST', params: {} },
getDuo: { url: _apiUri + '/two-factor/get-duo', method: 'POST', params: {} },
getOrganizationDuo: { url: _apiUri + '/organizations/:orgId/two-factor/get-duo', method: 'POST', params: { orgId: '@orgId' } },
getAuthenticator: { url: _apiUri + '/two-factor/get-authenticator', method: 'POST', params: {} },
getYubi: { url: _apiUri + '/two-factor/get-yubikey', method: 'POST', params: {} },
sendEmail: { url: _apiUri + '/two-factor/send-email', method: 'POST', params: {} },
@@ -169,8 +172,10 @@
putU2f: { url: _apiUri + '/two-factor/u2f', method: 'POST', params: {} },
putAuthenticator: { url: _apiUri + '/two-factor/authenticator', method: 'POST', params: {} },
putDuo: { url: _apiUri + '/two-factor/duo', method: 'POST', params: {} },
putOrganizationDuo: { url: _apiUri + '/organizations/:orgId/two-factor/duo', method: 'POST', params: { orgId: '@orgId' } },
putYubi: { url: _apiUri + '/two-factor/yubikey', method: 'POST', params: {} },
disable: { url: _apiUri + '/two-factor/disable', method: 'POST', params: {} },
disableOrganization: { url: _apiUri + '/organizations/:orgId/two-factor/disable', method: 'POST', params: { orgId: '@orgId' } },
recover: { url: _apiUri + '/two-factor/recover', method: 'POST', params: {} },
getRecover: { url: _apiUri + '/two-factor/get-recover', method: 'POST', params: {} }
});
@@ -184,11 +189,21 @@
getPublicKey: { url: _apiUri + '/users/:id/public-key', method: 'GET', params: { id: '@id' } }
});
_service.events = $resource(_apiUri + '/events', {}, {
list: { method: 'GET', params: {} },
listOrganization: { url: _apiUri + '/organizations/:orgId/events', method: 'GET', params: { id: '@orgId' } },
listCipher: { url: _apiUri + '/ciphers/:id/events', method: 'GET', params: { id: '@id' } },
listOrganizationUser: { url: _apiUri + '/organizations/:orgId/users/:id/events', method: 'GET', params: { orgId: '@orgId', id: '@id' } }
});
_service.identity = $resource(_identityUri + '/connect', {}, {
token: {
url: _identityUri + '/connect/token',
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' },
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
'Device-Type': utilsService.getDeviceType()
},
transformRequest: transformUrlEncoded,
skipAuthorization: true,
params: {}

View File

@@ -95,7 +95,7 @@ angular
_service.logOut = function () {
tokenService.clearTokens();
cryptoService.clearKeys();
$rootScope.vaultFolders = $rootScope.vaultLogins = null;
$rootScope.vaultCiphers = $rootScope.vaultFolders = $rootScope.vaultCollections = null;
_userProfile = null;
};
@@ -150,6 +150,9 @@ angular
maxStorageGb: profile.Organizations[i].MaxStorageGb,
seats: profile.Organizations[i].Seats,
useGroups: profile.Organizations[i].UseGroups,
useDirectory: profile.Organizations[i].UseDirectory,
useEvents: profile.Organizations[i].UseEvents,
use2fa: profile.Organizations[i].Use2fa,
useTotp: profile.Organizations[i].UseTotp
};
}
@@ -158,8 +161,8 @@ angular
cryptoService.setOrgKeys(orgs);
_setDeferred.resolve(_userProfile);
}
}, function () {
_setDeferred.reject();
}, function (error) {
_setDeferred.reject(error);
});
return _setDeferred.promise;
@@ -183,6 +186,9 @@ angular
maxStorageGb: org.MaxStorageGb,
seats: org.Seats,
useGroups: org.UseGroups,
useDirectory: org.UseDirectory,
useEvents: org.UseEvents,
use2fa: org.Use2fa,
useTotp: org.UseTotp
};
profile.organizations[o.id] = o;
@@ -233,7 +239,9 @@ angular
_service.refreshAccessToken = function () {
var refreshToken = tokenService.getRefreshToken();
if (!refreshToken) {
return null;
return $q(function (resolve, reject) {
resolve(null);
});
}
return apiService.identity.token({

View File

@@ -1,63 +1,23 @@
angular
.module('bit.services')
.factory('cipherService', function (cryptoService, apiService, $q, $window) {
var _service = {};
_service.decryptLogins = function (encryptedLogins) {
if (!encryptedLogins) throw "encryptedLogins is undefined or null";
var unencryptedLogins = [];
for (var i = 0; i < encryptedLogins.length; i++) {
unencryptedLogins.push(_service.decryptLogin(encryptedLogins[i]));
}
return unencryptedLogins;
.factory('cipherService', function (cryptoService, apiService, $q, $window, constants, appSettings, $localStorage) {
var _service = {
disableWebsiteIcons: $localStorage.disableWebsiteIcons
};
_service.decryptLogin = function (encryptedLogin, isCipher) {
if (!encryptedLogin) throw "encryptedLogin is undefined or null";
_service.decryptCiphers = function (encryptedCiphers) {
if (!encryptedCiphers) throw "encryptedCiphers is undefined or null";
var key = null;
if (encryptedLogin.OrganizationId) {
key = cryptoService.getOrgKey(encryptedLogin.OrganizationId);
var unencryptedCiphers = [];
for (var i = 0; i < encryptedCiphers.length; i++) {
unencryptedCiphers.push(_service.decryptCipher(encryptedCiphers[i]));
}
var login = {
id: encryptedLogin.Id,
organizationId: encryptedLogin.OrganizationId,
collectionIds: encryptedLogin.CollectionIds || [],
'type': 1,
folderId: encryptedLogin.FolderId,
favorite: encryptedLogin.Favorite,
edit: encryptedLogin.Edit,
organizationUseTotp: encryptedLogin.OrganizationUseTotp,
attachments: null
};
var loginData = encryptedLogin.Data || encryptedLogin;
if (loginData) {
login.name = cryptoService.decrypt(loginData.Name, key);
login.uri = loginData.Uri && loginData.Uri !== '' ? cryptoService.decrypt(loginData.Uri, key) : null;
login.username = loginData.Username && loginData.Username !== '' ? cryptoService.decrypt(loginData.Username, key) : null;
login.password = loginData.Password && loginData.Password !== '' ? cryptoService.decrypt(loginData.Password, key) : null;
login.notes = loginData.Notes && loginData.Notes !== '' ? cryptoService.decrypt(loginData.Notes, key) : null;
login.totp = loginData.Totp && loginData.Totp !== '' ? cryptoService.decrypt(loginData.Totp, key) : null;
}
if (!encryptedLogin.Attachments) {
return login;
}
login.attachments = [];
for (var i = 0; i < encryptedLogin.Attachments.length; i++) {
login.attachments.push(_service.decryptAttachment(key, encryptedLogin.Attachments[i]));
}
return login;
return unencryptedCiphers;
};
_service.decryptLoginPreview = function (encryptedCipher) {
_service.decryptCipher = function (encryptedCipher) {
if (!encryptedCipher) throw "encryptedCipher is undefined or null";
var key = null;
@@ -65,27 +25,209 @@ angular
key = cryptoService.getOrgKey(encryptedCipher.OrganizationId);
}
var login = {
var cipher = {
id: encryptedCipher.Id,
organizationId: encryptedCipher.OrganizationId,
collectionIds: encryptedCipher.CollectionIds || [],
'type': encryptedCipher.Type,
name: cryptoService.decrypt(encryptedCipher.Name, key),
notes: _service.decryptProperty(encryptedCipher.Notes, key, true, false),
fields: _service.decryptFields(key, encryptedCipher.Fields),
folderId: encryptedCipher.FolderId,
favorite: encryptedCipher.Favorite,
edit: encryptedCipher.Edit,
organizationUseTotp: encryptedCipher.OrganizationUseTotp,
hasAttachments: !!encryptedCipher.Attachments && encryptedCipher.Attachments.length > 0
attachments: null,
icon: null
};
var loginData = encryptedCipher.Data || encryptedCipher;
if (loginData) {
login.name = _service.decryptProperty(loginData.Name, key, false);
login.username = _service.decryptProperty(loginData.Username, key, true);
login.password = _service.decryptProperty(loginData.Password, key, true);
var i;
switch (cipher.type) {
case constants.cipherType.login:
cipher.login = {
username: _service.decryptProperty(encryptedCipher.Login.Username, key, true, false),
password: _service.decryptProperty(encryptedCipher.Login.Password, key, true, false),
totp: _service.decryptProperty(encryptedCipher.Login.Totp, key, true, false),
uris: null
};
if (encryptedCipher.Login.Uris) {
cipher.login.uris = [];
for (i = 0; i < encryptedCipher.Login.Uris.length; i++) {
cipher.login.uris.push({
uri: _service.decryptProperty(encryptedCipher.Login.Uris[i].Uri, key, true, false),
match: encryptedCipher.Login.Uris[i].Match
});
}
}
cipher.icon = 'fa-globe';
break;
case constants.cipherType.secureNote:
cipher.secureNote = {
type: encryptedCipher.SecureNote.Type
};
cipher.icon = 'fa-sticky-note-o';
break;
case constants.cipherType.card:
cipher.card = {
cardholderName: _service.decryptProperty(encryptedCipher.Card.CardholderName, key, true, false),
number: _service.decryptProperty(encryptedCipher.Card.Number, key, true, false),
brand: _service.decryptProperty(encryptedCipher.Card.Brand, key, true, false),
expMonth: _service.decryptProperty(encryptedCipher.Card.ExpMonth, key, true, false),
expYear: _service.decryptProperty(encryptedCipher.Card.ExpYear, key, true, false),
code: _service.decryptProperty(encryptedCipher.Card.Code, key, true, false)
};
cipher.icon = 'fa-credit-card';
break;
case constants.cipherType.identity:
cipher.identity = {
title: _service.decryptProperty(encryptedCipher.Identity.Title, key, true, false),
firstName: _service.decryptProperty(encryptedCipher.Identity.FirstName, key, true, false),
middleName: _service.decryptProperty(encryptedCipher.Identity.MiddleName, key, true, false),
lastName: _service.decryptProperty(encryptedCipher.Identity.LastName, key, true, false),
address1: _service.decryptProperty(encryptedCipher.Identity.Address1, key, true, false),
address2: _service.decryptProperty(encryptedCipher.Identity.Address2, key, true, false),
address3: _service.decryptProperty(encryptedCipher.Identity.Address3, key, true, false),
city: _service.decryptProperty(encryptedCipher.Identity.City, key, true, false),
state: _service.decryptProperty(encryptedCipher.Identity.State, key, true, false),
postalCode: _service.decryptProperty(encryptedCipher.Identity.PostalCode, key, true, false),
country: _service.decryptProperty(encryptedCipher.Identity.Country, key, true, false),
company: _service.decryptProperty(encryptedCipher.Identity.Company, key, true, false),
email: _service.decryptProperty(encryptedCipher.Identity.Email, key, true, false),
phone: _service.decryptProperty(encryptedCipher.Identity.Phone, key, true, false),
ssn: _service.decryptProperty(encryptedCipher.Identity.SSN, key, true, false),
username: _service.decryptProperty(encryptedCipher.Identity.Username, key, true, false),
passportNumber: _service.decryptProperty(encryptedCipher.Identity.PassportNumber, key, true, false),
licenseNumber: _service.decryptProperty(encryptedCipher.Identity.LicenseNumber, key, true, false)
};
cipher.icon = 'fa-id-card-o';
break;
default:
break;
}
return login;
if (!encryptedCipher.Attachments) {
return cipher;
}
cipher.attachments = [];
for (i = 0; i < encryptedCipher.Attachments.length; i++) {
cipher.attachments.push(_service.decryptAttachment(key, encryptedCipher.Attachments[i]));
}
return cipher;
};
_service.decryptCipherPreview = function (encryptedCipher) {
if (!encryptedCipher) throw "encryptedCipher is undefined or null";
var key = null;
if (encryptedCipher.OrganizationId) {
key = cryptoService.getOrgKey(encryptedCipher.OrganizationId);
}
var cipher = {
id: encryptedCipher.Id,
organizationId: encryptedCipher.OrganizationId,
collectionIds: encryptedCipher.CollectionIds || [],
'type': encryptedCipher.Type,
name: _service.decryptProperty(encryptedCipher.Name, key, false, true),
folderId: encryptedCipher.FolderId,
favorite: encryptedCipher.Favorite,
edit: encryptedCipher.Edit,
organizationUseTotp: encryptedCipher.OrganizationUseTotp,
hasAttachments: !!encryptedCipher.Attachments && encryptedCipher.Attachments.length > 0,
meta: {},
icon: null
};
switch (cipher.type) {
case constants.cipherType.login:
cipher.subTitle = _service.decryptProperty(encryptedCipher.Login.Username, key, true, true);
cipher.meta.password = _service.decryptProperty(encryptedCipher.Login.Password, key, true, true);
cipher.meta.uri = null;
if (encryptedCipher.Login.Uris && encryptedCipher.Login.Uris.length) {
cipher.meta.uri = _service.decryptProperty(encryptedCipher.Login.Uris[0].Uri, key, true, true);
}
setLoginIcon(cipher, cipher.meta.uri, true);
break;
case constants.cipherType.secureNote:
cipher.subTitle = null;
cipher.icon = 'fa-sticky-note-o';
break;
case constants.cipherType.card:
cipher.subTitle = '';
cipher.meta.number = _service.decryptProperty(encryptedCipher.Card.Number, key, true, true);
var brand = _service.decryptProperty(encryptedCipher.Card.Brand, key, true, true);
if (brand) {
cipher.subTitle = brand;
}
if (cipher.meta.number && cipher.meta.number.length >= 4) {
if (cipher.subTitle !== '') {
cipher.subTitle += ', ';
}
cipher.subTitle += ('*' + cipher.meta.number.substr(cipher.meta.number.length - 4));
}
cipher.icon = 'fa-credit-card';
break;
case constants.cipherType.identity:
var firstName = _service.decryptProperty(encryptedCipher.Identity.FirstName, key, true, true);
var lastName = _service.decryptProperty(encryptedCipher.Identity.LastName, key, true, true);
cipher.subTitle = '';
if (firstName) {
cipher.subTitle = firstName;
}
if (lastName) {
if (cipher.subTitle !== '') {
cipher.subTitle += ' ';
}
cipher.subTitle += lastName;
}
cipher.icon = 'fa-id-card-o';
break;
default:
break;
}
if (cipher.subTitle === '') {
cipher.subTitle = null;
}
return cipher;
};
function setLoginIcon(cipher, uri, setImage) {
if (!_service.disableWebsiteIcons && uri) {
var hostnameUri = uri,
isWebsite = false;
if (hostnameUri.indexOf('androidapp://') === 0) {
cipher.icon = 'fa-android';
}
else if (hostnameUri.indexOf('iosapp://') === 0) {
cipher.icon = 'fa-apple';
}
else if (hostnameUri.indexOf('://') === -1 && hostnameUri.indexOf('.') > -1) {
hostnameUri = "http://" + hostnameUri;
isWebsite = true;
}
else {
isWebsite = hostnameUri.indexOf('http') === 0 && hostnameUri.indexOf('.') > -1;
}
if (setImage && isWebsite) {
try {
var url = new URL(hostnameUri);
cipher.meta.image = appSettings.iconsUri + '/' + url.hostname + '/icon.png';
}
catch (e) { }
}
}
if (!cipher.icon) {
cipher.icon = 'fa-globe';
}
}
_service.decryptAttachment = function (key, encryptedAttachment) {
if (!encryptedAttachment) throw "encryptedAttachment is undefined or null";
@@ -134,6 +276,28 @@ angular
return deferred.promise;
};
_service.decryptFields = function (key, encryptedFields) {
var unencryptedFields = [];
if (encryptedFields) {
for (var i = 0; i < encryptedFields.length; i++) {
unencryptedFields.push(_service.decryptField(key, encryptedFields[i]));
}
}
return unencryptedFields;
};
_service.decryptField = function (key, encryptedField) {
if (!encryptedField) throw "encryptedField is undefined or null";
return {
type: encryptedField.Type.toString(),
name: encryptedField.Name && encryptedField.Name !== '' ? cryptoService.decrypt(encryptedField.Name, key) : null,
value: encryptedField.Value && encryptedField.Value !== '' ? cryptoService.decrypt(encryptedField.Value, key) : null
};
};
_service.decryptFolders = function (encryptedFolders) {
if (!encryptedFolders) throw "encryptedFolders is undefined or null";
@@ -159,7 +323,7 @@ angular
return {
id: encryptedFolder.Id,
name: _service.decryptProperty(encryptedFolder.Name, null, false)
name: _service.decryptProperty(encryptedFolder.Name, null, false, true)
};
};
@@ -183,12 +347,12 @@ angular
return {
id: encryptedCollection.Id,
name: catchError ? _service.decryptProperty(encryptedCollection.Name, key, false) :
name: catchError ? _service.decryptProperty(encryptedCollection.Name, key, false, true) :
cryptoService.decrypt(encryptedCollection.Name, key)
};
};
_service.decryptProperty = function (property, key, checkEmpty) {
_service.decryptProperty = function (property, key, checkEmpty, showError) {
if (checkEmpty && (!property || property === '')) {
return null;
}
@@ -200,50 +364,109 @@ angular
property = null;
}
return property || '[error: cannot decrypt]';
return property || (showError ? '[error: cannot decrypt]' : null);
};
_service.encryptLogins = function (unencryptedLogins, key) {
if (!unencryptedLogins) throw "unencryptedLogins is undefined or null";
_service.encryptCiphers = function (unencryptedCiphers, key) {
if (!unencryptedCiphers) throw "unencryptedCiphers is undefined or null";
var encryptedLogins = [];
for (var i = 0; i < unencryptedLogins.length; i++) {
encryptedLogins.push(_service.encryptLogin(unencryptedLogins[i], key));
var encryptedCiphers = [];
for (var i = 0; i < unencryptedCiphers.length; i++) {
encryptedCiphers.push(_service.encryptCipher(unencryptedCiphers[i], null, key));
}
return encryptedLogins;
return encryptedCiphers;
};
_service.encryptLogin = function (unencryptedLogin, key, attachments) {
if (!unencryptedLogin) throw "unencryptedLogin is undefined or null";
_service.encryptCipher = function (unencryptedCipher, type, key, attachments) {
if (!unencryptedCipher) throw "unencryptedCipher is undefined or null";
if (unencryptedLogin.organizationId) {
key = key || cryptoService.getOrgKey(unencryptedLogin.organizationId);
if (unencryptedCipher.organizationId) {
key = key || cryptoService.getOrgKey(unencryptedCipher.organizationId);
}
var login = {
id: unencryptedLogin.id,
'type': 1,
organizationId: unencryptedLogin.organizationId || null,
folderId: unencryptedLogin.folderId === '' ? null : unencryptedLogin.folderId,
favorite: unencryptedLogin.favorite !== null ? unencryptedLogin.favorite : false,
uri: !unencryptedLogin.uri || unencryptedLogin.uri === '' ? null : cryptoService.encrypt(unencryptedLogin.uri, key),
name: cryptoService.encrypt(unencryptedLogin.name, key),
username: !unencryptedLogin.username || unencryptedLogin.username === '' ? null : cryptoService.encrypt(unencryptedLogin.username, key),
password: !unencryptedLogin.password || unencryptedLogin.password === '' ? null : cryptoService.encrypt(unencryptedLogin.password, key),
notes: !unencryptedLogin.notes || unencryptedLogin.notes === '' ? null : cryptoService.encrypt(unencryptedLogin.notes, key),
totp: !unencryptedLogin.totp || unencryptedLogin.totp === '' ? null : cryptoService.encrypt(unencryptedLogin.totp, key)
var cipher = {
id: unencryptedCipher.id,
'type': type || unencryptedCipher.type,
organizationId: unencryptedCipher.organizationId || null,
folderId: unencryptedCipher.folderId === '' ? null : unencryptedCipher.folderId,
favorite: unencryptedCipher.favorite !== null ? unencryptedCipher.favorite : false,
name: cryptoService.encrypt(unencryptedCipher.name, key),
notes: encryptProperty(unencryptedCipher.notes, key),
fields: _service.encryptFields(unencryptedCipher.fields, key)
};
if (unencryptedLogin.attachments && attachments) {
login.attachments = {};
for (var i = 0; i < unencryptedLogin.attachments.length; i++) {
login.attachments[unencryptedLogin.attachments[i].id] =
cryptoService.encrypt(unencryptedLogin.attachments[i].fileName, key);
var i;
switch (cipher.type) {
case constants.cipherType.login:
var loginData = unencryptedCipher.login;
cipher.login = {
username: encryptProperty(loginData.username, key),
password: encryptProperty(loginData.password, key),
totp: encryptProperty(loginData.totp, key)
};
if (loginData.uris && loginData.uris.length) {
cipher.login.uris = [];
for (i = 0; i < loginData.uris.length; i++) {
cipher.login.uris.push({
uri: encryptProperty(loginData.uris[i].uri, key),
match: loginData.uris[i].match
});
}
}
break;
case constants.cipherType.secureNote:
cipher.secureNote = {
type: unencryptedCipher.secureNote.type
};
break;
case constants.cipherType.card:
var cardData = unencryptedCipher.card;
cipher.card = {
cardholderName: encryptProperty(cardData.cardholderName, key),
brand: encryptProperty(cardData.brand, key),
number: encryptProperty(cardData.number, key),
expMonth: encryptProperty(cardData.expMonth, key),
expYear: encryptProperty(cardData.expYear, key),
code: encryptProperty(cardData.code, key)
};
break;
case constants.cipherType.identity:
var identityData = unencryptedCipher.identity;
cipher.identity = {
title: encryptProperty(identityData.title, key),
firstName: encryptProperty(identityData.firstName, key),
middleName: encryptProperty(identityData.middleName, key),
lastName: encryptProperty(identityData.lastName, key),
address1: encryptProperty(identityData.address1, key),
address2: encryptProperty(identityData.address2, key),
address3: encryptProperty(identityData.address3, key),
city: encryptProperty(identityData.city, key),
state: encryptProperty(identityData.state, key),
postalCode: encryptProperty(identityData.postalCode, key),
country: encryptProperty(identityData.country, key),
company: encryptProperty(identityData.company, key),
email: encryptProperty(identityData.email, key),
phone: encryptProperty(identityData.phone, key),
ssn: encryptProperty(identityData.ssn, key),
username: encryptProperty(identityData.username, key),
passportNumber: encryptProperty(identityData.passportNumber, key),
licenseNumber: encryptProperty(identityData.licenseNumber, key)
};
break;
default:
break;
}
if (unencryptedCipher.attachments && attachments) {
cipher.attachments = {};
for (i = 0; i < unencryptedCipher.attachments.length; i++) {
cipher.attachments[unencryptedCipher.attachments[i].id] =
cryptoService.encrypt(unencryptedCipher.attachments[i].fileName, key);
}
}
return login;
return cipher;
};
_service.encryptAttachmentFile = function (key, unencryptedFile) {
@@ -272,6 +495,33 @@ angular
return deferred.promise;
};
_service.encryptFields = function (unencryptedFields, key) {
if (!unencryptedFields || !unencryptedFields.length) {
return null;
}
var encFields = [];
for (var i = 0; i < unencryptedFields.length; i++) {
if (!unencryptedFields[i]) {
continue;
}
encFields.push(_service.encryptField(unencryptedFields[i], key));
}
return encFields;
};
_service.encryptField = function (unencryptedField, key) {
if (!unencryptedField) throw "unencryptedField is undefined or null";
return {
type: parseInt(unencryptedField.type),
name: unencryptedField.name ? cryptoService.encrypt(unencryptedField.name, key) : null,
value: unencryptedField.value ? cryptoService.encrypt(unencryptedField.value.toString(), key) : null
};
};
_service.encryptFolders = function (unencryptedFolders, key) {
if (!unencryptedFolders) throw "unencryptedFolders is undefined or null";
@@ -312,5 +562,9 @@ angular
};
};
function encryptProperty(property, key) {
return !property || property === '' ? null : cryptoService.encrypt(property, key);
}
return _service;
});

View File

@@ -235,7 +235,7 @@ angular
};
_service.makeKey = function (password, salt) {
if (!$window.cryptoShimmed && $window.navigator.userAgent.indexOf('Edge') === -1) {
if (_subtle != null && !$window.cryptoShimmed && $window.navigator.userAgent.indexOf('Edge') === -1) {
return pbkdf2WC(password, salt, 5000, 256).then(function (keyBuf) {
return new SymmetricCryptoKey(bufToB64(keyBuf), true);
});
@@ -305,7 +305,7 @@ angular
throw 'Invalid parameters.';
}
if (!$window.cryptoShimmed && $window.navigator.userAgent.indexOf('Edge') === -1) {
if (_subtle != null && !$window.cryptoShimmed && $window.navigator.userAgent.indexOf('Edge') === -1) {
var keyBuf = key.getBuffers();
return pbkdf2WC(new Uint8Array(keyBuf.key), password, 1, 256).then(function (hashBuf) {
return bufToB64(hashBuf);
@@ -540,7 +540,7 @@ angular
if (key.macKey && encPieces.length > 2) {
var macBytes = forge.util.decode64(encPieces[2]);
var computedMacBytes = computeMac(ivBytes + ctBytes, key.macKey, false);
if (!macsEqual(key.macKey, macBytes, computedMacBytes)) {
if (!macsEqual(macBytes, computedMacBytes)) {
console.error('MAC failed.');
return null;
}
@@ -623,6 +623,10 @@ angular
throw 'Encryption key unavailable.';
}
if (key.macKey && !macBuf) {
throw 'macBuf required for this type of key.';
}
if (encType !== key.encType) {
throw 'encType unavailable.';
}
@@ -646,7 +650,7 @@ angular
if (computedMacBuf === null) {
return null;
}
return macsEqualWC(keyBuf.macKey, macBuf, computedMacBuf);
return macsEqualWC(macBuf, computedMacBuf);
}).then(function (macsMatch) {
if (macsMatch === false) {
console.error('MAC failed.');
@@ -704,7 +708,7 @@ angular
if (key && key.macKey && encPieces.length > 1) {
var macBytes = forge.util.decode64(encPieces[1]);
var computedMacBytes = computeMac(ctBytes, key.macKey, false);
if (!macsEqual(key.macKey, macBytes, computedMacBytes)) {
if (!macsEqual(macBytes, computedMacBytes)) {
console.error('MAC failed.');
return null;
}
@@ -747,10 +751,11 @@ angular
// Safely compare two MACs in a way that protects against timing attacks (Double HMAC Verification).
// ref: https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/
function macsEqual(macKey, mac1, mac2) {
// ref: https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy
function macsEqual(mac1, mac2) {
var hmac = forge.hmac.create();
hmac.start('sha256', macKey);
hmac.start('sha256', getRandomBytes(32));
hmac.update(mac1);
mac1 = hmac.digest().getBytes();
@@ -761,11 +766,14 @@ angular
return mac1 === mac2;
}
function macsEqualWC(macKeyBuf, mac1Buf, mac2Buf) {
function macsEqualWC(mac1Buf, mac2Buf) {
var mac1,
macKey;
return window.crypto.subtle.importKey('raw', macKeyBuf, { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign'])
var compareKey = new Uint8Array(32);
_crypto.getRandomValues(compareKey);
return window.crypto.subtle.importKey('raw', compareKey.buffer, { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign'])
.then(function (key) {
macKey = key;
return window.crypto.subtle.sign({ name: 'HMAC', hash: { name: 'SHA-256' } }, macKey, mac1Buf);
@@ -932,5 +940,15 @@ angular
return new Uint8Array(result);
}
function getRandomBytes(byteLength) {
var bytes = new Uint32Array(byteLength / 4);
_crypto.getRandomValues(bytes);
var buffer = forge.util.createBuffer();
for (var i = 0; i < bytes.length; i++) {
buffer.putInt32(bytes[i]);
}
return buffer.getBytes();
}
return _service;
});

View File

@@ -0,0 +1,269 @@
angular
.module('bit.services')
.factory('eventService', function (constants, $filter) {
var _service = {};
_service.getDefaultDateFilters = function () {
var d = new Date();
var filterEnd = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59);
d.setDate(d.getDate() - 30);
var filterStart = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0);
return {
start: filterStart,
end: filterEnd
};
};
_service.formatDateFilters = function (filterStart, filterEnd) {
var result = {
start: null,
end: null,
error: null
};
try {
var format = 'yyyy-MM-ddTHH:mm';
result.start = $filter('date')(filterStart, format + 'Z', 'UTC');
result.end = $filter('date')(filterEnd, format + ':59.999Z', 'UTC');
} catch (e) { }
if (!result.start || !result.end || result.end < result.start) {
result.error = 'Invalid date range.';
}
return result;
};
_service.getEventInfo = function (ev, options) {
options = options || {
cipherInfo: true
};
var appInfo = getAppInfo(ev);
return {
message: getEventMessage(ev, options),
appIcon: appInfo.icon,
appName: appInfo.name
};
};
function getEventMessage(ev, options) {
var msg = '';
switch (ev.Type) {
// User
case constants.eventType.User_LoggedIn:
msg = 'Logged in.';
break;
case constants.eventType.User_ChangedPassword:
msg = 'Changed account password.';
break;
case constants.eventType.User_Enabled2fa:
msg = 'Enabled two-step login.';
break;
case constants.eventType.User_Disabled2fa:
msg = 'Disabled two-step login.';
break;
case constants.eventType.User_Recovered2fa:
msg = 'Recovered account from two-step login.';
break;
case constants.eventType.User_FailedLogIn:
msg = 'Login attempt failed with incorrect password.';
break;
case constants.eventType.User_FailedLogIn2fa:
msg = 'Login attempt failed with incorrect two-step login.';
break;
// Cipher
case constants.eventType.Cipher_Created:
msg = options.cipherInfo ? 'Created item ' + formatCipherId(ev) + '.' : 'Created.';
break;
case constants.eventType.Cipher_Updated:
msg = options.cipherInfo ? 'Edited item ' + formatCipherId(ev) + '.' : 'Edited.';
break;
case constants.eventType.Cipher_Deleted:
msg = options.cipherInfo ? 'Deleted item ' + formatCipherId(ev) + '.' : 'Deleted';
break;
case constants.eventType.Cipher_AttachmentCreated:
msg = options.cipherInfo ? 'Created attachment for item ' + formatCipherId(ev) + '.' :
'Created attachment.';
break;
case constants.eventType.Cipher_AttachmentDeleted:
msg = options.cipherInfo ? 'Deleted attachment for item ' + formatCipherId(ev) + '.' :
'Deleted attachment.';
break;
case constants.eventType.Cipher_Shared:
msg = options.cipherInfo ? 'Shared item ' + formatCipherId(ev) + '.' : 'Shared.';
break;
case constants.eventType.Cipher_UpdatedCollections:
msg = options.cipherInfo ? 'Update collections for item ' + formatCipherId(ev) + '.' :
'Updated collections.';
break;
// Collection
case constants.eventType.Collection_Created:
msg = 'Created collection ' + formatCollectionId(ev) + '.';
break;
case constants.eventType.Collection_Updated:
msg = 'Edited collection ' + formatCollectionId(ev) + '.';
break;
case constants.eventType.Collection_Deleted:
msg = 'Deleted collection ' + formatCollectionId(ev) + '.';
break;
// Group
case constants.eventType.Group_Created:
msg = 'Created group ' + formatGroupId(ev) + '.';
break;
case constants.eventType.Group_Updated:
msg = 'Edited group ' + formatGroupId(ev) + '.';
break;
case constants.eventType.Group_Deleted:
msg = 'Deleted group ' + formatGroupId(ev) + '.';
break;
// Org user
case constants.eventType.OrganizationUser_Invited:
msg = 'Invited user ' + formatOrgUserId(ev) + '.';
break;
case constants.eventType.OrganizationUser_Confirmed:
msg = 'Confirmed user ' + formatOrgUserId(ev) + '.';
break;
case constants.eventType.OrganizationUser_Updated:
msg = 'Edited user ' + formatOrgUserId(ev) + '.';
break;
case constants.eventType.OrganizationUser_Removed:
msg = 'Removed user ' + formatOrgUserId(ev) + '.';
break;
case constants.eventType.OrganizationUser_UpdatedGroups:
msg = 'Edited groups for user ' + formatOrgUserId(ev) + '.';
break;
// Org
case constants.eventType.Organization_Updated:
msg = 'Edited organization settings.';
break;
default:
break;
}
return msg === '' ? null : msg;
}
function getAppInfo(ev) {
var appInfo = {
icon: 'fa-globe',
name: 'Unknown'
};
switch (ev.DeviceType) {
case constants.deviceType.android:
appInfo.icon = 'fa-android';
appInfo.name = 'Mobile App - Android';
break;
case constants.deviceType.ios:
appInfo.icon = 'fa-apple';
appInfo.name = 'Mobile App - iOS';
break;
case constants.deviceType.uwp:
appInfo.icon = 'fa-windows';
appInfo.name = 'Mobile App - Windows';
break;
case constants.deviceType.chromeExt:
appInfo.icon = 'fa-chrome';
appInfo.name = 'Extension - Chrome';
break;
case constants.deviceType.firefoxExt:
appInfo.icon = 'fa-firefox';
appInfo.name = 'Extension - Firefox';
break;
case constants.deviceType.operaExt:
appInfo.icon = 'fa-opera';
appInfo.name = 'Extension - Opera';
break;
case constants.deviceType.edgeExt:
appInfo.icon = 'fa-edge';
appInfo.name = 'Extension - Edge';
break;
case constants.deviceType.vivaldiExt:
appInfo.icon = 'fa-puzzle-piece';
appInfo.name = 'Extension - Vivaldi';
break;
case constants.deviceType.windowsDesktop:
appInfo.icon = 'fa-windows';
appInfo.name = 'Desktop - Windows';
break;
case constants.deviceType.macOsDesktop:
appInfo.icon = 'fa-apple';
appInfo.name = 'Desktop - macOS';
break;
case constants.deviceType.linuxDesktop:
appInfo.icon = 'fa-linux';
appInfo.name = 'Desktop - Linux';
break;
case constants.deviceType.chrome:
appInfo.icon = 'fa-globe';
appInfo.name = 'Web Vault - Chrome';
break;
case constants.deviceType.firefox:
appInfo.icon = 'fa-globe';
appInfo.name = 'Web Vault - Firefox';
break;
case constants.deviceType.opera:
appInfo.icon = 'fa-globe';
appInfo.name = 'Web Vault - Opera';
break;
case constants.deviceType.safari:
appInfo.icon = 'fa-globe';
appInfo.name = 'Web Vault - Safari';
break;
case constants.deviceType.vivaldi:
appInfo.icon = 'fa-globe';
appInfo.name = 'Web Vault - Vivaldi';
break;
case constants.deviceType.edge:
appInfo.icon = 'fa-globe';
appInfo.name = 'Web Vault - Edge';
break;
case constants.deviceType.ie:
appInfo.icon = 'fa-globe';
appInfo.name = 'Web Vault - IE';
break;
case constants.deviceType.unknown:
appInfo.icon = 'fa-globe';
appInfo.name = 'Web Vault - Unknown';
break;
default:
break;
}
return appInfo;
}
function formatCipherId(ev) {
var shortId = ev.CipherId.substring(0, 8);
if (!ev.OrganizationId) {
return '<code>' + shortId + '</code>';
}
return '<a title="View item ' + ev.CipherId + '" ui-sref="backend.org.vault({orgId:\'' + ev.OrganizationId + '\',search:\'' + shortId + '\',viewEvents:\'' + ev.CipherId + '\'})">' +
'<code>' + shortId + '</code></a>';
}
function formatGroupId(ev) {
var shortId = ev.GroupId.substring(0, 8);
return '<a title="View group ' + ev.GroupId + '" ui-sref="backend.org.groups({orgId:\'' + ev.OrganizationId + '\',search:\'' + shortId + '\'})">' +
'<code>' + shortId + '</code></a>';
}
function formatCollectionId(ev) {
var shortId = ev.CollectionId.substring(0, 8);
return '<a title="View collection ' + ev.CollectionId + '" ui-sref="backend.org.collections({orgId:\'' + ev.OrganizationId + '\',search:\'' + shortId + '\'})">' +
'<code>' + shortId + '</code></a>';
}
function formatOrgUserId(ev) {
var shortId = ev.OrganizationUserId.substring(0, 8);
return '<a title="View user ' + ev.OrganizationUserId + '" ui-sref="backend.org.people({orgId:\'' + ev.OrganizationId + '\',search:\'' + shortId + '\'})">' +
'<code>' + shortId + '</code></a>';
}
return _service;
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,47 @@
angular
.module('bit.services')
.factory('utilsService', function (constants) {
var _service = {};
var _browserCache;
_service.getDeviceType = function (token) {
if (_browserCache) {
return _browserCache;
}
if (navigator.userAgent.indexOf(' Vivaldi/') >= 0) {
_browserCache = constants.deviceType.vivaldi;
}
else if (!!window.chrome && !!window.chrome.webstore) {
_browserCache = constants.deviceType.chrome;
}
else if (typeof InstallTrigger !== 'undefined') {
_browserCache = constants.deviceType.firefox;
}
else if ((!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0) {
_browserCache = constants.deviceType.firefox;
}
else if (/constructor/i.test(window.HTMLElement) ||
safariCheck(!window.safari || (typeof safari !== 'undefined' && safari.pushNotification))) {
_browserCache = constants.deviceType.opera;
}
else if (!!document.documentMode) {
_browserCache = constants.deviceType.ie;
}
else if (!!window.StyleMedia) {
_browserCache = constants.deviceType.edge;
}
else {
_browserCache = constants.deviceType.unknown;
}
return _browserCache;
};
function safariCheck(p) {
return p.toString() === '[object SafariRemoteNotification]';
}
return _service;
});

View File

@@ -62,5 +62,41 @@
}
};
_service.parseErrors = function (reason) {
var data = reason.data;
var defaultErrorMessage = 'An unexpected error has occurred.';
var errors = [];
if (!data || !angular.isObject(data)) {
errors.push(defaultErrorMessage);
return errors;
}
if (data && data.ErrorModel) {
data = data.ErrorModel;
}
if (!data.ValidationErrors) {
if (data.Message) {
errors.push(data.Message);
}
else {
errors.push(defaultErrorMessage);
}
}
for (var key in data.ValidationErrors) {
if (!data.ValidationErrors.hasOwnProperty(key)) {
continue;
}
for (var i = 0; i < data.ValidationErrors[key].length; i++) {
errors.push(data.ValidationErrors[key][i]);
}
}
return errors;
};
return _service;
});

View File

@@ -1,2 +1,2 @@
angular.module("bit")
.constant("appSettings", {"apiUri":"https://api.bitwarden.com","identityUri":"https://identity.bitwarden.com","stripeKey":"pk_live_bpN0P37nMxrMQkcaHXtAybJk","braintreeKey":"production_qfbsv8kc_njj2zjtyngtjmbjd","whitelistDomains":["api.bitwarden.com"],"selfHosted":false,"version":"1.15.0","environment":"Production"});
.constant("appSettings", {"apiUri":"/api","identityUri":"/identity","iconsUri":"https://icons.bitwarden.com","stripeKey":"pk_live_bpN0P37nMxrMQkcaHXtAybJk","braintreeKey":"production_qfbsv8kc_njj2zjtyngtjmbjd","selfHosted":false,"version":"1.27.0","environment":"Production"});

View File

@@ -4,7 +4,9 @@
.controller('settingsBillingChangePaymentController', function ($scope, $state, $uibModalInstance, apiService,
$analytics, toastr, existingPaymentMethod, appSettings, $timeout
// @if !selfHosted
/* jshint ignore:start */
, stripe
/* jshint ignore:end */
// @endif
) {
$analytics.eventTrack('settingsBillingChangePaymentController', { category: 'Modal' });

View File

@@ -1,11 +1,12 @@
angular
.module('bit.settings')
.controller('settingsController', function ($scope, $state, $uibModal, apiService, toastr, authService) {
.controller('settingsController', function ($scope, $state, $uibModal, apiService, toastr, authService, $localStorage,
$rootScope, cipherService) {
$scope.model = {
profile: {},
twoFactorEnabled: false,
email: null
email: null,
disableWebsiteIcons: false
};
$scope.$on('$viewContentLoaded', function () {
@@ -17,7 +18,7 @@
culture: user.Culture
},
email: user.Email,
twoFactorEnabled: user.TwoFactorEnabled
disableWebsiteIcons: $localStorage.disableWebsiteIcons
};
if (user.Organizations) {
@@ -58,6 +59,13 @@
}).$promise;
};
$scope.optionsSave = function () {
$localStorage.disableWebsiteIcons = cipherService.disableWebsiteIcons = $scope.model.disableWebsiteIcons;
$rootScope.vaultCiphers = null;
toastr.success('Options have been updated.', 'Success!');
};
$scope.changePassword = function () {
$uibModal.open({
animation: true,
@@ -121,6 +129,14 @@
});
};
$scope.purge = function () {
$uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsPurge.html',
controller: 'settingsPurgeController'
});
};
function scrollToTop() {
$('html, body').animate({ scrollTop: 0 }, 200);
}

View File

@@ -4,7 +4,9 @@
.controller('settingsCreateOrganizationController', function ($scope, $state, apiService, cryptoService,
toastr, $analytics, authService, constants, appSettings, validationService
// @if !selfHosted
/* jshint ignore:start */
, stripe
/* jshint ignore:end */
// @endif
) {
$scope.plans = constants.plans;
@@ -113,6 +115,7 @@
additionalStorageGb: model.additionalStorageGb,
billingEmail: model.billingEmail,
businessName: model.ownedBusiness ? model.businessName : null,
country: $scope.paymentMethod === 'card' ? model.card.address_country : null,
collectionName: defaultCollectionCt
};

View File

@@ -4,11 +4,16 @@
.controller('settingsPremiumController', function ($scope, $state, apiService, toastr, $analytics, authService,
constants, $timeout, appSettings, validationService
// @if !selfHosted
/* jshint ignore:start */
, stripe
/* jshint ignore:end */
// @endif
) {
authService.getUserProfile().then(function (profile) {
if (profile.premium) {
var profile = null;
authService.getUserProfile().then(function (theProfile) {
profile = theProfile;
if (profile && profile.premium) {
return $state.go('backend.user.settingsBilling');
}
});
@@ -63,6 +68,11 @@
$scope.submit = function (model, form) {
if ($scope.selfHosted) {
if (profile && !profile.emailVerified) {
validationService.addError(form, null, 'Your account\'s email address first must be verified.', true);
return;
}
var fileEl = document.getElementById('file');
var files = fileEl.files;
if (!files || !files.length) {

View File

@@ -0,0 +1,24 @@
angular
.module('bit.settings')
.controller('settingsPurgeController', function ($scope, $state, apiService, $uibModalInstance, cryptoService,
authService, toastr, $analytics, tokenService) {
$analytics.eventTrack('settingsPurgeController', { category: 'Modal' });
$scope.submit = function (model) {
$scope.submitPromise = cryptoService.hashPassword(model.masterPassword).then(function (hash) {
return apiService.ciphers.purge({
masterPasswordHash: hash
}).$promise;
}).then(function () {
$uibModalInstance.dismiss('cancel');
$analytics.eventTrack('Purged Vault');
return $state.go('backend.user.vault', { refreshFromServer: true });
}).then(function () {
toastr.success('All items in your vault have been deleted.', 'Vault Purged');
});
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@@ -4,7 +4,7 @@
.controller('settingsTwoStepAuthenticatorController', function ($scope, apiService, $uibModalInstance, cryptoService,
authService, $q, toastr, $analytics, constants, $timeout) {
$analytics.eventTrack('settingsTwoStepAuthenticatorController', { category: 'Modal' });
var _issuer = 'bitwarden',
var _issuer = 'Bitwarden',
_profile = null,
_masterPasswordHash,
_key = null;
@@ -44,7 +44,7 @@
$scope.model = {
key: formatString(_key),
qr: 'https://chart.googleapis.com/chart?chs=123x123&chld=L|0&cht=qr&chl=otpauth://totp/' +
qr: 'https://chart.googleapis.com/chart?chs=160x160&chld=L|0&cht=qr&chl=otpauth://totp/' +
_issuer + ':' + encodeURIComponent(_profile.email) +
'%3Fsecret=' + encodeURIComponent(_key) +
'%26issuer=' + _issuer

View File

@@ -3,7 +3,7 @@
.controller('settingsTwoStepController', function ($scope, apiService, toastr, $analytics, constants,
$filter, $uibModal, authService) {
$scope.providers = constants.twoFactorProviderInfo;
$scope.providers = $filter('filter')(constants.twoFactorProviderInfo, { organization: false });
$scope.premium = true;
authService.getUserProfile().then(function (profile) {
@@ -60,7 +60,8 @@
templateUrl: 'app/settings/views/settingsTwoStep' + typeName + '.html',
controller: 'settingsTwoStep' + typeName + 'Controller',
resolve: {
enabled: function () { return provider.enabled; }
enabled: function () { return provider.enabled; },
orgId: function () { return null; }
}
});

View File

@@ -2,7 +2,7 @@
.module('bit.settings')
.controller('settingsTwoStepDuoController', function ($scope, apiService, $uibModalInstance, cryptoService,
toastr, $analytics, constants, $timeout) {
toastr, $analytics, constants, $timeout, orgId) {
$analytics.eventTrack('settingsTwoStepDuoController', { category: 'Modal' });
var _masterPasswordHash;
@@ -20,9 +20,16 @@
$scope.auth = function (model) {
$scope.authPromise = cryptoService.hashPassword(model.masterPassword).then(function (hash) {
_masterPasswordHash = hash;
return apiService.twoFactor.getDuo({}, {
var requestModel = {
masterPasswordHash: _masterPasswordHash
}).$promise;
};
if (orgId) {
return apiService.twoFactor.getOrganizationDuo({ orgId: orgId }, requestModel).$promise;
}
else {
return apiService.twoFactor.getDuo({}, requestModel).$promise;
}
}).then(function (apiResponse) {
processResult(apiResponse);
$scope.authed = true;
@@ -43,27 +50,52 @@
return;
}
$scope.submitPromise = apiService.twoFactor.disable({}, {
masterPasswordHash: _masterPasswordHash,
type: constants.twoFactorProvider.duo
}, function (response) {
$analytics.eventTrack('Disabled Two-step Duo');
toastr.success('Duo has been disabled.');
$scope.enabled = response.Enabled;
$scope.close();
}).$promise;
if (orgId) {
$scope.submitPromise = apiService.twoFactor.disableOrganization({ orgId: orgId }, {
masterPasswordHash: _masterPasswordHash,
type: constants.twoFactorProvider.organizationDuo
}, function (response) {
$analytics.eventTrack('Disabled Two-step Organization Duo');
toastr.success('Duo has been disabled.');
$scope.enabled = response.Enabled;
$scope.close();
}).$promise;
}
else {
$scope.submitPromise = apiService.twoFactor.disable({}, {
masterPasswordHash: _masterPasswordHash,
type: constants.twoFactorProvider.duo
}, function (response) {
$analytics.eventTrack('Disabled Two-step Duo');
toastr.success('Duo has been disabled.');
$scope.enabled = response.Enabled;
$scope.close();
}).$promise;
}
}
function update(model) {
$scope.submitPromise = apiService.twoFactor.putDuo({}, {
var requestModel = {
integrationKey: model.ikey,
secretKey: model.skey,
host: model.host,
masterPasswordHash: _masterPasswordHash
}, function (response) {
$analytics.eventTrack('Enabled Two-step Duo');
processResult(response);
}).$promise;
};
if (orgId) {
$scope.submitPromise = apiService.twoFactor.putOrganizationDuo({ orgId: orgId }, requestModel,
function (response) {
$analytics.eventTrack('Enabled Two-step Organization Duo');
processResult(response);
}).$promise;
}
else {
$scope.submitPromise = apiService.twoFactor.putDuo({}, requestModel,
function (response) {
$analytics.eventTrack('Enabled Two-step Duo');
processResult(response);
}).$promise;
}
}
function processResult(response) {
@@ -80,7 +112,7 @@
closing = true;
$uibModalInstance.close($scope.enabled);
};
$scope.$on('modal.closing', function (e, reason, closed) {
if (closing) {
return;

View File

@@ -28,7 +28,7 @@
$analytics.eventTrack('Print Recovery Code');
var w = window.open();
w.document.write('<div style="font-size: 18px; text-align: center;"><p>bitwarden two-step login recovery code:</p>' +
w.document.write('<div style="font-size: 18px; text-align: center;"><p>Bitwarden two-step login recovery code:</p>' +
'<code style="font-family: Menlo, Monaco, Consolas, \'Courier New\', monospace;">' + $scope.code + '</code>' +
'</div><p style="text-align: center;">' + new Date() + '</p>');
w.print();

View File

@@ -23,7 +23,7 @@
}, function (e) {
throw e ? e : 'Error occurred.';
}).then(function () {
toastr.success('Please log back in. If you are using other bitwarden applications, ' +
toastr.success('Please log back in. If you are using other Bitwarden applications, ' +
'log out and back in to those as well.', 'Key Updated', { timeOut: 10000 });
});
};
@@ -31,19 +31,19 @@
function updateKey(masterPasswordHash) {
var madeEncKey = cryptoService.makeEncKey(null);
var reencryptedLogins = [];
var loginsPromise = apiService.logins.list({}, function (encryptedLogins) {
var filteredEncryptedLogins = [];
for (var i = 0; i < encryptedLogins.Data.length; i++) {
if (encryptedLogins.Data[i].OrganizationId) {
var reencryptedCiphers = [];
var ciphersPromise = apiService.ciphers.list({}, function (encryptedCiphers) {
var filteredEncryptedCiphers = [];
for (var i = 0; i < encryptedCiphers.Data.length; i++) {
if (encryptedCiphers.Data[i].OrganizationId) {
continue;
}
filteredEncryptedLogins.push(encryptedLogins.Data[i]);
filteredEncryptedCiphers.push(encryptedCiphers.Data[i]);
}
var unencryptedLogins = cipherService.decryptLogins(filteredEncryptedLogins);
reencryptedLogins = cipherService.encryptLogins(unencryptedLogins, madeEncKey.encKey);
var unencryptedCiphers = cipherService.decryptCiphers(filteredEncryptedCiphers);
reencryptedCiphers = cipherService.encryptCiphers(unencryptedCiphers, madeEncKey.encKey);
}).$promise;
var reencryptedFolders = [];
@@ -58,10 +58,10 @@
reencryptedPrivateKey = cryptoService.encrypt(privateKey, madeEncKey.encKey, 'raw');
}
return $q.all([loginsPromise, foldersPromise]).then(function () {
return $q.all([ciphersPromise, foldersPromise]).then(function () {
var request = {
masterPasswordHash: masterPasswordHash,
ciphers: reencryptedLogins,
ciphers: reencryptedCiphers,
folders: reencryptedFolders,
privateKey: reencryptedPrivateKey,
key: madeEncKey.encKeyEnc

View File

@@ -9,7 +9,8 @@
<div class="box-header with-border">
<h3 class="box-title">General</h3>
</div>
<form role="form" name="generalForm" ng-submit="generalForm.$valid && generalSave()" api-form="generalPromise">
<form role="form" name="generalForm" ng-submit="generalForm.$valid && generalSave()" api-form="generalPromise"
autocomplete="off">
<div class="box-body">
<div class="row">
<div class="col-sm-9">
@@ -57,7 +58,7 @@
<h3 class="box-title">Master Password</h3>
</div>
<form role="form" name="masterPasswordForm" ng-submit="masterPasswordForm.$valid && passwordHintSave()"
api-form="passwordHintPromise">
api-form="passwordHintPromise" autocomplete="off">
<div class="box-body">
<div class="row">
<div class="col-sm-9">
@@ -85,6 +86,27 @@
</div>
</form>
</div>
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Web Vault Options</h3>
</div>
<form role="form" name="optionsForm" ng-submit="optionsForm.$valid && optionsSave()" autocomplete="off">
<div class="box-body">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="model.disableWebsiteIcons">
Disable Website Icons
</label>
<p class="help-block">Website Icons provide a recognizable image next to each login item in your vault.</p>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="optionsForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="optionsForm.$loading"></i>Save
</button>
</div>
</form>
</div>
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Organizations</h3>
@@ -128,10 +150,13 @@
Careful, these actions are not reversible!
</div>
<div class="box-footer">
<button type="submit" class="btn btn-default btn-flat" ng-click="sessions()">
<button type="button" class="btn btn-default btn-flat" ng-click="sessions()">
Deauthorize Sessions
</button>
<button type="submit" class="btn btn-default btn-flat" ng-click="delete()">
<button type="button" class="btn btn-default btn-flat" ng-click="purge()">
Purge Vault
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="delete()">
Delete Account
</button>
</div>

View File

@@ -2,7 +2,7 @@
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><i class="fa fa-globe"></i> {{index ? 'Edit Equivalent Domain' : 'Add Equivalent Domain'}}</h4>
</div>
<form name="domainAddEditForm" ng-submit="domainAddEditForm.$valid && submit(domainAddEditForm)">
<form name="domainAddEditForm" ng-submit="domainAddEditForm.$valid && submit(domainAddEditForm)" autocomplete="off">
<div class="modal-body">
<div class="callout callout-danger validation-errors" ng-show="domainAddEditForm.$errors">
<h4>Errors have occurred</h4>

View File

@@ -96,7 +96,7 @@
</div>
<div class="box-body">
<p>
You membership has a total of {{storage.maxGb}} GB of encrypted file storage.
Your membership has a total of {{storage.maxGb}} GB of encrypted file storage.
You are currently using {{storage.currentName}}.
</p>
<div class="progress" style="margin: 0;">

View File

@@ -5,7 +5,7 @@
{{add ? 'Add Storage' : 'Remove Storage'}}
</h4>
</div>
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise">
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise" autocomplete="off">
<div class="modal-body">
<div class="callout callout-default" ng-show="add">
<h4><i class="fa fa-dollar"></i> Note About Charges</h4>

View File

@@ -5,7 +5,7 @@
{{existingPaymentMethod ? 'Change Payment Method' : 'Add Payment Method'}}
</h4>
</div>
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise">
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise" autocomplete="off">
<div class="modal-body">
<div class="callout callout-danger validation-errors" ng-show="form.$errors">
<h4>Errors have occurred</h4>

View File

@@ -5,7 +5,7 @@
Update License
</h4>
</div>
<form name="form" ng-submit="form.$valid && submit(form)" api-form="submitPromise">
<form name="form" ng-submit="form.$valid && submit(form)" api-form="submitPromise" autocomplete="off">
<div class="modal-body">
<div class="callout callout-danger validation-errors" ng-show="form.$errors">
<h4>Errors have occurred</h4>

View File

@@ -30,12 +30,13 @@
</div>
</form>
<form name="changeEmailConfirmForm" ng-submit="changeEmailConfirmForm.$valid && confirm(model)" api-form="confirmPromise"
ng-show="tokenSent">
ng-show="tokenSent" autocomplete="off">
<div class="modal-body">
<p>We have emailed a verification code to <b>{{model.newEmail}}</b>. Please check your email for this code and enter it below to finalize your the email address change.</p>
<div class="callout callout-warning">
<h4><i class="fa fa-warning"></i> Warning</h4>
Proceeding will log you out of your current session, requiring you to log back in.
Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices
may continue to remain active for up to one hour.
</div>
<div class="callout callout-danger validation-errors" ng-show="changeEmailConfirmForm.$errors">
<h4>Errors have occurred</h4>

View File

@@ -8,7 +8,8 @@
<p>We recommend that you change your master password immediately if you believe that your credentials have been compromised.</p>
<div class="callout callout-warning">
<h4><i class="fa fa-warning"></i> Warning</h4>
Proceeding will log you out of your current session, requiring you to log back in.
Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices
may continue to remain active for up to one hour.
</div>
<div class="callout callout-danger validation-errors" ng-show="changePasswordForm.$errors">
<h4>Errors have occurred</h4>

View File

@@ -93,17 +93,17 @@
</div>
<div class="radio radio-block" ng-if="!model.ownedBusiness" ng-click="changedPlan()">
<label>
<input type="radio" ng-model="model.plan" name="PlanType" value="personal">
Personal
<span>For personal users such as families &amp; friends.</span>
<span>- Add and share with up to 10 users (5 included with base price)</span>
<input type="radio" ng-model="model.plan" name="PlanType" value="families">
Families
<span>For personal use, to share with family &amp; friends.</span>
<span>- Add and share with up to 5 users</span>
<span>- Create unlimited collections</span>
<span>- 1 GB encrypted file storage</span>
<span>- Self-hosting (optional)</span>
<span>- Priority customer support</span>
<span>- 7 day free trial, cancel anytime</span>
<span class="bottom-line">
{{plans.personal.basePrice | currency:'$'}} /month includes {{plans.personal.baseSeats}} users,
additional users {{plans.personal.seatPrice | currency:'$'}} /month
{{plans.families.basePrice | currency:'$'}} /month includes {{plans.families.baseSeats}} users
</span>
</label>
</div>
@@ -243,7 +243,7 @@
</span>
</label>
</div>
<div class="radio radio-block" ng-if="model.plan !== 'personal'">
<div class="radio radio-block" ng-if="model.plan !== 'families'">
<label>
<input type="radio" ng-model="model.interval" name="BillingInterval" value="month">
Monthly

View File

@@ -4,9 +4,9 @@
<section class="content">
<p>
If you have the same login across multiple different website domains, you can mark the website as "equivalent".
"Global" domains are ones already created for you by bitwarden.
"Global" domains are ones already created for you by Bitwarden.
</p>
<form name="customForm" ng-submit="customForm.$valid && saveCustom()" api-form="customPromise">
<form name="customForm" ng-submit="customForm.$valid && saveCustom()" api-form="customPromise" autocomplete="off">
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Custom <span class="hidden-xs">Equivalent Domains</span></h3>
@@ -59,7 +59,7 @@
</div>
</form>
<form name="globalForm" ng-submit="globalForm.$valid && saveGlobal()" api-form="globalPromise">
<form name="globalForm" ng-submit="globalForm.$valid && saveGlobal()" api-form="globalPromise" autocomplete="off">
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Global <span class="hidden-xs">Equivalent Domains</span></h3>

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