mirror of
https://github.com/bitwarden/browser
synced 2025-12-23 03:33:54 +00:00
reorganize project folder structure and remove asp.net dependency
This commit is contained in:
51
src/app/accounts/accountsLoginController.js
Normal file
51
src/app/accounts/accountsLoginController.js
Normal file
@@ -0,0 +1,51 @@
|
||||
angular
|
||||
.module('bit.accounts')
|
||||
|
||||
.controller('accountsLoginController', function ($scope, $rootScope, $cookies, apiService, cryptoService, authService, $state, appSettings, $analytics) {
|
||||
var rememberedEmail = $cookies.get(appSettings.rememberedEmailCookieName);
|
||||
if (rememberedEmail) {
|
||||
$scope.model = {
|
||||
email: rememberedEmail,
|
||||
rememberEmail: true
|
||||
};
|
||||
}
|
||||
|
||||
$scope.login = function (model) {
|
||||
$scope.loginPromise = authService.logIn(model.email, model.masterPassword);
|
||||
|
||||
$scope.loginPromise.then(function () {
|
||||
if (model.rememberEmail) {
|
||||
var cookieExpiration = new Date();
|
||||
cookieExpiration.setFullYear(cookieExpiration.getFullYear() + 10);
|
||||
|
||||
$cookies.put(
|
||||
appSettings.rememberedEmailCookieName,
|
||||
model.email,
|
||||
{ expires: cookieExpiration });
|
||||
}
|
||||
else {
|
||||
$cookies.remove(appSettings.rememberedEmailCookieName);
|
||||
}
|
||||
|
||||
var profile = authService.getUserProfile();
|
||||
if (profile.twoFactor) {
|
||||
$analytics.eventTrack('Logged In To Two-step');
|
||||
$state.go('frontend.login.twoFactor');
|
||||
}
|
||||
else {
|
||||
$analytics.eventTrack('Logged In');
|
||||
$state.go('backend.vault');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.twoFactor = function (model) {
|
||||
// Only supporting Authenticator provider for now
|
||||
$scope.twoFactorPromise = authService.logInTwoFactor(model.code, "Authenticator");
|
||||
|
||||
$scope.twoFactorPromise.then(function () {
|
||||
$analytics.eventTrack('Logged In From Two-step');
|
||||
$state.go('backend.vault');
|
||||
});
|
||||
};
|
||||
});
|
||||
8
src/app/accounts/accountsLogoutController.js
Normal file
8
src/app/accounts/accountsLogoutController.js
Normal file
@@ -0,0 +1,8 @@
|
||||
angular
|
||||
.module('bit.accounts')
|
||||
|
||||
.controller('accountsLogoutController', function ($scope, authService, $state, $analytics) {
|
||||
authService.logOut();
|
||||
$analytics.eventTrack('Logged Out');
|
||||
$state.go('frontend.login.info');
|
||||
});
|
||||
2
src/app/accounts/accountsModule.js
Normal file
2
src/app/accounts/accountsModule.js
Normal file
@@ -0,0 +1,2 @@
|
||||
angular
|
||||
.module('bit.accounts', ['ui.bootstrap', 'ngCookies']);
|
||||
12
src/app/accounts/accountsPasswordHintController.js
Normal file
12
src/app/accounts/accountsPasswordHintController.js
Normal file
@@ -0,0 +1,12 @@
|
||||
angular
|
||||
.module('bit.accounts')
|
||||
|
||||
.controller('accountsPasswordHintController', function ($scope, $rootScope, apiService) {
|
||||
$scope.success = false;
|
||||
|
||||
$scope.submit = function (model) {
|
||||
$scope.submitPromise = apiService.accounts.postPasswordHint({ email: model.email }, function () {
|
||||
$scope.success = true;
|
||||
}).$promise;
|
||||
};
|
||||
});
|
||||
21
src/app/accounts/accountsRecoverController.js
Normal file
21
src/app/accounts/accountsRecoverController.js
Normal file
@@ -0,0 +1,21 @@
|
||||
angular
|
||||
.module('bit.accounts')
|
||||
|
||||
.controller('accountsRecoverController', function ($scope, apiService, cryptoService) {
|
||||
$scope.success = false;
|
||||
|
||||
$scope.submit = function (model) {
|
||||
var email = model.email.toLowerCase();
|
||||
var key = cryptoService.makeKey(model.masterPassword, email);
|
||||
|
||||
var request = {
|
||||
email: email,
|
||||
masterPasswordHash: cryptoService.hashPassword(model.masterPassword, key),
|
||||
recoveryCode: model.code.replace(/\s/g, '').toLowerCase()
|
||||
};
|
||||
|
||||
$scope.submitPromise = apiService.accounts.postTwoFactorRecover(request, function () {
|
||||
$scope.success = true;
|
||||
}).$promise;
|
||||
};
|
||||
});
|
||||
43
src/app/accounts/accountsRegisterController.js
Normal file
43
src/app/accounts/accountsRegisterController.js
Normal file
@@ -0,0 +1,43 @@
|
||||
angular
|
||||
.module('bit.accounts')
|
||||
|
||||
.controller('accountsRegisterController', function ($scope, $location, apiService, cryptoService, validationService, $analytics) {
|
||||
var params = $location.search();
|
||||
|
||||
$scope.success = false;
|
||||
$scope.model = {
|
||||
email: params.email
|
||||
};
|
||||
|
||||
$scope.registerPromise = null;
|
||||
$scope.register = function (form) {
|
||||
var error = false;
|
||||
|
||||
if ($scope.model.masterPassword.length < 8) {
|
||||
validationService.addError(form, 'MasterPassword', 'Master password must be at least 8 characters long.', true);
|
||||
error = true;
|
||||
}
|
||||
if ($scope.model.masterPassword !== $scope.model.confirmMasterPassword) {
|
||||
validationService.addError(form, 'ConfirmMasterPassword', 'Master password confirmation does not match.', true);
|
||||
error = true;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
var email = $scope.model.email.toLowerCase();
|
||||
var key = cryptoService.makeKey($scope.model.masterPassword, email);
|
||||
var request = {
|
||||
name: $scope.model.name,
|
||||
email: email,
|
||||
masterPasswordHash: cryptoService.hashPassword($scope.model.masterPassword, key),
|
||||
masterPasswordHint: $scope.model.masterPasswordHint
|
||||
};
|
||||
|
||||
$scope.registerPromise = apiService.accounts.register(request, function () {
|
||||
$scope.success = true;
|
||||
$analytics.eventTrack('Registered');
|
||||
}).$promise;
|
||||
};
|
||||
});
|
||||
7
src/app/accounts/views/accountsLogin.html
Normal file
7
src/app/accounts/views/accountsLogin.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<div class="login-box">
|
||||
<div class="login-logo">
|
||||
<i class="fa fa-shield"></i> <b>bit</b>warden
|
||||
</div>
|
||||
<div class="login-box-body" ui-view>
|
||||
</div>
|
||||
</div>
|
||||
41
src/app/accounts/views/accountsLoginInfo.html
Normal file
41
src/app/accounts/views/accountsLoginInfo.html
Normal file
@@ -0,0 +1,41 @@
|
||||
<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>
|
||||
<ul>
|
||||
<li ng-repeat="e in loginForm.$errors">{{e}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form-group has-feedback" show-errors>
|
||||
<label for="email" class="sr-only">Email</label>
|
||||
<input type="email" id="email" name="Email" class="form-control" placeholder="Email" ng-model="model.email"
|
||||
required api-field />
|
||||
<span class="fa fa-envelope form-control-feedback"></span>
|
||||
</div>
|
||||
<div class="form-group has-feedback" show-errors>
|
||||
<label for="masterPassword" class="sr-only">Master Password</label>
|
||||
<input type="password" id="masterPassword" name="MasterPasswordHash" class="form-control" placeholder="Master Password"
|
||||
ng-model="model.masterPassword"
|
||||
required api-field />
|
||||
<span class="fa fa-lock form-control-feedback"></span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-7">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="rememberEmail" ng-model="model.rememberEmail" /> Remember Email
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-5">
|
||||
<button type="submit" class="btn btn-primary btn-block btn-flat" ng-disabled="loginForm.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="loginForm.$loading"></i>Log In
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<ul>
|
||||
<li><a ui-sref="frontend.register">Create a new account</a></li>
|
||||
<li><a ui-sref="frontend.passwordHint">Get master password hint</a></li>
|
||||
</ul>
|
||||
</form>
|
||||
25
src/app/accounts/views/accountsLoginTwoFactor.html
Normal file
25
src/app/accounts/views/accountsLoginTwoFactor.html
Normal file
@@ -0,0 +1,25 @@
|
||||
<p class="login-box-msg">Enter your two-step verification code.</p>
|
||||
<form name="twoFactorForm" ng-submit="twoFactorForm.$valid && twoFactor(model)" api-form="twoFactorPromise">
|
||||
<div class="callout callout-danger validation-errors" ng-show="twoFactorForm.$errors">
|
||||
<h4>Errors have occured</h4>
|
||||
<ul>
|
||||
<li ng-repeat="e in twoFactorForm.$errors">{{e}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form-group has-feedback" show-errors>
|
||||
<label for="code" class="sr-only">Code</label>
|
||||
<input type="text" id="code" name="Code" class="form-control" placeholder="Verification code" ng-model="model.code"
|
||||
required api-field />
|
||||
<span class="fa fa-lock form-control-feedback"></span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-7">
|
||||
<a ui-sref="frontend.recover">Lost authenticator app?</a>
|
||||
</div>
|
||||
<div class="col-xs-5">
|
||||
<button type="submit" class="btn btn-primary btn-block btn-flat" ng-disabled="twoFactorForm.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="twoFactorForm.$loading"></i>Log In
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
39
src/app/accounts/views/accountsPasswordHint.html
Normal file
39
src/app/accounts/views/accountsPasswordHint.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<div class="login-box">
|
||||
<div class="login-logo">
|
||||
<i class="fa fa-shield"></i> <b>bit</b>warden
|
||||
</div>
|
||||
<div class="login-box-body">
|
||||
<p class="login-box-msg">Get your master password hint.</p>
|
||||
<div class="text-center" ng-show="success">
|
||||
<div class="callout callout-success">
|
||||
If your account exists ({{model.email}}) we've sent you an email with your master password hint.
|
||||
</div>
|
||||
<a ui-sref="frontend.login.info">Ready to log in?</a>
|
||||
</div>
|
||||
<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>
|
||||
<ul>
|
||||
<li ng-repeat="e in passwordHintForm.$errors">{{e}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form-group has-feedback" show-errors>
|
||||
<label for="email" class="sr-only">Your account email address</label>
|
||||
<input type="email" id="email" name="Email" class="form-control" placeholder="Your account email address"
|
||||
ng-model="model.email" required api-field />
|
||||
<span class="fa fa-envelope form-control-feedback"></span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-7">
|
||||
<a ui-sref="frontend.login.info">Ready to log in?</a>
|
||||
</div>
|
||||
<div class="col-xs-5">
|
||||
<button type="submit" class="btn btn-primary btn-block btn-flat" ng-disabled="passwordHintForm.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="passwordHintForm.$loading"></i>Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
52
src/app/accounts/views/accountsRecover.html
Normal file
52
src/app/accounts/views/accountsRecover.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<div class="login-box">
|
||||
<div class="login-logo">
|
||||
<i class="fa fa-shield"></i> <b>bit</b>warden
|
||||
</div>
|
||||
<div class="login-box-body">
|
||||
<p class="login-box-msg">Lost your authenticator app?</p>
|
||||
<div class="text-center" ng-show="success">
|
||||
<div class="callout callout-success">
|
||||
Two-step login has been successfully disabled on your account.
|
||||
</div>
|
||||
<a ui-sref="frontend.login.info">Ready to log in?</a>
|
||||
</div>
|
||||
<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>
|
||||
<ul>
|
||||
<li ng-repeat="e in recoverForm.$errors">{{e}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form-group has-feedback" show-errors>
|
||||
<label for="email" class="sr-only">Email</label>
|
||||
<input type="email" id="email" name="Email" class="form-control" placeholder="Email" ng-model="model.email"
|
||||
required api-field />
|
||||
<span class="fa fa-envelope form-control-feedback"></span>
|
||||
</div>
|
||||
<div class="form-group has-feedback" show-errors>
|
||||
<label for="masterPassword" class="sr-only">Master Password</label>
|
||||
<input type="password" id="masterPassword" name="MasterPasswordHash" class="form-control" placeholder="Master Password"
|
||||
ng-model="model.masterPassword"
|
||||
required api-field />
|
||||
<span class="fa fa-lock form-control-feedback"></span>
|
||||
</div>
|
||||
<div class="form-group has-feedback" show-errors>
|
||||
<label for="code" class="sr-only">Recovery code</label>
|
||||
<input type="text" id="code" name="RecoveryCode" class="form-control" placeholder="Recovery code"
|
||||
ng-model="model.code" required api-field />
|
||||
<span class="fa fa-key form-control-feedback"></span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-7">
|
||||
<a ui-sref="frontend.login.info">Ready to log in?</a>
|
||||
</div>
|
||||
<div class="col-xs-5">
|
||||
<button type="submit" class="btn btn-primary btn-block btn-flat" ng-disabled="recoverForm.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="recoverForm.$loading"></i>Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
73
src/app/accounts/views/accountsRegister.html
Normal file
73
src/app/accounts/views/accountsRegister.html
Normal file
@@ -0,0 +1,73 @@
|
||||
<div class="register-box">
|
||||
<div class="register-logo">
|
||||
<i class="fa fa-shield"></i> <b>bit</b>warden
|
||||
</div>
|
||||
<div class="register-box-body">
|
||||
<p class="login-box-msg">Create a new account.</p>
|
||||
<div class="text-center" ng-show="success">
|
||||
<div class="callout callout-success">
|
||||
<h4>Account Created!</h4>
|
||||
<p>You may now log in to your new account.</p>
|
||||
</div>
|
||||
<a ui-sref="frontend.login.info">Ready to log in?</a>
|
||||
</div>
|
||||
<form name="registerForm" ng-submit="registerForm.$valid && register(registerForm)" ng-show="!success"
|
||||
api-form="registerPromise">
|
||||
<div class="callout callout-danger validation-errors" ng-show="registerForm.$errors">
|
||||
<h4>Errors have occured</h4>
|
||||
<ul>
|
||||
<li ng-repeat="e in registerForm.$errors">{{e}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form-group has-feedback" show-errors>
|
||||
<label for="email" class="sr-only">Email</label>
|
||||
<input type="email" id="email" name="Email" class="form-control" placeholder="Email" ng-model="model.email"
|
||||
required api-field />
|
||||
<span class="fa fa-envelope form-control-feedback"></span>
|
||||
<p class="help-block">You'll use your email address to log in.</p>
|
||||
</div>
|
||||
<div class="form-group has-feedback" show-errors>
|
||||
<label for="name" class="sr-only">Your Name</label>
|
||||
<input type="text" id="name" name="Name" class="form-control" ng-model="model.name"
|
||||
placeholder="Your Name" api-field>
|
||||
<span class="fa fa-user form-control-feedback"></span>
|
||||
<p class="help-block">What should we call you?</p>
|
||||
</div>
|
||||
<div class="form-group has-feedback" show-errors>
|
||||
<label for="masterPassword" class="sr-only">Master Password</label>
|
||||
<input type="password" id="masterPassword" name="MasterPasswordHash" class="form-control"
|
||||
ng-model="model.masterPassword" placeholder="Master Password" required api-field>
|
||||
<span class="fa fa-lock form-control-feedback"></span>
|
||||
<p class="help-block">The master password is the password you use to access your vault.</p>
|
||||
</div>
|
||||
<div class="form-group has-feedback" show-errors>
|
||||
<label form="confirmMasterPassword" class="sr-only">Re-type Master Password</label>
|
||||
<input type="password" id="confirmMasterPassword" name="ConfirmMasterPassword" class="form-control"
|
||||
placeholder="Re-type Master Password"
|
||||
ng-model="model.confirmMasterPassword" required api-field>
|
||||
<span class="fa fa-lock form-control-feedback"></span>
|
||||
<p class="help-block">
|
||||
It is very important that you do not forget your master password.
|
||||
There is <u>no way</u> to recover the password in the event that you forget it.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group has-feedback" show-errors>
|
||||
<label for="hint" class="sr-only">Master Password Hint (optional)</label>
|
||||
<input type="text" id="hint" name="MasterPasswordHint" class="form-control" ng-model="model.masterPasswordHint"
|
||||
placeholder="Master Password Hint (optional)" api-field>
|
||||
<span class="fa fa-lightbulb-o form-control-feedback"></span>
|
||||
<p class="help-block">A master password hint can help you remember your password if you forget it.</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-7">
|
||||
<a ui-sref="frontend.login.info">Already have an account?</a>
|
||||
</div>
|
||||
<div class="col-xs-5">
|
||||
<button type="submit" class="btn btn-primary btn-block btn-flat" ng-disabled="registerForm.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="registerForm.$loading"></i>Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
29
src/app/apiInterceptor.js
Normal file
29
src/app/apiInterceptor.js
Normal file
@@ -0,0 +1,29 @@
|
||||
angular
|
||||
.module('bit')
|
||||
|
||||
.factory('apiInterceptor', function ($injector, $q, toastr) {
|
||||
return {
|
||||
request: function (config) {
|
||||
return config;
|
||||
},
|
||||
response: function (response) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
$injector.get('authService').logOut();
|
||||
$injector.get('$state').go('frontend.login.info').then(function () {
|
||||
toastr.warning('Your login session has expired.', 'Logged out');
|
||||
});
|
||||
}
|
||||
|
||||
return response || $q.when(response);
|
||||
},
|
||||
responseError: function (rejection) {
|
||||
if (rejection.status === 401 || rejection.status === 403) {
|
||||
$injector.get('authService').logOut();
|
||||
$injector.get('$state').go('frontend.login.info').then(function () {
|
||||
toastr.warning('Your login session has expired.', 'Logged out');
|
||||
});
|
||||
}
|
||||
return $q.reject(rejection);
|
||||
}
|
||||
};
|
||||
});
|
||||
20
src/app/app.js
Normal file
20
src/app/app.js
Normal file
@@ -0,0 +1,20 @@
|
||||
angular
|
||||
.module('bit', [
|
||||
'ui.router',
|
||||
'ngMessages',
|
||||
'angular-jwt',
|
||||
'angular-md5',
|
||||
'ui.bootstrap.showErrors',
|
||||
'toastr',
|
||||
'angulartics',
|
||||
'angulartics.google.analytics',
|
||||
|
||||
'bit.directives',
|
||||
'bit.services',
|
||||
|
||||
'bit.global',
|
||||
'bit.accounts',
|
||||
'bit.vault',
|
||||
'bit.settings',
|
||||
'bit.tools'
|
||||
]);
|
||||
144
src/app/config.js
Normal file
144
src/app/config.js
Normal file
@@ -0,0 +1,144 @@
|
||||
angular
|
||||
.module('bit')
|
||||
|
||||
.config(function ($stateProvider, $urlRouterProvider, $httpProvider, jwtInterceptorProvider, $uibTooltipProvider, toastrConfig) {
|
||||
jwtInterceptorProvider.urlParam = 'access_token';
|
||||
jwtInterceptorProvider.tokenGetter = /*@ngInject*/ function (config, appSettings, tokenService) {
|
||||
if (config.url.indexOf(appSettings.apiUri) === 0) {
|
||||
return tokenService.getToken();
|
||||
}
|
||||
};
|
||||
|
||||
angular.extend(toastrConfig, {
|
||||
closeButton: true,
|
||||
progressBar: true,
|
||||
showMethod: 'slideDown',
|
||||
target: '.toast-target'
|
||||
});
|
||||
|
||||
$uibTooltipProvider.options({
|
||||
popupDelay: 600
|
||||
});
|
||||
|
||||
if ($httpProvider.defaults.headers.post) {
|
||||
$httpProvider.defaults.headers.post = {};
|
||||
}
|
||||
|
||||
$httpProvider.defaults.headers.post['Content-Type'] = 'text/plain; charset=utf-8';
|
||||
|
||||
$httpProvider.interceptors.push('apiInterceptor');
|
||||
$httpProvider.interceptors.push('jwtInterceptor');
|
||||
|
||||
$urlRouterProvider.otherwise('/');
|
||||
|
||||
$stateProvider
|
||||
// Backend
|
||||
.state('backend', {
|
||||
templateUrl: 'app/views/backendLayout.html',
|
||||
abstract: true,
|
||||
data: {
|
||||
authorize: true
|
||||
}
|
||||
})
|
||||
.state('backend.vault', {
|
||||
url: '^/',
|
||||
templateUrl: 'app/vault/views/vault.html',
|
||||
controller: 'vaultController',
|
||||
data: { pageTitle: 'My Vault' }
|
||||
})
|
||||
.state('backend.settings', {
|
||||
url: '^/settings',
|
||||
templateUrl: 'app/settings/views/settings.html',
|
||||
controller: 'settingsController',
|
||||
data: { pageTitle: 'Settings' }
|
||||
})
|
||||
.state('backend.tools', {
|
||||
url: '^/tools',
|
||||
templateUrl: 'app/tools/views/tools.html',
|
||||
controller: 'toolsController',
|
||||
data: { pageTitle: 'Tools' }
|
||||
})
|
||||
|
||||
// Frontend
|
||||
.state('frontend', {
|
||||
templateUrl: 'app/views/frontendLayout.html',
|
||||
abstract: true,
|
||||
data: {
|
||||
authorize: false
|
||||
}
|
||||
})
|
||||
.state('frontend.login', {
|
||||
templateUrl: 'app/accounts/views/accountsLogin.html',
|
||||
controller: 'accountsLoginController',
|
||||
data: {
|
||||
bodyClass: 'login-page'
|
||||
}
|
||||
})
|
||||
.state('frontend.login.info', {
|
||||
url: '^/login',
|
||||
templateUrl: 'app/accounts/views/accountsLoginInfo.html',
|
||||
data: {
|
||||
pageTitle: 'Log In'
|
||||
}
|
||||
})
|
||||
.state('frontend.login.twoFactor', {
|
||||
url: '^/login/two-factor',
|
||||
templateUrl: 'app/accounts/views/accountsLoginTwoFactor.html',
|
||||
data: {
|
||||
pageTitle: 'Log In (Two Factor)',
|
||||
authorizeTwoFactor: true
|
||||
}
|
||||
})
|
||||
.state('frontend.logout', {
|
||||
url: '^/logout',
|
||||
controller: 'accountsLogoutController',
|
||||
data: {
|
||||
authorize: true
|
||||
}
|
||||
})
|
||||
.state('frontend.passwordHint', {
|
||||
url: '^/password-hint',
|
||||
templateUrl: 'app/accounts/views/accountsPasswordHint.html',
|
||||
controller: 'accountsPasswordHintController',
|
||||
data: {
|
||||
pageTitle: 'Master Password Hint',
|
||||
bodyClass: 'login-page'
|
||||
}
|
||||
})
|
||||
.state('frontend.recover', {
|
||||
url: '^/recover',
|
||||
templateUrl: 'app/accounts/views/accountsRecover.html',
|
||||
controller: 'accountsRecoverController',
|
||||
data: {
|
||||
pageTitle: 'Recover Account',
|
||||
bodyClass: 'login-page'
|
||||
}
|
||||
})
|
||||
.state('frontend.register', {
|
||||
url: '^/register',
|
||||
templateUrl: 'app/accounts/views/accountsRegister.html',
|
||||
controller: 'accountsRegisterController',
|
||||
data: {
|
||||
pageTitle: 'Register',
|
||||
bodyClass: 'register-page'
|
||||
}
|
||||
});
|
||||
})
|
||||
.run(function ($rootScope, authService, jwtHelper, tokenService, $state) {
|
||||
$rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
|
||||
if (!toState.data || !toState.data.authorize) {
|
||||
if (authService.isAuthenticated() && !jwtHelper.isTokenExpired(tokenService.getToken())) {
|
||||
event.preventDefault();
|
||||
$state.go('backend.vault');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!authService.isAuthenticated() || jwtHelper.isTokenExpired(tokenService.getToken())) {
|
||||
event.preventDefault();
|
||||
authService.logOut();
|
||||
$state.go('frontend.login.info');
|
||||
}
|
||||
});
|
||||
});
|
||||
30
src/app/directives/apiFieldDirective.js
Normal file
30
src/app/directives/apiFieldDirective.js
Normal file
@@ -0,0 +1,30 @@
|
||||
angular
|
||||
.module('bit.directives')
|
||||
|
||||
.directive('apiField', function () {
|
||||
var linkFn = function (scope, element, attrs, ngModel) {
|
||||
ngModel.$registerApiError = registerApiError;
|
||||
ngModel.$validators.apiValidate = apiValidator;
|
||||
|
||||
function apiValidator() {
|
||||
ngModel.$setValidity('api', true);
|
||||
return true;
|
||||
}
|
||||
|
||||
function registerApiError() {
|
||||
ngModel.$setValidity('api', false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
require: 'ngModel',
|
||||
restrict: 'A',
|
||||
compile: function (elem, attrs) {
|
||||
if (!attrs.name || attrs.name === '') {
|
||||
throw 'api-field element does not have a valid name attribute';
|
||||
}
|
||||
|
||||
return linkFn;
|
||||
}
|
||||
};
|
||||
});
|
||||
35
src/app/directives/apiFormDirective.js
Normal file
35
src/app/directives/apiFormDirective.js
Normal file
@@ -0,0 +1,35 @@
|
||||
angular
|
||||
.module('bit.directives')
|
||||
|
||||
.directive('apiForm', function ($rootScope, validationService) {
|
||||
return {
|
||||
require: 'form',
|
||||
restrict: 'A',
|
||||
link: function (scope, element, attrs, formCtrl) {
|
||||
var watchPromise = attrs.apiForm || null;
|
||||
if (watchPromise !== void 0) {
|
||||
scope.$watch(watchPromise, formSubmitted.bind(null, formCtrl, scope));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function formSubmitted(form, scope, promise) {
|
||||
if (!promise || !promise.then) {
|
||||
return;
|
||||
}
|
||||
|
||||
// reset errors
|
||||
form.$errors = null;
|
||||
|
||||
// start loading
|
||||
form.$loading = true;
|
||||
|
||||
promise.then(function success(response) {
|
||||
form.$loading = false;
|
||||
}, function failure(reason) {
|
||||
form.$loading = false;
|
||||
validationService.addErrors(form, reason);
|
||||
scope.$broadcast('show-errors-check-validity');
|
||||
});
|
||||
}
|
||||
});
|
||||
2
src/app/directives/directivesModule.js
Normal file
2
src/app/directives/directivesModule.js
Normal file
@@ -0,0 +1,2 @@
|
||||
angular
|
||||
.module('bit.directives', []);
|
||||
40
src/app/directives/masterPasswordDirective.js
Normal file
40
src/app/directives/masterPasswordDirective.js
Normal file
@@ -0,0 +1,40 @@
|
||||
angular
|
||||
.module('bit.directives')
|
||||
|
||||
.directive('masterPassword', function (cryptoService, authService) {
|
||||
return {
|
||||
require: 'ngModel',
|
||||
restrict: 'A',
|
||||
link: function (scope, elem, attr, ngModel) {
|
||||
var profile = authService.getUserProfile();
|
||||
if (!profile) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For DOM -> model validation
|
||||
ngModel.$parsers.unshift(function (value) {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
var key = cryptoService.makeKey(value, profile.email, true);
|
||||
var valid = key === cryptoService.getKey(true);
|
||||
ngModel.$setValidity('masterPassword', valid);
|
||||
return valid ? value : undefined;
|
||||
});
|
||||
|
||||
// For model -> DOM validation
|
||||
ngModel.$formatters.unshift(function (value) {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
var key = cryptoService.makeKey(value, profile.email, true);
|
||||
var valid = key === cryptoService.getKey(true);
|
||||
|
||||
ngModel.$setValidity('masterPassword', valid);
|
||||
return value;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
22
src/app/directives/pageTitleDirective.js
Normal file
22
src/app/directives/pageTitleDirective.js
Normal file
@@ -0,0 +1,22 @@
|
||||
angular
|
||||
.module('bit.directives')
|
||||
|
||||
.directive('pageTitle', function ($rootScope, $timeout, appSettings) {
|
||||
return {
|
||||
link: function (scope, element) {
|
||||
var listener = function (event, toState, toParams, fromState, fromParams) {
|
||||
// Default title
|
||||
var title = 'bitwarden Password Manager';
|
||||
if (toState.data && toState.data.pageTitle) {
|
||||
title = toState.data.pageTitle + ' - bitwarden Password Manager';
|
||||
}
|
||||
|
||||
$timeout(function () {
|
||||
element.text(title);
|
||||
});
|
||||
};
|
||||
|
||||
$rootScope.$on('$stateChangeStart', listener);
|
||||
}
|
||||
};
|
||||
});
|
||||
73
src/app/directives/passwordMeterDirective.js
Normal file
73
src/app/directives/passwordMeterDirective.js
Normal file
@@ -0,0 +1,73 @@
|
||||
angular
|
||||
.module('bit.directives')
|
||||
|
||||
.directive('passwordMeter', function () {
|
||||
return {
|
||||
template: '<div class="progress {{outerClass}}"><div class="progress-bar progress-bar-{{valueClass}}" ' +
|
||||
'role="progressbar" aria-valuenow="{{value}}" aria-valuemin="0" aria-valuemax="100" ' +
|
||||
'ng-style="{width : ( value + \'%\' ) }"><span class="sr-only">{{value}}%</span></div></div>',
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
password: '=passwordMeter',
|
||||
username: '=passwordMeterUsername',
|
||||
outerClass: '@?'
|
||||
},
|
||||
link: function (scope) {
|
||||
var measureStrength = function (username, password) {
|
||||
if (!password || password === username) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var strength = password.length;
|
||||
|
||||
if (username && username !== '') {
|
||||
if (username.indexOf(password) !== -1) strength -= 15;
|
||||
if (password.indexOf(username) !== -1) strength -= username.length;
|
||||
}
|
||||
|
||||
if (password.length > 0 && password.length <= 4) strength += password.length;
|
||||
else if (password.length >= 5 && password.length <= 7) strength += 6;
|
||||
else if (password.length >= 8 && password.length <= 15) strength += 12;
|
||||
else if (password.length >= 16) strength += 18;
|
||||
|
||||
if (password.match(/[a-z]/)) strength += 1;
|
||||
if (password.match(/[A-Z]/)) strength += 5;
|
||||
if (password.match(/\d/)) strength += 5;
|
||||
if (password.match(/.*\d.*\d.*\d/)) strength += 5;
|
||||
if (password.match(/[!,@,#,$,%,^,&,*,?,_,~]/)) strength += 5;
|
||||
if (password.match(/.*[!,@,#,$,%,^,&,*,?,_,~].*[!,@,#,$,%,^,&,*,?,_,~]/)) strength += 5;
|
||||
if (password.match(/(?=.*[a-z])(?=.*[A-Z])/)) strength += 2;
|
||||
if (password.match(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/)) strength += 2;
|
||||
if (password.match(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!,@,#,$,%,^,&,*,?,_,~])/)) strength += 2;
|
||||
|
||||
strength = Math.round(strength * 2);
|
||||
return Math.max(0, Math.min(100, strength));
|
||||
};
|
||||
|
||||
var getClass = function (strength) {
|
||||
switch (Math.round(strength / 33)) {
|
||||
case 0:
|
||||
case 1:
|
||||
return 'danger';
|
||||
case 2:
|
||||
return 'warning';
|
||||
case 3:
|
||||
return 'success';
|
||||
}
|
||||
};
|
||||
|
||||
var updateMeter = function (scope) {
|
||||
scope.value = measureStrength(scope.username, scope.password);
|
||||
scope.valueClass = getClass(scope.value);
|
||||
};
|
||||
|
||||
scope.$watch('password', function () {
|
||||
updateMeter(scope);
|
||||
});
|
||||
|
||||
scope.$watch('username', function () {
|
||||
updateMeter(scope);
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
27
src/app/directives/passwordViewerDirective.js
Normal file
27
src/app/directives/passwordViewerDirective.js
Normal file
@@ -0,0 +1,27 @@
|
||||
angular
|
||||
.module('bit.directives')
|
||||
|
||||
.directive('passwordViewer', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope, element, attr) {
|
||||
var passwordViewer = attr.passwordViewer;
|
||||
if (!passwordViewer) {
|
||||
return;
|
||||
}
|
||||
|
||||
element.onclick = function (event) { };
|
||||
element.on('click', function (event) {
|
||||
var passwordElement = $(passwordViewer);
|
||||
if (passwordElement && passwordElement.attr('type') === 'password') {
|
||||
element.removeClass('fa-eye').addClass('fa-eye-slash');
|
||||
passwordElement.attr('type', 'text');
|
||||
}
|
||||
else if (passwordElement && passwordElement.attr('type') === 'text') {
|
||||
element.removeClass('fa-eye-slash').addClass('fa-eye');
|
||||
passwordElement.attr('type', 'password');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
2
src/app/global/globalModule.js
Normal file
2
src/app/global/globalModule.js
Normal file
@@ -0,0 +1,2 @@
|
||||
angular
|
||||
.module('bit.global', []);
|
||||
82
src/app/global/mainController.js
Normal file
82
src/app/global/mainController.js
Normal file
@@ -0,0 +1,82 @@
|
||||
angular
|
||||
.module('bit.global')
|
||||
|
||||
.controller('mainController', function ($scope, $state, authService, appSettings, toastr) {
|
||||
var vm = this;
|
||||
vm.bodyClass = '';
|
||||
vm.userProfile = null;
|
||||
vm.searchVaultText = null;
|
||||
vm.version = appSettings.version;
|
||||
|
||||
$scope.currentYear = new Date().getFullYear();
|
||||
|
||||
$scope.$on('$viewContentLoaded', function () {
|
||||
if ($.AdminLTE) {
|
||||
if ($.AdminLTE.layout) {
|
||||
$.AdminLTE.layout.fix();
|
||||
$.AdminLTE.layout.fixSidebar();
|
||||
}
|
||||
|
||||
if ($.AdminLTE.pushMenu) {
|
||||
$.AdminLTE.pushMenu.expandOnHover();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams) {
|
||||
vm.searchVaultText = null;
|
||||
vm.userProfile = authService.getUserProfile();
|
||||
|
||||
if (toState.data.bodyClass) {
|
||||
vm.bodyClass = toState.data.bodyClass;
|
||||
return;
|
||||
}
|
||||
else {
|
||||
vm.bodyClass = '';
|
||||
}
|
||||
});
|
||||
|
||||
$scope.searchVault = function () {
|
||||
$state.go('backend.vault');
|
||||
};
|
||||
|
||||
$scope.addSite = function () {
|
||||
$scope.$broadcast('vaultAddSite');
|
||||
};
|
||||
|
||||
$scope.addFolder = function () {
|
||||
$scope.$broadcast('vaultAddFolder');
|
||||
};
|
||||
|
||||
$scope.changeEmail = function () {
|
||||
$scope.$broadcast('settingsChangeEmail');
|
||||
};
|
||||
|
||||
$scope.changePassword = function () {
|
||||
$scope.$broadcast('settingsChangePassword');
|
||||
};
|
||||
|
||||
$scope.sessions = function () {
|
||||
$scope.$broadcast('settingsSessions');
|
||||
};
|
||||
|
||||
$scope.delete = function () {
|
||||
$scope.$broadcast('settingsDelete');
|
||||
};
|
||||
|
||||
$scope.twoFactor = function () {
|
||||
$scope.$broadcast('settingsTwoFactor');
|
||||
};
|
||||
|
||||
$scope.import = function () {
|
||||
$scope.$broadcast('toolsImport');
|
||||
};
|
||||
|
||||
$scope.export = function () {
|
||||
$scope.$broadcast('toolsExport');
|
||||
};
|
||||
|
||||
$scope.audits = function () {
|
||||
$scope.$broadcast('toolsAudits');
|
||||
};
|
||||
});
|
||||
6
src/app/global/sideNavController.js
Normal file
6
src/app/global/sideNavController.js
Normal file
@@ -0,0 +1,6 @@
|
||||
angular
|
||||
.module('bit.global')
|
||||
|
||||
.controller('sideNavController', function ($scope, $state) {
|
||||
$scope.$state = $state;
|
||||
});
|
||||
6
src/app/global/topNavController.js
Normal file
6
src/app/global/topNavController.js
Normal file
@@ -0,0 +1,6 @@
|
||||
angular
|
||||
.module('bit.global')
|
||||
|
||||
.controller('topNavController', function ($scope) {
|
||||
|
||||
});
|
||||
54
src/app/services/apiService.js
Normal file
54
src/app/services/apiService.js
Normal file
@@ -0,0 +1,54 @@
|
||||
angular
|
||||
.module('bit.services')
|
||||
|
||||
.factory('apiService', function ($resource, tokenService, appSettings) {
|
||||
var _service = {},
|
||||
_apiUri = appSettings.apiUri;
|
||||
|
||||
_service.sites = $resource(_apiUri + '/sites/:id', {}, {
|
||||
get: { method: 'GET', params: { id: '@id' } },
|
||||
list: { method: 'GET', params: {} },
|
||||
post: { method: 'POST', params: {} },
|
||||
put: { method: 'POST', params: { id: '@id' } },
|
||||
del: { url: _apiUri + '/sites/:id/delete', method: 'POST', params: { id: '@id' } }
|
||||
});
|
||||
|
||||
_service.folders = $resource(_apiUri + '/folders/:id', {}, {
|
||||
get: { method: 'GET', params: { id: '@id' } },
|
||||
list: { method: 'GET', params: {} },
|
||||
post: { method: 'POST', params: {} },
|
||||
put: { method: 'POST', params: { id: '@id' } },
|
||||
del: { url: _apiUri + '/folders/:id/delete', method: 'POST', params: { id: '@id' } }
|
||||
});
|
||||
|
||||
_service.ciphers = $resource(_apiUri + '/ciphers/:id', {}, {
|
||||
get: { method: 'GET', params: { id: '@id' } },
|
||||
list: { method: 'GET', params: {} },
|
||||
'import': { url: _apiUri + '/ciphers/import', method: 'POST', params: {} },
|
||||
favorite: { url: _apiUri + '/ciphers/:id/favorite', method: 'POST', params: { id: '@id' } },
|
||||
del: { url: _apiUri + '/ciphers/:id/delete', method: 'POST', params: { id: '@id' } }
|
||||
});
|
||||
|
||||
_service.accounts = $resource(_apiUri + '/accounts', {}, {
|
||||
register: { url: _apiUri + '/accounts/register', method: 'POST', params: {} },
|
||||
emailToken: { url: _apiUri + '/accounts/email-token', method: 'POST', params: {} },
|
||||
email: { url: _apiUri + '/accounts/email', method: 'POST', params: {} },
|
||||
putPassword: { url: _apiUri + '/accounts/password', method: 'POST', params: {} },
|
||||
getProfile: { url: _apiUri + '/accounts/profile', method: 'GET', params: {} },
|
||||
putProfile: { url: _apiUri + '/accounts/profile', method: 'POST', params: {} },
|
||||
getTwoFactor: { url: _apiUri + '/accounts/two-factor', method: 'GET', params: {} },
|
||||
putTwoFactor: { url: _apiUri + '/accounts/two-factor', method: 'POST', params: {} },
|
||||
postTwoFactorRecover: { url: _apiUri + '/accounts/two-factor-recover', method: 'POST', params: {} },
|
||||
postPasswordHint: { url: _apiUri + '/accounts/password-hint', method: 'POST', params: {} },
|
||||
putSecurityStamp: { url: _apiUri + '/accounts/security-stamp', method: 'POST', params: {} },
|
||||
'import': { url: _apiUri + '/accounts/import', method: 'POST', params: {} },
|
||||
postDelete: { url: _apiUri + '/accounts/delete', method: 'POST', params: {} }
|
||||
});
|
||||
|
||||
_service.auth = $resource(_apiUri + '/auth', {}, {
|
||||
token: { url: _apiUri + '/auth/token', method: 'POST', params: {} },
|
||||
tokenTwoFactor: { url: _apiUri + '/auth/token/two-factor', method: 'POST', params: {} }
|
||||
});
|
||||
|
||||
return _service;
|
||||
});
|
||||
112
src/app/services/authService.js
Normal file
112
src/app/services/authService.js
Normal file
@@ -0,0 +1,112 @@
|
||||
angular
|
||||
.module('bit.services')
|
||||
|
||||
.factory('authService', function (cryptoService, apiService, tokenService, $q, jwtHelper) {
|
||||
var _service = {},
|
||||
_userProfile = null;
|
||||
|
||||
_service.logIn = function (email, masterPassword) {
|
||||
email = email.toLowerCase();
|
||||
var key = cryptoService.makeKey(masterPassword, email);
|
||||
|
||||
var request = {
|
||||
email: email,
|
||||
masterPasswordHash: cryptoService.hashPassword(masterPassword, key)
|
||||
};
|
||||
|
||||
var deferred = $q.defer();
|
||||
apiService.auth.token(request, function (response) {
|
||||
if (!response || !response.Token) {
|
||||
return;
|
||||
}
|
||||
|
||||
tokenService.setToken(response.Token);
|
||||
cryptoService.setKey(key);
|
||||
_service.setUserProfile(response.Profile);
|
||||
|
||||
deferred.resolve(response);
|
||||
}, function (error) {
|
||||
deferred.reject(error);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
_service.logInTwoFactor = function (code, provider) {
|
||||
var request = {
|
||||
code: code.replace(' ', ''),
|
||||
provider: provider
|
||||
};
|
||||
|
||||
var deferred = $q.defer();
|
||||
apiService.auth.tokenTwoFactor(request, function (response) {
|
||||
if (!response || !response.Token) {
|
||||
return;
|
||||
}
|
||||
|
||||
tokenService.setToken(response.Token);
|
||||
_service.setUserProfile(response.Profile);
|
||||
|
||||
deferred.resolve(response);
|
||||
}, function (error) {
|
||||
deferred.reject(error);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
_service.logOut = function () {
|
||||
tokenService.clearToken();
|
||||
cryptoService.clearKey();
|
||||
_userProfile = null;
|
||||
};
|
||||
|
||||
_service.getUserProfile = function () {
|
||||
if (!_userProfile) {
|
||||
_service.setUserProfile();
|
||||
}
|
||||
|
||||
return _userProfile;
|
||||
};
|
||||
|
||||
_service.setUserProfile = function (profile) {
|
||||
var token = tokenService.getToken();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
var decodedToken = jwtHelper.decodeToken(token);
|
||||
var twoFactor = decodedToken.authmethod === "TwoFactor";
|
||||
|
||||
_userProfile = {
|
||||
id: decodedToken.nameid,
|
||||
email: decodedToken.email,
|
||||
twoFactor: twoFactor
|
||||
};
|
||||
|
||||
if (!twoFactor && profile) {
|
||||
loadProfile(profile);
|
||||
}
|
||||
else if (!twoFactor && !profile) {
|
||||
apiService.accounts.getProfile({}, loadProfile);
|
||||
}
|
||||
};
|
||||
|
||||
function loadProfile(profile) {
|
||||
_userProfile.extended = {
|
||||
name: profile.Name,
|
||||
twoFactorEnabled: profile.TwoFactorEnabled,
|
||||
culture: profile.Culture
|
||||
};
|
||||
}
|
||||
|
||||
_service.isAuthenticated = function () {
|
||||
return _service.getUserProfile() !== null && !_service.getUserProfile().twoFactor;
|
||||
};
|
||||
|
||||
_service.isTwoFactorAuthenticated = function () {
|
||||
return _service.getUserProfile() !== null && _service.getUserProfile().twoFactor;
|
||||
};
|
||||
|
||||
return _service;
|
||||
});
|
||||
112
src/app/services/cipherService.js
Normal file
112
src/app/services/cipherService.js
Normal file
@@ -0,0 +1,112 @@
|
||||
angular
|
||||
.module('bit.services')
|
||||
|
||||
.factory('cipherService', function (cryptoService, apiService) {
|
||||
var _service = {};
|
||||
|
||||
_service.decryptSites = function (encryptedSites) {
|
||||
if (!encryptedSites) throw "encryptedSites is undefined or null";
|
||||
|
||||
var unencryptedSites = [];
|
||||
for (var i = 0; i < encryptedSites.length; i++) {
|
||||
unencryptedSites.push(_service.decryptSite(encryptedSites[i]));
|
||||
}
|
||||
|
||||
return unencryptedSites;
|
||||
};
|
||||
|
||||
_service.decryptSite = function (encryptedSite) {
|
||||
if (!encryptedSite) throw "encryptedSite is undefined or null";
|
||||
|
||||
var site = {
|
||||
id: encryptedSite.Id,
|
||||
'type': 1,
|
||||
folderId: encryptedSite.FolderId,
|
||||
favorite: encryptedSite.Favorite,
|
||||
name: cryptoService.decrypt(encryptedSite.Name),
|
||||
uri: encryptedSite.Uri && encryptedSite.Uri !== '' ? cryptoService.decrypt(encryptedSite.Uri) : null,
|
||||
username: encryptedSite.Username && encryptedSite.Username !== '' ? cryptoService.decrypt(encryptedSite.Username) : null,
|
||||
password: encryptedSite.Password && encryptedSite.Password !== '' ? cryptoService.decrypt(encryptedSite.Password) : null,
|
||||
notes: encryptedSite.Notes && encryptedSite.Notes !== '' ? cryptoService.decrypt(encryptedSite.Notes) : null
|
||||
};
|
||||
|
||||
if (encryptedSite.Folder) {
|
||||
site.folder = {
|
||||
name: cryptoService.decrypt(encryptedSite.Folder.Name)
|
||||
};
|
||||
}
|
||||
|
||||
return site;
|
||||
};
|
||||
|
||||
_service.decryptFolders = function (encryptedFolders) {
|
||||
if (!encryptedFolders) throw "encryptedFolders is undefined or null";
|
||||
|
||||
var unencryptedFolders = [];
|
||||
for (var i = 0; i < encryptedFolders.length; i++) {
|
||||
unencryptedFolders.push(_service.decryptFolder(encryptedFolders[i]));
|
||||
}
|
||||
|
||||
return unencryptedFolders;
|
||||
};
|
||||
|
||||
_service.decryptFolder = function (encryptedFolder) {
|
||||
if (!encryptedFolder) throw "encryptedFolder is undefined or null";
|
||||
|
||||
return {
|
||||
id: encryptedFolder.Id,
|
||||
'type': 0,
|
||||
name: cryptoService.decrypt(encryptedFolder.Name)
|
||||
};
|
||||
};
|
||||
|
||||
_service.encryptSites = function (unencryptedSites, key) {
|
||||
if (!unencryptedSites) throw "unencryptedSites is undefined or null";
|
||||
|
||||
var encryptedSites = [];
|
||||
for (var i = 0; i < unencryptedSites.length; i++) {
|
||||
encryptedSites.push(_service.encryptSite(unencryptedSites[i], key));
|
||||
}
|
||||
|
||||
return encryptedSites;
|
||||
};
|
||||
|
||||
_service.encryptSite = function (unencryptedSite, key) {
|
||||
if (!unencryptedSite) throw "unencryptedSite is undefined or null";
|
||||
|
||||
return {
|
||||
id: unencryptedSite.id,
|
||||
'type': 1,
|
||||
folderId: unencryptedSite.folderId === '' ? null : unencryptedSite.folderId,
|
||||
favorite: unencryptedSite.favorite !== null ? unencryptedSite.favorite : false,
|
||||
uri: !unencryptedSite.uri || unencryptedSite.uri === '' ? null : cryptoService.encrypt(unencryptedSite.uri, key),
|
||||
name: cryptoService.encrypt(unencryptedSite.name, key),
|
||||
username: !unencryptedSite.username || unencryptedSite.username === '' ? null : cryptoService.encrypt(unencryptedSite.username, key),
|
||||
password: !unencryptedSite.password || unencryptedSite.password === '' ? null : cryptoService.encrypt(unencryptedSite.password, key),
|
||||
notes: !unencryptedSite.notes || unencryptedSite.notes === '' ? null : cryptoService.encrypt(unencryptedSite.notes, key)
|
||||
};
|
||||
};
|
||||
|
||||
_service.encryptFolders = function (unencryptedFolders, key) {
|
||||
if (!unencryptedFolders) throw "unencryptedFolders is undefined or null";
|
||||
|
||||
var encryptedFolders = [];
|
||||
for (var i = 0; i < unencryptedFolders.length; i++) {
|
||||
encryptedFolders.push(_service.encryptFolder(unencryptedFolders[i], key));
|
||||
}
|
||||
|
||||
return encryptedFolders;
|
||||
};
|
||||
|
||||
_service.encryptFolder = function (unencryptedFolder, key) {
|
||||
if (!unencryptedFolder) throw "unencryptedFolder is undefined or null";
|
||||
|
||||
return {
|
||||
id: unencryptedFolder.id,
|
||||
'type': 0,
|
||||
name: cryptoService.encrypt(unencryptedFolder.name, key)
|
||||
};
|
||||
};
|
||||
|
||||
return _service;
|
||||
});
|
||||
114
src/app/services/cryptoService.js
Normal file
114
src/app/services/cryptoService.js
Normal file
@@ -0,0 +1,114 @@
|
||||
angular
|
||||
.module('bit.services')
|
||||
|
||||
.factory('cryptoService', function ($sessionStorage) {
|
||||
var _service = {},
|
||||
_key,
|
||||
_b64Key,
|
||||
_aes;
|
||||
|
||||
sjcl.beware["CBC mode is dangerous because it doesn't protect message integrity."]();
|
||||
|
||||
_service.setKey = function (key) {
|
||||
_key = key;
|
||||
$sessionStorage.key = sjcl.codec.base64.fromBits(key);
|
||||
};
|
||||
|
||||
_service.getKey = function (b64) {
|
||||
if (b64 && b64 === true && _b64Key) {
|
||||
return _b64Key;
|
||||
}
|
||||
else if (!b64 && _key) {
|
||||
return _key;
|
||||
}
|
||||
|
||||
if ($sessionStorage.key) {
|
||||
_key = sjcl.codec.base64.toBits($sessionStorage.key);
|
||||
}
|
||||
|
||||
if (b64 && b64 === true) {
|
||||
_b64Key = sjcl.codec.base64.fromBits(_key);
|
||||
return _b64Key;
|
||||
}
|
||||
|
||||
return _key;
|
||||
};
|
||||
|
||||
_service.clearKey = function () {
|
||||
_key = _b64Key = _aes = null;
|
||||
delete $sessionStorage.key;
|
||||
};
|
||||
|
||||
_service.makeKey = function (password, salt, b64) {
|
||||
var key = sjcl.misc.pbkdf2(password, salt, 5000, 256, null);
|
||||
|
||||
if (b64 && b64 === true) {
|
||||
return sjcl.codec.base64.fromBits(key);
|
||||
}
|
||||
|
||||
return key;
|
||||
};
|
||||
|
||||
_service.hashPassword = function (password, key) {
|
||||
if (!key) {
|
||||
key = _service.getKey();
|
||||
}
|
||||
|
||||
if (!password || !key) {
|
||||
throw 'Invalid parameters.';
|
||||
}
|
||||
|
||||
var hashBits = sjcl.misc.pbkdf2(key, password, 1, 256, null);
|
||||
return sjcl.codec.base64.fromBits(hashBits);
|
||||
};
|
||||
|
||||
_service.getAes = function () {
|
||||
if (!_aes && _service.getKey()) {
|
||||
_aes = new sjcl.cipher.aes(_service.getKey());
|
||||
}
|
||||
|
||||
return _aes;
|
||||
};
|
||||
|
||||
_service.encrypt = function (plaintextValue, key) {
|
||||
if (!_service.getKey() && !key) {
|
||||
throw 'Encryption key unavailable.';
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
key = _service.getKey();
|
||||
}
|
||||
|
||||
var response = {};
|
||||
var params = {
|
||||
mode: "cbc",
|
||||
iv: sjcl.random.randomWords(4, 10)
|
||||
};
|
||||
|
||||
var ctJson = sjcl.encrypt(key, plaintextValue, params, response);
|
||||
|
||||
var ct = ctJson.match(/"ct":"([^"]*)"/)[1];
|
||||
var iv = sjcl.codec.base64.fromBits(response.iv);
|
||||
|
||||
return iv + "|" + ct;
|
||||
};
|
||||
|
||||
_service.decrypt = function (encValue) {
|
||||
if (!_service.getAes()) {
|
||||
throw 'AES encryption unavailable.';
|
||||
}
|
||||
|
||||
var encPieces = encValue.split('|');
|
||||
if (encPieces.length !== 2) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var ivBits = sjcl.codec.base64.toBits(encPieces[0]);
|
||||
var ctBits = sjcl.codec.base64.toBits(encPieces[1]);
|
||||
|
||||
var decBits = sjcl.mode.cbc.decrypt(_service.getAes(), ctBits, ivBits, null);
|
||||
return sjcl.codec.utf8String.fromBits(decBits);
|
||||
};
|
||||
|
||||
return _service;
|
||||
});
|
||||
986
src/app/services/importService.js
Normal file
986
src/app/services/importService.js
Normal file
@@ -0,0 +1,986 @@
|
||||
angular
|
||||
.module('bit.services')
|
||||
|
||||
.factory('importService', function () {
|
||||
var _service = {};
|
||||
|
||||
_service.import = function (source, file, success, error) {
|
||||
if (!file) {
|
||||
error();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (source) {
|
||||
case 'local':
|
||||
importLocal(file, success, error);
|
||||
break;
|
||||
case 'lastpass':
|
||||
importLastPass(file, success, error);
|
||||
break;
|
||||
case 'safeincloudcsv':
|
||||
importSafeInCloudCsv(file, success, error);
|
||||
break;
|
||||
case 'safeincloudxml':
|
||||
importSafeInCloudXml(file, success, error);
|
||||
break;
|
||||
case 'keypassxml':
|
||||
importKeyPassXml(file, success, error);
|
||||
break;
|
||||
case 'padlockcsv':
|
||||
importPadlockCsv(file, success, error);
|
||||
break;
|
||||
case '1password1pif':
|
||||
import1Password1Pif(file, success, error);
|
||||
break;
|
||||
case 'chromecsv':
|
||||
importChromeCsv(file, success, error);
|
||||
break;
|
||||
case 'firefoxpasswordexportercsvxml':
|
||||
importFirefoxPasswordExporterCsvXml(file, success, error);
|
||||
break;
|
||||
case 'upmcsv':
|
||||
importUpmCsv(file, success, error);
|
||||
break;
|
||||
case 'keepercsv':
|
||||
importKeeperCsv(file, success, error);
|
||||
break;
|
||||
case 'passworddragonxml':
|
||||
importPasswordDragonXml(file, success, error);
|
||||
break;
|
||||
default:
|
||||
error();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
function trimUri(uri) {
|
||||
if (uri.length > 1000) {
|
||||
return uri.substring(0, 1000);
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
function parseCsvErrors(results) {
|
||||
if (results.errors && results.errors.length) {
|
||||
for (var i = 0; i < results.errors.length; i++) {
|
||||
console.warn('Error parsing row ' + results.errors[i].row + ': ' + results.errors[i].message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function importLocal(file, success, error) {
|
||||
Papa.parse(file, {
|
||||
header: true,
|
||||
encoding: 'UTF-8',
|
||||
complete: function (results) {
|
||||
parseCsvErrors(results);
|
||||
|
||||
var folders = [],
|
||||
sites = [],
|
||||
folderRelationships = [];
|
||||
|
||||
angular.forEach(results.data, function (value, key) {
|
||||
var folderIndex = folders.length,
|
||||
siteIndex = sites.length,
|
||||
hasFolder = value.folder && value.folder !== '',
|
||||
addFolder = hasFolder;
|
||||
|
||||
if (hasFolder) {
|
||||
for (var i = 0; i < folders.length; i++) {
|
||||
if (folders[i].name === value.folder) {
|
||||
addFolder = false;
|
||||
folderIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sites.push({
|
||||
favorite: value.favorite !== null ? value.favorite : false,
|
||||
uri: value.uri && value.uri !== '' ? trimUri(value.uri) : null,
|
||||
username: value.username && value.username !== '' ? value.username : null,
|
||||
password: value.password && value.password !== '' ? value.password : null,
|
||||
notes: value.notes && value.notes !== '' ? value.notes : null,
|
||||
name: value.name && value.name !== '' ? value.name : '--',
|
||||
});
|
||||
|
||||
if (addFolder) {
|
||||
folders.push({
|
||||
name: value.folder
|
||||
});
|
||||
}
|
||||
|
||||
if (hasFolder) {
|
||||
var relationship = {
|
||||
key: siteIndex,
|
||||
value: folderIndex
|
||||
};
|
||||
folderRelationships.push(relationship);
|
||||
}
|
||||
});
|
||||
|
||||
success(folders, sites, folderRelationships);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function importLastPass(file, success, error) {
|
||||
if (file.type === 'text/html') {
|
||||
var reader = new FileReader();
|
||||
reader.readAsText(file, 'utf-8');
|
||||
reader.onload = function (evt) {
|
||||
var doc = $(evt.target.result);
|
||||
var pre = doc.find('pre');
|
||||
var csv, results;
|
||||
|
||||
if (pre.length === 1) {
|
||||
csv = pre.text().trim();
|
||||
results = Papa.parse(csv, {
|
||||
header: true,
|
||||
encoding: 'UTF-8'
|
||||
});
|
||||
parseData(results.data);
|
||||
}
|
||||
else {
|
||||
var foundPre = false;
|
||||
for (var i = 0; i < doc.length; i++) {
|
||||
if (doc[i].tagName.toLowerCase() === 'pre') {
|
||||
foundPre = true;
|
||||
csv = doc[i].outerText.trim();
|
||||
results = Papa.parse(csv, {
|
||||
header: true,
|
||||
encoding: 'UTF-8'
|
||||
});
|
||||
parseData(results.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundPre) {
|
||||
error();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = function (evt) {
|
||||
error();
|
||||
};
|
||||
}
|
||||
else {
|
||||
Papa.parse(file, {
|
||||
header: true,
|
||||
encoding: 'UTF-8',
|
||||
complete: function (results) {
|
||||
parseCsvErrors(results);
|
||||
parseData(results.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function parseData(data) {
|
||||
var folders = [],
|
||||
sites = [],
|
||||
siteRelationships = [],
|
||||
badDataSites = 0;
|
||||
|
||||
angular.forEach(data, function (value, key) {
|
||||
var folderIndex = folders.length,
|
||||
siteIndex = sites.length,
|
||||
hasFolder = value.grouping && value.grouping !== '' && value.grouping !== '(none)',
|
||||
addFolder = hasFolder;
|
||||
|
||||
if (hasFolder) {
|
||||
for (var i = 0; i < folders.length; i++) {
|
||||
if (folders[i].name === value.grouping) {
|
||||
addFolder = false;
|
||||
folderIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((!value.name || value.name === '') && (!value.password || value.password === '')) {
|
||||
badDataSites++;
|
||||
}
|
||||
|
||||
sites.push({
|
||||
favorite: value.fav === '1',
|
||||
uri: value.url && value.url !== '' ? trimUri(value.url) : null,
|
||||
username: value.username && value.username !== '' ? value.username : null,
|
||||
password: value.password && value.password !== '' ? value.password : null,
|
||||
notes: value.extra && value.extra !== '' ? value.extra : null,
|
||||
name: value.name && value.name !== '' ? value.name : '--',
|
||||
});
|
||||
|
||||
if (addFolder) {
|
||||
folders.push({
|
||||
name: value.grouping
|
||||
});
|
||||
}
|
||||
|
||||
if (hasFolder) {
|
||||
var relationship = {
|
||||
key: siteIndex,
|
||||
value: folderIndex
|
||||
};
|
||||
siteRelationships.push(relationship);
|
||||
}
|
||||
});
|
||||
|
||||
if (badDataSites && badDataSites > (data.length / 2)) {
|
||||
error('CSV data is not formatted correctly from LastPass. Please check your import file and try again.');
|
||||
}
|
||||
else {
|
||||
success(folders, sites, siteRelationships);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function importSafeInCloudCsv(file, success, error) {
|
||||
Papa.parse(file, {
|
||||
header: true,
|
||||
encoding: 'UTF-8',
|
||||
complete: function (results) {
|
||||
parseCsvErrors(results);
|
||||
|
||||
var folders = [],
|
||||
sites = [],
|
||||
siteRelationships = [];
|
||||
|
||||
angular.forEach(results.data, function (value, key) {
|
||||
sites.push({
|
||||
favorite: false,
|
||||
uri: value.URL && value.URL !== '' ? trimUri(value.URL) : null,
|
||||
username: value.Login && value.Login !== '' ? value.Login : null,
|
||||
password: value.Password && value.Password !== '' ? value.Password : null,
|
||||
notes: value.Notes && value.Notes !== '' ? value.Notes : null,
|
||||
name: value.Title && value.Title !== '' ? value.Title : '--',
|
||||
});
|
||||
});
|
||||
|
||||
success(folders, sites, siteRelationships);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function importSafeInCloudXml(file, success, error) {
|
||||
var folders = [],
|
||||
sites = [],
|
||||
siteRelationships = [],
|
||||
foldersIndex = [];
|
||||
|
||||
var i = 0,
|
||||
j = 0;
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.readAsText(file, 'utf-8');
|
||||
reader.onload = function (evt) {
|
||||
var xmlDoc = $.parseXML(evt.target.result),
|
||||
xml = $(xmlDoc);
|
||||
|
||||
var db = xml.find('database');
|
||||
if (db.length) {
|
||||
var labels = db.find('> label');
|
||||
if (labels.length) {
|
||||
for (i = 0; i < labels.length; i++) {
|
||||
var label = $(labels[i]);
|
||||
foldersIndex[label.attr('id')] = folders.length;
|
||||
folders.push({
|
||||
name: label.attr('name')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var cards = db.find('> card');
|
||||
if (cards.length) {
|
||||
for (i = 0; i < cards.length; i++) {
|
||||
var card = $(cards[i]);
|
||||
if (card.attr('template') === 'true') {
|
||||
continue;
|
||||
}
|
||||
|
||||
var site = {
|
||||
favorite: false,
|
||||
uri: null,
|
||||
username: null,
|
||||
password: null,
|
||||
notes: null,
|
||||
name: card.attr('title'),
|
||||
};
|
||||
|
||||
var fields = card.find('> field');
|
||||
for (j = 0; j < fields.length; j++) {
|
||||
var field = $(fields[j]);
|
||||
|
||||
var text = field.text();
|
||||
var type = field.attr('type');
|
||||
|
||||
if (text && text !== '') {
|
||||
if (type === 'login') {
|
||||
site.username = text;
|
||||
}
|
||||
else if (type === 'password') {
|
||||
site.password = text;
|
||||
}
|
||||
else if (type === 'notes') {
|
||||
site.notes = text;
|
||||
}
|
||||
else if (type === 'website') {
|
||||
site.uri = trimUri(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sites.push(site);
|
||||
|
||||
labels = card.find('> label_id');
|
||||
if (labels.length) {
|
||||
var labelId = $(labels[0]).text();
|
||||
var folderIndex = foldersIndex[labelId];
|
||||
if (labelId !== null && labelId !== '' && folderIndex !== null) {
|
||||
siteRelationships.push({
|
||||
key: sites.length - 1,
|
||||
value: folderIndex
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
success(folders, sites, siteRelationships);
|
||||
}
|
||||
else {
|
||||
error();
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = function (evt) {
|
||||
error();
|
||||
};
|
||||
}
|
||||
|
||||
function importPadlockCsv(file, success, error) {
|
||||
Papa.parse(file, {
|
||||
encoding: 'UTF-8',
|
||||
complete: function (results) {
|
||||
parseCsvErrors(results);
|
||||
|
||||
var folders = [],
|
||||
sites = [],
|
||||
folderRelationships = [];
|
||||
|
||||
var customFieldHeaders = [];
|
||||
|
||||
// CSV index ref: 0 = name, 1 = category, 2 = username, 3 = password, 4+ = custom fields
|
||||
|
||||
var i = 0,
|
||||
j = 0;
|
||||
|
||||
for (i = 0; i < results.data.length; i++) {
|
||||
var value = results.data[i];
|
||||
if (i === 0) {
|
||||
// header row
|
||||
for (j = 4; j < value.length; j++) {
|
||||
customFieldHeaders.push(value[j]);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var folderIndex = folders.length,
|
||||
siteIndex = sites.length,
|
||||
hasFolder = value[1] && value[1] !== '',
|
||||
addFolder = hasFolder;
|
||||
|
||||
if (hasFolder) {
|
||||
for (j = 0; j < folders.length; j++) {
|
||||
if (folders[j].name === value[1]) {
|
||||
addFolder = false;
|
||||
folderIndex = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var site = {
|
||||
favorite: false,
|
||||
uri: null,
|
||||
username: value[2] && value[2] !== '' ? value[2] : null,
|
||||
password: value[3] && value[3] !== '' ? value[3] : null,
|
||||
notes: null,
|
||||
name: value[0] && value[0] !== '' ? value[0] : '--',
|
||||
};
|
||||
|
||||
if (customFieldHeaders.length) {
|
||||
for (j = 4; j < value.length; j++) {
|
||||
var cf = value[j];
|
||||
if (!cf || cf === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
var cfHeader = customFieldHeaders[j - 4];
|
||||
if (cfHeader.toLowerCase() === 'url' || cfHeader.toLowerCase() === 'uri') {
|
||||
site.uri = trimUri(cf);
|
||||
}
|
||||
else {
|
||||
if (site.notes === null) {
|
||||
site.notes = '';
|
||||
}
|
||||
|
||||
site.notes += cfHeader + ': ' + cf + '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sites.push(site);
|
||||
|
||||
if (addFolder) {
|
||||
folders.push({
|
||||
name: value[1]
|
||||
});
|
||||
}
|
||||
|
||||
if (hasFolder) {
|
||||
folderRelationships.push({
|
||||
key: siteIndex,
|
||||
value: folderIndex
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
success(folders, sites, folderRelationships);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function importKeyPassXml(file, success, error) {
|
||||
var folders = [],
|
||||
sites = [],
|
||||
siteRelationships = [];
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.readAsText(file, 'utf-8');
|
||||
reader.onload = function (evt) {
|
||||
var xmlDoc = $.parseXML(evt.target.result),
|
||||
xml = $(xmlDoc);
|
||||
|
||||
var root = xml.find('Root');
|
||||
if (root.length) {
|
||||
var group = root.find('> Group');
|
||||
if (group.length) {
|
||||
traverse($(group[0]), true, '');
|
||||
success(folders, sites, siteRelationships);
|
||||
}
|
||||
}
|
||||
else {
|
||||
error();
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = function (evt) {
|
||||
error();
|
||||
};
|
||||
|
||||
function traverse(node, isRootNode, groupNamePrefix) {
|
||||
var nodeEntries = [];
|
||||
var folderIndex = folders.length;
|
||||
var groupName = groupNamePrefix;
|
||||
|
||||
if (!isRootNode) {
|
||||
if (groupName !== '') {
|
||||
groupName += ' > ';
|
||||
}
|
||||
groupName += node.find('> Name').text();
|
||||
folders.push({
|
||||
name: groupName
|
||||
});
|
||||
}
|
||||
|
||||
var entries = node.find('> Entry');
|
||||
if (entries.length) {
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
var entry = $(entries[i]);
|
||||
var siteIndex = sites.length;
|
||||
var site = {
|
||||
favorite: false,
|
||||
uri: null,
|
||||
username: null,
|
||||
password: null,
|
||||
notes: null,
|
||||
name: null
|
||||
};
|
||||
|
||||
var entryStrings = entry.find('> String');
|
||||
for (var j = 0; j < entryStrings.length; j++) {
|
||||
var entryString = $(entryStrings[j]);
|
||||
|
||||
var key = entryString.find('> Key').text();
|
||||
var value = entryString.find('> Value').text();
|
||||
if (value === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case 'URL':
|
||||
site.uri = trimUri(value);
|
||||
break;
|
||||
case 'UserName':
|
||||
site.username = value;
|
||||
break;
|
||||
case 'Password':
|
||||
site.password = value;
|
||||
break;
|
||||
case 'Title':
|
||||
site.name = value;
|
||||
break;
|
||||
case 'Notes':
|
||||
site.notes = site.notes === null ? value + '\n' : site.notes + value + '\n';
|
||||
break;
|
||||
default:
|
||||
// other custom fields
|
||||
site.notes = site.notes === null ? key + ': ' + value + '\n'
|
||||
: site.notes + key + ': ' + value + '\n';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (site.name === null) {
|
||||
site.name = '--';
|
||||
}
|
||||
|
||||
sites.push(site);
|
||||
|
||||
if (!isRootNode) {
|
||||
siteRelationships.push({
|
||||
key: siteIndex,
|
||||
value: folderIndex
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var groups = node.find('> Group');
|
||||
if (groups.length) {
|
||||
for (var k = 0; k < groups.length; k++) {
|
||||
traverse($(groups[k]), false, groupName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function import1Password1Pif(file, success, error) {
|
||||
var folders = [],
|
||||
sites = [],
|
||||
siteRelationships = [];
|
||||
|
||||
var i = 0,
|
||||
j = 0;
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.readAsText(file, 'utf-8');
|
||||
reader.onload = function (evt) {
|
||||
var fileContent = evt.target.result;
|
||||
var fileLines = fileContent.split(/(?:\r\n|\r|\n)/);
|
||||
|
||||
for (i = 0; i < fileLines.length; i++) {
|
||||
var line = fileLines[i];
|
||||
if (!line.length || line[0] !== '{') {
|
||||
continue;
|
||||
}
|
||||
|
||||
var item = JSON.parse(line);
|
||||
if (item.typeName !== 'webforms.WebForm') {
|
||||
continue;
|
||||
}
|
||||
|
||||
var site = {
|
||||
favorite: item.openContents && item.openContents.faveIndex ? true : false,
|
||||
uri: item.location && item.location !== '' ? trimUri(item.location) : null,
|
||||
username: null,
|
||||
password: null,
|
||||
notes: null,
|
||||
name: item.title && item.title !== '' ? item.title : '--',
|
||||
};
|
||||
|
||||
if (item.secureContents) {
|
||||
if (item.secureContents.notesPlain && item.secureContents.notesPlain !== '') {
|
||||
site.notes = item.secureContents.notesPlain;
|
||||
}
|
||||
|
||||
if (item.secureContents.fields) {
|
||||
for (j = 0; j < item.secureContents.fields.length; j++) {
|
||||
var field = item.secureContents.fields[j];
|
||||
if (field.designation === 'username') {
|
||||
site.username = field.value;
|
||||
}
|
||||
else if (field.designation === 'password') {
|
||||
site.password = field.value;
|
||||
}
|
||||
else {
|
||||
if (site.notes === null) {
|
||||
site.notes = '';
|
||||
}
|
||||
else {
|
||||
site.notes += '\n';
|
||||
}
|
||||
|
||||
site.notes += (field.name + ': ' + field.value + '\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sites.push(site);
|
||||
}
|
||||
|
||||
success(folders, sites, siteRelationships);
|
||||
};
|
||||
|
||||
reader.onerror = function (evt) {
|
||||
error();
|
||||
};
|
||||
}
|
||||
|
||||
function importChromeCsv(file, success, error) {
|
||||
Papa.parse(file, {
|
||||
header: true,
|
||||
encoding: 'UTF-8',
|
||||
complete: function (results) {
|
||||
parseCsvErrors(results);
|
||||
|
||||
var folders = [],
|
||||
sites = [],
|
||||
siteRelationships = [];
|
||||
|
||||
angular.forEach(results.data, function (value, key) {
|
||||
sites.push({
|
||||
favorite: false,
|
||||
uri: value.url && value.url !== '' ? trimUri(value.url) : null,
|
||||
username: value.username && value.username !== '' ? value.username : null,
|
||||
password: value.password && value.password !== '' ? value.password : null,
|
||||
notes: null,
|
||||
name: value.name && value.name !== '' ? value.name : '--',
|
||||
});
|
||||
});
|
||||
|
||||
success(folders, sites, siteRelationships);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function importFirefoxPasswordExporterCsvXml(file, success, error) {
|
||||
var folders = [],
|
||||
sites = [],
|
||||
siteRelationships = [];
|
||||
|
||||
function getNameFromHost(host) {
|
||||
var name = '--';
|
||||
try {
|
||||
if (host && host !== '') {
|
||||
var parser = document.createElement('a');
|
||||
parser.href = host;
|
||||
if (parser.hostname) {
|
||||
name = parser.hostname;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
if (file.type === 'text/xml') {
|
||||
var reader = new FileReader();
|
||||
reader.readAsText(file, 'utf-8');
|
||||
reader.onload = function (evt) {
|
||||
var xmlDoc = $.parseXML(evt.target.result),
|
||||
xml = $(xmlDoc);
|
||||
|
||||
var entries = xml.find('entry');
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
var entry = $(entries[i]);
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var host = entry.attr('host'),
|
||||
user = entry.attr('user'),
|
||||
password = entry.attr('password');
|
||||
|
||||
sites.push({
|
||||
favorite: false,
|
||||
uri: host && host !== '' ? trimUri(host) : null,
|
||||
username: user && user !== '' ? user : null,
|
||||
password: password && password !== '' ? password : null,
|
||||
notes: null,
|
||||
name: getNameFromHost(host),
|
||||
});
|
||||
}
|
||||
|
||||
success(folders, sites, siteRelationships);
|
||||
};
|
||||
|
||||
reader.onerror = function (evt) {
|
||||
error();
|
||||
};
|
||||
}
|
||||
else {
|
||||
// currently bugged due to the comment
|
||||
// ref: https://github.com/mholt/PapaParse/issues/351
|
||||
|
||||
error('Only .xml exports are supported.');
|
||||
return;
|
||||
|
||||
//Papa.parse(file, {
|
||||
// comments: '#',
|
||||
// header: true,
|
||||
// encoding: 'UTF-8',
|
||||
// complete: function (results) {
|
||||
// parseCsvErrors(results);
|
||||
|
||||
// angular.forEach(results.data, function (value, key) {
|
||||
// sites.push({
|
||||
// favorite: false,
|
||||
// uri: value.hostname && value.hostname !== '' ? trimUri(value.hostname) : null,
|
||||
// username: value.username && value.username !== '' ? value.username : null,
|
||||
// password: value.password && value.password !== '' ? value.password : null,
|
||||
// notes: null,
|
||||
// name: getNameFromHost(value.hostname),
|
||||
// });
|
||||
// });
|
||||
|
||||
// success(folders, sites, siteRelationships);
|
||||
// }
|
||||
//});
|
||||
}
|
||||
}
|
||||
|
||||
function importUpmCsv(file, success, error) {
|
||||
Papa.parse(file, {
|
||||
encoding: 'UTF-8',
|
||||
complete: function (results) {
|
||||
parseCsvErrors(results);
|
||||
|
||||
var folders = [],
|
||||
sites = [],
|
||||
siteRelationships = [];
|
||||
|
||||
angular.forEach(results.data, function (value, key) {
|
||||
if (value.length === 5) {
|
||||
sites.push({
|
||||
favorite: false,
|
||||
uri: value[3] && value[3] !== '' ? trimUri(value[3]) : null,
|
||||
username: value[1] && value[1] !== '' ? value[1] : null,
|
||||
password: value[2] && value[2] !== '' ? value[2] : null,
|
||||
notes: value[4] && value[4] !== '' ? value[4] : null,
|
||||
name: value[0] && value[0] !== '' ? value[0] : '--',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
success(folders, sites, siteRelationships);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function importKeeperCsv(file, success, error) {
|
||||
Papa.parse(file, {
|
||||
encoding: 'UTF-8',
|
||||
complete: function (results) {
|
||||
parseCsvErrors(results);
|
||||
|
||||
var folders = [],
|
||||
sites = [],
|
||||
folderRelationships = [];
|
||||
|
||||
angular.forEach(results.data, function (value, key) {
|
||||
if (value.length >= 6) {
|
||||
var folderIndex = folders.length,
|
||||
siteIndex = sites.length,
|
||||
hasFolder = value[0] && value[0] !== '',
|
||||
addFolder = hasFolder,
|
||||
i = 0;
|
||||
|
||||
if (hasFolder) {
|
||||
for (i = 0; i < folders.length; i++) {
|
||||
if (folders[i].name === value[0]) {
|
||||
addFolder = false;
|
||||
folderIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var site = {
|
||||
favorite: false,
|
||||
uri: value[4] && value[4] !== '' ? trimUri(value[4]) : null,
|
||||
username: value[2] && value[2] !== '' ? value[2] : null,
|
||||
password: value[3] && value[3] !== '' ? value[3] : null,
|
||||
notes: value[5] && value[5] !== '' ? value[5] : null,
|
||||
name: value[1] && value[1] !== '' ? value[1] : '--',
|
||||
};
|
||||
|
||||
if (value.length > 6) {
|
||||
// we have some custom fields. add them to notes.
|
||||
|
||||
if (site.notes === null) {
|
||||
site.notes = '';
|
||||
}
|
||||
else {
|
||||
site.notes += '\n';
|
||||
}
|
||||
|
||||
for (i = 6; i < value.length; i = i + 2) {
|
||||
var cfName = value[i];
|
||||
var cfValue = value[i + 1];
|
||||
site.notes += (cfName + ': ' + cfValue + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
sites.push(site);
|
||||
|
||||
if (addFolder) {
|
||||
folders.push({
|
||||
name: value[0]
|
||||
});
|
||||
}
|
||||
|
||||
if (hasFolder) {
|
||||
var relationship = {
|
||||
key: siteIndex,
|
||||
value: folderIndex
|
||||
};
|
||||
folderRelationships.push(relationship);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
success(folders, sites, folderRelationships);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function importPasswordDragonXml(file, success, error) {
|
||||
var folders = [],
|
||||
sites = [],
|
||||
folderRelationships = [],
|
||||
foldersIndex = [],
|
||||
j = 0;
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.readAsText(file, 'utf-8');
|
||||
reader.onload = function (evt) {
|
||||
var xmlDoc = $.parseXML(evt.target.result),
|
||||
xml = $(xmlDoc);
|
||||
|
||||
var pwManager = xml.find('PasswordManager');
|
||||
if (pwManager.length) {
|
||||
var records = pwManager.find('> record');
|
||||
if (records.length) {
|
||||
for (var i = 0; i < records.length; i++) {
|
||||
var record = $(records[i]);
|
||||
|
||||
var accountNameNode = record.find('> Account-Name'),
|
||||
accountName = accountNameNode.length ? $(accountNameNode) : null,
|
||||
userIdNode = record.find('> User-Id'),
|
||||
userId = userIdNode.length ? $(userIdNode) : null,
|
||||
passwordNode = record.find('> Password'),
|
||||
password = passwordNode.length ? $(passwordNode) : null,
|
||||
urlNode = record.find('> URL'),
|
||||
url = urlNode.length ? $(urlNode) : null,
|
||||
notesNode = record.find('> Notes'),
|
||||
notes = notesNode.length ? $(notesNode) : null,
|
||||
categoryNode = record.find('> Category'),
|
||||
category = categoryNode.length ? $(categoryNode) : null,
|
||||
categoryText = category ? category.text() : null;
|
||||
|
||||
var folderIndex = folders.length,
|
||||
siteIndex = sites.length,
|
||||
hasFolder = categoryText && categoryText !== '' && categoryText !== 'Unfiled',
|
||||
addFolder = hasFolder;
|
||||
|
||||
if (hasFolder) {
|
||||
for (j = 0; j < folders.length; j++) {
|
||||
if (folders[j].name === categoryText) {
|
||||
addFolder = false;
|
||||
folderIndex = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var site = {
|
||||
favorite: false,
|
||||
uri: url && url.text() !== '' ? trimUri(url.text()) : null,
|
||||
username: userId && userId.text() !== '' ? userId.text() : null,
|
||||
password: password && password.text() !== '' ? password.text() : null,
|
||||
notes: notes && notes.text() !== '' ? notes.text() : null,
|
||||
name: accountName && accountName.text() !== '' ? accountName.text() : '--',
|
||||
};
|
||||
|
||||
var attributesSelector = '';
|
||||
for (j = 1; j <= 10; j++) {
|
||||
attributesSelector += '> Attribute-' + j;
|
||||
if (j < 10) {
|
||||
attributesSelector += ', ';
|
||||
}
|
||||
}
|
||||
|
||||
var attributes = record.find(attributesSelector);
|
||||
if (attributes.length) {
|
||||
// we have some attributes. add them to notes.
|
||||
for (j = 0; j < attributes.length; j++) {
|
||||
var attr = $(attributes[j]),
|
||||
attrName = attr.prop('tagName'),
|
||||
attrValue = attr.text();
|
||||
|
||||
if (!attrValue || attrValue === '' || attrValue === 'null') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (site.notes === null) {
|
||||
site.notes = '';
|
||||
}
|
||||
else {
|
||||
site.notes += '\n';
|
||||
}
|
||||
|
||||
site.notes += (attrName + ': ' + attrValue);
|
||||
}
|
||||
}
|
||||
|
||||
sites.push(site);
|
||||
|
||||
if (addFolder) {
|
||||
folders.push({
|
||||
name: categoryText
|
||||
});
|
||||
}
|
||||
|
||||
if (hasFolder) {
|
||||
var relationship = {
|
||||
key: siteIndex,
|
||||
value: folderIndex
|
||||
};
|
||||
folderRelationships.push(relationship);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
success(folders, sites, folderRelationships);
|
||||
}
|
||||
else {
|
||||
error();
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = function (evt) {
|
||||
error();
|
||||
};
|
||||
}
|
||||
|
||||
return _service;
|
||||
});
|
||||
136
src/app/services/passwordService.js
Normal file
136
src/app/services/passwordService.js
Normal file
@@ -0,0 +1,136 @@
|
||||
angular
|
||||
.module('bit.services')
|
||||
|
||||
.factory('passwordService', function () {
|
||||
var _service = {};
|
||||
|
||||
_service.generatePassword = function (options) {
|
||||
var defaults = {
|
||||
length: 10,
|
||||
ambiguous: false,
|
||||
number: true,
|
||||
minNumber: 1,
|
||||
uppercase: true,
|
||||
minUppercase: 1,
|
||||
lowercase: true,
|
||||
minLowercase: 1,
|
||||
special: false,
|
||||
minSpecial: 1
|
||||
};
|
||||
|
||||
// overload defaults with given options
|
||||
var o = angular.extend({}, defaults, options);
|
||||
|
||||
// sanitize
|
||||
if (o.uppercase && o.minUppercase < 0) o.minUppercase = 1;
|
||||
if (o.lowercase && o.minLowercase < 0) o.minLowercase = 1;
|
||||
if (o.number && o.minNumber < 0) o.minNumber = 1;
|
||||
if (o.special && o.minSpecial < 0) o.minSpecial = 1;
|
||||
|
||||
if (!o.length || o.length < 1) o.length = 10;
|
||||
var minLength = o.minUppercase + o.minLowercase + o.minNumber + o.minSpecial;
|
||||
if (o.length < minLength) o.length = minLength;
|
||||
|
||||
var positions = [];
|
||||
if (o.lowercase && o.minLowercase > 0) {
|
||||
for (var i = 0; i < o.minLowercase; i++) {
|
||||
positions.push('l');
|
||||
}
|
||||
}
|
||||
if (o.uppercase && o.minUppercase > 0) {
|
||||
for (var j = 0; j < o.minUppercase; j++) {
|
||||
positions.push('u');
|
||||
}
|
||||
}
|
||||
if (o.number && o.minNumber > 0) {
|
||||
for (var k = 0; k < o.minNumber; k++) {
|
||||
positions.push('n');
|
||||
}
|
||||
}
|
||||
if (o.special && o.minSpecial > 0) {
|
||||
for (var l = 0; l < o.minSpecial; l++) {
|
||||
positions.push('s');
|
||||
}
|
||||
}
|
||||
while (positions.length < o.length) {
|
||||
positions.push('a');
|
||||
}
|
||||
|
||||
// shuffle
|
||||
positions.sort(function () {
|
||||
return randomInt(0, 1) * 2 - 1;
|
||||
});
|
||||
|
||||
// build out the char sets
|
||||
var allCharSet = '';
|
||||
|
||||
var lowercaseCharSet = 'abcdefghijkmnopqrstuvwxyz';
|
||||
if (o.ambiguous) lowercaseCharSet += 'l';
|
||||
if (o.lowercase) allCharSet += lowercaseCharSet;
|
||||
|
||||
var uppercaseCharSet = 'ABCDEFGHIJKLMNPQRSTUVWXYZ';
|
||||
if (o.ambiguous) uppercaseCharSet += 'O';
|
||||
if (o.uppercase) allCharSet += uppercaseCharSet;
|
||||
|
||||
var numberCharSet = '23456789';
|
||||
if (o.ambiguous) numberCharSet += '01';
|
||||
if (o.number) allCharSet += numberCharSet;
|
||||
|
||||
var specialCharSet = '!@#$%^&*';
|
||||
if (o.special) allCharSet += specialCharSet;
|
||||
|
||||
var password = '';
|
||||
for (var m = 0; m < o.length; m++) {
|
||||
var positionChars;
|
||||
switch (positions[m]) {
|
||||
case 'l': positionChars = lowercaseCharSet; break;
|
||||
case 'u': positionChars = uppercaseCharSet; break;
|
||||
case 'n': positionChars = numberCharSet; break;
|
||||
case 's': positionChars = specialCharSet; break;
|
||||
case 'a': positionChars = allCharSet; break;
|
||||
}
|
||||
|
||||
var randomCharIndex = randomInt(0, positionChars.length - 1);
|
||||
password += positionChars.charAt(randomCharIndex);
|
||||
}
|
||||
|
||||
return password;
|
||||
};
|
||||
|
||||
// EFForg/OpenWireless
|
||||
// ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js
|
||||
function randomInt(min, max) {
|
||||
var rval = 0;
|
||||
var range = max - min;
|
||||
|
||||
var bits_needed = Math.ceil(Math.log2(range));
|
||||
if (bits_needed > 53) {
|
||||
throw new Exception("We cannot generate numbers larger than 53 bits.");
|
||||
}
|
||||
var bytes_needed = Math.ceil(bits_needed / 8);
|
||||
var mask = Math.pow(2, bits_needed) - 1;
|
||||
// 7776 -> (2^13 = 8192) -1 == 8191 or 0x00001111 11111111
|
||||
|
||||
// Create byte array and fill with N random numbers
|
||||
var byteArray = new Uint8Array(bytes_needed);
|
||||
window.crypto.getRandomValues(byteArray);
|
||||
|
||||
var p = (bytes_needed - 1) * 8;
|
||||
for (var i = 0; i < bytes_needed; i++) {
|
||||
rval += byteArray[i] * Math.pow(2, p);
|
||||
p -= 8;
|
||||
}
|
||||
|
||||
// Use & to apply the mask and reduce the number of recursive lookups
|
||||
rval = rval & mask;
|
||||
|
||||
if (rval >= range) {
|
||||
// Integer out of acceptable range
|
||||
return randomInt(min, max);
|
||||
}
|
||||
// Return an integer that falls within the range
|
||||
return min + rval;
|
||||
}
|
||||
|
||||
return _service;
|
||||
});
|
||||
2
src/app/services/servicesModule.js
Normal file
2
src/app/services/servicesModule.js
Normal file
@@ -0,0 +1,2 @@
|
||||
angular
|
||||
.module('bit.services', ['ngResource', 'ngStorage', 'angular-jwt']);
|
||||
27
src/app/services/tokenService.js
Normal file
27
src/app/services/tokenService.js
Normal file
@@ -0,0 +1,27 @@
|
||||
angular
|
||||
.module('bit.services')
|
||||
|
||||
.factory('tokenService', function ($sessionStorage) {
|
||||
var _service = {},
|
||||
_token;
|
||||
|
||||
_service.setToken = function (token) {
|
||||
$sessionStorage.authBearer = token;
|
||||
_token = token;
|
||||
};
|
||||
|
||||
_service.getToken = function () {
|
||||
if (!_token) {
|
||||
_token = $sessionStorage.authBearer;
|
||||
}
|
||||
|
||||
return _token;
|
||||
};
|
||||
|
||||
_service.clearToken = function () {
|
||||
_token = null;
|
||||
delete $sessionStorage.authBearer;
|
||||
};
|
||||
|
||||
return _service;
|
||||
});
|
||||
62
src/app/services/validationService.js
Normal file
62
src/app/services/validationService.js
Normal file
@@ -0,0 +1,62 @@
|
||||
angular
|
||||
.module('bit.services')
|
||||
|
||||
.factory('validationService', function () {
|
||||
var _service = {};
|
||||
|
||||
_service.addErrors = function (form, reason) {
|
||||
var data = reason.data;
|
||||
var defaultErrorMessage = 'An unexpected error has occured.';
|
||||
form.$errors = [];
|
||||
|
||||
if (!data || !angular.isObject(data)) {
|
||||
form.$errors.push(defaultErrorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.ValidationErrors) {
|
||||
if (data.Message) {
|
||||
form.$errors.push(data.Message);
|
||||
}
|
||||
else {
|
||||
form.$errors.push(defaultErrorMessage);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (var key in data.ValidationErrors) {
|
||||
if (!data.ValidationErrors.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var i = 0; i < data.ValidationErrors[key].length; i++) {
|
||||
_service.addError(form, key, data.ValidationErrors[key][i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_service.addError = function (form, key, errorMessage, clearExistingErrors) {
|
||||
if (clearExistingErrors || !form.$errors) {
|
||||
form.$errors = [];
|
||||
}
|
||||
|
||||
var pushError = true;
|
||||
for (var i = 0; i < form.$errors.length; i++) {
|
||||
if (form.$errors[i] === errorMessage) {
|
||||
pushError = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (pushError) {
|
||||
form.$errors.push(errorMessage);
|
||||
}
|
||||
|
||||
if (key && key !== '' && form[key] && form[key].$registerApiError) {
|
||||
form[key].$registerApiError();
|
||||
}
|
||||
};
|
||||
|
||||
return _service;
|
||||
});
|
||||
2
src/app/settings.js
Normal file
2
src/app/settings.js
Normal file
@@ -0,0 +1,2 @@
|
||||
angular.module("bit")
|
||||
.constant("appSettings", {"rememberedEmailCookieName":"bit.rememberedEmail","version":"1.4.0","environment":"Development","apiUri":"http://localhost:4000"});
|
||||
69
src/app/settings/settingsChangeEmailController.js
Normal file
69
src/app/settings/settingsChangeEmailController.js
Normal file
@@ -0,0 +1,69 @@
|
||||
angular
|
||||
.module('bit.settings')
|
||||
|
||||
.controller('settingsChangeEmailController', function ($scope, $state, apiService, $uibModalInstance, cryptoService, cipherService, authService, $q, toastr, $analytics) {
|
||||
$analytics.eventTrack('settingsChangeEmailController', { category: 'Modal' });
|
||||
var _masterPasswordHash,
|
||||
_newMasterPasswordHash,
|
||||
_newKey;
|
||||
|
||||
$scope.token = function (model) {
|
||||
_masterPasswordHash = cryptoService.hashPassword(model.masterPassword);
|
||||
var newEmail = model.newEmail.toLowerCase();
|
||||
|
||||
var request = {
|
||||
newEmail: newEmail,
|
||||
masterPasswordHash: _masterPasswordHash
|
||||
};
|
||||
|
||||
$scope.tokenPromise = apiService.accounts.emailToken(request, function () {
|
||||
_newKey = cryptoService.makeKey(model.masterPassword, newEmail);
|
||||
_newMasterPasswordHash = cryptoService.hashPassword(model.masterPassword, _newKey);
|
||||
|
||||
$scope.tokenSent = true;
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
$scope.confirm = function (model) {
|
||||
$scope.processing = true;
|
||||
|
||||
var reencryptedSites = [];
|
||||
var sitesPromise = apiService.sites.list({ dirty: false }, function (encryptedSites) {
|
||||
var unencryptedSites = cipherService.decryptSites(encryptedSites.Data);
|
||||
reencryptedSites = cipherService.encryptSites(unencryptedSites, _newKey);
|
||||
}).$promise;
|
||||
|
||||
var reencryptedFolders = [];
|
||||
var foldersPromise = apiService.folders.list({ dirty: false }, function (encryptedFolders) {
|
||||
var unencryptedFolders = cipherService.decryptFolders(encryptedFolders.Data);
|
||||
reencryptedFolders = cipherService.encryptFolders(unencryptedFolders, _newKey);
|
||||
}).$promise;
|
||||
|
||||
$q.all([sitesPromise, foldersPromise]).then(function () {
|
||||
var request = {
|
||||
token: model.token,
|
||||
newEmail: model.newEmail.toLowerCase(),
|
||||
masterPasswordHash: _masterPasswordHash,
|
||||
newMasterPasswordHash: _newMasterPasswordHash,
|
||||
ciphers: reencryptedSites.concat(reencryptedFolders)
|
||||
};
|
||||
|
||||
$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 () {
|
||||
// TODO: recovery mode
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
toastr.error('Something went wrong.', 'Oh No!');
|
||||
}).$promise;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
});
|
||||
67
src/app/settings/settingsChangePasswordController.js
Normal file
67
src/app/settings/settingsChangePasswordController.js
Normal file
@@ -0,0 +1,67 @@
|
||||
angular
|
||||
.module('bit.settings')
|
||||
|
||||
.controller('settingsChangePasswordController', function ($scope, $state, apiService, $uibModalInstance,
|
||||
cryptoService, authService, cipherService, validationService, $q, toastr, $analytics) {
|
||||
$analytics.eventTrack('settingsChangePasswordController', { category: 'Modal' });
|
||||
$scope.save = function (model, form) {
|
||||
var error = false;
|
||||
|
||||
if ($scope.model.newMasterPassword.length < 8) {
|
||||
validationService.addError(form, 'NewMasterPasswordHash',
|
||||
'Master password must be at least 8 characters long.', true);
|
||||
error = true;
|
||||
}
|
||||
if ($scope.model.newMasterPassword !== $scope.model.confirmNewMasterPassword) {
|
||||
validationService.addError(form, 'ConfirmNewMasterPasswordHash',
|
||||
'New master password confirmation does not match.', true);
|
||||
error = true;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.processing = true;
|
||||
|
||||
var profile = authService.getUserProfile();
|
||||
var newKey = cryptoService.makeKey(model.newMasterPassword, profile.email.toLowerCase());
|
||||
|
||||
var reencryptedSites = [];
|
||||
var sitesPromise = apiService.sites.list({ dirty: false }, function (encryptedSites) {
|
||||
var unencryptedSites = cipherService.decryptSites(encryptedSites.Data);
|
||||
reencryptedSites = cipherService.encryptSites(unencryptedSites, newKey);
|
||||
}).$promise;
|
||||
|
||||
var reencryptedFolders = [];
|
||||
var foldersPromise = apiService.folders.list({ dirty: false }, function (encryptedFolders) {
|
||||
var unencryptedFolders = cipherService.decryptFolders(encryptedFolders.Data);
|
||||
reencryptedFolders = cipherService.encryptFolders(unencryptedFolders, newKey);
|
||||
}).$promise;
|
||||
|
||||
$q.all([sitesPromise, foldersPromise]).then(function () {
|
||||
var request = {
|
||||
masterPasswordHash: cryptoService.hashPassword(model.masterPassword),
|
||||
newMasterPasswordHash: cryptoService.hashPassword(model.newMasterPassword, newKey),
|
||||
ciphers: reencryptedSites.concat(reencryptedFolders)
|
||||
};
|
||||
|
||||
$scope.savePromise = apiService.accounts.putPassword(request, function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
authService.logOut();
|
||||
$analytics.eventTrack('Changed Password');
|
||||
$state.go('frontend.login.info').then(function () {
|
||||
toastr.success('Please log back in.', 'Master Password Changed');
|
||||
});
|
||||
}, function () {
|
||||
// TODO: recovery mode
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
toastr.error('Something went wrong.', 'Oh No!');
|
||||
}).$promise;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
});
|
||||
85
src/app/settings/settingsController.js
Normal file
85
src/app/settings/settingsController.js
Normal file
@@ -0,0 +1,85 @@
|
||||
angular
|
||||
.module('bit.settings')
|
||||
|
||||
.controller('settingsController', function ($scope, $uibModal, apiService, toastr, authService) {
|
||||
$scope.model = {};
|
||||
|
||||
apiService.accounts.getProfile({}, function (user) {
|
||||
$scope.model = {
|
||||
name: user.Name,
|
||||
email: user.Email,
|
||||
masterPasswordHint: user.MasterPasswordHint,
|
||||
culture: user.Culture,
|
||||
twoFactorEnabled: user.TwoFactorEnabled
|
||||
};
|
||||
});
|
||||
|
||||
$scope.save = function (model) {
|
||||
$scope.savePromise = apiService.accounts.putProfile({}, model, function (profile) {
|
||||
authService.setUserProfile(profile);
|
||||
toastr.success('Account has been updated.', 'Success!');
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
$scope.changePassword = function () {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/settings/views/settingsChangePassword.html',
|
||||
controller: 'settingsChangePasswordController'
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('settingsChangePassword', function (event, args) {
|
||||
$scope.changePassword();
|
||||
});
|
||||
|
||||
$scope.changeEmail = function () {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/settings/views/settingsChangeEmail.html',
|
||||
controller: 'settingsChangeEmailController',
|
||||
size: 'sm'
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('settingsChangeEmail', function (event, args) {
|
||||
$scope.changeEmail();
|
||||
});
|
||||
|
||||
$scope.twoFactor = function () {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/settings/views/settingsTwoFactor.html',
|
||||
controller: 'settingsTwoFactorController'
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('settingsTwoFactor', function (event, args) {
|
||||
$scope.twoFactor();
|
||||
});
|
||||
|
||||
$scope.sessions = function () {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/settings/views/settingsSessions.html',
|
||||
controller: 'settingsSessionsController'
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('settingsSessions', function (event, args) {
|
||||
$scope.sessions();
|
||||
});
|
||||
|
||||
$scope.delete = function () {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/settings/views/settingsDelete.html',
|
||||
controller: 'settingsDeleteController',
|
||||
size: 'sm'
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('settingsDelete', function (event, args) {
|
||||
$scope.delete();
|
||||
});
|
||||
});
|
||||
24
src/app/settings/settingsDeleteController.js
Normal file
24
src/app/settings/settingsDeleteController.js
Normal file
@@ -0,0 +1,24 @@
|
||||
angular
|
||||
.module('bit.settings')
|
||||
|
||||
.controller('settingsDeleteController', function ($scope, $state, apiService, $uibModalInstance, cryptoService, authService, toastr, $analytics) {
|
||||
$analytics.eventTrack('settingsDeleteController', { category: 'Modal' });
|
||||
$scope.submit = function (model) {
|
||||
var request = {
|
||||
masterPasswordHash: cryptoService.hashPassword(model.masterPassword)
|
||||
};
|
||||
|
||||
$scope.submitPromise = apiService.accounts.postDelete(request, function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
authService.logOut();
|
||||
$analytics.eventTrack('Deleted Account');
|
||||
$state.go('frontend.login.info').then(function () {
|
||||
toastr.success('Your account has been closed and all associated data has been deleted.', 'Account Deleted');
|
||||
});
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
});
|
||||
2
src/app/settings/settingsModule.js
Normal file
2
src/app/settings/settingsModule.js
Normal file
@@ -0,0 +1,2 @@
|
||||
angular
|
||||
.module('bit.settings', ['ui.bootstrap', 'toastr']);
|
||||
24
src/app/settings/settingsSessionsController.js
Normal file
24
src/app/settings/settingsSessionsController.js
Normal file
@@ -0,0 +1,24 @@
|
||||
angular
|
||||
.module('bit.settings')
|
||||
|
||||
.controller('settingsSessionsController', function ($scope, $state, apiService, $uibModalInstance, cryptoService, authService, toastr, $analytics) {
|
||||
$analytics.eventTrack('settingsSessionsController', { category: 'Modal' });
|
||||
$scope.submit = function (model) {
|
||||
var request = {
|
||||
masterPasswordHash: cryptoService.hashPassword(model.masterPassword)
|
||||
};
|
||||
|
||||
$scope.submitPromise = apiService.accounts.putSecurityStamp(request, function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
authService.logOut();
|
||||
$analytics.eventTrack('Deauthorized Sessions');
|
||||
$state.go('frontend.login.info').then(function () {
|
||||
toastr.success('Please log back in.', 'All Sessions Deauthorized');
|
||||
});
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
});
|
||||
80
src/app/settings/settingsTwoFactorController.js
Normal file
80
src/app/settings/settingsTwoFactorController.js
Normal file
@@ -0,0 +1,80 @@
|
||||
angular
|
||||
.module('bit.settings')
|
||||
|
||||
.controller('settingsTwoFactorController', function ($scope, apiService, $uibModalInstance, cryptoService, authService, $q, toastr, $analytics) {
|
||||
$analytics.eventTrack('settingsTwoFactorController', { category: 'Modal' });
|
||||
var _issuer = 'bitwarden',
|
||||
_profile = authService.getUserProfile(),
|
||||
_masterPasswordHash;
|
||||
|
||||
$scope.account = _profile.email;
|
||||
$scope.enabled = function () {
|
||||
return _profile.extended && _profile.extended.twoFactorEnabled;
|
||||
};
|
||||
|
||||
$scope.auth = function (model) {
|
||||
_masterPasswordHash = cryptoService.hashPassword(model.masterPassword);
|
||||
|
||||
$scope.authPromise = apiService.accounts.getTwoFactor({
|
||||
masterPasswordHash: _masterPasswordHash,
|
||||
provider: 0 /* Only authenticator provider for now. */
|
||||
}, function (response) {
|
||||
processResponse(response);
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
function formatString(s) {
|
||||
if (!s) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return s.replace(/(.{4})/g, '$1 ').trim().toUpperCase();
|
||||
}
|
||||
|
||||
function processResponse(response) {
|
||||
var key = response.AuthenticatorKey;
|
||||
$scope.twoFactorModel = {
|
||||
enabled: response.TwoFactorEnabled,
|
||||
key: formatString(key),
|
||||
recovery: formatString(response.TwoFactorRecoveryCode),
|
||||
qr: 'https://chart.googleapis.com/chart?chs=120x120&chld=L|0&cht=qr&chl=otpauth://totp/' +
|
||||
_issuer + ':' + encodeURIComponent(_profile.email) +
|
||||
'%3Fsecret=' + encodeURIComponent(key) +
|
||||
'%26issuer=' + _issuer
|
||||
};
|
||||
}
|
||||
|
||||
$scope.update = function (model) {
|
||||
var currentlyEnabled = $scope.twoFactorModel.enabled;
|
||||
if (currentlyEnabled && !confirm('Are you sure you want to disable two-step login?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var request = {
|
||||
enabled: !currentlyEnabled,
|
||||
token: model.token.replace(' ', ''),
|
||||
masterPasswordHash: _masterPasswordHash
|
||||
};
|
||||
|
||||
$scope.updatePromise = apiService.accounts.putTwoFactor({}, request, function (response) {
|
||||
if (response.TwoFactorEnabled) {
|
||||
$analytics.eventTrack('Enabled Two-step Login');
|
||||
toastr.success('Two-step login has been enabled.');
|
||||
if (_profile.extended) _profile.extended.twoFactorEnabled = true;
|
||||
processResponse(response);
|
||||
$('#token').blur();
|
||||
model.token = null;
|
||||
}
|
||||
else {
|
||||
$analytics.eventTrack('Disabled Two-step Login');
|
||||
toastr.success('Two-step login has been disabled.');
|
||||
if (_profile.extended) _profile.extended.twoFactorEnabled = false;
|
||||
$scope.close();
|
||||
}
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
});
|
||||
60
src/app/settings/views/settings.html
Normal file
60
src/app/settings/views/settings.html
Normal file
@@ -0,0 +1,60 @@
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
Settings
|
||||
<small>manage your account</small>
|
||||
</h1>
|
||||
</section>
|
||||
<section class="content">
|
||||
<div class="box box-default">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">General</h3>
|
||||
</div>
|
||||
<form role="form" name="profileForm" ng-submit="profileForm.$valid && save(model)" api-form="savePromise">
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-9">
|
||||
<div class="callout callout-danger validation-errors" ng-show="profileForm.$errors">
|
||||
<h4>Errors have occured</h4>
|
||||
<ul>
|
||||
<li ng-repeat="e in profileForm.$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">
|
||||
<label for="email">Email - <a href="javascript:void(0)" ng-click="changeEmail()">change</a></label>
|
||||
<input type="text" id="email" ng-model="model.email" class="form-control" readonly />
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="hint">Master Password Hint</label>
|
||||
<input type="text" id="hint" name="MasterPasswordHint" ng-model="model.masterPasswordHint"
|
||||
class="form-control" api-field />
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="culture">Language/Culture</label>
|
||||
<select id="culture" name="Culture" ng-model="model.culture" class="form-control" api-field>
|
||||
<option value="en-US">English (US)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3 settings-photo">
|
||||
<a href="http://www.gravatar.com/" target="_blank">
|
||||
<img src="//www.gravatar.com/avatar/{{ main.userProfile.email | gravatar }}.jpg?s=150&d=mm"
|
||||
class="img-rounded img-responsive" alt="User Image">
|
||||
</a>
|
||||
<a href="http://www.gravatar.com/" target="_blank" class="btn btn-link"
|
||||
analytics-on="click" analytics-event="Clicked Update Photo">Update Photo</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="profileForm.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="profileForm.$loading"></i>Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
54
src/app/settings/views/settingsChangeEmail.html
Normal file
54
src/app/settings/views/settingsChangeEmail.html
Normal file
@@ -0,0 +1,54 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="changeEmailModelLabel"><i class="fa fa-at"></i> Change Email</h4>
|
||||
</div>
|
||||
<form name="changeEmailForm" ng-submit="changeEmailForm.$valid && token(model)" api-form="tokenPromise" ng-show="!tokenSent && !processing">
|
||||
<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>
|
||||
<ul>
|
||||
<li ng-repeat="e in changeEmailForm.$errors">{{e}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="masterPassword">Master Password</label>
|
||||
<input type="password" id="masterPassword" name="MasterPasswordHash" ng-model="model.masterPassword" class="form-control"
|
||||
required api-field />
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="newEmail">New Email</label>
|
||||
<input type="email" id="newEmail" name="NewEmail" ng-model="model.newEmail" class="form-control" required api-field />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="changeEmailForm.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="changeEmailForm.$loading"></i>Submit
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
<form name="changeEmailConfirmForm" ng-submit="changeEmailConfirmForm.$valid && confirm(model)" api-form="confirmPromise"
|
||||
ng-show="tokenSent && !processing">
|
||||
<div class="modal-body">
|
||||
<p>We have emailed a verification code to <b>{{model.newEmail}}</b>. Please check your email for this code and enter it below to finalize your the email address change.</p>
|
||||
<div class="callout callout-warning">
|
||||
<h4><i class="fa fa-warning"></i> Warning</h4>
|
||||
Proceeding will log you out of your current session, requiring you to log back in.
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="token">Code</label>
|
||||
<input type="number" id="token" name="Token" ng-model="model.token" class="form-control" required api-field />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-flat">
|
||||
Change Email
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
<div ng-show="processing" class="modal-body text-center">
|
||||
<p><i class="fa fa-cog fa-spin fa-3x"></i></p>
|
||||
<p>Please wait. We are now changing your email and reencrypting all of your data. Do not close this window. You will be automatically logged out when this process has finished.</p>
|
||||
</div>
|
||||
46
src/app/settings/views/settingsChangePassword.html
Normal file
46
src/app/settings/views/settingsChangePassword.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="changePasswrdModelLabel"><i class="fa fa-key"></i> Change Password</h4>
|
||||
</div>
|
||||
<form name="changePasswordForm" ng-submit="changePasswordForm.$valid && save(model, changePasswordForm)" api-form="savePromise" ng-show="!processing">
|
||||
<div class="modal-body">
|
||||
<p>Below you can change your account's master password.</p>
|
||||
<p>We recommend that you change your master password immediately if you believe that your credentials have been compromised.</p>
|
||||
<div class="callout callout-warning">
|
||||
<h4><i class="fa fa-warning"></i> Warning</h4>
|
||||
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>
|
||||
<ul>
|
||||
<li ng-repeat="e in changePasswordForm.$errors">{{e}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="masterPassword">Current Master Password</label>
|
||||
<input type="password" id="masterPassword" name="MasterPasswordHash" ng-model="model.masterPassword" class="form-control"
|
||||
required api-field />
|
||||
</div>
|
||||
<hr />
|
||||
<div class="form-group" show-errors>
|
||||
<label for="newMasterPassword">New Master Password</label>
|
||||
<input type="password" id="newMasterPassword" name="NewMasterPasswordHash" ng-model="model.newMasterPassword" class="form-control"
|
||||
required api-field />
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="confirmNewMasterPassword">Confirm New Master Password</label>
|
||||
<input type="password" id="confirmNewMasterPassword" name="ConfirmNewMasterPasswordHash" ng-model="model.confirmNewMasterPassword"
|
||||
class="form-control" required api-field />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-flat">
|
||||
Change Password
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
<div ng-show="processing" class="modal-body text-center">
|
||||
<p><i class="fa fa-cog fa-spin fa-3x"></i></p>
|
||||
<p>Please wait. We are now changing your password and reencrypting all of your data. Do not close this window. You will be automatically logged out when this process has finished.</p>
|
||||
</div>
|
||||
30
src/app/settings/views/settingsDelete.html
Normal file
30
src/app/settings/views/settingsDelete.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="deleteAccountModelLabel"><i class="fa fa-trash"></i> Delete Account</h4>
|
||||
</div>
|
||||
<form name="deleteAccountForm" ng-submit="deleteAccountForm.$valid && submit(model)" api-form="submitPromise">
|
||||
<div class="modal-body">
|
||||
<p>Continue below to delete your account and all associated data.</p>
|
||||
<div class="callout callout-warning">
|
||||
<h4><i class="fa fa-warning"></i> Warning</h4>
|
||||
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>
|
||||
<ul>
|
||||
<li ng-repeat="e in deleteAccountForm.$errors">{{e}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="masterPassword">Master Password</label>
|
||||
<input type="password" id="masterPassword" name="MasterPasswordHash" ng-model="model.masterPassword" class="form-control"
|
||||
required api-field />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="deleteAccountForm.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="deleteAccountForm.$loading"></i>Delete
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
32
src/app/settings/views/settingsSessions.html
Normal file
32
src/app/settings/views/settingsSessions.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="logoutSessionsModelLabel"><i class="fa fa-ban"></i> Deauthorize Sessions</h4>
|
||||
</div>
|
||||
<form name="logoutSessionsForm" ng-submit="logoutSessionsForm.$valid && submit(model)" api-form="submitPromise">
|
||||
<div class="modal-body">
|
||||
<p>Concerned your account is logged in on another device?</p>
|
||||
<p>Proceed below to deauthorize all computers or devices that you have previously used.</p>
|
||||
<p>This security step is recommended if you previously used a public PC or accidentally saved your password on a device that isn't yours.</p>
|
||||
<div class="callout callout-warning">
|
||||
<h4><i class="fa fa-warning"></i> Warning</h4>
|
||||
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>
|
||||
<ul>
|
||||
<li ng-repeat="e in logoutSessionsForm.$errors">{{e}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="masterPassword">Master Password</label>
|
||||
<input type="password" id="masterPassword" name="MasterPasswordHash" ng-model="model.masterPassword" class="form-control"
|
||||
required api-field />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="logoutSessionsForm.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="logoutSessionsForm.$loading"></i>Deauthorize
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
95
src/app/settings/views/settingsTwoFactor.html
Normal file
95
src/app/settings/views/settingsTwoFactor.html
Normal file
@@ -0,0 +1,95 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="twoFactorModelLabel"><i class="fa fa-key"></i> Two-step Login</h4>
|
||||
</div>
|
||||
<form name="authTwoStepForm" ng-submit="authTwoStepForm.$valid && auth(authModel)" api-form="authPromise" ng-if="!twoFactorModel">
|
||||
<div class="modal-body">
|
||||
<p>Current Status: <span class="label bg-green" ng-show="enabled()">ENABLED</span><span class="label bg-gray" ng-show="!enabled()">DISABLED</span></p>
|
||||
<p>Two-step login helps keep your account more secure by requiring a code provided by an app on your mobile device while logging in (in addition to your master password).</p>
|
||||
<p ng-show="enabled()">Two-step login is already enabled on your account. To access your two-step settings enter your master password below.</p>
|
||||
<p ng-show="!enabled()">To get started with two-step login enter your master password below.</p>
|
||||
<div class="callout callout-danger validation-errors" ng-show="authTwoStepForm.$errors">
|
||||
<h4>Errors have occured</h4>
|
||||
<ul>
|
||||
<li ng-repeat="e in authTwoStepForm.$errors">{{e}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="masterPassword">Master Password</label>
|
||||
<input type="password" id="masterPassword" name="MasterPasswordHash" ng-model="authModel.masterPassword" class="form-control" required api-field />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="authTwoStepForm.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="authTwoStepForm.$loading"></i>Continue
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
<form name="updateTwoStepForm" ng-submit="updateTwoStepForm.$valid && update(updateModel)" api-form="updatePromise" ng-if="twoFactorModel">
|
||||
<div class="modal-body">
|
||||
<div ng-show="enabled()">
|
||||
<p>Two-step login is <strong class="text-green">enabled</strong> on your account. Below is the code required by your verification app.</p>
|
||||
<p>Need a two-step verification app? Download one of the following:</p>
|
||||
</div>
|
||||
<div ng-show="!enabled()">
|
||||
<p>Setting up two-step verification is easy, just follow these steps:</p>
|
||||
<h4>1. Download a two-step verification app</h4>
|
||||
</div>
|
||||
<ul class="fa-ul">
|
||||
<li><i class="fa-li fa fa-apple"></i> iOS devices: <a href="https://itunes.apple.com/us/app/authy/id494168017?mt=8" target="_blank">Authy for iOS</a></li>
|
||||
<li><i class="fa-li fa fa-android"></i> Android devices: <a href="https://play.google.com/store/apps/details?id=com.authy.authy" target="_blank">Authy for Android</a></li>
|
||||
<li><i class="fa-li fa fa-windows"></i> Windows devices: <a href="https://www.microsoft.com/en-us/store/apps/authenticator/9wzdncrfj3rj" target="_blank">Microsoft Authenticator </a></li>
|
||||
</ul>
|
||||
<hr ng-show="enabled()" />
|
||||
<h4 ng-show="!enabled()" style="margin-top: 30px;">2. Scan this QR code with your verification app</h4>
|
||||
<div class="row">
|
||||
<div class="col-md-4 text-center">
|
||||
<p><img ng-src="{{twoFactorModel.qr}}" alt="QR" class="img-thumbnail" /></p>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<p><strong>Can't scan the code?</strong> You can add the code to your application manually using the following details:</p>
|
||||
<ul class="list-unstyled">
|
||||
<li><strong>Key:</strong> <code>{{twoFactorModel.key}}</code></li>
|
||||
<li><strong>Account:</strong> {{account}}</li>
|
||||
<li><strong>Time based:</strong> Yes</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="enabled()">
|
||||
<hr />
|
||||
<h4>Recovery Code</h4>
|
||||
<p>
|
||||
The recovery code allows you to access your account in the event that you lose your authenticator app.
|
||||
bitwarden support won't be able to assist you if you lose access to your account. We recommend you write down or
|
||||
print the recovery code below and keep it in a safe place.
|
||||
</p>
|
||||
<ul class="list-unstyled">
|
||||
<li>
|
||||
<strong>Recovery Code:</strong> <code>{{twoFactorModel.recovery}}</code>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="callout callout-danger validation-errors" ng-show="updateTwoStepForm.$errors">
|
||||
<h4>Errors have occured</h4>
|
||||
<ul>
|
||||
<li ng-repeat="e in updateTwoStepForm.$errors">{{e}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<hr ng-show="enabled()" />
|
||||
<h4 style="margin-top: 30px;"><span ng-show="!enabled()">3. </span>Enter the resulting verification code from the app</h4>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="token" class="sr-only">Verification Code</label>
|
||||
<input type="text" id="token" name="Token" placeholder="Verification Code" ng-model="updateModel.token" class="form-control" required api-field />
|
||||
</div>
|
||||
<p ng-show="!enabled()">NOTE: After enabling two-step login, you will be required to enter the current code generated by your verification app each time you log in.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="updateTwoStepForm.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="updateTwoStepForm.$loading"></i>
|
||||
<span ng-show="enabled()">Disable Two-step</span>
|
||||
<span ng-show="!enabled()">Enable Two-step</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
9
src/app/tools/toolsAuditsController.js
Normal file
9
src/app/tools/toolsAuditsController.js
Normal file
@@ -0,0 +1,9 @@
|
||||
angular
|
||||
.module('bit.tools')
|
||||
|
||||
.controller('toolsAuditsController', function ($scope, apiService, $uibModalInstance, toastr, $analytics) {
|
||||
$analytics.eventTrack('toolsAuditsController', { category: 'Modal' });
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
});
|
||||
42
src/app/tools/toolsController.js
Normal file
42
src/app/tools/toolsController.js
Normal file
@@ -0,0 +1,42 @@
|
||||
angular
|
||||
.module('bit.tools')
|
||||
|
||||
.controller('toolsController', function ($scope, $uibModal, apiService, toastr, authService) {
|
||||
$scope.import = function () {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/tools/views/toolsImport.html',
|
||||
controller: 'toolsImportController',
|
||||
size: 'sm'
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('toolsImport', function (event, args) {
|
||||
$scope.import();
|
||||
});
|
||||
|
||||
$scope.export = function () {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/tools/views/toolsExport.html',
|
||||
controller: 'toolsExportController',
|
||||
size: 'sm'
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('toolsExport', function (event, args) {
|
||||
$scope.export();
|
||||
});
|
||||
|
||||
$scope.audits = function () {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/tools/views/toolsAudits.html',
|
||||
controller: 'toolsAuditsController'
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('toolsAudits', function (event, args) {
|
||||
$scope.audits();
|
||||
});
|
||||
});
|
||||
73
src/app/tools/toolsExportController.js
Normal file
73
src/app/tools/toolsExportController.js
Normal file
@@ -0,0 +1,73 @@
|
||||
angular
|
||||
.module('bit.tools')
|
||||
|
||||
.controller('toolsExportController', function ($scope, apiService, authService, $uibModalInstance, cryptoService, cipherService, $q, toastr, $analytics) {
|
||||
$analytics.eventTrack('toolsExportController', { category: 'Modal' });
|
||||
$scope.export = function (model) {
|
||||
$scope.startedExport = true;
|
||||
apiService.sites.list({ expand: ['folder'] }, function (sites) {
|
||||
try {
|
||||
var decSites = cipherService.decryptSites(sites.Data);
|
||||
|
||||
var exportSites = [];
|
||||
for (var i = 0; i < decSites.length; i++) {
|
||||
var site = {
|
||||
name: decSites[i].name,
|
||||
uri: decSites[i].uri,
|
||||
username: decSites[i].username,
|
||||
password: decSites[i].password,
|
||||
notes: decSites[i].notes,
|
||||
folder: decSites[i].folder ? decSites[i].folder.name : null
|
||||
};
|
||||
|
||||
exportSites.push(site);
|
||||
}
|
||||
|
||||
var csvString = Papa.unparse(exportSites);
|
||||
var csvBlob = new Blob([csvString]);
|
||||
if (window.navigator.msSaveOrOpenBlob) { // IE hack. ref http://msdn.microsoft.com/en-us/library/ie/hh779016.aspx
|
||||
window.navigator.msSaveBlob(csvBlob, makeFileName());
|
||||
}
|
||||
else {
|
||||
var a = window.document.createElement('a');
|
||||
a.href = window.URL.createObjectURL(csvBlob, { type: 'text/plain' });
|
||||
a.download = makeFileName();
|
||||
document.body.appendChild(a);
|
||||
a.click(); // IE: "Access is denied". ref: https://connect.microsoft.com/IE/feedback/details/797361/ie-10-treats-blob-url-as-cross-origin-and-denies-access
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
|
||||
$analytics.eventTrack('Exported Data');
|
||||
toastr.success('Your data has been exported. Check your browser\'s downloads folder.', 'Success!');
|
||||
$scope.close();
|
||||
}
|
||||
catch (err) {
|
||||
toastr.error('Something went wrong. Please try again.', 'Error!');
|
||||
$scope.close();
|
||||
}
|
||||
}, function () {
|
||||
toastr.error('Something went wrong. Please try again.', 'Error!');
|
||||
$scope.close();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
|
||||
function makeFileName() {
|
||||
var now = new Date();
|
||||
var dateString =
|
||||
now.getFullYear() + '' + padNumber(now.getMonth() + 1, 2) + '' + padNumber(now.getDate(), 2) +
|
||||
padNumber(now.getHours(), 2) + '' + padNumber(now.getMinutes(), 2) +
|
||||
padNumber(now.getSeconds(), 2);
|
||||
|
||||
return 'bitwarden_export_' + dateString + '.csv';
|
||||
}
|
||||
|
||||
function padNumber(number, width, paddingCharacter) {
|
||||
paddingCharacter = paddingCharacter || '0';
|
||||
number = number + '';
|
||||
return number.length >= width ? number : new Array(width - number.length + 1).join(paddingCharacter) + number;
|
||||
}
|
||||
});
|
||||
73
src/app/tools/toolsImportController.js
Normal file
73
src/app/tools/toolsImportController.js
Normal file
@@ -0,0 +1,73 @@
|
||||
angular
|
||||
.module('bit.tools')
|
||||
|
||||
.controller('toolsImportController', function ($scope, $state, apiService, $uibModalInstance, cryptoService, cipherService, toastr, importService, $analytics) {
|
||||
$analytics.eventTrack('toolsImportController', { category: 'Modal' });
|
||||
$scope.model = { source: 'local' };
|
||||
|
||||
$scope.import = function (model) {
|
||||
$scope.processing = true;
|
||||
var file = document.getElementById('file').files[0];
|
||||
importService.import(model.source, file, importSuccess, importError);
|
||||
};
|
||||
|
||||
function importSuccess(folders, sites, folderRelationships) {
|
||||
if (!folders.length && !sites.length) {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
toastr.error('Nothing was imported.');
|
||||
return;
|
||||
}
|
||||
|
||||
apiService.ciphers.import({
|
||||
folders: cipherService.encryptFolders(folders, cryptoService.getKey()),
|
||||
sites: cipherService.encryptSites(sites, cryptoService.getKey()),
|
||||
folderRelationships: folderRelationships
|
||||
}, function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
$state.go('backend.vault').then(function () {
|
||||
$analytics.eventTrack('Imported Data', { label: $scope.model.source });
|
||||
toastr.success('Data has been successfully imported into your vault.', 'Import Success');
|
||||
});
|
||||
}, importError);
|
||||
}
|
||||
|
||||
function importError(error) {
|
||||
$analytics.eventTrack('Import Data Failed', { label: $scope.model.source });
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
|
||||
if (error) {
|
||||
var data = error.data;
|
||||
if (data && data.ValidationErrors) {
|
||||
var message = '';
|
||||
for (var key in data.ValidationErrors) {
|
||||
if (!data.ValidationErrors.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var i = 0; i < data.ValidationErrors[key].length; i++) {
|
||||
message += (key + ': ' + data.ValidationErrors[key][i] + ' ');
|
||||
}
|
||||
}
|
||||
|
||||
if (message !== '') {
|
||||
toastr.error(message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (data && data.Message) {
|
||||
toastr.error(data.Message);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
toastr.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
toastr.error('Something went wrong. Try again.', 'Oh No!');
|
||||
}
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
});
|
||||
2
src/app/tools/toolsModule.js
Normal file
2
src/app/tools/toolsModule.js
Normal file
@@ -0,0 +1,2 @@
|
||||
angular
|
||||
.module('bit.tools', ['ui.bootstrap', 'toastr']);
|
||||
9
src/app/tools/views/tools.html
Normal file
9
src/app/tools/views/tools.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
Tools
|
||||
<small>helpful utilities</small>
|
||||
</h1>
|
||||
</section>
|
||||
<section class="content">
|
||||
Several tools are available in the menu to the left. More tools coming soon...
|
||||
</section>
|
||||
10
src/app/tools/views/toolsAudits.html
Normal file
10
src/app/tools/views/toolsAudits.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="auditsModelLabel"><i class="fa fa-search"></i> Audits</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Coming soon...
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
|
||||
</div>
|
||||
28
src/app/tools/views/toolsExport.html
Normal file
28
src/app/tools/views/toolsExport.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="exportModelLabel"><i class="fa fa-cloud-download"></i> Export</h4>
|
||||
</div>
|
||||
<form name="exportForm" ng-submit="exportForm.$valid && export(model)" api-form="exportPromise" ng-show="!startedExport">
|
||||
<div class="modal-body">
|
||||
<p>Export all of your vault data in <code>.csv</code> format. Enter your master password to continue.</p>
|
||||
<div class="callout callout-danger validation-errors" ng-show="exportForm.$errors">
|
||||
<h4>Errors have occured</h4>
|
||||
<ul>
|
||||
<li ng-repeat="e in exportForm.$errors">{{e}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="masterPassword">Master Password</label>
|
||||
<input type="password" id="masterPassword" name="MasterPasswordHash" ng-model="model.masterPassword" class="form-control"
|
||||
master-password required api-field ng-model-options="{ 'updateOn': 'blur'}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-flat">Export</button>
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="modal-body text-center" ng-show="startedExport">
|
||||
<p><i class="fa fa-cog fa-spin fa-3x"></i></p>
|
||||
<p>Please wait. We are now exporting all of your data to a <code>.csv</code> file.</p>
|
||||
</div>
|
||||
39
src/app/tools/views/toolsImport.html
Normal file
39
src/app/tools/views/toolsImport.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="importModelLabel"><i class="fa fa-cloud-upload"></i> Import</h4>
|
||||
</div>
|
||||
<form name="importForm" ng-submit="importForm.$valid && import(model)" ng-show="!processing">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="source">Source</label>
|
||||
<select id="source" name="source" class="form-control" ng-model="model.source">
|
||||
<option value="local">bitwarden (csv)</option>
|
||||
<option value="lastpass">LastPass (csv)</option>
|
||||
<option value="chromecsv">Chrome (csv)</option>
|
||||
<option value="firefoxpasswordexportercsvxml">Firefox Password Exporter (xml)</option>
|
||||
<option value="safeincloudxml">SafeInCloud (xml)</option>
|
||||
<option value="safeincloudcsv">SafeInCloud (csv)</option>
|
||||
<option value="keypassxml">KeyPass (xml)</option>
|
||||
<option value="padlockcsv">Padlock (csv)</option>
|
||||
<option value="1password1pif">1Password (1pif)</option>
|
||||
<option value="upmcsv">Universal Password Manager (csv)</option>
|
||||
<option value="keepercsv">Keeper (csv)</option>
|
||||
<option value="passworddragonxml">Password Dragon (xml)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="file">File</label>
|
||||
<input type="file" id="file" name="file" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-flat">
|
||||
Import
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
<div ng-show="processing" class="modal-body text-center">
|
||||
<p><i class="fa fa-cog fa-spin fa-3x"></i></p>
|
||||
<p>Please wait. We are now importing all of your data. Do not close this window. You will be redirected to your vault when the import has completed.</p>
|
||||
</div>
|
||||
19
src/app/vault/vaultAddFolderController.js
Normal file
19
src/app/vault/vaultAddFolderController.js
Normal file
@@ -0,0 +1,19 @@
|
||||
angular
|
||||
.module('bit.vault')
|
||||
|
||||
.controller('vaultAddFolderController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, $analytics) {
|
||||
$analytics.eventTrack('vaultAddFolderController', { category: 'Modal' });
|
||||
$scope.savePromise = null;
|
||||
$scope.save = function (model) {
|
||||
var folder = cipherService.encryptFolder(model);
|
||||
$scope.savePromise = apiService.folders.post(folder, function (response) {
|
||||
$analytics.eventTrack('Created Folder');
|
||||
var decFolder = cipherService.decryptFolder(response);
|
||||
$uibModalInstance.close(decFolder);
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('close');
|
||||
};
|
||||
});
|
||||
54
src/app/vault/vaultAddSiteController.js
Normal file
54
src/app/vault/vaultAddSiteController.js
Normal file
@@ -0,0 +1,54 @@
|
||||
angular
|
||||
.module('bit.vault')
|
||||
|
||||
.controller('vaultAddSiteController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, passwordService, folders, selectedFolder, $analytics) {
|
||||
$analytics.eventTrack('vaultAddSiteController', { category: 'Modal' });
|
||||
$scope.folders = folders;
|
||||
$scope.site = {
|
||||
folderId: selectedFolder ? selectedFolder.id : null
|
||||
};
|
||||
|
||||
$scope.savePromise = null;
|
||||
$scope.save = function (model) {
|
||||
var site = cipherService.encryptSite(model);
|
||||
$scope.savePromise = apiService.sites.post(site, function (siteResponse) {
|
||||
$analytics.eventTrack('Created Site');
|
||||
var decSite = cipherService.decryptSite(siteResponse);
|
||||
$uibModalInstance.close(decSite);
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
$scope.generatePassword = function () {
|
||||
if (!$scope.site.password || confirm('Are you sure you want to overwrite the current password?')) {
|
||||
$analytics.eventTrack('Generated Password From Add');
|
||||
$scope.site.password = passwordService.generatePassword({ length: 10, special: true });
|
||||
}
|
||||
};
|
||||
|
||||
$scope.clipboardSuccess = function (e) {
|
||||
e.clearSelection();
|
||||
selectPassword(e);
|
||||
};
|
||||
|
||||
$scope.clipboardError = function (e, password) {
|
||||
if (password) {
|
||||
selectPassword(e);
|
||||
}
|
||||
alert('Your web browser does not support easy clipboard copying. Copy it manually instead.');
|
||||
};
|
||||
|
||||
$scope.folderSort = function (item) {
|
||||
return item.name.toLowerCase();
|
||||
};
|
||||
|
||||
function selectPassword(e) {
|
||||
var target = $(e.trigger).parent().prev();
|
||||
if (target.attr('type') === 'text') {
|
||||
target.select();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('close');
|
||||
};
|
||||
});
|
||||
193
src/app/vault/vaultController.js
Normal file
193
src/app/vault/vaultController.js
Normal file
@@ -0,0 +1,193 @@
|
||||
angular
|
||||
.module('bit.vault')
|
||||
|
||||
.controller('vaultController', function ($scope, $uibModal, apiService, $filter, cryptoService, authService, toastr, cipherService) {
|
||||
$scope.sites = [];
|
||||
$scope.folders = [];
|
||||
|
||||
$scope.loadingSites = true;
|
||||
apiService.sites.list({}, function (sites) {
|
||||
$scope.loadingSites = false;
|
||||
|
||||
var decSites = [];
|
||||
for (var i = 0; i < sites.Data.length; i++) {
|
||||
var decSite = {
|
||||
id: sites.Data[i].Id,
|
||||
folderId: sites.Data[i].FolderId,
|
||||
favorite: sites.Data[i].Favorite
|
||||
};
|
||||
|
||||
try { decSite.name = cryptoService.decrypt(sites.Data[i].Name); }
|
||||
catch (err) { decSite.name = '[error: cannot decrypt]'; }
|
||||
|
||||
if (sites.Data[i].Username) {
|
||||
try { decSite.username = cryptoService.decrypt(sites.Data[i].Username); }
|
||||
catch (err) { decSite.username = '[error: cannot decrypt]'; }
|
||||
}
|
||||
|
||||
decSites.push(decSite);
|
||||
}
|
||||
|
||||
$scope.sites = decSites;
|
||||
}, function () {
|
||||
$scope.loadingSites = false;
|
||||
});
|
||||
|
||||
$scope.loadingFolders = true;
|
||||
apiService.folders.list({}, function (folders) {
|
||||
$scope.loadingFolders = false;
|
||||
|
||||
var decFolders = [{
|
||||
id: null,
|
||||
name: '(none)'
|
||||
}];
|
||||
|
||||
for (var i = 0; i < folders.Data.length; i++) {
|
||||
var decFolder = {
|
||||
id: folders.Data[i].Id
|
||||
};
|
||||
|
||||
try { decFolder.name = cryptoService.decrypt(folders.Data[i].Name); }
|
||||
catch (err) { decFolder.name = '[error: cannot decrypt]'; }
|
||||
|
||||
decFolders.push(decFolder);
|
||||
}
|
||||
|
||||
$scope.folders = decFolders;
|
||||
}, function () {
|
||||
$scope.loadingFolders = false;
|
||||
});
|
||||
|
||||
$scope.folderSort = function (item) {
|
||||
if (!item.id) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return item.name.toLowerCase();
|
||||
};
|
||||
|
||||
$scope.editSite = function (site) {
|
||||
var editModel = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/vault/views/vaultEditSite.html',
|
||||
controller: 'vaultEditSiteController',
|
||||
resolve: {
|
||||
siteId: function () { return site.id; },
|
||||
folders: function () { return $scope.folders; }
|
||||
}
|
||||
});
|
||||
|
||||
editModel.result.then(function (returnVal) {
|
||||
if (returnVal.action === 'edit') {
|
||||
var siteToUpdate = $filter('filter')($scope.sites, { id: returnVal.data.id }, true);
|
||||
|
||||
if (siteToUpdate && siteToUpdate.length > 0) {
|
||||
siteToUpdate[0].folderId = returnVal.data.folderId;
|
||||
siteToUpdate[0].name = returnVal.data.name;
|
||||
siteToUpdate[0].username = returnVal.data.username;
|
||||
siteToUpdate[0].favorite = returnVal.data.favorite;
|
||||
}
|
||||
}
|
||||
else if (returnVal.action === 'delete') {
|
||||
var siteToDelete = $filter('filter')($scope.sites, { id: returnVal.data }, true);
|
||||
if (siteToDelete && siteToDelete.length > 0) {
|
||||
var index = $scope.sites.indexOf(siteToDelete[0]);
|
||||
if (index > -1) {
|
||||
$scope.sites.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('vaultAddSite', function (event, args) {
|
||||
$scope.addSite();
|
||||
});
|
||||
|
||||
$scope.addSite = function (folder) {
|
||||
var addModel = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/vault/views/vaultAddSite.html',
|
||||
controller: 'vaultAddSiteController',
|
||||
resolve: {
|
||||
folders: function () { return $scope.folders; },
|
||||
selectedFolder: function () { return folder; }
|
||||
}
|
||||
});
|
||||
|
||||
addModel.result.then(function (addedSite) {
|
||||
$scope.sites.push(addedSite);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteSite = function (site) {
|
||||
if (!confirm('Are you sure you want to delete this site (' + site.name + ')?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
apiService.sites.del({ id: site.id }, function () {
|
||||
var index = $scope.sites.indexOf(site);
|
||||
if (index > -1) {
|
||||
$scope.sites.splice(index, 1);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.editFolder = function (folder) {
|
||||
var editModel = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/vault/views/vaultEditFolder.html',
|
||||
controller: 'vaultEditFolderController',
|
||||
size: 'sm',
|
||||
resolve: {
|
||||
folderId: function () { return folder.id; }
|
||||
}
|
||||
});
|
||||
|
||||
editModel.result.then(function (editedFolder) {
|
||||
var folder = $filter('filter')($scope.folders, { id: editedFolder.id }, true);
|
||||
if (folder && folder.length > 0) {
|
||||
folder[0].name = editedFolder.name;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('vaultAddFolder', function (event, args) {
|
||||
$scope.addFolder();
|
||||
});
|
||||
|
||||
$scope.addFolder = function () {
|
||||
var addModel = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/vault/views/vaultAddFolder.html',
|
||||
controller: 'vaultAddFolderController',
|
||||
size: 'sm'
|
||||
});
|
||||
|
||||
addModel.result.then(function (addedFolder) {
|
||||
$scope.folders.push(addedFolder);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteFolder = function (folder) {
|
||||
if (!confirm('Are you sure you want to delete this folder (' + folder.name + ')?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
apiService.folders.del({ id: folder.id }, function () {
|
||||
var index = $scope.folders.indexOf(folder);
|
||||
if (index > -1) {
|
||||
$scope.folders.splice(index, 1);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.canDeleteFolder = function (folder) {
|
||||
if (!folder || !folder.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var sites = $filter('filter')($scope.sites, { folderId: folder.id });
|
||||
return sites.length === 0;
|
||||
};
|
||||
});
|
||||
25
src/app/vault/vaultEditFolderController.js
Normal file
25
src/app/vault/vaultEditFolderController.js
Normal file
@@ -0,0 +1,25 @@
|
||||
angular
|
||||
.module('bit.vault')
|
||||
|
||||
.controller('vaultEditFolderController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, folderId, $analytics) {
|
||||
$analytics.eventTrack('vaultEditFolderController', { category: 'Modal' });
|
||||
$scope.folder = {};
|
||||
|
||||
apiService.folders.get({ id: folderId }, function (folder) {
|
||||
$scope.folder = cipherService.decryptFolder(folder);
|
||||
});
|
||||
|
||||
$scope.savePromise = null;
|
||||
$scope.save = function (model) {
|
||||
var folder = cipherService.encryptFolder(model);
|
||||
$scope.savePromise = apiService.folders.put({ id: folderId }, folder, function (response) {
|
||||
$analytics.eventTrack('Edited Folder');
|
||||
var decFolder = cipherService.decryptFolder(response);
|
||||
$uibModalInstance.close(decFolder);
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
});
|
||||
71
src/app/vault/vaultEditSiteController.js
Normal file
71
src/app/vault/vaultEditSiteController.js
Normal file
@@ -0,0 +1,71 @@
|
||||
angular
|
||||
.module('bit.vault')
|
||||
|
||||
.controller('vaultEditSiteController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, passwordService, siteId, folders, $analytics) {
|
||||
$analytics.eventTrack('vaultEditSiteController', { category: 'Modal' });
|
||||
$scope.folders = folders;
|
||||
$scope.site = {};
|
||||
|
||||
apiService.sites.get({ id: siteId }, function (site) {
|
||||
$scope.site = cipherService.decryptSite(site);
|
||||
});
|
||||
|
||||
$scope.save = function (model) {
|
||||
var site = cipherService.encryptSite(model);
|
||||
$scope.savePromise = apiService.sites.put({ id: siteId }, site, function (siteResponse) {
|
||||
$analytics.eventTrack('Edited Site');
|
||||
var decSite = cipherService.decryptSite(siteResponse);
|
||||
$uibModalInstance.close({
|
||||
action: 'edit',
|
||||
data: decSite
|
||||
});
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
$scope.generatePassword = function () {
|
||||
if (!$scope.site.password || confirm('Are you sure you want to overwrite the current password?')) {
|
||||
$analytics.eventTrack('Generated Password From Edit');
|
||||
$scope.site.password = passwordService.generatePassword({ length: 10, special: true });
|
||||
}
|
||||
};
|
||||
|
||||
$scope.clipboardSuccess = function (e) {
|
||||
e.clearSelection();
|
||||
selectPassword(e);
|
||||
};
|
||||
|
||||
$scope.clipboardError = function (e, password) {
|
||||
if (password) {
|
||||
selectPassword(e);
|
||||
}
|
||||
alert('Your web browser does not support easy clipboard copying. Copy it manually instead.');
|
||||
};
|
||||
|
||||
$scope.folderSort = function (item) {
|
||||
return item.name.toLowerCase();
|
||||
};
|
||||
|
||||
function selectPassword(e) {
|
||||
var target = $(e.trigger).parent().prev();
|
||||
if (target.attr('type') === 'text') {
|
||||
target.select();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.delete = function () {
|
||||
if (!confirm('Are you sure you want to delete this site (' + $scope.site.name + ')?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
apiService.sites.del({ id: $scope.site.id }, function () {
|
||||
$uibModalInstance.close({
|
||||
action: 'delete',
|
||||
data: $scope.site.id
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
});
|
||||
2
src/app/vault/vaultModule.js
Normal file
2
src/app/vault/vaultModule.js
Normal file
@@ -0,0 +1,2 @@
|
||||
angular
|
||||
.module('bit.vault', ['ui.bootstrap', 'ngclipboard']);
|
||||
60
src/app/vault/views/vault.html
Normal file
60
src/app/vault/views/vault.html
Normal file
@@ -0,0 +1,60 @@
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
My Vault
|
||||
<small>safe and secure</small>
|
||||
</h1>
|
||||
</section>
|
||||
<section class="content">
|
||||
<div ng-show="loadingFolders && !folders.length">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
<div class="box" ng-repeat="folder in folders | orderBy: folderSort" ng-show="folders.length">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title"><i class="fa fa-folder-open"></i> {{folder.name}}</h3>
|
||||
<div class="box-tools pull-right">
|
||||
<button type="button" class="btn btn-box-tool" ng-click="addSite(folder)" uib-tooltip="Add Site">
|
||||
<i class="fa fa-plus"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-box-tool" ng-click="deleteFolder(folder)" ng-show="canDeleteFolder(folder)" uib-tooltip="Delete">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-box-tool" ng-click="editFolder(folder)" ng-show="folder.id" uib-tooltip="Edit">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-box-tool" data-widget="collapse" uib-tooltip="Collapse">
|
||||
<i class="fa fa-minus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-body" ng-class="{'no-padding': folderSites.length}">
|
||||
<div ng-show="loadingSites && !folderSites.length">
|
||||
<p>Loading sites...</p>
|
||||
</div>
|
||||
<div ng-show="!loadingSites && !folderSites.length">
|
||||
<p>No sites in this folder.</p>
|
||||
<button type="button" ng-click="addSite(folder)" class="btn btn-default btn-flat">Add a Site</button>
|
||||
</div>
|
||||
<div class="table-responsive" ng-show="folderSites.length">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 75px; min-width: 75px;"></th>
|
||||
<th>Site</th>
|
||||
<th style="width: 300px;">Username</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="site in folderSites = (sites | filter: { folderId: folder.id } | filter: (main.searchVaultText || '') | orderBy: ['name', 'username'])">
|
||||
<td>
|
||||
<button type="button" ng-click="deleteSite(site)" class="btn btn-link btn-table" uib-tooltip="Delete"><i class="fa fa-lg fa-trash"></i></button>
|
||||
<button type="button" ng-click="editSite(site)" class="btn btn-link btn-table" uib-tooltip="View/Edit"><i class="fa fa-lg fa-pencil"></i></button>
|
||||
</td>
|
||||
<td>{{site.name}} <i class="fa fa-star text-muted" uib-tooltip="Favorite" ng-show="site.favorite"></i></td>
|
||||
<td>{{site.username}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
24
src/app/vault/views/vaultAddFolder.html
Normal file
24
src/app/vault/views/vaultAddFolder.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="addFolderModelLabel"><i class="fa fa-folder"></i> Add New Folder</h4>
|
||||
</div>
|
||||
<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>
|
||||
<ul>
|
||||
<li ng-repeat="e in addFolderForm.$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="folder.name" class="form-control" required api-field />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="addFolderForm.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="addFolderForm.$loading"></i>Submit
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
98
src/app/vault/views/vaultAddSite.html
Normal file
98
src/app/vault/views/vaultAddSite.html
Normal file
@@ -0,0 +1,98 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="addSiteModelLabel"><i class="fa fa-globe"></i> Add New Site</h4>
|
||||
</div>
|
||||
<form name="addSiteForm" ng-submit="addSiteForm.$valid && save(site)" api-form="savePromise">
|
||||
<div class="modal-body">
|
||||
<div class="callout callout-danger validation-errors" ng-show="addSiteForm.$errors">
|
||||
<h4>Errors have occured</h4>
|
||||
<ul>
|
||||
<li ng-repeat="e in addSiteForm.$errors">{{e}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="uri">URI</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="uri" ng-model="site.uri" name="Uri" class="form-control" placeholder="http://..." api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy URI" tooltip-placement="left">
|
||||
<button tabindex="-1" class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)"
|
||||
data-clipboard-target="#uri">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="name">Name</label>
|
||||
<input type="text" id="name" name="Name" ng-model="site.name" class="form-control" required api-field />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="folder">Folder</label>
|
||||
<select id="folder" name="FolderId" ng-model="site.folderId" class="form-control" api-field>
|
||||
<option ng-repeat="folder in folders | orderBy: folderSort" value="{{folder.id}}">{{folder.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="username">Username</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="username" name="Username" ng-model="site.username" class="form-control" api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy Username" tooltip-placement="left">
|
||||
<button tabindex="-1" class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)"
|
||||
data-clipboard-target="#username">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group" show-errors>
|
||||
<div class="pull-right password-options">
|
||||
<i class="fa fa-lg fa-refresh" uib-tooltip="Generate Password" tooltip-placement="left" ng-click="generatePassword()"></i>
|
||||
<i class="fa fa-lg fa-eye" uib-tooltip="Toggle Password" tooltip-placement="left" password-viewer="#password"></i>
|
||||
</div>
|
||||
<label for="password">Password</label>
|
||||
<div class="input-group">
|
||||
<input tabindex="-1" type="text" id="password-text" value="{{site.password}}" style="margin-left: -9999px;" />
|
||||
<input type="password" id="password" name="Password" ng-model="site.password" class="form-control" api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy Password" tooltip-placement="left">
|
||||
<button tabindex="-1" class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-success="clipboardSuccess(e)"
|
||||
ngclipboard-error="clipboardError(e, true)"
|
||||
data-clipboard-target="#password-text">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin: -10px 0 15px 0;" password-meter="site.password" password-meter-username="site.username" outer-class="xs"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="notes">Notes</label>
|
||||
<textarea id="notes" name="Notes" class="form-control" ng-model="site.notes" api-field></textarea>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="site.favorite" name="Favorite" />
|
||||
Favorite
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="addSiteForm.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="addSiteForm.$loading"></i>Submit
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
24
src/app/vault/views/vaultEditFolder.html
Normal file
24
src/app/vault/views/vaultEditFolder.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="editFolderModelLabel"><i class="fa fa-folder"></i> Edit Folder <small>{{folder.name}}</small></h4>
|
||||
</div>
|
||||
<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>
|
||||
<ul>
|
||||
<li ng-repeat="e in editFolderForm.$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="folder.name" class="form-control" required api-field />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="editFolderForm.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="editFolderForm.$loading"></i>Save
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
104
src/app/vault/views/vaultEditSite.html
Normal file
104
src/app/vault/views/vaultEditSite.html
Normal file
@@ -0,0 +1,104 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="editSiteModelLabel"><i class="fa fa-globe"></i> Site Information <small>{{site.name}}</small></h4>
|
||||
</div>
|
||||
<form name="editSiteForm" ng-submit="editSiteForm.$valid && save(site)" api-form="savePromise">
|
||||
<div class="modal-body">
|
||||
<div class="callout callout-danger validation-errors" ng-show="editSiteForm.$errors">
|
||||
<h4>Errors have occured</h4>
|
||||
<ul>
|
||||
<li ng-repeat="e in editSiteForm.$errors">{{e}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="uri">URI</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="uri" name="Uri" ng-model="site.uri" class="form-control" placeholder="http://..." api-field />
|
||||
<span class="input-group-btn">
|
||||
<button tabindex="-1" class="btn btn-default btn-flat" type="button" uib-tooltip="Copy URI" tooltip-placement="left" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)"
|
||||
data-clipboard-target="#uri">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
<a href="{{site.uri}}" target="_blank" class="btn btn-default btn-flat" uib-tooltip="Go To Site" tooltip-placement="left">
|
||||
<i class="fa fa-share"></i>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="name">Name</label>
|
||||
<input type="text" id="name" name="Name" ng-model="site.name" class="form-control" required api-field />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="folder">Folder</label>
|
||||
<select id="folder" name="FolderId" ng-model="site.folderId" class="form-control" api-field>
|
||||
<option ng-repeat="folder in folders | orderBy: folderSort" value="{{folder.id}}">{{folder.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="username">Username</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="username" name="Username" ng-model="site.username" class="form-control" api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy Username" tooltip-placement="left">
|
||||
<button tabindex="-1" class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)"
|
||||
data-clipboard-target="#username">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group" show-errors>
|
||||
<div class="pull-right password-options">
|
||||
<i class="fa fa-lg fa-refresh" uib-tooltip="Generate Password" tooltip-placement="left" ng-click="generatePassword()"></i>
|
||||
<i class="fa fa-lg fa-eye" uib-tooltip="Toggle Password" tooltip-placement="left" password-viewer="#password"></i>
|
||||
</div>
|
||||
<label for="password">Password</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="password-text" value="{{site.password}}" style="margin-left: -9999px;" />
|
||||
<input type="password" id="password" name="Password" ng-model="site.password" class="form-control" api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy Password" tooltip-placement="left">
|
||||
<button tabindex="-1" class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-success="clipboardSuccess(e)"
|
||||
ngclipboard-error="clipboardError(e, true)"
|
||||
data-clipboard-target="#password-text">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin: -10px 0 15px 0;" password-meter="site.password" password-meter-username="site.username" outer-class="xs"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="notes">Notes</label>
|
||||
<textarea id="notes" name="Notes" class="form-control" ng-model="site.notes" api-field></textarea>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="site.favorite" name="Favorite" />
|
||||
Favorite
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="editSiteForm.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="editSiteForm.$loading"></i>Save
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
|
||||
<button type="button" class="btn btn-link pull-right" ng-click="delete()" uib-tooltip="Delete">
|
||||
<i class="fa fa-trash fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
175
src/app/views/backendLayout.html
Normal file
175
src/app/views/backendLayout.html
Normal file
@@ -0,0 +1,175 @@
|
||||
<div class="wrapper toast-target">
|
||||
<header class="main-header" ng-controller="topNavController as topNav">
|
||||
<a ui-sref="backend.vault" class="logo">
|
||||
<span class="logo-mini"><i class="fa fa-shield"></i></span>
|
||||
<span class="logo-lg"><i class="fa fa-shield"></i> <b>bit</b>warden</span>
|
||||
</a>
|
||||
<nav class="navbar navbar-static-top" role="navigation">
|
||||
<a class="sidebar-toggle" data-toggle="offcanvas" role="button">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</a>
|
||||
<div class="navbar-custom-menu">
|
||||
<ul class="nav navbar-nav">
|
||||
<li><a ui-sref="frontend.logout">Log Out</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<aside class="main-sidebar" ng-controller="sideNavController as sideNav">
|
||||
<section class="sidebar">
|
||||
<div class="user-panel">
|
||||
<div class="pull-left image">
|
||||
<img src="//www.gravatar.com/avatar/{{ main.userProfile.email | gravatar }}.jpg?s=45&d=mm"
|
||||
class="img-circle" alt="User Image">
|
||||
</div>
|
||||
<div class="pull-left info">
|
||||
<p>{{main.userProfile.extended && main.userProfile.extended.name ? main.userProfile.extended.name : main.userProfile.email}}</p>
|
||||
<a ui-sref="frontend.logout">Log Out</a>
|
||||
</div>
|
||||
</div>
|
||||
<form class="sidebar-form">
|
||||
<label for="search" class="sr-only">Search</label>
|
||||
<div class="form-group has-feedback">
|
||||
<input type="text" id="search" class="form-control" placeholder="Search vault..."
|
||||
ng-focus="searchVault()" ng-model="main.searchVaultText" />
|
||||
<span class="fa fa-search form-control-feedback" aria-hidden="true"></span>
|
||||
</div>
|
||||
</form>
|
||||
<ul class="sidebar-menu">
|
||||
<li class="header">WEB VAULT</li>
|
||||
<li class="treeview" ng-class="{active: $state.includes('backend.vault')}">
|
||||
<a ui-sref="backend.vault"><i class="fa fa-lock"></i> <span>My Vault</span></a>
|
||||
<ul class="treeview-menu menu-open">
|
||||
<li>
|
||||
<a href="javascript:void(0)" ng-click="addSite()">
|
||||
<i class="fa fa-plus"></i> New Site
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="javascript:void(0)" ng-click="addFolder()">
|
||||
<i class="fa fa-folder"></i> New Folder
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="treeview" ng-class="{active: $state.includes('backend.settings')}">
|
||||
<a ui-sref="backend.settings"><i class="fa fa-cogs"></i> <span>Settings</span></a>
|
||||
<ul class="treeview-menu">
|
||||
<li>
|
||||
<a href="javascript:void(0)" ng-click="changePassword()">
|
||||
<i class="fa fa-circle-o"></i> Change Password
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="javascript:void(0)" ng-click="changeEmail()">
|
||||
<i class="fa fa-circle-o"></i> Change Email
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="javascript:void(0)" ng-click="sessions()">
|
||||
<i class="fa fa-circle-o"></i> Deauthorize Sessions
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="javascript:void(0)" ng-click="twoFactor()">
|
||||
<i class="fa fa-circle-o"></i> Two-step Login
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="javascript:void(0)" ng-click="delete()">
|
||||
<i class="fa fa-circle-o"></i> Delete Account
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="treeview" ng-class="{active: $state.includes('backend.tools')}">
|
||||
<a ui-sref="backend.tools"><i class="fa fa-wrench"></i> <span>Tools</span></a>
|
||||
<ul class="treeview-menu">
|
||||
<li>
|
||||
<a href="javascript:void(0)" ng-click="import()">
|
||||
<i class="fa fa-circle-o"></i> Import
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="javascript:void(0)" ng-click="export()">
|
||||
<i class="fa fa-circle-o"></i> Export
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="javascript:void(0)" ng-click="audits()">
|
||||
<i class="fa fa-circle-o"></i> Audits
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://help.bitwarden.com/" target="_blank"
|
||||
analytics-on="click" analytics-event="Clicked Get Help">
|
||||
<i class="fa fa-info-circle"></i> <span>Get Help</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="header">
|
||||
<small class="label pull-right bg-green">FREE</small>
|
||||
MOBILE APPS
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://itunes.apple.com/us/app/bitwarden-free-password-manager/id1137397744?mt=8"
|
||||
target="_blank" analytics-on="click" analytics-event="Clicked iOS">
|
||||
<i class="fa fa-apple"></i> <span>iOS</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://play.google.com/store/apps/details?id=com.x8bit.bitwarden"
|
||||
target="_blank" analytics-on="click" analytics-event="Clicked Android">
|
||||
<i class="fa fa-android"></i> <span>Android</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="header">
|
||||
<small class="label pull-right bg-green">FREE</small>
|
||||
BROWSER EXTENSIONS
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb"
|
||||
target="_blank" analytics-on="click" analytics-event="Clicked Chrome">
|
||||
<i class="fa fa-chrome"></i> <span>Chrome</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="javascript:void(0)"
|
||||
target="_blank" analytics-on="click" analytics-event="Clicked Firefox">
|
||||
<small class="label pull-right bg-gray">coming very soon</small>
|
||||
<i class="fa fa-firefox"></i> <span>Firefox</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="javascript:void(0)"
|
||||
target="_blank" analytics-on="click" analytics-event="Clicked Opera">
|
||||
<small class="label pull-right bg-gray">coming very soon</small>
|
||||
<i class="fa fa-opera"></i> <span>Opera</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="javascript:void(0)"
|
||||
target="_blank" analytics-on="click" analytics-event="Clicked Edge">
|
||||
<small class="label pull-right bg-gray">coming soon</small>
|
||||
<i class="fa fa-edge"></i> <span>Edge</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<div class="content-wrapper" ui-view>
|
||||
</div>
|
||||
|
||||
<footer class="main-footer">
|
||||
<div class="pull-right hidden-xs">
|
||||
<b>Version</b> {{main.version}}
|
||||
</div>
|
||||
<strong>Copyright © <span ng-bind="currentYear"></span></strong>, bitwarden.com
|
||||
</footer>
|
||||
</div>
|
||||
2
src/app/views/frontendLayout.html
Normal file
2
src/app/views/frontendLayout.html
Normal file
@@ -0,0 +1,2 @@
|
||||
<div class="toast-target" ui-view>
|
||||
</div>
|
||||
Reference in New Issue
Block a user