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

Compare commits

...

255 Commits

Author SHA1 Message Date
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
Kyle Spearrin
cc39e6402e version bump 2017-09-01 14:17:40 -04:00
Kyle Spearrin
c89b641b88 default collection on org create 2017-08-30 21:27:04 -04:00
Kyle Spearrin
465304b004 only show selected collection that are writeable 2017-08-30 17:09:22 -04:00
Kyle Spearrin
63033ca12d pull only writable collections when editing 2017-08-30 15:58:51 -04:00
Kyle Spearrin
f019dc6575 lint fix 2017-08-30 15:06:24 -04:00
Kyle Spearrin
d15e3a64e7 update libs 2017-08-30 15:04:05 -04:00
Kyle Spearrin
7099b0579a named args to server 2017-08-25 11:00:19 -04:00
Kyle Spearrin
2c2d08c7cc make sure key is generated on self host create 2017-08-22 08:37:07 -04:00
Kyle Spearrin
671e9ccb1c script fixes 2017-08-19 22:36:09 -04:00
Kyle Spearrin
f93c5cb9a1 finalize create properly 2017-08-17 00:57:25 -04:00
Kyle Spearrin
8c7f1c4359 copy updates 2017-08-16 15:18:30 -04:00
Kyle Spearrin
d7c1c6efa1 can only edit org when not self hosted 2017-08-16 14:08:11 -04:00
Kyle Spearrin
30a2301697 prompt for installation id and download license 2017-08-15 16:18:31 -04:00
Kyle Spearrin
c639186c60 correct billing icon 2017-08-15 15:37:59 -04:00
Kyle Spearrin
5618cfb031 use btiwarden kestrel server isntead of node 2017-08-15 11:57:04 -04:00
Kyle Spearrin
7e97c04d1e web vault page title 2017-08-15 10:12:08 -04:00
Kyle Spearrin
4d25077108 more preprocessing for self host 2017-08-15 10:05:39 -04:00
Kyle Spearrin
635caa9ad0 preprocess dist for self hosted 2017-08-15 09:16:19 -04:00
Kyle Spearrin
2772bffd09 qr code size and clean token on delete 2017-08-15 08:24:14 -04:00
Kyle Spearrin
995fc96a5d create and mange org through licensing 2017-08-14 22:06:51 -04:00
Kyle Spearrin
4660ad824d on premise feature on enterprise list 2017-08-14 13:13:39 -04:00
Kyle Spearrin
801049cbd0 billing & licensing 2017-08-14 13:08:48 -04:00
Kyle Spearrin
09a7b4ea90 billing license management when self hosted 2017-08-14 12:10:00 -04:00
Kyle Spearrin
226c201925 bank account payment method for orgs 2017-08-14 10:21:08 -04:00
Kyle Spearrin
4749a3da89 import 1password fields even if no name 2017-08-12 12:14:59 -04:00
Kyle Spearrin
ae567ab462 import totp keys from 1password 1pif export 2017-08-12 12:06:00 -04:00
Kyle Spearrin
bf382889d3 enpass import TOTP field resolves #8 2017-08-11 23:31:48 -04:00
Kyle Spearrin
2272bcac71 licensing options when self hosted 2017-08-11 23:23:14 -04:00
Kyle Spearrin
a209c9450a delete recovery token apis 2017-08-10 10:15:10 -04:00
Kyle Spearrin
2539a9c23f account recovery with delete 2017-08-09 10:44:49 -04:00
Kyle Spearrin
e95ede73ba fix bug with password going into username field 2017-08-09 08:24:16 -04:00
Kyle Spearrin
ad970b1cb7 dockerignore 2017-08-08 17:50:48 -04:00
Kyle Spearrin
161e7d1763 copy app-id.json for u2f 2017-08-08 00:44:58 -04:00
Kyle Spearrin
3a823d32b5 copy appsettings on entrypoint 2017-08-08 00:03:10 -04:00
Kyle Spearrin
4c46317f24 extension appsettings with runtime loadable props 2017-08-07 21:08:15 -04:00
Kyle Spearrin
0271c223a6 false dir listing command 2017-08-07 17:17:00 -04:00
Kyle Spearrin
9a4669067d docker image 2017-08-07 17:07:56 -04:00
Kyle Spearrin
53f3124345 paypal option 2017-08-04 13:11:25 -04:00
Kyle Spearrin
b49a40b077 unhide paypal option with braintree 2017-08-04 13:09:34 -04:00
Kyle Spearrin
fb10da8ce3 terms links 2017-08-04 11:43:21 -04:00
Kyle Spearrin
b286c1a29b version bump 2017-08-01 00:14:09 -04:00
Kyle Spearrin
e5e7712716 catch decryption failure on login previews 2017-08-01 00:13:10 -04:00
Kyle Spearrin
2beb22e8cf added error logs for decrypt methods 2017-07-31 23:19:02 -04:00
Kyle Spearrin
747b5608e8 re-worked change password, email, and update key 2017-07-31 22:53:27 -04:00
Kyle Spearrin
dad3cd9414 add samsung to unsupported browsers 2017-07-31 13:24:58 -04:00
Kyle Spearrin
0c1fb3e118 catch and throw proper stripe error message 2017-07-29 16:44:21 -04:00
169 changed files with 18866 additions and 4716 deletions

3
.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
*
!dist/*
!entrypoint.sh

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

15
Dockerfile Normal file
View File

@@ -0,0 +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 .
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,8 +1,8 @@
[![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" />

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!

13
build.ps1 Normal file
View File

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

33
build.sh Normal file
View File

@@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -e
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
echo ""
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 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 62e62e3684

64
entrypoint.sh Normal file
View File

@@ -0,0 +1,64 @@
#!/bin/bash
# Setup
GROUPNAME="bitwarden"
USERNAME="bitwarden"
CURRENTGID=`getent group $GROUPNAME | cut -d: -f3`
LGID=${LOCAL_GID:-999}
NOUSER=`id -u $USERNAME > /dev/null 2>&1; echo $?`
LUID=${LOCAL_UID:-999}
# Step down from host root
if [ $LGID == 0 ]
then
LGID=999
fi
if [ $LUID == 0 ]
then
LUID=999
fi
# Create group
if [ $CURRENTGID ]
then
if [ "$CURRENTGID" != "$LGID" ]
then
groupmod -g $LGID $GROUPNAME
fi
else
groupadd -g $LGID $GROUPNAME
fi
# Create user and assign group
if [ $NOUSER == 0 ] && [ `id -u $USERNAME` != $LUID ]
then
usermod -u $LUID $USERNAME
elif [ $NOUSER == 1 ]
then
useradd -r -u $LUID -g $GROUPNAME $USERNAME
fi
# Make home directory for user
if [ ! -d "/home/$USERNAME" ]
then
mkhomedir_helper $USERNAME
fi
# The rest...
chown -R $USERNAME:$GROUPNAME /etc/bitwarden
cp /etc/bitwarden/web/settings.js /app/js/settings.js
cp /etc/bitwarden/web/app-id.json /app/app-id.json
chown -R $USERNAME:$GROUPNAME /app
chown -R $USERNAME:$GROUPNAME /bitwarden_server
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,10 +72,13 @@ gulp.task('min:js', ['clean:js'], function () {
'!' + paths.minJs,
'!' + paths.jsDir + 'fallback*.js',
'!' + paths.jsDir + 'u2f-connector.js',
'!' + paths.jsDir + 'duo.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('.'));
});
@@ -240,6 +244,7 @@ function config() {
createModule: false,
constants: _.merge({}, {
appSettings: {
selfHosted: false,
version: project.version,
environment: project.env
}
@@ -288,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 () {
@@ -301,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'
},
@@ -332,6 +337,14 @@ 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'
},
{
src: paths.jsDir + 'bw.min.js',
dest: paths.dist + 'js'
@@ -344,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
@@ -363,7 +377,7 @@ gulp.task('dist:css', function () {
paths.cssDir + '**/*.css',
'!' + paths.cssDir + '**/*.min.css'
])
.pipe(preprocess({ context: { cacheTag: randomString } }))
.pipe(preprocess({ context: { cacheTag: randomString, selfHosted: selfHosted } }))
.pipe(cssmin())
.pipe(rename({ suffix: '.min' }))
.pipe(gulp.dest(paths.dist + 'css'));
@@ -379,10 +393,10 @@ gulp.task('dist:js:app', function () {
]);
merge(mainStream, config())
.pipe(preprocess({ context: { cacheTag: randomString } }))
.pipe(preprocess({ context: { cacheTag: randomString, selfHosted: selfHosted } }))
.pipe(concat(paths.dist + '/js/app.min.js'))
.pipe(ngAnnotate())
.pipe(uglify())
//.pipe(uglify())
.pipe(gulp.dest('.'));
});
@@ -393,8 +407,8 @@ gulp.task('dist:js:fallback', function () {
]);
merge(mainStream)
.pipe(preprocess({ context: { cacheTag: randomString } }))
.pipe(uglify())
.pipe(preprocess({ context: { cacheTag: randomString, selfHosted: selfHosted } }))
//.pipe(uglify())
.pipe(rename({ suffix: '.min' }))
.pipe(gulp.dest(paths.dist + 'js'));
});
@@ -407,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('.'));
});
@@ -422,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('.'));
});
@@ -431,18 +445,30 @@ gulp.task('dist:preprocess', function () {
.src([
paths.dist + '/**/*.html'
], { base: '.' })
.pipe(preprocess({ context: { cacheTag: randomString } }))
.pipe(preprocess({ context: { cacheTag: randomString, selfHosted: selfHosted } }))
.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);
});
var selfHosted = false;
gulp.task('dist:selfHosted', function (cb) {
selfHosted = true;
return runSequence('dist', cb);
});
gulp.task('deploy', ['dist'], function () {
return gulp.src(paths.dist + '**/*')
.pipe(ghPages({ cacheDir: paths.dist + '.publish' }));
@@ -452,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,51 +1,52 @@
{
"name": "bitwarden",
"version": "1.14.1",
"version": "1.26.0",
"env": "Production",
"devDependencies": {
"connect": "3.6.0",
"connect": "3.6.5",
"lodash": "4.17.4",
"gulp": "3.9.1",
"gulp-concat": "2.6.1",
"gulp-cssmin": "0.1.7",
"gulp-less": "3.3.0",
"gulp-cssmin": "0.2.0",
"gulp-less": "3.3.2",
"gulp-rename": "1.2.2",
"gulp-uglify": "2.1.2",
"gulp-gh-pages": "0.5.4",
"gulp-uglify": "3.0.0",
"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",
"jshint": "2.9.4",
"gulp-json-editor": "2.2.2",
"jshint": "2.9.5",
"gulp-jshint": "2.0.4",
"rimraf": "2.6.1",
"run-sequence": "1.2.2",
"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.3",
"angular-resource": "1.6.3",
"angular-sanitize": "1.6.3",
"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.3",
"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.3",
"angular-messages": "1.6.7",
"ngstorage": "0.3.11",
"papaparse": "4.2.0",
"clipboard": "1.6.1",
"ngclipboard": "1.1.1",
"angulartics": "1.4.0",
"papaparse": "4.3.6",
"clipboard": "1.7.1",
"ngclipboard": "1.1.2",
"angulartics": "1.5.0",
"angulartics-google-analytics": "0.4.0",
"node-forge": "0.7.1",
"webpack-stream": "3.2.0",
"angular-stripe": "4.2.12",
"webpack-stream": "4.0.0",
"angular-stripe": "5.0.0",
"angular-credit-cards": "3.1.6",
"browserify": "14.1.0",
"browserify": "14.5.0",
"vinyl-source-stream": "1.1.0",
"gulp-derequire": "2.1.0",
"exposify": "0.5.0",

View File

@@ -1,7 +1,8 @@
{
"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"
}

View File

@@ -1,8 +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": "TODO"
"braintreeKey": "production_qfbsv8kc_njj2zjtyngtjmbjd"
}
}

View File

@@ -2,6 +2,7 @@
"appSettings": {
"apiUri": "http://localhost:4000",
"identityUri": "http://localhost:33656",
"iconsUri": "https://icons.bitwarden.com",
"stripeKey": "pk_test_KPoCfZXu7mznb9uSCPZ2JpTD",
"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

@@ -0,0 +1,13 @@
angular
.module('bit.accounts')
.controller('accountsRecoverDeleteController', function ($scope, $rootScope, apiService, $analytics) {
$scope.success = false;
$scope.submit = function (model) {
$scope.submitPromise = apiService.accounts.postDeleteRecover({ email: model.email }, function () {
$analytics.eventTrack('Started Delete Recovery');
$scope.success = true;
}).$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

@@ -0,0 +1,36 @@
angular
.module('bit.accounts')
.controller('accountsVerifyRecoverDeleteController', function ($scope, $state, apiService, toastr, $analytics) {
if (!$state.params.userId || !$state.params.token || !$state.params.email) {
$state.go('frontend.login.info').then(function () {
toastr.error('Invalid parameters.');
});
return;
}
$scope.email = $state.params.email;
$scope.delete = function () {
if (!confirm('Are you sure you want to delete this account? This cannot be undone.')) {
return;
}
$scope.deleting = true;
apiService.accounts.postDeleteRecoverToken({},
{
token: $state.params.token,
userId: $state.params.userId
}, function () {
$analytics.eventTrack('Recovered Delete');
$state.go('frontend.login.info', null, { location: 'replace' }).then(function () {
toastr.success('Your account has been deleted. You can register a new account again if you like.',
'Success');
});
}, function () {
$state.go('frontend.login.info', null, { location: 'replace' }).then(function () {
toastr.error('Unable to delete account.', 'Error');
});
});
};
});

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

@@ -0,0 +1,39 @@
<div class="login-box">
<div class="login-logo">
<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>
<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.
</div>
<a ui-sref="frontend.login.info">Return to log in</a>
</div>
<form name="form" ng-submit="form.$valid && submit(model)" ng-show="!success"
api-form="submitPromise">
<div class="callout callout-danger validation-errors" ng-show="form.$errors">
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in form.$errors">{{e}}</li>
</ul>
</div>
<div class="form-group has-feedback" show-errors>
<label for="email" class="sr-only">Your account email address</label>
<input type="email" id="email" name="Email" class="form-control" placeholder="Your account email address"
ng-model="model.email" required api-field />
<span class="fa fa-envelope form-control-feedback"></span>
</div>
<div class="row">
<div class="col-xs-7">
<a ui-sref="frontend.login.info">Return to log in</a>
</div>
<div class="col-xs-5">
<button type="submit" class="btn btn-primary btn-block btn-flat" ng-disabled="form.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="form.$loading"></i>Submit
</button>
</div>
</div>
</form>
</div>
</div>

View File

@@ -72,6 +72,11 @@
</button>
</div>
</div>
<hr />
By clicking the above "Submit" button, you are agreeing to the
<a href="https://bitwarden.com/terms/" target="_blank">Terms of Service</a>
and the
<a href="https://bitwarden.com/privacy/" target="_blank">Privacy Policy</a>.
</form>
</div>
</div>

View File

@@ -0,0 +1,21 @@
<div class="login-box">
<div class="login-logo">
<i class="fa fa-shield"></i> <b>bit</b>warden
</div>
<div class="login-box-body">
<div ng-if="deleting">
Deleting account...
</div>
<div ng-if="!deleting">
<div class="callout callout-warning">
<h4><i class="fa fa-warning fa-fw"></i> Warning</h4>
This will permanently delete your account. This cannot be undone.
</div>
<p>
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>
</div>
</div>
</div>

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

@@ -6,9 +6,11 @@
'ui.bootstrap.showErrors',
'toastr',
'angulartics',
// @if !selfHosted
'angulartics.google.analytics',
'angular-stripe',
'credit-cards',
// @endif
'angular-promise-polyfill',
'bit.directives',

View File

@@ -2,16 +2,25 @@ angular
.module('bit')
.config(function ($stateProvider, $urlRouterProvider, $httpProvider, jwtInterceptorProvider, jwtOptionsProvider,
$uibTooltipProvider, toastrConfig, $locationProvider, $qProvider, stripeProvider, appSettings) {
$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_token3',
whiteListedDomains: ['api.bitwarden.com', 'preview-api.bitwarden.com', 'localhost', '192.168.1.3']
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;
}
@@ -28,14 +37,21 @@ 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;
});
return refreshPromise;
};
// @if !selfHosted
stripeProvider.setPublishableKey(appSettings.stripeKey);
// @endif
angular.extend(toastrConfig, {
closeButton: true,
@@ -49,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) {
@@ -94,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',
@@ -148,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
@@ -165,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' }
@@ -189,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', {
@@ -261,6 +268,24 @@ angular
bodyClass: 'login-page'
}
})
.state('frontend.recover-delete', {
url: '^/recover-delete',
templateUrl: 'app/accounts/views/accountsRecoverDelete.html',
controller: 'accountsRecoverDeleteController',
data: {
pageTitle: 'Delete Account',
bodyClass: 'login-page'
}
})
.state('frontend.verify-recover-delete', {
url: '^/verify-recover-delete?userId&token&email',
templateUrl: 'app/accounts/views/accountsVerifyRecoverDelete.html',
controller: 'accountsVerifyRecoverDeleteController',
data: {
pageTitle: 'Confirm Delete Account',
bodyClass: 'login-page'
}
})
.state('frontend.register', {
url: '^/register?org&premium',
templateUrl: 'app/accounts/views/accountsRegister.html',
@@ -327,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,9 +6,9 @@ angular
link: function (scope, element) {
var listener = function (event, toState, toParams, fromState, fromParams) {
// Default title
var title = 'bitwarden Password Manager';
var title = 'Bitwarden Web Vault';
if (toState.data && toState.data.pageTitle) {
title = toState.data.pageTitle + ' - bitwarden Password Manager';
title = toState.data.pageTitle + ' - ' + title;
}
$timeout(function () {

View File

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

View File

@@ -4,11 +4,13 @@ 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;
vm.version = appSettings.version;
vm.outdatedBrowser = navigator.userAgent.indexOf('MSIE') !== -1;
vm.outdatedBrowser = $window.navigator.userAgent.indexOf('MSIE') !== -1 ||
$window.navigator.userAgent.indexOf('SamsungBrowser') !== -1;
$scope.currentYear = new Date().getFullYear();
@@ -47,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

@@ -1,21 +1,48 @@
angular
.module('bit.organization')
.controller('organizationBillingChangePaymentController', function ($scope, $state, $uibModalInstance, apiService, stripe,
$analytics, toastr, existingPaymentMethod) {
.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' });
$scope.existingPaymentMethod = existingPaymentMethod;
$scope.paymentMethod = 'card';
$scope.showPaymentOptions = false;
$scope.showPaymentOptions = true;
$scope.hidePaypal = true;
$scope.card = {};
$scope.bank = {};
$scope.changePaymentMethod = function (val) {
$scope.paymentMethod = val;
};
$scope.submit = function () {
$scope.submitPromise = stripe.card.createToken($scope.card).then(function (response) {
var stripeReq = null;
if ($scope.paymentMethod === 'card') {
stripeReq = stripe.card.createToken($scope.card);
}
else if ($scope.paymentMethod === 'bank') {
$scope.bank.currency = 'USD';
$scope.bank.country = 'US';
stripeReq = stripe.bankAccount.createToken($scope.bank);
}
else {
return;
}
$scope.submitPromise = stripeReq.then(function (response) {
var request = {
paymentToken: response.id
};
return apiService.organizations.putPayment({ id: $state.params.orgId }, request).$promise;
}, function (err) {
throw err.message;
}).then(function (response) {
$scope.card = null;
if (existingPaymentMethod) {

View File

@@ -1,18 +1,26 @@
angular
.module('bit.organization')
.controller('organizationBillingController', function ($scope, apiService, $state, $uibModal, toastr, $analytics) {
.controller('organizationBillingController', function ($scope, apiService, $state, $uibModal, toastr, $analytics,
appSettings, tokenService, $window) {
$scope.selfHosted = appSettings.selfHosted;
$scope.charges = [];
$scope.paymentSource = null;
$scope.plan = null;
$scope.subscription = null;
$scope.loading = true;
var license = null;
$scope.expiration = null;
$scope.$on('$viewContentLoaded', function () {
load();
});
$scope.changePayment = function () {
if ($scope.selfHosted) {
return;
}
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsBillingChangePayment.html',
@@ -30,6 +38,10 @@
};
$scope.changePlan = function () {
if ($scope.selfHosted) {
return;
}
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationBillingChangePlan.html',
@@ -47,6 +59,10 @@
};
$scope.adjustSeats = function (add) {
if ($scope.selfHosted || !$scope.canAdjustSeats) {
return;
}
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationBillingAdjustSeats.html',
@@ -64,6 +80,10 @@
};
$scope.adjustStorage = function (add) {
if ($scope.selfHosted) {
return;
}
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsBillingAdjustStorage.html',
@@ -80,7 +100,27 @@
});
};
$scope.verifyBank = function () {
if ($scope.selfHosted) {
return;
}
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationBillingVerifyBank.html',
controller: 'organizationBillingVerifyBankController'
});
modal.result.then(function () {
load();
});
};
$scope.cancel = function () {
if ($scope.selfHosted) {
return;
}
if (!confirm('Are you sure you want to cancel? All users will lose access to the organization ' +
'at the end of this billing cycle.')) {
return;
@@ -95,6 +135,10 @@
};
$scope.reinstate = function () {
if ($scope.selfHosted) {
return;
}
if (!confirm('Are you sure you want to remove the cancellation request and reinstate this organization?')) {
return;
}
@@ -107,12 +151,81 @@
});
};
$scope.updateLicense = function () {
if (!$scope.selfHosted) {
return;
}
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsBillingUpdateLicense.html',
controller: 'organizationBillingUpdateLicenseController'
});
modal.result.then(function () {
load();
});
};
$scope.license = function () {
if ($scope.selfHosted) {
return;
}
var installationId = prompt("Enter your installation id");
if (!installationId || installationId === '') {
return;
}
apiService.organizations.getLicense({
id: $state.params.orgId,
installationId: installationId
}, function (license) {
var licenseString = JSON.stringify(license, null, 2);
var licenseBlob = new Blob([licenseString]);
// IE hack. ref http://msdn.microsoft.com/en-us/library/ie/hh779016.aspx
if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveBlob(licenseBlob, 'bitwarden_organization_license.json');
}
else {
var a = window.document.createElement('a');
a.href = window.URL.createObjectURL(licenseBlob, { type: 'text/plain' });
a.download = 'bitwarden_organization_license.json';
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);
}
}, function (err) {
if (err.status === 400) {
toastr.error("Invalid installation id.");
}
else {
toastr.error("Unable to generate license.");
}
});
};
$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;
license = org.License;
$scope.plan = {
name: org.Plan,
@@ -167,7 +280,8 @@
$scope.paymentSource = {
type: org.PaymentSource.Type,
description: org.PaymentSource.Description,
cardBrand: org.PaymentSource.CardBrand
cardBrand: org.PaymentSource.CardBrand,
needsVerification: org.PaymentSource.NeedsVerification
};
}

View File

@@ -0,0 +1,30 @@
angular
.module('bit.organization')
.controller('organizationBillingUpdateLicenseController', function ($scope, $state, $uibModalInstance, apiService,
$analytics, toastr, validationService) {
$analytics.eventTrack('organizationBillingUpdateLicenseController', { category: 'Modal' });
$scope.submit = function (form) {
var fileEl = document.getElementById('file');
var files = fileEl.files;
if (!files || !files.length) {
validationService.addError(form, 'file', 'Select a license file.', true);
return;
}
var fd = new FormData();
fd.append('license', files[0]);
$scope.submitPromise = apiService.organizations.putLicense({ id: $state.params.orgId }, fd)
.$promise.then(function (response) {
$analytics.eventTrack('Updated License');
toastr.success('You have updated your license.');
$uibModalInstance.close();
});
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@@ -0,0 +1,25 @@
angular
.module('bit.organization')
.controller('organizationBillingVerifyBankController', function ($scope, $state, $uibModalInstance, apiService,
$analytics, toastr) {
$analytics.eventTrack('organizationBillingVerifyBankController', { category: 'Modal' });
$scope.submit = function () {
var request = {
amount1: $scope.amount1,
amount2: $scope.amount2
};
$scope.submitPromise = apiService.organizations.postVerifyBank({ id: $state.params.orgId }, request)
.$promise.then(function (response) {
$analytics.eventTrack('Verified Bank Account');
toastr.success('You have successfully verified your bank account.');
$uibModalInstance.close();
});
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

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,19 +2,55 @@
.module('bit.organization')
.controller('organizationSettingsController', function ($scope, $state, apiService, toastr, authService, $uibModal,
$analytics) {
$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;
}
}
});
});
$scope.generalSave = function () {
if ($scope.selfHosted) {
return;
}
$scope.generalPromise = apiService.organizations.put({ id: $state.params.orgId }, $scope.model, function (org) {
authService.updateProfileOrganization(org).then(function (updatedOrg) {
$analytics.eventTrack('Updated Organization Settings');
@@ -23,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,
@@ -30,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

@@ -1,7 +1,7 @@
<section class="content-header">
<h1>
Billing
<small>manage your payments</small>
<small>manage your billing &amp; licensing</small>
</h1>
</section>
<section class="content">
@@ -26,14 +26,28 @@
<div class="box-body">
<div class="row">
<div class="col-sm-6">
<dl>
<dl ng-if="selfHosted">
<dt>Name</dt>
<dd>{{plan.name || '-'}}</dd>
<dt>Expiration</dt>
<dd ng-if="loading">
Loading...
</dd>
<dd ng-if="!loading && expiration">
{{expiration | date: 'medium'}}
</dd>
<dd ng-if="!loading && !expiration">
Never expires
</dd>
</dl>
<dl ng-if="!selfHosted">
<dt>Name</dt>
<dd>{{plan.name || '-'}}</dd>
<dt>Total Seats</dt>
<dd>{{plan.seats || '-'}}</dd>
</dl>
</div>
<div class="col-sm-6">
<div class="col-sm-6" ng-if="!selfHosted">
<dl>
<dt>Status</dt>
<dd>
@@ -41,11 +55,11 @@
<span ng-if="subscription.markedForCancel">- marked for cancellation</span>
</dd>
<dt>Next Charge</dt>
<dd>{{nextInvoice ? ((nextInvoice.date | date: format: mediumDate) + ', ' + (nextInvoice.amount | currency:'$')) : '-'}}</dd>
<dd>{{nextInvoice ? ((nextInvoice.date | date: 'mediumDate') + ', ' + (nextInvoice.amount | currency:'$')) : '-'}}</dd>
</dl>
</div>
</div>
<div class="row" ng-if="!noSubscription">
<div class="row" ng-if="!selfHosted && !noSubscription">
<div class="col-md-6">
<strong>Details</strong>
<div ng-show="loading">
@@ -67,7 +81,7 @@
</div>
</div>
</div>
<div class="box-footer">
<div class="box-footer" ng-if="!selfHosted">
<button type="button" class="btn btn-default btn-flat" ng-click="changePlan()">
Change Plan
</button>
@@ -79,6 +93,18 @@
ng-if="!noSubscription && subscription.markedForCancel">
Reinstate Plan
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="license()"
ng-if="!subscription.cancelled">
Download License
</button>
</div>
<div class="box-footer" ng-if="selfHosted">
<button type="button" class="btn btn-default btn-flat" ng-click="updateLicense()">
Update License
</button>
<a href="https://vault.bitwarden.com" class="btn btn-default btn-flat" target="_blank">
Manage Billing
</a>
</div>
</div>
<div class="box box-default">
@@ -90,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="!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>
@@ -102,13 +128,13 @@
</button>
</div>
</div>
<div class="box box-default" ng-if="storage">
<div class="box box-default" ng-if="storage && !selfHosted">
<div class="box-header with-border">
<h3 class="box-title">Storage</h3>
</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;">
@@ -128,7 +154,7 @@
</button>
</div>
</div>
<div class="box box-default">
<div class="box box-default" ng-if="!selfHosted">
<div class="box-header with-border">
<h3 class="box-title">Payment Method</h3>
</div>
@@ -140,6 +166,15 @@
<i class="fa fa-credit-card"></i> No payment method on file.
</div>
<div ng-show="!loading && paymentSource">
<div class="callout callout-warning" ng-if="paymentSource.type === 1 && paymentSource.needsVerification">
<h4><i class="fa fa-warning"></i> You must verify your bank account</h4>
<p>
We have made two micro-deposits to your bank account (it may take 1-2 business days to show up).
Enter these amounts to verify the bank account. Failure to verify the bank account will result in a
missed payment and your organization being disabled.
</p>
<button class="btn btn-default btn-flat" ng-click="verifyBank()">Verify Now</button>
</div>
<i class="fa" ng-class="{'fa-credit-card': paymentSource.type === 0,
'fa-university': paymentSource.type === 1, 'fa-paypal fa-fw text-blue': paymentSource.type === 2}"></i>
{{paymentSource.description}}
@@ -151,7 +186,7 @@
</button>
</div>
</div>
<div class="box box-default">
<div class="box box-default" ng-if="!selfHosted">
<div class="box-header with-border">
<h3 class="box-title">Charges</h3>
</div>
@@ -166,8 +201,13 @@
<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: format: mediumDate}}
{{charge.date | date: 'mediumDate'}}
</td>
<td style="min-width: 150px">
{{charge.paymentSource}}

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

@@ -0,0 +1,43 @@
<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-check-square-o"></i>
Verify Bank Account
</h4>
</div>
<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.
For example, if we deposited $0.32 and $0.45 you would enter the values "32" and "45".
</p>
<div class="callout callout-danger validation-errors" ng-show="form.$errors">
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in form.$errors">{{e}}</li>
</ul>
</div>
<div class="form-group">
<label for="amount1">Amount 1</label>
<div class="input-group">
<span class="input-group-addon">$ 0.</span>
<input type="number" id="amount1" name="Amount1" ng-model="amount1" class="form-control"
required min="1" max="99" placeholder="xx" />
</div>
</div>
<div class="form-group">
<label for="amount2">Amount 2</label>
<div class="input-group">
<span class="input-group-addon">$ 0.</span>
<input type="number" id="amount2" name="Amount2" ng-model="amount2" class="form-control"
required min="1" max="99" placeholder="xx" />
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="form.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="form.$loading"></i>Submit
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
</div>
</form>

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">
@@ -22,17 +23,30 @@
<div class="form-group" show-errors>
<label for="name">Organization Name</label>
<input type="text" id="name" name="Name" ng-model="model.name" class="form-control"
required api-field />
</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 />
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 />
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 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">
@@ -42,13 +56,58 @@
</div>
</div>
</div>
<div class="box-footer">
<div class="box-footer" ng-if="!selfHosted">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="generalForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="generalForm.$loading"></i>Save
</button>
</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,17 @@
_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 } },
list: { method: 'GET', params: {} },
listDetails: { url: _apiUri + '/ciphers/details', 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,24 +35,33 @@
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', {}, {
get: { method: 'GET', params: { id: '@id' } },
getBilling: { url: _apiUri + '/organizations/:id/billing', method: 'GET', params: { id: '@id' } },
getLicense: { url: _apiUri + '/organizations/:id/license', method: 'GET', params: { id: '@id' } },
list: { method: 'GET', params: {} },
post: { method: 'POST', params: {} },
put: { method: 'POST', params: { id: '@id' } },
@@ -69,7 +72,18 @@
putCancel: { url: _apiUri + '/organizations/:id/cancel', method: 'POST', params: { id: '@id' } },
putReinstate: { url: _apiUri + '/organizations/:id/reinstate', method: 'POST', params: { id: '@id' } },
postLeave: { url: _apiUri + '/organizations/:id/leave', method: 'POST', params: { id: '@id' } },
del: { url: _apiUri + '/organizations/:id/delete', method: 'POST', params: { id: '@id' } }
postVerifyBank: { url: _apiUri + '/organizations/:id/verify-bank', method: 'POST', params: { id: '@id' } },
del: { url: _apiUri + '/organizations/:id/delete', method: 'POST', params: { id: '@id' } },
postLicense: {
url: _apiUri + '/organizations/license',
method: 'POST',
headers: { 'Content-Type': undefined }
},
putLicense: {
url: _apiUri + '/organizations/:id/license',
method: 'POST',
headers: { 'Content-Type': undefined }
}
});
_service.organizationUsers = $resource(_apiUri + '/organizations/:orgId/users/:id', {}, {
@@ -88,7 +102,7 @@
_service.collections = $resource(_apiUri + '/organizations/:orgId/collections/:id', {}, {
get: { method: 'GET', params: { id: '@id', orgId: '@orgId' } },
getDetails: { url: _apiUri + '/organizations/:orgId/collections/:id/details', method: 'GET', params: { id: '@id', orgId: '@orgId' } },
listMe: { url: _apiUri + '/collections', method: 'GET', params: {} },
listMe: { url: _apiUri + '/collections?writeOnly=:writeOnly', method: 'GET', params: { writeOnly: '@writeOnly' } },
listOrganization: { method: 'GET', params: { orgId: '@orgId' } },
listUsers: { url: _apiUri + '/organizations/:orgId/collections/:id/users', method: 'GET', params: { id: '@id', orgId: '@orgId' } },
post: { method: 'POST', params: { orgId: '@orgId' } },
@@ -114,6 +128,8 @@
email: { url: _apiUri + '/accounts/email', method: 'POST', params: {} },
verifyEmailToken: { url: _apiUri + '/accounts/verify-email-token', method: 'POST', params: {} },
verifyEmail: { url: _apiUri + '/accounts/verify-email', method: 'POST', params: {} },
postDeleteRecoverToken: { url: _apiUri + '/accounts/delete-recover-token', method: 'POST', params: {} },
postDeleteRecover: { url: _apiUri + '/accounts/delete-recover', method: 'POST', params: {} },
putPassword: { url: _apiUri + '/accounts/password', method: 'POST', params: {} },
getProfile: { url: _apiUri + '/accounts/profile', method: 'GET', params: {} },
putProfile: { url: _apiUri + '/accounts/profile', method: 'POST', params: {} },
@@ -125,19 +141,30 @@
putKey: { url: _apiUri + '/accounts/key', method: 'POST', params: {} },
'import': { url: _apiUri + '/accounts/import', method: 'POST', params: {} },
postDelete: { url: _apiUri + '/accounts/delete', method: 'POST', params: {} },
postPremium: { url: _apiUri + '/accounts/premium', method: 'POST', params: {} },
putStorage: { url: _apiUri + '/accounts/storage', method: 'POST', params: {} },
putPayment: { url: _apiUri + '/accounts/payment', method: 'POST', params: {} },
putCancelPremium: { url: _apiUri + '/accounts/cancel-premium', method: 'POST', params: {} },
putReinstatePremium: { url: _apiUri + '/accounts/reinstate-premium', method: 'POST', params: {} },
getBilling: { url: _apiUri + '/accounts/billing', method: 'GET', params: {} }
getBilling: { url: _apiUri + '/accounts/billing', method: 'GET', params: {} },
postPremium: {
url: _apiUri + '/accounts/premium',
method: 'POST',
headers: { 'Content-Type': undefined }
},
putLicense: {
url: _apiUri + '/accounts/license',
method: 'POST',
headers: { 'Content-Type': undefined }
}
});
_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: {} },
@@ -146,8 +173,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: {} }
});
@@ -161,11 +190,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 = cryptoService.decrypt(loginData.Name, key);
login.username = loginData.Username && loginData.Username !== '' ? cryptoService.decrypt(loginData.Username, key) : null;
login.password = loginData.Password && loginData.Password !== '' ? cryptoService.decrypt(loginData.Password, key) : null;
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,56 +562,9 @@ angular
};
};
_service.updateKey = function (masterPasswordHash, success, error) {
var madeEncKey = cryptoService.makeEncKey(null);
encKey = madeEncKey.encKey;
var encKeyEnc = madeEncKey.encKeyEnc;
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) {
continue;
}
filteredEncryptedLogins.push(encryptedLogins.Data[i]);
}
var unencryptedLogins = _service.decryptLogins(filteredEncryptedLogins);
reencryptedLogins = _service.encryptLogins(unencryptedLogins, encKey);
}).$promise;
var reencryptedFolders = [];
var foldersPromise = apiService.folders.list({}, function (encryptedFolders) {
var unencryptedFolders = _service.decryptFolders(encryptedFolders.Data);
reencryptedFolders = _service.encryptFolders(unencryptedFolders, encKey);
}).$promise;
var privateKey = cryptoService.getPrivateKey('raw'),
reencryptedPrivateKey = null;
if (privateKey) {
reencryptedPrivateKey = cryptoService.encrypt(privateKey, encKey, 'raw');
}
return $q.all([loginsPromise, foldersPromise]).then(function () {
var request = {
masterPasswordHash: masterPasswordHash,
ciphers: reencryptedLogins,
folders: reencryptedFolders,
privateKey: reencryptedPrivateKey,
key: encKeyEnc
};
return apiService.accounts.putKey(request).$promise;
}, error).then(function () {
cryptoService.setEncKey(encKey, null, true);
return success();
}, function () {
cryptoService.clearEncKey();
error();
});
};
function encryptProperty(property, key) {
return !property || property === '' ? null : cryptoService.encrypt(property, key);
}
return _service;
});

View File

@@ -288,8 +288,12 @@ angular
return deferred.promise;
};
_service.makeShareKeyCt = function () {
return _service.rsaEncryptMe(forge.random.getBytesSync(512 / 8));
_service.makeShareKey = function () {
var key = forge.random.getBytesSync(512 / 8);
return {
key: new SymmetricCryptoKey(key),
ct: _service.rsaEncryptMe(key)
};
};
_service.hashPassword = function (password, key) {
@@ -477,121 +481,140 @@ angular
};
_service.decrypt = function (encValue, key, outputEncoding) {
key = key || _service.getEncKey() || _service.getKey();
try {
key = key || _service.getEncKey() || _service.getKey();
var headerPieces = encValue.split('.'),
encType,
encPieces;
var headerPieces = encValue.split('.'),
encType,
encPieces;
if (headerPieces.length === 2) {
try {
encType = parseInt(headerPieces[0]);
encPieces = headerPieces[1].split('|');
}
catch (e) {
return null;
}
}
else {
encPieces = encValue.split('|');
encType = encPieces.length === 3 ? constants.encType.AesCbc128_HmacSha256_B64 :
constants.encType.AesCbc256_B64;
}
if (encType === constants.encType.AesCbc128_HmacSha256_B64 && key.encType === constants.encType.AesCbc256_B64) {
// Old encrypt-then-mac scheme, swap out the key
_legacyEtmKey = _legacyEtmKey ||
new SymmetricCryptoKey(key.key, false, constants.encType.AesCbc128_HmacSha256_B64);
key = _legacyEtmKey;
}
if (encType !== key.encType) {
throw 'encType unavailable.';
}
switch (encType) {
case constants.encType.AesCbc128_HmacSha256_B64:
case constants.encType.AesCbc256_HmacSha256_B64:
if (encPieces.length !== 3) {
if (headerPieces.length === 2) {
try {
encType = parseInt(headerPieces[0]);
encPieces = headerPieces[1].split('|');
}
catch (e) {
console.error('Cannot parse headerPieces.');
return null;
}
break;
case constants.encType.AesCbc256_B64:
if (encPieces.length !== 2) {
}
else {
encPieces = encValue.split('|');
encType = encPieces.length === 3 ? constants.encType.AesCbc128_HmacSha256_B64 :
constants.encType.AesCbc256_B64;
}
if (encType === constants.encType.AesCbc128_HmacSha256_B64 && key.encType === constants.encType.AesCbc256_B64) {
// Old encrypt-then-mac scheme, swap out the key
_legacyEtmKey = _legacyEtmKey ||
new SymmetricCryptoKey(key.key, false, constants.encType.AesCbc128_HmacSha256_B64);
key = _legacyEtmKey;
}
if (encType !== key.encType) {
throw 'encType unavailable.';
}
switch (encType) {
case constants.encType.AesCbc128_HmacSha256_B64:
case constants.encType.AesCbc256_HmacSha256_B64:
if (encPieces.length !== 3) {
console.error('Enc type (' + encType + ') not valid.');
return null;
}
break;
case constants.encType.AesCbc256_B64:
if (encPieces.length !== 2) {
console.error('Enc type (' + encType + ') not valid.');
return null;
}
break;
default:
console.error('Enc type (' + encType + ') not supported.');
return null;
}
var ivBytes = forge.util.decode64(encPieces[0]);
var ctBytes = forge.util.decode64(encPieces[1]);
if (key.macKey && encPieces.length > 2) {
var macBytes = forge.util.decode64(encPieces[2]);
var computedMacBytes = computeMac(ivBytes + ctBytes, key.macKey, false);
if (!macsEqual(macBytes, computedMacBytes)) {
console.error('MAC failed.');
return null;
}
break;
default:
return null;
}
}
var ivBytes = forge.util.decode64(encPieces[0]);
var ctBytes = forge.util.decode64(encPieces[1]);
var ctBuffer = forge.util.createBuffer(ctBytes);
var decipher = forge.cipher.createDecipher('AES-CBC', key.encKey);
decipher.start({ iv: ivBytes });
decipher.update(ctBuffer);
decipher.finish();
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)) {
console.error('MAC failed.');
return null;
outputEncoding = outputEncoding || 'utf8';
if (outputEncoding === 'utf8') {
return decipher.output.toString('utf8');
}
else {
return decipher.output.getBytes();
}
}
var ctBuffer = forge.util.createBuffer(ctBytes);
var decipher = forge.cipher.createDecipher('AES-CBC', key.encKey);
decipher.start({ iv: ivBytes });
decipher.update(ctBuffer);
decipher.finish();
outputEncoding = outputEncoding || 'utf8';
if (outputEncoding === 'utf8') {
return decipher.output.toString('utf8');
}
else {
return decipher.output.getBytes();
catch (e) {
console.error('Caught unhandled error in decrypt: ' + e);
throw e;
}
};
_service.decryptFromBytes = function (encBuf, key) {
if (!encBuf) {
throw 'no encBuf.';
}
try {
if (!encBuf) {
throw 'no encBuf.';
}
var encBytes = new Uint8Array(encBuf),
encType = encBytes[0],
ctBytes = null,
ivBytes = null,
macBytes = null;
var encBytes = new Uint8Array(encBuf),
encType = encBytes[0],
ctBytes = null,
ivBytes = null,
macBytes = null;
switch (encType) {
case constants.encType.AesCbc128_HmacSha256_B64:
case constants.encType.AesCbc256_HmacSha256_B64:
if (encBytes.length <= 49) { // 1 + 16 + 32 + ctLength
switch (encType) {
case constants.encType.AesCbc128_HmacSha256_B64:
case constants.encType.AesCbc256_HmacSha256_B64:
if (encBytes.length <= 49) { // 1 + 16 + 32 + ctLength
console.error('Enc type (' + encType + ') not valid.');
return null;
}
ivBytes = slice(encBytes, 1, 17);
macBytes = slice(encBytes, 17, 49);
ctBytes = slice(encBytes, 49);
break;
case constants.encType.AesCbc256_B64:
if (encBytes.length <= 17) { // 1 + 16 + ctLength
console.error('Enc type (' + encType + ') not valid.');
return null;
}
ivBytes = slice(encBytes, 1, 17);
ctBytes = slice(encBytes, 17);
break;
default:
console.error('Enc type (' + encType + ') not supported.');
return null;
}
}
ivBytes = slice(encBytes, 1, 17);
macBytes = slice(encBytes, 17, 49);
ctBytes = slice(encBytes, 49);
break;
case constants.encType.AesCbc256_B64:
if (encBytes.length <= 17) { // 1 + 16 + ctLength
return null;
}
ivBytes = slice(encBytes, 1, 17);
ctBytes = slice(encBytes, 17);
break;
default:
return null;
return aesDecryptWC(
encType,
ctBytes.buffer,
ivBytes.buffer,
macBytes ? macBytes.buffer : null,
key);
}
catch (e) {
console.error('Caught unhandled error in decryptFromBytes: ' + e);
throw e;
}
return aesDecryptWC(
encType,
ctBytes.buffer,
ivBytes.buffer,
macBytes ? macBytes.buffer : null,
key);
};
function aesDecryptWC(encType, ctBuf, ivBuf, macBuf, key) {
@@ -600,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.';
}
@@ -623,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.');
@@ -681,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;
}
@@ -724,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();
@@ -738,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);
@@ -909,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":"TODO","version":"1.14.1","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.26.0","environment":"Production"});

View File

@@ -1,13 +1,20 @@
angular
.module('bit.organization')
.controller('settingsBillingChangePaymentController', function ($scope, $state, $uibModalInstance, apiService, stripe,
$analytics, toastr, existingPaymentMethod, appSettings, $timeout) {
.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' });
$scope.existingPaymentMethod = existingPaymentMethod;
$scope.paymentMethod = 'card';
$scope.dropinLoaded = false;
$scope.showPaymentOptions = false;
$scope.hideBank = true;
$scope.card = {};
var btInstance = null;
@@ -86,6 +93,8 @@
else {
return stripe.card.createToken(card).then(function (response) {
return response.id;
}).catch(function (err) {
throw err.message;
});
}
}

View File

@@ -1,17 +1,25 @@
angular
.module('bit.settings')
.controller('settingsBillingController', function ($scope, apiService, authService, $state, $uibModal, toastr, $analytics) {
.controller('settingsBillingController', function ($scope, apiService, authService, $state, $uibModal, toastr, $analytics,
appSettings) {
$scope.selfHosted = appSettings.selfHosted;
$scope.charges = [];
$scope.paymentSource = null;
$scope.subscription = null;
$scope.loading = true;
var license = null;
$scope.expiration = null;
$scope.$on('$viewContentLoaded', function () {
load();
});
$scope.changePayment = function () {
if ($scope.selfHosted) {
return;
}
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsBillingChangePayment.html',
@@ -29,6 +37,10 @@
};
$scope.adjustStorage = function (add) {
if ($scope.selfHosted) {
return;
}
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsBillingAdjustStorage.html',
@@ -46,6 +58,10 @@
};
$scope.cancel = function () {
if ($scope.selfHosted) {
return;
}
if (!confirm('Are you sure you want to cancel? You will lose access to all premium features at the end ' +
'of this billing cycle.')) {
return;
@@ -60,6 +76,10 @@
};
$scope.reinstate = function () {
if ($scope.selfHosted) {
return;
}
if (!confirm('Are you sure you want to remove the cancellation request and reinstate your premium membership?')) {
return;
}
@@ -72,6 +92,46 @@
});
};
$scope.updateLicense = function () {
if (!$scope.selfHosted) {
return;
}
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsBillingUpdateLicense.html',
controller: 'settingsBillingUpdateLicenseController'
});
modal.result.then(function () {
load();
});
};
$scope.license = function () {
if ($scope.selfHosted) {
return;
}
var licenseString = JSON.stringify(license, null, 2);
var licenseBlob = new Blob([licenseString]);
// IE hack. ref http://msdn.microsoft.com/en-us/library/ie/hh779016.aspx
if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveBlob(licenseBlob, 'bitwarden_premium_license.json');
}
else {
var a = window.document.createElement('a');
a.href = window.URL.createObjectURL(licenseBlob, { type: 'text/plain' });
a.download = 'bitwarden_premium_license.json';
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);
}
};
function load() {
authService.getUserProfile().then(function (profile) {
$scope.premium = profile.premium;
@@ -86,6 +146,8 @@
}
var i = 0;
$scope.expiration = billing.Expiration;
license = billing.License;
$scope.storage = null;
if (billing && billing.MaxStorageGb) {

View File

@@ -0,0 +1,30 @@
angular
.module('bit.settings')
.controller('settingsBillingUpdateLicenseController', function ($scope, $state, $uibModalInstance, apiService,
$analytics, toastr, validationService) {
$analytics.eventTrack('settingsBillingUpdateLicenseController', { category: 'Modal' });
$scope.submit = function (form) {
var fileEl = document.getElementById('file');
var files = fileEl.files;
if (!files || !files.length) {
validationService.addError(form, 'file', 'Select a license file.', true);
return;
}
var fd = new FormData();
fd.append('license', files[0]);
$scope.submitPromise = apiService.accounts.putLicense(fd)
.$promise.then(function (response) {
$analytics.eventTrack('Updated License');
toastr.success('You have updated your license.');
$uibModalInstance.close();
});
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@@ -2,49 +2,39 @@
.module('bit.settings')
.controller('settingsChangeEmailController', function ($scope, $state, apiService, $uibModalInstance, cryptoService,
cipherService, authService, $q, toastr, $analytics) {
authService, toastr, $analytics, validationService) {
$analytics.eventTrack('settingsChangeEmailController', { category: 'Modal' });
var _masterPasswordHash,
_masterPassword,
_newEmail;
$scope.token = function (model) {
$scope.token = function (model, form) {
var encKey = cryptoService.getEncKey();
if (!encKey) {
validationService.addError(form, null,
'You cannot change your email until you update your encryption key.', true);
return;
}
_masterPassword = model.masterPassword;
_newEmail = model.newEmail.toLowerCase();
cryptoService.hashPassword(_masterPassword).then(function (hash) {
$scope.tokenPromise = cryptoService.hashPassword(_masterPassword).then(function (hash) {
_masterPasswordHash = hash;
var encKey = cryptoService.getEncKey();
if (encKey) {
$scope.tokenPromise = requestToken();
}
else {
// User is not using an enc key, let's make them one
$scope.tokenPromise = cipherService.updateKey(_masterPasswordHash, function () {
return requestToken();
}, function (err) {
toastr.error('Something went wrong.', 'Oh No!');
});
}
var request = {
newEmail: _newEmail,
masterPasswordHash: _masterPasswordHash
};
return apiService.accounts.emailToken(request, function () {
$scope.tokenSent = true;
}).$promise;
});
};
function requestToken() {
var request = {
newEmail: _newEmail,
masterPasswordHash: _masterPasswordHash
};
return apiService.accounts.emailToken(request, function () {
$scope.tokenSent = true;
}).$promise;
}
$scope.confirm = function (model) {
$scope.processing = true;
$scope.confirmPromise = cryptoService.makeKeyAndHash(_newEmail, _masterPassword).then(function (result) {
var encKey = cryptoService.getEncKey();
var newEncKey = cryptoService.encrypt(encKey.key, result.key, 'raw');
@@ -64,9 +54,6 @@
return $state.go('frontend.login.info');
}).then(function () {
toastr.success('Please log back in.', 'Email Changed');
}, function () {
$uibModalInstance.dismiss('cancel');
toastr.error('Something went wrong. Try again.', 'Oh No!');
});
};

View File

@@ -2,12 +2,19 @@
.module('bit.settings')
.controller('settingsChangePasswordController', function ($scope, $state, apiService, $uibModalInstance,
cryptoService, authService, cipherService, validationService, toastr, $analytics) {
cryptoService, authService, validationService, toastr, $analytics) {
$analytics.eventTrack('settingsChangePasswordController', { category: 'Modal' });
$scope.save = function (model, form) {
var error = false;
var encKey = cryptoService.getEncKey();
if (!encKey) {
validationService.addError(form, null,
'You cannot change your master password until you update your encryption key.', true);
error = true;
}
if ($scope.model.newMasterPassword.length < 8) {
validationService.addError(form, 'NewMasterPasswordHash',
'Master password must be at least 8 characters long.', true);
@@ -23,27 +30,8 @@
return;
}
$scope.processing = true;
var encKey = cryptoService.getEncKey();
if (encKey) {
$scope.savePromise = changePassword(model);
}
else {
// User is not using an enc key, let's make them one
$scope.savePromise = cryptoService.hashPassword(model.masterPassword).then(function (hash) {
return cipherService.updateKey(hash);
}).then(function () {
return changePassword(model);
}, function (err) {
toastr.error('Something went wrong.', 'Oh No!');
});
}
};
function changePassword(model) {
var makeResult;
return authService.getUserProfile().then(function (profile) {
$scope.savePromise = authService.getUserProfile().then(function (profile) {
return cryptoService.makeKeyAndHash(profile.email, model.newMasterPassword);
}).then(function (result) {
makeResult = result;
@@ -66,11 +54,8 @@
return $state.go('frontend.login.info');
}).then(function () {
toastr.success('Please log back in.', 'Master Password Changed');
}, function () {
$uibModalInstance.dismiss('cancel');
toastr.error('Something went wrong.', 'Oh No!');
});
}
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');

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

@@ -2,9 +2,17 @@
.module('bit.settings')
.controller('settingsCreateOrganizationController', function ($scope, $state, apiService, cryptoService,
toastr, $analytics, authService, stripe, constants) {
toastr, $analytics, authService, constants, appSettings, validationService
// @if !selfHosted
/* jshint ignore:start */
, stripe
/* jshint ignore:end */
// @endif
) {
$scope.plans = constants.plans;
$scope.storageGb = constants.storageGb;
$scope.paymentMethod = 'card';
$scope.selfHosted = appSettings.selfHosted;
$scope.model = {
plan: 'free',
@@ -27,6 +35,10 @@
}
};
$scope.changePaymentMethod = function (val) {
$scope.paymentMethod = val;
};
$scope.changedPlan = function () {
if ($scope.plans[$scope.model.plan].hasOwnProperty('monthPlanType')) {
$scope.model.interval = 'year';
@@ -47,40 +59,76 @@
}
};
$scope.submit = function (model) {
var shareKeyCt = cryptoService.makeShareKeyCt();
$scope.submit = function (model, form) {
var shareKey = cryptoService.makeShareKey();
var defaultCollectionCt = cryptoService.encrypt('Default Collection', shareKey.key);
if (model.plan === 'free') {
var freeRequest = {
name: model.name,
planType: model.plan,
key: shareKeyCt,
billingEmail: model.billingEmail
};
if ($scope.selfHosted) {
var fileEl = document.getElementById('file');
var files = fileEl.files;
if (!files || !files.length) {
validationService.addError(form, 'file', 'Select a license file.', true);
return;
}
$scope.submitPromise = apiService.organizations.post(freeRequest).$promise.then(finalizeCreate);
var fd = new FormData();
fd.append('license', files[0]);
fd.append('key', shareKey.ct);
fd.append('collectionName', defaultCollectionCt);
$scope.submitPromise = apiService.organizations.postLicense(fd).$promise.then(finalizeCreate);
}
else {
$scope.submitPromise = stripe.card.createToken(model.card).then(function (response) {
var paidRequest = {
if (model.plan === 'free') {
var freeRequest = {
name: model.name,
planType: model.interval === 'month' ? $scope.plans[model.plan].monthPlanType :
$scope.plans[model.plan].annualPlanType,
key: shareKeyCt,
paymentToken: response.id,
additionalSeats: model.additionalSeats,
additionalStorageGb: model.additionalStorageGb,
planType: model.plan,
key: shareKey.ct,
billingEmail: model.billingEmail,
businessName: model.ownedBusiness ? model.businessName : null
collectionName: defaultCollectionCt
};
return apiService.organizations.post(paidRequest).$promise;
}).then(finalizeCreate);
$scope.submitPromise = apiService.organizations.post(freeRequest).$promise.then(finalizeCreate);
}
else {
var stripeReq = null;
if ($scope.paymentMethod === 'card') {
stripeReq = stripe.card.createToken(model.card);
}
else if ($scope.paymentMethod === 'bank') {
model.bank.currency = 'USD';
model.bank.country = 'US';
stripeReq = stripe.bankAccount.createToken(model.bank);
}
else {
return;
}
$scope.submitPromise = stripeReq.then(function (response) {
var paidRequest = {
name: model.name,
planType: model.interval === 'month' ? $scope.plans[model.plan].monthPlanType :
$scope.plans[model.plan].annualPlanType,
key: shareKey.ct,
paymentToken: response.id,
additionalSeats: model.additionalSeats,
additionalStorageGb: model.additionalStorageGb,
billingEmail: model.billingEmail,
businessName: model.ownedBusiness ? model.businessName : null,
country: $scope.paymentMethod === 'card' ? model.card.address_country : null,
collectionName: defaultCollectionCt
};
return apiService.organizations.post(paidRequest).$promise;
}, function (err) {
throw err.message;
}).then(finalizeCreate);
}
}
function finalizeCreate(result) {
$analytics.eventTrack('Created Organization');
authService.addProfileOrganizationOwner(result, shareKeyCt);
authService.addProfileOrganizationOwner(result, shareKey.ct);
authService.refreshAccessToken().then(function () {
goToOrg(result.Id);
}, function () {

View File

@@ -2,16 +2,22 @@
.module('bit.settings')
.controller('settingsDeleteController', function ($scope, $state, apiService, $uibModalInstance, cryptoService,
authService, toastr, $analytics) {
authService, toastr, $analytics, tokenService) {
$analytics.eventTrack('settingsDeleteController', { category: 'Modal' });
$scope.submit = function (model) {
$scope.submitPromise = cryptoService.hashPassword(model.masterPassword).then(function (hash) {
var profile;
$scope.submitPromise = authService.getUserProfile().then(function (theProfile) {
profile = theProfile;
return cryptoService.hashPassword(model.masterPassword);
}).then(function (hash) {
return apiService.accounts.postDelete({
masterPasswordHash: hash
}).$promise;
}).then(function () {
$uibModalInstance.dismiss('cancel');
authService.logOut();
tokenService.clearTwoFactorToken(profile.email);
$analytics.eventTrack('Deleted Account');
return $state.go('frontend.login.info');
}).then(function () {

View File

@@ -1,14 +1,25 @@
angular
.module('bit.settings')
.controller('settingsPremiumController', function ($scope, $state, apiService, toastr, $analytics, authService, stripe,
constants, $timeout, appSettings) {
authService.getUserProfile().then(function (profile) {
if (profile.premium) {
.controller('settingsPremiumController', function ($scope, $state, apiService, toastr, $analytics, authService,
constants, $timeout, appSettings, validationService
// @if !selfHosted
/* jshint ignore:start */
, stripe
/* jshint ignore:end */
// @endif
) {
var profile = null;
authService.getUserProfile().then(function (theProfile) {
profile = theProfile;
if (profile && profile.premium) {
return $state.go('backend.user.settingsBilling');
}
});
$scope.selfHosted = appSettings.selfHosted;
var btInstance = null;
$scope.storageGbPrice = constants.storageGb.yearlyPrice;
$scope.premiumPrice = constants.premium.price;
@@ -19,7 +30,8 @@
additionalStorageGb: null
};
$scope.changePaymentMethod = function () {
$scope.changePaymentMethod = function (val) {
$scope.paymentMethod = val;
if ($scope.paymentMethod !== 'paypal') {
return;
}
@@ -54,23 +66,48 @@
return $scope.premiumPrice + (($scope.model.additionalStorageGb || 0) * $scope.storageGbPrice);
};
$scope.submit = function (model) {
$scope.submitPromise = getPaymentToken(model).then(function (token) {
if (!token) {
throw 'No payment token.';
$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 request = {
paymentToken: token,
additionalStorageGb: model.additionalStorageGb
};
var fileEl = document.getElementById('file');
var files = fileEl.files;
if (!files || !files.length) {
validationService.addError(form, 'file', 'Select a license file.', true);
return;
}
return apiService.accounts.postPremium(request).$promise;
}, function (err) {
throw err;
}).then(function (result) {
return authService.updateProfilePremium(true);
}).then(function () {
var fd = new FormData();
fd.append('license', files[0]);
$scope.submitPromise = apiService.accounts.postPremium(fd).$promise.then(function (result) {
return finalizePremium();
});
}
else {
$scope.submitPromise = getPaymentToken(model).then(function (token) {
if (!token) {
throw 'No payment token.';
}
var fd = new FormData();
fd.append('paymentToken', token);
fd.append('additionalStorageGb', model.additionalStorageGb || 0);
return apiService.accounts.postPremium(fd).$promise;
}, function (err) {
throw err;
}).then(function (result) {
return finalizePremium();
});
}
};
function finalizePremium() {
return authService.updateProfilePremium(true).then(function () {
$analytics.eventTrack('Signed Up Premium');
return authService.refreshAccessToken();
}).then(function () {
@@ -78,7 +115,7 @@
}).then(function () {
toastr.success('Premium upgrade complete.', 'Success');
});
};
}
function getPaymentToken(model) {
if ($scope.paymentMethod === 'paypal') {
@@ -91,6 +128,8 @@
else {
return stripe.card.createToken(model.card).then(function (response) {
return response.id;
}).catch(function (err) {
throw err.message;
});
}
}

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=120x120&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();

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