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

Compare commits

..

426 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
Kyle Spearrin
afe223f410 version bump 2017-07-28 21:26:10 -04:00
Kyle Spearrin
e1ec50bcad hide paypal until ready 2017-07-28 21:16:33 -04:00
Kyle Spearrin
04da844b22 radio styling 2017-07-28 21:13:03 -04:00
Kyle Spearrin
f944910975 error handling for no payment method 2017-07-28 16:44:36 -04:00
Kyle Spearrin
96b8467859 support for paypal through braintree 2017-07-28 14:29:25 -04:00
Kyle Spearrin
84554174ac fix attachments for org edit 2017-07-27 22:14:42 -04:00
Kyle Spearrin
65e03e707c new duo path 2017-07-26 13:32:17 -04:00
Kyle Spearrin
fd9fcbea38 validation summary on payment 2017-07-26 10:12:20 -04:00
Kyle Spearrin
a1dfd7493a premium check updates 2017-07-26 10:07:12 -04:00
Kyle Spearrin
d4759d4056 fixes 2017-07-26 09:35:30 -04:00
Kyle Spearrin
d879518233 typo 2017-07-26 00:31:57 -04:00
Kyle Spearrin
ef6cb3779b local duo for iframe fixes 2017-07-25 22:54:08 -04:00
Kyle Spearrin
fc22114855 version bump 2017-07-25 22:39:17 -04:00
Kyle Spearrin
6b1eb5a479 cancellation notices 2017-07-25 15:53:17 -04:00
Kyle Spearrin
bbd8a1265b attachments for shared view 2017-07-25 15:45:52 -04:00
Kyle Spearrin
444f63db42 callback whenever closing modal 2017-07-25 15:00:20 -04:00
Kyle Spearrin
f46a6aefea update enc key article 2017-07-25 08:55:01 -04:00
Kyle Spearrin
10792f714e focus master password field on load 2017-07-24 12:08:21 -04:00
Kyle Spearrin
d6d535ed9e stop listening for u2f on destroy 2017-07-24 12:02:57 -04:00
Kyle Spearrin
55a50fac83 timeout when trying u2f again 2017-07-24 11:52:31 -04:00
Kyle Spearrin
a7beed334f u2f fixes and mobile filter for 2fa methods 2017-07-24 11:48:19 -04:00
Kyle Spearrin
83274ad7a4 duo lib should be copied 2017-07-24 11:10:31 -04:00
Kyle Spearrin
24056163dd premium required for attachments 2017-07-21 17:14:40 -04:00
Kyle Spearrin
79383ed693 limitations 2017-07-15 11:03:13 -04:00
Kyle Spearrin
d2da3f6e00 use better monospace font for code 2017-07-14 22:31:53 -04:00
Kyle Spearrin
c40193c861 no config in u2f build 2017-07-14 15:52:27 -04:00
Kyle Spearrin
715835c12f lint fixes 2017-07-14 14:32:26 -04:00
Kyle Spearrin
0242de9145 new preview repo 2017-07-14 14:32:26 -04:00
Kyle Spearrin
b075f25d7c add params for two-factor page 2017-07-14 14:32:26 -04:00
Kyle Spearrin
0b34b7a980 Update README.md 2017-07-14 08:32:58 -04:00
Kyle Spearrin
f291b24a7a Update README.md 2017-07-14 08:32:30 -04:00
Kyle Spearrin
9707fa34e4 login returnState conditions 2017-07-13 22:28:52 -04:00
Kyle Spearrin
cd19e0c9e4 totp code updates 2017-07-13 14:45:57 -04:00
Kyle Spearrin
38883b9550 add totp to import/export 2017-07-13 11:22:16 -04:00
Kyle Spearrin
f761733d0b Show file after upload and reset input 2017-07-12 14:17:21 -04:00
Kyle Spearrin
842b157955 provide callback functions 2017-07-12 10:57:17 -04:00
Kyle Spearrin
87f0e2be0e cleanup 2017-07-12 10:00:36 -04:00
Kyle Spearrin
c3bea80ec7 gnome importer 2017-07-11 12:05:44 -04:00
Kyle Spearrin
a1529bc4e9 change payment for premium 2017-07-11 11:17:43 -04:00
Kyle Spearrin
ccb7ede4fa storage percentage fix 2017-07-11 11:05:19 -04:00
Kyle Spearrin
1dbf831bda storage adjustment 2017-07-11 10:59:49 -04:00
Kyle Spearrin
ea4d772dda storage for org billing & signup 2017-07-11 10:24:46 -04:00
Kyle Spearrin
25536e10ef toasts and error handling 2017-07-10 23:16:34 -04:00
Kyle Spearrin
51e30b2f7a capture attachment in closure 2017-07-10 16:21:39 -04:00
Kyle Spearrin
47cb20f01e share login with attachments 2017-07-10 14:30:33 -04:00
Kyle Spearrin
204ee72926 outdated browser and edge checks for pbkdf2 2017-07-09 00:23:26 -04:00
Kyle Spearrin
b9cbc1546c undefined checks 2017-07-08 23:48:08 -04:00
Kyle Spearrin
bc8892a237 move pbkdf2 to web crypto with shim fallback 2017-07-08 23:41:02 -04:00
Kyle Spearrin
b62950fa2b IE fixes and crypto shims 2017-07-08 00:12:57 -04:00
Kyle Spearrin
ab12c990bc offset scroll 2017-07-07 16:15:40 -04:00
Kyle Spearrin
abed4df973 attachments for org logins 2017-07-07 15:43:24 -04:00
Kyle Spearrin
76da9b1f18 dont copy formatted code 2017-07-07 14:25:08 -04:00
Kyle Spearrin
11cbe3b7bb allow totp if from an org with totp 2017-07-07 14:16:15 -04:00
Kyle Spearrin
08b432775e totp flag on logins 2017-07-07 14:07:30 -04:00
Kyle Spearrin
49dbf4945f totp access for orgs 2017-07-07 12:12:08 -04:00
Kyle Spearrin
ff729608e1 delete attachments 2017-07-07 10:58:51 -04:00
Kyle Spearrin
b380d723b7 UI adjustments for premium adverts 2017-07-07 09:11:45 -04:00
Kyle Spearrin
ed13644a02 totp generator directive 2017-07-07 00:13:26 -04:00
Kyle Spearrin
8a90f562ef add field for totp to login 2017-07-06 21:22:06 -04:00
Kyle Spearrin
dfd791ecf9 premium required messages 2017-07-06 16:15:28 -04:00
Kyle Spearrin
8df16f28e7 premium signup and billing settings pages 2017-07-06 15:00:04 -04:00
Kyle Spearrin
1fb220c25e attachment errors 2017-07-05 16:27:28 -04:00
Kyle Spearrin
b24f892f60 verify email 2017-07-05 15:36:40 -04:00
Kyle Spearrin
5d81ed6a96 update key and verify email notification 2017-07-01 22:44:10 -04:00
Kyle Spearrin
7ff79a0fdd download and decrypt attachments 2017-06-30 22:34:26 -04:00
Kyle Spearrin
7b4cf53ec4 encrypt, upload, and view attachments 2017-06-30 16:22:39 -04:00
Kyle Spearrin
9c7b47c277 rename to duo-connector 2017-06-29 14:56:54 -04:00
Kyle Spearrin
547c7b8b70 nfc flag for yubi and duo mobile page 2017-06-29 12:35:10 -04:00
Kyle Spearrin
1d70434ed1 urls for appid 2017-06-27 14:49:39 -04:00
Kyle Spearrin
06d53d350d app id to json extension 2017-06-27 13:51:32 -04:00
Kyle Spearrin
742d7240f7 android facet 2017-06-27 13:49:39 -04:00
Kyle Spearrin
9b3ca76934 fido app id 2017-06-27 12:26:53 -04:00
Kyle Spearrin
9f1c445214 not supported scenario 2017-06-27 09:04:51 -04:00
Kyle Spearrin
075ba931ea added recovery code option to methods 2017-06-27 08:30:58 -04:00
Kyle Spearrin
29cbe48eb5 lint fixes 2017-06-27 08:26:00 -04:00
Kyle Spearrin
be1cc945a2 enabled fix 2017-06-27 08:23:00 -04:00
Kyle Spearrin
3e61d938bc token sanitization and adjust timeouts on u2f 2017-06-27 08:14:03 -04:00
Kyle Spearrin
0ee928cdce u2f connector updates 2017-06-26 23:52:49 -04:00
Kyle Spearrin
5d87fae906 U2f support 2017-06-26 15:52:50 -04:00
Kyle Spearrin
afcc5ceb5b adjust priorities 2017-06-26 15:32:34 -04:00
Kyle Spearrin
74d8e595f2 u2f connector frame 2017-06-26 14:49:20 -04:00
Kyle Spearrin
bc988181f9 update messages 2017-06-24 17:20:27 -04:00
Kyle Spearrin
1030654ce2 android with NFC 2017-06-24 17:15:36 -04:00
Kyle Spearrin
1c25143a75 platform warnings 2017-06-24 17:12:10 -04:00
Kyle Spearrin
39281811f5 recovery code 2017-06-24 16:59:01 -04:00
Kyle Spearrin
2f07d22a9e touch it 2017-06-24 15:49:45 -04:00
Kyle Spearrin
1d1b9706ce show redacted email 2017-06-24 11:55:39 -04:00
Kyle Spearrin
7a19d444f1 update 2fa setup pages 2017-06-24 11:26:24 -04:00
Kyle Spearrin
73eb743f54 2fa cleanup 2017-06-24 10:49:53 -04:00
Kyle Spearrin
181ee74ba3 email 2fa login 2017-06-24 09:19:04 -04:00
Kyle Spearrin
b8e9567501 u2f cleanup 2017-06-23 16:31:55 -04:00
Kyle Spearrin
dda64b301e 2fa cleanup 2017-06-23 12:39:56 -04:00
Kyle Spearrin
af56551fd2 remember two factor 2017-06-23 10:41:57 -04:00
Kyle Spearrin
c55d0449cb fido u2f login flow 2017-06-22 23:16:02 -04:00
Kyle Spearrin
0135476b68 configure u2f device 2017-06-22 17:02:24 -04:00
Kyle Spearrin
e366b7c7a7 u2f api 2017-06-21 22:47:42 -04:00
Kyle Spearrin
ca9a0b072e duo 2fa config and login with web sdk 2017-06-21 15:17:44 -04:00
Kyle Spearrin
2f3035a08f 2fa method selection 2017-06-20 17:06:14 -04:00
Kyle Spearrin
cf5b0635e4 Yubikey 2fa setup 2017-06-20 14:00:55 -04:00
Kyle Spearrin
4db5c96781 send key with auth app setup 2017-06-20 10:12:18 -04:00
Kyle Spearrin
e49948b512 two factor email setup 2017-06-20 09:21:53 -04:00
Kyle Spearrin
1298d42b09 login 2017-06-19 22:33:12 -04:00
Kyle Spearrin
00e74dd2c8 new two-factor management page 2017-06-19 22:26:57 -04:00
Kyle Spearrin
10fe79c558 stubbed out new two-step settings page 2017-06-19 15:29:33 -04:00
Kyle Spearrin
cddabebe86 lint fix 2017-06-19 10:23:50 -04:00
Kyle Spearrin
9a7dac706c sign rsa "me" encrypted data with enc key 2017-06-19 10:00:42 -04:00
Kyle Spearrin
2e2998bb8b cdn integrity checks 2017-06-17 10:14:44 -04:00
Kyle Spearrin
ce1352cb9f remove inline fallback code for CSP 2017-06-17 10:08:47 -04:00
Kyle Spearrin
00007c20a7 verison bump 2017-06-16 15:35:12 -04:00
Kyle Spearrin
cdaf3cb428 ux improvements for bulk actions 2017-06-09 12:30:47 -04:00
Kyle Spearrin
d640bb5a04 small modal 2017-06-09 12:05:22 -04:00
Kyle Spearrin
b1ebcb76f0 bulk move logins 2017-06-09 09:46:25 -04:00
Kyle Spearrin
488dbb6715 toggle checkboxes by clicking whole cell 2017-06-09 01:10:53 -04:00
Kyle Spearrin
f170157817 bulk actions with move and delete 2017-06-09 00:44:56 -04:00
Kyle Spearrin
c094a26cbf copy password from vault listings 2017-06-08 22:25:01 -04:00
Kyle Spearrin
366506555a lint fixes 2017-06-07 21:19:37 -04:00
Kyle Spearrin
9eb4043595 csp adjustments for angular 2017-06-07 21:18:29 -04:00
Kyle Spearrin
3359e78047 stop click directive to prevent CSP errors 2017-06-07 19:01:27 -04:00
Kyle Spearrin
7ebafaf0fc Content-Security-Policy 2017-06-07 17:03:23 -04:00
Kyle Spearrin
fadd070663 control sidebar adjustments 2017-06-06 12:18:43 -04:00
Kyle Spearrin
27d291b0e9 min height control sidebar 2017-06-05 23:38:31 -04:00
Kyle Spearrin
f07f58733c fix layout after filtering 2017-06-05 11:19:01 -04:00
Kyle Spearrin
b5521425ae folder icon. remove tags 2017-06-05 10:46:56 -04:00
Kyle Spearrin
b191ecd29e control sidebar for vault with filters 2017-06-05 10:38:37 -04:00
Kyle Spearrin
5989918300 try again 2017-05-31 16:25:22 -04:00
Kyle Spearrin
f5720cf20e new change email api with enc key 2017-05-31 16:16:21 -04:00
Kyle Spearrin
2106e48e0e move updateKey to cipher service for re-use 2017-05-31 14:49:18 -04:00
Kyle Spearrin
1dd9e459c6 change password with new enc key 2017-05-31 12:21:06 -04:00
Kyle Spearrin
138b57b33d always add header to ciphers on encrypt 2017-05-31 11:06:57 -04:00
Kyle Spearrin
3845c55155 generate enc key on registration 2017-05-31 11:05:52 -04:00
Kyle Spearrin
9aa2014e85 crypto adjustments for new account enc key 2017-05-31 10:25:25 -04:00
Kyle Spearrin
9239588757 recover help article 2017-05-25 23:37:09 -04:00
Kyle Spearrin
5904b269e7 version bump 2017-05-25 18:29:35 -04:00
Kyle Spearrin
9bf3e31d6f no paragraph 2017-05-25 18:29:11 -04:00
Kyle Spearrin
618cb07ead move reports to their own module 2017-05-25 18:22:19 -04:00
Kyle Spearrin
1e3a39defc data breach report. resolves #53 2017-05-25 17:41:29 -04:00
Kyle Spearrin
0aab548b87 new import article urls 2017-05-23 16:43:51 -04:00
Kyle Spearrin
489b93d5df fix lint errors 2017-05-20 08:55:43 -04:00
Kyle Spearrin
cfb2a4d404 version bump 2017-05-20 08:55:04 -04:00
Kyle Spearrin
3b8ad132bc ui adjustments 2017-05-19 20:33:13 -04:00
Kyle Spearrin
8510711e5d organize import dropdown. added opera and vivaldi 2017-05-19 16:03:39 -04:00
Kyle Spearrin
9918e903b2 add support for passkeep csv import 2017-05-19 14:10:45 -04:00
Kyle Spearrin
6a292d6905 Update README.md 2017-05-19 13:27:22 -04:00
Kyle Spearrin
62926d6e28 Update SECURITY.md 2017-05-19 12:03:37 -04:00
Kyle Spearrin
51edf80e48 allow bulk invite CSV list of email addresses 2017-05-18 12:19:49 -04:00
Kyle Spearrin
804f1f5610 meldium importer resolves #68 2017-05-17 16:20:22 -04:00
Kyle Spearrin
3f0b14e48a Create SECURITY.md 2017-05-17 11:34:51 -04:00
Kyle Spearrin
3e0ce5544c primary worker for forge key generation 2017-05-15 20:58:16 -04:00
Kyle Spearrin
933cbb72aa manage external ids 2017-05-15 14:42:24 -04:00
Kyle Spearrin
6bda5d5983 collection user refactor 2017-05-11 14:52:51 -04:00
Kyle Spearrin
bfae8e7def collection add/edit groups 2017-05-11 12:22:03 -04:00
Kyle Spearrin
96a91b97e9 cleanup and model changes 2017-05-11 10:32:39 -04:00
Kyle Spearrin
12096a8fb3 space out the icon a bit 2017-05-10 14:33:48 -04:00
Kyle Spearrin
e03d4d52c4 resolve issues with id on api calls 2017-05-10 14:20:45 -04:00
Kyle Spearrin
ea24d72f01 group accessall and readonly 2017-05-10 12:17:26 -04:00
Kyle Spearrin
a4473ad739 catch refresh token error 2017-05-10 11:47:53 -04:00
Kyle Spearrin
08c28950f4 dashlane importer fix for 6 cols 2017-05-10 11:37:27 -04:00
Kyle Spearrin
5cc8439f5b dont scroll to top with # on click resolves #62 2017-05-10 07:58:51 -04:00
Kyle Spearrin
eb7fd4a015 conditionally show groups option 2017-05-09 20:35:18 -04:00
Kyle Spearrin
dce609d141 no need to clean up card 2017-05-09 19:28:12 -04:00
Kyle Spearrin
f31360ecbf remove user from group 2017-05-09 19:23:49 -04:00
Kyle Spearrin
93e88d8b23 group user assignment 2017-05-09 19:04:26 -04:00
Kyle Spearrin
816cc0b17b occurred typo 2017-05-09 14:23:39 -04:00
Kyle Spearrin
1f73269480 manage groups from collection add/edit 2017-05-09 14:06:44 -04:00
Kyle Spearrin
f7d1b8821c ui tweaks 2017-05-08 22:18:07 -04:00
Kyle Spearrin
cd5ad9f85b select collections on group add/edit 2017-05-08 22:13:31 -04:00
Kyle Spearrin
9c706f07f0 groups list/add/edit 2017-05-08 16:01:36 -04:00
Kyle Spearrin
ea82925e14 new props for org profile 2017-05-08 15:28:40 -04:00
Kyle Spearrin
1c5f208ef1 enterprise plan signup 2017-05-08 15:20:01 -04:00
Kyle Spearrin
aeae0ba535 stripe key in app settings 2017-05-08 14:45:14 -04:00
Kyle Spearrin
f59b227c44 version bump 2017-05-08 12:39:46 -04:00
236 changed files with 27042 additions and 3956 deletions

3
.dockerignore Normal file
View File

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

2
.gitignore vendored
View File

@@ -199,4 +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,10 @@
[![appveyor build](https://ci.appveyor.com/api/projects/status/github/bitwarden/web?branch=master&svg=true)](https://ci.appveyor.com/project/bitwarden/web) [![Join the chat at https://gitter.im/bitwarden/Lobby](https://badges.gitter.im/bitwarden/Lobby.svg)](https://gitter.im/bitwarden/Lobby)
[![appveyor build](https://ci.appveyor.com/api/projects/status/github/bitwarden/web?branch=master&svg=true)](https://ci.appveyor.com/project/bitwarden/web) [![DockerHub](https://img.shields.io/docker/pulls/bitwarden/web.svg)](https://hub.docker.com/u/bitwarden/) [![Join the chat at https://gitter.im/bitwarden/Lobby](https://badges.gitter.im/bitwarden/Lobby.svg)](https://gitter.im/bitwarden/Lobby)
# bitwarden Web
# Bitwarden Web Vault
The bitwarden Web project is an AngularJS application that powers the web vault (https://vault.bitwarden.com/).
The Bitwarden web project is an AngularJS application that powers the web vault (https://vault.bitwarden.com/).
<img src="https://i.imgur.com/rxrykeX.png" alt="" width="791" height="739" />
# Build/Run
@@ -11,8 +13,9 @@ The bitwarden Web project is an AngularJS application that powers the web vault
- Node.js
- Gulp
Unless you are running the [Core](https://github.com/bitwarden/core) API locally, you'll probably need to switch the
application to target the production API. Open `package.json` and set `production` to `true`.
By default the application points to the production API. If you want to change that to point to a local instance of
the [Core](https://github.com/bitwarden/core) API, you can modify the `package.json` `env` property to `Development`
and then set your local endpoints in `settings.json`.
Then run the following commands:
@@ -26,4 +29,4 @@ You can now access the web vault at `http://localhost:4001`.
Code contributions are welcome! Please commit any pull requests against the `master` branch.
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature.
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file.

45
SECURITY.md Normal file
View File

@@ -0,0 +1,45 @@
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!
# Disclosure Policy
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every
effort to quickly resolve the issue.
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a
third-party. We may publicly disclose the issue before resolving it, if appropriate.
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or
degradation of our service. Only interact with accounts you own or with explicit permission of the
account holder.
- If you would like to encrypt your report, please use the PGP key with long ID
`0xDE6887086F892325FEC04CC0D847525B6931381F` (available in the public keyserver pool).
# In-scope
- 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.
# Exclusions
The following bug classes are out-of scope:
- 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
- Missing security best practices that do not directly lead to a vulnerability
- Issues that do not have any impact on the general public
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
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';
@@ -46,7 +47,7 @@ gulp.task('lint', function () {
gulp.task('build', function (cb) {
return runSequence(
'clean',
['browserify', 'lib', 'webpack', 'less', 'settings', 'lint'],
['browserify', 'lib', 'webpack', 'less', 'settings', 'lint', 'min:js'],
cb);
});
@@ -65,9 +66,19 @@ gulp.task('clean:lib', function (cb) {
gulp.task('clean', ['clean:js', 'clean:css', 'clean:lib', 'dist:clean']);
gulp.task('min:js', ['clean:js'], function () {
return gulp.src([paths.js, '!' + paths.minJs], { base: '.' })
return gulp.src(
[
paths.js,
'!' + paths.minJs,
'!' + paths.jsDir + 'fallback*.js',
'!' + paths.jsDir + 'u2f-connector.js',
'!' + paths.jsDir + 'duo-connector.js',
'!' + paths.jsDir + 'duo.js',
'!' + paths.jsDir + 'settings.js'
], { base: '.' })
.pipe(preprocess({ context: { cacheTag: randomString, selfHosted: selfHosted } }))
.pipe(concat(paths.concatJsDest))
.pipe(uglify())
//.pipe(uglify())
.pipe(gulp.dest('.'));
});
@@ -130,6 +141,10 @@ gulp.task('lib', ['clean:lib'], function () {
src: paths.npmDir + 'angular-resource/*resource*.js',
dest: paths.libDir + 'angular-resource'
},
{
src: paths.npmDir + 'angular-sanitize/*sanitize*.js',
dest: paths.libDir + 'angular-sanitize'
},
{
src: [paths.npmDir + 'angular-toastr/dist/**/*.css', paths.npmDir + 'angular-toastr/dist/**/*.js'],
dest: paths.libDir + 'angular-toastr'
@@ -158,12 +173,28 @@ gulp.task('lib', ['clean:lib'], function () {
src: paths.npmDir + 'clipboard/dist/clipboard*.js',
dest: paths.libDir + 'clipboard'
},
{
src: paths.npmDir + 'node-forge/dist/prime.worker.*',
dest: paths.libDir + 'forge'
},
{
src: [
paths.npmDir + 'angulartics-google-analytics/lib/angulartics*.js',
paths.npmDir + 'angulartics/src/angulartics.js'
],
dest: paths.libDir + 'angulartics'
},
//{
// src: paths.npmDir + 'duo_web_sdk/index.js',
// dest: paths.libDir + 'duo'
//},
{
src: paths.jsDir + 'duo.js',
dest: paths.libDir + 'duo'
},
{
src: paths.npmDir + 'angular-promise-polyfill/index.js',
dest: paths.libDir + 'angular-promise-polyfill'
}
];
@@ -213,6 +244,7 @@ function config() {
createModule: false,
constants: _.merge({}, {
appSettings: {
selfHosted: false,
version: project.version,
environment: project.env
}
@@ -261,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 () {
@@ -274,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'
},
@@ -293,12 +325,40 @@ gulp.task('dist:move', function () {
src: paths.npmDir + 'angular/angular.min.js',
dest: paths.dist + 'lib/angular'
},
{
src: paths.npmDir + 'node-forge/dist/prime.worker.*',
dest: paths.dist + 'lib/forge'
},
//{
// src: paths.npmDir + 'duo_web_sdk/index.js',
// dest: paths.dist + 'lib/duo'
//},
{
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'
},
{
src: [
paths.webroot + '**/app/**/*.html',
paths.webroot + '**/images/**/*',
paths.webroot + 'index.html',
paths.webroot + 'favicon.ico'
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
}
@@ -317,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'));
@@ -333,10 +393,35 @@ 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('.'));
});
gulp.task('dist:js:fallback', function () {
var mainStream = gulp
.src([
paths.jsDir + 'fallback*.js'
]);
merge(mainStream)
.pipe(preprocess({ context: { cacheTag: randomString, selfHosted: selfHosted } }))
//.pipe(uglify())
.pipe(rename({ suffix: '.min' }))
.pipe(gulp.dest(paths.dist + 'js'));
});
gulp.task('dist:js:u2f', function () {
var mainStream = gulp
.src([
paths.jsDir + 'u2f*.js'
]);
merge(mainStream)
.pipe(concat(paths.dist + '/js/u2f.min.js'))
//.pipe(uglify())
.pipe(gulp.dest('.'));
});
@@ -351,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('.'));
});
@@ -360,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: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' }));
@@ -388,6 +485,15 @@ gulp.task('deploy-preview', ['dist'], function () {
gulp.task('serve', function () {
connect.server({
port: 4001,
root: ['src']
root: ['src'],
//https: true,
middleware: function (connect, opt) {
return [function (req, res, next) {
if (req.originalUrl.indexOf('app-id.json') > -1) {
res.setHeader('Content-Type', 'application/fido.trusted-apps+json');
}
next();
}];
}
});
});

9469
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,52 +1,56 @@
{
"name": "bitwarden",
"version": "1.10.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-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"
"exposify": "0.5.0",
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git",
"angular-promise-polyfill": "0.0.4"
}
}

View File

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

View File

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

View File

@@ -1,6 +1,9 @@
{
"appSettings": {
"apiUri": "http://localhost:4000",
"identityUri": "http://localhost:33656"
"identityUri": "http://localhost:33656",
"iconsUri": "https://icons.bitwarden.com",
"stripeKey": "pk_test_KPoCfZXu7mznb9uSCPZ2JpTD",
"braintreeKey": "sandbox_r72q8jq6_9pnxkwm75f87sdc2"
}
}

15
src/app-id.json Normal file
View File

@@ -0,0 +1,15 @@
{
"trustedFacets": [
{
"version": {
"major": 1,
"minor": 0
},
"ids": [
"https://vault.bitwarden.com",
"ios:bundle-id:com.8bit.bitwarden",
"android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI"
]
}
]
}

View File

@@ -2,35 +2,55 @@ angular
.module('bit.accounts')
.controller('accountsLoginController', function ($scope, $rootScope, $cookies, apiService, cryptoService, authService,
$state, constants, $analytics) {
$state, constants, $analytics, $uibModal, $timeout, $window, $filter, toastr) {
$scope.state = $state;
$scope.twoFactorProviderConstants = constants.twoFactorProvider;
$scope.rememberTwoFactor = { checked: false };
var stopU2fCheck = true;
var returnState;
if (!$state.params.returnState && $state.params.org) {
returnState = {
$scope.returnState = $state.params.returnState;
$scope.stateEmail = $state.params.email;
if (!$scope.returnState && $state.params.org) {
$scope.returnState = {
name: 'backend.user.settingsCreateOrg',
params: { plan: $state.params.org }
};
}
else {
returnState = $state.params.returnState;
}
var rememberedEmail = $cookies.get(constants.rememberedEmailCookieName);
if (rememberedEmail || $state.params.email) {
$scope.model = {
email: $state.params.email ? $state.params.email : rememberedEmail,
rememberEmail: rememberedEmail !== null
else if (!$scope.returnState && $state.params.premium) {
$scope.returnState = {
name: 'backend.user.settingsPremium'
};
}
var email,
masterPassword;
if ($state.current.name.indexOf('twoFactor') > -1 && (!$scope.twoFactorProviders || !$scope.twoFactorProviders.length)) {
$state.go('frontend.login.info', { returnState: $scope.returnState });
}
var rememberedEmail = $cookies.get(constants.rememberedEmailCookieName);
if (rememberedEmail || $scope.stateEmail) {
$scope.model = {
email: $scope.stateEmail || rememberedEmail,
rememberEmail: rememberedEmail !== null
};
$timeout(function () {
$("#masterPassword").focus();
});
}
else {
$timeout(function () {
$("#email").focus();
});
}
var _email,
_masterPassword;
$scope.twoFactorProviders = null;
$scope.twoFactorProvider = null;
$scope.login = function (model) {
$scope.loginPromise = authService.logIn(model.email, model.masterPassword);
$scope.loginPromise.then(function (twoFactorProviders) {
$scope.loginPromise = authService.logIn(model.email, model.masterPassword).then(function (twoFactorProviders) {
if (model.rememberEmail) {
var cookieExpiration = new Date();
cookieExpiration.setFullYear(cookieExpiration.getFullYear() + 10);
@@ -44,36 +64,216 @@ angular
$cookies.remove(constants.rememberedEmailCookieName);
}
if (twoFactorProviders && twoFactorProviders.length > 0) {
email = model.email;
masterPassword = model.masterPassword;
if (twoFactorProviders && Object.keys(twoFactorProviders).length > 0) {
_email = model.email;
_masterPassword = model.masterPassword;
$scope.twoFactorProviders = cleanProviders(twoFactorProviders);
$scope.twoFactorProvider = getDefaultProvider($scope.twoFactorProviders);
$analytics.eventTrack('Logged In To Two-step');
$state.go('frontend.login.twoFactor', { returnState: returnState });
$state.go('frontend.login.twoFactor', { returnState: $scope.returnState }).then(function () {
$timeout(function () {
$("#code").focus();
init();
});
});
}
else {
$analytics.eventTrack('Logged In');
loggedInGo();
}
model.masterPassword = '';
});
};
$scope.twoFactor = function (model) {
// Only supporting Authenticator (0) provider for now
$scope.twoFactorPromise = authService.logIn(email, masterPassword, model.code, 0);
function getDefaultProvider(twoFactorProviders) {
var keys = Object.keys(twoFactorProviders);
var providerType = null;
var providerPriority = -1;
for (var i = 0; i < keys.length; i++) {
var provider = $filter('filter')(constants.twoFactorProviderInfo, { type: keys[i], active: true });
if (provider.length && provider[0].priority > providerPriority) {
if (provider[0].type === constants.twoFactorProvider.u2f && !u2f.isSupported) {
continue;
}
providerType = provider[0].type;
providerPriority = provider[0].priority;
}
}
if (providerType === null) {
return null;
}
return parseInt(providerType);
}
function cleanProviders(twoFactorProviders) {
if (canUseSecurityKey()) {
return twoFactorProviders;
}
var keys = Object.keys(twoFactorProviders);
for (var i = 0; i < keys.length; i++) {
var provider = $filter('filter')(constants.twoFactorProviderInfo, {
type: keys[i],
active: true,
requiresUsb: false
});
if (!provider.length) {
delete twoFactorProviders[keys[i]];
}
}
return twoFactorProviders;
}
// ref: https://stackoverflow.com/questions/11381673/detecting-a-mobile-browser
function canUseSecurityKey() {
var mobile = false;
(function (a) {
if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4))) {
mobile = true;
}
})(navigator.userAgent || navigator.vendor || window.opera);
return !mobile && !navigator.userAgent.match(/iPad/i);
}
$scope.twoFactor = function (token) {
if ($scope.twoFactorProvider === constants.twoFactorProvider.email ||
$scope.twoFactorProvider === constants.twoFactorProvider.authenticator) {
token = token.replace(' ', '');
}
$scope.twoFactorPromise = authService.logIn(_email, _masterPassword, token, $scope.twoFactorProvider,
$scope.rememberTwoFactor.checked || false);
$scope.twoFactorPromise.then(function () {
$analytics.eventTrack('Logged In From Two-step');
loggedInGo();
}, function () {
if ($scope.twoFactorProvider === constants.twoFactorProvider.u2f) {
init();
}
});
};
$scope.anotherMethod = function () {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/accounts/views/accountsTwoFactorMethods.html',
controller: 'accountsTwoFactorMethodsController',
resolve: {
providers: function () { return $scope.twoFactorProviders; }
}
});
modal.result.then(function (provider) {
$scope.twoFactorProvider = provider;
$timeout(function () {
$("#code").focus();
init();
});
});
};
$scope.sendEmail = function (doToast) {
if ($scope.twoFactorProvider !== constants.twoFactorProvider.email) {
return;
}
return cryptoService.makeKeyAndHash(_email, _masterPassword).then(function (result) {
return apiService.twoFactor.sendEmailLogin({
email: _email,
masterPasswordHash: result.hash
}).$promise;
}).then(function () {
if (doToast) {
toastr.success('Verification email sent to ' + $scope.twoFactorEmail + '.');
}
}, function () {
toastr.error('Could not send verification email.');
});
};
$scope.$on('$destroy', function () {
stopU2fCheck = true;
});
function loggedInGo() {
if (returnState) {
$state.go(returnState.name, returnState.params);
if ($scope.returnState) {
$state.go($scope.returnState.name, $scope.returnState.params);
}
else {
$state.go('backend.user.vault');
}
}
function init() {
stopU2fCheck = true;
var params;
if ($scope.twoFactorProvider === constants.twoFactorProvider.duo ||
$scope.twoFactorProvider === constants.twoFactorProvider.organizationDuo) {
params = $scope.twoFactorProviders[$scope.twoFactorProvider];
$window.Duo.init({
host: params.Host,
sig_request: params.Signature,
submit_callback: function (theForm) {
var response = $(theForm).find('input[name="sig_response"]').val();
$scope.twoFactor(response);
}
});
}
else if ($scope.twoFactorProvider === constants.twoFactorProvider.u2f) {
stopU2fCheck = false;
params = $scope.twoFactorProviders[constants.twoFactorProvider.u2f];
var challenges = JSON.parse(params.Challenges);
initU2f(challenges);
}
else if ($scope.twoFactorProvider === constants.twoFactorProvider.email) {
params = $scope.twoFactorProviders[constants.twoFactorProvider.email];
$scope.twoFactorEmail = params.Email;
if (Object.keys($scope.twoFactorProviders).length > 1) {
$scope.sendEmail(false);
}
}
}
function initU2f(challenges) {
if (stopU2fCheck) {
return;
}
if (challenges.length < 1 || $scope.twoFactorProvider !== constants.twoFactorProvider.u2f) {
return;
}
console.log('listening for u2f key...');
$window.u2f.sign(challenges[0].appId, challenges[0].challenge, [{
version: challenges[0].version,
keyHandle: challenges[0].keyHandle
}], function (data) {
if ($scope.twoFactorProvider !== constants.twoFactorProvider.u2f) {
return;
}
if (data.errorCode) {
console.log(data.errorCode);
$timeout(function () {
initU2f(challenges);
}, data.errorCode === 5 ? 0 : 1000);
return;
}
$scope.twoFactor(JSON.stringify(data));
}, 10);
}
});

View File

@@ -42,8 +42,4 @@ angular
$scope.loading = false;
}
});
$scope.submit = function (model) {
};
});

View File

@@ -6,17 +6,16 @@ angular
$scope.submit = function (model) {
var email = model.email.toLowerCase();
var key = cryptoService.makeKey(model.masterPassword, email);
var request = {
email: email,
masterPasswordHash: cryptoService.hashPassword(model.masterPassword, key),
recoveryCode: model.code.replace(/\s/g, '').toLowerCase()
};
$scope.submitPromise = apiService.accounts.postTwoFactorRecover(request, function () {
$scope.submitPromise = cryptoService.makeKeyAndHash(model.email, model.masterPassword).then(function (result) {
return apiService.twoFactor.recover({
email: email,
masterPasswordHash: result.hash,
recoveryCode: model.code.replace(/\s/g, '').toLowerCase()
}).$promise;
}).then(function () {
$analytics.eventTrack('Recovered 2FA');
$scope.success = true;
}).$promise;
});
};
});

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

@@ -2,7 +2,7 @@ angular
.module('bit.accounts')
.controller('accountsRegisterController', function ($scope, $location, apiService, cryptoService, validationService,
$analytics, $state) {
$analytics, $state, $timeout) {
var params = $location.search();
var stateParams = $state.params;
$scope.createOrg = stateParams.org;
@@ -13,6 +13,12 @@ angular
params: { plan: $state.params.org }
};
}
else if (!stateParams.returnState && stateParams.premium) {
$scope.returnState = {
name: 'backend.user.settingsPremium',
params: { plan: $state.params.org }
};
}
else {
$scope.returnState = stateParams.returnState;
}
@@ -23,6 +29,16 @@ angular
};
$scope.readOnlyEmail = stateParams.email !== null;
$timeout(function () {
if ($scope.model.email) {
$("#name").focus();
}
else {
$("#email").focus();
}
});
$scope.registerPromise = null;
$scope.register = function (form) {
var error = false;
@@ -41,14 +57,19 @@ angular
}
var email = $scope.model.email.toLowerCase();
var key = cryptoService.makeKey($scope.model.masterPassword, email);
var makeResult, encKey;
$scope.registerPromise = cryptoService.makeKeyPair(key).then(function (result) {
$scope.registerPromise = cryptoService.makeKeyAndHash(email, $scope.model.masterPassword).then(function (result) {
makeResult = result;
encKey = cryptoService.makeEncKey(result.key);
return cryptoService.makeKeyPair(encKey.encKey);
}).then(function (result) {
var request = {
name: $scope.model.name,
email: email,
masterPasswordHash: cryptoService.hashPassword($scope.model.masterPassword, key),
masterPasswordHash: makeResult.hash,
masterPasswordHint: $scope.model.masterPasswordHint,
key: encKey.encKeyEnc,
keys: {
publicKey: result.publicKey,
encryptedPrivateKey: result.privateKeyEnc

View File

@@ -0,0 +1,43 @@
angular
.module('bit.accounts')
.controller('accountsTwoFactorMethodsController', function ($scope, $uibModalInstance, $analytics, providers, constants) {
$analytics.eventTrack('accountsTwoFactorMethodsController', { category: 'Modal' });
$scope.providers = [];
if (providers.hasOwnProperty(constants.twoFactorProvider.organizationDuo)) {
add(constants.twoFactorProvider.organizationDuo);
}
if (providers.hasOwnProperty(constants.twoFactorProvider.authenticator)) {
add(constants.twoFactorProvider.authenticator);
}
if (providers.hasOwnProperty(constants.twoFactorProvider.yubikey)) {
add(constants.twoFactorProvider.yubikey);
}
if (providers.hasOwnProperty(constants.twoFactorProvider.email)) {
add(constants.twoFactorProvider.email);
}
if (providers.hasOwnProperty(constants.twoFactorProvider.duo)) {
add(constants.twoFactorProvider.duo);
}
if (providers.hasOwnProperty(constants.twoFactorProvider.u2f) && u2f.isSupported) {
add(constants.twoFactorProvider.u2f);
}
$scope.choose = function (provider) {
$uibModalInstance.close(provider.type);
};
$scope.close = function () {
$uibModalInstance.dismiss('close');
};
function add(type) {
for (var i = 0; i < constants.twoFactorProviderInfo.length; i++) {
if (constants.twoFactorProviderInfo[i].type === type) {
$scope.providers.push(constants.twoFactorProviderInfo[i]);
}
}
}
});

View File

@@ -0,0 +1,28 @@
angular
.module('bit.accounts')
.controller('accountsVerifyEmailController', function ($scope, $state, apiService, toastr, $analytics) {
if (!$state.params.userId || !$state.params.token) {
$state.go('frontend.login.info').then(function () {
toastr.error('Invalid parameters.');
});
return;
}
$scope.$on('$viewContentLoaded', function () {
apiService.accounts.verifyEmailToken({},
{
token: $state.params.token,
userId: $state.params.userId
}, function () {
$analytics.eventTrack('Verified Email');
$state.go('frontend.login.info', null, { location: 'replace' }).then(function () {
toastr.success('Your email has been verified. Thank you.', 'Success');
});
}, function () {
$state.go('frontend.login.info', null, { location: 'replace' }).then(function () {
toastr.error('Unable to verify email.', 'Error');
});
});
});
});

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

@@ -1,7 +1,7 @@
<p class="login-box-msg">Log in to access your vault.</p>
<form name="loginForm" ng-submit="loginForm.$valid && login(model)" api-form="loginPromise">
<div class="callout callout-danger validation-errors" ng-show="loginForm.$errors">
<h4>Errors have occured</h4>
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in loginForm.$errors">{{e}}</li>
</ul>
@@ -36,7 +36,7 @@
<hr />
<ul>
<li>
<a ui-sref="frontend.register({returnState: state.params.returnState, email: state.params.email})">
<a ui-sref="frontend.register({returnState: returnState, email: stateEmail})">
Create a new account
</a>
</li>

View File

@@ -1,25 +1,168 @@
<p class="login-box-msg">Enter your two-step verification code.</p>
<form name="twoFactorForm" ng-submit="twoFactorForm.$valid && twoFactor(model)" api-form="twoFactorPromise">
<div class="callout callout-danger validation-errors" ng-show="twoFactorForm.$errors">
<h4>Errors have occured</h4>
<ul>
<li ng-repeat="e in twoFactorForm.$errors">{{e}}</li>
</ul>
<div ng-if="twoFactorProvider === twoFactorProviderConstants.authenticator ||
twoFactorProvider === twoFactorProviderConstants.email">
<p class="login-box-msg" ng-if="twoFactorProvider === twoFactorProviderConstants.authenticator">
Enter the 6 digit verification code from your authenticator app.
</p>
<div ng-if="twoFactorProvider === twoFactorProviderConstants.email" class="text-center">
<p class="login-box-msg">
Enter the 6 digit verification code that was emailed to <b>{{twoFactorEmail}}</b>.
</p>
<p>
Didn't get the email?
<a href="#" stop-click ng-click="sendEmail(true)" ng-if="twoFactorProvider === twoFactorProviderConstants.email">
Send it again
</a>
</p>
</div>
<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="model.code"
required api-field />
<span class="fa fa-lock form-control-feedback"></span>
</div>
<div class="row">
<div class="col-xs-7">
<a ui-sref="frontend.recover">Lost authenticator app?</a>
<form name="twoFactorForm" ng-submit="twoFactorForm.$valid && twoFactor(token)" api-form="twoFactorPromise">
<div class="callout callout-danger validation-errors" ng-show="twoFactorForm.$errors">
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in twoFactorForm.$errors">{{e}}</li>
</ul>
</div>
<div class="col-xs-5">
<button type="submit" class="btn btn-primary btn-block btn-flat" ng-disabled="twoFactorForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="twoFactorForm.$loading"></i>Log In
</button>
<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 autocomplete="off" autocorrect="off" autocapitalize="off"
spellcheck="false" />
<span class="fa fa-lock form-control-feedback"></span>
</div>
</div>
</form>
<div class="row">
<div class="col-xs-7">
<div class="checkbox">
<label>
<input type="checkbox" id="rememberMe" ng-model="rememberTwoFactor.checked" /> Remember Me
</label>
</div>
</div>
<div class="col-xs-5">
<button type="submit" class="btn btn-primary btn-block btn-flat" ng-disabled="twoFactorForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="twoFactorForm.$loading"></i>Log In
</button>
</div>
</div>
</form>
</div>
<div ng-if="twoFactorProvider === twoFactorProviderConstants.yubikey">
<p class="login-box-msg">
Complete logging in with YubiKey.
</p>
<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>
<li ng-repeat="e in twoFactorForm.$errors">{{e}}</li>
</ul>
</div>
<p>Insert your YubiKey into your computer's USB port, then touch its button.</p>
<p>
<img src="images/two-factor/yubikey.jpg" alt="" class="img-rounded img-responsive" />
</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"
autocomplete="new-password" required api-field />
</div>
<div class="row">
<div class="col-xs-7">
<div class="checkbox">
<label>
<input type="checkbox" id="rememberMe" ng-model="rememberTwoFactor.checked" /> Remember Me
</label>
</div>
</div>
<div class="col-xs-5">
<button type="submit" class="btn btn-primary btn-block btn-flat" ng-disabled="twoFactorForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="twoFactorForm.$loading"></i>Log In
</button>
</div>
</div>
</form>
</div>
<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"
autocomplete="off">
<div class="callout callout-danger validation-errors" ng-show="twoFactorForm.$errors">
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in twoFactorForm.$errors">{{e}}</li>
</ul>
</div>
<div id="duoFrameWrapper">
<iframe id="duo_iframe"></iframe>
</div>
<div class="row">
<div class="col-xs-7">
<div class="checkbox">
<label>
<input type="checkbox" id="rememberMe" ng-model="rememberTwoFactor.checked" /> Remember Me
</label>
</div>
</div>
<div class="col-xs-5">
<span ng-show="twoFactorForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon"></i> Logging in...
</span>
</div>
</div>
</form>
</div>
<div ng-if="twoFactorProvider === twoFactorProviderConstants.u2f">
<p class="login-box-msg">
Complete logging in with FIDO U2F.
</p>
<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>
<li ng-repeat="e in twoFactorForm.$errors">{{e}}</li>
</ul>
</div>
<p>Insert your Security Key into your computer's USB port. If it has a button, touch it.</p>
<p>
<img src="images/two-factor/u2fkey.jpg" alt="" class="img-rounded img-responsive" />
</p>
<div class="row">
<div class="col-xs-7">
<div class="checkbox">
<label>
<input type="checkbox" id="rememberMe" ng-model="rememberTwoFactor.checked" /> Remember Me
</label>
</div>
</div>
<div class="col-xs-5">
<span ng-show="twoFactorForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon"></i> Logging in...
</span>
</div>
</div>
</form>
</div>
<div ng-if="twoFactorProvider === null">
<p>
This account has two-step login enabled, however, none of the configured two-step providers are supported by this
web browser.
</p>
Please use a supported web browser (such as Chrome) and/or add additional providers that are better supported
across web browsers (such as an authenticator app).
</div>
<hr />
<ul>
<li>
<a stop-click href="#" ng-click="anotherMethod()">Use another two-step login method</a>
</li>
<li>
<a ui-sref="frontend.login.info({returnState: returnState})">Back to log in</a>
</li>
</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

@@ -13,7 +13,7 @@
<form name="passwordHintForm" ng-submit="passwordHintForm.$valid && submit(model)" ng-show="!success"
api-form="submitPromise">
<div class="callout callout-danger validation-errors" ng-show="passwordHintForm.$errors">
<h4>Errors have occured</h4>
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in passwordHintForm.$errors">{{e}}</li>
</ul>

View File

@@ -3,7 +3,11 @@
<i class="fa fa-shield"></i> <b>bit</b>warden
</div>
<div class="login-box-body">
<p class="login-box-msg">Lost your authenticator app?</p>
<p class="login-box-msg">
In the event that you cannot access your account through your normal two-step login methods, you can use your
two-step login recovery code to disable all two-step providers on your account.
<a href="https://help.bitwarden.com/article/lost-two-step-device/" target="_blank">Learn more</a>
</p>
<div class="text-center" ng-show="success">
<div class="callout callout-success">
Two-step login has been successfully disabled on your account.
@@ -13,7 +17,7 @@
<form name="recoverForm" ng-submit="recoverForm.$valid && submit(model)" ng-show="!success"
api-form="submitPromise">
<div class="callout callout-danger validation-errors" ng-show="recoverForm.$errors">
<h4>Errors have occured</h4>
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in recoverForm.$errors">{{e}}</li>
</ul>

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

@@ -18,7 +18,7 @@
<p>Before creating your organization, you first need to create a free personal account.</p>
</div>
<div class="callout callout-danger validation-errors" ng-show="registerForm.$errors">
<h4>Errors have occured</h4>
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in registerForm.$errors">{{e}}</li>
</ul>
@@ -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,25 @@
<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-key"></i> Two-step Providers</h4>
</div>
<div class="modal-body">
<div class="list-group" ng-repeat="provider in providers | orderBy: 'displayOrder'">
<a href="#" stop-click class="list-group-item" ng-click="choose(provider)">
<img alt="{{::provider.name}}" ng-src="{{'images/two-factor/' + provider.image}}" class="pull-right hidden-xs" />
<h4 class="list-group-item-heading">{{::provider.name}}</h4>
<p class="list-group-item-text">{{::provider.description}}</p>
</a>
</div>
<div class="list-group" style="margin-bottom: 0;">
<a href="https://help.bitwarden.com/article/lost-two-step-device/" target="_blank" class="list-group-item">
<h4 class="list-group-item-heading">Recovery Code</h4>
<p class="list-group-item-text">
Lost access to all of your two-factor providers? Use your recovery code to disable
all two-factor providers from your account.
</p>
</a>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
</div>

View File

@@ -0,0 +1,8 @@
<div class="login-box">
<div class="login-logo">
<i class="fa fa-shield"></i> <b>bit</b>warden
</div>
<div class="login-box-body">
Verifying email...
</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,12 @@
'ui.bootstrap.showErrors',
'toastr',
'angulartics',
// @if !selfHosted
'angulartics.google.analytics',
'angular-stripe',
'credit-cards',
// @endif
'angular-promise-polyfill',
'bit.directives',
'bit.filters',
@@ -19,5 +22,6 @@
'bit.vault',
'bit.settings',
'bit.tools',
'bit.organization'
'bit.organization',
'bit.reports'
]);

View File

@@ -2,16 +2,25 @@ angular
.module('bit')
.config(function ($stateProvider, $urlRouterProvider, $httpProvider, jwtInterceptorProvider, jwtOptionsProvider,
$uibTooltipProvider, toastrConfig, $locationProvider, $qProvider, stripeProvider) {
$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.8']
whiteListedDomains: ['localhost', 'api.bitwarden.com', 'vault.bitwarden.com', 'haveibeenpwned.com']
});
var refreshPromise;
jwtInterceptorProvider.tokenGetter = /*@ngInject*/ function (options, appSettings, tokenService, authService) {
if (options.url.indexOf(appSettings.apiUri) !== 0) {
jwtInterceptorProvider.tokenGetter = /*@ngInject*/ function (options, tokenService, authService) {
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;
};
stripeProvider.setPublishableKey('pk_live_bpN0P37nMxrMQkcaHXtAybJk');
// @if !selfHosted
stripeProvider.setPublishableKey(appSettings.stripeKey);
// @endif
angular.extend(toastrConfig, {
closeButton: true,
@@ -49,12 +65,15 @@ angular
appendToBody: true
});
if ($httpProvider.defaults.headers.post) {
$httpProvider.defaults.headers.post = {};
// stop IE from caching get requests
if (navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > 0) {
if (!$httpProvider.defaults.headers.get) {
$httpProvider.defaults.headers.get = {};
}
$httpProvider.defaults.headers.get['Cache-Control'] = 'no-cache';
$httpProvider.defaults.headers.get.Pragma = 'no-cache';
}
$httpProvider.defaults.headers.post['Content-Type'] = 'text/plain; charset=utf-8';
$httpProvider.interceptors.push('apiInterceptor');
$httpProvider.interceptors.push('jwtInterceptor');
@@ -77,17 +96,14 @@ angular
url: '^/vault',
templateUrl: 'app/vault/views/vault.html',
controller: 'vaultController',
data: { pageTitle: 'My Vault' },
data: {
pageTitle: 'My Vault',
controlSidebar: true
},
params: {
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',
@@ -100,23 +116,41 @@ angular
controller: 'settingsDomainsController',
data: { pageTitle: 'Domain Settings' }
})
.state('backend.user.settingsTwoStep', {
url: '^/settings/two-step',
templateUrl: 'app/settings/views/settingsTwoStep.html',
controller: 'settingsTwoStepController',
data: { pageTitle: 'Two-step Login' }
})
.state('backend.user.settingsCreateOrg', {
url: '^/settings/create-organization',
templateUrl: 'app/settings/views/settingsCreateOrganization.html',
controller: 'settingsCreateOrganizationController',
data: { pageTitle: 'Create Organization' }
})
.state('backend.user.settingsBilling', {
url: '^/settings/billing',
templateUrl: 'app/settings/views/settingsBilling.html',
controller: 'settingsBillingController',
data: { pageTitle: 'Billing' }
})
.state('backend.user.settingsPremium', {
url: '^/settings/premium',
templateUrl: 'app/settings/views/settingsPremium.html',
controller: 'settingsPremiumController',
data: { pageTitle: 'Go Premium' }
})
.state('backend.user.tools', {
url: '^/tools',
templateUrl: 'app/tools/views/tools.html',
controller: 'toolsController',
data: { pageTitle: 'Tools' }
})
.state('backend.user.apps', {
url: '^/apps',
templateUrl: 'app/views/apps.html',
controller: 'appsController',
data: { pageTitle: 'Get the Apps' }
.state('backend.user.reportsBreach', {
url: '^/reports/breach',
templateUrl: 'app/reports/views/reportsBreach.html',
controller: 'reportsBreachController',
data: { pageTitle: 'Data Breach Report' }
})
.state('backend.org', {
templateUrl: 'app/views/organizationLayout.html',
@@ -129,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' }
@@ -153,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', {
@@ -178,25 +221,26 @@ angular
controller: 'accountsLoginController',
params: {
returnState: null,
email: null
email: null,
premium: null,
org: null
},
data: {
bodyClass: 'login-page'
}
})
.state('frontend.login.info', {
url: '^/?org',
url: '^/?org&premium&email',
templateUrl: 'app/accounts/views/accountsLoginInfo.html',
data: {
pageTitle: 'Log In'
}
})
.state('frontend.login.twoFactor', {
url: '^/two-factor',
url: '^/two-step?org&premium&email',
templateUrl: 'app/accounts/views/accountsLoginTwoFactor.html',
data: {
pageTitle: 'Log In (Two Factor)',
authorizeTwoFactor: true
pageTitle: 'Log In (Two-step)'
}
})
.state('frontend.logout', {
@@ -224,13 +268,33 @@ 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',
url: '^/register?org&premium',
templateUrl: 'app/accounts/views/accountsRegister.html',
controller: 'accountsRegisterController',
params: {
returnState: null,
email: null
email: null,
org: null,
premium: null
},
data: {
pageTitle: 'Register',
@@ -246,6 +310,16 @@ angular
bodyClass: 'login-page',
skipAuthorize: true
}
})
.state('frontend.verifyEmail', {
url: '^/verify-email?userId&token',
templateUrl: 'app/accounts/views/accountsVerifyEmail.html',
controller: 'accountsVerifyEmailController',
data: {
pageTitle: 'Verifying Email',
bodyClass: 'login-page',
skipAuthorize: true
}
});
})
.run(function ($rootScope, authService, $state) {
@@ -278,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

@@ -6,7 +6,9 @@ angular.module('bit')
AesCbc128_HmacSha256_B64: 1,
AesCbc256_HmacSha256_B64: 2,
Rsa2048_OaepSha256_B64: 3,
Rsa2048_OaepSha1_B64: 4
Rsa2048_OaepSha1_B64: 4,
Rsa2048_OaepSha256_HmacSha256_B64: 5,
Rsa2048_OaepSha1_HmacSha256_B64: 6
},
orgUserType: {
owner: 0,
@@ -18,6 +20,158 @@ angular.module('bit')
accepted: 1,
confirmed: 2
},
twoFactorProvider: {
u2f: 4,
yubikey: 3,
duo: 2,
authenticator: 0,
email: 1,
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.',
enabled: false,
active: true,
free: true,
image: 'authapp.png',
displayOrder: 0,
priority: 1,
requiresUsb: false,
organization: false
},
{
type: 3,
name: 'YubiKey OTP Security Key',
description: 'Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices.',
enabled: false,
active: true,
image: 'yubico.png',
displayOrder: 1,
priority: 3,
requiresUsb: true,
organization: false
},
{
type: 2,
name: 'Duo',
description: 'Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.',
enabled: false,
active: true,
image: 'duo.png',
displayOrder: 2,
priority: 2,
requiresUsb: false,
organization: false
},
{
type: 4,
name: 'FIDO U2F Security Key',
description: 'Use any FIDO U2F enabled security key to access your account.',
enabled: false,
active: true,
image: 'fido.png',
displayOrder: 3,
priority: 4,
requiresUsb: true,
organization: false
},
{
type: 1,
name: 'Email',
description: 'Verification codes will be emailed to you.',
enabled: false,
active: true,
free: true,
image: 'gmail.png',
displayOrder: 4,
priority: 0,
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: {
free: {
basePrice: 0,
@@ -25,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: {
@@ -46,6 +198,23 @@ angular.module('bit')
monthPlanType: 'teamsMonthly',
annualPlanType: 'teamsAnnually',
upgradeSortOrder: 2
},
enterprise: {
seatPrice: 3,
annualSeatPrice: 36,
monthlySeatPrice: 4,
monthPlanType: 'enterpriseMonthly',
annualPlanType: 'enterpriseAnnually',
upgradeSortOrder: 3
}
},
storageGb: {
price: 0.33,
monthlyPrice: 0.50,
yearlyPrice: 4
},
premium: {
price: 10,
yearlyPrice: 10
}
});

View File

@@ -1,7 +1,7 @@
angular
.module('bit.directives')
.directive('apiForm', function ($rootScope, validationService) {
.directive('apiForm', function ($rootScope, validationService, $timeout) {
return {
require: 'form',
restrict: 'A',
@@ -25,12 +25,21 @@ angular
form.$loading = true;
promise.then(function success(response) {
form.$loading = false;
$timeout(function () {
form.$loading = false;
});
}, function failure(reason) {
form.$loading = false;
validationService.addErrors(form, reason);
scope.$broadcast('show-errors-check-validity');
$('html, body').animate({ scrollTop: 0 }, 200);
$timeout(function () {
form.$loading = false;
if (typeof reason === 'string') {
validationService.addError(form, null, reason, true);
}
else {
validationService.addErrors(form, reason);
}
scope.$broadcast('show-errors-check-validity');
$('html, body').animate({ scrollTop: 0 }, 200);
});
});
}
});

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

@@ -13,10 +13,11 @@ angular
return undefined;
}
var key = cryptoService.makeKey(value, profile.email);
var valid = key.keyB64 === cryptoService.getKey().keyB64;
ngModel.$setValidity('masterPassword', valid);
return valid ? value : undefined;
return cryptoService.makeKey(value, profile.email).then(function (result) {
var valid = result.keyB64 === cryptoService.getKey().keyB64;
ngModel.$setValidity('masterPassword', valid);
return valid ? value : undefined;
});
});
// For model -> DOM validation
@@ -25,11 +26,11 @@ angular
return undefined;
}
var key = cryptoService.makeKey(value, profile.email);
var valid = key.keyB64 === cryptoService.getKey().keyB64;
ngModel.$setValidity('masterPassword', valid);
return value;
return cryptoService.makeKey(value, profile.email).then(function (result) {
var valid = result.keyB64 === cryptoService.getKey().keyB64;
ngModel.$setValidity('masterPassword', valid);
return value;
});
});
});
}

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

@@ -0,0 +1,11 @@
angular
.module('bit.directives')
// ref: https://stackoverflow.com/a/14165848/1090359
.directive('stopClick', function () {
return function (scope, element, attrs) {
$(element).click(function (event) {
event.preventDefault();
});
};
});

View File

@@ -0,0 +1,10 @@
angular
.module('bit.directives')
.directive('stopProp', function () {
return function (scope, element, attrs) {
$(element).click(function (event) {
event.stopPropagation();
});
};
});

View File

@@ -0,0 +1,193 @@
angular
.module('bit.directives')
.directive('totp', function ($timeout, $q) {
return {
template: '<div class="totp{{(low ? \' low\' : \'\')}}" ng-if="code">' +
'<span class="totp-countdown"><span class="totp-sec">{{sec}}</span>' +
'<svg><g><circle class="totp-circle inner" r="12.6" cy="16" cx="16" style="stroke-dashoffset: {{dash}}px;"></circle>' +
'<circle class="totp-circle outer" r="14" cy="16" cx="16"></circle></g></svg></span>' +
'<span class="totp-code" id="totp-code">{{codeFormatted}}</span>' +
'<a href="#" stop-click class="btn btn-link" ngclipboard ngclipboard-error="clipboardError(e)" ' +
'data-clipboard-text="{{code}}" uib-tooltip="Copy Code" tooltip-placement="right">' +
'<i class="fa fa-clipboard"></i></a>' +
'</div>',
restrict: 'A',
scope: {
key: '=totp'
},
link: function (scope) {
var interval = null;
var Totp = function () {
var b32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
var leftpad = function (s, l, p) {
if (l + 1 >= s.length) {
s = Array(l + 1 - s.length).join(p) + s;
}
return s;
};
var dec2hex = function (d) {
return (d < 15.5 ? '0' : '') + Math.round(d).toString(16);
};
var hex2dec = function (s) {
return parseInt(s, 16);
};
var hex2bytes = function (s) {
var bytes = new Uint8Array(s.length / 2);
for (var i = 0; i < s.length; i += 2) {
bytes[i / 2] = parseInt(s.substr(i, 2), 16);
}
return bytes;
};
var buff2hex = function (buff) {
var bytes = new Uint8Array(buff);
var hex = [];
for (var i = 0; i < bytes.length; i++) {
hex.push((bytes[i] >>> 4).toString(16));
hex.push((bytes[i] & 0xF).toString(16));
}
return hex.join('');
};
var b32tohex = function (s) {
s = s.toUpperCase();
var cleanedInput = '';
var i;
for (i = 0; i < s.length; i++) {
if (b32Chars.indexOf(s[i]) < 0) {
continue;
}
cleanedInput += s[i];
}
s = cleanedInput;
var bits = '';
var hex = '';
for (i = 0; i < s.length; i++) {
var byteIndex = b32Chars.indexOf(s.charAt(i));
if (byteIndex < 0) {
continue;
}
bits += leftpad(byteIndex.toString(2), 5, '0');
}
for (i = 0; i + 4 <= bits.length; i += 4) {
var chunk = bits.substr(i, 4);
hex = hex + parseInt(chunk, 2).toString(16);
}
return hex;
};
var b32tobytes = function (s) {
return hex2bytes(b32tohex(s));
};
var sign = function (keyBytes, timeBytes) {
return window.crypto.subtle.importKey('raw', keyBytes,
{ name: 'HMAC', hash: { name: 'SHA-1' } }, false, ['sign']).then(function (key) {
return window.crypto.subtle.sign({ name: 'HMAC', hash: { name: 'SHA-1' } }, key, timeBytes);
}).then(function (signature) {
return buff2hex(signature);
}).catch(function (err) {
return null;
});
};
this.getCode = function (keyb32) {
var epoch = Math.round(new Date().getTime() / 1000.0);
var timeHex = leftpad(dec2hex(Math.floor(epoch / 30)), 16, '0');
var timeBytes = hex2bytes(timeHex);
var keyBytes = b32tobytes(keyb32);
if (!keyBytes.length || !timeBytes.length) {
return $q(function (resolve, reject) {
resolve(null);
});
}
return sign(keyBytes, timeBytes).then(function (hashHex) {
if (!hashHex) {
return null;
}
var offset = hex2dec(hashHex.substring(hashHex.length - 1));
var otp = (hex2dec(hashHex.substr(offset * 2, 8)) & hex2dec('7fffffff')) + '';
otp = (otp).substr(otp.length - 6, 6);
return otp;
});
};
};
var totp = new Totp();
var updateCode = function (scope) {
totp.getCode(scope.key).then(function (code) {
$timeout(function () {
if (code) {
scope.codeFormatted = code.substring(0, 3) + ' ' + code.substring(3);
scope.code = code;
}
else {
scope.code = null;
if (interval) {
clearInterval(interval);
}
}
});
});
};
var tick = function (scope) {
$timeout(function () {
var epoch = Math.round(new Date().getTime() / 1000.0);
var mod = epoch % 30;
var sec = 30 - mod;
scope.sec = sec;
scope.dash = (2.62 * mod).toFixed(2);
scope.low = sec <= 7;
if (mod === 0) {
updateCode(scope);
}
});
};
scope.$watch('key', function () {
if (!scope.key) {
scope.code = null;
if (interval) {
clearInterval(interval);
}
return;
}
updateCode(scope);
tick(scope);
if (interval) {
clearInterval(interval);
}
interval = setInterval(function () {
tick(scope);
}, 1000);
});
scope.$on('$destroy', function () {
if (interval) {
clearInterval(interval);
}
});
scope.clipboardError = function (e) {
alert('Your web browser does not support easy clipboard copying.');
};
},
};
});

View File

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

View File

@@ -1,11 +1,16 @@
angular
.module('bit.global')
.controller('mainController', function ($scope, $state, authService, appSettings, toastr, $window, $document) {
.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 = $window.navigator.userAgent.indexOf('MSIE') !== -1 ||
$window.navigator.userAgent.indexOf('SamsungBrowser') !== -1;
$scope.currentYear = new Date().getFullYear();
@@ -24,11 +29,12 @@ angular
$.AdminLTE.pushMenu.expandOnHover();
}
$(document).off('click', '.sidebar li a');
$document.off('click', '.sidebar li a');
}
});
$scope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams) {
vm.usingEncKey = !!cryptoService.getEncKey();
vm.searchVaultText = null;
if (toState.data.bodyClass) {
@@ -38,18 +44,21 @@ angular
else {
vm.bodyClass = '';
}
vm.usingControlSidebar = !!toState.data.controlSidebar;
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 () {
@@ -60,6 +69,38 @@ angular
$scope.$broadcast('organizationPeopleInvite');
};
$scope.addOrganizationGroup = function () {
$scope.$broadcast('organizationGroupsAdd');
};
$scope.updateKey = function () {
$uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsUpdateKey.html',
controller: 'settingsUpdateKeyController'
});
};
$scope.verifyEmail = function () {
if ($scope.sendingVerify) {
return;
}
$scope.sendingVerify = true;
apiService.accounts.verifyEmail({}, null).$promise.then(function () {
toastr.success('Verification email sent.');
$scope.sendingVerify = false;
$scope.verifyEmailSent = true;
}).catch(function () {
toastr.success('Verification email failed.');
$scope.sendingVerify = false;
});
};
$scope.updateBrowser = function () {
$window.open('https://browser-update.org/update.html', '_blank');
};
// Append dropdown menu somewhere else
var bodyScrollbarWidth,
appendedDropdownMenu,
@@ -101,7 +142,7 @@ angular
var offset = target.offset();
var css = {
display: 'block',
top: offset.top + target.outerHeight()
top: offset.top + target.outerHeight() - (appendTo !== 'body' ? $(window).scrollTop() : 0)
};
if (appendedDropdownMenu.hasClass('dropdown-menu-right')) {

View File

@@ -0,0 +1,26 @@
angular
.module('bit.global')
.controller('paidOrgRequiredController', function ($scope, $state, $uibModalInstance, $analytics, $uibModalStack, orgId,
constants, authService) {
$analytics.eventTrack('paidOrgRequiredController', { category: 'Modal' });
authService.getUserProfile().then(function (profile) {
$scope.admin = profile.organizations[orgId].type !== constants.orgUserType.user;
});
$scope.go = function () {
if (!$scope.admin) {
return;
}
$analytics.eventTrack('Get Paid Org');
$state.go('backend.org.billing', { orgId: orgId }).then(function () {
$uibModalStack.dismissAll();
});
};
$scope.close = function () {
$uibModalInstance.dismiss('close');
};
});

View File

@@ -0,0 +1,17 @@
angular
.module('bit.global')
.controller('premiumRequiredController', function ($scope, $state, $uibModalInstance, $analytics, $uibModalStack) {
$analytics.eventTrack('premiumRequiredController', { category: 'Modal' });
$scope.go = function () {
$analytics.eventTrack('Get Premium');
$state.go('backend.user.settingsPremium').then(function () {
$uibModalStack.dismissAll();
});
};
$scope.close = function () {
$uibModalInstance.dismiss('close');
};
});

View File

@@ -1,12 +1,23 @@
angular
.module('bit.global')
.controller('sideNavController', function ($scope, $state, authService, toastr, $analytics) {
.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;
@@ -31,7 +42,7 @@ angular
});
$scope.viewOrganization = function (org) {
if (org.type === 2) { // 2 = User
if (org.type === constants.orgUserType.user) {
toastr.error('You cannot manage this organization.');
return;
}
@@ -40,15 +51,7 @@ 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 === 0;
return org && org.type === constants.orgUserType.owner;
};
});

View File

@@ -2,5 +2,13 @@ angular
.module('bit.global')
.controller('topNavController', function ($scope) {
$scope.toggleControlSidebar = function () {
var bod = $('body');
if (!bod.hasClass('control-sidebar-open')) {
bod.addClass('control-sidebar-open');
}
else {
bod.removeClass('control-sidebar-open');
}
};
});

View File

@@ -0,0 +1,37 @@
angular
.module('bit.organization')
.controller('organizationBillingAdjustStorageController', function ($scope, $state, $uibModalInstance, apiService,
$analytics, toastr, add) {
$analytics.eventTrack('organizationBillingAdjustStorageController', { category: 'Modal' });
$scope.add = add;
$scope.storageAdjustment = 0;
$scope.submit = function () {
var request = {
storageGbAdjustment: $scope.storageAdjustment
};
if (!add) {
request.storageGbAdjustment *= -1;
}
$scope.submitPromise = apiService.organizations.putStorage({ id: $state.params.orgId }, request)
.$promise.then(function (response) {
if (add) {
$analytics.eventTrack('Added Organization Storage');
toastr.success('You have added ' + $scope.storageAdjustment + ' GB.');
}
else {
$analytics.eventTrack('Removed Organization Storage');
toastr.success('You have removed ' + $scope.storageAdjustment + ' GB.');
}
$uibModalInstance.close();
});
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@@ -1,26 +1,56 @@
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 = 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) {
$analytics.eventTrack('Changed Payment Method');
$analytics.eventTrack('Changed Organization Payment Method');
toastr.success('You have changed your payment method.');
}
else {
$analytics.eventTrack('Added Payment Method');
$analytics.eventTrack('Added Organization Payment Method');
toastr.success('You have added a payment method.');
}

View File

@@ -1,21 +1,29 @@
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/organization/views/organizationBillingChangePayment.html',
templateUrl: 'app/settings/views/settingsBillingChangePayment.html',
controller: 'organizationBillingChangePaymentController',
resolve: {
existingPaymentMethod: function () {
@@ -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',
@@ -63,7 +79,48 @@
});
};
$scope.adjustStorage = function (add) {
if ($scope.selfHosted) {
return;
}
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsBillingAdjustStorage.html',
controller: 'organizationBillingAdjustStorageController',
resolve: {
add: function () {
return add;
}
}
});
modal.result.then(function () {
load();
});
};
$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;
@@ -78,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;
}
@@ -90,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,
@@ -103,14 +233,25 @@
seats: org.Seats
};
$scope.storage = null;
if ($scope && org.MaxStorageGb) {
$scope.storage = {
currentGb: org.StorageGb || 0,
maxGb: org.MaxStorageGb,
currentName: org.StorageName || '0 GB'
};
$scope.storage.percentage = +(100 * ($scope.storage.currentGb / $scope.storage.maxGb)).toFixed(2);
}
$scope.subscription = null;
if (org.Subscription) {
$scope.subscription = {
trialEndDate: org.Subscription.TrialEndDate,
cancelledDate: org.Subscription.CancelledDate,
status: org.Subscription.Status,
cancelled: org.Subscription.Status === 'cancelled',
markedForCancel: org.Subscription.Status === 'active' && org.Subscription.CancelledDate
cancelled: org.Subscription.Cancelled,
markedForCancel: !org.Subscription.Cancelled && org.Subscription.CancelAtEndDate
};
}
@@ -139,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,11 +2,111 @@
.module('bit.organization')
.controller('organizationCollectionsAddController', function ($scope, $state, $uibModalInstance, apiService, cipherService,
$analytics) {
$analytics, authService) {
$analytics.eventTrack('organizationCollectionsAddController', { category: 'Modal' });
var groupsLength = 0;
$scope.groups = [];
$scope.selectedGroups = {};
$scope.loading = true;
$scope.useGroups = false;
$uibModalInstance.opened.then(function () {
return authService.getUserProfile();
}).then(function (profile) {
if (profile.organizations) {
var org = profile.organizations[$state.params.orgId];
$scope.useGroups = !!org.useGroups;
}
if ($scope.useGroups) {
return apiService.groups.listOrganization({ orgId: $state.params.orgId }).$promise;
}
return null;
}).then(function (groups) {
if (!groups) {
$scope.loading = false;
return;
}
var groupsArr = [];
for (var i = 0; i < groups.Data.length; i++) {
groupsArr.push({
id: groups.Data[i].Id,
name: groups.Data[i].Name,
accessAll: groups.Data[i].AccessAll
});
if (!groups.Data[i].AccessAll) {
groupsLength++;
}
}
$scope.groups = groupsArr;
$scope.loading = false;
});
$scope.toggleGroupSelectionAll = function ($event) {
var groups = {};
if ($event.target.checked) {
for (var i = 0; i < $scope.groups.length; i++) {
groups[$scope.groups[i].id] = {
id: $scope.groups[i].id,
readOnly: ($scope.groups[i].id in $scope.selectedGroups) ?
$scope.selectedGroups[$scope.groups[i].id].readOnly : false
};
}
}
$scope.selectedGroups = groups;
};
$scope.toggleGroupSelection = function (id) {
if (id in $scope.selectedGroups) {
delete $scope.selectedGroups[id];
}
else {
$scope.selectedGroups[id] = {
id: id,
readOnly: false
};
}
};
$scope.toggleGroupReadOnlySelection = function (group) {
if (group.id in $scope.selectedGroups) {
$scope.selectedGroups[group.id].readOnly = !group.accessAll && !!!$scope.selectedGroups[group.id].readOnly;
}
};
$scope.groupSelected = function (group) {
return group.id in $scope.selectedGroups || group.accessAll;
};
$scope.allSelected = function () {
return Object.keys($scope.selectedGroups).length >= groupsLength;
};
$scope.submit = function (model) {
var collection = cipherService.encryptCollection(model, $state.params.orgId);
if ($scope.useGroups) {
collection.groups = [];
for (var groupId in $scope.selectedGroups) {
if ($scope.selectedGroups.hasOwnProperty(groupId)) {
for (var i = 0; i < $scope.groups.length; i++) {
if ($scope.groups[i].id === $scope.selectedGroups[groupId].id) {
if (!$scope.groups[i].accessAll) {
collection.groups.push($scope.selectedGroups[groupId]);
}
break;
}
}
}
}
}
$scope.submitPromise = apiService.collections.post({ orgId: $state.params.orgId }, collection, function (response) {
$analytics.eventTrack('Created Collection');
var decCollection = cipherService.decryptCollection(response, $state.params.orgId, true);

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

@@ -2,19 +2,131 @@
.module('bit.organization')
.controller('organizationCollectionsEditController', function ($scope, $state, $uibModalInstance, apiService, cipherService,
$analytics, id) {
$analytics, id, authService) {
$analytics.eventTrack('organizationCollectionsEditController', { category: 'Modal' });
var groupsLength = 0;
$scope.collection = {};
$scope.groups = [];
$scope.selectedGroups = {};
$scope.loading = true;
$scope.useGroups = false;
$uibModalInstance.opened.then(function () {
apiService.collections.get({ orgId: $state.params.orgId, id: id }, function (collection) {
$scope.collection = cipherService.decryptCollection(collection);
});
return apiService.collections.getDetails({ orgId: $state.params.orgId, id: id }).$promise;
}).then(function (collection) {
$scope.collection = cipherService.decryptCollection(collection);
var groups = {};
if (collection.Groups) {
for (var i = 0; i < collection.Groups.length; i++) {
groups[collection.Groups[i].Id] = {
id: collection.Groups[i].Id,
readOnly: collection.Groups[i].ReadOnly
};
}
}
$scope.selectedGroups = groups;
return authService.getUserProfile();
}).then(function (profile) {
if (profile.organizations) {
var org = profile.organizations[$state.params.orgId];
$scope.useGroups = !!org.useGroups;
}
if ($scope.useGroups) {
return apiService.groups.listOrganization({ orgId: $state.params.orgId }).$promise;
}
return null;
}).then(function (groups) {
if (!groups) {
$scope.loading = false;
return;
}
var groupsArr = [];
for (var i = 0; i < groups.Data.length; i++) {
groupsArr.push({
id: groups.Data[i].Id,
name: groups.Data[i].Name,
accessAll: groups.Data[i].AccessAll
});
if (!groups.Data[i].AccessAll) {
groupsLength++;
}
}
$scope.groups = groupsArr;
$scope.loading = false;
});
$scope.toggleGroupSelectionAll = function ($event) {
var groups = {};
if ($event.target.checked) {
for (var i = 0; i < $scope.groups.length; i++) {
groups[$scope.groups[i].id] = {
id: $scope.groups[i].id,
readOnly: ($scope.groups[i].id in $scope.selectedGroups) ?
$scope.selectedGroups[$scope.groups[i].id].readOnly : false
};
}
}
$scope.selectedGroups = groups;
};
$scope.toggleGroupSelection = function (id) {
if (id in $scope.selectedGroups) {
delete $scope.selectedGroups[id];
}
else {
$scope.selectedGroups[id] = {
id: id,
readOnly: false
};
}
};
$scope.toggleGroupReadOnlySelection = function (group) {
if (group.id in $scope.selectedGroups) {
$scope.selectedGroups[group.id].readOnly = !group.accessAll && !!!$scope.selectedGroups[group.id].readOnly;
}
};
$scope.groupSelected = function (group) {
return group.id in $scope.selectedGroups || group.accessAll;
};
$scope.allSelected = function () {
return Object.keys($scope.selectedGroups).length >= groupsLength;
};
$scope.submit = function (model) {
var collection = cipherService.encryptCollection(model, $state.params.orgId);
$scope.submitPromise = apiService.collections.put({ orgId: $state.params.orgId }, collection, function (response) {
if ($scope.useGroups) {
collection.groups = [];
for (var groupId in $scope.selectedGroups) {
if ($scope.selectedGroups.hasOwnProperty(groupId)) {
for (var i = 0; i < $scope.groups.length; i++) {
if ($scope.groups[i].id === $scope.selectedGroups[groupId].id) {
if (!$scope.groups[i].accessAll) {
collection.groups.push($scope.selectedGroups[groupId]);
}
break;
}
}
}
}
}
$scope.submitPromise = apiService.collections.put({
orgId: $state.params.orgId,
id: id
}, collection, function (response) {
$analytics.eventTrack('Edited Collection');
var decCollection = cipherService.decryptCollection(response, $state.params.orgId, true);
$uibModalInstance.close(decCollection);

View File

@@ -1,11 +0,0 @@
angular
.module('bit.organization')
.controller('organizationCollectionsGroupsController', function ($scope, $state, $uibModalInstance, collection, $analytics) {
$analytics.eventTrack('organizationCollectionsGroupsController', { category: 'Modal' });
$scope.collection = collection;
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@@ -10,18 +10,17 @@
$uibModalInstance.opened.then(function () {
$scope.loading = false;
apiService.collectionUsers.listCollection(
apiService.collections.listUsers(
{
orgId: $state.params.orgId,
collectionId: collection.id
id: collection.id
},
function (userList) {
if (userList && userList.Data.length) {
var users = [];
for (var i = 0; i < userList.Data.length; i++) {
users.push({
id: userList.Data[i].Id,
userId: userList.Data[i].UserId,
organizationUserId: userList.Data[i].OrganizationUserId,
name: userList.Data[i].Name,
email: userList.Data[i].Email,
type: userList.Data[i].Type,
@@ -41,16 +40,21 @@
return;
}
apiService.collectionUsers.del({ orgId: $state.params.orgId, id: user.id }, null, function () {
toastr.success(user.email + ' has been removed.', 'User Removed');
$analytics.eventTrack('Removed User From Collection');
var index = $scope.users.indexOf(user);
if (index > -1) {
$scope.users.splice(index, 1);
}
}, function () {
toastr.error('Unable to remove user.', 'Error');
});
apiService.collections.delUser(
{
orgId: $state.params.orgId,
id: collection.id,
orgUserId: user.organizationUserId
}, null, function () {
toastr.success(user.email + ' has been removed.', 'User Removed');
$analytics.eventTrack('Removed User From Collection');
var index = $scope.users.indexOf(user);
if (index > -1) {
$scope.users.splice(index, 1);
}
}, function () {
toastr.error('Unable to remove user.', 'Error');
});
};
$scope.close = function () {

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

@@ -5,19 +5,18 @@
authService, toastr, $analytics) {
$analytics.eventTrack('organizationDeleteController', { category: 'Modal' });
$scope.submit = function () {
var request = {
masterPasswordHash: cryptoService.hashPassword($scope.masterPassword)
};
$scope.submitPromise = apiService.organizations.del({ id: $state.params.orgId }, request, function () {
$scope.submitPromise = cryptoService.hashPassword($scope.masterPassword).then(function (hash) {
return apiService.organizations.del({ id: $state.params.orgId }, {
masterPasswordHash: hash
}).$promise;
}).then(function () {
$uibModalInstance.dismiss('cancel');
authService.removeProfileOrganization($state.params.orgId);
$analytics.eventTrack('Deleted Organization');
$state.go('backend.user.vault').then(function () {
toastr.success('This organization and all associated data has been deleted.',
'Organization Deleted');
});
}).$promise;
return $state.go('backend.user.vault');
}).then(function () {
toastr.success('This organization and all associated data has been deleted.', 'Organization Deleted');
});
};
$scope.close = function () {

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

@@ -0,0 +1,87 @@
angular
.module('bit.organization')
.controller('organizationGroupsAddController', function ($scope, $state, $uibModalInstance, apiService, cipherService,
$analytics) {
$analytics.eventTrack('organizationGroupsAddController', { category: 'Modal' });
$scope.collections = [];
$scope.selectedCollections = {};
$scope.loading = true;
$uibModalInstance.opened.then(function () {
return apiService.collections.listOrganization({ orgId: $state.params.orgId }).$promise;
}).then(function (collections) {
$scope.collections = cipherService.decryptCollections(collections.Data, $state.params.orgId, true);
$scope.loading = false;
});
$scope.toggleCollectionSelectionAll = function ($event) {
var collections = {};
if ($event.target.checked) {
for (var i = 0; i < $scope.collections.length; i++) {
collections[$scope.collections[i].id] = {
id: $scope.collections[i].id,
readOnly: ($scope.collections[i].id in $scope.selectedCollections) ?
$scope.selectedCollections[$scope.collections[i].id].readOnly : false
};
}
}
$scope.selectedCollections = collections;
};
$scope.toggleCollectionSelection = function (id) {
if (id in $scope.selectedCollections) {
delete $scope.selectedCollections[id];
}
else {
$scope.selectedCollections[id] = {
id: id,
readOnly: false
};
}
};
$scope.toggleCollectionReadOnlySelection = function (id) {
if (id in $scope.selectedCollections) {
$scope.selectedCollections[id].readOnly = !!!$scope.selectedCollections[id].readOnly;
}
};
$scope.collectionSelected = function (collection) {
return collection.id in $scope.selectedCollections;
};
$scope.allSelected = function () {
return Object.keys($scope.selectedCollections).length === $scope.collections.length;
};
$scope.submit = function (model) {
var group = {
name: model.name,
accessAll: !!model.accessAll,
externalId: model.externalId
};
if (!group.accessAll) {
group.collections = [];
for (var collectionId in $scope.selectedCollections) {
if ($scope.selectedCollections.hasOwnProperty(collectionId)) {
group.collections.push($scope.selectedCollections[collectionId]);
}
}
}
$scope.submitPromise = apiService.groups.post({ orgId: $state.params.orgId }, group, function (response) {
$analytics.eventTrack('Created Group');
$uibModalInstance.close({
id: response.Id,
name: response.Name
});
}).$promise;
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@@ -1,6 +1,99 @@
angular
.module('bit.organization')
.controller('organizationGroupsController', function ($scope, $state) {
.controller('organizationGroupsController', function ($scope, $state, apiService, $uibModal, $filter,
toastr, $analytics, $uibModalStack) {
$scope.groups = [];
$scope.loading = true;
$scope.$on('$viewContentLoaded', function () {
loadList();
});
$scope.$on('organizationGroupsAdd', function (event, args) {
$scope.add();
});
$scope.add = function () {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationGroupsAdd.html',
controller: 'organizationGroupsAddController'
});
modal.result.then(function (group) {
$scope.groups.push(group);
});
};
$scope.edit = function (group) {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationGroupsEdit.html',
controller: 'organizationGroupsEditController',
resolve: {
id: function () { return group.id; }
}
});
modal.result.then(function (editedGroup) {
var existingGroups = $filter('filter')($scope.groups, { id: editedGroup.id }, true);
if (existingGroups && existingGroups.length > 0) {
existingGroups[0].name = editedGroup.name;
}
});
};
$scope.users = function (group) {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationGroupsUsers.html',
controller: 'organizationGroupsUsersController',
size: 'lg',
resolve: {
group: function () { return group; }
}
});
modal.result.then(function () {
// nothing to do
});
};
$scope.delete = function (group) {
if (!confirm('Are you sure you want to delete this group (' + group.name + ')?')) {
return;
}
apiService.groups.del({ orgId: $state.params.orgId, id: group.id }, function () {
var index = $scope.groups.indexOf(group);
if (index > -1) {
$scope.groups.splice(index, 1);
}
$analytics.eventTrack('Deleted Group');
toastr.success(group.name + ' has been deleted.', 'Group Deleted');
}, function () {
toastr.error(group.name + ' was not able to be deleted.', 'Error');
});
};
function loadList() {
apiService.groups.listOrganization({ orgId: $state.params.orgId }, function (list) {
var groups = [];
for (var i = 0; i < list.Data.length; i++) {
groups.push({
id: list.Data[i].Id,
name: list.Data[i].Name
});
}
$scope.groups = groups;
$scope.loading = false;
if ($state.params.search) {
$uibModalStack.dismissAll();
$scope.filterSearch = $state.params.search;
$('#filterSearch').focus();
}
});
}
});

View File

@@ -0,0 +1,110 @@
angular
.module('bit.organization')
.controller('organizationGroupsEditController', function ($scope, $state, $uibModalInstance, apiService, cipherService,
$analytics, id) {
$analytics.eventTrack('organizationGroupsEditController', { category: 'Modal' });
$scope.collections = [];
$scope.selectedCollections = {};
$scope.loading = true;
$uibModalInstance.opened.then(function () {
return apiService.groups.getDetails({ orgId: $state.params.orgId, id: id }).$promise;
}).then(function (group) {
$scope.group = {
id: id,
name: group.Name,
externalId: group.ExternalId,
accessAll: group.AccessAll
};
var collections = {};
if (group.Collections) {
for (var i = 0; i < group.Collections.length; i++) {
collections[group.Collections[i].Id] = {
id: group.Collections[i].Id,
readOnly: group.Collections[i].ReadOnly
};
}
}
$scope.selectedCollections = collections;
return apiService.collections.listOrganization({ orgId: $state.params.orgId }).$promise;
}).then(function (collections) {
$scope.collections = cipherService.decryptCollections(collections.Data, $state.params.orgId, true);
$scope.loading = false;
});
$scope.toggleCollectionSelectionAll = function ($event) {
var collections = {};
if ($event.target.checked) {
for (var i = 0; i < $scope.collections.length; i++) {
collections[$scope.collections[i].id] = {
id: $scope.collections[i].id,
readOnly: ($scope.collections[i].id in $scope.selectedCollections) ?
$scope.selectedCollections[$scope.collections[i].id].readOnly : false
};
}
}
$scope.selectedCollections = collections;
};
$scope.toggleCollectionSelection = function (id) {
if (id in $scope.selectedCollections) {
delete $scope.selectedCollections[id];
}
else {
$scope.selectedCollections[id] = {
id: id,
readOnly: false
};
}
};
$scope.toggleCollectionReadOnlySelection = function (id) {
if (id in $scope.selectedCollections) {
$scope.selectedCollections[id].readOnly = !!!$scope.selectedCollections[id].readOnly;
}
};
$scope.collectionSelected = function (collection) {
return collection.id in $scope.selectedCollections;
};
$scope.allSelected = function () {
return Object.keys($scope.selectedCollections).length === $scope.collections.length;
};
$scope.submit = function () {
var group = {
name: $scope.group.name,
accessAll: !!$scope.group.accessAll,
externalId: $scope.group.externalId
};
if (!group.accessAll) {
group.collections = [];
for (var collectionId in $scope.selectedCollections) {
if ($scope.selectedCollections.hasOwnProperty(collectionId)) {
group.collections.push($scope.selectedCollections[collectionId]);
}
}
}
$scope.submitPromise = apiService.groups.put({
orgId: $state.params.orgId,
id: id
}, group, function (response) {
$analytics.eventTrack('Edited Group');
$uibModalInstance.close({
id: response.Id,
name: response.Name
});
}).$promise;
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@@ -0,0 +1,57 @@
angular
.module('bit.organization')
.controller('organizationGroupsUsersController', function ($scope, $state, $uibModalInstance, apiService,
$analytics, group, toastr) {
$analytics.eventTrack('organizationGroupUsersController', { category: 'Modal' });
$scope.loading = true;
$scope.group = group;
$scope.users = [];
$uibModalInstance.opened.then(function () {
return apiService.groups.listUsers({
orgId: $state.params.orgId,
id: group.id
}).$promise;
}).then(function (userList) {
var users = [];
if (userList && userList.Data.length) {
for (var i = 0; i < userList.Data.length; i++) {
users.push({
organizationUserId: userList.Data[i].OrganizationUserId,
name: userList.Data[i].Name,
email: userList.Data[i].Email,
type: userList.Data[i].Type,
status: userList.Data[i].Status,
accessAll: userList.Data[i].AccessAll
});
}
}
$scope.users = users;
$scope.loading = false;
});
$scope.remove = function (user) {
if (!confirm('Are you sure you want to remove this user (' + user.email + ') from this ' +
'group (' + group.name + ')?')) {
return;
}
apiService.groups.delUser({ orgId: $state.params.orgId, id: group.id, orgUserId: user.organizationUserId }, null,
function () {
toastr.success(user.email + ' has been removed.', 'User Removed');
$analytics.eventTrack('Removed User From Group');
var index = $scope.users.indexOf(user);
if (index > -1) {
$scope.users.splice(index, 1);
}
}, function () {
toastr.error('Unable to remove user.', 'Error');
});
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@@ -1,11 +1,22 @@
angular
.module('bit.organization')
.controller('organizationPeopleController', function ($scope, $state, $uibModal, cryptoService, apiService,
toastr, $analytics) {
.controller('organizationPeopleController', function ($scope, $state, $uibModal, cryptoService, apiService, authService,
toastr, $analytics, $filter, $uibModalStack) {
$scope.users = [];
$scope.useGroups = false;
$scope.useEvents = false;
$scope.$on('$viewContentLoaded', function () {
loadList();
authService.getUserProfile().then(function (profile) {
if (profile.organizations) {
var org = profile.organizations[$state.params.orgId];
$scope.useGroups = !!org.useGroups;
$scope.useEvents = !!org.useEvents;
}
});
});
$scope.reinvite = function (user) {
@@ -71,13 +82,13 @@
});
};
$scope.edit = function (id) {
$scope.edit = function (orgUser) {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationPeopleEdit.html',
controller: 'organizationPeopleEditController',
resolve: {
id: function () { return id; }
orgUser: function () { return orgUser; }
}
});
@@ -86,6 +97,33 @@
});
};
$scope.groups = function (user) {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationPeopleGroups.html',
controller: 'organizationPeopleGroupsController',
resolve: {
orgUser: function () { return user; }
}
});
modal.result.then(function () {
});
};
$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 = [];
@@ -105,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

@@ -1,8 +1,8 @@
angular
.module('bit.organization')
.controller('organizationPeopleEditController', function ($scope, $state, $uibModalInstance, apiService, cipherService, id,
$analytics) {
.controller('organizationPeopleEditController', function ($scope, $state, $uibModalInstance, apiService, cipherService,
orgUser, $analytics) {
$analytics.eventTrack('organizationPeopleEditController', { category: 'Modal' });
$scope.loading = true;
@@ -15,17 +15,17 @@
$scope.loading = false;
});
apiService.organizationUsers.get({ orgId: $state.params.orgId, id: id }, function (user) {
apiService.organizationUsers.get({ orgId: $state.params.orgId, id: orgUser.id }, function (user) {
var collections = {};
if (user && user.Collections) {
for (var i = 0; i < user.Collections.Data.length; i++) {
collections[user.Collections.Data[i].Id] = {
collectionId: user.Collections.Data[i].Id,
readOnly: user.Collections.Data[i].ReadOnly
for (var i = 0; i < user.Collections.length; i++) {
collections[user.Collections[i].Id] = {
id: user.Collections[i].Id,
readOnly: user.Collections[i].ReadOnly
};
}
}
$scope.email = user.Email;
$scope.email = orgUser.email;
$scope.type = user.Type;
$scope.accessAll = user.AccessAll;
$scope.selectedCollections = collections;
@@ -37,7 +37,7 @@
if ($event.target.checked) {
for (var i = 0; i < $scope.collections.length; i++) {
collections[$scope.collections[i].id] = {
collectionId: $scope.collections[i].id,
id: $scope.collections[i].id,
readOnly: ($scope.collections[i].id in $scope.selectedCollections) ?
$scope.selectedCollections[$scope.collections[i].id].readOnly : false
};
@@ -53,7 +53,7 @@
}
else {
$scope.selectedCollections[id] = {
collectionId: id,
id: id,
readOnly: false
};
}
@@ -84,14 +84,18 @@
}
}
$scope.submitPromise = apiService.organizationUsers.put({ orgId: $state.params.orgId, id: id }, {
type: $scope.type,
collections: collections,
accessAll: $scope.accessAll
}, function () {
$analytics.eventTrack('Edited User');
$uibModalInstance.close();
}).$promise;
$scope.submitPromise = apiService.organizationUsers.put(
{
orgId: $state.params.orgId,
id: orgUser.id
}, {
type: $scope.type,
collections: collections,
accessAll: $scope.accessAll
}, function () {
$analytics.eventTrack('Edited User');
$uibModalInstance.close();
}).$promise;
};
$scope.close = function () {

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

@@ -0,0 +1,85 @@
angular
.module('bit.organization')
.controller('organizationPeopleGroupsController', function ($scope, $state, $uibModalInstance, apiService,
orgUser, $analytics) {
$analytics.eventTrack('organizationPeopleGroupsController', { category: 'Modal' });
$scope.loading = true;
$scope.groups = [];
$scope.selectedGroups = {};
$scope.orgUser = orgUser;
$uibModalInstance.opened.then(function () {
return apiService.groups.listOrganization({ orgId: $state.params.orgId }).$promise;
}).then(function (groupsList) {
var groups = [];
for (var i = 0; i < groupsList.Data.length; i++) {
groups.push({
id: groupsList.Data[i].Id,
name: groupsList.Data[i].Name
});
}
$scope.groups = groups;
return apiService.organizationUsers.listGroups({ orgId: $state.params.orgId, id: orgUser.id }).$promise;
}).then(function (groupIds) {
var selectedGroups = {};
if (groupIds) {
for (var i = 0; i < groupIds.length; i++) {
selectedGroups[groupIds[i]] = true;
}
}
$scope.selectedGroups = selectedGroups;
$scope.loading = false;
});
$scope.toggleGroupSelectionAll = function ($event) {
var groups = {};
if ($event.target.checked) {
for (var i = 0; i < $scope.groups.length; i++) {
groups[$scope.groups[i].id] = true;
}
}
$scope.selectedGroups = groups;
};
$scope.toggleGroupSelection = function (id) {
if (id in $scope.selectedGroups) {
delete $scope.selectedGroups[id];
}
else {
$scope.selectedGroups[id] = true;
}
};
$scope.groupSelected = function (group) {
return group.id in $scope.selectedGroups;
};
$scope.allSelected = function () {
return Object.keys($scope.selectedGroups).length === $scope.groups.length;
};
$scope.submitPromise = null;
$scope.submit = function (model) {
var groups = [];
for (var groupId in $scope.selectedGroups) {
if ($scope.selectedGroups.hasOwnProperty(groupId)) {
groups.push(groupId);
}
}
$scope.submitPromise = apiService.organizationUsers.putGroups({ orgId: $state.params.orgId, id: orgUser.id }, {
groupIds: groups,
}, function () {
$analytics.eventTrack('Edited User Groups');
$uibModalInstance.close();
}).$promise;
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@@ -24,7 +24,7 @@
if ($event.target.checked) {
for (var i = 0; i < $scope.collections.length; i++) {
collections[$scope.collections[i].id] = {
collectionId: $scope.collections[i].id,
id: $scope.collections[i].id,
readOnly: ($scope.collections[i].id in $scope.selectedCollections) ?
$scope.selectedCollections[$scope.collections[i].id].readOnly : false
};
@@ -40,7 +40,7 @@
}
else {
$scope.selectedCollections[id] = {
collectionId: id,
id: id,
readOnly: false
};
}
@@ -72,8 +72,10 @@
}
}
var splitEmails = model.emails.trim().split(/\s*,\s*/);
$scope.submitPromise = apiService.organizationUsers.invite({ orgId: $state.params.orgId }, {
email: model.email,
emails: splitEmails,
type: model.type,
collections: collections,
accessAll: model.accessAll

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,50 +0,0 @@
angular
.module('bit.vault')
.controller('organizationVaultAddLoginController', function ($scope, apiService, $uibModalInstance, cryptoService,
cipherService, passwordService, $analytics, orgId) {
$analytics.eventTrack('organizationVaultAddLoginController', { category: 'Modal' });
$scope.login = {};
$scope.hideFolders = $scope.hideFavorite = true;
$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');
};
});

View File

@@ -0,0 +1,90 @@
angular
.module('bit.organization')
.controller('organizationVaultAttachmentsController', function ($scope, apiService, $uibModalInstance, cryptoService,
cipherService, cipherId, $analytics, validationService, toastr, $timeout) {
$analytics.eventTrack('organizationVaultAttachmentsController', { category: 'Modal' });
$scope.cipher = {};
$scope.loading = true;
$scope.isPremium = true;
$scope.canUseAttachments = true;
var closing = false;
apiService.ciphers.getAdmin({ id: cipherId }, function (cipher) {
$scope.cipher = cipherService.decryptCipher(cipher);
$scope.loading = false;
}, function () {
$scope.loading = false;
});
$scope.save = function (form) {
var files = document.getElementById('file').files;
if (!files || !files.length) {
validationService.addError(form, 'file', 'Select a file.', true);
return;
}
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.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 (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.cipher.organizationId);
cipherService.downloadAndDecryptAttachment(key, attachment, true).then(function (res) {
$timeout(function () {
attachment.loading = false;
});
}, function () {
$timeout(function () {
attachment.loading = false;
});
});
};
$scope.remove = function (attachment) {
if (!confirm('Are you sure you want to delete this attachment (' + attachment.fileName + ')?')) {
return;
}
attachment.loading = true;
apiService.ciphers.delAttachmentAdmin({ id: cipherId, attachmentId: attachment.id }).$promise.then(function () {
attachment.loading = false;
$analytics.eventTrack('Deleted Organization Attachment');
var index = $scope.cipher.attachments.indexOf(attachment);
if (index > -1) {
$scope.cipher.attachments.splice(index, 1);
}
}, function () {
toastr.error('Cannot delete attachment.');
attachment.loading = false;
});
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
$scope.$on('modal.closing', function (e, reason, closed) {
if (closing) {
return;
}
e.preventDefault();
closing = true;
$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) {
$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,69 +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; }
@@ -138,39 +147,134 @@
});
};
$scope.removeLogin = function (login, collection) {
if (!confirm('Are you sure you want to remove this login (' + login.name + ') from the ' +
'collection (' + collection.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]);
$scope.viewEvents = function (cipher) {
$uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationVaultCipherEvents.html',
controller: 'organizationVaultCipherEventsController',
resolve: {
cipher: function () { return cipher; }
}
}
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 + ')?')) {
$scope.attachments = function (cipher) {
authService.getUserProfile().then(function (profile) {
return !!profile.organizations[cipher.organizationId].maxStorageGb;
}).then(function (useStorage) {
if (!useStorage) {
$uibModal.open({
animation: true,
templateUrl: 'app/views/paidOrgRequired.html',
controller: 'paidOrgRequiredController',
resolve: {
orgId: function () { return cipher.organizationId; }
}
});
return;
}
var attachmentModel = $uibModal.open({
animation: true,
templateUrl: 'app/vault/views/vaultAttachments.html',
controller: 'organizationVaultAttachmentsController',
resolve: {
cipherId: function () { return cipher.id; }
}
});
attachmentModel.result.then(function (hasAttachments) {
cipher.hasAttachments = hasAttachments;
});
});
};
$scope.deleteCipher = function (cipher) {
if (!confirm('Are you sure you want to delete this item (' + cipher.name + ')?')) {
return;
}
apiService.ciphers.delAdmin({ id: login.id }, function () {
$analytics.eventTrack('Deleted Login');
var index = $scope.logins.indexOf(login);
apiService.ciphers.delAdmin({ id: cipher.id }, function () {
$analytics.eventTrack('Deleted Cipher');
var index = $scope.ciphers.indexOf(cipher);
if (index > -1) {
$scope.logins.splice(index, 1);
$scope.ciphers.splice(index, 1);
}
});
};
$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();
};
$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,69 +0,0 @@
angular
.module('bit.vault')
.controller('organizationVaultEditLoginController', function ($scope, apiService, $uibModalInstance, cryptoService,
cipherService, passwordService, loginId, $analytics) {
$analytics.eventTrack('organizationVaultEditLoginController', { category: 'Modal' });
$scope.login = {};
$scope.hideFolders = $scope.hideFavorite = true;
apiService.logins.getAdmin({ id: loginId }, function (login) {
$scope.login = cipherService.decryptLogin(login);
});
$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');
};
});

View File

@@ -1,12 +1,12 @@
<section class="content-header">
<h1>
Billing
<small>manage your payments</small>
<small>manage your billing &amp; licensing</small>
</h1>
</section>
<section class="content">
<div class="callout callout-warning" ng-if="subscription && subscription.cancelled">
<h4><i class="fa fa-warning"></i> Cancelled</h4>
<h4><i class="fa fa-warning"></i> Canceled</h4>
The subscription to this organization has been canceled.
</div>
<div class="callout callout-warning" ng-if="subscription && subscription.markedForCancel">
@@ -19,30 +19,47 @@
Reinstate Plan
</button>
</div>
<div class="box">
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Plan</h3>
</div>
<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 style="text-transform: capitalize;">{{(subscription && subscription.status) || '-'}}</dd>
<dd>
<span style="text-transform: capitalize;">{{(subscription && subscription.status) || '-'}}</span>
<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">
@@ -64,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>
@@ -76,9 +93,21 @@
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">
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">User Seats</h3>
</div>
@@ -87,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>
@@ -99,7 +128,33 @@
</button>
</div>
</div>
<div class="box">
<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>
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;">
<div class="progress-bar progress-bar-info" role="progressbar"
aria-valuenow="{{storage.percentage}}" aria-valuemin="0" aria-valuemax="1"
style="min-width: 50px; width: {{storage.percentage}}%;">
{{storage.percentage}}%
</div>
</div>
</div>
<div class="box-footer">
<button type="button" class="btn btn-default btn-flat" ng-click="adjustStorage(true)">
Add Storage
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="adjustStorage(false)">
Remove Storage
</button>
</div>
</div>
<div class="box box-default" ng-if="!selfHosted">
<div class="box-header with-border">
<h3 class="box-title">Payment Method</h3>
</div>
@@ -111,8 +166,17 @@
<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}"></i>
'fa-university': paymentSource.type === 1, 'fa-paypal fa-fw text-blue': paymentSource.type === 2}"></i>
{{paymentSource.description}}
</div>
</div>
@@ -122,7 +186,7 @@
</button>
</div>
</div>
<div class="box">
<div class="box box-default" ng-if="!selfHosted">
<div class="box-header with-border">
<h3 class="box-title">Charges</h3>
</div>
@@ -137,10 +201,15 @@
<table class="table">
<tbody>
<tr ng-repeat="charge in charges">
<td style="width: 200px">
{{charge.date | date: format: mediumDate}}
<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>
<td style="width: 200px">
{{charge.date | date: 'mediumDate'}}
</td>
<td style="min-width: 150px">
{{charge.paymentSource}}
</td>
<td style="width: 150px; text-transform: capitalize;">
@@ -155,7 +224,7 @@
</div>
</div>
<div class="box-footer">
Note: Any charges will appears on your credit card statement as <b>BITWARDEN</b>.
Note: Any charges will appear on your statement as <b>BITWARDEN</b>.
</div>
</div>
</section>

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>
@@ -22,7 +22,7 @@
</p>
</div>
<div class="callout callout-danger validation-errors" ng-show="form.$errors">
<h4>Errors have occured</h4>
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in form.$errors">{{e}}</li>
</ul>

View File

@@ -1,364 +0,0 @@
<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-credit-card"></i>
{{existingPaymentMethod ? 'Change Payment Method' : 'Add Payment Method'}}
</h4>
</div>
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise">
<div class="modal-body">
<div class="callout callout-danger validation-errors" ng-show="form.$errors">
<h4>Errors have occured</h4>
<ul>
<li ng-repeat="e in form.$errors">{{e}}</li>
</ul>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group" show-errors>
<label for="card_number">Card Number</label>
<input type="text" id="card_number" name="card_number" ng-model="card.number"
class="form-control" cc-number required api-field />
</div>
</div>
</div>
<ul class="list-inline">
<li><div class="cc visa"></div></li>
<li><div class="cc mastercard"></div></li>
<li><div class="cc amex"></div></li>
<li><div class="cc discover"></div></li>
<li><div class="cc diners"></div></li>
<li><div class="cc jcb"></div></li>
</ul>
<div class="row">
<div class="col-sm-4">
<div class="form-group" show-errors>
<label for="exp_month">Expiration Month</label>
<select id="exp_month" class="form-control" ng-model="card.exp_month" required cc-exp-month
name="exp_month" api-field>
<option value="">-- Select --</option>
<option value="01">01 - January</option>
<option value="02">02 - February</option>
<option value="03">03 - March</option>
<option value="04">04 - April</option>
<option value="05">05 - May</option>
<option value="06">06 - June</option>
<option value="07">07 - July</option>
<option value="08">08 - August</option>
<option value="09">09 - September</option>
<option value="10">10 - October</option>
<option value="11">11 - November</option>
<option value="12">12 - December</option>
</select>
</div>
</div>
<div class="col-sm-4">
<div class="form-group" show-errors>
<label for="exp_year">Expiration Year</label>
<select id="exp_year" class="form-control" ng-model="card.exp_year" required cc-exp-year
name="exp_year" api-field>
<option value="">-- Select --</option>
<option value="17">2017</option>
<option value="18">2018</option>
<option value="19">2019</option>
<option value="20">2020</option>
<option value="21">2021</option>
<option value="22">2022</option>
<option value="23">2023</option>
<option value="24">2024</option>
<option value="25">2025</option>
<option value="26">2026</option>
</select>
</div>
</div>
<div class="col-sm-4">
<div class="form-group" show-errors>
<label for="cvc">
CVC
<a href="https://www.cvvnumber.com/cvv.html" target="_blank" title="What is this?"
rel="noopener noreferrer">
<i class="fa fa-question-circle"></i>
</a>
</label>
<input type="text" id="cvc" ng-model="card.cvc" class="form-control" name="cvc"
cc-type="number.$ccType" cc-cvc required api-field />
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="address_country">Country</label>
<select id="address_country" class="form-control" ng-model="card.address_country"
required name="address_country" api-field>
<option value="">-- Select --</option>
<option value="US">United States</option>
<option value="CN">China</option>
<option value="FR">France</option>
<option value="DE">Germany</option>
<option value="CA">Canada</option>
<option value="GB">United Kingdom</option>
<option value="AU">Australia</option>
<option value="IN">India</option>
<option value="-" disabled></option>
<option value="AF">Afghanistan</option>
<option value="AX">Åland Islands</option>
<option value="AL">Albania</option>
<option value="DZ">Algeria</option>
<option value="AS">American Samoa</option>
<option value="AD">Andorra</option>
<option value="AO">Angola</option>
<option value="AI">Anguilla</option>
<option value="AQ">Antarctica</option>
<option value="AG">Antigua and Barbuda</option>
<option value="AR">Argentina</option>
<option value="AM">Armenia</option>
<option value="AW">Aruba</option>
<option value="AT">Austria</option>
<option value="AZ">Azerbaijan</option>
<option value="BS">Bahamas</option>
<option value="BH">Bahrain</option>
<option value="BD">Bangladesh</option>
<option value="BB">Barbados</option>
<option value="BY">Belarus</option>
<option value="BE">Belgium</option>
<option value="BZ">Belize</option>
<option value="BJ">Benin</option>
<option value="BM">Bermuda</option>
<option value="BT">Bhutan</option>
<option value="BO">Bolivia, Plurinational State of</option>
<option value="BQ">Bonaire, Sint Eustatius and Saba</option>
<option value="BA">Bosnia and Herzegovina</option>
<option value="BW">Botswana</option>
<option value="BV">Bouvet Island</option>
<option value="BR">Brazil</option>
<option value="IO">British Indian Ocean Territory</option>
<option value="BN">Brunei Darussalam</option>
<option value="BG">Bulgaria</option>
<option value="BF">Burkina Faso</option>
<option value="BI">Burundi</option>
<option value="KH">Cambodia</option>
<option value="CM">Cameroon</option>
<option value="CV">Cape Verde</option>
<option value="KY">Cayman Islands</option>
<option value="CF">Central African Republic</option>
<option value="TD">Chad</option>
<option value="CL">Chile</option>
<option value="CX">Christmas Island</option>
<option value="CC">Cocos (Keeling) Islands</option>
<option value="CO">Colombia</option>
<option value="KM">Comoros</option>
<option value="CG">Congo</option>
<option value="CD">Congo, the Democratic Republic of the</option>
<option value="CK">Cook Islands</option>
<option value="CR">Costa Rica</option>
<option value="CI">Côte d'Ivoire</option>
<option value="HR">Croatia</option>
<option value="CU">Cuba</option>
<option value="CW">Curaçao</option>
<option value="CY">Cyprus</option>
<option value="CZ">Czech Republic</option>
<option value="DK">Denmark</option>
<option value="DJ">Djibouti</option>
<option value="DM">Dominica</option>
<option value="DO">Dominican Republic</option>
<option value="EC">Ecuador</option>
<option value="EG">Egypt</option>
<option value="SV">El Salvador</option>
<option value="GQ">Equatorial Guinea</option>
<option value="ER">Eritrea</option>
<option value="EE">Estonia</option>
<option value="ET">Ethiopia</option>
<option value="FK">Falkland Islands (Malvinas)</option>
<option value="FO">Faroe Islands</option>
<option value="FJ">Fiji</option>
<option value="FI">Finland</option>
<option value="GF">French Guiana</option>
<option value="PF">French Polynesia</option>
<option value="TF">French Southern Territories</option>
<option value="GA">Gabon</option>
<option value="GM">Gambia</option>
<option value="GE">Georgia</option>
<option value="GH">Ghana</option>
<option value="GI">Gibraltar</option>
<option value="GR">Greece</option>
<option value="GL">Greenland</option>
<option value="GD">Grenada</option>
<option value="GP">Guadeloupe</option>
<option value="GU">Guam</option>
<option value="GT">Guatemala</option>
<option value="GG">Guernsey</option>
<option value="GN">Guinea</option>
<option value="GW">Guinea-Bissau</option>
<option value="GY">Guyana</option>
<option value="HT">Haiti</option>
<option value="HM">Heard Island and McDonald Islands</option>
<option value="VA">Holy See (Vatican City State)</option>
<option value="HN">Honduras</option>
<option value="HK">Hong Kong</option>
<option value="HU">Hungary</option>
<option value="IS">Iceland</option>
<option value="ID">Indonesia</option>
<option value="IR">Iran, Islamic Republic of</option>
<option value="IQ">Iraq</option>
<option value="IE">Ireland</option>
<option value="IM">Isle of Man</option>
<option value="IL">Israel</option>
<option value="IT">Italy</option>
<option value="JM">Jamaica</option>
<option value="JP">Japan</option>
<option value="JE">Jersey</option>
<option value="JO">Jordan</option>
<option value="KZ">Kazakhstan</option>
<option value="KE">Kenya</option>
<option value="KI">Kiribati</option>
<option value="KP">Korea, Democratic People's Republic of</option>
<option value="KR">Korea, Republic of</option>
<option value="KW">Kuwait</option>
<option value="KG">Kyrgyzstan</option>
<option value="LA">Lao People's Democratic Republic</option>
<option value="LV">Latvia</option>
<option value="LB">Lebanon</option>
<option value="LS">Lesotho</option>
<option value="LR">Liberia</option>
<option value="LY">Libya</option>
<option value="LI">Liechtenstein</option>
<option value="LT">Lithuania</option>
<option value="LU">Luxembourg</option>
<option value="MO">Macao</option>
<option value="MK">Macedonia, the former Yugoslav Republic of</option>
<option value="MG">Madagascar</option>
<option value="MW">Malawi</option>
<option value="MY">Malaysia</option>
<option value="MV">Maldives</option>
<option value="ML">Mali</option>
<option value="MT">Malta</option>
<option value="MH">Marshall Islands</option>
<option value="MQ">Martinique</option>
<option value="MR">Mauritania</option>
<option value="MU">Mauritius</option>
<option value="YT">Mayotte</option>
<option value="MX">Mexico</option>
<option value="FM">Micronesia, Federated States of</option>
<option value="MD">Moldova, Republic of</option>
<option value="MC">Monaco</option>
<option value="MN">Mongolia</option>
<option value="ME">Montenegro</option>
<option value="MS">Montserrat</option>
<option value="MA">Morocco</option>
<option value="MZ">Mozambique</option>
<option value="MM">Myanmar</option>
<option value="NA">Namibia</option>
<option value="NR">Nauru</option>
<option value="NP">Nepal</option>
<option value="NL">Netherlands</option>
<option value="NC">New Caledonia</option>
<option value="NZ">New Zealand</option>
<option value="NI">Nicaragua</option>
<option value="NE">Niger</option>
<option value="NG">Nigeria</option>
<option value="NU">Niue</option>
<option value="NF">Norfolk Island</option>
<option value="MP">Northern Mariana Islands</option>
<option value="NO">Norway</option>
<option value="OM">Oman</option>
<option value="PK">Pakistan</option>
<option value="PW">Palau</option>
<option value="PS">Palestinian Territory, Occupied</option>
<option value="PA">Panama</option>
<option value="PG">Papua New Guinea</option>
<option value="PY">Paraguay</option>
<option value="PE">Peru</option>
<option value="PH">Philippines</option>
<option value="PN">Pitcairn</option>
<option value="PL">Poland</option>
<option value="PT">Portugal</option>
<option value="PR">Puerto Rico</option>
<option value="QA">Qatar</option>
<option value="RE">Réunion</option>
<option value="RO">Romania</option>
<option value="RU">Russian Federation</option>
<option value="RW">Rwanda</option>
<option value="BL">Saint Barthélemy</option>
<option value="SH">Saint Helena, Ascension and Tristan da Cunha</option>
<option value="KN">Saint Kitts and Nevis</option>
<option value="LC">Saint Lucia</option>
<option value="MF">Saint Martin (French part)</option>
<option value="PM">Saint Pierre and Miquelon</option>
<option value="VC">Saint Vincent and the Grenadines</option>
<option value="WS">Samoa</option>
<option value="SM">San Marino</option>
<option value="ST">Sao Tome and Principe</option>
<option value="SA">Saudi Arabia</option>
<option value="SN">Senegal</option>
<option value="RS">Serbia</option>
<option value="SC">Seychelles</option>
<option value="SL">Sierra Leone</option>
<option value="SG">Singapore</option>
<option value="SX">Sint Maarten (Dutch part)</option>
<option value="SK">Slovakia</option>
<option value="SI">Slovenia</option>
<option value="SB">Solomon Islands</option>
<option value="SO">Somalia</option>
<option value="ZA">South Africa</option>
<option value="GS">South Georgia and the South Sandwich Islands</option>
<option value="SS">South Sudan</option>
<option value="ES">Spain</option>
<option value="LK">Sri Lanka</option>
<option value="SD">Sudan</option>
<option value="SR">Suriname</option>
<option value="SJ">Svalbard and Jan Mayen</option>
<option value="SZ">Swaziland</option>
<option value="SE">Sweden</option>
<option value="CH">Switzerland</option>
<option value="SY">Syrian Arab Republic</option>
<option value="TW">Taiwan, Province of China</option>
<option value="TJ">Tajikistan</option>
<option value="TZ">Tanzania, United Republic of</option>
<option value="TH">Thailand</option>
<option value="TL">Timor-Leste</option>
<option value="TG">Togo</option>
<option value="TK">Tokelau</option>
<option value="TO">Tonga</option>
<option value="TT">Trinidad and Tobago</option>
<option value="TN">Tunisia</option>
<option value="TR">Turkey</option>
<option value="TM">Turkmenistan</option>
<option value="TC">Turks and Caicos Islands</option>
<option value="TV">Tuvalu</option>
<option value="UG">Uganda</option>
<option value="UA">Ukraine</option>
<option value="AE">United Arab Emirates</option>
<option value="UM">United States Minor Outlying Islands</option>
<option value="UY">Uruguay</option>
<option value="UZ">Uzbekistan</option>
<option value="VU">Vanuatu</option>
<option value="VE">Venezuela, Bolivarian Republic of</option>
<option value="VN">Viet Nam</option>
<option value="VG">Virgin Islands, British</option>
<option value="VI">Virgin Islands, U.S.</option>
<option value="WF">Wallis and Futuna</option>
<option value="EH">Western Sahara</option>
<option value="YE">Yemen</option>
<option value="ZM">Zambia</option>
<option value="ZW">Zimbabwe</option>
</select>
</div>
</div>
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="address_zip"
ng-bind="card.address_country === 'US' ? 'Zip Code' : 'Postal Code'"></label>
<input type="text" id="address_zip" ng-model="card.address_zip"
class="form-control" required name="address_zip" api-field />
</div>
</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

@@ -2,10 +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-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">
Coming soon. In the meantime, please <a href="https://bitwarden.com/contact/" target="_blank">contact us</a>
if you would like to change your plan.
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
method on file.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>

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>
@@ -44,17 +44,12 @@
</button>
<ul class="dropdown-menu">
<li>
<a href="javascript:void(0)" ng-click="users(collection)">
<a href="#" stop-click ng-click="users(collection)">
<i class="fa fa-fw fa-users"></i> Users
</a>
</li>
<li>
<a href="javascript:void(0)" ng-click="groups(collection)">
<i class="fa fa-fw fa-sitemap"></i> Groups
</a>
</li>
<li>
<a href="javascript:void(0)" ng-click="delete(collection)" class="text-red">
<a href="#" stop-click ng-click="delete(collection)" class="text-red">
<i class="fa fa-fw fa-trash"></i> Delete
</a>
</li>
@@ -62,7 +57,7 @@
</div>
</td>
<td valign="middle">
<a href="javascript:void(0)" ng-click="edit(collection)">
<a href="#" stop-click ng-click="edit(collection)">
{{collection.name}}
</a>
</td>

View File

@@ -2,18 +2,8 @@
<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-danger validation-errors" ng-show="form.$errors">
<h4>Errors have occured</h4>
<ul>
<li ng-repeat="e in form.$errors">{{e}}</li>
</ul>
</div>
<div class="form-group" show-errors>
<label for="email">Name</label>
<input type="text" id="name" name="Name" ng-model="model.name" class="form-control" required api-field />
</div>
<div class="callout callout-default">
<h4><i class="fa fa-info-circle"></i> Note</h4>
<p>
@@ -24,6 +14,65 @@
login from "My vault".
</p>
</div>
<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" show-errors>
<label for="email">Name</label>
<input type="text" id="name" name="Name" ng-model="model.name" class="form-control" required api-field />
</div>
<div ng-if="useGroups">
<h4>Group Access</h4>
<div ng-show="loading && !groups.length">
Loading groups...
</div>
<div ng-show="!loading && !groups.length">
<p>No groups for your organization.</p>
</div>
<div class="table-responsive" ng-show="groups.length" style="margin: 0;">
<table class="table table-striped table-hover" style="margin: 0;">
<thead>
<tr>
<th style="width: 40px;">
<input type="checkbox"
ng-checked="allSelected()"
ng-click="toggleGroupSelectionAll($event)">
</th>
<th>Name</th>
<th style="width: 100px; text-align: center;">Read Only</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="group in groups | orderBy: ['name']">
<td valign="middle">
<input type="checkbox"
name="selectedGroups[]"
value="{{group.id}}"
ng-checked="groupSelected(group)"
ng-click="toggleGroupSelection(group.id)"
ng-disabled="group.accessAll">
</td>
<td valign="middle">
{{group.name}}
<i class="fa fa-unlock text-muted fa-fw" ng-show="group.accessAll"
title="This group can access all items"></i>
</td>
<td style="width: 100px; text-align: center;" valign="middle">
<input type="checkbox"
name="selectedGroupsReadonly[]"
value="{{group.id}}"
ng-disabled="!groupSelected(group) || group.accessAll"
ng-checked="groupSelected(group) && selectedGroups[group.id].readOnly"
ng-click="toggleGroupReadOnlySelection(group)">
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="form.$loading">

View File

@@ -2,18 +2,8 @@
<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-danger validation-errors" ng-show="form.$errors">
<h4>Errors have occured</h4>
<ul>
<li ng-repeat="e in form.$errors">{{e}}</li>
</ul>
</div>
<div class="form-group" show-errors>
<label for="email">Name</label>
<input type="text" id="name" name="Name" ng-model="collection.name" class="form-control" required api-field />
</div>
<div class="callout callout-default">
<h4><i class="fa fa-info-circle"></i> Note</h4>
<p>
@@ -25,6 +15,65 @@
login from "My vault".
</p>
</div>
<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" show-errors>
<label for="email">Name</label>
<input type="text" id="name" name="Name" ng-model="collection.name" class="form-control" required api-field />
</div>
<div ng-if="useGroups">
<h4>Group Access</h4>
<div ng-show="loading && !groups.length">
Loading groups...
</div>
<div ng-show="!loading && !groups.length">
<p>No groups for your organization.</p>
</div>
<div class="table-responsive" ng-show="groups.length" style="margin: 0;">
<table class="table table-striped table-hover" style="margin: 0;">
<thead>
<tr>
<th style="width: 40px;">
<input type="checkbox"
ng-checked="allSelected()"
ng-click="toggleGroupSelectionAll($event)">
</th>
<th>Name</th>
<th style="width: 100px; text-align: center;">Read Only</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="group in groups | orderBy: ['name']">
<td valign="middle">
<input type="checkbox"
name="selectedGroups[]"
value="{{group.id}}"
ng-checked="groupSelected(group)"
ng-click="toggleGroupSelection(group.id)"
ng-disabled="group.accessAll">
</td>
<td valign="middle">
{{group.name}}
<i class="fa fa-unlock text-muted fa-fw" ng-show="group.accessAll"
title="This group can access all items"></i>
</td>
<td style="width: 100px; text-align: center;" valign="middle">
<input type="checkbox"
name="selectedGroupsReadonly[]"
value="{{group.id}}"
ng-disabled="!groupSelected(group) || group.accessAll"
ng-checked="groupSelected(group) && selectedGroups[group.id].readOnly"
ng-click="toggleGroupReadOnlySelection(group)">
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="form.$loading">

View File

@@ -1,10 +0,0 @@
<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-sitemap"></i> Groups <small>{{collection.name}}</small></h4>
</div>
<div class="modal-body">
Groups are coming soon to bitwarden Enterprise organizations.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
</div>

View File

@@ -22,13 +22,13 @@
<i class="fa fa-cog"></i> <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li ng-show="user.id">
<a href="javascript:void(0)" ng-click="remove(user)" class="text-red">
<li ng-show="!user.accessAll">
<a href="#" stop-click ng-click="remove(user)" class="text-red">
<i class="fa fa-fw fa-remove"></i> Remove
</a>
</li>
<li ng-show="!user.id">
<a href="javascript:void(0)">
<li ng-show="user.accessAll">
<a href="#" stop-click>
No options...
</a>
</li>

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

@@ -14,7 +14,7 @@
Deleting this organization is permanent. It cannot be undone.
</div>
<div class="callout callout-danger validation-errors" ng-show="form.$errors">
<h4>Errors have occured</h4>
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in form.$errors">{{e}}</li>
</ul>

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

@@ -7,10 +7,64 @@
<section class="content">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Coming soon...</h3>
&nbsp;
<div class="box-filters hidden-xs">
<div class="form-group form-group-sm has-feedback has-feedback-left">
<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>
</div>
<div class="box-tools">
<button type="button" class="btn btn-primary btn-sm btn-flat" ng-click="add()">
<i class="fa fa-fw fa-plus-circle"></i> New Group
</button>
</div>
</div>
<div class="box-body">
<p>Groups are coming soon to bitwarden Enterprise organizations.</p>
<div class="box-body" ng-class="{'no-padding': filteredGroups.length}">
<div ng-show="loading && !groups.length">
Loading...
</div>
<div ng-show="!filteredGroups.length && filterSearch">
No groups to list.
</div>
<div ng-show="!loading && !groups.length">
<p>There are no groups yet for your organization.</p>
<button type="button" ng-click="add()" class="btn btn-default btn-flat">Add a Group</button>
</div>
<div class="table-responsive" ng-show="groups.length">
<table class="table table-striped table-hover table-vmiddle">
<tbody>
<tr ng-repeat="group in filteredGroups = (groups | filter: (filterSearch || '') |
orderBy: ['name']) track by group.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">
<i class="fa fa-cog"></i> <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li>
<a href="#" stop-click ng-click="users(group)">
<i class="fa fa-fw fa-users"></i> Users
</a>
</li>
<li>
<a href="#" stop-click ng-click="delete(group)" class="text-red">
<i class="fa fa-fw fa-trash"></i> Delete
</a>
</li>
</ul>
</div>
</td>
<td valign="middle">
<a href="#" stop-click ng-click="edit(group)">
{{group.name}}
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>

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