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

Compare commits

..

172 Commits

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

3
.gitignore vendored
View File

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

View File

@@ -4,6 +4,8 @@
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**
@@ -11,8 +13,9 @@ The bitwarden Web project is an AngularJS application that powers the web vault
- Node.js
- Gulp
Unless you are running the [Core](https://github.com/bitwarden/core) API locally, you'll probably need to switch the
application to target the production API. Open `package.json` and set `production` to `true`.
By default the application points to the production API. If you want to change that to point to a local instance of
the [Core](https://github.com/bitwarden/core) API, you can modify the `package.json` `env` property to `Development`
and then set your local endpoints in `settings.json`.
Then run the following commands:
@@ -26,4 +29,4 @@ You can now access the web vault at `http://localhost:4001`.
Code contributions are welcome! Please commit any pull requests against the `master` branch.
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature.
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file.

45
SECURITY.md Normal file
View File

@@ -0,0 +1,45 @@
bitwarden believes that working with security researchers across the globe is crucial to keeping our
users safe. If you believe you've found a security issue in our product or service, we encourage you to
notify us. We welcome working with you to resolve the issue promptly. Thanks in advance!
# Disclosure Policy
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every
effort to quickly resolve the issue.
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a
third-party. We may publicly disclose the issue before resolving it, if appropriate.
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or
degradation of our service. Only interact with accounts you own or with explicit permission of the
account holder.
- If you would like to encrypt your report, please use the PGP key with long ID
`0xDE6887086F892325FEC04CC0D847525B6931381F` (available in the public keyserver pool).
# In-scope
- Security issues in any current release of bitwarden. This includes the web vault, browser extension,
and mobile apps (iOS and Android). Product downloads are available at https://bitwarden.com. Source
code is available at https://github.com/bitwarden.
# Exclusions
The following bug classes are out-of scope:
- Bugs that are already reported on any of bitwarden's issue trackers (https://github.com/bitwarden),
or that we already know of. Note that some of our issue tracking is private.
- Issues in an upstream software dependency (ex: Xamarin, ASP.NET) which are already reported to the
upstream maintainer.
- Attacks requiring physical access to a user's device.
- Self-XSS
- Issues related to software or protocols not under bitwarden's control
- Vulnerabilities in outdated versions of bitwarden
- Missing security best practices that do not directly lead to a vulnerability
- Issues that do not have any impact on the general public
While researching, we'd like to ask you to refrain from:
- Denial of service
- Spamming
- Social engineering (including phishing) of bitwarden staff or contractors
- Any physical attempts against bitwarden property or data centers
Thank you for helping keep bitwarden and our users safe!

View File

@@ -46,7 +46,7 @@ gulp.task('lint', function () {
gulp.task('build', function (cb) {
return runSequence(
'clean',
['browserify', 'lib', 'webpack', 'less', 'settings', 'lint'],
['browserify', 'lib', 'webpack', 'less', 'settings', 'lint', 'min:js'],
cb);
});
@@ -65,7 +65,14 @@ gulp.task('clean:lib', function (cb) {
gulp.task('clean', ['clean:js', 'clean:css', 'clean:lib', 'dist:clean']);
gulp.task('min:js', ['clean:js'], function () {
return gulp.src([paths.js, '!' + paths.minJs], { base: '.' })
return gulp.src(
[
paths.js,
'!' + paths.minJs,
'!' + paths.jsDir + 'fallback*.js',
'!' + paths.jsDir + 'u2f-connector.js',
'!' + paths.jsDir + 'duo.js'
], { base: '.' })
.pipe(concat(paths.concatJsDest))
.pipe(uglify())
.pipe(gulp.dest('.'));
@@ -130,6 +137,10 @@ gulp.task('lib', ['clean:lib'], function () {
src: paths.npmDir + 'angular-resource/*resource*.js',
dest: paths.libDir + 'angular-resource'
},
{
src: paths.npmDir + 'angular-sanitize/*sanitize*.js',
dest: paths.libDir + 'angular-sanitize'
},
{
src: [paths.npmDir + 'angular-toastr/dist/**/*.css', paths.npmDir + 'angular-toastr/dist/**/*.js'],
dest: paths.libDir + 'angular-toastr'
@@ -158,12 +169,28 @@ gulp.task('lib', ['clean:lib'], function () {
src: paths.npmDir + 'clipboard/dist/clipboard*.js',
dest: paths.libDir + 'clipboard'
},
{
src: paths.npmDir + 'node-forge/dist/prime.worker.*',
dest: paths.libDir + 'forge'
},
{
src: [
paths.npmDir + 'angulartics-google-analytics/lib/angulartics*.js',
paths.npmDir + 'angulartics/src/angulartics.js'
],
dest: paths.libDir + 'angulartics'
},
//{
// src: paths.npmDir + 'duo_web_sdk/index.js',
// dest: paths.libDir + 'duo'
//},
{
src: paths.jsDir + 'duo.js',
dest: paths.libDir + 'duo'
},
{
src: paths.npmDir + 'angular-promise-polyfill/index.js',
dest: paths.libDir + 'angular-promise-polyfill'
}
];
@@ -293,12 +320,31 @@ gulp.task('dist:move', function () {
src: paths.npmDir + 'angular/angular.min.js',
dest: paths.dist + 'lib/angular'
},
{
src: paths.npmDir + 'node-forge/dist/prime.worker.*',
dest: paths.dist + 'lib/forge'
},
//{
// src: paths.npmDir + 'duo_web_sdk/index.js',
// dest: paths.dist + 'lib/duo'
//},
{
src: paths.jsDir + 'duo.js',
dest: paths.dist + 'js'
},
{
src: paths.jsDir + 'bw.min.js',
dest: paths.dist + 'js'
},
{
src: [
paths.webroot + '**/app/**/*.html',
paths.webroot + '**/images/**/*',
paths.webroot + 'index.html',
paths.webroot + 'favicon.ico'
paths.webroot + 'u2f-connector.html',
paths.webroot + 'duo-connector.html',
paths.webroot + 'favicon.ico',
paths.webroot + 'app-id.json'
],
dest: paths.dist
}
@@ -340,6 +386,31 @@ gulp.task('dist:js:app', function () {
.pipe(gulp.dest('.'));
});
gulp.task('dist:js:fallback', function () {
var mainStream = gulp
.src([
paths.jsDir + 'fallback*.js'
]);
merge(mainStream)
.pipe(preprocess({ context: { cacheTag: randomString } }))
.pipe(uglify())
.pipe(rename({ suffix: '.min' }))
.pipe(gulp.dest(paths.dist + 'js'));
});
gulp.task('dist:js:u2f', function () {
var mainStream = gulp
.src([
paths.jsDir + 'u2f*.js'
]);
merge(mainStream)
.pipe(concat(paths.dist + '/js/u2f.min.js'))
.pipe(uglify())
.pipe(gulp.dest('.'));
});
gulp.task('dist:js:lib', function () {
return gulp
.src([
@@ -367,7 +438,7 @@ gulp.task('dist:preprocess', function () {
gulp.task('dist', ['build'], function (cb) {
return runSequence(
'dist:clean',
['dist:move', 'dist:css', 'dist:js:app', 'dist:js:lib'],
['dist:move', 'dist:css', 'dist:js:app', 'dist:js:lib', 'dist:js:fallback', 'dist:js:u2f'],
'dist:preprocess',
cb);
});
@@ -381,13 +452,22 @@ 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'
remoteUrl: 'git@github.com:kspearrin/bitwarden-web-preview.git'
}));
});
gulp.task('serve', function () {
connect.server({
port: 4001,
root: ['src']
root: ['src'],
//https: true,
middleware: function (connect, opt) {
return [function (req, res, next) {
if (req.originalUrl.indexOf('app-id.json') > -1) {
res.setHeader('Content-Type', 'application/fido.trusted-apps+json');
}
next();
}];
}
});
});

View File

@@ -1,6 +1,6 @@
{
"name": "bitwarden",
"version": "1.10.1",
"version": "1.14.1",
"env": "Production",
"devDependencies": {
"connect": "3.6.0",
@@ -26,6 +26,7 @@
"bootstrap": "3.3.7",
"angular": "1.6.3",
"angular-resource": "1.6.3",
"angular-sanitize": "1.6.3",
"angular-ui-bootstrap": "2.5.0",
"angular-ui-router": "0.4.2",
"angular-jwt": "0.1.9",
@@ -47,6 +48,8 @@
"browserify": "14.1.0",
"vinyl-source-stream": "1.1.0",
"gulp-derequire": "2.1.0",
"exposify": "0.5.0"
"exposify": "0.5.0",
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git",
"angular-promise-polyfill": "0.0.4"
}
}

View File

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

View File

@@ -1,6 +1,8 @@
{
"appSettings": {
"apiUri": "https://api.bitwarden.com",
"identityUri": "https://identity.bitwarden.com"
"identityUri": "https://identity.bitwarden.com",
"stripeKey": "pk_live_bpN0P37nMxrMQkcaHXtAybJk",
"braintreeKey": "TODO"
}
}

View File

@@ -1,6 +1,8 @@
{
"appSettings": {
"apiUri": "http://localhost:4000",
"identityUri": "http://localhost:33656"
"identityUri": "http://localhost:33656",
"stripeKey": "pk_test_KPoCfZXu7mznb9uSCPZ2JpTD",
"braintreeKey": "sandbox_r72q8jq6_9pnxkwm75f87sdc2"
}
}

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,7 @@
<p>Before creating your organization, you first need to create a free personal account.</p>
</div>
<div class="callout callout-danger validation-errors" ng-show="registerForm.$errors">
<h4>Errors have occured</h4>
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in registerForm.$errors">{{e}}</li>
</ul>

View File

@@ -0,0 +1,25 @@
<div class="modal-header">
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><i class="fa fa-key"></i> Two-step Providers</h4>
</div>
<div class="modal-body">
<div class="list-group" ng-repeat="provider in providers | orderBy: 'displayOrder'">
<a href="#" stop-click class="list-group-item" ng-click="choose(provider)">
<img alt="{{::provider.name}}" ng-src="{{'images/two-factor/' + provider.image}}" class="pull-right hidden-xs" />
<h4 class="list-group-item-heading">{{::provider.name}}</h4>
<p class="list-group-item-text">{{::provider.description}}</p>
</a>
</div>
<div class="list-group" style="margin-bottom: 0;">
<a href="https://help.bitwarden.com/article/lost-two-step-device/" target="_blank" class="list-group-item">
<h4 class="list-group-item-heading">Recovery Code</h4>
<p class="list-group-item-text">
Lost access to all of your two-factor providers? Use your recovery code to disable
all two-factor providers from your account.
</p>
</a>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
</div>

View File

@@ -0,0 +1,8 @@
<div class="login-box">
<div class="login-logo">
<i class="fa fa-shield"></i> <b>bit</b>warden
</div>
<div class="login-box-body">
Verifying email...
</div>
</div>

View File

@@ -9,6 +9,7 @@
'angulartics.google.analytics',
'angular-stripe',
'credit-cards',
'angular-promise-polyfill',
'bit.directives',
'bit.filters',
@@ -19,5 +20,6 @@
'bit.vault',
'bit.settings',
'bit.tools',
'bit.organization'
'bit.organization',
'bit.reports'
]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
angular
.module('bit.global')
.controller('sideNavController', function ($scope, $state, authService, toastr, $analytics) {
.controller('sideNavController', function ($scope, $state, authService, toastr, $analytics, constants) {
$scope.$state = $state;
$scope.params = $state.params;
$scope.orgs = [];
@@ -31,7 +31,7 @@ angular
});
$scope.viewOrganization = function (org) {
if (org.type === 2) { // 2 = User
if (org.type === constants.orgUserType.user) {
toastr.error('You cannot manage this organization.');
return;
}
@@ -49,6 +49,6 @@ angular
};
$scope.isOrgOwner = function (org) {
return org && org.type === 0;
return org && org.type === constants.orgUserType.owner;
};
});

View File

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

View File

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

View File

@@ -5,6 +5,9 @@
$analytics, toastr, existingPaymentMethod) {
$analytics.eventTrack('organizationBillingChangePaymentController', { category: 'Modal' });
$scope.existingPaymentMethod = existingPaymentMethod;
$scope.paymentMethod = 'card';
$scope.showPaymentOptions = false;
$scope.card = {};
$scope.submit = function () {
$scope.submitPromise = stripe.card.createToken($scope.card).then(function (response) {
@@ -16,11 +19,11 @@
}).then(function (response) {
$scope.card = null;
if (existingPaymentMethod) {
$analytics.eventTrack('Changed Payment Method');
$analytics.eventTrack('Changed Organization Payment Method');
toastr.success('You have changed your payment method.');
}
else {
$analytics.eventTrack('Added Payment Method');
$analytics.eventTrack('Added Organization Payment Method');
toastr.success('You have added a payment method.');
}

View File

@@ -15,7 +15,7 @@
$scope.changePayment = function () {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationBillingChangePayment.html',
templateUrl: 'app/settings/views/settingsBillingChangePayment.html',
controller: 'organizationBillingChangePaymentController',
resolve: {
existingPaymentMethod: function () {
@@ -63,6 +63,23 @@
});
};
$scope.adjustStorage = function (add) {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsBillingAdjustStorage.html',
controller: 'organizationBillingAdjustStorageController',
resolve: {
add: function () {
return add;
}
}
});
modal.result.then(function () {
load();
});
};
$scope.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.')) {
@@ -103,14 +120,25 @@
seats: org.Seats
};
$scope.storage = null;
if ($scope && org.MaxStorageGb) {
$scope.storage = {
currentGb: org.StorageGb || 0,
maxGb: org.MaxStorageGb,
currentName: org.StorageName || '0 GB'
};
$scope.storage.percentage = +(100 * ($scope.storage.currentGb / $scope.storage.maxGb)).toFixed(2);
}
$scope.subscription = null;
if (org.Subscription) {
$scope.subscription = {
trialEndDate: org.Subscription.TrialEndDate,
cancelledDate: org.Subscription.CancelledDate,
status: org.Subscription.Status,
cancelled: org.Subscription.Status === 'cancelled',
markedForCancel: org.Subscription.Status === 'active' && org.Subscription.CancelledDate
cancelled: org.Subscription.Cancelled,
markedForCancel: !org.Subscription.Cancelled && org.Subscription.CancelAtEndDate
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,93 @@
angular
.module('bit.organization')
.controller('organizationGroupsController', function ($scope, $state) {
.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

@@ -1,11 +1,20 @@
angular
.module('bit.organization')
.controller('organizationPeopleController', function ($scope, $state, $uibModal, cryptoService, apiService,
.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) {
@@ -71,13 +80,13 @@
});
};
$scope.edit = function (id) {
$scope.edit = function (orgUser) {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationPeopleEdit.html',
controller: 'organizationPeopleEditController',
resolve: {
id: function () { return id; }
orgUser: function () { return orgUser; }
}
});
@@ -86,6 +95,21 @@
});
};
$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 = [];

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,16 @@
angular
.module('bit.vault')
.module('bit.organization')
.controller('organizationVaultAddLoginController', function ($scope, apiService, $uibModalInstance, cryptoService,
cipherService, passwordService, $analytics, orgId) {
cipherService, passwordService, $analytics, authService, orgId, $uibModal) {
$analytics.eventTrack('organizationVaultAddLoginController', { category: 'Modal' });
$scope.login = {};
$scope.hideFolders = $scope.hideFavorite = true;
$scope.hideFolders = $scope.hideFavorite = $scope.fromOrg = true;
authService.getUserProfile().then(function (userProfile) {
var orgProfile = userProfile.organizations[orgId];
$scope.useTotp = orgProfile.useTotp;
});
$scope.savePromise = null;
$scope.save = function (model) {
@@ -47,4 +52,15 @@
$scope.close = function () {
$uibModalInstance.dismiss('close');
};
$scope.showUpgrade = function () {
$uibModal.open({
animation: true,
templateUrl: 'app/views/paidOrgRequired.html',
controller: 'paidOrgRequiredController',
resolve: {
orgId: function () { return orgId; }
}
});
};
});

View File

@@ -0,0 +1,94 @@
angular
.module('bit.organization')
.controller('organizationVaultAttachmentsController', function ($scope, apiService, $uibModalInstance, cryptoService,
cipherService, loginId, $analytics, validationService, toastr, $timeout) {
$analytics.eventTrack('organizationVaultAttachmentsController', { category: 'Modal' });
$scope.login = {};
$scope.loading = true;
$scope.isPremium = true;
$scope.canUseAttachments = true;
var closing = false;
apiService.logins.getAdmin({ id: loginId }, function (login) {
$scope.login = cipherService.decryptLogin(login);
$scope.loading = false;
}, function () {
$scope.loading = false;
});
$scope.save = function (form) {
var files = document.getElementById('file').files;
if (!files || !files.length) {
validationService.addError(form, 'file', 'Select a file.', true);
return;
}
var key = cryptoService.getOrgKey($scope.login.organizationId);
$scope.savePromise = cipherService.encryptAttachmentFile(key, files[0]).then(function (encValue) {
var fd = new FormData();
var blob = new Blob([encValue.data], { type: 'application/octet-stream' });
fd.append('data', blob, encValue.fileName);
return apiService.ciphers.postAttachment({ id: loginId }, fd).$promise;
}).then(function (response) {
$analytics.eventTrack('Added Attachment');
toastr.success('The attachment has been added.');
closing = true;
$uibModalInstance.close(true);
}, function (err) {
if (err) {
validationService.addError(form, 'file', err, true);
}
else {
validationService.addError(form, 'file', 'Something went wrong.', true);
}
});
};
$scope.download = function (attachment) {
attachment.loading = true;
var key = cryptoService.getOrgKey($scope.login.organizationId);
cipherService.downloadAndDecryptAttachment(key, attachment, true).then(function (res) {
$timeout(function () {
attachment.loading = false;
});
}, function () {
$timeout(function () {
attachment.loading = false;
});
});
};
$scope.remove = function (attachment) {
if (!confirm('Are you sure you want to delete this attachment (' + attachment.fileName + ')?')) {
return;
}
attachment.loading = true;
apiService.ciphers.delAttachment({ id: loginId, attachmentId: attachment.id }).$promise.then(function () {
attachment.loading = false;
$analytics.eventTrack('Deleted Organization Attachment');
var index = $scope.login.attachments.indexOf(attachment);
if (index > -1) {
$scope.login.attachments.splice(index, 1);
}
}, function () {
toastr.error('Cannot delete attachment.');
attachment.loading = false;
});
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
$scope.$on('modal.closing', function (e, reason, closed) {
if (closing) {
return;
}
e.preventDefault();
closing = true;
$uibModalInstance.close(!!$scope.login.attachments && $scope.login.attachments.length > 0);
});
});

View File

@@ -2,7 +2,7 @@
.module('bit.organization')
.controller('organizationVaultController', function ($scope, apiService, cipherService, $analytics, $q, $state,
$localStorage, $uibModal, $filter) {
$localStorage, $uibModal, $filter, authService) {
$scope.logins = [];
$scope.collections = [];
$scope.loading = true;
@@ -83,7 +83,8 @@
templateUrl: 'app/vault/views/vaultEditLogin.html',
controller: 'organizationVaultEditLoginController',
resolve: {
loginId: function () { return login.id; }
loginId: function () { return login.id; },
orgId: function () { return $state.params.orgId; }
}
});
@@ -138,6 +139,37 @@
});
};
$scope.attachments = function (login) {
authService.getUserProfile().then(function (profile) {
return !!profile.organizations[login.organizationId].maxStorageGb;
}).then(function (useStorage) {
if (!useStorage) {
$uibModal.open({
animation: true,
templateUrl: 'app/views/paidOrgRequired.html',
controller: 'paidOrgRequiredController',
resolve: {
orgId: function () { return login.organizationId; }
}
});
return;
}
var attachmentModel = $uibModal.open({
animation: true,
templateUrl: 'app/vault/views/vaultAttachments.html',
controller: 'organizationVaultAttachmentsController',
resolve: {
loginId: function () { return login.id; }
}
});
attachmentModel.result.then(function (hasAttachments) {
login.hasAttachments = hasAttachments;
});
});
};
$scope.removeLogin = function (login, collection) {
if (!confirm('Are you sure you want to remove this login (' + login.name + ') from the ' +
'collection (' + collection.name + ') ?')) {

View File

@@ -1,14 +1,15 @@
angular
.module('bit.vault')
.module('bit.organization')
.controller('organizationVaultEditLoginController', function ($scope, apiService, $uibModalInstance, cryptoService,
cipherService, passwordService, loginId, $analytics) {
cipherService, passwordService, loginId, $analytics, orgId, $uibModal) {
$analytics.eventTrack('organizationVaultEditLoginController', { category: 'Modal' });
$scope.login = {};
$scope.hideFolders = $scope.hideFavorite = true;
$scope.hideFolders = $scope.hideFavorite = $scope.fromOrg = true;
apiService.logins.getAdmin({ id: loginId }, function (login) {
$scope.login = cipherService.decryptLogin(login);
$scope.useTotp = $scope.login.organizationUseTotp;
});
$scope.save = function (model) {
@@ -66,4 +67,15 @@
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
$scope.showUpgrade = function () {
$uibModal.open({
animation: true,
templateUrl: 'app/views/paidOrgRequired.html',
controller: 'paidOrgRequiredController',
resolve: {
orgId: function () { return orgId; }
}
});
};
});

View File

@@ -6,7 +6,7 @@
</section>
<section class="content">
<div class="callout callout-warning" ng-if="subscription && subscription.cancelled">
<h4><i class="fa fa-warning"></i> Cancelled</h4>
<h4><i class="fa fa-warning"></i> Canceled</h4>
The subscription to this organization has been canceled.
</div>
<div class="callout callout-warning" ng-if="subscription && subscription.markedForCancel">
@@ -19,7 +19,7 @@
Reinstate Plan
</button>
</div>
<div class="box">
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Plan</h3>
</div>
@@ -36,7 +36,10 @@
<div class="col-sm-6">
<dl>
<dt>Status</dt>
<dd style="text-transform: capitalize;">{{(subscription && subscription.status) || '-'}}</dd>
<dd>
<span style="text-transform: capitalize;">{{(subscription && subscription.status) || '-'}}</span>
<span ng-if="subscription.markedForCancel">- marked for cancellation</span>
</dd>
<dt>Next Charge</dt>
<dd>{{nextInvoice ? ((nextInvoice.date | date: format: mediumDate) + ', ' + (nextInvoice.amount | currency:'$')) : '-'}}</dd>
</dl>
@@ -78,7 +81,7 @@
</button>
</div>
</div>
<div class="box">
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">User Seats</h3>
</div>
@@ -99,7 +102,33 @@
</button>
</div>
</div>
<div class="box">
<div class="box box-default" ng-if="storage">
<div class="box-header with-border">
<h3 class="box-title">Storage</h3>
</div>
<div class="box-body">
<p>
You plan has a total of {{storage.maxGb}} GB of encrypted file storage.
You are currently using {{storage.currentName}}.
</p>
<div class="progress" style="margin: 0;">
<div class="progress-bar progress-bar-info" role="progressbar"
aria-valuenow="{{storage.percentage}}" aria-valuemin="0" aria-valuemax="1"
style="min-width: 50px; width: {{storage.percentage}}%;">
{{storage.percentage}}%
</div>
</div>
</div>
<div class="box-footer">
<button type="button" class="btn btn-default btn-flat" ng-click="adjustStorage(true)">
Add Storage
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="adjustStorage(false)">
Remove Storage
</button>
</div>
</div>
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Payment Method</h3>
</div>
@@ -112,7 +141,7 @@
</div>
<div ng-show="!loading && paymentSource">
<i class="fa" ng-class="{'fa-credit-card': paymentSource.type === 0,
'fa-university': paymentSource.type === 1}"></i>
'fa-university': paymentSource.type === 1, 'fa-paypal fa-fw text-blue': paymentSource.type === 2}"></i>
{{paymentSource.description}}
</div>
</div>
@@ -122,7 +151,7 @@
</button>
</div>
</div>
<div class="box">
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Charges</h3>
</div>
@@ -140,7 +169,7 @@
<td style="width: 200px">
{{charge.date | date: format: mediumDate}}
</td>
<td>
<td style="min-width: 150px">
{{charge.paymentSource}}
</td>
<td style="width: 150px; text-transform: capitalize;">
@@ -155,7 +184,7 @@
</div>
</div>
<div class="box-footer">
Note: Any charges will appears on your credit card statement as <b>BITWARDEN</b>.
Note: Any charges will appear on your statement as <b>BITWARDEN</b>.
</div>
</div>
</section>

View File

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

View File

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

View File

@@ -4,8 +4,9 @@
</div>
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise">
<div class="modal-body">
Coming soon. In the meantime, please <a href="https://bitwarden.com/contact/" target="_blank">contact us</a>
if you would like to change your plan.
You can <a href="https://bitwarden.com/contact/" target="_blank">contact us</a>
if you would like to change your plan. Please ensure that you have an active payment
method on file.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>

View File

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

View File

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

View File

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

View File

@@ -1,10 +0,0 @@
<div class="modal-header">
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><i class="fa fa-sitemap"></i> Groups <small>{{collection.name}}</small></h4>
</div>
<div class="modal-body">
Groups are coming soon to bitwarden Enterprise organizations.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
</div>

View File

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

View File

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

View File

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

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="#" stop-click 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

@@ -37,22 +37,27 @@
</button>
<ul class="dropdown-menu">
<li>
<a href="javascript:void(0)" ng-click="edit(user.id)">
<a href="#" stop-click ng-click="edit(user)">
<i class="fa fa-fw fa-pencil"></i> Edit
</a>
</li>
<li>
<a href="#" stop-click 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)">
<a href="#" stop-click 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)">
<a href="#" stop-click 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">
<a href="#" stop-click ng-click="delete(user)" class="text-red">
<i class="fa fa-fw fa-remove"></i> Remove
</a>
</li>
@@ -63,7 +68,7 @@
<letter-avatar data="{{user.name || user.email}}"></letter-avatar>
</td>
<td>
<a href="javascript:void(0)" ng-click="edit(user.id)">{{user.email}}</a>
<a href="#" stop-click 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>

View File

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

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

@@ -9,14 +9,15 @@
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 occured</h4>
<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="email">Email</label>
<input type="email" id="email" name="Email" ng-model="model.email" class="form-control" required api-field />
<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">

View File

@@ -14,7 +14,7 @@
<div class="row">
<div class="col-sm-9">
<div class="callout callout-danger validation-errors" ng-show="generalForm.$errors">
<h4>Errors have occured</h4>
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in generalForm.$errors">{{e}}</li>
</ul>

View File

@@ -42,23 +42,28 @@
</button>
<ul class="dropdown-menu">
<li>
<a href="javascript:void(0)" ng-click="editLogin(login)">
<a href="#" stop-click 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)">
<a href="#" stop-click ng-click="attachments(login)">
<i class="fa fa-fw fa-paperclip"></i> Attachments
</a>
</li>
<li>
<a href="#" stop-click 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"
<a href="#" stop-click ng-click="removeLogin(login, collection)" class="text-red"
ng-if="collection.id">
<i class="fa fa-fw fa-remove"></i> Remove
</a>
</li>
<li>
<a href="javascript:void(0)" ng-click="deleteLogin(login)" class="text-red">
<a href="#" stop-click ng-click="deleteLogin(login)" class="text-red">
<i class="fa fa-fw fa-trash"></i> Delete
</a>
</li>
@@ -66,7 +71,7 @@
</div>
</td>
<td>
<a href="javascript:void(0)" ng-click="editLogin(login)">{{login.name}}</a>
<a href="#" stop-click ng-click="editLogin(login)">{{login.name}}</a>
<div class="text-sm text-muted">{{login.username}}</div>
</td>
</tr>

View File

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

View File

@@ -0,0 +1,38 @@
angular
.module('bit.tools')
.controller('reportsBreachController', function ($scope, apiService, toastr, authService) {
$scope.loading = true;
$scope.error = false;
$scope.breachAccounts = [];
$scope.email = null;
$scope.$on('$viewContentLoaded', function () {
authService.getUserProfile().then(function (userProfile) {
$scope.email = userProfile.email;
return apiService.hibp.get({ email: $scope.email }).$promise;
}).then(function (response) {
var breachAccounts = [];
for (var i = 0; i < response.length; i++) {
var breach = {
id: response[i].Name,
title: response[i].Title,
domain: response[i].Domain,
date: new Date(response[i].BreachDate),
reportedDate: new Date(response[i].AddedDate),
modifiedDate: new Date(response[i].ModifiedDate),
count: response[i].PwnCount,
description: response[i].Description,
classes: response[i].DataClasses,
image: 'https://haveibeenpwned.com/Content/Images/PwnedLogos/' + response[i].Name + '.' + response[i].LogoType
};
breachAccounts.push(breach);
}
$scope.breachAccounts = breachAccounts;
$scope.loading = false;
}, function (response) {
$scope.error = response.status !== 404;
$scope.loading = false;
});
});
});

View File

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

View File

@@ -0,0 +1,74 @@
<section class="content-header">
<h1>
Data Breach Report
<small>have you been pwned?</small>
</h1>
</section>
<section class="content">
<div ng-show="loading && !breachAccounts.length">
<p>Loading...</p>
</div>
<div ng-show="!loading && error">
<p>An error occurred trying to load the report. Try again...</p>
</div>
<div class="callout callout-danger" ng-show="!error && !loading && breachAccounts.length">
<h4><i class="fa fa-frown-o"></i> Oh No, Data Breaches Found!</h4>
<p>
Your email ({{email}}) was found in {{breachAccounts.length}}
<span ng-if="breachAccounts.length > 1">different</span> data
<span ng-pluralize count="breachAccounts.length" when="{'1': 'breach', 'other': 'breaches'}"></span>
online.
</p>
<p>
A "breach" is an incident where a site's data has been illegally accessed by hackers and then released publicly.
Review the types of data that were compromised (email addresses, passwords, credit cards etc.) and take appropriate
action, such as changing passwords.
</p>
<a href="https://haveibeenpwned.com" rel="noopener" target="_blank" class="btn btn-default btn-flat">Check another email</a>
</div>
<div class="callout callout-success" ng-show="!error && !loading && !breachAccounts.length">
<h4><i class="fa fa-smile-o"></i> Good News, Nothing Found!</h4>
<p>Your email ({{email}}) was not found in any known data breaches.</p>
<a href="https://haveibeenpwned.com" rel="noopener" target="_blank" class="btn btn-default btn-flat">Check another email</a>
</div>
<div class="box box-danger" ng-repeat="breach in breachAccounts track by breach.id">
<div class="box-header with-border">
<h3 class="box-title">{{breach.title}}</h3>
</div>
<div class="box-body box-breach">
<div class="row">
<div class="col-sm-2">
<img ng-src="{{breach.image}}" alt="{{breach.id}} logo" class="img-responsive" />
</div>
<div class="col-sm-10">
<div class="row">
<div class="col-sm-8">
<p ng-bind-html="breach.description"></p>
<h5><b>Compromised Data</b></h5>
<ul>
<li ng-repeat="class in breach.classes">{{class}}</li>
</ul>
</div>
<div class="col-sm-4">
<dl>
<dt><span class="hidden-sm">Website</dt>
<dd>{{breach.domain}}</dd>
<dt><span class="hidden-sm">Affected </span>Users</dt>
<dd>{{breach.count | number: 0}}</dd>
<dt><span class="hidden-sm">Breach </span>Occurred</dt>
<dd>{{breach.date | date: format: mediumDate}}</dd>
<dt><span class="hidden-sm">Breach </span>Reported</dt>
<dd>{{breach.reportedDate | date: format: mediumDate}}</dd>
<dt><span class="hidden-sm">Information </span>Updated</dt>
<dd>{{breach.modifiedDate | date: format: mediumDate}}</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
</div>
This data is brought to you as a service from
<b><a href="https://haveibeenpwned.com/" target="_blank" rel="noopener">Have I been pwned?</a></b>.
Please check out their wonderful services and subscribe to receive notifications about future data breaches.
</section>

View File

@@ -27,7 +27,7 @@
_service.ciphers = $resource(_apiUri + '/ciphers/:id', {}, {
get: { method: 'GET', params: { id: '@id' } },
getFullDetails: { url: _apiUri + '/ciphers/:id/full-details', method: 'GET', params: { id: '@id' } },
getDetails: { url: _apiUri + '/ciphers/:id/details', method: 'GET', params: { id: '@id' } },
list: { method: 'GET', params: { includeFolders: false, includeShared: true } },
listDetails: { url: _apiUri + '/ciphers/details', method: 'GET', params: {} },
listOrganizationDetails: { url: _apiUri + '/ciphers/organization-details', method: 'GET', params: {} },
@@ -38,7 +38,22 @@
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' } }
delAdmin: { url: _apiUri + '/ciphers/:id/delete-admin', method: 'POST', params: { id: '@id' } },
delMany: { url: _apiUri + '/ciphers/delete', method: 'POST' },
moveMany: { url: _apiUri + '/ciphers/move', method: 'POST' },
postAttachment: {
url: _apiUri + '/ciphers/:id/attachment',
method: 'POST',
headers: { 'Content-Type': undefined },
params: { id: '@id' }
},
postShareAttachment: {
url: _apiUri + '/ciphers/:id/attachment/:attachmentId/share?organizationId=:orgId',
method: 'POST',
headers: { 'Content-Type': undefined },
params: { id: '@id', attachmentId: '@attachmentId', orgId: '@orgId' }
},
delAttachment: { url: _apiUri + '/ciphers/:id/attachment/:attachmentId/delete', method: 'POST', params: { id: '@id', attachmentId: '@attachmentId' } }
});
_service.organizations = $resource(_apiUri + '/organizations/:id', {}, {
@@ -49,6 +64,7 @@
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' } },
putStorage: { url: _apiUri + '/organizations/:id/storage', 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' } },
@@ -59,45 +75,81 @@
_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' } }
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.collectionUsers = $resource(_apiUri + '/organizations/:orgId/collectionUsers/:id', {}, {
listCollection: { url: _apiUri + '/organizations/:orgId/collectionUsers/:collectionId', method: 'GET', params: { collectionId: '@collectionId', orgId: '@orgId' } },
del: { url: _apiUri + '/organizations/:orgId/collectionUsers/:id/delete', method: 'POST', params: { id: '@id', orgId: '@orgId' } }
_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', {}, {
register: { url: _apiUri + '/accounts/register', method: 'POST', params: {} },
emailToken: { url: _apiUri + '/accounts/email-token', method: 'POST', params: {} },
email: { url: _apiUri + '/accounts/email', method: 'POST', params: {} },
verifyEmailToken: { url: _apiUri + '/accounts/verify-email-token', method: 'POST', params: {} },
verifyEmail: { url: _apiUri + '/accounts/verify-email', method: 'POST', params: {} },
putPassword: { url: _apiUri + '/accounts/password', method: 'POST', params: {} },
getProfile: { url: _apiUri + '/accounts/profile', method: 'GET', params: {} },
putProfile: { url: _apiUri + '/accounts/profile', method: 'POST', params: {} },
getDomains: { url: _apiUri + '/accounts/domains', method: 'GET', params: {} },
putDomains: { url: _apiUri + '/accounts/domains', method: 'POST', params: {} },
getTwoFactor: { url: _apiUri + '/accounts/two-factor', method: 'GET', params: {} },
putTwoFactor: { url: _apiUri + '/accounts/two-factor', method: 'POST', params: {} },
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: {} },
putKey: { url: _apiUri + '/accounts/key', method: 'POST', params: {} },
'import': { url: _apiUri + '/accounts/import', method: 'POST', params: {} },
postDelete: { url: _apiUri + '/accounts/delete', method: 'POST', params: {} }
postDelete: { url: _apiUri + '/accounts/delete', method: 'POST', params: {} },
postPremium: { url: _apiUri + '/accounts/premium', method: 'POST', params: {} },
putStorage: { url: _apiUri + '/accounts/storage', method: 'POST', params: {} },
putPayment: { url: _apiUri + '/accounts/payment', method: 'POST', params: {} },
putCancelPremium: { url: _apiUri + '/accounts/cancel-premium', method: 'POST', params: {} },
putReinstatePremium: { url: _apiUri + '/accounts/reinstate-premium', method: 'POST', params: {} },
getBilling: { url: _apiUri + '/accounts/billing', method: 'GET', params: {} }
});
_service.twoFactor = $resource(_apiUri + '/two-factor', {}, {
list: { method: 'GET', params: {} },
getEmail: { url: _apiUri + '/two-factor/get-email', method: 'POST', params: {} },
getU2f: { url: _apiUri + '/two-factor/get-u2f', method: 'POST', params: {} },
getDuo: { url: _apiUri + '/two-factor/get-duo', method: 'POST', params: {} },
getAuthenticator: { url: _apiUri + '/two-factor/get-authenticator', method: 'POST', params: {} },
getYubi: { url: _apiUri + '/two-factor/get-yubikey', method: 'POST', params: {} },
sendEmail: { url: _apiUri + '/two-factor/send-email', method: 'POST', params: {} },
sendEmailLogin: { url: _apiUri + '/two-factor/send-email-login', method: 'POST', params: {} },
putEmail: { url: _apiUri + '/two-factor/email', method: 'POST', params: {} },
putU2f: { url: _apiUri + '/two-factor/u2f', method: 'POST', params: {} },
putAuthenticator: { url: _apiUri + '/two-factor/authenticator', method: 'POST', params: {} },
putDuo: { url: _apiUri + '/two-factor/duo', method: 'POST', params: {} },
putYubi: { url: _apiUri + '/two-factor/yubikey', method: 'POST', params: {} },
disable: { url: _apiUri + '/two-factor/disable', method: 'POST', params: {} },
recover: { url: _apiUri + '/two-factor/recover', method: 'POST', params: {} },
getRecover: { url: _apiUri + '/two-factor/get-recover', method: 'POST', params: {} }
});
_service.settings = $resource(_apiUri + '/settings', {}, {
@@ -120,6 +172,10 @@
}
});
_service.hibp = $resource('https://haveibeenpwned.com/api/v2/breachedaccount/:email', {}, {
get: { method: 'GET', params: { email: '@email' }, isArray: true },
});
function transformUrlEncoded(data) {
return $httpParamSerializer(data);
}

View File

@@ -1,53 +1,73 @@
angular
.module('bit.services')
.factory('authService', function (cryptoService, apiService, tokenService, $q, jwtHelper, $rootScope) {
.factory('authService', function (cryptoService, apiService, tokenService, $q, jwtHelper, $rootScope, constants) {
var _service = {},
_userProfile = null;
_service.logIn = function (email, masterPassword, token, provider) {
_service.logIn = function (email, masterPassword, token, provider, remember) {
email = email.toLowerCase();
var key = cryptoService.makeKey(masterPassword, email);
var request = {
username: email,
password: cryptoService.hashPassword(masterPassword, key),
grant_type: 'password',
scope: 'api offline_access',
client_id: 'web'
};
if (token && typeof (provider) !== 'undefined' && provider !== null) {
request.twoFactorToken = token.replace(' ', '');
request.twoFactorProvider = provider;
}
// TODO: device information one day?
var deferred = $q.defer();
apiService.identity.token(request).$promise.then(function (response) {
var makeResult;
cryptoService.makeKeyAndHash(email, masterPassword).then(function (result) {
makeResult = result;
var request = {
username: email,
password: result.hash,
grant_type: 'password',
scope: 'api offline_access',
client_id: 'web'
};
// TODO: device information one day?
if (token && typeof (provider) !== 'undefined' && provider !== null) {
remember = remember || remember !== false;
request.twoFactorToken = token;
request.twoFactorProvider = provider;
request.twoFactorRemember = remember ? '1' : '0';
}
else if (tokenService.getTwoFactorToken(email)) {
request.twoFactorToken = tokenService.getTwoFactorToken(email);
request.twoFactorProvider = constants.twoFactorProvider.remember;
request.twoFactorRemember = '0';
}
return apiService.identity.token(request).$promise;
}).then(function (response) {
if (!response || !response.access_token) {
return;
}
tokenService.setToken(response.access_token);
tokenService.setRefreshToken(response.refresh_token);
cryptoService.setKey(key);
cryptoService.setKey(makeResult.key);
if (response.TwoFactorToken) {
tokenService.setTwoFactorToken(response.TwoFactorToken, email);
}
if (response.Key) {
cryptoService.setEncKey(response.Key, makeResult.key);
}
if (response.PrivateKey) {
cryptoService.setPrivateKey(response.PrivateKey, key);
cryptoService.setPrivateKey(response.PrivateKey);
return true;
}
else {
return cryptoService.makeKeyPair(key);
return cryptoService.makeKeyPair();
}
}).then(function (keyResults) {
if (keyResults === true) {
return;
}
cryptoService.setPrivateKey(keyResults.privateKeyEnc, key);
cryptoService.setPrivateKey(keyResults.privateKeyEnc);
return apiService.accounts.putKeys({
publicKey: keyResults.publicKey,
encryptedPrivateKey: keyResults.privateKeyEnc
@@ -59,8 +79,10 @@ angular
}, function (error) {
_service.logOut();
if (error.status === 400 && error.data.TwoFactorProviders && error.data.TwoFactorProviders.length) {
deferred.resolve(error.data.TwoFactorProviders);
if (error.status === 400 && error.data.TwoFactorProviders2 &&
Object.keys(error.data.TwoFactorProviders2).length) {
tokenService.clearTwoFactorToken(email);
deferred.resolve(error.data.TwoFactorProviders2);
}
else {
deferred.reject(error);
@@ -71,8 +93,7 @@ angular
};
_service.logOut = function () {
tokenService.clearToken();
tokenService.clearRefreshToken();
tokenService.clearTokens();
cryptoService.clearKeys();
$rootScope.vaultFolders = $rootScope.vaultLogins = null;
_userProfile = null;
@@ -102,11 +123,12 @@ angular
return _setDeferred.promise;
}
var decodedToken = jwtHelper.decodeToken(token);
apiService.accounts.getProfile({}, function (profile) {
_userProfile = {
id: decodedToken.name,
email: decodedToken.email,
id: profile.Id,
email: profile.Email,
emailVerified: profile.EmailVerified,
premium: profile.Premium,
extended: {
name: profile.Name,
twoFactorEnabled: profile.TwoFactorEnabled,
@@ -123,7 +145,12 @@ angular
key: profile.Organizations[i].Key,
status: profile.Organizations[i].Status,
type: profile.Organizations[i].Type,
enabled: profile.Organizations[i].Enabled
enabled: profile.Organizations[i].Enabled,
maxCollections: profile.Organizations[i].MaxCollections,
maxStorageGb: profile.Organizations[i].MaxStorageGb,
seats: profile.Organizations[i].Seats,
useGroups: profile.Organizations[i].UseGroups,
useTotp: profile.Organizations[i].UseTotp
};
}
@@ -151,7 +178,12 @@ angular
key: keyCt,
status: 2, // 2 = Confirmed
type: 0, // 0 = Owner
enabled: true
enabled: true,
maxCollections: org.MaxCollections,
maxStorageGb: org.MaxStorageGb,
seats: org.Seats,
useGroups: org.UseGroups,
useTotp: org.UseTotp
};
profile.organizations[o.id] = o;
@@ -185,6 +217,15 @@ angular
});
};
_service.updateProfilePremium = function (isPremium) {
return _service.getUserProfile().then(function (profile) {
if (profile) {
profile.premium = isPremium;
_userProfile = profile;
}
});
};
_service.isAuthenticated = function () {
return tokenService.getToken() !== null;
};
@@ -203,7 +244,7 @@ angular
tokenService.setToken(response.access_token);
tokenService.setRefreshToken(response.refresh_token);
return response.access_token;
});
}, function (response) { });
};
return _service;

View File

@@ -1,7 +1,7 @@
angular
.module('bit.services')
.factory('cipherService', function (cryptoService, apiService) {
.factory('cipherService', function (cryptoService, apiService, $q, $window) {
var _service = {};
_service.decryptLogins = function (encryptedLogins) {
@@ -15,7 +15,7 @@ angular
return unencryptedLogins;
};
_service.decryptLogin = function (encryptedLogin) {
_service.decryptLogin = function (encryptedLogin, isCipher) {
if (!encryptedLogin) throw "encryptedLogin is undefined or null";
var key = null;
@@ -30,13 +30,30 @@ angular
'type': 1,
folderId: encryptedLogin.FolderId,
favorite: encryptedLogin.Favorite,
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
edit: encryptedLogin.Edit,
organizationUseTotp: encryptedLogin.OrganizationUseTotp,
attachments: null
};
var loginData = encryptedLogin.Data || encryptedLogin;
if (loginData) {
login.name = cryptoService.decrypt(loginData.Name, key);
login.uri = loginData.Uri && loginData.Uri !== '' ? cryptoService.decrypt(loginData.Uri, key) : null;
login.username = loginData.Username && loginData.Username !== '' ? cryptoService.decrypt(loginData.Username, key) : null;
login.password = loginData.Password && loginData.Password !== '' ? cryptoService.decrypt(loginData.Password, key) : null;
login.notes = loginData.Notes && loginData.Notes !== '' ? cryptoService.decrypt(loginData.Notes, key) : null;
login.totp = loginData.Totp && loginData.Totp !== '' ? cryptoService.decrypt(loginData.Totp, key) : null;
}
if (!encryptedLogin.Attachments) {
return login;
}
login.attachments = [];
for (var i = 0; i < encryptedLogin.Attachments.length; i++) {
login.attachments.push(_service.decryptAttachment(key, encryptedLogin.Attachments[i]));
}
return login;
};
@@ -54,13 +71,69 @@ angular
collectionIds: encryptedCipher.CollectionIds || [],
folderId: encryptedCipher.FolderId,
favorite: encryptedCipher.Favorite,
name: _service.decryptProperty(encryptedCipher.Data.Name, key, false),
username: _service.decryptProperty(encryptedCipher.Data.Username, key, true)
edit: encryptedCipher.Edit,
organizationUseTotp: encryptedCipher.OrganizationUseTotp,
hasAttachments: !!encryptedCipher.Attachments && encryptedCipher.Attachments.length > 0
};
var loginData = encryptedCipher.Data || encryptedCipher;
if (loginData) {
login.name = cryptoService.decrypt(loginData.Name, key);
login.username = loginData.Username && loginData.Username !== '' ? cryptoService.decrypt(loginData.Username, key) : null;
login.password = loginData.Password && loginData.Password !== '' ? cryptoService.decrypt(loginData.Password, key) : null;
}
return login;
};
_service.decryptAttachment = function (key, encryptedAttachment) {
if (!encryptedAttachment) throw "encryptedAttachment is undefined or null";
return {
id: encryptedAttachment.Id,
url: encryptedAttachment.Url,
fileName: cryptoService.decrypt(encryptedAttachment.FileName, key),
size: encryptedAttachment.SizeName
};
};
_service.downloadAndDecryptAttachment = function (key, decryptedAttachment, openDownload) {
var deferred = $q.defer();
var req = new XMLHttpRequest();
req.open('GET', decryptedAttachment.url, true);
req.responseType = 'arraybuffer';
req.onload = function (evt) {
if (!req.response) {
deferred.reject('No response');
// error
return;
}
cryptoService.decryptFromBytes(req.response, key).then(function (decBuf) {
if (openDownload) {
var blob = new Blob([decBuf]);
// IE hack. ref http://msdn.microsoft.com/en-us/library/ie/hh779016.aspx
if ($window.navigator.msSaveOrOpenBlob) {
$window.navigator.msSaveBlob(blob, decryptedAttachment.fileName);
}
else {
var a = $window.document.createElement('a');
a.href = $window.URL.createObjectURL(blob);
a.download = decryptedAttachment.fileName;
$window.document.body.appendChild(a);
a.click();
$window.document.body.removeChild(a);
}
}
deferred.resolve(new Uint8Array(decBuf));
});
};
req.send(null);
return deferred.promise;
};
_service.decryptFolders = function (encryptedFolders) {
if (!encryptedFolders) throw "encryptedFolders is undefined or null";
@@ -141,14 +214,14 @@ angular
return encryptedLogins;
};
_service.encryptLogin = function (unencryptedLogin, key) {
_service.encryptLogin = function (unencryptedLogin, key, attachments) {
if (!unencryptedLogin) throw "unencryptedLogin is undefined or null";
if (unencryptedLogin.organizationId) {
key = key || cryptoService.getOrgKey(unencryptedLogin.organizationId);
}
return {
var login = {
id: unencryptedLogin.id,
'type': 1,
organizationId: unencryptedLogin.organizationId || null,
@@ -158,8 +231,45 @@ angular
name: cryptoService.encrypt(unencryptedLogin.name, key),
username: !unencryptedLogin.username || unencryptedLogin.username === '' ? null : cryptoService.encrypt(unencryptedLogin.username, key),
password: !unencryptedLogin.password || unencryptedLogin.password === '' ? null : cryptoService.encrypt(unencryptedLogin.password, key),
notes: !unencryptedLogin.notes || unencryptedLogin.notes === '' ? null : cryptoService.encrypt(unencryptedLogin.notes, key)
notes: !unencryptedLogin.notes || unencryptedLogin.notes === '' ? null : cryptoService.encrypt(unencryptedLogin.notes, key),
totp: !unencryptedLogin.totp || unencryptedLogin.totp === '' ? null : cryptoService.encrypt(unencryptedLogin.totp, key)
};
if (unencryptedLogin.attachments && attachments) {
login.attachments = {};
for (var i = 0; i < unencryptedLogin.attachments.length; i++) {
login.attachments[unencryptedLogin.attachments[i].id] =
cryptoService.encrypt(unencryptedLogin.attachments[i].fileName, key);
}
}
return login;
};
_service.encryptAttachmentFile = function (key, unencryptedFile) {
var deferred = $q.defer();
if (unencryptedFile.size > 104857600) { // 100 MB
deferred.reject('Maximum file size is 100 MB.');
return;
}
var reader = new FileReader();
reader.readAsArrayBuffer(unencryptedFile);
reader.onload = function (evt) {
cryptoService.encryptToBytes(evt.target.result, key).then(function (encData) {
deferred.resolve({
fileName: cryptoService.encrypt(unencryptedFile.name, key),
data: new Uint8Array(encData),
size: unencryptedFile.size
});
});
};
reader.onerror = function (evt) {
deferred.reject('Error reading file.');
};
return deferred.promise;
};
_service.encryptFolders = function (unencryptedFolders, key) {
@@ -202,5 +312,56 @@ angular
};
};
_service.updateKey = function (masterPasswordHash, success, error) {
var madeEncKey = cryptoService.makeEncKey(null);
encKey = madeEncKey.encKey;
var encKeyEnc = madeEncKey.encKeyEnc;
var reencryptedLogins = [];
var loginsPromise = apiService.logins.list({}, function (encryptedLogins) {
var filteredEncryptedLogins = [];
for (var i = 0; i < encryptedLogins.Data.length; i++) {
if (encryptedLogins.Data[i].OrganizationId) {
continue;
}
filteredEncryptedLogins.push(encryptedLogins.Data[i]);
}
var unencryptedLogins = _service.decryptLogins(filteredEncryptedLogins);
reencryptedLogins = _service.encryptLogins(unencryptedLogins, encKey);
}).$promise;
var reencryptedFolders = [];
var foldersPromise = apiService.folders.list({}, function (encryptedFolders) {
var unencryptedFolders = _service.decryptFolders(encryptedFolders.Data);
reencryptedFolders = _service.encryptFolders(unencryptedFolders, encKey);
}).$promise;
var privateKey = cryptoService.getPrivateKey('raw'),
reencryptedPrivateKey = null;
if (privateKey) {
reencryptedPrivateKey = cryptoService.encrypt(privateKey, encKey, 'raw');
}
return $q.all([loginsPromise, foldersPromise]).then(function () {
var request = {
masterPasswordHash: masterPasswordHash,
ciphers: reencryptedLogins,
folders: reencryptedFolders,
privateKey: reencryptedPrivateKey,
key: encKeyEnc
};
return apiService.accounts.putKey(request).$promise;
}, error).then(function () {
cryptoService.setEncKey(encKey, null, true);
return success();
}, function () {
cryptoService.clearEncKey();
error();
});
};
return _service;
});

View File

@@ -1,19 +1,39 @@
angular
.module('bit.services')
.factory('cryptoService', function ($sessionStorage, constants, $q) {
.factory('cryptoService', function ($sessionStorage, constants, $q, $window) {
var _service = {},
_key,
_encKey,
_legacyEtmKey,
_orgKeys,
_privateKey,
_publicKey;
_publicKey,
_crypto = typeof $window.crypto != 'undefined' ? $window.crypto : null,
_subtle = (!!_crypto && typeof $window.crypto.subtle != 'undefined') ? $window.crypto.subtle : null;
_service.setKey = function (key) {
_key = key;
$sessionStorage.key = _key.keyB64;
};
_service.setEncKey = function (encKey, key, alreadyDecrypted) {
if (alreadyDecrypted) {
_encKey = encKey;
$sessionStorage.encKey = _encKey.keyB64;
return;
}
try {
var encKeyBytes = _service.decrypt(encKey, key, 'raw');
$sessionStorage.encKey = forge.util.encode64(encKeyBytes);
_encKey = new SymmetricCryptoKey(encKeyBytes);
}
catch (e) {
console.log('Cannot set enc key. Decryption failed.');
}
};
_service.setPrivateKey = function (privateKeyCt, key) {
try {
var privateKeyBytes = _service.decrypt(privateKeyCt, key, 'raw');
@@ -45,7 +65,7 @@ angular
setKey = true;
}
catch (e) {
console.log('Cannot set org key ' + i + '. Decryption failed.');
console.log('Cannot set org key for ' + orgId + '. Decryption failed.');
}
}
}
@@ -95,6 +115,14 @@ angular
return _key;
};
_service.getEncKey = function () {
if (!_encKey && $sessionStorage.encKey) {
_encKey = new SymmetricCryptoKey($sessionStorage.encKey, true);
}
return _encKey;
};
_service.getPrivateKey = function (outputEncoding) {
outputEncoding = outputEncoding || 'native';
@@ -173,6 +201,11 @@ angular
delete $sessionStorage.key;
};
_service.clearEncKey = function () {
_encKey = null;
delete $sessionStorage.encKey;
};
_service.clearKeyPair = function () {
_privateKey = null;
_publicKey = null;
@@ -196,20 +229,43 @@ angular
_service.clearKeys = function () {
_service.clearKey();
_service.clearEncKey();
_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);
if (!$window.cryptoShimmed && $window.navigator.userAgent.indexOf('Edge') === -1) {
return pbkdf2WC(password, salt, 5000, 256).then(function (keyBuf) {
return new SymmetricCryptoKey(bufToB64(keyBuf), true);
});
}
else {
var deferred = $q.defer();
var keyBytes = forge.pbkdf2(forge.util.encodeUtf8(password), forge.util.encodeUtf8(salt),
5000, 256 / 8, 'sha256');
deferred.resolve(new SymmetricCryptoKey(keyBytes));
return deferred.promise;
}
};
_service.makeEncKey = function (key) {
var encKey = forge.random.getBytesSync(512 / 8);
var encKeyEnc = _service.encrypt(encKey, key, 'raw');
return {
encKey: new SymmetricCryptoKey(encKey),
encKeyEnc: encKeyEnc
};
};
_service.makeKeyPair = function (key) {
var deferred = $q.defer();
forge.pki.rsa.generateKeyPair({ bits: 2048, workers: 2 }, function (error, keypair) {
forge.pki.rsa.generateKeyPair({
bits: 2048,
workers: 2,
workerScript: '/lib/forge/prime.worker.min.js'
}, function (error, keypair) {
if (error) {
deferred.reject(error);
return;
@@ -233,7 +289,7 @@ angular
};
_service.makeShareKeyCt = function () {
return _service.rsaEncrypt(forge.random.getBytesSync(512 / 8));
return _service.rsaEncryptMe(forge.random.getBytesSync(512 / 8));
};
_service.hashPassword = function (password, key) {
@@ -245,12 +301,85 @@ angular
throw 'Invalid parameters.';
}
var hashBits = forge.pbkdf2(key.key, forge.util.encodeUtf8(password), 1, 256 / 8, 'sha256');
return forge.util.encode64(hashBits);
if (!$window.cryptoShimmed && $window.navigator.userAgent.indexOf('Edge') === -1) {
var keyBuf = key.getBuffers();
return pbkdf2WC(new Uint8Array(keyBuf.key), password, 1, 256).then(function (hashBuf) {
return bufToB64(hashBuf);
});
}
else {
var deferred = $q.defer();
var hashBits = forge.pbkdf2(key.key, forge.util.encodeUtf8(password), 1, 256 / 8, 'sha256');
deferred.resolve(forge.util.encode64(hashBits));
return deferred.promise;
}
};
function pbkdf2WC(password, salt, iterations, size) {
password = typeof (password) === 'string' ? utf8ToArray(password) : password;
salt = typeof (salt) === 'string' ? utf8ToArray(salt) : salt;
return _subtle.importKey('raw', password.buffer, { name: 'PBKDF2' }, false, ['deriveKey', 'deriveBits'])
.then(function (importedKey) {
return _subtle.deriveKey(
{ name: 'PBKDF2', salt: salt.buffer, iterations: iterations, hash: { name: 'SHA-256' } },
importedKey, { name: 'AES-CBC', length: size }, true, ['encrypt', 'decrypt']);
}).then(function (derivedKey) {
return _subtle.exportKey('raw', derivedKey);
});
}
_service.makeKeyAndHash = function (email, password) {
email = email.toLowerCase();
var key;
return _service.makeKey(password, email).then(function (theKey) {
key = theKey;
return _service.hashPassword(password, theKey);
}).then(function (theHash) {
return {
key: key,
hash: theHash
};
});
};
_service.encrypt = function (plainValue, key, plainValueEncoding) {
key = key || _service.getKey();
var encValue = aesEncrypt(plainValue, key, plainValueEncoding);
var iv = forge.util.encode64(encValue.iv);
var ct = forge.util.encode64(encValue.ct);
var cipherString = iv + '|' + ct;
if (encValue.mac) {
var mac = forge.util.encode64(encValue.mac);
cipherString = cipherString + '|' + mac;
}
return encValue.key.encType + '.' + cipherString;
};
_service.encryptToBytes = function (plainValue, key) {
return aesEncryptWC(plainValue, key).then(function (encValue) {
var macLen = 0;
if (encValue.mac) {
macLen = encValue.mac.length;
}
var encBytes = new Uint8Array(1 + encValue.iv.length + macLen + encValue.ct.length);
encBytes.set([encValue.key.encType]);
encBytes.set(encValue.iv, 1);
if (encValue.mac) {
encBytes.set(encValue.mac, 1 + encValue.iv.length);
}
encBytes.set(encValue.ct, 1 + encValue.iv.length + macLen);
return encBytes.buffer;
});
};
function aesEncrypt(plainValue, key, plainValueEncoding) {
key = key || _service.getEncKey() || _service.getKey();
if (!key) {
throw 'Encryption key unavailable.';
@@ -264,24 +393,61 @@ angular
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;
var macBytes = null;
if (key.macKey) {
var mac = computeMac(ctBytes, ivBytes, key.macKey, true);
cipherString = cipherString + '|' + mac;
macBytes = computeMac(ivBytes + ctBytes, key.macKey, false);
}
if (key.encType === constants.encType.AesCbc256_B64) {
return cipherString;
return {
iv: ivBytes,
ct: ctBytes,
mac: macBytes,
key: key,
plainValueEncoding: plainValueEncoding
};
}
function aesEncryptWC(plainValue, key) {
key = key || _service.getEncKey() || _service.getKey();
if (!key) {
throw 'Encryption key unavailable.';
}
return key.encType + '.' + cipherString;
};
var obj = {
iv: new Uint8Array(16),
ct: null,
mac: null,
key: key
};
_service.rsaEncrypt = function (plainValue, publicKey) {
var keyBuf = key.getBuffers();
_crypto.getRandomValues(obj.iv);
return _subtle.importKey('raw', keyBuf.encKey, { name: 'AES-CBC' }, false, ['encrypt'])
.then(function (encKey) {
return _subtle.encrypt({ name: 'AES-CBC', iv: obj.iv }, encKey, plainValue);
}).then(function (encValue) {
obj.ct = new Uint8Array(encValue);
if (!keyBuf.macKey) {
return null;
}
var data = new Uint8Array(obj.iv.length + obj.ct.length);
data.set(obj.iv, 0);
data.set(obj.ct, obj.iv.length);
return computeMacWC(data.buffer, keyBuf.macKey);
}).then(function (mac) {
if (mac) {
obj.mac = new Uint8Array(mac);
}
return obj;
});
}
_service.rsaEncrypt = function (plainValue, publicKey, key) {
publicKey = publicKey || _service.getPublicKey();
if (!publicKey) {
throw 'Public key unavailable.';
@@ -295,12 +461,23 @@ angular
var encryptedBytes = publicKey.encrypt(plainValue, 'RSA-OAEP', {
md: forge.md.sha1.create()
});
var cipherString = forge.util.encode64(encryptedBytes);
return constants.encType.Rsa2048_OaepSha1_B64 + '.' + forge.util.encode64(encryptedBytes);
if (key && key.macKey) {
var mac = computeMac(encryptedBytes, key.macKey, true);
return constants.encType.Rsa2048_OaepSha1_HmacSha256_B64 + '.' + cipherString + '|' + mac;
}
else {
return constants.encType.Rsa2048_OaepSha1_B64 + '.' + cipherString;
}
};
_service.rsaEncryptMe = function (plainValue) {
return _service.rsaEncrypt(plainValue, _service.getPublicKey(), _service.getEncKey());
};
_service.decrypt = function (encValue, key, outputEncoding) {
key = key || _service.getKey();
key = key || _service.getEncKey() || _service.getKey();
var headerPieces = encValue.split('.'),
encType,
@@ -334,10 +511,6 @@ angular
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;
@@ -357,7 +530,7 @@ angular
if (key.macKey && encPieces.length > 2) {
var macBytes = forge.util.decode64(encPieces[2]);
var computedMacBytes = computeMac(ctBytes, ivBytes, key.macKey, false);
var computedMacBytes = computeMac(ivBytes + ctBytes, key.macKey, false);
if (!macsEqual(key.macKey, macBytes, computedMacBytes)) {
console.error('MAC failed.');
return null;
@@ -379,37 +552,148 @@ angular
}
};
_service.rsaDecrypt = function (encValue, privateKey) {
_service.decryptFromBytes = function (encBuf, key) {
if (!encBuf) {
throw 'no encBuf.';
}
var encBytes = new Uint8Array(encBuf),
encType = encBytes[0],
ctBytes = null,
ivBytes = null,
macBytes = null;
switch (encType) {
case constants.encType.AesCbc128_HmacSha256_B64:
case constants.encType.AesCbc256_HmacSha256_B64:
if (encBytes.length <= 49) { // 1 + 16 + 32 + ctLength
return null;
}
ivBytes = slice(encBytes, 1, 17);
macBytes = slice(encBytes, 17, 49);
ctBytes = slice(encBytes, 49);
break;
case constants.encType.AesCbc256_B64:
if (encBytes.length <= 17) { // 1 + 16 + ctLength
return null;
}
ivBytes = slice(encBytes, 1, 17);
ctBytes = slice(encBytes, 17);
break;
default:
return null;
}
return aesDecryptWC(
encType,
ctBytes.buffer,
ivBytes.buffer,
macBytes ? macBytes.buffer : null,
key);
};
function aesDecryptWC(encType, ctBuf, ivBuf, macBuf, key) {
key = key || _service.getEncKey() || _service.getKey();
if (!key) {
throw 'Encryption key unavailable.';
}
if (encType !== key.encType) {
throw 'encType unavailable.';
}
var keyBuf = key.getBuffers(),
encKey = null;
return _subtle.importKey('raw', keyBuf.encKey, { name: 'AES-CBC' }, false, ['decrypt'])
.then(function (theEncKey) {
encKey = theEncKey;
if (!key.macKey || !macBuf) {
return null;
}
var data = new Uint8Array(ivBuf.byteLength + ctBuf.byteLength);
data.set(new Uint8Array(ivBuf), 0);
data.set(new Uint8Array(ctBuf), ivBuf.byteLength);
return computeMacWC(data.buffer, keyBuf.macKey);
}).then(function (computedMacBuf) {
if (computedMacBuf === null) {
return null;
}
return macsEqualWC(keyBuf.macKey, macBuf, computedMacBuf);
}).then(function (macsMatch) {
if (macsMatch === false) {
console.error('MAC failed.');
return null;
}
return _subtle.decrypt({ name: 'AES-CBC', iv: ivBuf }, encKey, ctBuf);
});
}
_service.rsaDecrypt = function (encValue, privateKey, key) {
privateKey = privateKey || _service.getPrivateKey();
key = key || _service.getEncKey();
if (!privateKey) {
throw 'Private key unavailable.';
}
var headerPieces = encValue.split('.'),
encType,
encPiece;
encPieces;
if (headerPieces.length === 1) {
encType = constants.encType.Rsa2048_OaepSha256_B64;
encPiece = headerPieces[0];
encPieces = [headerPieces[0]];
}
else if (headerPieces.length === 2) {
try {
encType = parseInt(headerPieces[0]);
encPiece = headerPieces[1];
encPieces = headerPieces[1].split('|');
}
catch (e) {
return null;
}
}
var ctBytes = forge.util.decode64(encPiece);
var md;
switch (encType) {
case constants.encType.Rsa2048_OaepSha256_B64:
case constants.encType.Rsa2048_OaepSha1_B64:
if (encPieces.length !== 1) {
return null;
}
break;
case constants.encType.Rsa2048_OaepSha256_HmacSha256_B64:
case constants.encType.Rsa2048_OaepSha1_HmacSha256_B64:
if (encPieces.length !== 2) {
return null;
}
break;
default:
return null;
}
if (encType === constants.encType.Rsa2048_OaepSha256_B64) {
var ctBytes = forge.util.decode64(encPieces[0]);
if (key && key.macKey && encPieces.length > 1) {
var macBytes = forge.util.decode64(encPieces[1]);
var computedMacBytes = computeMac(ctBytes, key.macKey, false);
if (!macsEqual(key.macKey, macBytes, computedMacBytes)) {
console.error('MAC failed.');
return null;
}
}
var md;
if (encType === constants.encType.Rsa2048_OaepSha256_B64 ||
encType === constants.encType.Rsa2048_OaepSha256_HmacSha256_B64) {
md = forge.md.sha256.create();
}
else if (encType === constants.encType.Rsa2048_OaepSha1_B64) {
else if (encType === constants.encType.Rsa2048_OaepSha1_B64 ||
encType === constants.encType.Rsa2048_OaepSha1_HmacSha256_B64) {
md = forge.md.sha1.create();
}
else {
@@ -423,14 +707,21 @@ angular
return decBytes;
};
function computeMac(ct, iv, macKey, b64Output) {
function computeMac(dataBytes, macKey, b64Output) {
var hmac = forge.hmac.create();
hmac.start('sha256', macKey);
hmac.update(iv + ct);
hmac.update(dataBytes);
var mac = hmac.digest();
return b64Output ? forge.util.encode64(mac.getBytes()) : mac.getBytes();
}
function computeMacWC(dataBuf, macKeyBuf) {
return _subtle.importKey('raw', macKeyBuf, { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign'])
.then(function (key) {
return _subtle.sign({ name: 'HMAC', hash: { name: 'SHA-256' } }, key, dataBuf);
});
}
// 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) {
@@ -447,6 +738,35 @@ angular
return mac1 === mac2;
}
function macsEqualWC(macKeyBuf, mac1Buf, mac2Buf) {
var mac1,
macKey;
return window.crypto.subtle.importKey('raw', macKeyBuf, { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign'])
.then(function (key) {
macKey = key;
return window.crypto.subtle.sign({ name: 'HMAC', hash: { name: 'SHA-256' } }, macKey, mac1Buf);
}).then(function (mac) {
mac1 = mac;
return window.crypto.subtle.sign({ name: 'HMAC', hash: { name: 'SHA-256' } }, macKey, mac2Buf);
}).then(function (mac2) {
if (mac1.byteLength !== mac2.byteLength) {
return false;
}
var arr1 = new Uint8Array(mac1);
var arr2 = new Uint8Array(mac2);
for (var i = 0; i < arr2.length; i++) {
if (arr1[i] !== arr2[i]) {
return false;
}
}
return true;
});
}
function SymmetricCryptoKey(keyBytes, b64KeyBytes, encType) {
if (b64KeyBytes) {
keyBytes = forge.util.decode64(keyBytes);
@@ -495,5 +815,99 @@ angular
}
}
SymmetricCryptoKey.prototype.getBuffers = function () {
if (this.keyBuf) {
return this.keyBuf;
}
var key = b64ToArray(this.keyB64);
var keys = {
key: key.buffer
};
if (this.macKey) {
keys.encKey = slice(key, 0, key.length / 2).buffer;
keys.macKey = slice(key, key.length / 2).buffer;
}
else {
keys.encKey = key.buffer;
keys.macKey = null;
}
this.keyBuf = keys;
return this.keyBuf;
};
function b64ToArray(b64Str) {
var binaryString = $window.atob(b64Str);
var arr = new Uint8Array(binaryString.length);
for (var i = 0; i < binaryString.length; i++) {
arr[i] = binaryString.charCodeAt(i);
}
return arr;
}
function bufToB64(buf) {
var binary = '';
var bytes = new Uint8Array(buf);
for (var i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return $window.btoa(binary);
}
function utf8ToArray(str) {
var utf8Str = unescape(encodeURIComponent(str));
var arr = new Uint8Array(utf8Str.length);
for (var i = 0; i < utf8Str.length; i++) {
arr[i] = utf8Str.charCodeAt(i);
}
return arr;
}
function slice(arr, begin, end) {
if (arr.slice) {
return arr.slice(begin, end);
}
// shim for IE
// ref: https://stackoverflow.com/a/21440217
arr = arr.buffer;
if (begin === void 0) {
begin = 0;
}
if (end === void 0) {
end = arr.byteLength;
}
begin = Math.floor(begin);
end = Math.floor(end);
if (begin < 0) {
begin += arr.byteLength;
}
if (end < 0) {
end += arr.byteLength;
}
begin = Math.min(Math.max(0, begin), arr.byteLength);
end = Math.min(Math.max(0, end), arr.byteLength);
if (end - begin <= 0) {
return new ArrayBuffer(0);
}
var result = new ArrayBuffer(end - begin);
var resultBytes = new Uint8Array(result);
var sourceBytes = new Uint8Array(arr, begin, end - begin);
resultBytes.set(sourceBytes);
return new Uint8Array(result);
}
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':
@@ -92,6 +94,15 @@
case 'splashidcsv':
importSplashIdCsv(file, success, error);
break;
case 'meldiumcsv':
importMeldiumCsv(file, success, error);
break;
case 'passkeepcsv':
importPassKeepCsv(file, success, error);
break;
case 'gnomejson':
importGnomeJson(file, success, error);
break;
default:
error();
break;
@@ -238,6 +249,7 @@
password: value.password && value.password !== '' ? value.password : null,
notes: value.notes && value.notes !== '' ? value.notes : null,
name: value.name && value.name !== '' ? value.name : '--',
totp: value.totp && value.totp !== '' ? value.totp : null
});
if (addFolder) {
@@ -1437,15 +1449,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] === '') {
@@ -2335,5 +2348,185 @@
});
}
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);
}
});
}
function importGnomeJson(file, success, error) {
var folders = [],
logins = [],
loginRelationships = [],
i = 0;
getFileContents(file, parseJson, error);
function parseJson(fileContent) {
var fileJson = JSON.parse(fileContent);
var folderIndex = 0;
var loginIndex = 0;
if (fileJson && Object.keys(fileJson).length) {
for (var keyRing in fileJson) {
if (fileJson.hasOwnProperty(keyRing) && fileJson[keyRing].length) {
folderIndex = folders.length;
folders.push({
name: keyRing
});
for (i = 0; i < fileJson[keyRing].length; i++) {
var item = fileJson[keyRing][i];
if (!item.display_name || item.display_name.indexOf('http') !== 0) {
continue;
}
loginIndex = logins.length;
var login = {
favorite: false,
uri: fixUri(item.display_name),
username: item.attributes.username_value && item.attributes.username_value !== '' ?
item.attributes.username_value : null,
password: item.secret && item.secret !== '' ? item.secret : null,
notes: '',
name: item.display_name.replace('http://', '').replace('https://', ''),
};
if (login.name > 30) {
login.name = login.name.substring(0, 30);
}
for (var attr in item.attributes) {
if (item.attributes.hasOwnProperty(attr) && attr !== 'username_value' &&
attr !== 'xdg:schema') {
if (login.notes !== '') {
login.notes += '\n';
}
login.notes += (attr + ': ' + item.attributes[attr]);
}
}
if (login.notes === '') {
login.notes = null;
}
logins.push(login);
loginRelationships.push({
key: loginIndex,
value: folderIndex
});
}
}
}
}
success(folders, logins, loginRelationships);
}
}
return _service;
});

View File

@@ -1,7 +1,7 @@
angular
.module('bit.services')
.factory('tokenService', function ($sessionStorage, jwtHelper) {
.factory('tokenService', function ($sessionStorage, $localStorage, jwtHelper) {
var _service = {},
_token = null,
_refreshToken = null;
@@ -42,6 +42,33 @@ angular
delete $sessionStorage.refreshToken;
};
_service.setTwoFactorToken = function (token, email) {
if (!$localStorage.twoFactor) {
$localStorage.twoFactor = {};
}
$localStorage.twoFactor[email] = token;
};
_service.getTwoFactorToken = function (email) {
return $localStorage.twoFactor ? $localStorage.twoFactor[email] : null;
};
_service.clearTwoFactorToken = function (email) {
if (email) {
if ($localStorage.twoFactor && $localStorage.twoFactor[email]) {
delete $localStorage.twoFactor[email];
}
}
else {
delete $localStorage.twoFactor;
}
};
_service.clearTokens = function () {
_service.clearToken();
_service.clearRefreshToken();
};
_service.tokenSecondsRemaining = function (token, offsetSeconds) {
var d = jwtHelper.getTokenExpirationDate(token);
offsetSeconds = offsetSeconds || 0;

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", {"apiUri":"https://api.bitwarden.com","identityUri":"https://identity.bitwarden.com","version":"1.10.1","environment":"Production"});
.constant("appSettings", {"apiUri":"https://api.bitwarden.com","identityUri":"https://identity.bitwarden.com","stripeKey":"pk_live_bpN0P37nMxrMQkcaHXtAybJk","braintreeKey":"TODO","version":"1.14.1","environment":"Production"});

View File

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

View File

@@ -0,0 +1,92 @@
angular
.module('bit.organization')
.controller('settingsBillingChangePaymentController', function ($scope, $state, $uibModalInstance, apiService, stripe,
$analytics, toastr, existingPaymentMethod, appSettings, $timeout) {
$analytics.eventTrack('settingsBillingChangePaymentController', { category: 'Modal' });
$scope.existingPaymentMethod = existingPaymentMethod;
$scope.paymentMethod = 'card';
$scope.dropinLoaded = false;
$scope.showPaymentOptions = false;
$scope.card = {};
var btInstance = null;
$scope.changePaymentMethod = function (val) {
$scope.paymentMethod = val;
if ($scope.paymentMethod !== 'paypal') {
return;
}
braintree.dropin.create({
authorization: appSettings.braintreeKey,
container: '#bt-dropin-container',
paymentOptionPriority: ['paypal'],
paypal: {
flow: 'vault',
buttonStyle: {
label: 'pay',
size: 'medium',
shape: 'pill',
color: 'blue'
}
}
}, function (createErr, instance) {
if (createErr) {
console.error(createErr);
return;
}
btInstance = instance;
$timeout(function () {
$scope.dropinLoaded = true;
});
});
};
$scope.submit = function () {
$scope.submitPromise = getPaymentToken($scope.card).then(function (token) {
if (!token) {
throw 'No payment token.';
}
var request = {
paymentToken: token
};
return apiService.accounts.putPayment(null, request).$promise;
}, function (err) {
throw err;
}).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');
};
function getPaymentToken(card) {
if ($scope.paymentMethod === 'paypal') {
return btInstance.requestPaymentMethod().then(function (payload) {
return payload.nonce;
}).catch(function (err) {
throw err.message;
});
}
else {
return stripe.card.createToken(card).then(function (response) {
return response.id;
});
}
}
});

View File

@@ -0,0 +1,163 @@
angular
.module('bit.settings')
.controller('settingsBillingController', function ($scope, apiService, authService, $state, $uibModal, toastr, $analytics) {
$scope.charges = [];
$scope.paymentSource = null;
$scope.subscription = null;
$scope.loading = true;
$scope.$on('$viewContentLoaded', function () {
load();
});
$scope.changePayment = function () {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsBillingChangePayment.html',
controller: 'settingsBillingChangePaymentController',
resolve: {
existingPaymentMethod: function () {
return $scope.paymentSource ? $scope.paymentSource.description : null;
}
}
});
modal.result.then(function () {
load();
});
};
$scope.adjustStorage = function (add) {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsBillingAdjustStorage.html',
controller: 'settingsBillingAdjustStorageController',
resolve: {
add: function () {
return add;
}
}
});
modal.result.then(function () {
load();
});
};
$scope.cancel = function () {
if (!confirm('Are you sure you want to cancel? You will lose access to all premium features at the end ' +
'of this billing cycle.')) {
return;
}
apiService.accounts.putCancelPremium({}, {})
.$promise.then(function (response) {
$analytics.eventTrack('Canceled Premium');
toastr.success('Premium subscription has been canceled.');
load();
});
};
$scope.reinstate = function () {
if (!confirm('Are you sure you want to remove the cancellation request and reinstate your premium membership?')) {
return;
}
apiService.accounts.putReinstatePremium({}, {})
.$promise.then(function (response) {
$analytics.eventTrack('Reinstated Premium');
toastr.success('Premium cancellation request has been removed.');
load();
});
};
function load() {
authService.getUserProfile().then(function (profile) {
$scope.premium = profile.premium;
if (!profile.premium) {
return null;
}
return apiService.accounts.getBilling({}).$promise;
}).then(function (billing) {
if (!billing) {
return $state.go('backend.user.settingsPremium');
}
var i = 0;
$scope.storage = null;
if (billing && billing.MaxStorageGb) {
$scope.storage = {
currentGb: billing.StorageGb || 0,
maxGb: billing.MaxStorageGb,
currentName: billing.StorageName || '0 GB'
};
$scope.storage.percentage = +(100 * ($scope.storage.currentGb / $scope.storage.maxGb)).toFixed(2);
}
$scope.subscription = null;
if (billing && billing.Subscription) {
$scope.subscription = {
trialEndDate: billing.Subscription.TrialEndDate,
cancelledDate: billing.Subscription.CancelledDate,
status: billing.Subscription.Status,
cancelled: billing.Subscription.Cancelled,
markedForCancel: !billing.Subscription.Cancelled && billing.Subscription.CancelAtEndDate
};
}
$scope.nextInvoice = null;
if (billing && billing.UpcomingInvoice) {
$scope.nextInvoice = {
date: billing.UpcomingInvoice.Date,
amount: billing.UpcomingInvoice.Amount
};
}
if (billing && billing.Subscription && billing.Subscription.Items) {
$scope.subscription.items = [];
for (i = 0; i < billing.Subscription.Items.length; i++) {
$scope.subscription.items.push({
amount: billing.Subscription.Items[i].Amount,
name: billing.Subscription.Items[i].Name,
interval: billing.Subscription.Items[i].Interval,
qty: billing.Subscription.Items[i].Quantity
});
}
}
$scope.paymentSource = null;
if (billing && billing.PaymentSource) {
$scope.paymentSource = {
type: billing.PaymentSource.Type,
description: billing.PaymentSource.Description,
cardBrand: billing.PaymentSource.CardBrand
};
}
var charges = [];
if (billing && billing.Charges) {
for (i = 0; i < billing.Charges.length; i++) {
charges.push({
date: billing.Charges[i].CreatedDate,
paymentSource: billing.Charges[i].PaymentSource ?
billing.Charges[i].PaymentSource.Description : '-',
amount: billing.Charges[i].Amount,
status: billing.Charges[i].Status,
failureMessage: billing.Charges[i].FailureMessage,
refunded: billing.Charges[i].Refunded,
partiallyRefunded: billing.Charges[i].PartiallyRefunded,
refundedAmount: billing.Charges[i].RefundedAmount,
invoiceId: billing.Charges[i].InvoiceId
});
}
}
$scope.charges = charges;
$scope.loading = false;
});
}
});

View File

@@ -4,81 +4,69 @@
.controller('settingsChangeEmailController', function ($scope, $state, apiService, $uibModalInstance, cryptoService,
cipherService, authService, $q, toastr, $analytics) {
$analytics.eventTrack('settingsChangeEmailController', { category: 'Modal' });
var _masterPasswordHash,
_newMasterPasswordHash,
_newKey;
_masterPassword,
_newEmail;
$scope.token = function (model) {
_masterPasswordHash = cryptoService.hashPassword(model.masterPassword);
var newEmail = model.newEmail.toLowerCase();
_masterPassword = model.masterPassword;
_newEmail = model.newEmail.toLowerCase();
cryptoService.hashPassword(_masterPassword).then(function (hash) {
_masterPasswordHash = hash;
var encKey = cryptoService.getEncKey();
if (encKey) {
$scope.tokenPromise = requestToken();
}
else {
// User is not using an enc key, let's make them one
$scope.tokenPromise = cipherService.updateKey(_masterPasswordHash, function () {
return requestToken();
}, function (err) {
toastr.error('Something went wrong.', 'Oh No!');
});
}
});
};
function requestToken() {
var request = {
newEmail: newEmail,
newEmail: _newEmail,
masterPasswordHash: _masterPasswordHash
};
$scope.tokenPromise = apiService.accounts.emailToken(request, function () {
_newKey = cryptoService.makeKey(model.masterPassword, newEmail);
_newMasterPasswordHash = cryptoService.hashPassword(model.masterPassword, _newKey);
return apiService.accounts.emailToken(request, function () {
$scope.tokenSent = true;
}).$promise;
};
}
$scope.confirm = function (model) {
$scope.processing = true;
var reencryptedLogins = [];
var loginsPromise = apiService.logins.list({}, function (encryptedLogins) {
var filteredEncryptedLogins = [];
for (var i = 0; i < encryptedLogins.Data.length; i++) {
if (encryptedLogins.Data[i].OrganizationId) {
continue;
}
filteredEncryptedLogins.push(encryptedLogins.Data[i]);
}
var unencryptedLogins = 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 () {
$scope.confirmPromise = cryptoService.makeKeyAndHash(_newEmail, _masterPassword).then(function (result) {
var encKey = cryptoService.getEncKey();
var newEncKey = cryptoService.encrypt(encKey.key, result.key, 'raw');
var request = {
token: model.token,
newEmail: model.newEmail.toLowerCase(),
newEmail: _newEmail,
masterPasswordHash: _masterPasswordHash,
newMasterPasswordHash: _newMasterPasswordHash,
data: {
ciphers: reencryptedLogins,
folders: reencryptedFolders,
privateKey: reencryptedPrivateKey
}
newMasterPasswordHash: result.hash,
key: newEncKey
};
$scope.confirmPromise = apiService.accounts.email(request, function () {
$uibModalInstance.dismiss('cancel');
$analytics.eventTrack('Changed Email');
authService.logOut();
$state.go('frontend.login.info').then(function () {
toastr.success('Please log back in.', 'Email Changed');
});
}, function () {
$uibModalInstance.dismiss('cancel');
toastr.error('Something went wrong.', 'Oh No!');
}).$promise;
return apiService.accounts.email(request).$promise;
}).then(function () {
$uibModalInstance.dismiss('cancel');
authService.logOut();
$analytics.eventTrack('Changed Email');
return $state.go('frontend.login.info');
}).then(function () {
toastr.success('Please log back in.', 'Email Changed');
}, function () {
$uibModalInstance.dismiss('cancel');
toastr.error('Something went wrong. Try again.', 'Oh No!');
});
};

View File

@@ -2,8 +2,9 @@
.module('bit.settings')
.controller('settingsChangePasswordController', function ($scope, $state, apiService, $uibModalInstance,
cryptoService, authService, cipherService, validationService, $q, toastr, $analytics) {
cryptoService, authService, cipherService, validationService, toastr, $analytics) {
$analytics.eventTrack('settingsChangePasswordController', { category: 'Modal' });
$scope.save = function (model, form) {
var error = false;
@@ -24,63 +25,53 @@
$scope.processing = true;
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;
}
filteredEncryptedLogins.push(encryptedLogins.Data[i]);
}
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;
var encKey = cryptoService.getEncKey();
if (encKey) {
$scope.savePromise = changePassword(model);
}
else {
// User is not using an enc key, let's make them one
$scope.savePromise = cryptoService.hashPassword(model.masterPassword).then(function (hash) {
return cipherService.updateKey(hash);
}).then(function () {
return changePassword(model);
}, function (err) {
toastr.error('Something went wrong.', 'Oh No!');
});
});
}
};
function changePassword(model) {
var makeResult;
return authService.getUserProfile().then(function (profile) {
return cryptoService.makeKeyAndHash(profile.email, model.newMasterPassword);
}).then(function (result) {
makeResult = result;
return cryptoService.hashPassword(model.masterPassword);
}).then(function (hash) {
var encKey = cryptoService.getEncKey();
var newEncKey = cryptoService.encrypt(encKey.key, makeResult.key, 'raw');
var request = {
masterPasswordHash: hash,
newMasterPasswordHash: makeResult.hash,
key: newEncKey
};
return apiService.accounts.putPassword(request).$promise;
}).then(function () {
$uibModalInstance.dismiss('cancel');
authService.logOut();
$analytics.eventTrack('Changed Password');
return $state.go('frontend.login.info');
}).then(function () {
toastr.success('Please log back in.', 'Master Password Changed');
}, function () {
$uibModalInstance.dismiss('cancel');
toastr.error('Something went wrong.', 'Oh No!');
});
}
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};

View File

@@ -105,22 +105,6 @@
});
};
$scope.twoFactor = function () {
var twoFactorModal = $uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsTwoFactor.html',
controller: 'settingsTwoFactorController'
});
twoFactorModal.result.then(function (enabled) {
if (enabled === null) {
return;
}
$scope.model.twoFactorEnabled = enabled;
});
};
$scope.sessions = function () {
$uibModal.open({
animation: true,

View File

@@ -4,22 +4,26 @@
.controller('settingsCreateOrganizationController', function ($scope, $state, apiService, cryptoService,
toastr, $analytics, authService, stripe, constants) {
$scope.plans = constants.plans;
$scope.storageGb = constants.storageGb;
$scope.model = {
plan: 'free',
additionalSeats: 0,
interval: 'year',
ownedBusiness: false
ownedBusiness: false,
additionalStorageGb: null
};
$scope.totalPrice = function () {
if ($scope.model.interval === 'month') {
return ($scope.model.additionalSeats || 0) * $scope.plans[$scope.model.plan].monthlySeatPrice +
$scope.plans[$scope.model.plan].monthlyBasePrice;
return (($scope.model.additionalSeats || 0) * ($scope.plans[$scope.model.plan].monthlySeatPrice || 0)) +
(($scope.model.additionalStorageGb || 0) * $scope.storageGb.monthlyPrice) +
($scope.plans[$scope.model.plan].monthlyBasePrice || 0);
}
else {
return ($scope.model.additionalSeats || 0) * $scope.plans[$scope.model.plan].annualSeatPrice +
$scope.plans[$scope.model.plan].annualBasePrice;
return (($scope.model.additionalSeats || 0) * ($scope.plans[$scope.model.plan].annualSeatPrice || 0)) +
(($scope.model.additionalStorageGb || 0) * $scope.storageGb.yearlyPrice) +
($scope.plans[$scope.model.plan].annualBasePrice || 0);
}
};
@@ -27,6 +31,14 @@
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 () {
@@ -57,6 +69,7 @@
key: shareKeyCt,
paymentToken: response.id,
additionalSeats: model.additionalSeats,
additionalStorageGb: model.additionalStorageGb,
billingEmail: model.billingEmail,
businessName: model.ownedBusiness ? model.businessName : null
};
@@ -66,8 +79,6 @@
}
function finalizeCreate(result) {
$scope.model.card = null;
$analytics.eventTrack('Created Organization');
authService.addProfileOrganizationOwner(result, shareKeyCt);
authService.refreshAccessToken().then(function () {

View File

@@ -5,18 +5,18 @@
authService, toastr, $analytics) {
$analytics.eventTrack('settingsDeleteController', { category: 'Modal' });
$scope.submit = function (model) {
var request = {
masterPasswordHash: cryptoService.hashPassword(model.masterPassword)
};
$scope.submitPromise = apiService.accounts.postDelete(request, function () {
$scope.submitPromise = cryptoService.hashPassword(model.masterPassword).then(function (hash) {
return apiService.accounts.postDelete({
masterPasswordHash: hash
}).$promise;
}).then(function () {
$uibModalInstance.dismiss('cancel');
authService.logOut();
$analytics.eventTrack('Deleted Account');
$state.go('frontend.login.info').then(function () {
toastr.success('Your account has been closed and all associated data has been deleted.', 'Account Deleted');
});
}).$promise;
return $state.go('frontend.login.info');
}).then(function () {
toastr.success('Your account has been closed and all associated data has been deleted.', 'Account Deleted');
});
};
$scope.close = function () {

View File

@@ -0,0 +1,97 @@
angular
.module('bit.settings')
.controller('settingsPremiumController', function ($scope, $state, apiService, toastr, $analytics, authService, stripe,
constants, $timeout, appSettings) {
authService.getUserProfile().then(function (profile) {
if (profile.premium) {
return $state.go('backend.user.settingsBilling');
}
});
var btInstance = null;
$scope.storageGbPrice = constants.storageGb.yearlyPrice;
$scope.premiumPrice = constants.premium.price;
$scope.paymentMethod = 'card';
$scope.dropinLoaded = false;
$scope.model = {
additionalStorageGb: null
};
$scope.changePaymentMethod = function () {
if ($scope.paymentMethod !== 'paypal') {
return;
}
braintree.dropin.create({
authorization: appSettings.braintreeKey,
container: '#bt-dropin-container',
paymentOptionPriority: ['paypal'],
paypal: {
flow: 'vault',
buttonStyle: {
label: 'pay',
size: 'medium',
shape: 'pill',
color: 'blue'
}
}
}, function (createErr, instance) {
if (createErr) {
console.error(createErr);
return;
}
btInstance = instance;
$timeout(function () {
$scope.dropinLoaded = true;
});
});
};
$scope.totalPrice = function () {
return $scope.premiumPrice + (($scope.model.additionalStorageGb || 0) * $scope.storageGbPrice);
};
$scope.submit = function (model) {
$scope.submitPromise = getPaymentToken(model).then(function (token) {
if (!token) {
throw 'No payment token.';
}
var request = {
paymentToken: token,
additionalStorageGb: model.additionalStorageGb
};
return apiService.accounts.postPremium(request).$promise;
}, function (err) {
throw err;
}).then(function (result) {
return authService.updateProfilePremium(true);
}).then(function () {
$analytics.eventTrack('Signed Up Premium');
return authService.refreshAccessToken();
}).then(function () {
return $state.go('backend.user.settingsBilling');
}).then(function () {
toastr.success('Premium upgrade complete.', 'Success');
});
};
function getPaymentToken(model) {
if ($scope.paymentMethod === 'paypal') {
return btInstance.requestPaymentMethod().then(function (payload) {
return payload.nonce;
}).catch(function (err) {
throw err.message;
});
}
else {
return stripe.card.createToken(model.card).then(function (response) {
return response.id;
});
}
}
});

View File

@@ -2,21 +2,28 @@
.module('bit.settings')
.controller('settingsSessionsController', function ($scope, $state, apiService, $uibModalInstance, cryptoService,
authService, toastr, $analytics) {
authService, tokenService, toastr, $analytics) {
$analytics.eventTrack('settingsSessionsController', { category: 'Modal' });
$scope.submit = function (model) {
var request = {
masterPasswordHash: cryptoService.hashPassword(model.masterPassword)
};
var hash, profile;
$scope.submitPromise = apiService.accounts.putSecurityStamp(request, function () {
$scope.submitPromise = cryptoService.hashPassword(model.masterPassword).then(function (theHash) {
hash = theHash;
return authService.getUserProfile();
}).then(function (theProfile) {
profile = theProfile;
return apiService.accounts.putSecurityStamp({
masterPasswordHash: hash
}).$promise;
}).then(function () {
$uibModalInstance.dismiss('cancel');
authService.logOut();
tokenService.clearTwoFactorToken(profile.email);
$analytics.eventTrack('Deauthorized Sessions');
$state.go('frontend.login.info').then(function () {
toastr.success('Please log back in.', 'All Sessions Deauthorized');
});
}).$promise;
return $state.go('frontend.login.info');
}).then(function () {
toastr.success('Please log back in.', 'All Sessions Deauthorized');
});
};
$scope.close = function () {

View File

@@ -1,93 +0,0 @@
angular
.module('bit.settings')
.controller('settingsTwoFactorController', function ($scope, apiService, $uibModalInstance, cryptoService, authService,
$q, toastr, $analytics) {
$analytics.eventTrack('settingsTwoFactorController', { category: 'Modal' });
var _issuer = 'bitwarden',
_profile = null,
_masterPasswordHash;
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);
$scope.authPromise = apiService.accounts.getTwoFactor({
masterPasswordHash: _masterPasswordHash,
provider: 0 /* Only authenticator provider for now. */
}, function (response) {
processResponse(response);
}).$promise;
};
function formatString(s) {
if (!s) {
return null;
}
return s.replace(/(.{4})/g, '$1 ').trim().toUpperCase();
}
function processResponse(response) {
var key = response.AuthenticatorKey;
$scope.twoFactorModel = {
enabled: response.TwoFactorEnabled,
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
};
}
$scope.update = function (model) {
var currentlyEnabled = $scope.twoFactorModel.enabled;
if (currentlyEnabled && !confirm('Are you sure you want to disable two-step login?')) {
return;
}
var request = {
enabled: !currentlyEnabled,
token: model.token.replace(' ', ''),
masterPasswordHash: _masterPasswordHash
};
$scope.updatePromise = apiService.accounts.putTwoFactor({}, request, function (response) {
if (response.TwoFactorEnabled) {
$analytics.eventTrack('Enabled Two-step Login');
toastr.success('Two-step login has been enabled.');
if (_profile.extended) _profile.extended.twoFactorEnabled = true;
processResponse(response);
$('#token').blur();
model.token = null;
}
else {
$analytics.eventTrack('Disabled Two-step Login');
toastr.success('Two-step login has been disabled.');
if (_profile.extended) _profile.extended.twoFactorEnabled = false;
$scope.close();
}
}).$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.close(!_profile.extended ? null : _profile.extended.twoFactorEnabled);
};
});

View File

@@ -0,0 +1,108 @@
angular
.module('bit.settings')
.controller('settingsTwoStepAuthenticatorController', function ($scope, apiService, $uibModalInstance, cryptoService,
authService, $q, toastr, $analytics, constants, $timeout) {
$analytics.eventTrack('settingsTwoStepAuthenticatorController', { category: 'Modal' });
var _issuer = 'bitwarden',
_profile = null,
_masterPasswordHash,
_key = null;
$timeout(function () {
$("#masterPassword").focus();
});
$scope.auth = function (model) {
var response = null;
$scope.authPromise = cryptoService.hashPassword(model.masterPassword).then(function (hash) {
_masterPasswordHash = hash;
return apiService.twoFactor.getAuthenticator({}, {
masterPasswordHash: _masterPasswordHash
}).$promise;
}).then(function (apiResponse) {
response = apiResponse;
return authService.getUserProfile();
}).then(function (profile) {
_profile = profile;
$scope.account = _profile.email;
processResponse(response);
});
};
function formatString(s) {
if (!s) {
return null;
}
return s.replace(/(.{4})/g, '$1 ').trim().toUpperCase();
}
function processResponse(response) {
$scope.enabled = response.Enabled;
_key = response.Key;
$scope.model = {
key: formatString(_key),
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
};
$scope.updateModel = {
token: null
};
}
$scope.submit = function (model) {
if (!model || !model.token) {
disable();
return;
}
update(model);
};
function disable() {
if (!confirm('Are you sure you want to disable the authenticator app provider?')) {
return;
}
$scope.submitPromise = apiService.twoFactor.disable({}, {
masterPasswordHash: _masterPasswordHash,
type: constants.twoFactorProvider.authenticator
}, function (response) {
$analytics.eventTrack('Disabled Two-step Authenticator');
toastr.success('Authenticator app has been disabled.');
$scope.enabled = response.Enabled;
$scope.close();
}).$promise;
}
function update(model) {
$scope.submitPromise = apiService.twoFactor.putAuthenticator({}, {
token: model.token.replace(' ', ''),
key: _key,
masterPasswordHash: _masterPasswordHash
}, function (response) {
$analytics.eventTrack('Enabled Two-step Authenticator');
processResponse(response);
model.token = null;
}).$promise;
}
var closing = false;
$scope.close = function () {
closing = true;
$uibModalInstance.close($scope.enabled);
};
$scope.$on('modal.closing', function (e, reason, closed) {
if (closing) {
return;
}
e.preventDefault();
$scope.close();
});
});

View File

@@ -0,0 +1,82 @@
angular
.module('bit.settings')
.controller('settingsTwoStepController', function ($scope, apiService, toastr, $analytics, constants,
$filter, $uibModal, authService) {
$scope.providers = constants.twoFactorProviderInfo;
$scope.premium = true;
authService.getUserProfile().then(function (profile) {
$scope.premium = profile.premium;
return apiService.twoFactor.list({}).$promise;
}).then(function (response) {
if (response.Data) {
for (var i = 0; i < response.Data.length; i++) {
if (!response.Data[i].Enabled) {
continue;
}
var provider = $filter('filter')($scope.providers, { type: response.Data[i].Type });
if (provider.length) {
provider[0].enabled = true;
}
}
}
return;
});
$scope.edit = function (provider) {
if (!$scope.premium && !provider.free) {
$uibModal.open({
animation: true,
templateUrl: 'app/views/premiumRequired.html',
controller: 'premiumRequiredController'
});
return;
}
if (provider.type === constants.twoFactorProvider.authenticator) {
typeName = 'Authenticator';
}
else if (provider.type === constants.twoFactorProvider.email) {
typeName = 'Email';
}
else if (provider.type === constants.twoFactorProvider.yubikey) {
typeName = 'Yubi';
}
else if (provider.type === constants.twoFactorProvider.duo) {
typeName = 'Duo';
}
else if (provider.type === constants.twoFactorProvider.u2f) {
typeName = 'U2f';
}
else {
return;
}
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsTwoStep' + typeName + '.html',
controller: 'settingsTwoStep' + typeName + 'Controller',
resolve: {
enabled: function () { return provider.enabled; }
}
});
modal.result.then(function (enabled) {
if (enabled || enabled === false) {
// do not adjust when undefined or null
provider.enabled = enabled;
}
});
};
$scope.viewRecover = function () {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsTwoStepRecover.html',
controller: 'settingsTwoStepRecoverController'
});
};
});

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