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

Compare commits

...

69 Commits

Author SHA1 Message Date
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
Kyle Spearrin
565c6bafae version bump 2017-05-08 08:15:39 -04:00
Kyle Spearrin
584e8131cd version bump 2017-05-06 21:32:56 -04:00
Kyle Spearrin
20e958b1ee new identity server uri for auth 2017-05-06 21:32:51 -04:00
104 changed files with 2584 additions and 433 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**
@@ -26,4 +28,4 @@ You can now access the web vault at `http://localhost:4001`.
Code contributions are welcome! Please commit any pull requests against the `master` branch.
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature.
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file.

45
SECURITY.md Normal file
View File

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

View File

@@ -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,12 @@ 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.webroot + 'js/fallback*.js'
], { base: '.' })
.pipe(concat(paths.concatJsDest))
.pipe(uglify())
.pipe(gulp.dest('.'));
@@ -130,6 +135,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,6 +167,10 @@ gulp.task('lib', ['clean:lib'], function () {
src: paths.npmDir + 'clipboard/dist/clipboard*.js',
dest: paths.libDir + 'clipboard'
},
{
src: paths.npmDir + 'node-forge/dist/prime.worker.*',
dest: paths.libDir + 'forge'
},
{
src: [
paths.npmDir + 'angulartics-google-analytics/lib/angulartics*.js',
@@ -293,6 +306,14 @@ gulp.task('dist:move', function () {
src: paths.npmDir + 'angular/angular.min.js',
dest: paths.dist + 'lib/angular'
},
{
src: paths.npmDir + 'node-forge/dist/prime.worker.*',
dest: paths.dist + 'lib/forge'
},
{
src: paths.webroot + 'js/bw.min.js',
dest: paths.dist + 'js'
},
{
src: [
paths.webroot + '**/app/**/*.html',
@@ -340,6 +361,19 @@ gulp.task('dist:js:app', function () {
.pipe(gulp.dest('.'));
});
gulp.task('dist:js:fallback', function () {
var mainStream = gulp
.src([
paths.webroot + 'js/fallback*.js'
]);
merge(mainStream, config())
.pipe(preprocess({ context: { cacheTag: randomString } }))
.pipe(uglify())
.pipe(rename({ suffix: '.min' }))
.pipe(gulp.dest(paths.dist + 'js'));
});
gulp.task('dist:js:lib', function () {
return gulp
.src([
@@ -367,7 +401,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:preprocess',
cb);
});

View File

@@ -1,6 +1,6 @@
{
"name": "bitwarden",
"version": "1.10.0",
"version": "1.13.0",
"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",

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
{
"appSettings": {
"apiUri": "http://localhost:4000"
"apiUri": "http://localhost:4000",
"identityUri": "http://localhost:33656",
"stripeKey": "pk_test_KPoCfZXu7mznb9uSCPZ2JpTD"
}
}

View File

@@ -42,13 +42,15 @@ angular
var email = $scope.model.email.toLowerCase();
var key = cryptoService.makeKey($scope.model.masterPassword, email);
var encKey = cryptoService.makeEncKey(key);
$scope.registerPromise = cryptoService.makeKeyPair(key).then(function (result) {
$scope.registerPromise = cryptoService.makeKeyPair(encKey.encKey).then(function (result) {
var request = {
name: $scope.model.name,
email: email,
masterPasswordHash: cryptoService.hashPassword($scope.model.masterPassword, key),
masterPasswordHint: $scope.model.masterPasswordHint,
key: encKey.encKeyEnc,
keys: {
publicKey: result.publicKey,
encryptedPrivateKey: result.privateKeyEnc

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>

View File

@@ -1,7 +1,7 @@
<p class="login-box-msg">Enter your two-step verification code.</p>
<form name="twoFactorForm" ng-submit="twoFactorForm.$valid && twoFactor(model)" api-form="twoFactorPromise">
<div class="callout callout-danger validation-errors" ng-show="twoFactorForm.$errors">
<h4>Errors have occured</h4>
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in twoFactorForm.$errors">{{e}}</li>
</ul>

View File

@@ -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,12 @@
<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">
Lost your authenticator app?
<a href="https://help.bitwarden.com/article/lost-two-step-device/" target="_blank">
Help me!
</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 +18,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

@@ -19,5 +19,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_token2',
whiteListedDomains: ['api.bitwarden.com', 'preview-api.bitwarden.com', 'localhost', '192.168.1.8']
urlParam: 'access_token3',
whiteListedDomains: ['api.bitwarden.com', 'preview-api.bitwarden.com', 'localhost', '192.168.1.6']
});
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,
@@ -77,7 +77,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
}
@@ -112,6 +115,12 @@ angular
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',

View File

@@ -46,6 +46,14 @@ angular.module('bit')
monthPlanType: 'teamsMonthly',
annualPlanType: 'teamsAnnually',
upgradeSortOrder: 2
},
enterprise: {
seatPrice: 3,
annualSeatPrice: 36,
monthlySeatPrice: 4,
monthPlanType: 'enterpriseMonthly',
annualPlanType: 'enterpriseAnnually',
upgradeSortOrder: 3
}
}
});

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

@@ -4,6 +4,7 @@ angular
.controller('mainController', function ($scope, $state, authService, appSettings, toastr, $window, $document) {
var vm = this;
vm.bodyClass = '';
vm.usingControlSidebar = vm.openControlSidebar = false;
vm.searchVaultText = null;
vm.version = appSettings.version;
@@ -24,7 +25,7 @@ angular
$.AdminLTE.pushMenu.expandOnHover();
}
$(document).off('click', '.sidebar li a');
$document.off('click', '.sidebar li a');
}
});
@@ -38,6 +39,9 @@ angular
else {
vm.bodyClass = '';
}
vm.usingControlSidebar = !!toState.data.controlSidebar;
vm.openControlSidebar = vm.usingControlSidebar && $document.width() > 768;
});
$scope.addLogin = function () {
@@ -60,6 +64,10 @@ angular
$scope.$broadcast('organizationPeopleInvite');
};
$scope.addOrganizationGroup = function () {
$scope.$broadcast('organizationGroupsAdd');
};
// Append dropdown menu somewhere else
var bodyScrollbarWidth,
appendedDropdownMenu,

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

@@ -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

@@ -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

@@ -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

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

@@ -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,23 @@
</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="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 +66,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

@@ -3,7 +3,8 @@
.factory('apiService', function ($resource, tokenService, appSettings, $httpParamSerializer) {
var _service = {},
_apiUri = appSettings.apiUri;
_apiUri = appSettings.apiUri,
_identityUri = appSettings.identityUri;
_service.logins = $resource(_apiUri + '/logins/:id', {}, {
get: { method: 'GET', params: { id: '@id' } },
@@ -26,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: {} },
@@ -37,7 +38,9 @@
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' }
});
_service.organizations = $resource(_apiUri + '/organizations/:id', {}, {
@@ -58,26 +61,37 @@
_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', {}, {
@@ -95,6 +109,7 @@
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: {} }
});
@@ -108,9 +123,9 @@
getPublicKey: { url: _apiUri + '/users/:id/public-key', method: 'GET', params: { id: '@id' } }
});
_service.identity = $resource(_apiUri + '/connect', {}, {
_service.identity = $resource(_identityUri + '/connect', {}, {
token: {
url: _apiUri + '/connect/token',
url: _identityUri + '/connect/token',
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' },
transformRequest: transformUrlEncoded,
@@ -119,6 +134,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

@@ -35,19 +35,23 @@ angular
tokenService.setRefreshToken(response.refresh_token);
cryptoService.setKey(key);
if (response.Key) {
cryptoService.setEncKey(response.Key, 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
@@ -123,7 +127,10 @@ 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,
seats: profile.Organizations[i].Seats,
useGroups: profile.Organizations[i].UseGroups
};
}
@@ -151,7 +158,10 @@ angular
key: keyCt,
status: 2, // 2 = Confirmed
type: 0, // 0 = Owner
enabled: true
enabled: true,
maxCollections: org.MaxCollections,
seats: org.Seats,
useGroups: org.UseGroups
};
profile.organizations[o.id] = o;
@@ -203,7 +213,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) {
var _service = {};
_service.decryptLogins = function (encryptedLogins) {
@@ -30,6 +30,7 @@ angular
'type': 1,
folderId: encryptedLogin.FolderId,
favorite: encryptedLogin.Favorite,
edit: encryptedLogin.Edit,
name: cryptoService.decrypt(encryptedLogin.Name, key),
uri: encryptedLogin.Uri && encryptedLogin.Uri !== '' ? cryptoService.decrypt(encryptedLogin.Uri, key) : null,
username: encryptedLogin.Username && encryptedLogin.Username !== '' ? cryptoService.decrypt(encryptedLogin.Username, key) : null,
@@ -54,8 +55,10 @@ angular
collectionIds: encryptedCipher.CollectionIds || [],
folderId: encryptedCipher.FolderId,
favorite: encryptedCipher.Favorite,
edit: encryptedCipher.Edit,
name: _service.decryptProperty(encryptedCipher.Data.Name, key, false),
username: _service.decryptProperty(encryptedCipher.Data.Username, key, true)
username: _service.decryptProperty(encryptedCipher.Data.Username, key, true),
password: _service.decryptProperty(encryptedCipher.Data.Password, key, true)
};
return login;
@@ -202,5 +205,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

@@ -4,6 +4,7 @@ angular
.factory('cryptoService', function ($sessionStorage, constants, $q) {
var _service = {},
_key,
_encKey,
_legacyEtmKey,
_orgKeys,
_privateKey,
@@ -14,6 +15,23 @@ angular
$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 +63,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 +113,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 +199,11 @@ angular
delete $sessionStorage.key;
};
_service.clearEncKey = function () {
_encKey = null;
delete $sessionStorage.encKey;
};
_service.clearKeyPair = function () {
_privateKey = null;
_publicKey = null;
@@ -196,6 +227,7 @@ angular
_service.clearKeys = function () {
_service.clearKey();
_service.clearEncKey();
_service.clearKeyPair();
_service.clearOrgKeys();
};
@@ -206,10 +238,23 @@ angular
return new SymmetricCryptoKey(keyBytes);
};
_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;
@@ -250,7 +295,7 @@ angular
};
_service.encrypt = function (plainValue, key, plainValueEncoding) {
key = key || _service.getKey();
key = key || _service.getEncKey() || _service.getKey();
if (!key) {
throw 'Encryption key unavailable.';
@@ -274,10 +319,6 @@ angular
cipherString = cipherString + '|' + mac;
}
if (key.encType === constants.encType.AesCbc256_B64) {
return cipherString;
}
return key.encType + '.' + cipherString;
};
@@ -300,7 +341,7 @@ angular
};
_service.decrypt = function (encValue, key, outputEncoding) {
key = key || _service.getKey();
key = key || _service.getEncKey() || _service.getKey();
var headerPieces = encValue.split('.'),
encType,

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,12 @@
case 'splashidcsv':
importSplashIdCsv(file, success, error);
break;
case 'meldiumcsv':
importMeldiumCsv(file, success, error);
break;
case 'passkeepcsv':
importPassKeepCsv(file, success, error);
break;
default:
error();
break;
@@ -1437,15 +1445,16 @@
else if (row.length === 6) {
if (row[2] === '') {
login.username = row[3];
login.password = row[4];
login.notes = row[5];
}
else {
login.username = row[2];
login.notes = row[3] + '\n' + row[5];
login.password = row[3];
login.notes = row[4] + '\n' + row[5];
}
login.uri = fixUri(row[1]);
login.password = row[4];
}
else if (row.length === 7) {
if (row[2] === '') {
@@ -2335,5 +2344,114 @@
});
}
function importMeldiumCsv(file, success, error) {
Papa.parse(file, {
header: true,
encoding: 'UTF-8',
complete: function (results) {
parseCsvErrors(results);
var folders = [],
logins = [],
loginRelationships = [];
for (var j = 0; j < results.data.length; j++) {
var row = results.data[j];
var login = {
name: row.DisplayName && row.DisplayName !== '' ? row.DisplayName : '--',
favorite: false,
uri: row.Url && row.Url !== '' ? fixUri(row.Url) : null,
password: row.Password && row.Password !== '' ? row.Password : null,
username: row.UserName && row.UserName !== '' ? row.UserName : null,
notes: row.Notes && row.Notes !== '' ? row.Notes : null
};
logins.push(login);
}
success(folders, logins, loginRelationships);
}
});
}
function importPassKeepCsv(file, success, error) {
function getValue(key, obj) {
var val = obj[key] || obj[(' ' + key)];
if (val && val !== '') {
return val;
}
return null;
}
Papa.parse(file, {
header: true,
encoding: 'UTF-8',
complete: function (results) {
parseCsvErrors(results);
var folders = [],
logins = [],
folderRelationships = [];
angular.forEach(results.data, function (value, key) {
var folderIndex = folders.length,
loginIndex = logins.length,
hasFolder = !!getValue('category', value),
addFolder = hasFolder,
i = 0;
if (hasFolder) {
for (i = 0; i < folders.length; i++) {
if (folders[i].name === getValue('category', value)) {
addFolder = false;
folderIndex = i;
break;
}
}
}
var login = {
favorite: false,
uri: !!getValue('site', value) ? fixUri(getValue('site', value)) : null,
username: !!getValue('username', value) ? getValue('username', value) : null,
password: !!getValue('password', value) ? getValue('password', value) : null,
notes: !!getValue('description', value) ? getValue('description', value) : null,
name: !!getValue('title', value) ? getValue('title', value) : '--'
};
if (!!getValue('password2', value)) {
if (!login.notes) {
login.notes = '';
}
else {
login.notes += '\n';
}
login.notes += ('Password 2: ' + getValue('password2', value));
}
logins.push(login);
if (addFolder) {
folders.push({
name: getValue('category', value)
});
}
if (hasFolder) {
var relationship = {
key: loginIndex,
value: folderIndex
};
folderRelationships.push(relationship);
}
});
success(folders, logins, folderRelationships);
}
});
}
return _service;
});

View File

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

View File

@@ -1,2 +1,2 @@
angular.module("bit")
.constant("appSettings", {"apiUri":"https://api.bitwarden.com","version":"1.10.0","environment":"Production"});
.constant("appSettings", {"apiUri":"https://api.bitwarden.com","identityUri":"https://identity.bitwarden.com","stripeKey":"pk_live_bpN0P37nMxrMQkcaHXtAybJk","version":"1.13.0","environment":"Production"});

View File

@@ -4,84 +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;
_masterPasswordHash = cryptoService.hashPassword(_masterPassword);
_newEmail = model.newEmail.toLowerCase();
var encKey = cryptoService.getEncKey();
if (encKey) {
$scope.tokenPromise = requestToken(model);
}
else {
// User is not using an enc key, let's make them one
$scope.tokenPromise = cipherService.updateKey(_masterPasswordHash, function () {
return requestToken(model);
}, processError);
}
};
function requestToken(model) {
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;
}
var newKey = cryptoService.makeKey(_masterPassword, _newEmail);
var encKey = cryptoService.getEncKey();
var newEncKey = cryptoService.encrypt(encKey.key, newKey, 'raw');
filteredEncryptedLogins.push(encryptedLogins.Data[i]);
}
var request = {
token: model.token,
newEmail: _newEmail,
masterPasswordHash: _masterPasswordHash,
newMasterPasswordHash: cryptoService.hashPassword(_masterPassword, newKey),
key: newEncKey
};
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 = {
token: model.token,
newEmail: model.newEmail.toLowerCase(),
masterPasswordHash: _masterPasswordHash,
newMasterPasswordHash: _newMasterPasswordHash,
data: {
ciphers: reencryptedLogins,
folders: reencryptedFolders,
privateKey: reencryptedPrivateKey
}
};
$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;
});
$scope.confirmPromise = apiService.accounts.email(request).$promise.then(function () {
$uibModalInstance.dismiss('cancel');
authService.logOut();
$analytics.eventTrack('Changed Email');
return $state.go('frontend.login.info');
}, processError).then(function () {
toastr.success('Please log back in.', 'Email Changed');
}, processError);
};
function processError() {
$uibModalInstance.dismiss('cancel');
toastr.error('Something went wrong. Try again.', 'Oh No!');
}
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};

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,47 @@
$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
var mpHash = cryptoService.hashPassword(model.masterPassword);
$scope.savePromise = cipherService.updateKey(mpHash, function () {
return changePassword(model);
}, processError);
}
};
function changePassword(model) {
return authService.getUserProfile().then(function (profile) {
var newKey = cryptoService.makeKey(model.newMasterPassword, profile.email.toLowerCase());
var encKey = cryptoService.getEncKey();
var newEncKey = cryptoService.encrypt(encKey.key, newKey, 'raw');
var request = {
masterPasswordHash: cryptoService.hashPassword(model.masterPassword),
newMasterPasswordHash: cryptoService.hashPassword(model.newMasterPassword, newKey),
key: newEncKey
};
return apiService.accounts.putPassword(request).$promise;
}, processError).then(function () {
$uibModalInstance.dismiss('cancel');
authService.logOut();
$analytics.eventTrack('Changed Password');
return $state.go('frontend.login.info');
}, processError).then(function () {
toastr.success('Please log back in.', 'Master Password Changed');
}, processError);
}
function processError() {
$uibModalInstance.dismiss('cancel');
toastr.error('Something went wrong.', 'Oh No!');
}
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};

View File

@@ -14,12 +14,12 @@
$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.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.plans[$scope.model.plan].annualBasePrice || 0);
}
};
@@ -27,6 +27,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 () {
@@ -66,8 +74,6 @@
}
function finalizeCreate(result) {
$scope.model.card = null;
$analytics.eventTrack('Created Organization');
authService.addProfileOrganizationOwner(result, shareKeyCt);
authService.refreshAccessToken().then(function () {

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>
@@ -25,7 +25,7 @@
required api-field />
</div>
<div class="form-group">
<label for="email">Email - <a href="javascript:void(0)" ng-click="changeEmail()">change</a></label>
<label for="email">Email - <a href="#" stop-click ng-click="changeEmail()">change</a></label>
<input type="text" id="email" ng-model="model.email" class="form-control" readonly />
</div>
<div class="form-group" show-errors>
@@ -62,7 +62,7 @@
<div class="row">
<div class="col-sm-9">
<div class="callout callout-danger validation-errors" ng-show="masterPasswordForm.$errors">
<h4>Errors have occured</h4>
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in masterPasswordForm.$errors">{{e}}</li>
</ul>
@@ -121,13 +121,13 @@
</button>
<ul class="dropdown-menu">
<li>
<a href="javascript:void(0)" ng-click="leaveOrganization(org)" class="text-red">
<a href="#" stop-click ng-click="leaveOrganization(org)" class="text-red">
<i class="fa fa-fw fa-sign-out"></i> Leave
</a>
</li>
</ul>
</div>
<a href="javascript:void(0)" ng-click="viewOrganization(org)">
<a href="#" stop-click ng-click="viewOrganization(org)">
<letter-avatar data="{{org.name}}" round="false" avwidth="25" avheight="25"
avclass="img-rounded" fontsize="10"></letter-avatar>
{{org.name}}

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@
</div>
<div class="box-body">
<div class="callout callout-danger validation-errors" ng-show="createOrgForm.$errors">
<h4>Errors have occured</h4>
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in createOrgForm.$errors">{{e}}</li>
</ul>
@@ -78,7 +78,7 @@
<span>- Priority customer support</span>
<span>- 7 day free trial, cancel anytime</span>
<span class="bottom-line">
{{plans.personal.basePrice | currency:'$'}} /month for {{plans.personal.baseSeats}} users,
{{plans.personal.basePrice | currency:'$'}} /month includes {{plans.personal.baseSeats}} users,
additional users {{plans.personal.seatPrice | currency:'$'}} /month
</span>
</label>
@@ -93,11 +93,27 @@
<span>- Priority customer support</span>
<span>- 7 day free trial, cancel anytime</span>
<span class="bottom-line">
{{plans.teams.basePrice | currency:'$'}} /month for {{plans.teams.baseSeats}} users,
{{plans.teams.basePrice | currency:'$'}} /month includes {{plans.teams.baseSeats}} users,
additional users {{plans.teams.seatPrice | currency:'$'}} /month
</span>
</label>
</div>
<div class="radio radio-block" ng-click="changedPlan()">
<label>
<input type="radio" ng-model="model.plan" name="PlanType" value="enterprise">
Enterprise
<span>For businesses and other large organizations.</span>
<span>- Add and share with unlimited users</span>
<span>- Create unlimited collections</span>
<span>- Control user access with groups</span>
<span>- Sync your users and groups from a directory (AD, Azure AD, GSuite, LDAP)</span>
<span>- Priority customer support</span>
<span>- 7 day free trial, cancel anytime</span>
<span class="bottom-line">
{{plans.enterprise.seatPrice | currency:'$'}} per user /month
</span>
</label>
</div>
</div>
<div class="box-footer" ng-show="plans[model.plan].noPayment">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="createOrgForm.$loading">
@@ -105,7 +121,7 @@
</button>
</div>
</div>
<div class="box box-default" ng-if="!plans[model.plan].noAdditionalSeats">
<div class="box box-default" ng-if="!plans[model.plan].noAdditionalSeats && plans[model.plan].baseSeats">
<div class="box-header with-border">
<h3 class="box-title">Additional Users (Seats)</h3>
</div>
@@ -129,6 +145,27 @@
</div>
</div>
</div>
<div class="box box-default" ng-if="!plans[model.plan].noAdditionalSeats && !plans[model.plan].baseSeats">
<div class="box-header with-border">
<h3 class="box-title">Users (Seats)</h3>
</div>
<div class="box-body">
<p>
How many user seats do you need?
You can also add additional seats later if needed.
</p>
<div class="row">
<div class="col-md-4">
<div class="form-group" show-errors style="margin: 0;">
<label for="additionalSeats" class="sr-only">Users</label>
<input type="number" id="additionalSeats" name="AdditionalSeats" ng-model="model.additionalSeats"
min="1" class="form-control" placeholder="# of users" api-field
ng-attr-max="{{plans[model.plan].maxAdditionalSeats || 1000000}}" />
</div>
</div>
</div>
</div>
</div>
<div class="box box-default" ng-if="!plans[model.plan].noPayment">
<div class="box-header with-border">
<h3 class="box-title">Billing Summary</h3>
@@ -138,15 +175,17 @@
<label>
<input type="radio" ng-model="model.interval" name="BillingInterval" value="year">
Annually
<span>
<span ng-if="plans[model.plan].annualBasePrice">
Base price:
{{plans[model.plan].basePrice | currency:"$":2}} &times;12 mo. =
{{plans[model.plan].annualBasePrice | currency:"$":2}} /year
</span>
<span>
Additional users:
<span ng-if="plans[model.plan].baseSeats">Additional users:</span>
<span ng-if="!plans[model.plan].baseSeats">Users:</span>
{{model.additionalSeats || 0}} &times;{{plans[model.plan].seatPrice | currency:"$":2}}
&times;12 mo. = {{(model.additionalSeats * plans[model.plan].annualSeatPrice) | currency:"$":2}} /year
&times;12 mo. =
{{((model.additionalSeats || 0) * plans[model.plan].annualSeatPrice) | currency:"$":2}} /year
</span>
</label>
</div>
@@ -154,14 +193,16 @@
<label>
<input type="radio" ng-model="model.interval" name="BillingInterval" value="month">
Monthly
<span>
<span ng-if="plans[model.plan].monthlyBasePrice">
Base price:
{{plans[model.plan].monthlyBasePrice | currency:"$":2}} /month
</span>
<span>
Additional users:
{{model.additionalSeats || 0}} &times;{{plans[model.plan].monthlySeatPrice | currency:"$":2}} =
{{(model.additionalSeats * plans[model.plan].monthlySeatPrice) | currency:"$":2}} /month
<span ng-if="plans[model.plan].baseSeats">Additional users:</span>
<span ng-if="!plans[model.plan].baseSeats">Users:</span>
{{model.additionalSeats || 0}}
&times;{{plans[model.plan].monthlySeatPrice | currency:"$":2}} =
{{((model.additionalSeats || 0) * plans[model.plan].monthlySeatPrice) | currency:"$":2}} /month
</span>
</label>
</div>

View File

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

View File

@@ -28,12 +28,12 @@
</button>
<ul class="dropdown-menu">
<li>
<a href="javascript:void(0)" ng-click="addEdit($index)">
<a href="#" stop-click ng-click="addEdit($index)">
<i class="fa fa-fw fa-pencil"></i> Edit
</a>
</li>
<li>
<a href="javascript:void(0)" ng-click="delete($index)" class="text-red">
<a href="#" stop-click ng-click="delete($index)" class="text-red">
<i class="fa fa-fw fa-trash"></i> Delete
</a>
</li>
@@ -76,19 +76,19 @@
</button>
<ul class="dropdown-menu">
<li>
<a href="javascript:void(0)" ng-if="!globalDomain.excluded"
<a href="#" stop-click ng-if="!globalDomain.excluded"
ng-click="toggleExclude(globalDomain)">
<i class="fa fa-fw fa-remove"></i> Exclude
</a>
</li>
<li>
<a href="javascript:void(0)" ng-if="globalDomain.excluded"
<a href="#" stop-click ng-if="globalDomain.excluded"
ng-click="toggleExclude(globalDomain)">
<i class="fa fa-fw fa-plus"></i> Include
</a>
</li>
<li>
<a href="javascript:void(0)" ng-click="customize(globalDomain)">
<a href="#" stop-click ng-click="customize(globalDomain)">
<i class="fa fa-fw fa-cut"></i> Customize
</a>
</li>

View File

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

View File

@@ -11,7 +11,7 @@
</p>
<p ng-show="!enabled()">To get started with two-step log in enter your master password below.</p>
<div class="callout callout-danger validation-errors" ng-show="authTwoStepForm.$errors">
<h4>Errors have occured</h4>
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in authTwoStepForm.$errors">{{e}}</li>
</ul>
@@ -105,7 +105,7 @@
</div>
<hr ng-show="enabled()" />
<div class="callout callout-danger validation-errors" ng-show="updateTwoStepForm.$errors">
<h4>Errors have occured</h4>
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in updateTwoStepForm.$errors">{{e}}</li>
</ul>

View File

@@ -11,26 +11,34 @@
{
id: 'bitwardencsv',
name: 'bitwarden (csv)',
featured: true,
sort: 1,
instructions: $sce.trustAsHtml('Export using the web vault (vault.bitwarden.com). ' +
'Log into the web vault and navigate to "Tools" > "Export".')
},
{
id: 'lastpass',
name: 'LastPass (csv)',
featured: true,
sort: 2,
instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' +
'<a target="_blank" href="https://help.bitwarden.com/getting-started/import-from-lastpass/">' +
'https://help.bitwarden.com/getting-started/import-from-lastpass/</a>')
'<a target="_blank" href="https://help.bitwarden.com/article/import-from-lastpass/">' +
'https://help.bitwarden.com/article/import-from-lastpass/</a>')
},
{
id: 'chromecsv',
name: 'Chrome (csv)',
featured: true,
sort: 3,
instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' +
'<a target="_blank" href="https://help.bitwarden.com/getting-started/import-from-chrome/">' +
'https://help.bitwarden.com/getting-started/import-from-chrome/</a>')
'<a target="_blank" href="https://help.bitwarden.com/article/import-from-chrome/">' +
'https://help.bitwarden.com/article/import-from-chrome/</a>')
},
{
id: 'firefoxpasswordexportercsvxml',
name: 'Firefox Password Exporter (xml)',
featured: true,
sort: 4,
instructions: $sce.trustAsHtml('Use the ' +
'<a target="_blank" href="https://addons.mozilla.org/en-US/firefox/addon/password-exporter/">' +
'Password Exporter</a> addon for FireFox to export your passwords to a XML file. After installing ' +
@@ -41,6 +49,8 @@
{
id: 'keepass2xml',
name: 'KeePass 2 (xml)',
featured: true,
sort: 5,
instructions: $sce.trustAsHtml('Using the KeePass 2 desktop application, navigate to "File" > "Export" and ' +
'select the KeePass XML (2.x) option.')
},
@@ -53,22 +63,26 @@
{
id: 'dashlanecsv',
name: 'Dashlane (csv)',
featured: true,
sort: 7,
instructions: $sce.trustAsHtml('Using the Dashlane desktop application, navigate to "File" > "Export" > ' +
'"Unsecured archive (readable) in CSV format" and save the CSV file.')
},
{
id: '1password1pif',
name: '1Password (1pif)',
featured: true,
sort: 6,
instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' +
'<a target="_blank" href="https://help.bitwarden.com/getting-started/import-from-1password/">' +
'https://help.bitwarden.com/getting-started/import-from-1password/</a>')
'<a target="_blank" href="https://help.bitwarden.com/article/import-from-1password/">' +
'https://help.bitwarden.com/article/import-from-1password/</a>')
},
{
id: '1password6wincsv',
name: '1Password 6 Windows (csv)',
instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' +
'<a target="_blank" href="https://help.bitwarden.com/getting-started/import-from-1password/">' +
'https://help.bitwarden.com/getting-started/import-from-1password/</a>')
'<a target="_blank" href="https://help.bitwarden.com/article/import-from-1password/">' +
'https://help.bitwarden.com/article/import-from-1password/</a>')
},
{
id: 'roboformhtml',
@@ -190,6 +204,34 @@
instructions: $sce.trustAsHtml('Using the Ascendo DataVault desktop application, navigate ' +
'to "Tools" > "Export". In the dialog that pops up, select the "All Items (DVX, CSV)" ' +
'option. Click the "Ok" button to save the CSV file.')
},
{
id: 'meldiumcsv',
name: 'Meldium (csv)',
instructions: $sce.trustAsHtml('Using the Meldium web vault, navigate to "Settings". ' +
'Locate the "Export data" function and click "Show me my data" to save the CSV file.')
},
{
id: 'passkeepcsv',
name: 'PassKeep (csv)',
instructions: $sce.trustAsHtml('Using the PassKeep mobile app, navigate to "Backup/Restore". ' +
'Locate the "CSV Backup/Restore" section and click "Backup to CSV" to save the CSV file.')
},
{
id: 'operacsv',
name: 'Opera (csv)',
instructions: $sce.trustAsHtml('The process for importing from Opera is exactly the same as ' +
'importing from Google Chrome. See detailed instructions on our help site at ' +
'<a target="_blank" href="https://help.bitwarden.com/article/import-from-chrome/">' +
'https://help.bitwarden.com/article/import-from-chrome/</a>')
},
{
id: 'vivaldicsv',
name: 'Vivaldi (csv)',
instructions: $sce.trustAsHtml('The process for importing from Vivaldi is exactly the same as ' +
'importing from Google Chrome. See detailed instructions on our help site at ' +
'<a target="_blank" href="https://help.bitwarden.com/article/import-from-chrome/">' +
'https://help.bitwarden.com/article/import-from-chrome/</a>')
}
];

View File

@@ -11,7 +11,7 @@
over unsecure channels (such as email). Delete it immediately after you are done using it.
</div>
<div class="callout callout-danger validation-errors" ng-show="exportForm.$errors">
<h4>Errors have occured</h4>
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in exportForm.$errors">{{e}}</li>
</ul>

View File

@@ -5,7 +5,7 @@
<form name="importForm" ng-submit="importForm.$valid && import(model, importForm)" ng-show="!processing">
<div class="modal-body">
<div class="callout callout-danger validation-errors" ng-show="importForm.$errors">
<h4>Errors have occured</h4>
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in importForm.$errors">{{e}}</li>
</ul>
@@ -14,7 +14,11 @@
<label for="source">1. Select the format of the import file</label>
<select id="source" name="source" class="form-control" ng-model="model.source" ng-change="setSource()" required>
<option value="">-- Select --</option>
<option ng-repeat="option in options" value="{{option.id}}">{{option.name}}</option>
<option ng-repeat="option in options | filter: { featured: true } | orderBy: ['sort', 'name']"
value="{{option.id}}">{{option.name}}</option>
<option value="-" disabled></option>
<option ng-repeat="option in options | filter: { featured: '!true' } | orderBy: ['sort', 'name']"
value="{{option.id}}">{{option.name}}</option>
</select>
</div>
<div class="callout callout-default" ng-show="model.source">

View File

@@ -40,6 +40,10 @@
};
$scope.folderSort = function (item) {
if (!item.id) {
return '';
}
return item.name.toLowerCase();
};

View File

@@ -6,6 +6,7 @@
$scope.loading = true;
$scope.logins = [];
$scope.favoriteCollapsed = $localStorage.collapsedFolders && 'favorite' in $localStorage.collapsedFolders;
$scope.folderIdFilter = undefined;
if ($state.params.refreshFromServer) {
$rootScope.vaultFolders = $rootScope.vaultLogins = null;
@@ -117,6 +118,11 @@
return item.name.toLowerCase();
}
$scope.clipboardError = function (e) {
alert('Your web browser does not support easy clipboard copying. ' +
'Edit the login and copy it manually instead.');
};
$scope.collapseExpand = function (folder, favorite) {
if (!$localStorage.collapsedFolders) {
$localStorage.collapsedFolders = {};
@@ -146,6 +152,7 @@
login.folderId = returnVal.data.folderId;
login.name = returnVal.data.name;
login.username = returnVal.data.username;
login.password = returnVal.data.password;
login.favorite = returnVal.data.favorite;
sortScopedLoginData();
@@ -281,6 +288,120 @@
});
};
$scope.filterFolder = function (folder) {
$scope.folderIdFilter = folder.id;
if ($.AdminLTE && $.AdminLTE.layout) {
$timeout(function () {
$.AdminLTE.layout.fix();
}, 0);
}
};
$scope.clearFilters = function () {
$scope.folderIdFilter = undefined;
if ($.AdminLTE && $.AdminLTE.layout) {
$timeout(function () {
$.AdminLTE.layout.fix();
}, 0);
}
};
$scope.folderFilter = function (folder) {
return $scope.folderIdFilter === undefined || folder.id === $scope.folderIdFilter;
};
$scope.unselectAll = function () {
selectAll(false);
};
$scope.selectFolder = function (folder, $event) {
var checkbox = $($event.currentTarget).closest('.box').find('input[name="loginSelection"]');
checkbox.prop('checked', true);
};
$scope.select = function ($event) {
var checkbox = $($event.currentTarget).closest('tr').find('input[name="loginSelection"]');
checkbox.prop('checked', !checkbox.prop('checked'));
};
function distinct(value, index, self) {
return self.indexOf(value) === index;
}
function getSelectedLogins() {
return $('input[name="loginSelection"]:checked').map(function () {
return $(this).val();
}).get().filter(distinct);
}
function selectAll(select) {
$('input[name="loginSelection"]').prop('checked', select);
}
$scope.bulkMove = function () {
var ids = getSelectedLogins();
if (ids.length === 0) {
alert('You have not selected anything.');
return;
}
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/vault/views/vaultMoveLogins.html',
controller: 'vaultMoveLoginsController',
size: 'sm',
resolve: {
ids: function () { return ids; }
}
});
modal.result.then(function (folderId) {
for (var i = 0; i < ids.length; i++) {
var login = $filter('filter')($rootScope.vaultLogins, { id: ids[i] });
if (login.length) {
login[0].folderId = folderId;
}
}
selectAll(false);
sortScopedLoginData();
toastr.success('Items have been moved!');
});
};
$scope.bulkDelete = function () {
var ids = getSelectedLogins();
if (ids.length === 0) {
alert('You have not selected anything.');
return;
}
if (!confirm('Are you sure you want to delete the selected logins (total: ' + ids.length + ')?')) {
return;
}
$scope.bulkActionLoading = true;
apiService.ciphers.delMany({ ids: ids }, function () {
$analytics.eventTrack('Bulk Deleted Logins');
for (var i = 0; i < ids.length; i++) {
var login = $filter('filter')($rootScope.vaultLogins, { id: ids[i] });
if (login.length && login[0].edit) {
removeLoginFromScopes(login[0]);
}
}
selectAll(false);
$scope.bulkActionLoading = false;
toastr.success('Items have been deleted!');
}, function () {
toastr.error('An error occurred.');
$scope.bulkActionLoading = false;
});
};
function removeLoginFromScopes(login) {
var index = $rootScope.vaultLogins.indexOf(login);
if (index > -1) {

View File

@@ -63,6 +63,10 @@
};
$scope.folderSort = function (item) {
if (!item.id) {
return '';
}
return item.name.toLowerCase();
};

View File

@@ -12,7 +12,7 @@
$scope.collections = [];
$uibModalInstance.opened.then(function () {
apiService.ciphers.getFullDetails({ id: loginId }).$promise.then(function (cipher) {
apiService.ciphers.getDetails({ id: loginId }).$promise.then(function (cipher) {
$scope.loadingLogin = false;
$scope.readOnly = !cipher.Edit;
@@ -42,7 +42,10 @@
apiService.collections.listMe(function (response) {
var collections = [];
for (var i = 0; i < response.Data.length; i++) {
if (response.Data[i].OrganizationId !== cipher.OrganizationId) {
if (response.Data[i].OrganizationId !== cipher.OrganizationId || response.Data[i].ReadOnly) {
if (response.Data[i].Id in $scope.selectedCollections) {
delete $scope.selectedCollections[response.Data[i].Id];
}
continue;
}

View File

@@ -0,0 +1,28 @@
angular
.module('bit.vault')
.controller('vaultMoveLoginsController', function ($scope, apiService, $uibModalInstance, ids, $analytics,
$rootScope) {
$analytics.eventTrack('vaultMoveLoginsController', { category: 'Modal' });
$scope.folders = $rootScope.vaultFolders;
$scope.count = ids.length;
$scope.save = function () {
$scope.savePromise = apiService.ciphers.moveMany({ ids: ids, folderId: $scope.folderId }, function () {
$analytics.eventTrack('Bulk Moved Logins');
$uibModalInstance.close($scope.folderId || null);
}).$promise;
};
$scope.folderSort = function (item) {
if (!item.id) {
return '!';
}
return item.name.toLowerCase();
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@@ -54,6 +54,10 @@
apiService.collections.listMe(function (response) {
var collections = [];
for (var i = 0; i < response.Data.length; i++) {
if (response.Data[i].ReadOnly) {
continue;
}
var decCollection = cipherService.decryptCollection(response.Data[i]);
decCollection.organizationId = response.Data[i].OrganizationId;
collections.push(decCollection);
@@ -70,7 +74,8 @@
var collections = {};
if ($event.target.checked) {
for (var i = 0; i < $scope.collections.length; i++) {
if ($scope.model.organizationId && $scope.collections[i].organizationId === $scope.model.organizationId) {
if ($scope.model.organizationId && $scope.collections[i].organizationId === $scope.model.organizationId &&
!$scope.collections[i].readOnly) {
collections[$scope.collections[i].id] = true;
}
}

View File

@@ -47,6 +47,11 @@
});
});
$scope.clipboardError = function (e) {
alert('Your web browser does not support easy clipboard copying. ' +
'Edit the login and copy it manually instead.');
};
$scope.filterByCollection = function (collection) {
return function (cipher) {
if (!cipher.collectionIds || !cipher.collectionIds.length) {
@@ -97,6 +102,7 @@
login.folderId = rootLogin.folderId = returnVal.data.folderId;
login.name = rootLogin.name = returnVal.data.name;
login.username = rootLogin.username = returnVal.data.username;
login.password = rootLogin.username = returnVal.data.password;
login.favorite = rootLogin.favorite = returnVal.data.favorite;
}
else if (returnVal.action === 'partialEdit') {

View File

@@ -1,4 +1,29 @@
<section class="content-header">
<div class="btn-group pull-right">
<button type="button" class="btn btn-link dropdown-toggle" data-toggle="dropdown"
ng-disabled="bulkActionLoading">
<i class="fa fa-refresh fa-spin" ng-show="bulkActionLoading"></i>
Bulk Actions <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li>
<a href="#" stop-click ng-click="bulkMove()">
<i class="fa fa-fw fa-share"></i> Move Selected
</a>
</li>
<li>
<a href="#" stop-click ng-click="bulkDelete()">
<i class="fa fa-fw fa-trash"></i> Delete Selected
</a>
</li>
<li role="separator" class="divider"></li>
<li>
<a href="#" stop-click ng-click="unselectAll()">
<i class="fa fa-fw fa-minus-square-o"></i> Unselect All
</a>
</li>
</ul>
</div>
<h1>
My Vault
<small>
@@ -14,7 +39,7 @@
<p>Loading...</p>
</div>
<div class="box box-primary" ng-class="{'collapsed-box': favoriteCollapsed}" style="margin-bottom: 40px;"
ng-show="vaultFolders.length && (!main.searchVaultText || favoriteLogins.length)">
ng-show="vaultFolders.length && folderIdFilter === undefined && (!main.searchVaultText || favoriteLogins.length)">
<div class="box-header with-border">
<h3 class="box-title">
<i class="fa fa-star"></i>
@@ -28,7 +53,7 @@
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li>
<a href="#" ng-click="addLogin(null, true)">
<a href="#" stop-click ng-click="addLogin(null, true)">
<i class="fa fa-fw fa-plus-circle"></i> Add Login
</a>
</li>
@@ -57,32 +82,42 @@
</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 ng-show="!login.organizationId">
<a href="javascript:void(0)" ng-click="share(login)">
<a href="#" stop-click ng-click="share(login)">
<i class="fa fa-fw fa-share-alt"></i> Share
</a>
</li>
<li ng-show="login.organizationId">
<a href="javascript:void(0)" ng-click="collections(login)">
<li ng-show="login.organizationId && login.edit">
<a href="#" stop-click ng-click="collections(login)">
<i class="fa fa-fw fa-cubes"></i> Collections
</a>
</li>
<li>
<a href="javascript:void(0)" ng-click="deleteLogin(login)" class="text-red">
<li ng-show="login.password">
<a href="#" stop-click ngclipboard ngclipboard-error="clipboardError(e)"
data-clipboard-text="{{login.password}}">
<i class="fa fa-fw fa-clipboard"></i> Copy Password
</a>
</li>
<li ng-show="login.edit">
<a href="#" stop-click ng-click="deleteLogin(login)" class="text-red">
<i class="fa fa-fw fa-trash"></i> Delete
</a>
</li>
</ul>
</div>
</td>
<td>
<a href="javascript:void(0)" ng-click="editLogin(login)">{{login.name}}</a>
<i class="fa fa-share-alt text-muted" title="Shared" ng-show="login.organizationId"></i>
<div class="text-sm text-muted">{{login.username}}</div>
<td class="action-select">
<input type="checkbox" value="{{::login.id}}" name="loginSelection" />
</td>
<td ng-click="select($event)">
<a href="#" stop-click ng-click="editLogin(login)" stop-prop>{{login.name}}</a>
<i class="fa fa-share-alt text-muted" title="Shared" ng-show="login.organizationId"
stop-prop></i><br />
<span class="text-sm text-muted" stop-prop>{{login.username}}</span>
</td>
</tr>
</tbody>
@@ -90,7 +125,8 @@
</div>
</div>
</div>
<div class="box" ng-class="{'collapsed-box': folder.collapsed}" ng-repeat="folder in vaultFolders track by folder.id"
<div class="box" ng-class="{'collapsed-box': folder.collapsed}"
ng-repeat="folder in filteredVaultFolders = (vaultFolders | filter: folderFilter) track by folder.id"
ng-show="vaultFolders.length && (!main.searchVaultText || folderLogins.length)">
<div class="box-header with-border">
<h3 class="box-title">
@@ -105,17 +141,22 @@
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li>
<a href="#" ng-click="addLogin(folder)">
<a href="#" stop-click ng-click="addLogin(folder)">
<i class="fa fa-fw fa-plus-circle"></i> Add Login
</a>
</li>
<li ng-show="folder.id">
<a href="#" ng-click="editFolder(folder)">
<a href="#" stop-click ng-click="editFolder(folder)">
<i class="fa fa-fw fa-pencil"></i> Edit Folder
</a>
</li>
<li>
<a href="#" stop-click ng-click="selectFolder(folder, $event)">
<i class="fa fa-fw fa-check-square-o"></i> Select All
</a>
</li>
<li ng-show="canDeleteFolder(folder)">
<a href="#" ng-click="deleteFolder(folder)" class="text-red">
<a href="#" stop-click ng-click="deleteFolder(folder)" class="text-red">
<i class="fa fa-fw fa-trash"></i> Delete Folder
</a>
</li>
@@ -144,33 +185,43 @@
</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 ng-show="!login.organizationId">
<a href="javascript:void(0)" ng-click="share(login)">
<a href="#" stop-click ng-click="share(login)">
<i class="fa fa-fw fa-share-alt"></i> Share
</a>
</li>
<li ng-show="login.organizationId">
<a href="javascript:void(0)" ng-click="collections(login)">
<li ng-show="login.organizationId && login.edit">
<a href="#" stop-click ng-click="collections(login)">
<i class="fa fa-fw fa-cubes"></i> Collections
</a>
</li>
<li>
<a href="javascript:void(0)" ng-click="deleteLogin(login)" class="text-red">
<li ng-show="login.password">
<a href="#" stop-click ngclipboard ngclipboard-error="clipboardError(e)"
data-clipboard-text="{{login.password}}">
<i class="fa fa-fw fa-clipboard"></i> Copy Password
</a>
</li>
<li ng-show="login.edit">
<a href="#" stop-click ng-click="deleteLogin(login)" class="text-red">
<i class="fa fa-fw fa-trash"></i> Delete
</a>
</li>
</ul>
</div>
</td>
<td>
<a href="javascript:void(0)" ng-click="editLogin(login)">{{login.name}}</a>
<i class="fa fa-star text-muted" title="Favorite" ng-show="login.favorite"></i>
<i class="fa fa-share-alt text-muted" title="Shared" ng-show="login.organizationId"></i>
<div class="text-sm text-muted">{{login.username}}</div>
<td class="action-select" ng-click="select($event)">
<input type="checkbox" value="{{::login.id}}" name="loginSelection" stop-prop />
</td>
<td ng-click="select($event)">
<a href="#" stop-click ng-click="editLogin(login)" stop-prop>{{login.name}}</a>
<i class="fa fa-star text-muted" title="Favorite" ng-show="login.favorite" stop-prop></i>
<i class="fa fa-share-alt text-muted" title="Shared" ng-show="login.organizationId" stop-prop></i>
<br />
<span class="text-sm text-muted" stop-prop>{{login.username}}</span>
</td>
</tr>
</tbody>
@@ -179,3 +230,28 @@
</div>
</div>
</section>
<aside class="control-sidebar control-sidebar-light">
<div class="tab-content">
<ul class="control-sidebar-menu">
<li>
<a href="#" stop-click ng-click="clearFilters()">
Clear All Filters
</a>
</li>
</ul>
<h3 class="control-sidebar-heading">
<i class="fa fa-folder fa-fw"></i> Folders
</h3>
<div ng-show="loading && !vaultFolders.length">
<p>Loading...</p>
</div>
<ul class="control-sidebar-menu" ng-show="!loading && vaultFolders.length">
<li ng-repeat="folder in vaultFolders track by folder.id">
<a href="#" stop-click ng-click="filterFolder(folder)">
<i class="fa fa-check" ng-if="folder.id === folderIdFilter"></i>
{{folder.name}}
</a>
</li>
</ul>
</div>
</aside>

View File

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

View File

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

View File

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

View File

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

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,34 @@
<div class="modal-header">
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">
<i class="fa fa-share"></i> Move Logins
</h4>
</div>
<form name="form" ng-submit="form.$valid && save()" api-form="savePromise">
<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>
<p>
Select a folder that you would like to move the <b>{{count}}</b> selected
<span ng-pluralize count="count" when="{'1': 'login', 'other': 'logins'}"></span> to.
</p>
<div class="form-group" show-errors>
<label for="folder">Folder</label>
<select id="folder" name="FolderId" ng-model="folderId" class="form-control" api-field>
<option ng-repeat="folder in folders | orderBy: folderSort" value="{{folder.id}}">
{{folder.name}}
</option>
</select>
</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>Save
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
</div>
</form>

View File

@@ -6,7 +6,7 @@
<div class="modal-body">
<p>Choose an organization that you wish to share this login 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

@@ -58,17 +58,23 @@
</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)">
<li ng-show="login.edit">
<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)"
<li ng-show="login.password">
<a href="#" stop-click ngclipboard ngclipboard-error="clipboardError(e)"
data-clipboard-text="{{login.password}}">
<i class="fa fa-fw fa-clipboard"></i> Copy Password
</a>
</li>
<li ng-show="login.edit">
<a href="#" stop-click ng-click="removeLogin(login, collection)"
ng-if="collection.id" class="text-red">
<i class="fa fa-fw fa-remove"></i> Remove
</a>
@@ -77,7 +83,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>
<i class="fa fa-star text-muted" title="Favorite" ng-show="login.favorite"></i>
<div class="text-sm text-muted">{{login.username}}</div>
</td>

View File

@@ -29,7 +29,7 @@
</a>
</li>
<li>
<a href="javascript:void(0)">
<a href="#" stop-click>
<i class="fa fa-edge fa-lg fa-fw fa-li"></i> Edge
<small class="text-muted">(coming soon)</small>
</a>
@@ -87,7 +87,7 @@
</a>
</li>
<li>
<a href="javascript:void(0)">
<a href="#" stop-click>
<i class="fa fa-windows fa-lg fa-fw fa-li"></i> Windows
<small class="text-muted">(coming soon)</small>
</a>
@@ -106,13 +106,13 @@
<div class="col-sm-6">
<ul class="fa-ul">
<li>
<a href="javascript:void(0)">
<a href="#" stop-click>
<i class="fa fa-windows fa-lg fa-fw fa-li"></i> Desktop Windows
<small class="text-muted">(coming soon)</small>
</a>
</li>
<li>
<a href="javascript:void(0)">
<a href="#" stop-click>
<i class="fa fa-apple fa-lg fa-fw fa-li"></i> Desktop macOS
<small class="text-muted">(coming soon)</small>
</a>
@@ -122,13 +122,13 @@
<div class="col-sm-6">
<ul class="fa-ul">
<li>
<a href="javascript:void(0)">
<a href="#" stop-click>
<i class="fa fa-linux fa-lg fa-fw fa-li"></i> Desktop Linux
<small class="text-muted">(coming soon)</small>
</a>
</li>
<li>
<a href="javascript:void(0)">
<a href="#" stop-click>
<i class="fa fa-terminal fa-lg fa-fw fa-li"></i> CLI
<small class="text-muted">(coming soon)</small>
</a>

View File

@@ -13,6 +13,9 @@
<div class="navbar-custom-menu">
<ul class="nav navbar-nav">
<li><a ui-sref="frontend.logout">Log Out</a></li>
<li class="control-sidebar-show">
<a href="#" stop-click ng-click="toggleControlSidebar()"><i class="fa fa-bars"></i></a>
</li>
</ul>
</div>
</nav>
@@ -50,7 +53,7 @@
</a>
<ul class="treeview-menu" ng-class="{'menu-open': $state.includes('backend.org.vault')}">
<li>
<a href="javascript:void(0)" ng-click="addOrganizationLogin()">
<a href="#" stop-click ng-click="addOrganizationLogin()">
<i class="fa fa-plus-circle fa-fw"></i> New Login
</a>
</li>
@@ -62,7 +65,7 @@
</a>
<ul class="treeview-menu" ng-class="{'menu-open': $state.includes('backend.org.collections')}">
<li>
<a href="javascript:void(0)" ng-click="addOrganizationCollection()">
<a href="#" stop-click ng-click="addOrganizationCollection()">
<i class="fa fa-plus-circle fa-fw"></i> New Collection
</a>
</li>
@@ -74,16 +77,23 @@
</a>
<ul class="treeview-menu" ng-class="{'menu-open': $state.includes('backend.org.people')}">
<li>
<a href="javascript:void(0)" ng-click="inviteOrganizationUser()">
<a href="#" stop-click ng-click="inviteOrganizationUser()">
<i class="fa fa-plus-circle fa-fw"></i> Invite a User
</a>
</li>
</ul>
</li>
<li ng-class="{active: $state.is('backend.org.groups')}">
<li ng-class="{active: $state.is('backend.org.groups')}" ng-if="orgProfile.useGroups">
<a ui-sref="backend.org.groups({orgId: params.orgId})">
<i class="fa fa-sitemap fa-fw"></i> <span>Groups</span>
</a>
<ul class="treeview-menu" ng-class="{'menu-open': $state.includes('backend.org.groups')}">
<li>
<a href="#" stop-click ng-click="addOrganizationGroup()">
<i class="fa fa-plus-circle fa-fw"></i> New Group
</a>
</li>
</ul>
</li>
<li ng-class="{active: $state.is('backend.org.billing')}" ng-if="isOrgOwner(orgProfile)">
<a ui-sref="backend.org.billing({orgId: params.orgId})">

View File

@@ -13,6 +13,9 @@
<div class="navbar-custom-menu">
<ul class="nav navbar-nav">
<li><a ui-sref="frontend.logout">Log Out</a></li>
<li class="control-sidebar-show">
<a href="#" stop-click ng-click="toggleControlSidebar()"><i class="fa fa-bars"></i></a>
</li>
</ul>
</div>
</nav>
@@ -43,12 +46,12 @@
<a ui-sref="backend.user.vault"><i class="fa fa-lock fa-fw"></i> <span>My Vault</span></a>
<ul class="treeview-menu" ng-class="{'menu-open': $state.includes('backend.user.vault')}">
<li>
<a href="javascript:void(0)" ng-click="addLogin()">
<a href="#" stop-click ng-click="addLogin()">
<i class="fa fa-plus-circle fa-fw"></i> New Login
</a>
</li>
<li>
<a href="javascript:void(0)" ng-click="addFolder()">
<a href="#" stop-click ng-click="addFolder()">
<i class="fa fa-folder fa-fw"></i> New Folder
</a>
</li>
@@ -61,8 +64,17 @@
<i class="fa fa-share-alt fa-fw"></i> <span>Shared</span>
</a>
</li>
<li class="treeview" ng-class="{active: $state.is('backend.user.tools')}">
<li class="treeview" ng-class="{active: $state.is('backend.user.tools') ||
$state.is('backend.user.reportsBreach')}">
<a ui-sref="backend.user.tools"><i class="fa fa-wrench fa-fw"></i> <span>Tools</span></a>
<ul class="treeview-menu" ng-class="{'menu-open': $state.is('backend.user.tools') ||
$state.is('backend.user.reportsBreach')}">
<li ng-class="{active: $state.is('backend.user.reportsBreach')}">
<a ui-sref="backend.user.reportsBreach">
<i class="fa fa-circle-o fa-fw"></i> Data Breach Report
</a>
</li>
</ul>
</li>
<li class="treeview"
ng-class="{active: $state.is('backend.user.settings') || $state.is('backend.user.settingsDomains') ||
@@ -107,7 +119,7 @@
</div>
<ul class="list-inline" ng-if="orgs.length">
<li ng-repeat="org in orgs | orderBy: ['name'] track by org.id">
<a href="javascript:void(0)" ng-click="viewOrganization(org)">
<a href="#" stop-click ng-click="viewOrganization(org)">
<letter-avatar data="{{org.name}}" avclass="img-responsive img-rounded" round="false"
avheight="40" avwidth="40" bgcolor="#2c3b41" avborder="true"
borderstyle="3px solid #1a2226"></letter-avatar>

View File

@@ -1,7 +1,40 @@
<!DOCTYPE html>
<html ng-app="bit">
<html ng-app="bit" ng-csp>
<head>
<meta charset="utf-8" />
<meta http-equiv="Content-Security-Policy" content="
default-src
'self';
script-src
'self'
https://www.google-analytics.com
https://js.stripe.com
https://maxcdn.bootstrapcdn.com
https://ajax.googleapis.com;
style-src
'self'
'unsafe-inline'
https://maxcdn.bootstrapcdn.com
https://fonts.googleapis.com;
img-src
'self'
data:
https://q.stripe.com
https://haveibeenpwned.com
https://chart.googleapis.com
https://www.google-analytics.com;
font-src
'self'
https://maxcdn.bootstrapcdn.com
https://fonts.gstatic.com;
child-src
'self'
https://js.stripe.com;
frame-src
'self'
https://js.stripe.com;
connect-src
*;">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<base href="/" />
@@ -9,13 +42,12 @@
<script src="https://js.stripe.com/v2/"></script>
<!-- @if true !>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" />
<meta name="x-stylesheet-test-bs" content="" class="invisible" />
<script>!function(a,b,c){var d,e=document,f=e.getElementsByTagName('SCRIPT'),g=f[f.length-1].previousElementSibling,h=e.defaultView&&e.defaultView.getComputedStyle?e.defaultView.getComputedStyle(g):g.currentStyle;if(h&&h[a]!==b)for(d=0;d<c.length;d++)e.write('<link rel="stylesheet" href="'+c[d]+'"/>')}('visibility','hidden',['lib\/bootstrap\/css\/bootstrap.min.css?v=<!-- @echo cacheTag !>']);</script>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" />
<meta name="x-stylesheet-test-fa" content="" class="fa" />
<script>!function(a,b,c){var d,e=document,f=e.getElementsByTagName('SCRIPT'),g=f[f.length-1].previousElementSibling,h=e.defaultView&&e.defaultView.getComputedStyle?e.defaultView.getComputedStyle(g):g.currentStyle;if(h&&h[a]!==b)for(d=0;d<c.length;d++)e.write('<link rel="stylesheet" href="'+c[d]+'"/>')}('font-family','FontAwesome',['lib\/font-awesome\/css\/font-awesome.min.css?v=<!-- @echo cacheTag !>']);</script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">
<meta name="x-stylesheet-test" content="" class="fa invisible" />
<script src="js/fallback-styles.min.js?v=<!-- @echo cacheTag !>"></script>
<link rel="stylesheet" href="css/vault.min.css?v=<!-- @echo cacheTag !>" />
<!-- @endif -->
@@ -26,34 +58,28 @@
<link rel="stylesheet" href="css/vault.css" />
<!-- @endexclude -->
</head>
<body ng-controller="mainController as main" class="layout-boxed skin-blue sidebar-mini {{main.bodyClass}}">
<body ng-controller="mainController as main" class="layout-boxed skin-blue sidebar-mini {{main.bodyClass}}"
ng-class="{'using-control-sidebar': main.usingControlSidebar,
'control-sidebar-open': main.usingControlSidebar && main.openControlSidebar}">
<div ui-view></div>
<!-- @if true !>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script>(window.jQuery||document.write('<script src="lib\/jquery\/jquery.min.js?v=<!-- @echo cacheTag !>"><\/script>'));</script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script>((window.jQuery&&window.jQuery.fn&&window.jQuery.fn.modal)||document.write('<script src="lib\/bootstrap\/js\/bootstrap.min.js?v=<!-- @echo cacheTag !>"><\/script>'));</script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.6.3/angular.min.js"></script>
<script>(window.angular||document.write('<script src="lib\/angular\/angular.min.js?v=<!-- @echo cacheTag !>"><\/script>'));</script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"
integrity="sha384-rY/jv8mMhqDabXSo+UCggqKtdmBfd3qC2/KvyTDNQ6PcUJXaxK1tMepoQda4g5vB" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.3/angular.min.js"
integrity="sha384-AH/e+s4V4kUifvnNED2x1XZqArO5qTFU4YKRzUXbz4IgPG1H0Xmz6fP1XUmO4vT/" crossorigin="anonymous"></script>
<script src="js/fallback-scripts.min.js?v=<!-- @echo cacheTag !>"></script>
<script src="js/lib.min.js?v=<!-- @echo cacheTag !>"></script>
<script src="js/bw.min.js?v=<!-- @echo cacheTag !>"></script>
<script src="js/app.min.js?v=<!-- @echo cacheTag !>"></script>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-81915606-3', 'auto');
</script>
<!-- @endif -->
<!-- @exclude -->
<script src="lib/jquery/jquery.js"></script>
<script src="lib/bootstrap/js/bootstrap.js"></script>
<script src="js/main.js"></script>
<script src="lib/admin-lte/js/app.js"></script>
<script src="lib/forge/forge.js"></script>
@@ -62,6 +88,7 @@
<script src="lib/angular/angular.js"></script>
<script src="lib/angular-cookies/angular-cookies.js"></script>
<script src="lib/angular-sanitize/angular-sanitize.js"></script>
<script src="lib/angular-messages/angular-messages.js"></script>
<script src="lib/angular-jwt/angular-jwt.js"></script>
<script src="lib/angular-resource/angular-resource.js"></script>
@@ -90,6 +117,8 @@
<script src="app/directives/passwordMeterDirective.js"></script>
<script src="app/directives/passwordViewerDirective.js"></script>
<script src="app/directives/letterAvatarDirective.js"></script>
<script src="app/directives/stopClickDirective.js"></script>
<script src="app/directives/stopPropDirective.js"></script>
<script src="app/filters/filtersModule.js"></script>
<script src="app/filters/enumNameFilter.js"></script>
@@ -128,12 +157,14 @@
<script src="app/vault/vaultShareLoginController.js"></script>
<script src="app/vault/vaultSharedController.js"></script>
<script src="app/vault/vaultLoginCollectionsController.js"></script>
<script src="app/vault/vaultMoveLoginsController.js"></script>
<script src="app/organization/organizationModule.js"></script>
<script src="app/organization/organizationDashboardController.js"></script>
<script src="app/organization/organizationPeopleController.js"></script>
<script src="app/organization/organizationPeopleInviteController.js"></script>
<script src="app/organization/organizationPeopleEditController.js"></script>
<script src="app/organization/organizationPeopleGroupsController.js"></script>
<script src="app/organization/organizationCollectionsController.js"></script>
<script src="app/organization/organizationCollectionsAddController.js"></script>
<script src="app/organization/organizationCollectionsEditController.js"></script>
@@ -149,7 +180,9 @@
<script src="app/organization/organizationVaultEditLoginController.js"></script>
<script src="app/organization/organizationVaultLoginCollectionsController.js"></script>
<script src="app/organization/organizationGroupsController.js"></script>
<script src="app/organization/organizationCollectionsGroupsController.js"></script>
<script src="app/organization/organizationGroupsAddController.js"></script>
<script src="app/organization/organizationGroupsEditController.js"></script>
<script src="app/organization/organizationGroupsUsersController.js"></script>
<script src="app/settings/settingsModule.js"></script>
<script src="app/settings/settingsController.js"></script>
@@ -166,6 +199,9 @@
<script src="app/tools/toolsController.js"></script>
<script src="app/tools/toolsImportController.js"></script>
<script src="app/tools/toolsExportController.js"></script>
<script src="app/reports/reportsModule.js"></script>
<script src="app/reports/reportsBreachController.js"></script>
<!-- @endexclude -->
</body>
</html>

8
src/js/analytics.js Normal file
View File

@@ -0,0 +1,8 @@
(function (i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () {
(i[r].q = i[r].q || []).push(arguments)
}, i[r].l = 1 * new Date(); a = s.createElement(o),
m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m)
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
ga('create', 'UA-81915606-3', 'auto');

View File

@@ -0,0 +1,9 @@
function loadScriptIfMissing(condition, path) {
if (!condition) {
document.write('<script src="' + path + '?v=' + cacheTag + '"><\/script>');
}
}
loadScriptIfMissing(window.jQuery, 'lib\/jquery\/jquery.min.js');
loadScriptIfMissing(window.jQuery && window.jQuery.fn && window.jQuery.fn.modal, 'lib\/bootstrap\/js\/bootstrap.min.js');
loadScriptIfMissing(window.angular, 'lib\/angular\/angular.min.js');

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