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

Compare commits

...

288 Commits

Author SHA1 Message Date
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
Kyle Spearrin
4518e7056c fixed to collection sharing. observe login edit. 2017-05-08 11:36:11 -04:00
Kyle Spearrin
565c6bafae version bump 2017-05-08 08:15:39 -04:00
Kyle Spearrin
584e8131cd version bump 2017-05-06 21:32:56 -04:00
Kyle Spearrin
20e958b1ee new identity server uri for auth 2017-05-06 21:32:51 -04:00
Kyle Spearrin
21ca3abc7e importer fixes for ipif and safe in cloud 2017-05-04 15:56:45 -04:00
Kyle Spearrin
612ad32722 update forge 2017-05-04 00:13:01 -04:00
Kyle Spearrin
8ec07266b9 trimleft on first lastpass chunk 2017-05-03 14:48:29 -04:00
Kyle Spearrin
a9a7b0b317 typo on export 2017-05-03 11:47:09 -04:00
Kyle Spearrin
e634e3e28f change stripe key to live 2017-05-03 10:34:23 -04:00
Kyle Spearrin
86de4b721f callout when registering for org create 2017-05-03 10:23:01 -04:00
Kyle Spearrin
1d95a78e75 payment page UI updates 2017-04-28 21:50:08 -04:00
Kyle Spearrin
f5e44163be style sweaks 2017-04-28 21:39:16 -04:00
Kyle Spearrin
1ffc005479 adjusted warning color to be darker 2017-04-28 21:36:03 -04:00
Kyle Spearrin
31f67d412b Two-step login UI tweaks 2017-04-28 21:31:57 -04:00
Kyle Spearrin
cc62237ab5 UI/UX tweaks 2017-04-28 15:28:00 -04:00
Kyle Spearrin
f11d4a92df notes 2017-04-27 16:40:45 -04:00
Kyle Spearrin
0be6249c2b shared bugs 2017-04-27 16:34:04 -04:00
Kyle Spearrin
a083fc9084 user vault collections changed to show all shared 2017-04-27 16:24:38 -04:00
Kyle Spearrin
54172c441f rename AccessAllCollections => AccessAll 2017-04-27 15:39:24 -04:00
Kyle Spearrin
b5f8b1014e add/edit logins from org admin vault 2017-04-27 14:47:44 -04:00
Kyle Spearrin
df42c6176d comment update 2017-04-27 12:14:11 -04:00
Kyle Spearrin
7d0a34fceb protect mac comparisons from timing attacks 2017-04-27 12:00:32 -04:00
Kyle Spearrin
b3e94b13f7 constant time equality for mac check on decrypt 2017-04-27 11:35:30 -04:00
Kyle Spearrin
4eee908f2f subvault => collections file renames 2017-04-27 09:35:21 -04:00
Kyle Spearrin
1ebae5c284 rename subvault => collection 2017-04-27 09:33:12 -04:00
Kyle Spearrin
361f03eb5f remove audits controller ref 2017-04-26 10:39:34 -04:00
Kyle Spearrin
d8f54fc15a telemetry for organizations 2017-04-26 10:32:14 -04:00
Kyle Spearrin
90b0f3201e telemetry events 2017-04-26 10:21:06 -04:00
Kyle Spearrin
b0d2374960 misc cleanup 2017-04-25 16:26:25 -04:00
Kyle Spearrin
5c471e43dd return state for org create on register/login 2017-04-25 10:46:54 -04:00
Kyle Spearrin
c69169cbf9 rename CryptoKey to SymmetricCryptoKey 2017-04-22 14:39:40 -04:00
Kyle Spearrin
f2c670dfd0 whitelist desktop IP 2017-04-21 22:40:21 -04:00
Kyle Spearrin
cfdd6dc0d9 Clear selected subvaults when changing orgs 2017-04-21 16:02:46 -04:00
Kyle Spearrin
d61b6c2faa force vault refresh upon importing 2017-04-21 14:24:24 -04:00
Kyle Spearrin
e010995b19 Add support for OAEP SHA1 digest.
Note that iOS does not support any other OAEP format, such as SHA256.
2017-04-21 13:46:07 -04:00
Kyle Spearrin
053a1c1394 arrange icons better 2017-04-20 23:58:38 -04:00
Kyle Spearrin
581184e2ae wording update 2017-04-20 23:55:07 -04:00
Kyle Spearrin
84e617b201 list details about user w/ access to all subvaults 2017-04-20 23:49:33 -04:00
Kyle Spearrin
4ba21638b1 access all subvaults option for org users 2017-04-20 22:19:18 -04:00
Kyle Spearrin
f92c5a214f crypto fix for mac 2017-04-20 16:32:03 -04:00
Kyle Spearrin
180101400f groups pages 2017-04-20 16:31:52 -04:00
Kyle Spearrin
ede10677f9 includeShared for backwards compat APIs 2017-04-19 17:03:47 -04:00
Kyle Spearrin
7627601ff8 handle legacy encrypt-then-mac scheme 2017-04-19 16:45:16 -04:00
Kyle Spearrin
cb120d2e75 opt out of backwards compat folder ciphers 2017-04-19 16:44:21 -04:00
Kyle Spearrin
ec86ccd956 org block styling 2017-04-19 13:56:11 -04:00
Kyle Spearrin
63a657cac5 encrypt key bytes when confirming, not object 2017-04-19 11:21:58 -04:00
Kyle Spearrin
c3eb6bb972 check that chunks has length 2017-04-19 10:10:27 -04:00
Kyle Spearrin
eab5c0db12 org admin delete cipher 2017-04-19 10:06:59 -04:00
Kyle Spearrin
0b9083915a remove login from individual subvault 2017-04-19 09:57:47 -04:00
Kyle Spearrin
051703234c cleanup crypto API 2017-04-19 09:27:38 -04:00
Kyle Spearrin
6d555bcf84 fix lint errors 2017-04-19 09:03:47 -04:00
Kyle Spearrin
d99fcd8e59 fix promise on register 2017-04-18 22:58:14 -04:00
Kyle Spearrin
04eee919e8 preview domain adjustments 2017-04-18 22:56:41 -04:00
Kyle Spearrin
0926c82878 wrap key into new CryptoKey object 2017-04-18 22:28:49 -04:00
Kyle Spearrin
79744d89ce constants for orguser type/status 2017-04-18 20:40:17 -04:00
Kyle Spearrin
214274f495 track by on repeats 2017-04-18 15:34:16 -04:00
Kyle Spearrin
2425eb0ff8 whitelist preview api url 2017-04-18 14:10:03 -04:00
Kyle Spearrin
c8931cde6e gulp fix for env 2017-04-18 14:01:28 -04:00
Kyle Spearrin
af698c7628 adjust configs 2017-04-18 13:54:46 -04:00
Kyle Spearrin
52745993cb preview deploy fix 2017-04-18 12:51:11 -04:00
Kyle Spearrin
7a8d23ba84 rework configs to accomedate preview env 2017-04-18 12:33:21 -04:00
Kyle Spearrin
34559f0dbd re-org menu 2017-04-18 12:10:06 -04:00
Kyle Spearrin
b34a205ace proper count for org subvaults 2017-04-18 12:05:51 -04:00
Kyle Spearrin
799fbeba72 cleanup styles and pluralize vault counts 2017-04-18 12:03:06 -04:00
Kyle Spearrin
0e36abe1ad of 2017-04-18 11:53:21 -04:00
Kyle Spearrin
69ce07ef01 no org callout on sidebar 2017-04-18 11:52:44 -04:00
Kyle Spearrin
9f32e76a99 clear vault rootScope when visiting org admin 2017-04-18 11:31:43 -04:00
Kyle Spearrin
e89e48014c manage root scope from subvault list edits 2017-04-18 11:27:44 -04:00
Kyle Spearrin
9863a95a71 root scope bug fixes 2017-04-18 10:45:35 -04:00
Kyle Spearrin
dc0bf54401 org existance check 2017-04-18 10:24:47 -04:00
Kyle Spearrin
3728f012d7 make dropdown append more generic 2017-04-18 10:19:42 -04:00
Kyle Spearrin
f904558315 manage cipher subvaults from org admin 2017-04-17 23:11:24 -04:00
Kyle Spearrin
a79556dfce org vault listing 2017-04-17 17:01:12 -04:00
Kyle Spearrin
901332dbee change from deprecated sites endpoint to logins 2017-04-17 15:48:02 -04:00
Kyle Spearrin
1ab75115f0 filter out org logins 2017-04-17 15:47:24 -04:00
Kyle Spearrin
bc431b896b change email/password adjustments 2017-04-17 14:53:26 -04:00
Kyle Spearrin
aa7a3c442c adjust vault login chunking 2017-04-15 01:02:56 -04:00
Kyle Spearrin
6825967cb9 domain rules style updates 2017-04-15 01:00:25 -04:00
Kyle Spearrin
cdc06a2b49 convert listings from uib-tooltip to title 2017-04-14 23:48:51 -04:00
Kyle Spearrin
309c73a972 update org after share 2017-04-14 23:36:43 -04:00
Kyle Spearrin
c4a3e5c4fd body dropdown tweaks 2017-04-14 23:30:58 -04:00
Kyle Spearrin
8d6cbe8e1e append dropdown menus to body 2017-04-14 22:49:51 -04:00
Kyle Spearrin
ff4e76b723 convert dropdowns back to regular bootstrap 2017-04-14 22:37:41 -04:00
Kyle Spearrin
acdbc6b9a3 undo comments 2017-04-14 14:37:36 -04:00
Kyle Spearrin
6714390890 clear root scope vault data on logout 2017-04-14 12:38:44 -04:00
Kyle Spearrin
249d00b285 cache vault data in root scope 2017-04-14 12:35:46 -04:00
Kyle Spearrin
e4ffdf6815 promisify makekeypair and generate keys on login 2017-04-13 18:18:32 -04:00
Kyle Spearrin
2228263b9f remove orderby on fav list 2017-04-13 17:25:02 -04:00
Kyle Spearrin
ee1c884ef1 load vault in chunks so that it appears faster 2017-04-13 17:19:54 -04:00
Kyle Spearrin
0d29c75e7f handle null condition when decrypting 2017-04-13 11:53:07 -04:00
Kyle Spearrin
7042f4bca8 labels in nav 2017-04-13 10:39:11 -04:00
Kyle Spearrin
ea42ed5381 move apps menu item up one 2017-04-13 10:12:48 -04:00
Kyle Spearrin
ba6ca4a6bb lowercase the 2017-04-13 10:10:46 -04:00
Kyle Spearrin
ce68c1599f apps page 2017-04-13 10:09:19 -04:00
Kyle Spearrin
ce64601e38 ui tweaks 2017-04-12 21:58:36 -04:00
Kyle Spearrin
b9f6351720 import bitwarden fav fix 2017-04-12 16:47:53 -04:00
Kyle Spearrin
da8b31533a export data fixes due to api cahnges 2017-04-12 16:41:31 -04:00
Kyle Spearrin
0591f106d3 syntax fixes 2017-04-12 16:14:29 -04:00
Kyle Spearrin
40f9961541 export and import favorites for bitwarden csv 2017-04-12 16:12:28 -04:00
Kyle Spearrin
5c8117539c add back exposify package for gulp build 2017-04-12 15:55:26 -04:00
Kyle Spearrin
af7400642b password gen message 2017-04-12 13:28:11 -04:00
Kyle Spearrin
f8c5f31f97 org owner check on side nav menu 2017-04-12 13:06:18 -04:00
Kyle Spearrin
5f2c2a8064 copy updates 2017-04-12 13:01:38 -04:00
Kyle Spearrin
08aa53748e manage subvaults for login in vault 2017-04-12 12:41:43 -04:00
Kyle Spearrin
673485b5c4 fix card scope 2017-04-12 11:16:14 -04:00
Kyle Spearrin
18bea7edb2 updates to change payment form 2017-04-12 11:13:41 -04:00
Kyle Spearrin
cdf029bc84 fix null check on subvault management 2017-04-12 11:11:01 -04:00
Kyle Spearrin
31ce92fa9d info text on invite 2017-04-12 11:01:03 -04:00
Kyle Spearrin
f6b1666cd7 leave organization 2017-04-12 10:07:16 -04:00
Kyle Spearrin
5f130bdda7 notes about sharing 2017-04-11 17:29:45 -04:00
Kyle Spearrin
d619167c02 disabled org labeling 2017-04-11 15:56:57 -04:00
Kyle Spearrin
400932c6de refresh access token after creating org 2017-04-11 15:00:53 -04:00
Kyle Spearrin
8984ec3127 change plan modal and adjust seat callouts 2017-04-11 14:26:17 -04:00
Kyle Spearrin
02076fadf4 some styling on org create form 2017-04-11 13:05:17 -04:00
Kyle Spearrin
1d93d5c687 show errors on payment form page 2017-04-11 12:27:03 -04:00
Kyle Spearrin
5f028ea65f delete organization 2017-04-11 10:52:16 -04:00
Kyle Spearrin
cf22ea2b78 move some values to constants for better sharing 2017-04-10 18:55:18 -04:00
Kyle Spearrin
58df3e692b rename to reinstate 2017-04-10 18:31:01 -04:00
Kyle Spearrin
80ca89b3f6 cancel/uncancel sub 2017-04-10 16:43:24 -04:00
Kyle Spearrin
4209d91c43 obj change fix 2017-04-10 12:45:46 -04:00
Kyle Spearrin
79b878209d revert settings commit 2017-04-10 12:30:16 -04:00
Kyle Spearrin
24cbe13ca7 billing seat adjustments 2017-04-10 12:29:06 -04:00
Kyle Spearrin
f8fcbbea85 change payment 2017-04-10 11:30:23 -04:00
Kyle Spearrin
40d38ec0db users => seats 2017-04-10 10:43:18 -04:00
Kyle Spearrin
f63f4e0aa3 change payment method for org 2017-04-08 16:42:05 -04:00
Kyle Spearrin
d4b4c7bd71 max additional users for personal plan 2017-04-08 11:05:32 -04:00
Kyle Spearrin
bdef522da7 org create styling 2017-04-07 16:13:52 -04:00
Kyle Spearrin
bb1ba1dbc4 move finalizeCreate to scope of shareKey 2017-04-07 15:09:09 -04:00
Kyle Spearrin
2b880d322a use ngif so that form elements are not on page 2017-04-07 14:15:11 -04:00
Kyle Spearrin
60f62b2b50 set teams plan when business is checked 2017-04-07 13:54:03 -04:00
Kyle Spearrin
b11d7be990 fix subvault collapse and add org plan details 2017-04-07 13:50:34 -04:00
Kyle Spearrin
05d153e1d2 org styling 2017-04-07 12:50:56 -04:00
Kyle Spearrin
eaba45369b org create desc and page scroll on state changes 2017-04-07 12:39:52 -04:00
Kyle Spearrin
71adf31f7b org create form on it's own page instead of modal 2017-04-07 12:32:15 -04:00
Kyle Spearrin
d39d49fb8f create org form styling 2017-04-07 11:39:56 -04:00
Kyle Spearrin
7c91066618 turn off enc header until all clients are updated 2017-04-07 09:26:43 -04:00
Kyle Spearrin
57116c4f54 added encType header to ciphers 2017-04-06 23:00:33 -04:00
Kyle Spearrin
80e4d2329a org settings and billing 2017-04-06 16:52:25 -04:00
Kyle Spearrin
7591843220 stub out org billing 2017-04-06 13:13:54 -04:00
Kyle Spearrin
653afe9f8b stub out org settings 2017-04-06 13:10:43 -04:00
Kyle Spearrin
8f007a70db dropdown options and iconography for subvaults 2017-04-06 11:00:53 -04:00
Kyle Spearrin
0feea6091b subvault messages when sharing 2017-04-06 10:24:15 -04:00
Kyle Spearrin
b27b4bef44 border options for avatars 2017-04-06 00:00:04 -04:00
Kyle Spearrin
2798a05e8e avatar tweaks. sidebar org avatars 2017-04-05 23:53:17 -04:00
Kyle Spearrin
fe039f7b35 custom letter avatar directive 2017-04-05 23:20:51 -04:00
Kyle Spearrin
ea5dc4b7fc remove gravatar for letter avatars #4 2017-04-05 17:59:48 -04:00
Kyle Spearrin
acc214d7c1 refactor to remove deprecated apis 2017-04-05 16:14:52 -04:00
Kyle Spearrin
83c232ecb5 edit logins from subvaults page 2017-04-05 11:37:22 -04:00
Kyle Spearrin
157875f7d5 use checkboxes for subvault selection 2017-04-04 22:08:04 -04:00
Kyle Spearrin
ef00e57f72 load cipher subvaults 2017-04-04 17:21:47 -04:00
Kyle Spearrin
8098ab50e8 organization signup plan details 2017-04-04 12:57:31 -04:00
Kyle Spearrin
ebb1044c43 cc details on org create 2017-04-04 10:14:54 -04:00
Kyle Spearrin
751935e90b persist folder/subvault collapse 2017-04-03 14:07:39 -04:00
Kyle Spearrin
a81572914a Manage subvault users 2017-04-03 12:26:43 -04:00
Kyle Spearrin
e00f033ffd resolve lint errors 2017-04-03 09:30:21 -04:00
Kyle Spearrin
bf9414199c subvault list UI updates 2017-04-01 22:17:28 -04:00
Kyle Spearrin
3011e9a804 use uib-dropdowns 2017-04-01 10:26:33 -04:00
Kyle Spearrin
a678f03284 button groups for vault 2017-03-30 23:49:35 -04:00
Kyle Spearrin
11002c2881 enum filters and org accept state 2017-03-30 22:06:01 -04:00
Kyle Spearrin
2692bbaa63 subvault operations 2017-03-30 21:08:07 -04:00
Kyle Spearrin
1db6d7f32b import via textarea 2017-03-30 00:07:26 -04:00
Kyle Spearrin
61cce7e8e7 subvault listing search and edit subvault 2017-03-29 22:23:00 -04:00
Kyle Spearrin
616a442fcb handle errors in org people edit 2017-03-29 21:26:48 -04:00
Kyle Spearrin
916519a43a org name from mail invite link 2017-03-29 20:58:27 -04:00
Kyle Spearrin
af2f7a7a5a organization listing from side menu 2017-03-29 19:21:06 -04:00
Kyle Spearrin
9ab9fcd577 adjust table label 2017-03-29 18:59:14 -04:00
Kyle Spearrin
853d1f4cfa status label 2017-03-29 18:05:56 -04:00
Kyle Spearrin
cbcfdafef6 UI updates for org pages 2017-03-28 22:09:27 -04:00
Kyle Spearrin
b156a27d1f api form 2017-03-28 22:04:09 -04:00
Kyle Spearrin
f6ce6426f1 add search to people listing 2017-03-28 21:44:12 -04:00
Kyle Spearrin
e12582c2c2 UI tweaks for org invites 2017-03-28 21:16:44 -04:00
Kyle Spearrin
4d2cae0b0f share profile promise result when called at same
time
2017-03-27 22:22:56 -04:00
Kyle Spearrin
35e0f27f52 access control on orgs pages 2017-03-27 21:55:39 -04:00
Kyle Spearrin
77ddc83a04 check status and types for org management 2017-03-25 21:52:27 -04:00
Kyle Spearrin
3c83741b13 ui updates for vault logins list 2017-03-25 16:09:06 -04:00
Kyle Spearrin
636c709671 hide favorites box when loading 2017-03-25 15:58:39 -04:00
Kyle Spearrin
f3f1b413b7 hide favorites box when no search results 2017-03-25 15:56:43 -04:00
Kyle Spearrin
8eaad64dd6 added favorites box to top of my vault listing 2017-03-25 15:50:24 -04:00
Kyle Spearrin
f80ba6b87c share promises and readonly check 2017-03-25 11:41:06 -04:00
Kyle Spearrin
5e5e3b5359 set profile after auth logIn 2017-03-25 11:03:11 -04:00
Kyle Spearrin
19203e976b convert auth service profile methods to promises 2017-03-25 10:43:19 -04:00
Kyle Spearrin
2154607d11 revert settings 2017-03-24 16:10:22 -04:00
Kyle Spearrin
072de1ea44 readonly and partial login updates 2017-03-24 16:09:57 -04:00
Kyle Spearrin
1818dad0d1 remove sharing module. move subvaults 2017-03-23 23:01:22 -04:00
Kyle Spearrin
d51eab779c subvault listing 2017-03-23 18:10:00 -04:00
Kyle Spearrin
9f1ab6f961 accept org invite. return state for login 2017-03-23 16:58:06 -04:00
Ben Brooks
0b875fc6f7 Add link to Firefox addon (#49)
* Add link to Firefox addon

* De-localise URLs

* re-instate media type param for iOS hint
2017-03-23 14:05:00 -04:00
Kyle Spearrin
fd62938db0 fix wrong org user type id 2017-03-23 00:40:23 -04:00
Kyle Spearrin
4499ec6a22 reinvite and remove org users 2017-03-23 00:33:35 -04:00
Kyle Spearrin
dde20f4451 resolve lint errors 2017-03-21 23:07:53 -04:00
Kyle Spearrin
715b91ab96 update all the things 2017-03-21 23:07:53 -04:00
Kyle Spearrin
7d26361680 Update README.md 2017-03-21 18:12:02 -04:00
Kyle Spearrin
b85a45d8f9 Move and list ciphers from org subvaults 2017-03-21 00:05:20 -04:00
Kyle Spearrin
22ab5d334e load folders from it's api 2017-03-18 22:55:54 -04:00
Kyle Spearrin
acf124c81e re-stub frontend sharing center 2017-03-16 22:44:54 -04:00
Kyle Spearrin
51d81dea9f manage user type 2017-03-13 23:31:01 -04:00
Kyle Spearrin
4a6066bb88 user vault associations 2017-03-13 22:54:57 -04:00
Kyle Spearrin
6ece16ccc9 org people subvault selection 2017-03-11 23:02:43 -05:00
Kyle Spearrin
0acab61f2e add new org to profile 2017-03-11 20:46:33 -05:00
Kyle Spearrin
1cbd322105 back to port 4001 2017-03-11 19:51:28 -05:00
Kyle Spearrin
ed9d26fd1b serialize private key to pkcs8 format 2017-03-10 20:49:50 -05:00
Kyle Spearrin
14e290c489 org key fixes 2017-03-09 22:28:14 -05:00
Kyle Spearrin
429b2b8a21 add subvault 2017-03-09 22:08:47 -05:00
Kyle Spearrin
e7707c4826 Set private key from asn1 on initial set 2017-03-09 20:59:10 -05:00
Kyle Spearrin
290cbe6b55 list subvaults for org 2017-03-07 23:05:49 -05:00
Kyle Spearrin
d5708f24e6 settings caret 2017-03-07 00:41:49 -05:00
Kyle Spearrin
3d273f041e do api calls on viewContentLoaded 2017-03-07 00:36:27 -05:00
Kyle Spearrin
22299c03cd list-groups for org box listing 2017-03-07 00:19:00 -05:00
Kyle Spearrin
0ea4b4400f org keys and optimized org profile load for sidenav 2017-03-06 23:54:06 -05:00
Kyle Spearrin
b3c8337f83 routes for org subvaults 2017-03-06 23:01:08 -05:00
Kyle Spearrin
a9e85f8765 org user invites and confirmation 2017-03-04 20:41:45 -05:00
Kyle Spearrin
b36799bf0c subvaults page stubbed out 2017-03-03 22:45:10 -05:00
Kyle Spearrin
4d71a05d2a organization pages and routing 2017-03-03 21:53:02 -05:00
Kyle Spearrin
4fdf2a98bf org dashboard route 2017-03-03 19:14:14 -05:00
Kyle Spearrin
880be03211 organization signup 2017-03-03 00:07:31 -05:00
Kyle Spearrin
27495d5055 Organization profile 2017-03-02 21:51:24 -05:00
Kyle Spearrin
492e2e693c setup new organization layout within backend 2017-03-01 22:47:24 -05:00
Kyle Spearrin
05a92ebd26 remove share login modal and add organizations box 2017-02-28 23:43:54 -05:00
Kyle Spearrin
0d2e296eda lint fixes 2017-02-28 22:53:19 -05:00
Kyle Spearrin
ad25267ed7 folder options 2017-02-28 00:20:03 -05:00
Kyle Spearrin
1ed86899bb share login modal 2017-02-28 00:18:11 -05:00
Kyle Spearrin
63c136a1ff share modal 2017-02-25 23:37:42 -05:00
Kyle Spearrin
3905b2b945 beta badge 2017-02-25 22:41:42 -05:00
Kyle Spearrin
afaaf7d73a modal UI for sharing folders/logins from vault 2017-02-25 22:38:30 -05:00
Kyle Spearrin
642b35582f vault row selectable 2017-02-25 22:22:25 -05:00
Kyle Spearrin
117188769c format vault listing 2017-02-25 22:13:16 -05:00
Kyle Spearrin
bd7aad37e6 copyright update 2017-02-25 22:09:58 -05:00
Kyle Spearrin
08b4e08820 style updates 2017-02-25 21:53:39 -05:00
Kyle Spearrin
aa4f360f59 combine import/export 2017-02-25 02:51:42 -05:00
Kyle Spearrin
2420375d56 remove unused service references 2017-02-23 19:32:56 -05:00
Kyle Spearrin
bc5c738c25 rework share pages 2017-02-23 00:45:54 -05:00
Kyle Spearrin
ccc527f329 Switch vault listing to user ciphers apis instead of calling login and folder separately 2017-02-21 22:50:48 -05:00
Kyle Spearrin
cf144aa2c1 set private key when logging in 2017-02-21 00:30:00 -05:00
Kyle Spearrin
086d924f06 generate keypair on registration 2017-02-21 00:30:00 -05:00
Kyle Spearrin
24862f31b3 tab layout for sharing center 2017-02-21 00:30:00 -05:00
Kyle Spearrin
877eb4d423 setup UI pages for sharing center 2017-02-21 00:30:00 -05:00
Kyle Spearrin
a37a5fa1b5 added rsa to gulp task for forge 2017-02-21 00:30:00 -05:00
Kyle Spearrin
2478a8f3cc updates to cryptoService for rsa keypairs 2017-02-21 00:30:00 -05:00
Kyle Spearrin
3ed69d887f utf8 encode params for key derivation 2017-02-15 19:03:56 -05:00
Kyle Spearrin
f0d440d204 Move tools from side nav into tools page boxes 2017-02-14 00:41:23 -05:00
Kyle Spearrin
3e18f812db lint errors 2017-02-11 18:22:13 -05:00
Kyle Spearrin
8cf02fd59a version bump 2017-02-11 17:12:50 -05:00
Kyle Spearrin
06bfab3afa version bump 2017-02-11 17:12:09 -05:00
Kyle Spearrin
71e4697562 two factor edits 2017-02-11 17:08:06 -05:00
Kyle Spearrin
cf1bffe2f1 change email button 2017-02-11 16:48:52 -05:00
Kyle Spearrin
55a5fd49dc Moved domain rules page out from modal into it's own page 2017-02-11 16:46:24 -05:00
Kyle Spearrin
3f6637eb8f move many account settings into main settings page instead of nav menu 2017-02-11 15:44:22 -05:00
Kyle Spearrin
7373e281ac print recovery code. changed vault and login route 2017-02-11 14:21:21 -05:00
Kyle Spearrin
52b89455d7 replace sjcl cryptoservice implementation with forge 2017-02-11 13:03:48 -05:00
Kyle Spearrin
bca7592c77 production by default settings 2017-02-01 22:56:39 -05:00
Kyle Spearrin
f6ab0bfe82 version bump 2017-02-01 22:53:38 -05:00
Kyle Spearrin
012a5c491d version bump 2017-02-01 22:53:15 -05:00
Kyle Spearrin
7666d6136d updated truekey importer to their new csv format 2017-02-01 22:52:36 -05:00
Kyle Spearrin
7bdda34f14 remove old auth endpoints from apiservice 2017-01-29 21:39:38 -05:00
131 changed files with 8838 additions and 1185 deletions

View File

@@ -1,9 +1,11 @@
[![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) [![Join the chat at https://gitter.im/bitwarden/Lobby](https://badges.gitter.im/bitwarden/Lobby.svg)](https://gitter.im/bitwarden/Lobby)
# bitwarden Web
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
**Requirements**
@@ -26,4 +28,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!

View File

@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.25420.1
# Visual Studio 15
VisualStudioVersion = 15.0.26228.9
MinimumVisualStudioVersion = 10.0.40219.1
Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "bitwarden-web", ".", "{25BEDEF4-2CAF-445A-807D-63C17FF85694}"
ProjectSection(WebsiteProperties) = preProject

View File

@@ -17,7 +17,11 @@ var gulp = require('gulp'),
settings = require('./settings.json'),
project = require('./package.json'),
jshint = require('gulp-jshint'),
_ = require('lodash');
_ = require('lodash'),
webpack = require('webpack-stream'),
browserify = require('browserify'),
derequire = require('gulp-derequire'),
source = require('vinyl-source-stream');
var paths = {};
paths.dist = './dist/';
@@ -42,7 +46,7 @@ gulp.task('lint', function () {
gulp.task('build', function (cb) {
return runSequence(
'clean',
['lib', 'less', 'settings', 'lint'],
['browserify', 'lib', 'webpack', 'less', 'settings', 'lint'],
cb);
});
@@ -107,8 +111,8 @@ gulp.task('lib', ['clean:lib'], function () {
dest: paths.libDir + 'angular'
},
{
src: paths.npmDir + 'angular-bootstrap-npm/dist/*tpls*.js',
dest: paths.libDir + 'angular-bootstrap'
src: paths.npmDir + 'angular-ui-bootstrap/dist/*tpls*.js',
dest: paths.libDir + 'angular-ui-bootstrap'
},
{
src: paths.npmDir + 'angular-bootstrap-show-errors/src/*.js',
@@ -122,10 +126,6 @@ gulp.task('lib', ['clean:lib'], function () {
src: paths.npmDir + 'angular-jwt/dist/*.js',
dest: paths.libDir + 'angular-jwt'
},
{
src: paths.npmDir + 'angular-md5/angular-md5*.js',
dest: paths.libDir + 'angular-md5'
},
{
src: paths.npmDir + 'angular-resource/*resource*.js',
dest: paths.libDir + 'angular-resource'
@@ -142,10 +142,6 @@ gulp.task('lib', ['clean:lib'], function () {
src: paths.npmDir + 'angular-messages/*messages*.js',
dest: paths.libDir + 'angular-messages'
},
{
src: [paths.npmDir + 'sjcl/core/cbc.js', paths.npmDir + 'sjcl/core/bitArray.js', paths.npmDir + 'sjcl/sjcl.js'],
dest: paths.libDir + 'sjcl'
},
{
src: paths.npmDir + 'ngstorage/*.js',
dest: paths.libDir + 'ngstorage'
@@ -162,6 +158,10 @@ 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',
@@ -178,6 +178,34 @@ gulp.task('lib', ['clean:lib'], function () {
return merge(tasks);
});
gulp.task('webpack', ['webpack:forge']);
gulp.task('webpack:forge', function () {
var forgeDir = paths.npmDir + '/node-forge/lib/';
return gulp.src([
forgeDir + 'pbkdf2.js',
forgeDir + 'aes.js',
forgeDir + 'rsa.js',
forgeDir + 'hmac.js',
forgeDir + 'sha256.js',
forgeDir + 'random.js',
forgeDir + 'forge.js'
]).pipe(webpack({
output: {
filename: 'forge.js',
library: 'forge',
libraryTarget: 'umd'
},
node: {
Buffer: false,
process: false,
crypto: false,
setImmediate: false
}
})).pipe(gulp.dest(paths.libDir + 'forge'));
});
gulp.task('settings', function () {
return config()
.pipe(gulp.dest(paths.webroot + 'app'));
@@ -190,9 +218,9 @@ function config() {
constants: _.merge({}, {
appSettings: {
version: project.version,
environment: project.production ? 'Production' : 'Development'
environment: project.env
}
}, require('./settings' + (project.production ? '.Production' : '') + '.json') || {})
}, require('./settings' + (project.env !== 'Development' ? ('.' + project.env) : '') + '.json') || {})
}));
}
@@ -207,6 +235,35 @@ gulp.task('watch', function () {
gulp.watch('./settings*.json', ['settings']);
});
gulp.task('browserify', ['browserify:stripe', 'browserify:cc']);
gulp.task('browserify:stripe', function () {
return browserify(paths.npmDir + 'angular-stripe/src/index.js',
{
entry: '.',
standalone: 'angularStripe',
global: true
})
.transform('exposify', { expose: { angular: 'angular' } })
.bundle()
.pipe(source('angular-stripe.js'))
.pipe(derequire())
.pipe(gulp.dest(paths.libDir + 'angular-stripe'));
});
gulp.task('browserify:cc', function () {
return browserify(paths.npmDir + 'angular-credit-cards/src/index.js',
{
entry: '.',
standalone: 'angularCreditCards'
})
.transform('exposify', { expose: { angular: 'angular' } })
.bundle()
.pipe(source('angular-credit-cards.js'))
.pipe(derequire())
.pipe(gulp.dest(paths.libDir + 'angular-credit-cards'));
});
gulp.task('dist:clean', function (cb) {
return rimraf(paths.dist, cb);
});
@@ -240,6 +297,10 @@ 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.webroot + '**/app/**/*.html',
@@ -290,8 +351,6 @@ gulp.task('dist:js:app', function () {
gulp.task('dist:js:lib', function () {
return gulp
.src([
paths.libDir + 'sjcl/sjcl.js',
paths.libDir + 'sjcl/*.js',
paths.libDir + 'angulartics/angulartics.js',
paths.libDir + '**/*.js',
'!' + paths.libDir + '**/*.min.js',
@@ -309,7 +368,7 @@ gulp.task('dist:preprocess', function () {
.src([
paths.dist + '/**/*.html'
], { base: '.' })
.pipe(preprocess({ context: { cacheTag: randomString }}))
.pipe(preprocess({ context: { cacheTag: randomString } }))
.pipe(gulp.dest('.'));
});
@@ -326,6 +385,14 @@ gulp.task('deploy', ['dist'], function () {
.pipe(ghPages({ cacheDir: paths.dist + '.publish' }));
});
gulp.task('deploy-preview', ['dist'], function () {
return gulp.src(paths.dist + '**/*')
.pipe(ghPages({
cacheDir: paths.dist + '.publish',
remoteUrl: 'git@github.com:bitwarden/web-preview.git'
}));
});
gulp.task('serve', function () {
connect.server({
port: 4001,

View File

@@ -1,47 +1,52 @@
{
"name": "bitwarden",
"version": "1.8.0",
"production": false,
"version": "1.11.0",
"env": "Production",
"devDependencies": {
"connect": "3.4.1",
"lodash": "4.13.1",
"connect": "3.6.0",
"lodash": "4.17.4",
"gulp": "3.9.1",
"gulp-concat": "2.6.0",
"gulp-concat": "2.6.1",
"gulp-cssmin": "0.1.7",
"gulp-less": "3.1.0",
"gulp-less": "3.3.0",
"gulp-rename": "1.2.2",
"gulp-uglify": "1.5.3",
"gulp-uglify": "2.1.2",
"gulp-gh-pages": "0.5.4",
"gulp-preprocess": "2.0.0",
"gulp-ng-annotate": "2.0.0",
"gulp-ng-config": "1.3.1",
"gulp-ng-config": "1.4.0",
"gulp-connect": "5.0.0",
"jshint": "2.9.2",
"gulp-jshint": "2.0.1",
"rimraf": "2.5.2",
"run-sequence": "1.2.1",
"merge-stream": "1.0.0",
"jshint": "2.9.4",
"gulp-jshint": "2.0.4",
"rimraf": "2.6.1",
"run-sequence": "1.2.2",
"merge-stream": "1.0.1",
"jquery": "2.2.4",
"font-awesome": "4.6.3",
"bootstrap": "3.3.6",
"sjcl": "1.0.3",
"angular": "1.5.6",
"angular-resource": "1.5.6",
"angular-bootstrap-npm": "0.14.3",
"angular-ui-router": "0.3.1",
"angular-jwt": "0.0.9",
"angular-cookies": "1.5.6",
"admin-lte": "2.3.5",
"angular-md5": "0.1.10",
"angular-toastr": "1.7.0",
"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-ui-router": "0.4.2",
"angular-jwt": "0.1.9",
"angular-cookies": "1.6.3",
"admin-lte": "2.3.11",
"angular-toastr": "2.1.1",
"angular-bootstrap-show-errors": "2.3.0",
"angular-messages": "1.5.6",
"ngstorage": "0.3.10",
"papaparse": "4.1.2",
"toastr": "2.1.2",
"clipboard": "1.5.12",
"angular-messages": "1.6.3",
"ngstorage": "0.3.11",
"papaparse": "4.2.0",
"clipboard": "1.6.1",
"ngclipboard": "1.1.1",
"angulartics": "1.1.2",
"angulartics-google-analytics": "0.2.1"
"angulartics": "1.4.0",
"angulartics-google-analytics": "0.4.0",
"node-forge": "0.7.1",
"webpack-stream": "3.2.0",
"angular-stripe": "4.2.12",
"angular-credit-cards": "3.1.6",
"browserify": "14.1.0",
"vinyl-source-stream": "1.1.0",
"gulp-derequire": "2.1.0",
"exposify": "0.5.0"
}
}

7
settings.Preview.json Normal file
View File

@@ -0,0 +1,7 @@
{
"appSettings": {
"apiUri": "https://preview-api.bitwarden.com",
"identityUri": "https://preview-identity.bitwarden.com",
"stripeKey": "pk_test_KPoCfZXu7mznb9uSCPZ2JpTD"
}
}

View File

@@ -1,5 +1,7 @@
{
"appSettings": {
"apiUri": "https://api.bitwarden.com"
}
"appSettings": {
"apiUri": "https://api.bitwarden.com",
"identityUri": "https://identity.bitwarden.com",
"stripeKey": "pk_live_bpN0P37nMxrMQkcaHXtAybJk"
}
}

View File

@@ -1,6 +1,7 @@
{
"appSettings": {
"rememberedEmailCookieName": "bit.rememberedEmail",
"apiUri": "http://localhost:4000"
"apiUri": "http://localhost:4000",
"identityUri": "http://localhost:33656",
"stripeKey": "pk_test_KPoCfZXu7mznb9uSCPZ2JpTD"
}
}

View File

@@ -2,12 +2,25 @@ angular
.module('bit.accounts')
.controller('accountsLoginController', function ($scope, $rootScope, $cookies, apiService, cryptoService, authService,
$state, appSettings, $analytics) {
var rememberedEmail = $cookies.get(appSettings.rememberedEmailCookieName);
if (rememberedEmail) {
$state, constants, $analytics) {
$scope.state = $state;
var returnState;
if (!$state.params.returnState && $state.params.org) {
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: rememberedEmail,
rememberEmail: true
email: $state.params.email ? $state.params.email : rememberedEmail,
rememberEmail: rememberedEmail !== null
};
}
@@ -23,12 +36,12 @@ angular
cookieExpiration.setFullYear(cookieExpiration.getFullYear() + 10);
$cookies.put(
appSettings.rememberedEmailCookieName,
constants.rememberedEmailCookieName,
model.email,
{ expires: cookieExpiration });
}
else {
$cookies.remove(appSettings.rememberedEmailCookieName);
$cookies.remove(constants.rememberedEmailCookieName);
}
if (twoFactorProviders && twoFactorProviders.length > 0) {
@@ -36,11 +49,11 @@ angular
masterPassword = model.masterPassword;
$analytics.eventTrack('Logged In To Two-step');
$state.go('frontend.login.twoFactor');
$state.go('frontend.login.twoFactor', { returnState: returnState });
}
else {
$analytics.eventTrack('Logged In');
$state.go('backend.vault');
loggedInGo();
}
});
};
@@ -51,7 +64,16 @@ angular
$scope.twoFactorPromise.then(function () {
$analytics.eventTrack('Logged In From Two-step');
$state.go('backend.vault');
loggedInGo();
});
};
function loggedInGo() {
if (returnState) {
$state.go(returnState.name, returnState.params);
}
else {
$state.go('backend.user.vault');
}
}
});

View File

@@ -0,0 +1,49 @@
angular
.module('bit.accounts')
.controller('accountsOrganizationAcceptController', function ($scope, $state, apiService, authService, toastr, $analytics) {
$scope.state = {
name: $state.current.name,
params: $state.params
};
if (!$state.params.organizationId || !$state.params.organizationUserId || !$state.params.token ||
!$state.params.email || !$state.params.organizationName) {
$state.go('frontend.login.info').then(function () {
toastr.error('Invalid parameters.');
});
return;
}
$scope.$on('$viewContentLoaded', function () {
if (authService.isAuthenticated()) {
$scope.accepting = true;
apiService.organizationUsers.accept(
{
orgId: $state.params.organizationId,
id: $state.params.organizationUserId
},
{
token: $state.params.token
}, function () {
$analytics.eventTrack('Accepted Invitation');
$state.go('backend.user.vault', null, { location: 'replace' }).then(function () {
toastr.success('You can access this organization once an administrator confirms your membership.' +
' We\'ll send an email when that happens.', 'Invite Accepted', { timeOut: 10000 });
});
}, function () {
$analytics.eventTrack('Failed To Accept Invitation');
$state.go('backend.user.vault', null, { location: 'replace' }).then(function () {
toastr.error('Unable to accept invitation.', 'Error');
});
});
}
else {
$scope.loading = false;
}
});
$scope.submit = function (model) {
};
});

View File

@@ -1,11 +1,12 @@
angular
.module('bit.accounts')
.controller('accountsPasswordHintController', function ($scope, $rootScope, apiService) {
.controller('accountsPasswordHintController', function ($scope, $rootScope, apiService, $analytics) {
$scope.success = false;
$scope.submit = function (model) {
$scope.submitPromise = apiService.accounts.postPasswordHint({ email: model.email }, function () {
$analytics.eventTrack('Requested Password Hint');
$scope.success = true;
}).$promise;
};

View File

@@ -1,7 +1,7 @@
angular
.module('bit.accounts')
.controller('accountsRecoverController', function ($scope, apiService, cryptoService) {
.controller('accountsRecoverController', function ($scope, apiService, cryptoService, $analytics) {
$scope.success = false;
$scope.submit = function (model) {
@@ -15,6 +15,7 @@ angular
};
$scope.submitPromise = apiService.accounts.postTwoFactorRecover(request, function () {
$analytics.eventTrack('Recovered 2FA');
$scope.success = true;
}).$promise;
};

View File

@@ -1,13 +1,27 @@
angular
.module('bit.accounts')
.controller('accountsRegisterController', function ($scope, $location, apiService, cryptoService, validationService, $analytics) {
.controller('accountsRegisterController', function ($scope, $location, apiService, cryptoService, validationService,
$analytics, $state) {
var params = $location.search();
var stateParams = $state.params;
$scope.createOrg = stateParams.org;
if (!stateParams.returnState && stateParams.org) {
$scope.returnState = {
name: 'backend.user.settingsCreateOrg',
params: { plan: $state.params.org }
};
}
else {
$scope.returnState = stateParams.returnState;
}
$scope.success = false;
$scope.model = {
email: params.email
email: params.email ? params.email : stateParams.email
};
$scope.readOnlyEmail = stateParams.email !== null;
$scope.registerPromise = null;
$scope.register = function (form) {
@@ -28,16 +42,30 @@ angular
var email = $scope.model.email.toLowerCase();
var key = cryptoService.makeKey($scope.model.masterPassword, email);
var request = {
name: $scope.model.name,
email: email,
masterPasswordHash: cryptoService.hashPassword($scope.model.masterPassword, key),
masterPasswordHint: $scope.model.masterPasswordHint
};
$scope.registerPromise = apiService.accounts.register(request, function () {
$scope.registerPromise = cryptoService.makeKeyPair(key).then(function (result) {
var request = {
name: $scope.model.name,
email: email,
masterPasswordHash: cryptoService.hashPassword($scope.model.masterPassword, key),
masterPasswordHint: $scope.model.masterPasswordHint,
keys: {
publicKey: result.publicKey,
encryptedPrivateKey: result.privateKeyEnc
}
};
return apiService.accounts.register(request).$promise;
}, function (errors) {
validationService.addError(form, null, 'Problem generating keys.', true);
return false;
}).then(function (result) {
if (result === false) {
return;
}
$scope.success = true;
$analytics.eventTrack('Registered');
}).$promise;
});
};
});

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>
@@ -35,7 +35,15 @@
</div>
<hr />
<ul>
<li><a ui-sref="frontend.register">Create a new account</a></li>
<li><a ui-sref="frontend.passwordHint">Get master password hint</a></li>
<li>
<a ui-sref="frontend.register({returnState: state.params.returnState, email: state.params.email})">
Create a new account
</a>
</li>
<li>
<a ui-sref="frontend.passwordHint">
Get master password hint
</a>
</li>
</ul>
</form>

View File

@@ -1,7 +1,7 @@
<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>
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in twoFactorForm.$errors">{{e}}</li>
</ul>

View File

@@ -0,0 +1,32 @@
<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-show="loading">
Loading...
</div>
<div ng-show="accepting">
Accepting invitation...
</div>
<div ng-show="!loading && !accepting">
<p class="login-box-msg">Join {{state.params.organizationName}}</p>
<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.
</p>
<hr />
<div class="row">
<div class="col-sm-6">
<a ui-sref="frontend.login.info({returnState: state, email: state.params.email})"
class="btn btn-primary btn-block btn-flat">Log In</a>
</div>
<div class="col-sm-6">
<a ui-sref="frontend.register({returnState: state, email: state.params.email})"
class="btn btn-primary btn-block btn-flat">Create Account</a>
</div>
</div>
</div>
</div>
</div>

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

@@ -13,7 +13,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

@@ -9,12 +9,16 @@
<h4>Account Created!</h4>
<p>You may now log in to your new account.</p>
</div>
<a ui-sref="frontend.login.info">Ready to log in?</a>
<a ui-sref="frontend.login.info({returnState: returnState, email: model.email})">Ready to log in?</a>
</div>
<form name="registerForm" ng-submit="registerForm.$valid && register(registerForm)" ng-show="!success"
api-form="registerPromise">
<div class="callout callout-default" ng-show="createOrg">
<h4>Create Organization, Step 1</h4>
<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>
@@ -22,7 +26,7 @@
<div class="form-group has-feedback" show-errors>
<label for="email" class="sr-only">Email</label>
<input type="email" id="email" name="Email" class="form-control" placeholder="Email" ng-model="model.email"
required api-field />
ng-readonly="readOnlyEmail" required api-field />
<span class="fa fa-envelope form-control-feedback"></span>
<p class="help-block">You'll use your email address to log in.</p>
</div>
@@ -60,7 +64,7 @@
</div>
<div class="row">
<div class="col-xs-7">
<a ui-sref="frontend.login.info">Already have an account?</a>
<a ui-sref="frontend.login.info({returnState: returnState})">Already have an account?</a>
</div>
<div class="col-xs-5">
<button type="submit" class="btn btn-primary btn-block btn-flat" ng-disabled="registerForm.$loading">

View File

@@ -3,18 +3,21 @@
'ui.router',
'ngMessages',
'angular-jwt',
'angular-md5',
'ui.bootstrap.showErrors',
'toastr',
'angulartics',
'angulartics.google.analytics',
'angular-stripe',
'credit-cards',
'bit.directives',
'bit.filters',
'bit.services',
'bit.global',
'bit.accounts',
'bit.vault',
'bit.settings',
'bit.tools'
'bit.tools',
'bit.organization'
]);

View File

@@ -1,11 +1,17 @@
angular
.module('bit')
.config(function ($stateProvider, $urlRouterProvider, $httpProvider, jwtInterceptorProvider, $uibTooltipProvider, toastrConfig) {
jwtInterceptorProvider.urlParam = 'access_token2';
.config(function ($stateProvider, $urlRouterProvider, $httpProvider, jwtInterceptorProvider, jwtOptionsProvider,
$uibTooltipProvider, toastrConfig, $locationProvider, $qProvider, stripeProvider, appSettings) {
$qProvider.errorOnUnhandledRejections(false);
$locationProvider.hashPrefix('');
jwtOptionsProvider.config({
urlParam: 'access_token3',
whiteListedDomains: ['api.bitwarden.com', 'preview-api.bitwarden.com', 'localhost', '192.168.1.8']
});
var refreshPromise;
jwtInterceptorProvider.tokenGetter = /*@ngInject*/ function (config, appSettings, tokenService, apiService, jwtHelper, $q) {
if (config.url.indexOf(appSettings.apiUri) !== 0) {
jwtInterceptorProvider.tokenGetter = /*@ngInject*/ function (options, tokenService, authService) {
if (options.url.indexOf(appSettings.apiUri) !== 0) {
return;
}
@@ -22,26 +28,15 @@ angular
return token;
}
var refreshToken = tokenService.getRefreshToken();
if (!refreshToken) {
return;
}
var deferred = $q.defer();
apiService.identity.token({
grant_type: 'refresh_token',
client_id: 'web',
refresh_token: refreshToken
}, function (response) {
tokenService.setToken(response.access_token);
tokenService.setRefreshToken(response.refresh_token);
refreshPromise = authService.refreshAccessToken().then(function (newToken) {
refreshPromise = null;
deferred.resolve(response.access_token);
return newToken || token;
});
refreshPromise = deferred.promise;
return refreshPromise;
};
stripeProvider.setPublishableKey(appSettings.stripeKey);
angular.extend(toastrConfig, {
closeButton: true,
progressBar: true,
@@ -52,7 +47,6 @@ angular
$uibTooltipProvider.options({
popupDelay: 600,
appendToBody: true
});
if ($httpProvider.defaults.headers.post) {
@@ -67,7 +61,7 @@ angular
$urlRouterProvider.otherwise('/');
$stateProvider
// Backend
// Backend
.state('backend', {
templateUrl: 'app/views/backendLayout.html',
abstract: true,
@@ -75,26 +69,103 @@ angular
authorize: true
}
})
.state('backend.vault', {
url: '^/',
.state('backend.user', {
templateUrl: 'app/views/userLayout.html',
abstract: true
})
.state('backend.user.vault', {
url: '^/vault',
templateUrl: 'app/vault/views/vault.html',
controller: 'vaultController',
data: { pageTitle: 'My Vault' }
data: { pageTitle: 'My Vault' },
params: {
refreshFromServer: false
}
})
.state('backend.settings', {
.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',
controller: 'settingsController',
data: { pageTitle: 'Settings' }
})
.state('backend.tools', {
.state('backend.user.settingsDomains', {
url: '^/settings/domains',
templateUrl: 'app/settings/views/settingsDomains.html',
controller: 'settingsDomainsController',
data: { pageTitle: 'Domain Settings' }
})
.state('backend.user.settingsCreateOrg', {
url: '^/settings/create-organization',
templateUrl: 'app/settings/views/settingsCreateOrganization.html',
controller: 'settingsCreateOrganizationController',
data: { pageTitle: 'Create Organization' }
})
.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.org', {
templateUrl: 'app/views/organizationLayout.html',
abstract: true
})
.state('backend.org.dashboard', {
url: '^/organization/:orgId',
templateUrl: 'app/organization/views/organizationDashboard.html',
controller: 'organizationDashboardController',
data: { pageTitle: 'Organization Dashboard' }
})
.state('backend.org.people', {
url: '/organization/:orgId/people',
templateUrl: 'app/organization/views/organizationPeople.html',
controller: 'organizationPeopleController',
data: { pageTitle: 'Organization People' }
})
.state('backend.org.collections', {
url: '/organization/:orgId/collections',
templateUrl: 'app/organization/views/organizationCollections.html',
controller: 'organizationCollectionsController',
data: { pageTitle: 'Organization Collections' }
})
.state('backend.org.settings', {
url: '/organization/:orgId/settings',
templateUrl: 'app/organization/views/organizationSettings.html',
controller: 'organizationSettingsController',
data: { pageTitle: 'Organization Settings' }
})
.state('backend.org.billing', {
url: '/organization/:orgId/billing',
templateUrl: 'app/organization/views/organizationBilling.html',
controller: 'organizationBillingController',
data: { pageTitle: 'Organization Billing' }
})
.state('backend.org.vault', {
url: '/organization/:orgId/vault',
templateUrl: 'app/organization/views/organizationVault.html',
controller: 'organizationVaultController',
data: { pageTitle: 'Organization Vault' }
})
.state('backend.org.groups', {
url: '/organization/:orgId/groups',
templateUrl: 'app/organization/views/organizationGroups.html',
controller: 'organizationGroupsController',
data: { pageTitle: 'Organization Groups' }
})
// Frontend
// Frontend
.state('frontend', {
templateUrl: 'app/views/frontendLayout.html',
abstract: true,
@@ -105,19 +176,23 @@ angular
.state('frontend.login', {
templateUrl: 'app/accounts/views/accountsLogin.html',
controller: 'accountsLoginController',
params: {
returnState: null,
email: null
},
data: {
bodyClass: 'login-page'
}
})
.state('frontend.login.info', {
url: '^/login',
url: '^/?org',
templateUrl: 'app/accounts/views/accountsLoginInfo.html',
data: {
pageTitle: 'Log In'
}
})
.state('frontend.login.twoFactor', {
url: '^/login/two-factor',
url: '^/two-factor',
templateUrl: 'app/accounts/views/accountsLoginTwoFactor.html',
data: {
pageTitle: 'Log In (Two Factor)',
@@ -150,23 +225,46 @@ angular
}
})
.state('frontend.register', {
url: '^/register',
url: '^/register?org',
templateUrl: 'app/accounts/views/accountsRegister.html',
controller: 'accountsRegisterController',
params: {
returnState: null,
email: null
},
data: {
pageTitle: 'Register',
bodyClass: 'register-page'
}
})
.state('frontend.organizationAccept', {
url: '^/accept-organization?organizationId&organizationUserId&token&email&organizationName',
templateUrl: 'app/accounts/views/accountsOrganizationAccept.html',
controller: 'accountsOrganizationAcceptController',
data: {
pageTitle: 'Accept Organization Invite',
bodyClass: 'login-page',
skipAuthorize: true
}
});
})
.run(function ($rootScope, authService, jwtHelper, tokenService, $state) {
.run(function ($rootScope, authService, $state) {
$rootScope.$on('$stateChangeSuccess', function () {
$('html, body').animate({ scrollTop: 0 }, 200);
});
$rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
if (!toState.data || !toState.data.authorize) {
if (authService.isAuthenticated()) {
event.preventDefault();
$state.go('backend.vault');
if (toState.data && toState.data.skipAuthorize) {
return;
}
if (!authService.isAuthenticated()) {
return;
}
event.preventDefault();
$state.go('backend.user.vault');
return;
}
@@ -174,6 +272,22 @@ angular
event.preventDefault();
authService.logOut();
$state.go('frontend.login.info');
return;
}
// 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;
authService.getUserProfile().then(function (profile) {
var orgs = profile.organizations;
if (!orgs || !(toParams.orgId in orgs) || orgs[toParams.orgId].status !== 2 ||
orgs[toParams.orgId].type === 2) {
event.preventDefault();
$state.go('backend.user.vault');
}
});
}
});
});

59
src/app/constants.js Normal file
View File

@@ -0,0 +1,59 @@
angular.module('bit')
.constant('constants', {
rememberedEmailCookieName: 'bit.rememberedEmail',
encType: {
AesCbc256_B64: 0,
AesCbc128_HmacSha256_B64: 1,
AesCbc256_HmacSha256_B64: 2,
Rsa2048_OaepSha256_B64: 3,
Rsa2048_OaepSha1_B64: 4
},
orgUserType: {
owner: 0,
admin: 1,
user: 2
},
orgUserStatus: {
invited: 0,
accepted: 1,
confirmed: 2
},
plans: {
free: {
basePrice: 0,
noAdditionalSeats: true,
noPayment: true,
upgradeSortOrder: -1
},
personal: {
basePrice: 1,
annualBasePrice: 12,
baseSeats: 5,
seatPrice: 1,
annualSeatPrice: 12,
maxAdditionalSeats: 5,
annualPlanType: 'personalAnnually',
upgradeSortOrder: 1
},
teams: {
basePrice: 5,
annualBasePrice: 60,
monthlyBasePrice: 8,
baseSeats: 5,
seatPrice: 2,
annualSeatPrice: 24,
monthlySeatPrice: 2.5,
monthPlanType: 'teamsMonthly',
annualPlanType: 'teamsAnnually',
upgradeSortOrder: 2
},
enterprise: {
seatPrice: 3,
annualSeatPrice: 36,
monthlySeatPrice: 4,
monthPlanType: 'enterpriseMonthly',
annualPlanType: 'enterpriseAnnually',
upgradeSortOrder: 3
}
}
});

View File

@@ -30,6 +30,7 @@ angular
form.$loading = false;
validationService.addErrors(form, reason);
scope.$broadcast('show-errors-check-validity');
$('html, body').animate({ scrollTop: 0 }, 200);
});
}
});

View File

@@ -0,0 +1,151 @@
angular
.module('bit.directives')
// adaptation of https://github.com/uttesh/ngletteravatar
.directive('letterAvatar', function () {
// ref: http://stackoverflow.com/a/16348977/1090359
function stringToColor(str) {
var hash = 0,
i = 0;
for (i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
var color = '#';
for (i = 0; i < 3; i++) {
var value = (hash >> (i * 8)) & 0xFF;
color += ('00' + value.toString(16)).substr(-2);
}
return color;
}
function getFirstLetters(data, count) {
var parts = data.split(' ');
if (parts && parts.length > 1) {
var text = '';
for (var i = 0; i < count; i++) {
text += parts[i].substr(0, 1);
}
return text;
}
return null;
}
function getSvg(width, height, color) {
var svgTag = angular.element('<svg></svg>')
.attr({
'xmlns': 'http://www.w3.org/2000/svg',
'pointer-events': 'none',
'width': width,
'height': height
})
.css({
'background-color': color,
'width': width + 'px',
'height': height + 'px'
});
return svgTag;
}
function getCharText(character, textColor, fontFamily, fontWeight, fontsize) {
var textTag = angular.element('<text text-anchor="middle"></text>')
.attr({
'y': '50%',
'x': '50%',
'dy': '0.35em',
'pointer-events': 'auto',
'fill': textColor,
'font-family': fontFamily
})
.text(character)
.css({
'font-weight': fontWeight,
'font-size': fontsize + 'px',
});
return textTag;
}
return {
restrict: 'AE',
replace: true,
scope: {
data: '@'
},
link: function (scope, element, attrs) {
var params = {
charCount: attrs.charcount || 2,
data: attrs.data,
textColor: attrs.textcolor || '#ffffff',
bgColor: attrs.bgcolor,
height: attrs.avheight || 45,
width: attrs.avwidth || 45,
fontSize: attrs.fontsize || 20,
fontWeight: attrs.fontweight || 300,
fontFamily: attrs.fontfamily || 'Open Sans, HelveticaNeue-Light, Helvetica Neue Light, ' +
'Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif',
round: attrs.round || 'true',
dynamic: attrs.dynamic || 'true',
class: attrs.avclass || '',
border: attrs.avborder || 'false',
borderStyle: attrs.borderstyle || '3px solid white'
};
if (params.dynamic === 'true') {
scope.$watch('data', function () {
generateLetterAvatar();
});
}
else {
generateLetterAvatar();
}
function generateLetterAvatar() {
var c = null,
upperData = scope.data.toUpperCase();
if (params.charCount > 1) {
c = getFirstLetters(upperData, params.charCount);
}
if (!c) {
c = upperData.substr(0, params.charCount);
}
var cobj = getCharText(c, params.textColor, params.fontFamily, params.fontWeight, params.fontSize);
var color = params.bgColor ? params.bgColor : stringToColor(upperData);
var svg = getSvg(params.width, params.height, color);
svg.append(cobj);
var lvcomponent = angular.element('<div>').append(svg).html();
var svgHtml = window.btoa(unescape(encodeURIComponent(lvcomponent)));
var src = 'data:image/svg+xml;base64,' + svgHtml;
var img = angular.element('<img>').attr({ src: src, title: scope.data });
if (params.round === 'true') {
img.css('border-radius', '50%');
}
if (params.border === 'true') {
img.css('border', params.borderStyle);
}
if (params.class) {
img.addClass(params.class);
}
if (params.dynamic === 'true') {
element.empty();
element.append(img);
}
else {
element.replaceWith(img);
}
}
}
};
});

View File

@@ -6,34 +6,31 @@ angular
require: 'ngModel',
restrict: 'A',
link: function (scope, elem, attr, ngModel) {
var profile = authService.getUserProfile();
if (!profile) {
return;
}
authService.getUserProfile().then(function (profile) {
// For DOM -> model validation
ngModel.$parsers.unshift(function (value) {
if (!value) {
return undefined;
}
// For DOM -> model validation
ngModel.$parsers.unshift(function (value) {
if (!value) {
return undefined;
}
var key = cryptoService.makeKey(value, profile.email);
var valid = key.keyB64 === cryptoService.getKey().keyB64;
ngModel.$setValidity('masterPassword', valid);
return valid ? value : undefined;
});
var key = cryptoService.makeKey(value, profile.email, true);
var valid = key === cryptoService.getKey(true);
ngModel.$setValidity('masterPassword', valid);
return valid ? value : undefined;
});
// For model -> DOM validation
ngModel.$formatters.unshift(function (value) {
if (!value) {
return undefined;
}
// For model -> DOM validation
ngModel.$formatters.unshift(function (value) {
if (!value) {
return undefined;
}
var key = cryptoService.makeKey(value, profile.email);
var valid = key.keyB64 === cryptoService.getKey().keyB64;
var key = cryptoService.makeKey(value, profile.email, true);
var valid = key === cryptoService.getKey(true);
ngModel.$setValidity('masterPassword', valid);
return value;
ngModel.$setValidity('masterPassword', valid);
return value;
});
});
}
};

View File

@@ -0,0 +1,32 @@
angular
.module('bit.filters')
.filter('enumLabelClass', function () {
return function (input, name) {
if (typeof input !== 'number') {
return input.toString();
}
var output;
switch (name) {
case 'OrgUserStatus':
switch (input) {
case 0:
output = 'label-default';
break;
case 1:
output = 'label-warning';
break;
case 2:
/* falls through */
default:
output = 'label-success';
}
break;
default:
output = 'label-default';
}
return output;
};
});

View File

@@ -0,0 +1,46 @@
angular
.module('bit.filters')
.filter('enumName', function () {
return function (input, name) {
if (typeof input !== 'number') {
return input.toString();
}
var output;
switch (name) {
case 'OrgUserStatus':
switch (input) {
case 0:
output = 'Invited';
break;
case 1:
output = 'Accepted';
break;
case 2:
/* falls through */
default:
output = 'Confirmed';
}
break;
case 'OrgUserType':
switch (input) {
case 0:
output = 'Owner';
break;
case 1:
output = 'Admin';
break;
case 2:
/* falls through */
default:
output = 'User';
}
break;
default:
output = input.toString();
}
return output;
};
});

View File

@@ -0,0 +1,2 @@
angular
.module('bit.filters', []);

View File

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

View File

@@ -1,16 +1,19 @@
angular
.module('bit.global')
.controller('mainController', function ($scope, $state, authService, appSettings, toastr) {
.controller('mainController', function ($scope, $state, authService, appSettings, toastr, $window, $document) {
var vm = this;
vm.bodyClass = '';
vm.userProfile = null;
vm.searchVaultText = null;
vm.version = appSettings.version;
$scope.currentYear = new Date().getFullYear();
$scope.$on('$viewContentLoaded', function () {
authService.getUserProfile().then(function (profile) {
vm.userProfile = profile;
});
if ($.AdminLTE) {
if ($.AdminLTE.layout) {
$.AdminLTE.layout.fix();
@@ -20,12 +23,13 @@ angular
if ($.AdminLTE.pushMenu) {
$.AdminLTE.pushMenu.expandOnHover();
}
$(document).off('click', '.sidebar li a');
}
});
$scope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams) {
vm.searchVaultText = null;
vm.userProfile = authService.getUserProfile();
if (toState.data.bodyClass) {
vm.bodyClass = toState.data.bodyClass;
@@ -36,10 +40,6 @@ angular
}
});
$scope.searchVault = function () {
$state.go('backend.vault');
};
$scope.addLogin = function () {
$scope.$broadcast('vaultAddLogin');
};
@@ -48,39 +48,103 @@ angular
$scope.$broadcast('vaultAddFolder');
};
$scope.changeEmail = function () {
$scope.$broadcast('settingsChangeEmail');
$scope.addOrganizationLogin = function () {
$scope.$broadcast('organizationVaultAddLogin');
};
$scope.changePassword = function () {
$scope.$broadcast('settingsChangePassword');
$scope.addOrganizationCollection = function () {
$scope.$broadcast('organizationCollectionsAdd');
};
$scope.sessions = function () {
$scope.$broadcast('settingsSessions');
$scope.inviteOrganizationUser = function () {
$scope.$broadcast('organizationPeopleInvite');
};
$scope.domains = function () {
$scope.$broadcast('settingsDomains');
$scope.addOrganizationGroup = function () {
$scope.$broadcast('organizationGroupsAdd');
};
$scope.delete = function () {
$scope.$broadcast('settingsDelete');
// Append dropdown menu somewhere else
var bodyScrollbarWidth,
appendedDropdownMenu,
appendedDropdownMenuParent;
var dropdownHelpers = {
scrollbarWidth: function () {
if (!bodyScrollbarWidth) {
var bodyElem = $('body');
bodyElem.addClass('bit-position-body-scrollbar-measure');
bodyScrollbarWidth = $window.innerWidth - bodyElem[0].clientWidth;
bodyScrollbarWidth = isFinite(bodyScrollbarWidth) ? bodyScrollbarWidth : 0;
bodyElem.removeClass('bit-position-body-scrollbar-measure');
}
return bodyScrollbarWidth;
},
scrollbarInfo: function () {
return {
width: dropdownHelpers.scrollbarWidth(),
visible: $document.height() > $($window).height()
};
}
};
$scope.twoFactor = function () {
$scope.$broadcast('settingsTwoFactor');
};
$(window).on('show.bs.dropdown', function (e) {
/*jshint -W120 */
var target = appendedDropdownMenuParent = $(e.target);
$scope.import = function () {
$scope.$broadcast('toolsImport');
};
var appendTo = target.data('appendTo');
if (!appendTo) {
return true;
}
$scope.export = function () {
$scope.$broadcast('toolsExport');
};
appendedDropdownMenu = target.find('.dropdown-menu');
var appendToEl = $(appendTo);
appendToEl.append(appendedDropdownMenu.detach());
$scope.audits = function () {
$scope.$broadcast('toolsAudits');
};
var offset = target.offset();
var css = {
display: 'block',
top: offset.top + target.outerHeight()
};
if (appendedDropdownMenu.hasClass('dropdown-menu-right')) {
var scrollbarInfo = dropdownHelpers.scrollbarInfo();
var scrollbarWidth = 0;
if (scrollbarInfo.visible && scrollbarInfo.width) {
scrollbarWidth = scrollbarInfo.width;
}
css.right = $window.innerWidth - scrollbarWidth - (offset.left + target.prop('offsetWidth')) + 'px';
css.left = 'auto';
}
else {
css.left = offset.left + 'px';
css.right = 'auto';
}
appendedDropdownMenu.css(css);
});
$(window).on('hide.bs.dropdown', function (e) {
if (!appendedDropdownMenu) {
return true;
}
$(e.target).append(appendedDropdownMenu.detach());
appendedDropdownMenu.hide();
appendedDropdownMenu = null;
appendedDropdownMenuParent = null;
});
$scope.$on('removeAppendedDropdownMenu', function (event, args) {
if (!appendedDropdownMenu && !appendedDropdownMenuParent) {
return true;
}
appendedDropdownMenuParent.append(appendedDropdownMenu.detach());
appendedDropdownMenu.hide();
appendedDropdownMenu = null;
appendedDropdownMenuParent = null;
});
});

View File

@@ -1,6 +1,54 @@
angular
.module('bit.global')
.controller('sideNavController', function ($scope, $state) {
.controller('sideNavController', function ($scope, $state, authService, toastr, $analytics) {
$scope.$state = $state;
$scope.params = $state.params;
$scope.orgs = [];
$scope.name = '';
authService.getUserProfile().then(function (userProfile) {
$scope.name = userProfile.extended && userProfile.extended.name ?
userProfile.extended.name : userProfile.email;
if (!userProfile.organizations) {
return;
}
if ($state.includes('backend.org') && ($state.params.orgId in userProfile.organizations)) {
$scope.orgProfile = userProfile.organizations[$state.params.orgId];
}
else {
var orgs = [];
for (var orgId in userProfile.organizations) {
if (userProfile.organizations.hasOwnProperty(orgId) &&
(userProfile.organizations[orgId].enabled || userProfile.organizations[orgId].type < 2)) { // 2 = User
orgs.push(userProfile.organizations[orgId]);
}
}
$scope.orgs = orgs;
}
});
$scope.viewOrganization = function (org) {
if (org.type === 2) { // 2 = User
toastr.error('You cannot manage this organization.');
return;
}
$analytics.eventTrack('View Organization From Side Nav');
$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;
};
});

View File

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

View File

@@ -0,0 +1,34 @@
angular
.module('bit.organization')
.controller('organizationBillingChangePaymentController', function ($scope, $state, $uibModalInstance, apiService, stripe,
$analytics, toastr, existingPaymentMethod) {
$analytics.eventTrack('organizationBillingChangePaymentController', { category: 'Modal' });
$scope.existingPaymentMethod = existingPaymentMethod;
$scope.submit = function () {
$scope.submitPromise = stripe.card.createToken($scope.card).then(function (response) {
var request = {
paymentToken: response.id
};
return apiService.organizations.putPayment({ id: $state.params.orgId }, request).$promise;
}).then(function (response) {
$scope.card = null;
if (existingPaymentMethod) {
$analytics.eventTrack('Changed Payment Method');
toastr.success('You have changed your payment method.');
}
else {
$analytics.eventTrack('Added Payment Method');
toastr.success('You have added a payment method.');
}
$uibModalInstance.close();
});
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

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

View File

@@ -0,0 +1,163 @@
angular
.module('bit.organization')
.controller('organizationBillingController', function ($scope, apiService, $state, $uibModal, toastr, $analytics) {
$scope.charges = [];
$scope.paymentSource = null;
$scope.plan = null;
$scope.subscription = null;
$scope.loading = true;
$scope.$on('$viewContentLoaded', function () {
load();
});
$scope.changePayment = function () {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationBillingChangePayment.html',
controller: 'organizationBillingChangePaymentController',
resolve: {
existingPaymentMethod: function () {
return $scope.paymentSource ? $scope.paymentSource.description : null;
}
}
});
modal.result.then(function () {
load();
});
};
$scope.changePlan = function () {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationBillingChangePlan.html',
controller: 'organizationBillingChangePlanController',
resolve: {
plan: function () {
return $scope.plan;
}
}
});
modal.result.then(function () {
load();
});
};
$scope.adjustSeats = function (add) {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationBillingAdjustSeats.html',
controller: 'organizationBillingAdjustSeatsController',
resolve: {
add: function () {
return add;
}
}
});
modal.result.then(function () {
load();
});
};
$scope.cancel = function () {
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;
}
apiService.organizations.putCancel({ id: $state.params.orgId }, {})
.$promise.then(function (response) {
$analytics.eventTrack('Canceled Plan');
toastr.success('Organization subscription has been canceled.');
load();
});
};
$scope.reinstate = function () {
if (!confirm('Are you sure you want to remove the cancellation request and reinstate this organization?')) {
return;
}
apiService.organizations.putReinstate({ id: $state.params.orgId }, {})
.$promise.then(function (response) {
$analytics.eventTrack('Reinstated Plan');
toastr.success('Organization cancellation request has been removed.');
load();
});
};
function load() {
apiService.organizations.getBilling({ id: $state.params.orgId }, function (org) {
$scope.loading = false;
$scope.noSubscription = org.PlanType === 0;
var i = 0;
$scope.plan = {
name: org.Plan,
type: org.PlanType,
seats: org.Seats
};
$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
};
}
$scope.nextInvoice = null;
if (org.UpcomingInvoice) {
$scope.nextInvoice = {
date: org.UpcomingInvoice.Date,
amount: org.UpcomingInvoice.Amount
};
}
if (org.Subscription && org.Subscription.Items) {
$scope.subscription.items = [];
for (i = 0; i < org.Subscription.Items.length; i++) {
$scope.subscription.items.push({
amount: org.Subscription.Items[i].Amount,
name: org.Subscription.Items[i].Name,
interval: org.Subscription.Items[i].Interval,
qty: org.Subscription.Items[i].Quantity
});
}
}
$scope.paymentSource = null;
if (org.PaymentSource) {
$scope.paymentSource = {
type: org.PaymentSource.Type,
description: org.PaymentSource.Description,
cardBrand: org.PaymentSource.CardBrand
};
}
var charges = [];
for (i = 0; i < org.Charges.length; i++) {
charges.push({
date: org.Charges[i].CreatedDate,
paymentSource: org.Charges[i].PaymentSource ? org.Charges[i].PaymentSource.Description : '-',
amount: org.Charges[i].Amount,
status: org.Charges[i].Status,
failureMessage: org.Charges[i].FailureMessage,
refunded: org.Charges[i].Refunded,
partiallyRefunded: org.Charges[i].PartiallyRefunded,
refundedAmount: org.Charges[i].RefundedAmount,
invoiceId: org.Charges[i].InvoiceId
});
}
$scope.charges = charges;
});
}
});

View File

@@ -0,0 +1,120 @@
angular
.module('bit.organization')
.controller('organizationCollectionsAddController', function ($scope, $state, $uibModalInstance, apiService, cipherService,
$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);
$uibModalInstance.close(decCollection);
}).$promise;
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@@ -0,0 +1,101 @@
angular
.module('bit.organization')
.controller('organizationCollectionsController', function ($scope, $state, apiService, $uibModal, cipherService, $filter,
toastr, $analytics) {
$scope.collections = [];
$scope.loading = true;
$scope.$on('$viewContentLoaded', function () {
loadList();
});
$scope.$on('organizationCollectionsAdd', function (event, args) {
$scope.add();
});
$scope.add = function () {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationCollectionsAdd.html',
controller: 'organizationCollectionsAddController'
});
modal.result.then(function (collection) {
$scope.collections.push(collection);
});
};
$scope.edit = function (collection) {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationCollectionsEdit.html',
controller: 'organizationCollectionsEditController',
resolve: {
id: function () { return collection.id; }
}
});
modal.result.then(function (editedCollection) {
var existingCollections = $filter('filter')($scope.collections, { id: editedCollection.id }, true);
if (existingCollections && existingCollections.length > 0) {
existingCollections[0].name = editedCollection.name;
}
});
};
$scope.users = function (collection) {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationCollectionsUsers.html',
controller: 'organizationCollectionsUsersController',
size: 'lg',
resolve: {
collection: function () { return collection; }
}
});
modal.result.then(function () {
// nothing to do
});
};
$scope.groups = function (collection) {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationCollectionsGroups.html',
controller: 'organizationCollectionsGroupsController',
resolve: {
collection: function () { return collection; }
}
});
modal.result.then(function () {
// nothing to do
});
};
$scope.delete = function (collection) {
if (!confirm('Are you sure you want to delete this collection (' + collection.name + ')?')) {
return;
}
apiService.collections.del({ orgId: $state.params.orgId, id: collection.id }, function () {
var index = $scope.collections.indexOf(collection);
if (index > -1) {
$scope.collections.splice(index, 1);
}
$analytics.eventTrack('Deleted Collection');
toastr.success(collection.name + ' has been deleted.', 'Collection Deleted');
}, function () {
toastr.error(collection.name + ' was not able to be deleted.', 'Error');
});
};
function loadList() {
apiService.collections.listOrganization({ orgId: $state.params.orgId }, function (list) {
$scope.collections = cipherService.decryptCollections(list.Data, $state.params.orgId, true);
$scope.loading = false;
});
}
});

View File

@@ -0,0 +1,139 @@
angular
.module('bit.organization')
.controller('organizationCollectionsEditController', function ($scope, $state, $uibModalInstance, apiService, cipherService,
$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 () {
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);
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);
}).$promise;
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@@ -0,0 +1,63 @@
angular
.module('bit.organization')
.controller('organizationCollectionsUsersController', function ($scope, $state, $uibModalInstance, apiService, cipherService,
$analytics, collection, toastr) {
$analytics.eventTrack('organizationCollectionsUsersController', { category: 'Modal' });
$scope.loading = true;
$scope.collection = collection;
$scope.users = [];
$uibModalInstance.opened.then(function () {
$scope.loading = false;
apiService.collections.listUsers(
{
orgId: $state.params.orgId,
id: collection.id
},
function (userList) {
if (userList && userList.Data.length) {
var users = [];
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,
readOnly: userList.Data[i].ReadOnly,
accessAll: userList.Data[i].AccessAll
});
}
$scope.users = users;
}
});
});
$scope.remove = function (user) {
if (!confirm('Are you sure you want to remove this user (' + user.email + ') from this ' +
'collection (' + collection.name + ')?')) {
return;
}
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 () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@@ -0,0 +1,13 @@
angular
.module('bit.organization')
.controller('organizationDashboardController', function ($scope, authService, $state) {
$scope.$on('$viewContentLoaded', function () {
authService.getUserProfile().then(function (userProfile) {
if (!userProfile.organizations) {
return;
}
$scope.orgProfile = userProfile.organizations[$state.params.orgId];
});
});
});

View File

@@ -0,0 +1,26 @@
angular
.module('bit.organization')
.controller('organizationDeleteController', function ($scope, $state, apiService, $uibModalInstance, cryptoService,
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 () {
$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;
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

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

@@ -0,0 +1,93 @@
angular
.module('bit.organization')
.controller('organizationGroupsController', function ($scope, $state, apiService, $uibModal, $filter,
toastr, $analytics) {
$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;
});
}
});

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

@@ -0,0 +1,2 @@
angular
.module('bit.organization', ['ui.bootstrap']);

View File

@@ -0,0 +1,134 @@
angular
.module('bit.organization')
.controller('organizationPeopleController', function ($scope, $state, $uibModal, cryptoService, apiService, authService,
toastr, $analytics) {
$scope.users = [];
$scope.useGroups = 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.reinvite = function (user) {
apiService.organizationUsers.reinvite({ orgId: $state.params.orgId, id: user.id }, null, function () {
$analytics.eventTrack('Reinvited User');
toastr.success(user.email + ' has been invited again.', 'User Invited');
}, function () {
toastr.error('Unable to invite user.', 'Error');
});
};
$scope.delete = function (user) {
if (!confirm('Are you sure you want to remove this user (' + user.email + ')?')) {
return;
}
apiService.organizationUsers.del({ orgId: $state.params.orgId, id: user.id }, null, function () {
$analytics.eventTrack('Deleted User');
toastr.success(user.email + ' has been removed.', 'User Removed');
var index = $scope.users.indexOf(user);
if (index > -1) {
$scope.users.splice(index, 1);
}
}, function () {
toastr.error('Unable to remove user.', 'Error');
});
};
$scope.confirm = function (user) {
apiService.users.getPublicKey({ id: user.userId }, function (userKey) {
var orgKey = cryptoService.getOrgKey($state.params.orgId);
if (!orgKey) {
toastr.error('Unable to confirm user.', 'Error');
return;
}
var key = cryptoService.rsaEncrypt(orgKey.key, userKey.PublicKey);
apiService.organizationUsers.confirm({ orgId: $state.params.orgId, id: user.id }, { key: key }, function () {
user.status = 2;
$analytics.eventTrack('Confirmed User');
toastr.success(user.email + ' has been confirmed.', 'User Confirmed');
}, function () {
toastr.error('Unable to confirm user.', 'Error');
});
}, function () {
toastr.error('Unable to confirm user.', 'Error');
});
};
$scope.$on('organizationPeopleInvite', function (event, args) {
$scope.invite();
});
$scope.invite = function () {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationPeopleInvite.html',
controller: 'organizationPeopleInviteController'
});
modal.result.then(function () {
loadList();
});
};
$scope.edit = function (orgUser) {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationPeopleEdit.html',
controller: 'organizationPeopleEditController',
resolve: {
orgUser: function () { return orgUser; }
}
});
modal.result.then(function () {
loadList();
});
};
$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 () {
});
};
function loadList() {
apiService.organizationUsers.list({ orgId: $state.params.orgId }, function (list) {
var users = [];
for (var 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,
status: list.Data[i].Status,
type: list.Data[i].Type,
accessAll: list.Data[i].AccessAll
};
users.push(user);
}
$scope.users = users;
});
}
});

View File

@@ -0,0 +1,104 @@
angular
.module('bit.organization')
.controller('organizationPeopleEditController', function ($scope, $state, $uibModalInstance, apiService, cipherService,
orgUser, $analytics) {
$analytics.eventTrack('organizationPeopleEditController', { category: 'Modal' });
$scope.loading = true;
$scope.collections = [];
$scope.selectedCollections = {};
$uibModalInstance.opened.then(function () {
apiService.collections.listOrganization({ orgId: $state.params.orgId }, function (list) {
$scope.collections = cipherService.decryptCollections(list.Data, $state.params.orgId, true);
$scope.loading = false;
});
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.length; i++) {
collections[user.Collections[i].Id] = {
id: user.Collections[i].Id,
readOnly: user.Collections[i].ReadOnly
};
}
}
$scope.email = orgUser.email;
$scope.type = user.Type;
$scope.accessAll = user.AccessAll;
$scope.selectedCollections = collections;
});
});
$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.submitPromise = null;
$scope.submit = function (model) {
var collections = [];
if (!$scope.accessAll) {
for (var collectionId in $scope.selectedCollections) {
if ($scope.selectedCollections.hasOwnProperty(collectionId)) {
collections.push($scope.selectedCollections[collectionId]);
}
}
}
$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 () {
$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

@@ -0,0 +1,91 @@
angular
.module('bit.organization')
.controller('organizationPeopleInviteController', function ($scope, $state, $uibModalInstance, apiService, cipherService,
$analytics) {
$analytics.eventTrack('organizationPeopleInviteController', { category: 'Modal' });
$scope.loading = true;
$scope.collections = [];
$scope.selectedCollections = {};
$scope.model = {
type: 'User'
};
$uibModalInstance.opened.then(function () {
apiService.collections.listOrganization({ orgId: $state.params.orgId }, function (list) {
$scope.collections = cipherService.decryptCollections(list.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.submitPromise = null;
$scope.submit = function (model) {
var collections = [];
if (!model.accessAll) {
for (var collectionId in $scope.selectedCollections) {
if ($scope.selectedCollections.hasOwnProperty(collectionId)) {
collections.push($scope.selectedCollections[collectionId]);
}
}
}
var splitEmails = model.emails.trim().split(/\s*,\s*/);
$scope.submitPromise = apiService.organizationUsers.invite({ orgId: $state.params.orgId }, {
emails: splitEmails,
type: model.type,
collections: collections,
accessAll: model.accessAll
}, function () {
$analytics.eventTrack('Invited User');
$uibModalInstance.close();
}).$promise;
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@@ -0,0 +1,33 @@
angular
.module('bit.organization')
.controller('organizationSettingsController', function ($scope, $state, apiService, toastr, authService, $uibModal,
$analytics) {
$scope.model = {};
$scope.$on('$viewContentLoaded', function () {
apiService.organizations.get({ id: $state.params.orgId }, function (org) {
$scope.model = {
name: org.Name,
billingEmail: org.BillingEmail,
businessName: org.BusinessName
};
});
});
$scope.generalSave = function () {
$scope.generalPromise = apiService.organizations.put({ id: $state.params.orgId }, $scope.model, function (org) {
authService.updateProfileOrganization(org).then(function (updatedOrg) {
$analytics.eventTrack('Updated Organization Settings');
toastr.success('Organization has been updated.', 'Success!');
});
}).$promise;
};
$scope.delete = function () {
$uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationDelete.html',
controller: 'organizationDeleteController'
});
};
});

View File

@@ -0,0 +1,50 @@
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,176 @@
angular
.module('bit.organization')
.controller('organizationVaultController', function ($scope, apiService, cipherService, $analytics, $q, $state,
$localStorage, $uibModal, $filter) {
$scope.logins = [];
$scope.collections = [];
$scope.loading = true;
$scope.$on('$viewContentLoaded', function () {
var collectionPromise = apiService.collections.listOrganization({ orgId: $state.params.orgId }, function (collections) {
var decCollections = [{
id: null,
name: 'Unassigned',
collapsed: $localStorage.collapsedOrgCollections && 'unassigned' in $localStorage.collapsedOrgCollections
}];
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);
}
$scope.collections = decCollections;
}).$promise;
var cipherPromise = apiService.ciphers.listOrganizationDetails({ organizationId: $state.params.orgId },
function (ciphers) {
var decLogins = [];
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);
}
}
$scope.logins = decLogins;
}).$promise;
$q.all([collectionPromise, cipherPromise]).then(function () {
$scope.loading = false;
});
});
$scope.filterByCollection = function (collection) {
return function (cipher) {
if (!cipher.collectionIds || !cipher.collectionIds.length) {
return collection.id === null;
}
return cipher.collectionIds.indexOf(collection.id) > -1;
};
};
$scope.collectionSort = function (item) {
if (!item.id) {
return '';
}
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) {
var editModel = $uibModal.open({
animation: true,
templateUrl: 'app/vault/views/vaultEditLogin.html',
controller: 'organizationVaultEditLoginController',
resolve: {
loginId: function () { return login.id; }
}
});
editModel.result.then(function (returnVal) {
if (returnVal.action === 'edit') {
login.name = returnVal.data.name;
login.username = returnVal.data.username;
}
else if (returnVal.action === 'delete') {
var index = $scope.logins.indexOf(login);
if (index > -1) {
$scope.logins.splice(index, 1);
}
}
});
};
$scope.$on('organizationVaultAddLogin', function (event, args) {
$scope.addLogin();
});
$scope.addLogin = function () {
var addModel = $uibModal.open({
animation: true,
templateUrl: 'app/vault/views/vaultAddLogin.html',
controller: 'organizationVaultAddLoginController',
resolve: {
orgId: function () { return $state.params.orgId; }
}
});
addModel.result.then(function (addedLogin) {
$scope.logins.push(addedLogin);
});
};
$scope.editCollections = function (cipher) {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationVaultLoginCollections.html',
controller: 'organizationVaultLoginCollectionsController',
resolve: {
cipher: function () { return cipher; },
collections: function () { return $scope.collections; }
}
});
modal.result.then(function (response) {
if (response.collectionIds) {
cipher.collectionIds = response.collectionIds;
}
});
};
$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]);
}
}
apiService.ciphers.putCollections({ id: login.id }, request).$promise.then(function (response) {
$analytics.eventTrack('Removed Login From Collection');
login.collectionIds = request.collectionIds;
});
};
$scope.deleteLogin = function (login) {
if (!confirm('Are you sure you want to delete this login (' + login.name + ')?')) {
return;
}
apiService.ciphers.delAdmin({ id: login.id }, function () {
$analytics.eventTrack('Deleted Login');
var index = $scope.logins.indexOf(login);
if (index > -1) {
$scope.logins.splice(index, 1);
}
});
};
});

View File

@@ -0,0 +1,69 @@
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

@@ -0,0 +1,83 @@
angular
.module('bit.organization')
.controller('organizationVaultLoginCollectionsController', function ($scope, apiService, $uibModalInstance, cipherService,
cipher, $analytics, collections) {
$analytics.eventTrack('organizationVaultLoginCollectionsController', { category: 'Modal' });
$scope.cipher = {};
$scope.collections = [];
$scope.selectedCollections = {};
$uibModalInstance.opened.then(function () {
var collectionUsed = [];
for (var i = 0; i < collections.length; i++) {
if (collections[i].id) {
collectionUsed.push(collections[i]);
}
}
$scope.collections = collectionUsed;
$scope.cipher = cipher;
var selectedCollections = {};
if ($scope.cipher.collectionIds) {
for (i = 0; i < $scope.cipher.collectionIds.length; i++) {
selectedCollections[$scope.cipher.collectionIds[i]] = true;
}
}
$scope.selectedCollections = selectedCollections;
});
$scope.toggleCollectionSelectionAll = function ($event) {
var collections = {};
if ($event.target.checked) {
for (var i = 0; i < $scope.collections.length; i++) {
collections[$scope.collections[i].id] = true;
}
}
$scope.selectedCollections = collections;
};
$scope.toggleCollectionSelection = function (id) {
if (id in $scope.selectedCollections) {
delete $scope.selectedCollections[id];
}
else {
$scope.selectedCollections[id] = true;
}
};
$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 request = {
collectionIds: []
};
for (var id in $scope.selectedCollections) {
if ($scope.selectedCollections.hasOwnProperty(id)) {
request.collectionIds.push(id);
}
}
$scope.submitPromise = apiService.ciphers.putCollectionsAdmin({ id: cipher.id }, request)
.$promise.then(function (response) {
$analytics.eventTrack('Edited Login Collections');
$uibModalInstance.close({
action: 'collectionsEdit',
collectionIds: request.collectionIds
});
});
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@@ -0,0 +1,161 @@
<section class="content-header">
<h1>
Billing
<small>manage your payments</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>
The subscription to this organization has been canceled.
</div>
<div class="callout callout-warning" ng-if="subscription && subscription.markedForCancel">
<h4><i class="fa fa-warning"></i> Pending Cancellation</h4>
<p>
The subscription to this organization has been marked for cancellation at the end of the
current billing period.
</p>
<button type="button" class="btn btn-default btn-flat" ng-click="reinstate()">
Reinstate Plan
</button>
</div>
<div class="box">
<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>
<dt>Name</dt>
<dd>{{plan.name || '-'}}</dd>
<dt>Total Seats</dt>
<dd>{{plan.seats || '-'}}</dd>
</dl>
</div>
<div class="col-sm-6">
<dl>
<dt>Status</dt>
<dd style="text-transform: capitalize;">{{(subscription && subscription.status) || '-'}}</dd>
<dt>Next Charge</dt>
<dd>{{nextInvoice ? ((nextInvoice.date | date: format: mediumDate) + ', ' + (nextInvoice.amount | currency:'$')) : '-'}}</dd>
</dl>
</div>
</div>
<div class="row" ng-if="!noSubscription">
<div class="col-md-6">
<strong>Details</strong>
<div ng-show="loading">
Loading...
</div>
<div class="table-responsive" style="margin: 0;" ng-show="!loading">
<table class="table" style="margin: 0;">
<tbody>
<tr ng-repeat="item in subscription.items">
<td>
{{item.name}} {{item.qty > 1 ? '&times;' + item.qty : ''}}
@ {{item.amount | currency:'$'}} /{{item.interval}}
</td>
<td class="text-right">{{(item.qty * item.amount) | currency:'$'}} /{{item.interval}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="box-footer">
<button type="button" class="btn btn-default btn-flat" ng-click="changePlan()">
Change Plan
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="cancel()"
ng-if="!noSubscription && !subscription.cancelled && !subscription.markedForCancel">
Cancel Plan
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="reinstate()"
ng-if="!noSubscription && subscription.markedForCancel">
Reinstate Plan
</button>
</div>
</div>
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">User Seats</h3>
</div>
<div class="box-body">
<div ng-show="loading">
Loading...
</div>
<div ng-show="!loading">
You plan currently has a total of <b>{{plan.seats}}</b> seats.
</div>
</div>
<div class="box-footer" ng-if="!noSubscription">
<button type="button" class="btn btn-default btn-flat" ng-click="adjustSeats(true)">
Add Seats
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="adjustSeats(false)">
Remove Seats
</button>
</div>
</div>
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Payment Method</h3>
</div>
<div class="box-body">
<div ng-show="loading">
Loading...
</div>
<div ng-show="!loading && !paymentSource">
<i class="fa fa-credit-card"></i> No payment method on file.
</div>
<div ng-show="!loading && paymentSource">
<i class="fa" ng-class="{'fa-credit-card': paymentSource.type === 0,
'fa-university': paymentSource.type === 1}"></i>
{{paymentSource.description}}
</div>
</div>
<div class="box-footer">
<button type="button" class="btn btn-default btn-flat" ng-click="changePayment()">
{{ paymentSource ? 'Change Payment Method' : 'Add Payment Method' }}
</button>
</div>
</div>
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Charges</h3>
</div>
<div class="box-body">
<div ng-show="loading">
Loading...
</div>
<div ng-show="!loading && !charges.length">
No charges.
</div>
<div class="table-responsive" ng-show="charges.length">
<table class="table">
<tbody>
<tr ng-repeat="charge in charges">
<td style="width: 200px">
{{charge.date | date: format: mediumDate}}
</td>
<td>
{{charge.paymentSource}}
</td>
<td style="width: 150px; text-transform: capitalize;">
{{charge.status}}
</td>
<td class="text-right" style="width: 150px;">
{{charge.amount | currency:'$'}}
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="box-footer">
Note: Any charges will appears on your credit card statement as <b>BITWARDEN</b>.
</div>
</div>
</section>

View File

@@ -0,0 +1,46 @@
<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-users"></i>
{{add ? 'Add Seats' : 'Remove Seats'}}
</h4>
</div>
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise">
<div class="modal-body">
<div class="callout callout-default" ng-show="add">
<h4><i class="fa fa-dollar"></i> Note About Charges</h4>
<p>
Adding seats to your plan will result in adjustments to your billing totals and immediately charge your
payment method on file. The first charge will be prorated for the remainder of the current billing cycle.
</p>
</div>
<div class="callout callout-default" ng-show="!add">
<h4><i class="fa fa-dollar"></i> Note About Charges</h4>
<p>
Removing seats will result in adjustments to your billing totals that will be prorated as credits
to your next billing charge.
</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="row">
<div class="col-md-6">
<div class="form-group">
<label for="seats">{{add ? 'Seats To Add' : 'Seats To Remove'}}</label>
<input type="number" id="seats" name="SeatAdjustment" ng-model="seatAdjustment" class="form-control"
required min="0" />
</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

@@ -0,0 +1,364 @@
<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 occurred</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

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

View File

@@ -0,0 +1,70 @@
<section class="content-header">
<h1>
Collections
<small>control what you share</small>
</h1>
</section>
<section class="content">
<div class="box">
<div class="box-header with-border">
&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..."
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 Collection
</button>
</div>
</div>
<div class="box-body" ng-class="{'no-padding': filteredCollections.length}">
<div ng-show="loading && !collections.length">
Loading...
</div>
<div ng-show="!filteredCollections.length && filterSearch">
No collections to list.
</div>
<div ng-show="!loading && !collections.length">
<p>There are no collections yet for your organization.</p>
<button type="button" ng-click="add()" class="btn btn-default btn-flat">Add a Collection</button>
</div>
<div class="table-responsive" ng-show="collections.length">
<table class="table table-striped table-hover table-vmiddle">
<tbody>
<tr ng-repeat="collection in filteredCollections = (collections | filter: (filterSearch || '') |
orderBy: ['name']) track by collection.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="javascript:void(0)" ng-click="users(collection)">
<i class="fa fa-fw fa-users"></i> Users
</a>
</li>
<li>
<a href="javascript:void(0)" ng-click="delete(collection)" class="text-red">
<i class="fa fa-fw fa-trash"></i> Delete
</a>
</li>
</ul>
</div>
</td>
<td valign="middle">
<a href="javascript:void(0)" ng-click="edit(collection)">
{{collection.name}}
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>

View File

@@ -0,0 +1,83 @@
<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-cubes"></i> Add New Collection</h4>
</div>
<form name="form" ng-submit="form.$valid && submit(model)" api-form="submitPromise">
<div class="modal-body">
<div class="callout callout-default">
<h4><i class="fa fa-info-circle"></i> Note</h4>
<p>
After creating the collection, you can associate a user to it by selecting a specific user on the "People" page.
</p>
<p>
You can associate new logins to the collection from your organization's "Vault" or by sharing an existing
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">
<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

@@ -0,0 +1,84 @@
<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-cubes"></i> Edit Collection</h4>
</div>
<form name="form" ng-submit="form.$valid && submit(collection)" api-form="submitPromise">
<div class="modal-body">
<div class="callout callout-default">
<h4><i class="fa fa-info-circle"></i> Note</h4>
<p>
Select "Users" from the listing options to manage existing users for this collection. Associate new users by
editing the user's access on the "People" page.
</p>
<p>
You can associate new logins to the collection from your organization's "Vault" or by sharing an existing
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">
<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

@@ -0,0 +1,64 @@
<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-users"></i> User Access <small>{{collection.name}}</small></h4>
</div>
<div class="modal-body">
<div ng-show="loading && !users.length">
Loading...
</div>
<div ng-show="!loading && !users.length">
<p>
No users for this collection. You can associate a new user to this collection by
selecting a specific user on the "People" page.
</p>
</div>
<div class="table-responsive" ng-show="users.length" style="margin: 0;">
<table class="table table-striped table-hover table-vmiddle" style="margin: 0;">
<tbody>
<tr ng-repeat="user in users | orderBy: ['email']">
<td style="width: 70px;">
<div class="btn-group" data-append-to=".modal">
<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 ng-show="!user.accessAll">
<a href="javascript:void(0)" ng-click="remove(user)" class="text-red">
<i class="fa fa-fw fa-remove"></i> Remove
</a>
</li>
<li ng-show="user.accessAll">
<a href="javascript:void(0)">
No options...
</a>
</li>
</ul>
</div>
</td>
<td style="width: 45px;">
<letter-avatar data="{{user.name || user.email}}"></letter-avatar>
</td>
<td>
{{user.email}}
<div ng-if="user.name"><small class="text-muted">{{user.name}}</small></div>
</td>
<td style="width: 60px;" class="text-right">
<i class="fa fa-unlock" ng-show="user.accessAll" title="Can Access All Items"></i>
<i class="fa fa-pencil-square-o" ng-show="!user.readOnly" title="Can Edit"></i>
</td>
<td style="width: 100px;">
{{user.type | enumName: 'OrgUserType'}}
</td>
<td style="width: 120px;">
<span class="label {{user.status | enumLabelClass: 'OrgUserStatus'}}">
{{user.status | enumName: 'OrgUserStatus'}}
</span>
</td>
</tr>
</tbody>
</table>
</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,32 @@
<section class="content-header">
<h1>
Dashboard
<small>{{orgProfile.name}}</small>
</h1>
</section>
<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>
<a class="btn btn-default btn-flat" href="https://bitwarden.com/contact/" target="_blank">
Contact Us
</a>
</div>
<div class="box">
<div class="box-header with-border">
<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>
<a class="btn btn-default btn-flat" ui-sref="backend.org.people({orgId: orgProfile.id})">
Invite Users
</a>
<a class="btn btn-default btn-flat" ui-sref="backend.org.collections({orgId: orgProfile.id})">
Manage Collections
</a>
</div>
</div>
</section>

View File

@@ -0,0 +1,34 @@
<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-trash"></i> Delete Organization</h4>
</div>
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise">
<div class="modal-body">
<p>
Continue below to delete this organization and all associated data. This data includes any collections and
their associated logins. Individual user accounts will remain, though they will not be associated to this
organization anymore.
</p>
<div class="callout callout-warning">
<h4><i class="fa fa-warning"></i> Warning</h4>
Deleting this organization is permanent. It cannot be undone.
</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="masterPassword">Master Password</label>
<input type="password" id="masterPassword" name="MasterPasswordHash" ng-model="masterPassword" class="form-control"
required api-field />
</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>Delete
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
</div>
</form>

View File

@@ -0,0 +1,70 @@
<section class="content-header">
<h1>
Groups
<small>organize your users</small>
</h1>
</section>
<section class="content">
<div class="box">
<div class="box-header with-border">
&nbsp;
<div class="box-filters hidden-xs">
<div class="form-group form-group-sm has-feedback has-feedback-left">
<input type="text" id="search" class="form-control" placeholder="Search groups..."
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" 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="javascript:void(0)" ng-click="users(group)">
<i class="fa fa-fw fa-users"></i> Users
</a>
</li>
<li>
<a href="javascript:void(0)" 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="javascript:void(0)" ng-click="edit(group)">
{{group.name}}
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>

View File

@@ -0,0 +1,95 @@
<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> Add New Group</h4>
</div>
<form name="form" ng-submit="form.$valid && submit(model)" api-form="submitPromise">
<div class="modal-body">
<div class="callout callout-default">
<h4><i class="fa fa-info-circle"></i> Note</h4>
<p>
After creating the group, you can associate a user to it by selecting the "Groups" option for a specific user
on the "People" page.
</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="name">Name</label>
<input type="text" id="name" name="Name" ng-model="model.name" class="form-control" required api-field />
</div>
<div class="form-group" show-errors>
<label for="externalId">External Id</label>
<input type="text" id="externalId" name="ExternalId" ng-model="model.externalId" class="form-control" api-field />
</div>
<h4>Access</h4>
<div class="radio">
<label>
<input type="radio" ng-model="model.accessAll" name="AccessAll"
ng-value="true" ng-checked="model.accessAll">
This group can access and modify <u>all items</u>.
</label>
</div>
<div class="radio">
<label>
<input type="radio" ng-model="model.accessAll" name="AccessAll"
ng-value="false" ng-checked="!model.accessAll">
This group can access only the selected collections.
</label>
</div>
<div ng-show="!model.accessAll">
<div ng-show="loading && !collections.length">
Loading collections...
</div>
<div ng-show="!loading && !collections.length">
<p>No collections for your organization.</p>
</div>
<div class="table-responsive" ng-show="collections.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="toggleCollectionSelectionAll($event)">
</th>
<th>Name</th>
<th style="width: 100px; text-align: center;">Read Only</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="collection in collections | orderBy: ['name']">
<td valign="middle">
<input type="checkbox"
name="selectedCollections[]"
value="{{collection.id}}"
ng-checked="collectionSelected(collection)"
ng-click="toggleCollectionSelection(collection.id)">
</td>
<td valign="middle">
{{collection.name}}
</td>
<td style="width: 100px; text-align: center;" valign="middle">
<input type="checkbox"
name="selectedCollectionsReadonly[]"
value="{{collection.id}}"
ng-disabled="!collectionSelected(collection)"
ng-checked="collectionSelected(collection) && selectedCollections[collection.id].readOnly"
ng-click="toggleCollectionReadOnlySelection(collection.id)">
</td>
</tr>
</tbody>
</table>
</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

@@ -0,0 +1,95 @@
<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> Edit Group</h4>
</div>
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise">
<div class="modal-body">
<div class="callout callout-default">
<h4><i class="fa fa-info-circle"></i> Note</h4>
<p>
Select "Users" from the listing options to manage existing users for this group. Associate new users by
selecting "Groups" the "People" page for a specific user.
</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="name">Name</label>
<input type="text" id="name" name="Name" ng-model="group.name" class="form-control" required api-field />
</div>
<div class="form-group" show-errors>
<label for="externalId">External Id</label>
<input type="text" id="externalId" name="ExternalId" ng-model="group.externalId" class="form-control" api-field />
</div>
<h4>Access</h4>
<div class="radio">
<label>
<input type="radio" ng-model="group.accessAll" name="AccessAll"
ng-value="true" ng-checked="group.accessAll">
This group can access and modify <u>all items</u>.
</label>
</div>
<div class="radio">
<label>
<input type="radio" ng-model="group.accessAll" name="AccessAll"
ng-value="false" ng-checked="!group.accessAll">
This group can access only the selected collections.
</label>
</div>
<div ng-show="!group.accessAll">
<div ng-show="loading && !collections.length">
Loading collections...
</div>
<div ng-show="!loading && !collections.length">
<p>No collections for your organization.</p>
</div>
<div class="table-responsive" ng-show="collections.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="toggleCollectionSelectionAll($event)">
</th>
<th>Name</th>
<th style="width: 100px; text-align: center;">Read Only</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="collection in collections | orderBy: ['name']">
<td valign="middle">
<input type="checkbox"
name="selectedCollections[]"
value="{{collection.id}}"
ng-checked="collectionSelected(collection)"
ng-click="toggleCollectionSelection(collection.id)">
</td>
<td valign="middle">
{{collection.name}}
</td>
<td style="width: 100px; text-align: center;" valign="middle">
<input type="checkbox"
name="selectedCollectionsReadonly[]"
value="{{collection.id}}"
ng-disabled="!collectionSelected(collection)"
ng-checked="collectionSelected(collection) && selectedCollections[collection.id].readOnly"
ng-click="toggleCollectionReadOnlySelection(collection.id)">
</td>
</tr>
</tbody>
</table>
</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

@@ -0,0 +1,55 @@
<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-users"></i> User Access <small>{{group.name}}</small></h4>
</div>
<div class="modal-body">
<div ng-show="loading && !users.length">
Loading...
</div>
<div ng-show="!loading && !users.length">
<p>
No users for this group. You can associate a new user to this group by
selecting a specific user's "Groups" on the "People" page.
</p>
</div>
<div class="table-responsive" ng-show="users.length" style="margin: 0;">
<table class="table table-striped table-hover table-vmiddle" style="margin: 0;">
<tbody>
<tr ng-repeat="user in users | orderBy: ['email']">
<td style="width: 70px;">
<div class="btn-group" data-append-to=".modal">
<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 ng-show="user.organizationUserId">
<a href="javascript:void(0)" ng-click="remove(user)" class="text-red">
<i class="fa fa-fw fa-remove"></i> Remove
</a>
</li>
</ul>
</div>
</td>
<td style="width: 45px;">
<letter-avatar data="{{user.name || user.email}}"></letter-avatar>
</td>
<td>
{{user.email}}
<div ng-if="user.name"><small class="text-muted">{{user.name}}</small></div>
</td>
<td style="width: 100px;">
{{user.type | enumName: 'OrgUserType'}}
</td>
<td style="width: 120px;">
<span class="label {{user.status | enumLabelClass: 'OrgUserStatus'}}">
{{user.status | enumName: 'OrgUserStatus'}}
</span>
</td>
</tr>
</tbody>
</table>
</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,90 @@
<section class="content-header">
<h1>
People
<small>users for your organization</small>
</h1>
</section>
<section class="content">
<div class="box">
<div class="box-header with-border">
&nbsp;
<div class="box-filters hidden-xs">
<div class="form-group form-group-sm has-feedback has-feedback-left">
<input type="text" id="search" class="form-control" placeholder="Search people..."
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="invite()">
<i class="fa fa-fw fa-plus-circle"></i> Invite User
</button>
</div>
</div>
<div class="box-body" ng-class="{'no-padding': filteredUsers.length}">
<div ng-show="!filteredUsers.length && !filterSearch">
Loading...
</div>
<div class="table-responsive" ng-show="filteredUsers.length">
<table class="table table-striped table-hover table-vmiddle">
<tbody>
<tr ng-repeat="user in filteredUsers = (users | filter: (filterSearch || '') |
orderBy: ['type', 'name', 'email']) track by user.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="javascript:void(0)" ng-click="edit(user)">
<i class="fa fa-fw fa-pencil"></i> Edit
</a>
</li>
<li>
<a href="javascript:void(0)" ng-click="groups(user)" ng-if="useGroups">
<i class="fa fa-fw fa-sitemap"></i> Groups
</a>
</li>
<li ng-show="user.status === 1">
<a href="javascript:void(0)" ng-click="confirm(user)">
<i class="fa fa-fw fa-check"></i> Confirm
</a>
</li>
<li ng-show="user.status === 0">
<a href="javascript:void(0)" ng-click="reinvite(user)">
<i class="fa fa-fw fa-envelope-o"></i> Re-send Invitation
</a>
</li>
<li>
<a href="javascript:void(0)" ng-click="delete(user)" class="text-red">
<i class="fa fa-fw fa-remove"></i> Remove
</a>
</li>
</ul>
</div>
</td>
<td style="width: 45px;">
<letter-avatar data="{{user.name || user.email}}"></letter-avatar>
</td>
<td>
<a href="javascript:void(0)" ng-click="edit(user)">{{user.email}}</a>
<i class="fa fa-unlock text-muted" ng-show="user.accessAll"
title="Can Access All Items"></i>
<div ng-if="user.name"><small class="text-muted">{{user.name}}</small></div>
</td>
<td style="width: 100px;">
{{user.type | enumName: 'OrgUserType'}}
</td>
<td style="width: 120px;">
<span class="label {{user.status | enumLabelClass: 'OrgUserStatus'}}">
{{user.status | enumName: 'OrgUserStatus'}}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>

View File

@@ -0,0 +1,101 @@
<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-user"></i> Edit User <small>{{email}}</small></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 occurred</h4>
<ul>
<li ng-repeat="e in form.$errors">{{e}}</li>
</ul>
</div>
<h4>User Type</h4>
<div class="form-group">
<div class="radio">
<label>
<input type="radio" id="user-type" ng-model="type" name="Type" value="2" ng-checked="type === 2">
<strong>User</strong> - A regular user with access to your organization's collections.
</label>
</div>
<div class="radio">
<label>
<input type="radio" ng-model="type" name="Type" value="1" ng-checked="type === 1">
<strong>Admin</strong> - Admins can manage collections and users for your organization.
</label>
</div>
<div class="radio">
<label>
<input type="radio" ng-model="type" name="Type" value="0" ng-checked="type === 0">
<strong>Owner</strong> - The highest access user that can manage all aspects of your organization.
</label>
</div>
</div>
<h4>Access</h4>
<div class="radio">
<label>
<input type="radio" ng-model="accessAll" name="AccessAll"
ng-value="true" ng-checked="accessAll">
This user can access and modify <u>all items</u>.
</label>
</div>
<div class="radio">
<label>
<input type="radio" ng-model="accessAll" name="AccessAll"
ng-value="false" ng-checked="!accessAll">
This user can access only the selected collections.
</label>
</div>
<div ng-show="!accessAll">
<div ng-show="loading && !collections.length">
Loading collections...
</div>
<div ng-show="!loading && !collections.length">
<p>No collections for your organization.</p>
</div>
<div class="table-responsive" ng-show="collections.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="toggleCollectionSelectionAll($event)">
</th>
<th>Name</th>
<th style="width: 100px; text-align: center;">Read Only</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="collection in collections | orderBy: ['name']">
<td valign="middle">
<input type="checkbox"
name="selectedCollections[]"
value="{{collection.id}}"
ng-checked="collectionSelected(collection)"
ng-click="toggleCollectionSelection(collection.id)">
</td>
<td valign="middle">
{{collection.name}}
</td>
<td style="text-align: center;" valign="middle">
<input type="checkbox"
name="selectedCollectionsReadonly[]"
value="{{collection.id}}"
ng-disabled="!collectionSelected(collection)"
ng-checked="collectionSelected(collection) && selectedCollections[collection.id].readOnly"
ng-click="toggleCollectionReadOnlySelection(collection.id)">
</td>
</tr>
</tbody>
</table>
</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

@@ -0,0 +1,55 @@
<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> Edit User Groups <small>{{orgUser.email}}</small></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 occurred</h4>
<ul>
<li ng-repeat="e in form.$errors">{{e}}</li>
</ul>
</div>
<div ng-show="loading && !groups.length">
Loading...
</div>
<div ng-show="!loading && !groups.length">
<p>No groups for your organization.</p>
</div>
<p ng-show="groups.length">Edit the groups that this user belongs to.</p>
<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>
</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)">
</td>
<td valign="middle">
{{group.name}}
</td>
</tr>
</tbody>
</table>
</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

@@ -0,0 +1,110 @@
<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-user"></i> Invite User</h4>
</div>
<form name="inviteForm" ng-submit="inviteForm.$valid && submit(model)" api-form="submitPromise">
<div class="modal-body">
<p>
Invite a new user to your organization by entering their bitwarden account email address below. If they do not have
a bitwarden account already, they will be prompted to create a new account.
</p>
<div class="callout callout-danger validation-errors" ng-show="inviteForm.$errors">
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in inviteForm.$errors">{{e}}</li>
</ul>
</div>
<div class="form-group" show-errors>
<label for="emails">Email</label>
<input type="text" id="emails" name="Emails" ng-model="model.emails" class="form-control" required api-field />
<p class="help-block">You can invite up to 20 users at a time by comma separating a list of email addresses.</p>
</div>
<h4>User Type</h4>
<div class="form-group">
<div class="radio">
<label>
<input type="radio" id="user-type" ng-model="model.type" name="Type" value="User">
<strong>User</strong> - A regular user with access to your organization's collections.
</label>
</div>
<div class="radio">
<label>
<input type="radio" ng-model="model.type" name="Type" value="Admin">
<strong>Admin</strong> - Admins can manage collections and users for your organization.
</label>
</div>
<div class="radio">
<label>
<input type="radio" ng-model="model.type" name="Type" value="Owner">
<strong>Owner</strong> - The highest access user that can manage all aspects of your organization.
</label>
</div>
</div>
<h4>Access</h4>
<div class="radio">
<label>
<input type="radio" ng-model="model.accessAll" name="AccessAll"
ng-value="true" ng-checked="model.accessAll">
This user can access and modify <u>all items</u>.
</label>
</div>
<div class="radio">
<label>
<input type="radio" ng-model="model.accessAll" name="AccessAll"
ng-value="false" ng-checked="!model.accessAll">
This user can access only the selected collections.
</label>
</div>
<div ng-show="!model.accessAll">
<div ng-show="loading && !collections.length">
Loading collections...
</div>
<div ng-show="!loading && !collections.length">
<p>No collections for your organization.</p>
</div>
<div class="table-responsive" ng-show="collections.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="toggleCollectionSelectionAll($event)">
</th>
<th>Name</th>
<th style="width: 100px; text-align: center;">Read Only</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="collection in collections | orderBy: ['name'] track by collection.id">
<td style="width: 40px;" valign="middle">
<input type="checkbox"
name="selectedCollections[]"
value="{{collection.id}}"
ng-checked="collectionSelected(collection)"
ng-click="toggleCollectionSelection(collection.id)">
</td>
<td valign="middle">
{{collection.name}}
</td>
<td style="width: 100px; text-align: center;" valign="middle">
<input type="checkbox"
name="selectedCollectionsReadonly[]"
value="{{collection.id}}"
ng-disabled="!collectionSelected(collection)"
ng-checked="collectionSelected(collection) && selectedCollections[collection.id].readOnly"
ng-click="toggleCollectionReadOnlySelection(collection.id)">
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="inviteForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="inviteForm.$loading"></i>Send Invite
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
</div>
</form>

View File

@@ -0,0 +1,65 @@
<section class="content-header">
<h1>
Settings
<small>manage your organization</small>
</h1>
</section>
<section class="content">
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">General</h3>
</div>
<form role="form" name="generalForm" ng-submit="generalForm.$valid && generalSave()" api-form="generalPromise">
<div class="box-body">
<div class="row">
<div class="col-sm-9">
<div class="callout callout-danger validation-errors" ng-show="generalForm.$errors">
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in generalForm.$errors">{{e}}</li>
</ul>
</div>
<div class="form-group" show-errors>
<label for="name">Organization Name</label>
<input type="text" id="name" name="Name" ng-model="model.name" class="form-control"
required api-field />
</div>
<div class="form-group" show-errors>
<label for="name">Business Name</label>
<input type="text" id="businessName" name="BusinessName" ng-model="model.businessName"
class="form-control" api-field />
</div>
<div class="form-group" show-errors>
<label for="name">Billing Email</label>
<input type="email" id="billingEmail" name="BillingEmail" ng-model="model.billingEmail"
class="form-control" required api-field />
</div>
</div>
<div class="col-sm-3 settings-photo">
<letter-avatar data="{{model.name}}" round="false"
avclass="img-responsive img-rounded" avwidth="200" avheight="200"
fontsize="90"></letter-avatar>
</div>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="generalForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="generalForm.$loading"></i>Save
</button>
</div>
</form>
</div>
<div class="box box-danger">
<div class="box-header with-border">
<h3 class="box-title">Danger Zone</h3>
</div>
<div class="box-body">
Careful, these actions are not reversible!
</div>
<div class="box-footer">
<button type="submit" class="btn btn-default btn-flat" ng-click="delete()">
Delete Organization
</button>
</div>
</div>
</section>

View File

@@ -0,0 +1,78 @@
<section class="content-header">
<h1>
Org<span class="hidden-xs">anization</span> Vault
<small>
<span ng-pluralize
count="collections.length > 0 ? collections.length - 1 : 0"
when="{'1': '{} collection', 'other': '{} collections'}"></span>,
<span ng-pluralize count="logins.length" when="{'1': '{} login', 'other': '{} logins'}"></span>
</small>
</h1>
</section>
<section class="content">
<p ng-show="loading && !collections.length">Loading...</p>
<div class="box" ng-class="{'collapsed-box': collection.collapsed}" ng-repeat="collection in collections |
orderBy: collectionSort track by collection.id"
ng-show="collections.length && (!main.searchVaultText || collectionLogins.length)">
<div class="box-header with-border">
<h3 class="box-title">
<i class="fa" ng-class="{'fa-cubes': collection.id, 'fa-sitemap': !collection.id}"></i>
{{collection.name}}
<small ng-pluralize count="collectionLogins.length" when="{'1': '{} login', 'other': '{} logins'}"></small>
</h3>
<div class="box-tools">
<button type="button" class="btn btn-box-tool" data-widget="collapse" title="Collapse/Expand"
ng-click="collapseExpand(collection)">
<i class="fa" ng-class="{'fa-minus': !collection.collapsed, 'fa-plus': collection.collapsed}"></i>
</button>
</div>
</div>
<div class="box-body" ng-class="{'no-padding': collectionLogins.length}">
<div ng-show="!collectionLogins.length && collection.id">No logins in this collection.</div>
<div ng-show="!collectionLogins.length && !collection.id">No unassigned logins.</div>
<div class="table-responsive" ng-show="collectionLogins.length">
<table class="table table-striped table-hover table-vmiddle">
<tbody>
<tr ng-repeat="login in collectionLogins = (logins | filter: filterByCollection(collection) |
filter: (main.searchVaultText || '') | orderBy: ['name', 'username']) track by login.id">
<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="javascript:void(0)" ng-click="editLogin(login)">
<i class="fa fa-fw fa-pencil"></i> Edit
</a>
</li>
<li>
<a href="javascript:void(0)" ng-click="editCollections(login)">
<i class="fa fa-fw fa-cubes"></i> Collections
</a>
</li>
<li>
<a href="javascript:void(0)" ng-click="removeLogin(login, collection)" class="text-red"
ng-if="collection.id">
<i class="fa fa-fw fa-remove"></i> Remove
</a>
</li>
<li>
<a href="javascript:void(0)" ng-click="deleteLogin(login)" class="text-red">
<i class="fa fa-fw fa-trash"></i> Delete
</a>
</li>
</ul>
</div>
</td>
<td>
<a href="javascript:void(0)" ng-click="editLogin(login)">{{login.name}}</a>
<div class="text-sm text-muted">{{login.username}}</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>

View File

@@ -0,0 +1,52 @@
<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-cubes"></i> Collections <small>{{cipher.name}}</small></h4>
</div>
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise">
<div class="modal-body">
<p>Edit the collections that this login is being shared with.</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 ng-show="!collections.length" class="callout callout-default">
<p>There are no collections yet for your organization.</p>
</div>
<div class="table-responsive" ng-show="collections.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="toggleCollectionSelectionAll($event)">
</th>
<th>Name</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="collection in collections | orderBy: ['name'] track by collection.id">
<td valign="middle">
<input type="checkbox"
name="selectedCollections[]"
value="{{collection.id}}"
ng-checked="collectionSelected(collection)"
ng-click="toggleCollectionSelection(collection.id)">
</td>
<td valign="middle" ng-click="toggleCollectionSelection(collection.id)">
{{collection.name}}
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="form.$loading" ng-show="collections.length">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="form.$loading"></i>Save
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
</div>
</form>

View File

@@ -3,14 +3,18 @@
.factory('apiService', function ($resource, tokenService, appSettings, $httpParamSerializer) {
var _service = {},
_apiUri = appSettings.apiUri;
_apiUri = appSettings.apiUri,
_identityUri = appSettings.identityUri;
_service.logins = $resource(_apiUri + '/sites/:id', {}, {
_service.logins = $resource(_apiUri + '/logins/:id', {}, {
get: { method: 'GET', params: { id: '@id' } },
getAdmin: { url: _apiUri + '/logins/:id/admin', method: 'GET', params: { id: '@id' } },
list: { method: 'GET', params: {} },
post: { method: 'POST', params: {} },
postAdmin: { url: _apiUri + '/logins/admin', method: 'POST', params: {} },
put: { method: 'POST', params: { id: '@id' } },
del: { url: _apiUri + '/sites/:id/delete', method: 'POST', params: { id: '@id' } }
putAdmin: { url: _apiUri + '/logins/:id/admin', method: 'POST', params: { id: '@id' } },
del: { url: _apiUri + '/logins/:id/delete', method: 'POST', params: { id: '@id' } }
});
_service.folders = $resource(_apiUri + '/folders/:id', {}, {
@@ -23,10 +27,69 @@
_service.ciphers = $resource(_apiUri + '/ciphers/:id', {}, {
get: { method: 'GET', params: { id: '@id' } },
list: { method: 'GET', params: {} },
getDetails: { url: _apiUri + '/ciphers/:id/details', method: 'GET', params: { id: '@id' } },
list: { method: 'GET', params: { includeFolders: false, includeShared: true } },
listDetails: { url: _apiUri + '/ciphers/details', method: 'GET', params: {} },
listOrganizationDetails: { url: _apiUri + '/ciphers/organization-details', method: 'GET', params: {} },
'import': { url: _apiUri + '/ciphers/import', method: 'POST', params: {} },
favorite: { url: _apiUri + '/ciphers/:id/favorite', method: 'POST', params: { id: '@id' } },
del: { url: _apiUri + '/ciphers/:id/delete', method: 'POST', params: { id: '@id' } }
putPartial: { url: _apiUri + '/ciphers/:id/partial', method: 'POST', params: { id: '@id' } },
putShare: { url: _apiUri + '/ciphers/:id/share', method: 'POST', params: { id: '@id' } },
putCollections: { url: _apiUri + '/ciphers/:id/collections', method: 'POST', params: { id: '@id' } },
putCollectionsAdmin: { url: _apiUri + '/ciphers/:id/collections-admin', method: 'POST', params: { id: '@id' } },
del: { url: _apiUri + '/ciphers/:id/delete', method: 'POST', params: { id: '@id' } },
delAdmin: { url: _apiUri + '/ciphers/:id/delete-admin', method: 'POST', params: { id: '@id' } }
});
_service.organizations = $resource(_apiUri + '/organizations/:id', {}, {
get: { method: 'GET', params: { id: '@id' } },
getBilling: { url: _apiUri + '/organizations/:id/billing', method: 'GET', params: { id: '@id' } },
list: { method: 'GET', params: {} },
post: { method: 'POST', params: {} },
put: { method: 'POST', params: { id: '@id' } },
putPayment: { url: _apiUri + '/organizations/:id/payment', method: 'POST', params: { id: '@id' } },
putSeat: { url: _apiUri + '/organizations/:id/seat', method: 'POST', params: { id: '@id' } },
putUpgrade: { url: _apiUri + '/organizations/:id/upgrade', method: 'POST', params: { id: '@id' } },
putCancel: { url: _apiUri + '/organizations/:id/cancel', method: 'POST', params: { id: '@id' } },
putReinstate: { url: _apiUri + '/organizations/:id/reinstate', method: 'POST', params: { id: '@id' } },
postLeave: { url: _apiUri + '/organizations/:id/leave', method: 'POST', params: { id: '@id' } },
del: { url: _apiUri + '/organizations/:id/delete', method: 'POST', params: { id: '@id' } }
});
_service.organizationUsers = $resource(_apiUri + '/organizations/:orgId/users/:id', {}, {
get: { method: 'GET', params: { id: '@id', orgId: '@orgId' } },
list: { method: 'GET', params: { orgId: '@orgId' } },
listGroups: { url: _apiUri + '/organizations/:orgId/users/:id/groups', method: 'GET', params: { id: '@id', orgId: '@orgId' }, isArray: true },
invite: { url: _apiUri + '/organizations/:orgId/users/invite', method: 'POST', params: { orgId: '@orgId' } },
reinvite: { url: _apiUri + '/organizations/:orgId/users/:id/reinvite', method: 'POST', params: { id: '@id', orgId: '@orgId' } },
accept: { url: _apiUri + '/organizations/:orgId/users/:id/accept', method: 'POST', params: { id: '@id', orgId: '@orgId' } },
confirm: { url: _apiUri + '/organizations/:orgId/users/:id/confirm', method: 'POST', params: { id: '@id', orgId: '@orgId' } },
put: { method: 'POST', params: { id: '@id', orgId: '@orgId' } },
putGroups: { url: _apiUri + '/organizations/:orgId/users/:id/groups', method: 'POST', params: { id: '@id', orgId: '@orgId' } },
del: { url: _apiUri + '/organizations/:orgId/users/:id/delete', method: 'POST', params: { id: '@id', orgId: '@orgId' } }
});
_service.collections = $resource(_apiUri + '/organizations/:orgId/collections/:id', {}, {
get: { method: 'GET', params: { id: '@id', orgId: '@orgId' } },
getDetails: { url: _apiUri + '/organizations/:orgId/collections/:id/details', method: 'GET', params: { id: '@id', orgId: '@orgId' } },
listMe: { url: _apiUri + '/collections', method: 'GET', params: {} },
listOrganization: { method: 'GET', params: { orgId: '@orgId' } },
listUsers: { url: _apiUri + '/organizations/:orgId/collections/:id/users', method: 'GET', params: { id: '@id', orgId: '@orgId' } },
post: { method: 'POST', params: { orgId: '@orgId' } },
put: { method: 'POST', params: { id: '@id', orgId: '@orgId' } },
del: { url: _apiUri + '/organizations/:orgId/collections/:id/delete', method: 'POST', params: { id: '@id', orgId: '@orgId' } },
delUser: { url: _apiUri + '/organizations/:orgId/collections/:id/delete-user/:orgUserId', method: 'POST', params: { id: '@id', orgId: '@orgId', orgUserId: '@orgUserId' } }
});
_service.groups = $resource(_apiUri + '/organizations/:orgId/groups/:id', {}, {
get: { method: 'GET', params: { id: '@id', orgId: '@orgId' } },
getDetails: { url: _apiUri + '/organizations/:orgId/groups/:id/details', method: 'GET', params: { id: '@id', orgId: '@orgId' } },
listOrganization: { method: 'GET', params: { orgId: '@orgId' } },
listUsers: { url: _apiUri + '/organizations/:orgId/groups/:id/users', method: 'GET', params: { id: '@id', orgId: '@orgId' } },
post: { method: 'POST', params: { orgId: '@orgId' } },
put: { method: 'POST', params: { id: '@id', orgId: '@orgId' } },
del: { url: _apiUri + '/organizations/:orgId/groups/:id/delete', method: 'POST', params: { id: '@id', orgId: '@orgId' } },
delUser: { url: _apiUri + '/organizations/:orgId/groups/:id/delete-user/:orgUserId', method: 'POST', params: { id: '@id', orgId: '@orgId', orgUserId: '@orgUserId' } }
});
_service.accounts = $resource(_apiUri + '/accounts', {}, {
@@ -43,6 +106,7 @@
postTwoFactorRecover: { url: _apiUri + '/accounts/two-factor-recover', method: 'POST', params: {} },
postPasswordHint: { url: _apiUri + '/accounts/password-hint', method: 'POST', params: {} },
putSecurityStamp: { url: _apiUri + '/accounts/security-stamp', method: 'POST', params: {} },
putKeys: { url: _apiUri + '/accounts/keys', method: 'POST', params: {} },
'import': { url: _apiUri + '/accounts/import', method: 'POST', params: {} },
postDelete: { url: _apiUri + '/accounts/delete', method: 'POST', params: {} }
});
@@ -52,14 +116,13 @@
putDomains: { url: _apiUri + '/settings/domains', method: 'POST', params: {} },
});
_service.auth = $resource(_apiUri + '/auth', {}, {
token: { url: _apiUri + '/auth/token', method: 'POST', params: {} },
tokenTwoFactor: { url: _apiUri + '/auth/token/two-factor', method: 'POST', params: {} }
_service.users = $resource(_apiUri + '/users/:id', {}, {
getPublicKey: { url: _apiUri + '/users/:id/public-key', method: 'GET', params: { id: '@id' } }
});
_service.identity = $resource(_apiUri + '/connect', {}, {
_service.identity = $resource(_identityUri + '/connect', {}, {
token: {
url: _apiUri + '/connect/token',
url: _identityUri + '/connect/token',
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' },
transformRequest: transformUrlEncoded,

View File

@@ -1,7 +1,7 @@
angular
.module('bit.services')
.factory('authService', function (cryptoService, apiService, tokenService, $q, jwtHelper) {
.factory('authService', function (cryptoService, apiService, tokenService, $q, jwtHelper, $rootScope) {
var _service = {},
_userProfile = null;
@@ -25,7 +25,8 @@ angular
// TODO: device information one day?
var deferred = $q.defer();
apiService.identity.token(request, function (response) {
apiService.identity.token(request).$promise.then(function (response) {
if (!response || !response.access_token) {
return;
}
@@ -33,8 +34,31 @@ angular
tokenService.setToken(response.access_token);
tokenService.setRefreshToken(response.refresh_token);
cryptoService.setKey(key);
if (response.PrivateKey) {
cryptoService.setPrivateKey(response.PrivateKey, key);
return true;
}
else {
return cryptoService.makeKeyPair(key);
}
}).then(function (keyResults) {
if (keyResults === true) {
return;
}
cryptoService.setPrivateKey(keyResults.privateKeyEnc, key);
return apiService.accounts.putKeys({
publicKey: keyResults.publicKey,
encryptedPrivateKey: keyResults.privateKeyEnc
}).$promise;
}).then(function () {
return _service.setUserProfile();
}).then(function () {
deferred.resolve();
}, function (error) {
_service.logOut();
if (error.status === 400 && error.data.TwoFactorProviders && error.data.TwoFactorProviders.length) {
deferred.resolve(error.data.TwoFactorProviders);
}
@@ -49,45 +73,144 @@ angular
_service.logOut = function () {
tokenService.clearToken();
tokenService.clearRefreshToken();
cryptoService.clearKey();
cryptoService.clearKeys();
$rootScope.vaultFolders = $rootScope.vaultLogins = null;
_userProfile = null;
};
_service.getUserProfile = function () {
if (!_userProfile) {
_service.setUserProfile();
return _service.setUserProfile();
}
return _userProfile;
var deferred = $q.defer();
deferred.resolve(_userProfile);
return deferred.promise;
};
var _setDeferred = null;
_service.setUserProfile = function () {
if (_setDeferred && _setDeferred.promise.$$state.status === 0) {
return _setDeferred.promise;
}
_setDeferred = $q.defer();
var token = tokenService.getToken();
if (!token) {
return;
_setDeferred.reject();
return _setDeferred.promise;
}
var decodedToken = jwtHelper.decodeToken(token);
apiService.accounts.getProfile({}, function (profile) {
_userProfile = {
id: decodedToken.name,
email: decodedToken.email,
extended: {
name: profile.Name,
twoFactorEnabled: profile.TwoFactorEnabled,
culture: profile.Culture
}
};
_userProfile = {
id: decodedToken.name,
email: decodedToken.email
};
if (profile.Organizations) {
var orgs = {};
for (var i = 0; i < profile.Organizations.length; i++) {
orgs[profile.Organizations[i].Id] = {
id: profile.Organizations[i].Id,
name: profile.Organizations[i].Name,
key: profile.Organizations[i].Key,
status: profile.Organizations[i].Status,
type: profile.Organizations[i].Type,
enabled: profile.Organizations[i].Enabled,
maxCollections: profile.Organizations[i].MaxCollections,
seats: profile.Organizations[i].Seats,
useGroups: profile.Organizations[i].UseGroups
};
}
apiService.accounts.getProfile({}, loadProfile);
_userProfile.organizations = orgs;
cryptoService.setOrgKeys(orgs);
_setDeferred.resolve(_userProfile);
}
}, function () {
_setDeferred.reject();
});
return _setDeferred.promise;
};
function loadProfile(profile) {
_userProfile.extended = {
name: profile.Name,
twoFactorEnabled: profile.TwoFactorEnabled,
culture: profile.Culture
};
}
_service.addProfileOrganizationOwner = function (org, keyCt) {
return _service.getUserProfile().then(function (profile) {
if (profile) {
if (!profile.organizations) {
profile.organizations = {};
}
var o = {
id: org.Id,
name: org.Name,
key: keyCt,
status: 2, // 2 = Confirmed
type: 0, // 0 = Owner
enabled: true,
maxCollections: org.MaxCollections,
seats: org.Seats,
useGroups: org.UseGroups
};
profile.organizations[o.id] = o;
_userProfile = profile;
cryptoService.addOrgKey(o.id, o.key);
}
});
};
_service.removeProfileOrganization = function (orgId) {
return _service.getUserProfile().then(function (profile) {
if (profile) {
if (profile.organizations && profile.organizations.hasOwnProperty(orgId)) {
delete profile.organizations[orgId];
_userProfile = profile;
}
cryptoService.clearOrgKey(orgId);
}
});
};
_service.updateProfileOrganization = function (org) {
return _service.getUserProfile().then(function (profile) {
if (profile) {
if (profile.organizations && org.Id in profile.organizations) {
profile.organizations[org.Id].name = org.Name;
_userProfile = profile;
}
}
});
};
_service.isAuthenticated = function () {
return tokenService.getToken() !== null;
};
_service.refreshAccessToken = function () {
var refreshToken = tokenService.getRefreshToken();
if (!refreshToken) {
return null;
}
return apiService.identity.token({
grant_type: 'refresh_token',
client_id: 'web',
refresh_token: refreshToken
}).$promise.then(function (response) {
tokenService.setToken(response.access_token);
tokenService.setRefreshToken(response.refresh_token);
return response.access_token;
}, function (response) { });
};
return _service;
});

View File

@@ -18,24 +18,48 @@ angular
_service.decryptLogin = function (encryptedLogin) {
if (!encryptedLogin) throw "encryptedLogin is undefined or null";
var key = null;
if (encryptedLogin.OrganizationId) {
key = cryptoService.getOrgKey(encryptedLogin.OrganizationId);
}
var login = {
id: encryptedLogin.Id,
organizationId: encryptedLogin.OrganizationId,
collectionIds: encryptedLogin.CollectionIds || [],
'type': 1,
folderId: encryptedLogin.FolderId,
favorite: encryptedLogin.Favorite,
name: cryptoService.decrypt(encryptedLogin.Name),
uri: encryptedLogin.Uri && encryptedLogin.Uri !== '' ? cryptoService.decrypt(encryptedLogin.Uri) : null,
username: encryptedLogin.Username && encryptedLogin.Username !== '' ? cryptoService.decrypt(encryptedLogin.Username) : null,
password: encryptedLogin.Password && encryptedLogin.Password !== '' ? cryptoService.decrypt(encryptedLogin.Password) : null,
notes: encryptedLogin.Notes && encryptedLogin.Notes !== '' ? cryptoService.decrypt(encryptedLogin.Notes) : null
edit: encryptedLogin.Edit,
name: cryptoService.decrypt(encryptedLogin.Name, key),
uri: encryptedLogin.Uri && encryptedLogin.Uri !== '' ? cryptoService.decrypt(encryptedLogin.Uri, key) : null,
username: encryptedLogin.Username && encryptedLogin.Username !== '' ? cryptoService.decrypt(encryptedLogin.Username, key) : null,
password: encryptedLogin.Password && encryptedLogin.Password !== '' ? cryptoService.decrypt(encryptedLogin.Password, key) : null,
notes: encryptedLogin.Notes && encryptedLogin.Notes !== '' ? cryptoService.decrypt(encryptedLogin.Notes, key) : null
};
if (encryptedLogin.Folder) {
login.folder = {
name: cryptoService.decrypt(encryptedLogin.Folder.Name)
};
return login;
};
_service.decryptLoginPreview = function (encryptedCipher) {
if (!encryptedCipher) throw "encryptedCipher is undefined or null";
var key = null;
if (encryptedCipher.OrganizationId) {
key = cryptoService.getOrgKey(encryptedCipher.OrganizationId);
}
var login = {
id: encryptedCipher.Id,
organizationId: encryptedCipher.OrganizationId,
collectionIds: encryptedCipher.CollectionIds || [],
folderId: encryptedCipher.FolderId,
favorite: encryptedCipher.Favorite,
edit: encryptedCipher.Edit,
name: _service.decryptProperty(encryptedCipher.Data.Name, key, false),
username: _service.decryptProperty(encryptedCipher.Data.Username, key, true)
};
return login;
};
@@ -55,11 +79,59 @@ angular
return {
id: encryptedFolder.Id,
'type': 0,
name: cryptoService.decrypt(encryptedFolder.Name)
};
};
_service.decryptFolderPreview = function (encryptedFolder) {
if (!encryptedFolder) throw "encryptedFolder is undefined or null";
return {
id: encryptedFolder.Id,
name: _service.decryptProperty(encryptedFolder.Name, null, false)
};
};
_service.decryptCollections = function (encryptedCollections, orgId, catchError) {
if (!encryptedCollections) throw "encryptedCollections is undefined or null";
var unencryptedCollections = [];
for (var i = 0; i < encryptedCollections.length; i++) {
unencryptedCollections.push(_service.decryptCollection(encryptedCollections[i], orgId, catchError));
}
return unencryptedCollections;
};
_service.decryptCollection = function (encryptedCollection, orgId, catchError) {
if (!encryptedCollection) throw "encryptedCollection is undefined or null";
catchError = catchError === true ? true : false;
orgId = orgId || encryptedCollection.OrganizationId;
var key = cryptoService.getOrgKey(orgId);
return {
id: encryptedCollection.Id,
name: catchError ? _service.decryptProperty(encryptedCollection.Name, key, false) :
cryptoService.decrypt(encryptedCollection.Name, key)
};
};
_service.decryptProperty = function (property, key, checkEmpty) {
if (checkEmpty && (!property || property === '')) {
return null;
}
try {
property = cryptoService.decrypt(property, key);
}
catch (err) {
property = null;
}
return property || '[error: cannot decrypt]';
};
_service.encryptLogins = function (unencryptedLogins, key) {
if (!unencryptedLogins) throw "unencryptedLogins is undefined or null";
@@ -74,9 +146,14 @@ angular
_service.encryptLogin = function (unencryptedLogin, key) {
if (!unencryptedLogin) throw "unencryptedLogin is undefined or null";
if (unencryptedLogin.organizationId) {
key = key || cryptoService.getOrgKey(unencryptedLogin.organizationId);
}
return {
id: unencryptedLogin.id,
'type': 1,
organizationId: unencryptedLogin.organizationId || null,
folderId: unencryptedLogin.folderId === '' ? null : unencryptedLogin.folderId,
favorite: unencryptedLogin.favorite !== null ? unencryptedLogin.favorite : false,
uri: !unencryptedLogin.uri || unencryptedLogin.uri === '' ? null : cryptoService.encrypt(unencryptedLogin.uri, key),
@@ -103,10 +180,29 @@ angular
return {
id: unencryptedFolder.id,
'type': 0,
name: cryptoService.encrypt(unencryptedFolder.name, key)
};
};
_service.encryptCollections = function (unencryptedCollections, orgId) {
if (!unencryptedCollections) throw "unencryptedCollections is undefined or null";
var encryptedCollections = [];
for (var i = 0; i < unencryptedCollections.length; i++) {
encryptedCollections.push(_service.encryptCollection(unencryptedCollections[i], orgId));
}
return encryptedCollections;
};
_service.encryptCollection = function (unencryptedCollection, orgId) {
if (!unencryptedCollection) throw "unencryptedCollection is undefined or null";
return {
id: unencryptedCollection.id,
name: cryptoService.encrypt(unencryptedCollection.name, cryptoService.getOrgKey(orgId))
};
};
return _service;
});

View File

@@ -1,63 +1,243 @@
angular
.module('bit.services')
.factory('cryptoService', function ($sessionStorage) {
.factory('cryptoService', function ($sessionStorage, constants, $q) {
var _service = {},
_key,
_b64Key,
_aes,
_aesWithMac;
sjcl.beware["CBC mode is dangerous because it doesn't protect message integrity."]();
_legacyEtmKey,
_orgKeys,
_privateKey,
_publicKey;
_service.setKey = function (key) {
_key = key;
$sessionStorage.key = sjcl.codec.base64.fromBits(key);
$sessionStorage.key = _key.keyB64;
};
_service.getKey = function (b64) {
if (b64 && b64 === true && _b64Key) {
return _b64Key;
_service.setPrivateKey = function (privateKeyCt, key) {
try {
var privateKeyBytes = _service.decrypt(privateKeyCt, key, 'raw');
$sessionStorage.privateKey = forge.util.encode64(privateKeyBytes);
_privateKey = forge.pki.privateKeyFromAsn1(forge.asn1.fromDer(privateKeyBytes));
}
else if (!b64 && _key) {
return _key;
catch (e) {
console.log('Cannot set private key. Decryption failed.');
}
};
_service.setOrgKeys = function (orgKeysCt, privateKey) {
if (!orgKeysCt || Object.keys(orgKeysCt).length === 0) {
return;
}
if ($sessionStorage.key) {
_key = sjcl.codec.base64.toBits($sessionStorage.key);
_service.clearOrgKeys();
var orgKeysb64 = {},
_orgKeys = {},
setKey = false;
for (var orgId in orgKeysCt) {
if (orgKeysCt.hasOwnProperty(orgId)) {
try {
var decBytes = _service.rsaDecrypt(orgKeysCt[orgId].key, privateKey);
var decKey = new SymmetricCryptoKey(decBytes);
_orgKeys[orgId] = decKey;
orgKeysb64[orgId] = decKey.keyB64;
setKey = true;
}
catch (e) {
console.log('Cannot set org key ' + i + '. Decryption failed.');
}
}
}
if (b64 && b64 === true) {
_b64Key = sjcl.codec.base64.fromBits(_key);
return _b64Key;
if (setKey) {
$sessionStorage.orgKeys = orgKeysb64;
}
else {
_orgKeys = null;
}
};
_service.addOrgKey = function (orgId, encOrgKey, privateKey) {
_orgKeys = _service.getOrgKeys();
if (!_orgKeys) {
_orgKeys = {};
}
var orgKeysb64 = $sessionStorage.orgKeys;
if (!orgKeysb64) {
orgKeysb64 = {};
}
try {
var decBytes = _service.rsaDecrypt(encOrgKey, privateKey);
var decKey = new SymmetricCryptoKey(decBytes);
_orgKeys[orgId] = decKey;
orgKeysb64[orgId] = decKey.keyB64;
}
catch (e) {
_orgKeys = null;
console.log('Cannot set org key. Decryption failed.');
}
$sessionStorage.orgKeys = orgKeysb64;
};
_service.getKey = function () {
if (!_key && $sessionStorage.key) {
_key = new SymmetricCryptoKey($sessionStorage.key, true);
}
if (!_key) {
throw 'key unavailable';
}
return _key;
};
_service.getEncKey = function (key) {
key = key || _service.getKey();
return key.slice(0, 4);
_service.getPrivateKey = function (outputEncoding) {
outputEncoding = outputEncoding || 'native';
if (_privateKey) {
if (outputEncoding === 'raw') {
var privateKeyAsn1 = forge.pki.privateKeyToAsn1(_privateKey);
var privateKeyPkcs8 = forge.pki.wrapRsaPrivateKey(privateKeyAsn1);
return forge.asn1.toDer(privateKeyPkcs8).getBytes();
}
return _privateKey;
}
if ($sessionStorage.privateKey) {
var privateKeyBytes = forge.util.decode64($sessionStorage.privateKey);
_privateKey = forge.pki.privateKeyFromAsn1(forge.asn1.fromDer(privateKeyBytes));
if (outputEncoding === 'raw') {
return privateKeyBytes;
}
}
return _privateKey;
};
_service.getMacKey = function (key) {
key = key || _service.getKey();
return key.slice(4);
_service.getPublicKey = function () {
if (_publicKey) {
return _publicKey;
}
var privateKey = _service.getPrivateKey();
if (!privateKey) {
return null;
}
_publicKey = forge.pki.setRsaPublicKey(privateKey.n, privateKey.e);
return _publicKey;
};
_service.getOrgKeys = function () {
if (_orgKeys) {
return _orgKeys;
}
if ($sessionStorage.orgKeys) {
var orgKeys = {},
setKey = false;
for (var orgId in $sessionStorage.orgKeys) {
if ($sessionStorage.orgKeys.hasOwnProperty(orgId)) {
orgKeys[orgId] = new SymmetricCryptoKey($sessionStorage.orgKeys[orgId], true);
setKey = true;
}
}
if (setKey) {
_orgKeys = orgKeys;
}
}
return _orgKeys;
};
_service.getOrgKey = function (orgId) {
var orgKeys = _service.getOrgKeys();
if (!orgKeys || !(orgId in orgKeys)) {
return null;
}
return orgKeys[orgId];
};
_service.clearKey = function () {
_key = _b64Key = _aes = _aesWithMac = null;
_key = null;
_legacyEtmKey = null;
delete $sessionStorage.key;
};
_service.makeKey = function (password, salt, b64) {
var key = sjcl.misc.pbkdf2(password, salt, 5000, 256, null);
_service.clearKeyPair = function () {
_privateKey = null;
_publicKey = null;
delete $sessionStorage.privateKey;
};
if (b64 && b64 === true) {
return sjcl.codec.base64.fromBits(key);
_service.clearOrgKeys = function () {
_orgKeys = null;
delete $sessionStorage.orgKeys;
};
_service.clearOrgKey = function (orgId) {
if (_orgKeys.hasOwnProperty(orgId)) {
delete _orgKeys[orgId];
}
return key;
if ($sessionStorage.orgKeys.hasOwnProperty(orgId)) {
delete $sessionStorage.orgKeys[orgId];
}
};
_service.clearKeys = function () {
_service.clearKey();
_service.clearKeyPair();
_service.clearOrgKeys();
};
_service.makeKey = function (password, salt) {
var keyBytes = forge.pbkdf2(forge.util.encodeUtf8(password), forge.util.encodeUtf8(salt),
5000, 256 / 8, 'sha256');
return new SymmetricCryptoKey(keyBytes);
};
_service.makeKeyPair = function (key) {
var deferred = $q.defer();
forge.pki.rsa.generateKeyPair({
bits: 2048,
workers: 2,
workerScript: '/lib/forge/prime.worker.min.js'
}, function (error, keypair) {
if (error) {
deferred.reject(error);
return;
}
var privateKeyAsn1 = forge.pki.privateKeyToAsn1(keypair.privateKey);
var privateKeyPkcs8 = forge.pki.wrapRsaPrivateKey(privateKeyAsn1);
var privateKeyBytes = forge.asn1.toDer(privateKeyPkcs8).getBytes();
var privateKeyEncCt = _service.encrypt(privateKeyBytes, key, 'raw');
var publicKeyAsn1 = forge.pki.publicKeyToAsn1(keypair.publicKey);
var publicKeyBytes = forge.asn1.toDer(publicKeyAsn1).getBytes();
deferred.resolve({
publicKey: forge.util.encode64(publicKeyBytes),
privateKeyEnc: privateKeyEncCt
});
});
return deferred.promise;
};
_service.makeShareKeyCt = function () {
return _service.rsaEncrypt(forge.random.getBytesSync(512 / 8));
};
_service.hashPassword = function (password, key) {
@@ -69,102 +249,255 @@ angular
throw 'Invalid parameters.';
}
var hashBits = sjcl.misc.pbkdf2(key, password, 1, 256, null);
return sjcl.codec.base64.fromBits(hashBits);
var hashBits = forge.pbkdf2(key.key, forge.util.encodeUtf8(password), 1, 256 / 8, 'sha256');
return forge.util.encode64(hashBits);
};
_service.getAes = function () {
if (!_aes && _service.getKey()) {
_aes = new sjcl.cipher.aes(_service.getKey());
}
_service.encrypt = function (plainValue, key, plainValueEncoding) {
key = key || _service.getKey();
return _aes;
};
_service.getAesWithMac = function () {
if (!_aesWithMac && _service.getKey()) {
_aesWithMac = new sjcl.cipher.aes(_service.getEncKey());
}
return _aesWithMac;
};
_service.encrypt = function (plaintextValue, key) {
if (!_service.getKey() && !key) {
if (!key) {
throw 'Encryption key unavailable.';
}
// TODO: Turn on whenever ready to support encrypt-then-mac
var encKey = null;
if (false) {
encKey = _service.getEncKey(key);
}
else {
encKey = key || _service.getKey();
}
var response = {};
var params = {
mode: 'cbc',
iv: sjcl.random.randomWords(4, 10)
};
var ctJson = sjcl.encrypt(encKey, plaintextValue, params, response);
var ct = ctJson.match(/"ct":"([^"]*)"/)[1];
var iv = sjcl.codec.base64.fromBits(response.iv);
plainValueEncoding = plainValueEncoding || 'utf8';
var buffer = forge.util.createBuffer(plainValue, plainValueEncoding);
var ivBytes = forge.random.getBytesSync(16);
var cipher = forge.cipher.createCipher('AES-CBC', key.encKey);
cipher.start({ iv: ivBytes });
cipher.update(buffer);
cipher.finish();
var iv = forge.util.encode64(ivBytes);
var ctBytes = cipher.output.getBytes();
var ct = forge.util.encode64(ctBytes);
var cipherString = iv + '|' + ct;
// TODO: Turn on whenever ready to support encrypt-then-mac
if (false) {
var mac = computeMac(ct, response.iv);
if (key.macKey) {
var mac = computeMac(ctBytes, ivBytes, key.macKey, true);
cipherString = cipherString + '|' + mac;
}
return cipherString;
if (key.encType === constants.encType.AesCbc256_B64) {
return cipherString;
}
return key.encType + '.' + cipherString;
};
_service.decrypt = function (encValue) {
if (!_service.getAes() || !_service.getAesWithMac()) {
throw 'AES encryption unavailable.';
_service.rsaEncrypt = function (plainValue, publicKey) {
publicKey = publicKey || _service.getPublicKey();
if (!publicKey) {
throw 'Public key unavailable.';
}
var encPieces = encValue.split('|');
if (encPieces.length !== 2 && encPieces.length !== 3) {
return '';
if (typeof publicKey === 'string') {
var publicKeyBytes = forge.util.decode64(publicKey);
publicKey = forge.pki.publicKeyFromAsn1(forge.asn1.fromDer(publicKeyBytes));
}
var ivBits = sjcl.codec.base64.toBits(encPieces[0]);
var ctBits = sjcl.codec.base64.toBits(encPieces[1]);
var encryptedBytes = publicKey.encrypt(plainValue, 'RSA-OAEP', {
md: forge.md.sha1.create()
});
var computedMac = null;
if (encPieces.length === 3) {
computedMac = computeMac(ctBits, ivBits);
if (computedMac !== encPieces[2]) {
return constants.encType.Rsa2048_OaepSha1_B64 + '.' + forge.util.encode64(encryptedBytes);
};
_service.decrypt = function (encValue, key, outputEncoding) {
key = key || _service.getKey();
var headerPieces = encValue.split('.'),
encType,
encPieces;
if (headerPieces.length === 2) {
try {
encType = parseInt(headerPieces[0]);
encPieces = headerPieces[1].split('|');
}
catch (e) {
return null;
}
}
else {
encPieces = encValue.split('|');
encType = encPieces.length === 3 ? constants.encType.AesCbc128_HmacSha256_B64 :
constants.encType.AesCbc256_B64;
}
if (encType === constants.encType.AesCbc128_HmacSha256_B64 && key.encType === constants.encType.AesCbc256_B64) {
// Old encrypt-then-mac scheme, swap out the key
_legacyEtmKey = _legacyEtmKey ||
new SymmetricCryptoKey(key.key, false, constants.encType.AesCbc128_HmacSha256_B64);
key = _legacyEtmKey;
}
if (encType !== key.encType) {
throw 'encType unavailable.';
}
switch (encType) {
case constants.encType.AesCbc128_HmacSha256_B64:
if (encPieces.length !== 3) {
return null;
}
break;
case constants.encType.AesCbc256_HmacSha256_B64:
if (encPieces.length !== 3) {
return null;
}
break;
case constants.encType.AesCbc256_B64:
if (encPieces.length !== 2) {
return null;
}
break;
default:
return null;
}
var ivBytes = forge.util.decode64(encPieces[0]);
var ctBytes = forge.util.decode64(encPieces[1]);
if (key.macKey && encPieces.length > 2) {
var macBytes = forge.util.decode64(encPieces[2]);
var computedMacBytes = computeMac(ctBytes, ivBytes, key.macKey, false);
if (!macsEqual(key.macKey, macBytes, computedMacBytes)) {
console.error('MAC failed.');
return '';
return null;
}
}
var decBits = sjcl.mode.cbc.decrypt(computedMac ? _service.getAesWithMac() : _service.getAes(), ctBits, ivBits, null);
return sjcl.codec.utf8String.fromBits(decBits);
var ctBuffer = forge.util.createBuffer(ctBytes);
var decipher = forge.cipher.createDecipher('AES-CBC', key.encKey);
decipher.start({ iv: ivBytes });
decipher.update(ctBuffer);
decipher.finish();
outputEncoding = outputEncoding || 'utf8';
if (outputEncoding === 'utf8') {
return decipher.output.toString('utf8');
}
else {
return decipher.output.getBytes();
}
};
function computeMac(ct, iv) {
if (typeof ct === 'string') {
ct = sjcl.codec.base64.toBits(ct);
}
if (typeof iv === 'string') {
iv = sjcl.codec.base64.toBits(iv);
_service.rsaDecrypt = function (encValue, privateKey) {
privateKey = privateKey || _service.getPrivateKey();
if (!privateKey) {
throw 'Private key unavailable.';
}
var macKey = _service.getMacKey();
var hmac = new sjcl.misc.hmac(macKey, sjcl.hash.sha256);
var bits = iv.concat(ct);
var mac = hmac.encrypt(bits);
return sjcl.codec.base64.fromBits(mac);
var headerPieces = encValue.split('.'),
encType,
encPiece;
if (headerPieces.length === 1) {
encType = constants.encType.Rsa2048_OaepSha256_B64;
encPiece = headerPieces[0];
}
else if (headerPieces.length === 2) {
try {
encType = parseInt(headerPieces[0]);
encPiece = headerPieces[1];
}
catch (e) {
return null;
}
}
var ctBytes = forge.util.decode64(encPiece);
var md;
if (encType === constants.encType.Rsa2048_OaepSha256_B64) {
md = forge.md.sha256.create();
}
else if (encType === constants.encType.Rsa2048_OaepSha1_B64) {
md = forge.md.sha1.create();
}
else {
throw 'encType unavailable.';
}
var decBytes = privateKey.decrypt(ctBytes, 'RSA-OAEP', {
md: md
});
return decBytes;
};
function computeMac(ct, iv, macKey, b64Output) {
var hmac = forge.hmac.create();
hmac.start('sha256', macKey);
hmac.update(iv + ct);
var mac = hmac.digest();
return b64Output ? forge.util.encode64(mac.getBytes()) : mac.getBytes();
}
// Safely compare two MACs in a way that protects against timing attacks (Double HMAC Verification).
// ref: https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/
function macsEqual(macKey, mac1, mac2) {
var hmac = forge.hmac.create();
hmac.start('sha256', macKey);
hmac.update(mac1);
mac1 = hmac.digest().getBytes();
hmac.start(null, null);
hmac.update(mac2);
mac2 = hmac.digest().getBytes();
return mac1 === mac2;
}
function SymmetricCryptoKey(keyBytes, b64KeyBytes, encType) {
if (b64KeyBytes) {
keyBytes = forge.util.decode64(keyBytes);
}
if (!keyBytes) {
throw 'Must provide keyBytes';
}
var buffer = forge.util.createBuffer(keyBytes);
if (!buffer || buffer.length() === 0) {
throw 'Couldn\'t make buffer';
}
var bufferLength = buffer.length();
if (encType === null || encType === undefined) {
if (bufferLength === 32) {
encType = constants.encType.AesCbc256_B64;
}
else if (bufferLength === 64) {
encType = constants.encType.AesCbc256_HmacSha256_B64;
}
else {
throw 'Unable to determine encType.';
}
}
this.key = keyBytes;
this.keyB64 = forge.util.encode64(keyBytes);
this.encType = encType;
if (encType === constants.encType.AesCbc256_B64 && bufferLength === 32) {
this.encKey = keyBytes;
this.macKey = null;
}
else if (encType === constants.encType.AesCbc128_HmacSha256_B64 && bufferLength === 32) {
this.encKey = buffer.getBytes(16); // first half
this.macKey = buffer.getBytes(16); // second half
}
else if (encType === constants.encType.AesCbc256_HmacSha256_B64 && bufferLength === 64) {
this.encKey = buffer.getBytes(32); // first half
this.macKey = buffer.getBytes(32); // second half
}
else {
throw 'Unsupported encType/key length.';
}
}
return _service;
});
});

View File

@@ -36,6 +36,8 @@
import1Password6WinCsv(file, success, error);
break;
case 'chromecsv':
case 'vivaldicsv':
case 'operacsv':
importChromeCsv(file, success, error);
break;
case 'firefoxpasswordexportercsvxml':
@@ -65,8 +67,8 @@
case 'msecurecsv':
importmSecureCsv(file, success, error);
break;
case 'truekeyjson':
importTrueKeyJson(file, success, error);
case 'truekeycsv':
importTrueKeyCsv(file, success, error);
break;
case 'clipperzhtml':
importClipperzHtml(file, success, error);
@@ -92,6 +94,12 @@
case 'splashidcsv':
importSplashIdCsv(file, success, error);
break;
case 'meldiumcsv':
importMeldiumCsv(file, success, error);
break;
case 'passkeepcsv':
importPassKeepCsv(file, success, error);
break;
default:
error();
break;
@@ -182,6 +190,28 @@
}
}
function getFileContents(file, contentsCallback, errorCallback) {
if (typeof file === 'string') {
contentsCallback(file);
}
else {
var reader = new FileReader();
reader.readAsText(file, 'utf-8');
reader.onload = function (evt) {
contentsCallback(evt.target.result);
};
reader.onerror = function (evt) {
errorCallback();
};
}
}
function getXmlFileContents(file, xmlCallback, errorCallback) {
getFileContents(file, function (fileContents) {
xmlCallback($.parseXML(fileContents));
}, errorCallback);
}
function importBitwardenCsv(file, success, error) {
Papa.parse(file, {
header: true,
@@ -210,7 +240,7 @@
}
logins.push({
favorite: value.favorite !== null ? value.favorite : false,
favorite: value.favorite && value.favorite !== '' && value.favorite !== '0' ? true : false,
uri: value.uri && value.uri !== '' ? trimUri(value.uri) : null,
username: value.username && value.username !== '' ? value.username : null,
password: value.password && value.password !== '' ? value.password : null,
@@ -239,7 +269,7 @@
}
function importLastPass(file, success, error) {
if (file.type === 'text/html') {
if (typeof file !== 'string' && file.type && file.type === 'text/html') {
var reader = new FileReader();
reader.readAsText(file, 'utf-8');
reader.onload = function (evt) {
@@ -287,6 +317,9 @@
complete: function (results) {
parseCsvErrors(results);
parseData(results.data);
},
beforeFirstChunk: function (chunk) {
return chunk.replace(/^\s+/, '');
}
});
}
@@ -344,16 +377,14 @@
var folders = [],
logins = [],
loginRelationships = [],
foldersIndex = [];
var i = 0,
foldersIndex = [],
i = 0,
j = 0;
var reader = new FileReader();
reader.readAsText(file, 'utf-8');
reader.onload = function (evt) {
var xmlDoc = $.parseXML(evt.target.result),
xml = $(xmlDoc);
getXmlFileContents(file, parse, error);
function parse(xmlDoc) {
var xml = $(xmlDoc);
var db = xml.find('database');
if (db.length) {
@@ -381,7 +412,7 @@
uri: null,
username: null,
password: null,
notes: null,
notes: '',
name: card.attr('title'),
};
@@ -391,6 +422,7 @@
var text = field.text();
var type = field.attr('type');
var name = field.attr('name');
if (text && text !== '') {
if (type === 'login') {
@@ -400,14 +432,26 @@
login.password = text;
}
else if (type === 'notes') {
login.notes = text;
login.notes += (text + '\n');
}
else if (type === 'weblogin') {
else if (type === 'weblogin' || type === 'website') {
login.uri = trimUri(text);
}
else {
login.notes += (name + ': ' + text + '\n');
}
}
}
var notes = card.find('> notes');
for (j = 0; j < notes.length; j++) {
login.notes += ($(notes[j]).text() + '\n');
}
if (login.notes === '') {
login.notes = null;
}
logins.push(login);
labels = card.find('> label_id');
@@ -429,11 +473,7 @@
else {
error();
}
};
reader.onerror = function (evt) {
error();
};
}
}
function importPadlockCsv(file, success, error) {
@@ -535,11 +575,10 @@
logins = [],
loginRelationships = [];
var reader = new FileReader();
reader.readAsText(file, 'utf-8');
reader.onload = function (evt) {
var xmlDoc = $.parseXML(evt.target.result),
xml = $(xmlDoc);
getXmlFileContents(file, parse, error);
function parse(xmlDoc) {
var xml = $(xmlDoc);
var root = xml.find('Root');
if (root.length) {
@@ -552,11 +591,7 @@
else {
error();
}
};
reader.onerror = function (evt) {
error();
};
}
function traverse(node, isRootNode, groupNamePrefix) {
var nodeEntries = [];
@@ -745,10 +780,9 @@
}
}
var reader = new FileReader();
reader.readAsText(file, 'utf-8');
reader.onload = function (evt) {
var fileContent = evt.target.result;
getFileContents(file, parse, error);
function parse(fileContent) {
var fileLines = fileContent.split(/(?:\r\n|\r|\n)/);
for (i = 0; i < fileLines.length; i++) {
@@ -790,11 +824,7 @@
}
success(folders, logins, loginRelationships);
};
reader.onerror = function (evt) {
error();
};
}
}
function import1Password6WinCsv(file, success, error) {
@@ -922,40 +952,35 @@
return name;
}
if (file.type === 'text/xml') {
var reader = new FileReader();
reader.readAsText(file, 'utf-8');
reader.onload = function (evt) {
var xmlDoc = $.parseXML(evt.target.result),
xml = $(xmlDoc);
function parseXml(xmlDoc) {
var xml = $(xmlDoc);
var entries = xml.find('entry');
for (var i = 0; i < entries.length; i++) {
var entry = $(entries[i]);
if (!entry) {
continue;
}
var host = entry.attr('host'),
user = entry.attr('user'),
password = entry.attr('password');
logins.push({
favorite: false,
uri: host && host !== '' ? trimUri(host) : null,
username: user && user !== '' ? user : null,
password: password && password !== '' ? password : null,
notes: null,
name: getNameFromHost(host),
});
var entries = xml.find('entry');
for (var i = 0; i < entries.length; i++) {
var entry = $(entries[i]);
if (!entry) {
continue;
}
success(folders, logins, loginRelationships);
};
var host = entry.attr('host'),
user = entry.attr('user'),
password = entry.attr('password');
reader.onerror = function (evt) {
error();
};
logins.push({
favorite: false,
uri: host && host !== '' ? trimUri(host) : null,
username: user && user !== '' ? user : null,
password: password && password !== '' ? password : null,
notes: null,
name: getNameFromHost(host),
});
}
success(folders, logins, loginRelationships);
}
if (file.type && file.type === 'text/xml') {
getXmlFileContents(file, parseXml, error);
}
else {
// currently bugged due to the comment
@@ -1100,11 +1125,10 @@
foldersIndex = [],
j = 0;
var reader = new FileReader();
reader.readAsText(file, 'utf-8');
reader.onload = function (evt) {
var xmlDoc = $.parseXML(evt.target.result),
xml = $(xmlDoc);
getXmlFileContents(file, parseXml, error);
function parseXml(xmlDoc) {
var xml = $(xmlDoc);
var pwManager = xml.find('PasswordManager');
if (pwManager.length) {
@@ -1205,11 +1229,7 @@
else {
error();
}
};
reader.onerror = function (evt) {
error();
};
}
}
function importEnpassCsv(file, success, error) {
@@ -1283,11 +1303,10 @@
foldersIndex = [],
j = 0;
var reader = new FileReader();
reader.readAsText(file, 'utf-8');
reader.onload = function (evt) {
var xmlDoc = $.parseXML(evt.target.result),
xml = $(xmlDoc);
getXmlFileContents(file, parseXml, error);
function parseXml(xmlDoc) {
var xml = $(xmlDoc);
var pwsafe = xml.find('passwordsafe');
if (pwsafe.length) {
@@ -1371,11 +1390,7 @@
else {
error();
}
};
reader.onerror = function (evt) {
error();
};
}
}
function importDashlaneCsv(file, success, error) {
@@ -1430,15 +1445,16 @@
else if (row.length === 6) {
if (row[2] === '') {
login.username = row[3];
login.password = row[4];
login.notes = row[5];
}
else {
login.username = row[2];
login.notes = row[3] + '\n' + row[5];
login.password = row[3];
login.notes = row[4] + '\n' + row[5];
}
login.uri = fixUri(row[1]);
login.password = row[4];
}
else if (row.length === 7) {
if (row[2] === '') {
@@ -1509,11 +1525,10 @@
return groupText;
}
var reader = new FileReader();
reader.readAsText(file, 'utf-8');
reader.onload = function (evt) {
var xmlDoc = $.parseXML(evt.target.result),
xml = $(xmlDoc);
getXmlFileContents(file, parseXml, error);
function parseXml(xmlDoc) {
var xml = $(xmlDoc);
var database = xml.find('root > Database');
if (database.length) {
@@ -1599,11 +1614,7 @@
else {
error();
}
};
reader.onerror = function (evt) {
error();
};
}
}
function importmSecureCsv(file, success, error) {
@@ -1692,84 +1703,71 @@
});
}
function importTrueKeyJson(file, success, error) {
function importTrueKeyCsv(file, success, error) {
var folders = [],
logins = [],
loginRelationships = [],
i = 0;
propsToIgnore = [
'kind',
'autologin',
'favorite',
'hexcolor',
'protectedwithpassword',
'subdomainonly',
'type',
'tk_export_version',
'note',
'title',
'document_content'
];
var reader = new FileReader();
reader.readAsText(file, 'utf-8');
reader.onload = function (evt) {
var fileContent = evt.target.result;
var fileJson = JSON.parse(fileContent);
if (fileJson) {
if (fileJson.logins) {
for (i = 0; i < fileJson.logins.length; i++) {
var login = fileJson.logins[i];
logins.push({
favorite: login.favorite && login.favorite === true,
uri: login.url && login.url !== '' ? trimUri(login.url) : null,
username: login.login && login.login !== '' ? login.login : null,
password: login.password && login.password !== '' ? login.password : null,
notes: null,
name: login.name && login.name !== '' ? login.name : '--',
});
}
}
Papa.parse(file, {
header: true,
encoding: 'UTF-8',
complete: function (results) {
parseCsvErrors(results);
if (fileJson.documents) {
for (i = 0; i < fileJson.documents.length; i++) {
var doc = fileJson.documents[i];
var note = {
favorite: false,
uri: null,
username: null,
password: null,
notes: '',
name: doc.title && doc.title !== '' ? doc.title : '--',
};
angular.forEach(results.data, function (value, key) {
var login = {
favorite: value.favorite && value.favorite.toLowerCase() === 'true' ? true : false,
uri: value.url && value.url !== '' ? trimUri(value.url) : null,
username: value.login && value.login !== '' ? value.login : null,
password: value.password && value.password !== '' ? value.password : null,
notes: value.memo && value.memo !== '' ? value.memo : null,
name: value.name && value.name !== '' ? value.name : '--'
};
if (doc.kind === 'note') {
if (!doc.document_content || doc.document_content === '') {
continue;
}
note.notes = doc.document_content;
if (value.kind !== 'login') {
login.name = value.title && value.title !== '' ? value.title : '--';
login.notes = value.note && value.note !== '' ? value.note : null;
if (!login.notes) {
login.notes = value.document_content && value.document_content !== '' ?
value.document_content : null;
}
else {
for (var property in doc) {
if (doc.hasOwnProperty(property)) {
if (property === 'title' || property === 'hexColor' || property === 'kind' ||
doc[property] === '' || doc[property] === null) {
continue;
}
if (note.notes !== '') {
note.notes = note.notes + '\n';
}
note.notes = note.notes + property + ': ' + doc[property];
for (var property in value) {
if (value.hasOwnProperty(property) && propsToIgnore.indexOf(property.toLowerCase()) < 0 &&
value[property] && value[property] !== '') {
if (!login.notes) {
login.notes = '';
}
else {
login.notes += '\n';
}
// other custom fields
login.notes += (property + ': ' + value[property]);
}
}
if (!note.notes || note.notes === '') {
continue;
}
else {
note.notes = note.notes.split('\\n').join('\n');
}
logins.push(note);
}
}
logins.push(login);
});
success(folders, logins, loginRelationships);
}
success(folders, logins, loginRelationships);
};
reader.onerror = function (evt) {
error();
};
});
}
function importClipperzHtml(file, success, error) {
@@ -1777,10 +1775,10 @@
logins = [],
loginRelationships = [];
var reader = new FileReader();
reader.readAsText(file, 'utf-8');
reader.onload = function (evt) {
var doc = $(evt.target.result);
getFileContents(file, parse, error);
function parse(fileContents) {
var doc = $(fileContents);
var textarea = doc.find('textarea');
var json = textarea && textarea.length ? textarea.val() : null;
var entries = json ? JSON.parse(json) : null;
@@ -1849,11 +1847,7 @@
}
success(folders, logins, loginRelationships);
};
reader.onerror = function (evt) {
error();
};
}
}
function importAviraJson(file, success, error) {
@@ -1862,10 +1856,9 @@
loginRelationships = [],
i = 0;
var reader = new FileReader();
reader.readAsText(file, 'utf-8');
reader.onload = function (evt) {
var fileContent = evt.target.result;
getFileContents(file, parseJson, error);
function parseJson(fileContent) {
var fileJson = JSON.parse(fileContent);
if (fileJson) {
if (fileJson.accounts) {
@@ -1899,11 +1892,7 @@
}
success(folders, logins, loginRelationships);
};
reader.onerror = function (evt) {
error();
};
}
}
function importRoboFormHtml(file, success, error) {
@@ -1911,10 +1900,10 @@
logins = [],
loginRelationships = [];
var reader = new FileReader();
reader.readAsText(file, 'utf-8');
reader.onload = function (evt) {
var doc = $(evt.target.result.split('&shy;').join('').split('<WBR>').join(''));
getFileContents(file, parse, error);
function parse(fileContents) {
var doc = $(fileContents.split('&shy;').join('').split('<WBR>').join(''));
var outterTables = doc.find('table.nobr');
if (outterTables.length) {
for (var i = 0; i < outterTables.length; i++) {
@@ -1975,11 +1964,7 @@
}
success(folders, logins, loginRelationships);
};
reader.onerror = function (evt) {
error();
};
}
}
function importSaferPassCsv(file, success, error) {
@@ -2088,10 +2073,9 @@
loginRelationships = [],
i = 0;
var reader = new FileReader();
reader.readAsText(file, 'utf-8');
reader.onload = function (evt) {
var fileContent = evt.target.result;
getFileContents(file, parseJson, error);
function parseJson(fileContent) {
var fileJson = JSON.parse(fileContent);
if (fileJson && fileJson.length) {
for (i = 0; i < fileJson.length; i++) {
@@ -2146,11 +2130,7 @@
}
success(folders, logins, loginRelationships);
};
reader.onerror = function (evt) {
error();
};
}
}
function importZohoVaultCsv(file, success, error) {
@@ -2364,5 +2344,114 @@
});
}
function importMeldiumCsv(file, success, error) {
Papa.parse(file, {
header: true,
encoding: 'UTF-8',
complete: function (results) {
parseCsvErrors(results);
var folders = [],
logins = [],
loginRelationships = [];
for (var j = 0; j < results.data.length; j++) {
var row = results.data[j];
var login = {
name: row.DisplayName && row.DisplayName !== '' ? row.DisplayName : '--',
favorite: false,
uri: row.Url && row.Url !== '' ? fixUri(row.Url) : null,
password: row.Password && row.Password !== '' ? row.Password : null,
username: row.UserName && row.UserName !== '' ? row.UserName : null,
notes: row.Notes && row.Notes !== '' ? row.Notes : null
};
logins.push(login);
}
success(folders, logins, loginRelationships);
}
});
}
function importPassKeepCsv(file, success, error) {
function getValue(key, obj) {
var val = obj[key] || obj[(' ' + key)];
if (val && val !== '') {
return val;
}
return null;
}
Papa.parse(file, {
header: true,
encoding: 'UTF-8',
complete: function (results) {
parseCsvErrors(results);
var folders = [],
logins = [],
folderRelationships = [];
angular.forEach(results.data, function (value, key) {
var folderIndex = folders.length,
loginIndex = logins.length,
hasFolder = !!getValue('category', value),
addFolder = hasFolder,
i = 0;
if (hasFolder) {
for (i = 0; i < folders.length; i++) {
if (folders[i].name === getValue('category', value)) {
addFolder = false;
folderIndex = i;
break;
}
}
}
var login = {
favorite: false,
uri: !!getValue('site', value) ? fixUri(getValue('site', value)) : null,
username: !!getValue('username', value) ? getValue('username', value) : null,
password: !!getValue('password', value) ? getValue('password', value) : null,
notes: !!getValue('description', value) ? getValue('description', value) : null,
name: !!getValue('title', value) ? getValue('title', value) : '--'
};
if (!!getValue('password2', value)) {
if (!login.notes) {
login.notes = '';
}
else {
login.notes += '\n';
}
login.notes += ('Password 2: ' + getValue('password2', value));
}
logins.push(login);
if (addFolder) {
folders.push({
name: getValue('category', value)
});
}
if (hasFolder) {
var relationship = {
key: loginIndex,
value: folderIndex
};
folderRelationships.push(relationship);
}
});
success(folders, logins, folderRelationships);
}
});
}
return _service;
});

View File

@@ -6,7 +6,7 @@
_service.addErrors = function (form, reason) {
var data = reason.data;
var defaultErrorMessage = 'An unexpected error has occured.';
var defaultErrorMessage = 'An unexpected error has occurred.';
form.$errors = [];
if (!data || !angular.isObject(data)) {

View File

@@ -1,2 +1,2 @@
angular.module("bit")
.constant("appSettings", {"rememberedEmailCookieName":"bit.rememberedEmail","apiUri":"http://localhost:4000","version":"1.8.0","environment":"Development"});
.constant("appSettings", {"apiUri":"https://api.bitwarden.com","identityUri":"https://identity.bitwarden.com","stripeKey":"pk_live_bpN0P37nMxrMQkcaHXtAybJk","version":"1.11.0","environment":"Production"});

View File

@@ -1,7 +1,8 @@
angular
.module('bit.vault')
.controller('settingsAddEditEquivalentDomainController', function ($scope, $uibModalInstance, $analytics, domainIndex, domains) {
.controller('settingsAddEditEquivalentDomainController', function ($scope, $uibModalInstance, $analytics,
domainIndex, domains) {
$analytics.eventTrack('settingsAddEditEquivalentDomainController', { category: 'Modal' });
$scope.domains = domains;

View File

@@ -1,7 +1,8 @@
angular
.module('bit.settings')
.controller('settingsChangeEmailController', function ($scope, $state, apiService, $uibModalInstance, cryptoService, cipherService, authService, $q, toastr, $analytics) {
.controller('settingsChangeEmailController', function ($scope, $state, apiService, $uibModalInstance, cryptoService,
cipherService, authService, $q, toastr, $analytics) {
$analytics.eventTrack('settingsChangeEmailController', { category: 'Modal' });
var _masterPasswordHash,
_newMasterPasswordHash,
@@ -28,24 +29,43 @@
$scope.processing = true;
var reencryptedLogins = [];
var loginsPromise = apiService.logins.list({ dirty: false }, function (encryptedLogins) {
var unencryptedLogins = cipherService.decryptLogins(encryptedLogins.Data);
var loginsPromise = apiService.logins.list({}, function (encryptedLogins) {
var filteredEncryptedLogins = [];
for (var i = 0; i < encryptedLogins.Data.length; i++) {
if (encryptedLogins.Data[i].OrganizationId) {
continue;
}
filteredEncryptedLogins.push(encryptedLogins.Data[i]);
}
var unencryptedLogins = cipherService.decryptLogins(filteredEncryptedLogins);
reencryptedLogins = cipherService.encryptLogins(unencryptedLogins, _newKey);
}).$promise;
var reencryptedFolders = [];
var foldersPromise = apiService.folders.list({ dirty: false }, function (encryptedFolders) {
var foldersPromise = apiService.folders.list({}, function (encryptedFolders) {
var unencryptedFolders = cipherService.decryptFolders(encryptedFolders.Data);
reencryptedFolders = cipherService.encryptFolders(unencryptedFolders, _newKey);
}).$promise;
var privateKey = cryptoService.getPrivateKey('raw'),
reencryptedPrivateKey = null;
if (privateKey) {
reencryptedPrivateKey = cryptoService.encrypt(privateKey, _newKey, 'raw');
}
$q.all([loginsPromise, foldersPromise]).then(function () {
var request = {
token: model.token,
newEmail: model.newEmail.toLowerCase(),
masterPasswordHash: _masterPasswordHash,
newMasterPasswordHash: _newMasterPasswordHash,
ciphers: reencryptedLogins.concat(reencryptedFolders)
data: {
ciphers: reencryptedLogins,
folders: reencryptedFolders,
privateKey: reencryptedPrivateKey
}
};
$scope.confirmPromise = apiService.accounts.email(request, function () {
@@ -56,7 +76,6 @@
toastr.success('Please log back in.', 'Email Changed');
});
}, function () {
// TODO: recovery mode
$uibModalInstance.dismiss('cancel');
toastr.error('Something went wrong.', 'Oh No!');
}).$promise;

View File

@@ -24,40 +24,60 @@
$scope.processing = true;
var profile = authService.getUserProfile();
var newKey = cryptoService.makeKey(model.newMasterPassword, profile.email.toLowerCase());
authService.getUserProfile().then(function (profile) {
return cryptoService.makeKey(model.newMasterPassword, profile.email.toLowerCase());
}).then(function (newKey) {
var reencryptedLogins = [];
var loginsPromise = apiService.logins.list({}, function (encryptedLogins) {
var filteredEncryptedLogins = [];
for (var i = 0; i < encryptedLogins.Data.length; i++) {
if (encryptedLogins.Data[i].OrganizationId) {
continue;
}
var reencryptedLogins = [];
var loginsPromise = apiService.logins.list({ dirty: false }, function (encryptedLogins) {
var unencryptedLogins = cipherService.decryptLogins(encryptedLogins.Data);
reencryptedLogins = cipherService.encryptLogins(unencryptedLogins, newKey);
}).$promise;
filteredEncryptedLogins.push(encryptedLogins.Data[i]);
}
var reencryptedFolders = [];
var foldersPromise = apiService.folders.list({ dirty: false }, function (encryptedFolders) {
var unencryptedFolders = cipherService.decryptFolders(encryptedFolders.Data);
reencryptedFolders = cipherService.encryptFolders(unencryptedFolders, newKey);
}).$promise;
$q.all([loginsPromise, foldersPromise]).then(function () {
var request = {
masterPasswordHash: cryptoService.hashPassword(model.masterPassword),
newMasterPasswordHash: cryptoService.hashPassword(model.newMasterPassword, newKey),
ciphers: reencryptedLogins.concat(reencryptedFolders)
};
$scope.savePromise = apiService.accounts.putPassword(request, function () {
$uibModalInstance.dismiss('cancel');
authService.logOut();
$analytics.eventTrack('Changed Password');
$state.go('frontend.login.info').then(function () {
toastr.success('Please log back in.', 'Master Password Changed');
});
}, function () {
// TODO: recovery mode
$uibModalInstance.dismiss('cancel');
toastr.error('Something went wrong.', 'Oh No!');
var unencryptedLogins = cipherService.decryptLogins(filteredEncryptedLogins);
reencryptedLogins = cipherService.encryptLogins(unencryptedLogins, newKey);
}).$promise;
var reencryptedFolders = [];
var foldersPromise = apiService.folders.list({}, function (encryptedFolders) {
var unencryptedFolders = cipherService.decryptFolders(encryptedFolders.Data);
reencryptedFolders = cipherService.encryptFolders(unencryptedFolders, newKey);
}).$promise;
var privateKey = cryptoService.getPrivateKey('raw'),
reencryptedPrivateKey = null;
if (privateKey) {
reencryptedPrivateKey = cryptoService.encrypt(privateKey, newKey, 'raw');
}
$q.all([loginsPromise, foldersPromise]).then(function () {
var request = {
masterPasswordHash: cryptoService.hashPassword(model.masterPassword),
newMasterPasswordHash: cryptoService.hashPassword(model.newMasterPassword, newKey),
data: {
ciphers: reencryptedLogins,
folders: reencryptedFolders,
privateKey: reencryptedPrivateKey
}
};
$scope.savePromise = apiService.accounts.putPassword(request, function () {
$uibModalInstance.dismiss('cancel');
authService.logOut();
$analytics.eventTrack('Changed Password');
$state.go('frontend.login.info').then(function () {
toastr.success('Please log back in.', 'Master Password Changed');
});
}, function () {
$uibModalInstance.dismiss('cancel');
toastr.error('Something went wrong.', 'Oh No!');
}).$promise;
});
});
};

View File

@@ -1,23 +1,60 @@
angular
.module('bit.settings')
.controller('settingsController', function ($scope, $uibModal, apiService, toastr, authService) {
$scope.model = {};
.controller('settingsController', function ($scope, $state, $uibModal, apiService, toastr, authService) {
$scope.model = {
profile: {},
twoFactorEnabled: false,
email: null
};
apiService.accounts.getProfile({}, function (user) {
$scope.model = {
name: user.Name,
email: user.Email,
masterPasswordHint: user.MasterPasswordHint,
culture: user.Culture,
twoFactorEnabled: user.TwoFactorEnabled
};
$scope.$on('$viewContentLoaded', function () {
apiService.accounts.getProfile({}, function (user) {
$scope.model = {
profile: {
name: user.Name,
masterPasswordHint: user.MasterPasswordHint,
culture: user.Culture
},
email: user.Email,
twoFactorEnabled: user.TwoFactorEnabled
};
if (user.Organizations) {
var orgs = [];
for (var i = 0; i < user.Organizations.length; i++) {
// Only confirmed
if (user.Organizations[i].Status !== 2) {
continue;
}
orgs.push({
id: user.Organizations[i].Id,
name: user.Organizations[i].Name,
status: user.Organizations[i].Status,
type: user.Organizations[i].Type,
enabled: user.Organizations[i].Enabled
});
}
$scope.model.organizations = orgs;
}
});
});
$scope.save = function (model) {
$scope.savePromise = apiService.accounts.putProfile({}, model, function (profile) {
authService.setUserProfile(profile);
toastr.success('Account has been updated.', 'Success!');
$scope.generalSave = function () {
$scope.generalPromise = apiService.accounts.putProfile({}, $scope.model.profile, function (profile) {
authService.setUserProfile(profile).then(function (updatedProfile) {
toastr.success('Account has been updated.', 'Success!');
});
}).$promise;
};
$scope.passwordHintSave = function () {
$scope.passwordHintPromise = apiService.accounts.putProfile({}, $scope.model.profile, function (profile) {
authService.setUserProfile(profile).then(function (updatedProfile) {
toastr.success('Account has been updated.', 'Success!');
});
}).$promise;
};
@@ -29,34 +66,60 @@
});
};
$scope.$on('settingsChangePassword', function (event, args) {
$scope.changePassword();
});
$scope.changeEmail = function () {
$uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsChangeEmail.html',
controller: 'settingsChangeEmailController',
size: 'sm'
controller: 'settingsChangeEmailController'
});
};
$scope.$on('settingsChangeEmail', function (event, args) {
$scope.changeEmail();
});
$scope.viewOrganization = function (org) {
if (org.type === 2) { // 2 = User
scrollToTop();
toastr.error('You cannot manage this organization.');
return;
}
$state.go('backend.org.dashboard', { orgId: org.id });
};
$scope.leaveOrganization = function (org) {
if (!confirm('Are you sure you want to leave this organization (' + org.name + ')?')) {
return;
}
apiService.organizations.postLeave({ id: org.id }, {}, function (response) {
authService.refreshAccessToken().then(function () {
var index = $scope.model.organizations.indexOf(org);
if (index > -1) {
$scope.model.organizations.splice(index, 1);
}
toastr.success('You have left the organization.');
scrollToTop();
});
}, function (error) {
toastr.error('Unable to leave this organization.');
scrollToTop();
});
};
$scope.twoFactor = function () {
$uibModal.open({
var twoFactorModal = $uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsTwoFactor.html',
controller: 'settingsTwoFactorController'
});
};
$scope.$on('settingsTwoFactor', function (event, args) {
$scope.twoFactor();
});
twoFactorModal.result.then(function (enabled) {
if (enabled === null) {
return;
}
$scope.model.twoFactorEnabled = enabled;
});
};
$scope.sessions = function () {
$uibModal.open({
@@ -66,32 +129,15 @@
});
};
$scope.$on('settingsSessions', function (event, args) {
$scope.sessions();
});
$scope.domains = function () {
$uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsDomains.html',
controller: 'settingsDomainsController'
});
};
$scope.$on('settingsDomains', function (event, args) {
$scope.domains();
});
$scope.delete = function () {
$uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsDelete.html',
controller: 'settingsDeleteController',
size: 'sm'
controller: 'settingsDeleteController'
});
};
$scope.$on('settingsDelete', function (event, args) {
$scope.delete();
});
function scrollToTop() {
$('html, body').animate({ scrollTop: 0 }, 200);
}
});

View File

@@ -0,0 +1,92 @@
angular
.module('bit.settings')
.controller('settingsCreateOrganizationController', function ($scope, $state, apiService, cryptoService,
toastr, $analytics, authService, stripe, constants) {
$scope.plans = constants.plans;
$scope.model = {
plan: 'free',
additionalSeats: 0,
interval: 'year',
ownedBusiness: false
};
$scope.totalPrice = function () {
if ($scope.model.interval === 'month') {
return ($scope.model.additionalSeats || 0) * ($scope.plans[$scope.model.plan].monthlySeatPrice || 0) +
($scope.plans[$scope.model.plan].monthlyBasePrice || 0);
}
else {
return ($scope.model.additionalSeats || 0) * ($scope.plans[$scope.model.plan].annualSeatPrice || 0) +
($scope.plans[$scope.model.plan].annualBasePrice || 0);
}
};
$scope.changedPlan = function () {
if ($scope.plans[$scope.model.plan].hasOwnProperty('monthPlanType')) {
$scope.model.interval = 'year';
}
if ($scope.plans[$scope.model.plan].noAdditionalSeats) {
$scope.model.additionalSeats = 0;
}
else if (!$scope.model.additionalSeats && !$scope.plans[$scope.model.plan].baseSeats &&
!$scope.plans[$scope.model.plan].noAdditionalSeats) {
$scope.model.additionalSeats = 1;
}
};
$scope.changedBusiness = function () {
if ($scope.model.ownedBusiness) {
$scope.model.plan = 'teams';
}
};
$scope.submit = function (model) {
var shareKeyCt = cryptoService.makeShareKeyCt();
if (model.plan === 'free') {
var freeRequest = {
name: model.name,
planType: model.plan,
key: shareKeyCt,
billingEmail: model.billingEmail
};
$scope.submitPromise = apiService.organizations.post(freeRequest).$promise.then(finalizeCreate);
}
else {
$scope.submitPromise = stripe.card.createToken(model.card).then(function (response) {
var paidRequest = {
name: model.name,
planType: model.interval === 'month' ? $scope.plans[model.plan].monthPlanType :
$scope.plans[model.plan].annualPlanType,
key: shareKeyCt,
paymentToken: response.id,
additionalSeats: model.additionalSeats,
billingEmail: model.billingEmail,
businessName: model.ownedBusiness ? model.businessName : null
};
return apiService.organizations.post(paidRequest).$promise;
}).then(finalizeCreate);
}
function finalizeCreate(result) {
$analytics.eventTrack('Created Organization');
authService.addProfileOrganizationOwner(result, shareKeyCt);
authService.refreshAccessToken().then(function () {
goToOrg(result.Id);
}, function () {
goToOrg(result.Id);
});
}
function goToOrg(id) {
$state.go('backend.org.dashboard', { orgId: id }).then(function () {
toastr.success('Your new organization is ready to go!', 'Organization Created');
});
}
};
});

View File

@@ -1,7 +1,8 @@
angular
.module('bit.settings')
.controller('settingsDeleteController', function ($scope, $state, apiService, $uibModalInstance, cryptoService, authService, toastr, $analytics) {
.controller('settingsDeleteController', function ($scope, $state, apiService, $uibModalInstance, cryptoService,
authService, toastr, $analytics) {
$analytics.eventTrack('settingsDeleteController', { category: 'Modal' });
$scope.submit = function (model) {
var request = {

View File

@@ -1,9 +1,7 @@
angular
.module('bit.settings')
.controller('settingsDomainsController', function ($scope, $state, apiService, $uibModalInstance, toastr, $analytics, $uibModal) {
$analytics.eventTrack('settingsDomainsController', { category: 'Modal' });
.controller('settingsDomainsController', function ($scope, $state, apiService, toastr, $analytics, $uibModal) {
$scope.globalEquivalentDomains = [];
$scope.equivalentDomains = [];
@@ -37,6 +35,7 @@
$scope.delete = function (i) {
$scope.equivalentDomains.splice(i, 1);
$scope.$emit('removeAppendedDropdownMenu');
};
$scope.addEdit = function (i) {
@@ -44,7 +43,6 @@
animation: true,
templateUrl: 'app/settings/views/settingsAddEditEquivalentDomain.html',
controller: 'settingsAddEditEquivalentDomainController',
size: 'sm',
resolve: {
domainIndex: function () { return i; },
domains: function () { return i !== null ? $scope.equivalentDomains[i] : null; }
@@ -65,7 +63,15 @@
});
};
$scope.save = function () {
$scope.saveGlobal = function () {
$scope.globalPromise = save();
};
$scope.saveCustom = function () {
$scope.customPromise = save();
};
var save = function () {
var request = {
ExcludedGlobalEquivalentDomains: [],
EquivalentDomains: []
@@ -89,13 +95,9 @@
request.ExcludedGlobalEquivalentDomains = null;
}
$scope.submitPromise = apiService.settings.putDomains(request, function (domains) {
$scope.close();
return apiService.settings.putDomains(request, function (domains) {
$analytics.eventTrack('Saved Equivalent Domains');
toastr.success('Domains have been updated.', 'Success!');
}).$promise;
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@@ -1,7 +1,8 @@
angular
.module('bit.settings')
.controller('settingsSessionsController', function ($scope, $state, apiService, $uibModalInstance, cryptoService, authService, toastr, $analytics) {
.controller('settingsSessionsController', function ($scope, $state, apiService, $uibModalInstance, cryptoService,
authService, toastr, $analytics) {
$analytics.eventTrack('settingsSessionsController', { category: 'Modal' });
$scope.submit = function (model) {
var request = {

View File

@@ -1,16 +1,20 @@
angular
.module('bit.settings')
.controller('settingsTwoFactorController', function ($scope, apiService, $uibModalInstance, cryptoService, authService, $q, toastr, $analytics) {
.controller('settingsTwoFactorController', function ($scope, apiService, $uibModalInstance, cryptoService, authService,
$q, toastr, $analytics) {
$analytics.eventTrack('settingsTwoFactorController', { category: 'Modal' });
var _issuer = 'bitwarden',
_profile = authService.getUserProfile(),
_profile = null,
_masterPasswordHash;
$scope.account = _profile.email;
$scope.enabled = function () {
return _profile.extended && _profile.extended.twoFactorEnabled;
};
authService.getUserProfile().then(function (profile) {
_profile = profile;
$scope.account = _profile.email;
$scope.enabled = function () {
return _profile.extended && _profile.extended.twoFactorEnabled;
};
});
$scope.auth = function (model) {
_masterPasswordHash = cryptoService.hashPassword(model.masterPassword);
@@ -38,9 +42,9 @@
key: formatString(key),
recovery: formatString(response.TwoFactorRecoveryCode),
qr: 'https://chart.googleapis.com/chart?chs=120x120&chld=L|0&cht=qr&chl=otpauth://totp/' +
_issuer + ':' + encodeURIComponent(_profile.email) +
'%3Fsecret=' + encodeURIComponent(key) +
'%26issuer=' + _issuer
_issuer + ':' + encodeURIComponent(_profile.email) +
'%3Fsecret=' + encodeURIComponent(key) +
'%26issuer=' + _issuer
};
}
@@ -74,7 +78,16 @@
}).$promise;
};
$scope.print = function (printContent) {
$analytics.eventTrack('Print Recovery Code');
var w = window.open();
w.document.write('<div style="font-size: 18px; text-align: center;"><p>bitwarden two-step login recovery code:</p>' +
'<pre>' + printContent + '</pre>');
w.print();
w.close();
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
$uibModalInstance.close(!_profile.extended ? null : _profile.extended.twoFactorEnabled);
};
});

View File

@@ -9,52 +9,152 @@
<div class="box-header with-border">
<h3 class="box-title">General</h3>
</div>
<form role="form" name="profileForm" ng-submit="profileForm.$valid && save(model)" api-form="savePromise">
<form role="form" name="generalForm" ng-submit="generalForm.$valid && generalSave()" api-form="generalPromise">
<div class="box-body">
<div class="row">
<div class="col-sm-9">
<div class="callout callout-danger validation-errors" ng-show="profileForm.$errors">
<h4>Errors have occured</h4>
<div class="callout callout-danger validation-errors" ng-show="generalForm.$errors">
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in profileForm.$errors">{{e}}</li>
<li ng-repeat="e in generalForm.$errors">{{e}}</li>
</ul>
</div>
<div class="form-group" show-errors>
<label for="name">Name</label>
<input type="text" id="name" name="Name" ng-model="model.name" class="form-control"
<input type="text" id="name" name="Name" ng-model="model.profile.name" class="form-control"
required api-field />
</div>
<div class="form-group">
<label for="email">Email - <a href="javascript:void(0)" ng-click="changeEmail()">change</a></label>
<input type="text" id="email" ng-model="model.email" class="form-control" readonly />
</div>
<div class="form-group" show-errors>
<label for="hint">Master Password Hint</label>
<input type="text" id="hint" name="MasterPasswordHint" ng-model="model.masterPasswordHint"
class="form-control" api-field />
</div>
<div class="form-group" show-errors>
<label for="culture">Language/Culture</label>
<select id="culture" name="Culture" ng-model="model.culture" class="form-control" api-field>
<select id="culture" name="Culture" ng-model="model.profile.culture" class="form-control" api-field>
<option value="en-US">English (US)</option>
</select>
</div>
</div>
<div class="col-sm-3 settings-photo">
<a href="http://www.gravatar.com/" target="_blank">
<img src="//www.gravatar.com/avatar/{{ main.userProfile.email | gravatar }}.jpg?s=150&d=mm"
class="img-rounded img-responsive" alt="User Image">
</a>
<a href="http://www.gravatar.com/" target="_blank" class="btn btn-link"
analytics-on="click" analytics-event="Clicked Update Photo">Update Photo</a>
<letter-avatar data="{{model.profile.name || model.email}}" round="false"
avclass="img-responsive img-rounded" avwidth="200" avheight="200"
fontsize="90"></letter-avatar>
</div>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="profileForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="profileForm.$loading"></i>Save
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="generalForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="generalForm.$loading"></i>Save
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="changeEmail()">
Change Email
</button>
</div>
</form>
</div>
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Master Password</h3>
</div>
<form role="form" name="masterPasswordForm" ng-submit="masterPasswordForm.$valid && passwordHintSave()"
api-form="passwordHintPromise">
<div class="box-body">
<div class="row">
<div class="col-sm-9">
<div class="callout callout-danger validation-errors" ng-show="masterPasswordForm.$errors">
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in masterPasswordForm.$errors">{{e}}</li>
</ul>
</div>
<div class="form-group" show-errors>
<label for="hint">Master Password Hint</label>
<input type="text" id="hint" name="MasterPasswordHint" ng-model="model.profile.masterPasswordHint"
class="form-control" api-field />
</div>
</div>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="masterPasswordForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="masterPasswordForm.$loading"></i>Save
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="changePassword()">
Change Master Password
</button>
</div>
</form>
</div>
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Two-step Log In</h3>
</div>
<div class="box-body">
<p>
Current Status:
<span class="label bg-green" ng-show="model.twoFactorEnabled">ENABLED</span>
<span class="label bg-gray" ng-show="!model.twoFactorEnabled">DISABLED</span>
</p>
<p>
Two-step login helps keep your account more secure by requiring a code provided by an app on your mobile device
while logging in (in addition to your master password).
</p>
</div>
<div class="box-footer">
<button type="button" class="btn btn-default btn-flat" ng-click="twoFactor()">
Manage Two-step Log In
</button>
</div>
</div>
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Organizations</h3>
</div>
<div class="box-body" ng-if="!model.organizations || !model.organizations.length">
No organizations yet for your account.
</div>
<div class="list-group" ng-if="model.organizations && model.organizations.length">
<div class="list-group-item" ng-repeat="org in model.organizations | orderBy: ['name']">
<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="javascript:void(0)" ng-click="leaveOrganization(org)" class="text-red">
<i class="fa fa-fw fa-sign-out"></i> Leave
</a>
</li>
</ul>
</div>
<a href="javascript:void(0)" ng-click="viewOrganization(org)">
<letter-avatar data="{{org.name}}" round="false" avwidth="25" avheight="25"
avclass="img-rounded" fontsize="10"></letter-avatar>
{{org.name}}
<span class="label bg-gray" ng-if="!org.enabled">DISABLED</span>
</a>
</div>
</div>
<div class="box-footer">
<a ui-sref="backend.user.settingsCreateOrg" class="btn btn-default btn-flat">
Create an Organization
</a>
</div>
</div>
<div class="box box-danger">
<div class="box-header with-border">
<h3 class="box-title">Danger Zone</h3>
</div>
<div class="box-body">
Careful, these actions are not reversible!
</div>
<div class="box-footer">
<button type="submit" class="btn btn-default btn-flat" ng-click="sessions()">
Deauthorize Sessions
</button>
<button type="submit" class="btn btn-default btn-flat" ng-click="delete()">
Delete Account
</button>
</div>
</div>
</section>

View File

@@ -5,7 +5,7 @@
<form name="domainAddEditForm" ng-submit="domainAddEditForm.$valid && submit(domainAddEditForm)">
<div class="modal-body">
<div class="callout callout-danger validation-errors" ng-show="domainAddEditForm.$errors">
<h4>Errors have occured</h4>
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in domainAddEditForm.$errors">{{e}}</li>
</ul>
@@ -17,10 +17,12 @@
<label for="name">Domains</label> <span>*</span>
<textarea id="domains" name="Domains" ng-model="domains" class="form-control" placeholder="ex. google.com, gmail.com"
style="height: 100px;" required></textarea>
<p class="help-block">
Only "base" domains are allowed. Do not enter subdomains.
For example, enter "google.com" instead of "www.google.com".
Only "base" domains are allowed. Do not enter subdomains. For example, enter "google.com" instead of
"www.google.com".
</p>
<p class="help-block">
You can also enter "androidapp://package.name" to associate an android app with other website domains.
</p>
</div>
</div>

View File

@@ -6,7 +6,7 @@
<div class="modal-body">
<p>Below you can change your account's email address.</p>
<div class="callout callout-danger validation-errors" ng-show="changeEmailForm.$errors">
<h4>Errors have occured</h4>
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in changeEmailForm.$errors">{{e}}</li>
</ul>

View File

@@ -11,7 +11,7 @@
Proceeding will log you out of your current session, requiring you to log back in.
</div>
<div class="callout callout-danger validation-errors" ng-show="changePasswordForm.$errors">
<h4>Errors have occured</h4>
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in changePasswordForm.$errors">{{e}}</li>
</ul>

View File

@@ -0,0 +1,576 @@
<section class="content-header">
<h1>Create Organization</h1>
</section>
<section class="content">
<p>
Organizations allow you to share parts of your vault with others as well as manage related users
for a specific entity (such as a family, small team, or large company).
</p>
<form name="createOrgForm" ng-submit="createOrgForm.$valid && submit(model)" api-form="submitPromise">
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">General Information</h3>
</div>
<div class="box-body">
<div class="callout callout-danger validation-errors" ng-show="createOrgForm.$errors">
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in createOrgForm.$errors">{{e}}</li>
</ul>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group" show-errors>
<label for="name">Organization Name</label>
<input type="text" id="name" name="Name" ng-model="model.name" class="form-control"
required api-field />
</div>
</div>
<div class="col-md-6">
<div class="form-group" show-errors>
<label for="billingEmail">Billing Email</label>
<input type="email" id="billingEmail" name="BillingEmail" ng-model="model.billingEmail"
class="form-control" required api-field />
</div>
</div>
</div>
<div class="checkbox">
<label>
<input type="checkbox" ng-model="model.ownedBusiness" ng-click="changedBusiness()">
This account is owned by a business.
</label>
</div>
<div class="row" ng-show="model.ownedBusiness">
<div class="col-md-6">
<div class="form-group" show-errors>
<label for="businessName">Business Name</label>
<input type="text" id="businessName" name="BusinessName" ng-model="model.businessName"
class="form-control" api-field />
</div>
</div>
</div>
</div>
</div>
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Choose Your Plan</h3>
</div>
<div class="box-body">
<div class="radio radio-block" ng-if="!model.ownedBusiness" ng-click="changedPlan()">
<label>
<input type="radio" ng-model="model.plan" name="PlanType" value="free">
Free
<span>For personal users to share with 1 other user.</span>
<span>- Limit 2 users (including you)</span>
<span>- Limit 2 collections</span>
<span class="bottom-line">
Free forever
</span>
</label>
</div>
<div class="radio radio-block" ng-if="!model.ownedBusiness" ng-click="changedPlan()">
<label>
<input type="radio" ng-model="model.plan" name="PlanType" value="personal">
Personal
<span>For personal users such as families &amp; friends.</span>
<span>- Add and share with up to 10 users (5 included with base price)</span>
<span>- Create unlimited collections</span>
<span>- Priority customer support</span>
<span>- 7 day free trial, cancel anytime</span>
<span class="bottom-line">
{{plans.personal.basePrice | currency:'$'}} /month includes {{plans.personal.baseSeats}} users,
additional users {{plans.personal.seatPrice | currency:'$'}} /month
</span>
</label>
</div>
<div class="radio radio-block" ng-click="changedPlan()">
<label>
<input type="radio" ng-model="model.plan" name="PlanType" value="teams">
Teams
<span>For businesses and other team organizations.</span>
<span>- Add and share with unlimited users</span>
<span>- Create unlimited collections</span>
<span>- Priority customer support</span>
<span>- 7 day free trial, cancel anytime</span>
<span class="bottom-line">
{{plans.teams.basePrice | currency:'$'}} /month includes {{plans.teams.baseSeats}} users,
additional users {{plans.teams.seatPrice | currency:'$'}} /month
</span>
</label>
</div>
<div class="radio radio-block" ng-click="changedPlan()">
<label>
<input type="radio" ng-model="model.plan" name="PlanType" value="enterprise">
Enterprise
<span>For businesses and other large organizations.</span>
<span>- Add and share with unlimited users</span>
<span>- Create unlimited collections</span>
<span>- Control user access with groups</span>
<span>- Sync your users and groups from a directory (AD, Azure AD, GSuite, LDAP)</span>
<span>- Priority customer support</span>
<span>- 7 day free trial, cancel anytime</span>
<span class="bottom-line">
{{plans.enterprise.seatPrice | currency:'$'}} per user /month
</span>
</label>
</div>
</div>
<div class="box-footer" ng-show="plans[model.plan].noPayment">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="createOrgForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="createOrgForm.$loading"></i>Submit
</button>
</div>
</div>
<div class="box box-default" ng-if="!plans[model.plan].noAdditionalSeats && plans[model.plan].baseSeats">
<div class="box-header with-border">
<h3 class="box-title">Additional Users (Seats)</h3>
</div>
<div class="box-body">
<p>
Your plan comes with <b>{{plans[model.plan].baseSeats}}</b> users (seats). You can add additional users
<span ng-if="plans[model.plan].maxAdditionalSeats">
(up to {{plans[model.plan].maxAdditionalSeats}} more)
</span>
for {{plans[model.plan].seatPrice | currency:'$'}} per user /month.
</p>
<div class="row">
<div class="col-md-4">
<div class="form-group" show-errors style="margin: 0;">
<label for="additionalSeats" class="sr-only">Additional Users</label>
<input type="number" id="additionalSeats" name="AdditionalSeats" ng-model="model.additionalSeats"
min="0" class="form-control" placeholder="# of users" api-field
ng-attr-max="{{plans[model.plan].maxAdditionalSeats || 1000000}}" />
</div>
</div>
</div>
</div>
</div>
<div class="box box-default" ng-if="!plans[model.plan].noAdditionalSeats && !plans[model.plan].baseSeats">
<div class="box-header with-border">
<h3 class="box-title">Users (Seats)</h3>
</div>
<div class="box-body">
<p>
How many user seats do you need?
You can also add additional seats later if needed.
</p>
<div class="row">
<div class="col-md-4">
<div class="form-group" show-errors style="margin: 0;">
<label for="additionalSeats" class="sr-only">Users</label>
<input type="number" id="additionalSeats" name="AdditionalSeats" ng-model="model.additionalSeats"
min="1" class="form-control" placeholder="# of users" api-field
ng-attr-max="{{plans[model.plan].maxAdditionalSeats || 1000000}}" />
</div>
</div>
</div>
</div>
</div>
<div class="box box-default" ng-if="!plans[model.plan].noPayment">
<div class="box-header with-border">
<h3 class="box-title">Billing Summary</h3>
</div>
<div class="box-body">
<div class="radio radio-block">
<label>
<input type="radio" ng-model="model.interval" name="BillingInterval" value="year">
Annually
<span ng-if="plans[model.plan].annualBasePrice">
Base price:
{{plans[model.plan].basePrice | currency:"$":2}} &times;12 mo. =
{{plans[model.plan].annualBasePrice | currency:"$":2}} /year
</span>
<span>
<span ng-if="plans[model.plan].baseSeats">Additional users:</span>
<span ng-if="!plans[model.plan].baseSeats">Users:</span>
{{model.additionalSeats || 0}} &times;{{plans[model.plan].seatPrice | currency:"$":2}}
&times;12 mo. =
{{((model.additionalSeats || 0) * plans[model.plan].annualSeatPrice) | currency:"$":2}} /year
</span>
</label>
</div>
<div class="radio radio-block" ng-if="model.plan !== 'personal'">
<label>
<input type="radio" ng-model="model.interval" name="BillingInterval" value="month">
Monthly
<span ng-if="plans[model.plan].monthlyBasePrice">
Base price:
{{plans[model.plan].monthlyBasePrice | currency:"$":2}} /month
</span>
<span>
<span ng-if="plans[model.plan].baseSeats">Additional users:</span>
<span ng-if="!plans[model.plan].baseSeats">Users:</span>
{{model.additionalSeats || 0}}
&times;{{plans[model.plan].monthlySeatPrice | currency:"$":2}} =
{{((model.additionalSeats || 0) * plans[model.plan].monthlySeatPrice) | currency:"$":2}} /month
</span>
</label>
</div>
</div>
<div class="box-footer">
<h4>
<b>Total:</b>
{{totalPrice() | currency:"USD $":2}} /{{model.interval}}
</h4>
Your plan comes with a free 7 day trial. Your card will not be charged until the trial has ended.
You may cancel at any time.
</div>
</div>
<div class="box box-default" ng-if="!plans[model.plan].noPayment">
<div class="box-header with-border">
<h3 class="box-title">Payment Information</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-md-5">
<div class="form-group" show-errors>
<label for="card_number">Card Number</label>
<input type="text" id="card_number" name="card_number" ng-model="model.card.number"
class="form-control" cc-number required api-field />
</div>
</div>
<div class="col-md-7">
<br class="hidden-sm hidden-xs" />
<ul class="list-inline" style="margin: 0;">
<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>
</div>
<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="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="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="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="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-4">
<div class="form-group" show-errors>
<label for="address_zip"
ng-bind="model.card.address_country === 'US' ? 'Zip Code' : 'Postal Code'"></label>
<input type="text" id="address_zip" ng-model="model.card.address_zip"
class="form-control" required name="address_zip" api-field />
</div>
</div>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="createOrgForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="createOrgForm.$loading"></i>Submit
</button>
</div>
</div>
</form>
</section>

View File

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

View File

@@ -1,83 +1,117 @@
<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-globe"></i> Domain Rules</h4>
</div>
<form name="domainsForm" ng-submit="domainsForm.$valid && save()" api-form="submitPromise">
<div class="modal-body">
<p>
If you have the same login across multiple different website domains, you can mark the website as "equivalent".
"Global" domains are ones created for you by bitwarden.
</p>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th colspan="2">Global Equivalent Domains</th>
</tr>
</thead>
<tbody ng-if="globalEquivalentDomains.length">
<tr ng-repeat="globalDomain in globalEquivalentDomains">
<td style="width: 80px; min-width: 80px;">
<button type="button" class="btn btn-link btn-table" uib-tooltip="Exclude"
ng-if="!globalDomain.excluded" ng-click="toggleExclude(globalDomain)">
<i class="fa fa-lg fa-ban"></i>
</button>
<button type="button" class="btn btn-link btn-table" uib-tooltip="Include"
ng-if="globalDomain.excluded" ng-click="toggleExclude(globalDomain)">
<i class="fa fa-lg fa-plus"></i>
</button>
<button type="button" class="btn btn-link btn-table" uib-tooltip="Customize"
ng-click="customize(globalDomain)">
<i class="fa fa-lg fa-cut"></i>
</button>
</td>
<td ng-class="{strike: globalDomain.excluded}">{{globalDomain.domains}}</td>
</tr>
</tbody>
<tbody ng-if="!globalEquivalentDomains.length">
<tr>
<td>No domains to list.</td>
</tr>
</tbody>
</table>
<section class="content-header">
<h1>Domain Rules</h1>
</section>
<section class="content">
<p>
If you have the same login across multiple different website domains, you can mark the website as "equivalent".
"Global" domains are ones already created for you by bitwarden.
</p>
<form name="customForm" ng-submit="customForm.$valid && saveCustom()" api-form="customPromise">
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Custom <span class="hidden-xs">Equivalent Domains</span></h3>
<div class="box-tools">
<button type="button" class="btn btn-primary btn-sm btn-flat" ng-click="addEdit(null)">
<i class="fa fa-fw fa-plus-circle"></i> New Domain
</button>
</div>
</div>
<div class="box-body no-padding">
<div class="table-responsive">
<table class="table table-striped table-hover table-vmiddle">
<tbody ng-if="equivalentDomains.length">
<tr ng-repeat="customDomain in equivalentDomains track by $index">
<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="javascript:void(0)" ng-click="addEdit($index)">
<i class="fa fa-fw fa-pencil"></i> Edit
</a>
</li>
<li>
<a href="javascript:void(0)" ng-click="delete($index)" class="text-red">
<i class="fa fa-fw fa-trash"></i> Delete
</a>
</li>
</ul>
</div>
</td>
<td>{{customDomain}}</td>
</tr>
</tbody>
<tbody ng-if="!equivalentDomains.length">
<tr>
<td>No domains to list.</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="customForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="customForm.$loading"></i>Save
</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th colspan="2">
Custom Equivalent Domains
<a href="javascript:void(0)" ng-click="addEdit(null)">
<i class="fa fa-plus"></i> Add New
</a>
</th>
</tr>
</thead>
<tbody ng-if="equivalentDomains.length">
<tr ng-repeat="customDomain in equivalentDomains track by $index">
<td style="width: 80px; min-width: 80px;">
<button type="button" class="btn btn-link btn-table" uib-tooltip="Edit" ng-click="addEdit($index)">
<i class="fa fa-lg fa-pencil"></i>
</button>
<button type="button" class="btn btn-link btn-table" uib-tooltip="Delete" ng-click="delete($index)">
<i class="fa fa-lg fa-trash"></i>
</button>
</td>
<td>{{customDomain}}</td>
</tr>
</tbody>
<tbody ng-if="!equivalentDomains.length">
<tr>
<td>No domains to list.</td>
</tr>
</tbody>
</table>
</form>
<form name="globalForm" ng-submit="globalForm.$valid && saveGlobal()" api-form="globalPromise">
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Global <span class="hidden-xs">Equivalent Domains</span></h3>
</div>
<div class="box-body no-padding">
<div class="table-responsive">
<table class="table table-striped table-hover table-vmiddle">
<tbody ng-if="globalEquivalentDomains.length">
<tr ng-repeat="globalDomain in globalEquivalentDomains">
<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="javascript:void(0)" ng-if="!globalDomain.excluded"
ng-click="toggleExclude(globalDomain)">
<i class="fa fa-fw fa-remove"></i> Exclude
</a>
</li>
<li>
<a href="javascript:void(0)" ng-if="globalDomain.excluded"
ng-click="toggleExclude(globalDomain)">
<i class="fa fa-fw fa-plus"></i> Include
</a>
</li>
<li>
<a href="javascript:void(0)" ng-click="customize(globalDomain)">
<i class="fa fa-fw fa-cut"></i> Customize
</a>
</li>
</ul>
</div>
</td>
<td ng-class="{strike: globalDomain.excluded}">{{::globalDomain.domains}}</td>
</tr>
</tbody>
<tbody ng-if="!globalEquivalentDomains.length">
<tr>
<td>No domains to list.</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="globalForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="globalForm.$loading"></i>Save
</button>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="domainsForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="domainsForm.$loading"></i>Save
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
</div>
</form>
</form>
</section>

View File

@@ -12,7 +12,7 @@
Proceeding will log you out of your current session as well, requiring you to log back in.
</div>
<div class="callout callout-danger validation-errors" ng-show="logoutSessionsForm.$errors">
<h4>Errors have occured</h4>
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in logoutSessionsForm.$errors">{{e}}</li>
</ul>

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