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

Compare commits

...

55 Commits

Author SHA1 Message Date
Kyle Spearrin
3e18f812db lint errors 2017-02-11 18:22:13 -05:00
Kyle Spearrin
8cf02fd59a version bump 2017-02-11 17:12:50 -05:00
Kyle Spearrin
06bfab3afa version bump 2017-02-11 17:12:09 -05:00
Kyle Spearrin
71e4697562 two factor edits 2017-02-11 17:08:06 -05:00
Kyle Spearrin
cf1bffe2f1 change email button 2017-02-11 16:48:52 -05:00
Kyle Spearrin
55a5fd49dc Moved domain rules page out from modal into it's own page 2017-02-11 16:46:24 -05:00
Kyle Spearrin
3f6637eb8f move many account settings into main settings page instead of nav menu 2017-02-11 15:44:22 -05:00
Kyle Spearrin
7373e281ac print recovery code. changed vault and login route 2017-02-11 14:21:21 -05:00
Kyle Spearrin
52b89455d7 replace sjcl cryptoservice implementation with forge 2017-02-11 13:03:48 -05:00
Kyle Spearrin
bca7592c77 production by default settings 2017-02-01 22:56:39 -05:00
Kyle Spearrin
f6ab0bfe82 version bump 2017-02-01 22:53:38 -05:00
Kyle Spearrin
012a5c491d version bump 2017-02-01 22:53:15 -05:00
Kyle Spearrin
7666d6136d updated truekey importer to their new csv format 2017-02-01 22:52:36 -05:00
Kyle Spearrin
7bdda34f14 remove old auth endpoints from apiservice 2017-01-29 21:39:38 -05:00
Kyle Spearrin
df21f89fcb lint fix 2017-01-29 21:24:32 -05:00
Kyle Spearrin
f3b4cdca8a back to enum ints for 2fa providers 2017-01-28 17:27:37 -05:00
Kyle Spearrin
52460bf47b version bump 2017-01-28 17:00:26 -05:00
Kyle Spearrin
e674e7287e token refresh 2017-01-28 16:09:38 -05:00
Kyle Spearrin
a20e8b6228 fix string split bug on 1password 1pif importer 2017-01-28 02:03:49 -05:00
Kyle Spearrin
8d50e96dab splashid importer 2017-01-28 01:57:36 -05:00
Kyle Spearrin
1fe673951b WIP convert web vault to new identity server 2017-01-28 01:19:43 -05:00
Kyle Spearrin
3df5a9454e 1password importer naming adjustments 2017-01-21 02:08:14 -05:00
Kyle Spearrin
79fecd6b03 fix some linter complaints 2017-01-20 23:39:43 -05:00
Kyle Spearrin
f3445c24b9 added example placeholder text 2017-01-10 21:54:32 -05:00
Kyle Spearrin
a3150d8505 cleanup domain add/edit submission 2017-01-10 21:53:03 -05:00
Kyle Spearrin
828b5d8703 Add/edit equivalent domains 2017-01-10 21:38:53 -05:00
Kyle Spearrin
39559e203a react to model restructure on API 2017-01-10 17:01:38 -05:00
Kyle Spearrin
605bdd0ea0 domain rules page setup with new APIs 2017-01-09 22:26:20 -05:00
Kyle Spearrin
74945e03ce linter fixes 2017-01-06 00:03:33 -05:00
Kyle Spearrin
c772502af5 version bump fix for settings 2017-01-05 23:57:45 -05:00
Kyle Spearrin
401d9db0f2 version bump 2017-01-05 23:56:59 -05:00
Kyle Spearrin
2306da94fe More instructions for firefox password exporter. 2017-01-04 22:43:22 -05:00
Kyle Spearrin
9f7ed11082 fix bug from site rename 2017-01-04 22:32:47 -05:00
Kyle Spearrin
fff0efb095 append tooltips to body 2017-01-04 22:23:21 -05:00
Kyle Spearrin
9aa61f4bca complete import options array 2017-01-04 20:39:55 -05:00
Kyle Spearrin
bd70dc5966 Update README.md 2017-01-04 00:17:24 -05:00
Kyle Spearrin
ac6a3caa8f importer instructions 2017-01-04 00:11:27 -05:00
Kyle Spearrin
0914776152 re-ordered fields on site add/edit. marked name as required with asterisk 2017-01-03 22:09:26 -05:00
Kyle Spearrin
45d0f43e90 If searching, only show folder if it has filtered logins 2017-01-03 19:19:54 -05:00
Kyle Spearrin
7264367fa3 Collapse/Expand #32 2017-01-03 19:06:50 -05:00
Kyle Spearrin
022fa34478 switch back to sites enpoint until API is updated 2017-01-03 00:35:04 -05:00
Kyle Spearrin
f7fd28fded refactored naming Site => Login 2017-01-02 22:26:32 -05:00
Kyle Spearrin
e01a22de48 importer fixes 2017-01-02 21:37:20 -05:00
Kyle Spearrin
711c8e63c1 fixes to 1password4 1pif. new uri formatter. added importers for 1password6 csv, zoho vault csv, password boss json, keepassx csv, and ascendo data vault csv. 2017-01-02 18:20:42 -05:00
Kyle Spearrin
f186ec160a saferpass csv importer 2016-12-31 16:24:11 -05:00
Kyle Spearrin
53fcfd13ee folder and site count to vault list 2016-12-31 15:18:40 -05:00
Kyle Spearrin
c684d66ec0 use reference field names on clipperz importer 2016-12-31 14:52:04 -05:00
Kyle Spearrin
6496f750b0 roboform html importer 2016-12-31 14:48:56 -05:00
Kyle Spearrin
6aaa47cccd avira json importer 2016-12-31 01:00:51 -05:00
Kyle Spearrin
1f6677d610 clipperz html importer 2016-12-31 00:38:12 -05:00
Kyle Spearrin
54b659aff0 true key json importer 2016-12-29 15:33:37 -05:00
Kyle Spearrin
b9db21309e split newline for msecure notes 2016-12-29 11:41:13 -05:00
Kyle Spearrin
8649c3b2b1 msecure csv importer 2016-12-29 02:33:37 -05:00
Kyle Spearrin
6bf6cc365b sticky password importer (#1) 2016-12-28 10:36:44 -05:00
Kyle Spearrin
7c2d5448e8 catch bad data on all importers 2016-12-27 10:36:02 -05:00
39 changed files with 2398 additions and 723 deletions

View File

@@ -16,6 +16,7 @@ application to target the production API. Open `package.json` and set `productio
Then run the following commands:
- `npm install`
- `gulp build`
- `gulp serve`

View File

@@ -17,7 +17,8 @@ var gulp = require('gulp'),
settings = require('./settings.json'),
project = require('./package.json'),
jshint = require('gulp-jshint'),
_ = require('lodash');
_ = require('lodash'),
webpack = require('webpack-stream');
var paths = {};
paths.dist = './dist/';
@@ -42,7 +43,7 @@ gulp.task('lint', function () {
gulp.task('build', function (cb) {
return runSequence(
'clean',
['lib', 'less', 'settings', 'lint'],
['lib', 'webpack', 'less', 'settings', 'lint'],
cb);
});
@@ -142,10 +143,6 @@ gulp.task('lib', ['clean:lib'], function () {
src: paths.npmDir + 'angular-messages/*messages*.js',
dest: paths.libDir + 'angular-messages'
},
{
src: [paths.npmDir + 'sjcl/core/cbc.js', paths.npmDir + 'sjcl/core/bitArray.js', paths.npmDir + 'sjcl/sjcl.js'],
dest: paths.libDir + 'sjcl'
},
{
src: paths.npmDir + 'ngstorage/*.js',
dest: paths.libDir + 'ngstorage'
@@ -178,6 +175,33 @@ gulp.task('lib', ['clean:lib'], function () {
return merge(tasks);
});
gulp.task('webpack', ['webpack:forge']);
gulp.task('webpack:forge', function () {
var forgeDir = paths.npmDir + '/node-forge/lib/';
return gulp.src([
forgeDir + 'pbkdf2.js',
forgeDir + 'aes.js',
forgeDir + 'hmac.js',
forgeDir + 'sha256.js',
forgeDir + 'random.js',
forgeDir + 'forge.js'
]).pipe(webpack({
output: {
filename: 'forge.js',
library: 'forge',
libraryTarget: 'umd'
},
node: {
Buffer: false,
process: false,
crypto: false,
setImmediate: false
}
})).pipe(gulp.dest(paths.libDir + 'forge'));
});
gulp.task('settings', function () {
return config()
.pipe(gulp.dest(paths.webroot + 'app'));
@@ -290,8 +314,6 @@ gulp.task('dist:js:app', function () {
gulp.task('dist:js:lib', function () {
return gulp
.src([
paths.libDir + 'sjcl/sjcl.js',
paths.libDir + 'sjcl/*.js',
paths.libDir + 'angulartics/angulartics.js',
paths.libDir + '**/*.js',
'!' + paths.libDir + '**/*.min.js',

View File

@@ -1,7 +1,7 @@
{
"name": "bitwarden",
"version": "1.6.0",
"production": false,
"version": "1.9.0",
"production": true,
"devDependencies": {
"connect": "3.4.1",
"lodash": "4.13.1",
@@ -24,7 +24,6 @@
"jquery": "2.2.4",
"font-awesome": "4.6.3",
"bootstrap": "3.3.6",
"sjcl": "1.0.3",
"angular": "1.5.6",
"angular-resource": "1.5.6",
"angular-bootstrap-npm": "0.14.3",
@@ -42,6 +41,8 @@
"clipboard": "1.5.12",
"ngclipboard": "1.1.1",
"angulartics": "1.1.2",
"angulartics-google-analytics": "0.2.1"
"angulartics-google-analytics": "0.2.1",
"node-forge": "0.7.0",
"webpack-stream": "3.2.0"
}
}

View File

@@ -1,7 +1,8 @@
angular
.module('bit.accounts')
.controller('accountsLoginController', function ($scope, $rootScope, $cookies, apiService, cryptoService, authService, $state, appSettings, $analytics) {
.controller('accountsLoginController', function ($scope, $rootScope, $cookies, apiService, cryptoService, authService,
$state, appSettings, $analytics) {
var rememberedEmail = $cookies.get(appSettings.rememberedEmailCookieName);
if (rememberedEmail) {
$scope.model = {
@@ -10,10 +11,13 @@ angular
};
}
var email,
masterPassword;
$scope.login = function (model) {
$scope.loginPromise = authService.logIn(model.email, model.masterPassword);
$scope.loginPromise.then(function () {
$scope.loginPromise.then(function (twoFactorProviders) {
if (model.rememberEmail) {
var cookieExpiration = new Date();
cookieExpiration.setFullYear(cookieExpiration.getFullYear() + 10);
@@ -27,8 +31,10 @@ angular
$cookies.remove(appSettings.rememberedEmailCookieName);
}
var profile = authService.getUserProfile();
if (profile.twoFactor) {
if (twoFactorProviders && twoFactorProviders.length > 0) {
email = model.email;
masterPassword = model.masterPassword;
$analytics.eventTrack('Logged In To Two-step');
$state.go('frontend.login.twoFactor');
}
@@ -40,8 +46,8 @@ angular
};
$scope.twoFactor = function (model) {
// Only supporting Authenticator provider for now
$scope.twoFactorPromise = authService.logInTwoFactor(model.code, "Authenticator");
// Only supporting Authenticator (0) provider for now
$scope.twoFactorPromise = authService.logIn(email, masterPassword, model.code, 0);
$scope.twoFactorPromise.then(function () {
$analytics.eventTrack('Logged In From Two-step');

View File

@@ -2,11 +2,44 @@ 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();
jwtInterceptorProvider.urlParam = 'access_token2';
var refreshPromise;
jwtInterceptorProvider.tokenGetter = /*@ngInject*/ function (config, appSettings, tokenService, apiService, jwtHelper, $q) {
if (config.url.indexOf(appSettings.apiUri) !== 0) {
return;
}
if (refreshPromise) {
return refreshPromise;
}
var token = tokenService.getToken();
if (!token) {
return;
}
if (!tokenService.tokenNeedsRefresh(token)) {
return token;
}
var refreshToken = tokenService.getRefreshToken();
if (!refreshToken) {
return;
}
var deferred = $q.defer();
apiService.identity.token({
grant_type: 'refresh_token',
client_id: 'web',
refresh_token: refreshToken
}, function (response) {
tokenService.setToken(response.access_token);
tokenService.setRefreshToken(response.refresh_token);
refreshPromise = null;
deferred.resolve(response.access_token);
});
refreshPromise = deferred.promise;
return refreshPromise;
};
angular.extend(toastrConfig, {
@@ -17,7 +50,9 @@ angular
});
$uibTooltipProvider.options({
popupDelay: 600
popupDelay: 600,
appendToBody: true
});
if ($httpProvider.defaults.headers.post) {
@@ -41,7 +76,7 @@ angular
}
})
.state('backend.vault', {
url: '^/',
url: '^/vault',
templateUrl: 'app/vault/views/vault.html',
controller: 'vaultController',
data: { pageTitle: 'My Vault' }
@@ -52,6 +87,12 @@ angular
controller: 'settingsController',
data: { pageTitle: 'Settings' }
})
.state('backend.settingsDomains', {
url: '^/settings/domains',
templateUrl: 'app/settings/views/settingsDomains.html',
controller: 'settingsDomainsController',
data: { pageTitle: 'Domain Settings' }
})
.state('backend.tools', {
url: '^/tools',
templateUrl: 'app/tools/views/tools.html',
@@ -75,14 +116,14 @@ angular
}
})
.state('frontend.login.info', {
url: '^/login',
url: '^/',
templateUrl: 'app/accounts/views/accountsLoginInfo.html',
data: {
pageTitle: 'Log In'
}
})
.state('frontend.login.twoFactor', {
url: '^/login/two-factor',
url: '^/two-factor',
templateUrl: 'app/accounts/views/accountsLoginTwoFactor.html',
data: {
pageTitle: 'Log In (Two Factor)',
@@ -127,7 +168,7 @@ angular
.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())) {
if (authService.isAuthenticated()) {
event.preventDefault();
$state.go('backend.vault');
}
@@ -135,7 +176,7 @@ angular
return;
}
if (!authService.isAuthenticated() || jwtHelper.isTokenExpired(tokenService.getToken())) {
if (!authService.isAuthenticated()) {
event.preventDefault();
authService.logOut();
$state.go('frontend.login.info');

View File

@@ -40,34 +40,14 @@ angular
$state.go('backend.vault');
};
$scope.addSite = function () {
$scope.$broadcast('vaultAddSite');
$scope.addLogin = function () {
$scope.$broadcast('vaultAddLogin');
};
$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');
};

View File

@@ -1,11 +1,11 @@
angular
.module('bit.services')
.factory('apiService', function ($resource, tokenService, appSettings) {
.factory('apiService', function ($resource, tokenService, appSettings, $httpParamSerializer) {
var _service = {},
_apiUri = appSettings.apiUri;
_service.sites = $resource(_apiUri + '/sites/:id', {}, {
_service.logins = $resource(_apiUri + '/sites/:id', {}, {
get: { method: 'GET', params: { id: '@id' } },
list: { method: 'GET', params: {} },
post: { method: 'POST', params: {} },
@@ -36,6 +36,8 @@
putPassword: { url: _apiUri + '/accounts/password', method: 'POST', params: {} },
getProfile: { url: _apiUri + '/accounts/profile', method: 'GET', params: {} },
putProfile: { url: _apiUri + '/accounts/profile', method: 'POST', params: {} },
getDomains: { url: _apiUri + '/accounts/domains', method: 'GET', params: {} },
putDomains: { url: _apiUri + '/accounts/domains', method: 'POST', params: {} },
getTwoFactor: { url: _apiUri + '/accounts/two-factor', method: 'GET', params: {} },
putTwoFactor: { url: _apiUri + '/accounts/two-factor', method: 'POST', params: {} },
postTwoFactorRecover: { url: _apiUri + '/accounts/two-factor-recover', method: 'POST', params: {} },
@@ -45,10 +47,25 @@
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: {} }
_service.settings = $resource(_apiUri + '/settings', {}, {
getDomains: { url: _apiUri + '/settings/domains', method: 'GET', params: {} },
putDomains: { url: _apiUri + '/settings/domains', method: 'POST', params: {} },
});
_service.identity = $resource(_apiUri + '/connect', {}, {
token: {
url: _apiUri + '/connect/token',
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' },
transformRequest: transformUrlEncoded,
skipAuthorization: true,
params: {}
}
});
function transformUrlEncoded(data) {
return $httpParamSerializer(data);
}
return _service;
});

View File

@@ -5,51 +5,42 @@ angular
var _service = {},
_userProfile = null;
_service.logIn = function (email, masterPassword) {
_service.logIn = function (email, masterPassword, token, provider) {
email = email.toLowerCase();
var key = cryptoService.makeKey(masterPassword, email);
var request = {
email: email,
masterPasswordHash: cryptoService.hashPassword(masterPassword, key)
username: email,
password: cryptoService.hashPassword(masterPassword, key),
grant_type: 'password',
scope: 'api offline_access',
client_id: 'web'
};
if (token && typeof (provider) !== 'undefined' && provider !== null) {
request.twoFactorToken = token.replace(' ', '');
request.twoFactorProvider = provider;
}
// TODO: device information one day?
var deferred = $q.defer();
apiService.auth.token(request, function (response) {
if (!response || !response.Token) {
apiService.identity.token(request, function (response) {
if (!response || !response.access_token) {
return;
}
tokenService.setToken(response.Token);
tokenService.setToken(response.access_token);
tokenService.setRefreshToken(response.refresh_token);
cryptoService.setKey(key);
_service.setUserProfile(response.Profile);
deferred.resolve(response);
deferred.resolve();
}, 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;
if (error.status === 400 && error.data.TwoFactorProviders && error.data.TwoFactorProviders.length) {
deferred.resolve(error.data.TwoFactorProviders);
}
else {
deferred.reject(error);
}
tokenService.setToken(response.Token);
_service.setUserProfile(response.Profile);
deferred.resolve(response);
}, function (error) {
deferred.reject(error);
});
return deferred.promise;
@@ -57,6 +48,7 @@ angular
_service.logOut = function () {
tokenService.clearToken();
tokenService.clearRefreshToken();
cryptoService.clearKey();
_userProfile = null;
};
@@ -69,27 +61,20 @@ angular
return _userProfile;
};
_service.setUserProfile = function (profile) {
_service.setUserProfile = function () {
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
id: decodedToken.name,
email: decodedToken.email
};
if (!twoFactor && profile) {
loadProfile(profile);
}
else if (!twoFactor && !profile) {
apiService.accounts.getProfile({}, loadProfile);
}
apiService.accounts.getProfile({}, loadProfile);
};
function loadProfile(profile) {
@@ -101,11 +86,7 @@ angular
}
_service.isAuthenticated = function () {
return _service.getUserProfile() !== null && !_service.getUserProfile().twoFactor;
};
_service.isTwoFactorAuthenticated = function () {
return _service.getUserProfile() !== null && _service.getUserProfile().twoFactor;
return tokenService.getToken() !== null;
};
return _service;

View File

@@ -4,39 +4,39 @@ angular
.factory('cipherService', function (cryptoService, apiService) {
var _service = {};
_service.decryptSites = function (encryptedSites) {
if (!encryptedSites) throw "encryptedSites is undefined or null";
_service.decryptLogins = function (encryptedLogins) {
if (!encryptedLogins) throw "encryptedLogins is undefined or null";
var unencryptedSites = [];
for (var i = 0; i < encryptedSites.length; i++) {
unencryptedSites.push(_service.decryptSite(encryptedSites[i]));
var unencryptedLogins = [];
for (var i = 0; i < encryptedLogins.length; i++) {
unencryptedLogins.push(_service.decryptLogin(encryptedLogins[i]));
}
return unencryptedSites;
return unencryptedLogins;
};
_service.decryptSite = function (encryptedSite) {
if (!encryptedSite) throw "encryptedSite is undefined or null";
_service.decryptLogin = function (encryptedLogin) {
if (!encryptedLogin) throw "encryptedLogin is undefined or null";
var site = {
id: encryptedSite.Id,
var login = {
id: encryptedLogin.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
folderId: encryptedLogin.FolderId,
favorite: encryptedLogin.Favorite,
name: cryptoService.decrypt(encryptedLogin.Name),
uri: encryptedLogin.Uri && encryptedLogin.Uri !== '' ? cryptoService.decrypt(encryptedLogin.Uri) : null,
username: encryptedLogin.Username && encryptedLogin.Username !== '' ? cryptoService.decrypt(encryptedLogin.Username) : null,
password: encryptedLogin.Password && encryptedLogin.Password !== '' ? cryptoService.decrypt(encryptedLogin.Password) : null,
notes: encryptedLogin.Notes && encryptedLogin.Notes !== '' ? cryptoService.decrypt(encryptedLogin.Notes) : null
};
if (encryptedSite.Folder) {
site.folder = {
name: cryptoService.decrypt(encryptedSite.Folder.Name)
if (encryptedLogin.Folder) {
login.folder = {
name: cryptoService.decrypt(encryptedLogin.Folder.Name)
};
}
return site;
return login;
};
_service.decryptFolders = function (encryptedFolders) {
@@ -60,30 +60,30 @@ angular
};
};
_service.encryptSites = function (unencryptedSites, key) {
if (!unencryptedSites) throw "unencryptedSites is undefined or null";
_service.encryptLogins = function (unencryptedLogins, key) {
if (!unencryptedLogins) throw "unencryptedLogins is undefined or null";
var encryptedSites = [];
for (var i = 0; i < unencryptedSites.length; i++) {
encryptedSites.push(_service.encryptSite(unencryptedSites[i], key));
var encryptedLogins = [];
for (var i = 0; i < unencryptedLogins.length; i++) {
encryptedLogins.push(_service.encryptLogin(unencryptedLogins[i], key));
}
return encryptedSites;
return encryptedLogins;
};
_service.encryptSite = function (unencryptedSite, key) {
if (!unencryptedSite) throw "unencryptedSite is undefined or null";
_service.encryptLogin = function (unencryptedLogin, key) {
if (!unencryptedLogin) throw "unencryptedLogin is undefined or null";
return {
id: unencryptedSite.id,
id: unencryptedLogin.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)
folderId: unencryptedLogin.folderId === '' ? null : unencryptedLogin.folderId,
favorite: unencryptedLogin.favorite !== null ? unencryptedLogin.favorite : false,
uri: !unencryptedLogin.uri || unencryptedLogin.uri === '' ? null : cryptoService.encrypt(unencryptedLogin.uri, key),
name: cryptoService.encrypt(unencryptedLogin.name, key),
username: !unencryptedLogin.username || unencryptedLogin.username === '' ? null : cryptoService.encrypt(unencryptedLogin.username, key),
password: !unencryptedLogin.password || unencryptedLogin.password === '' ? null : cryptoService.encrypt(unencryptedLogin.password, key),
notes: !unencryptedLogin.notes || unencryptedLogin.notes === '' ? null : cryptoService.encrypt(unencryptedLogin.notes, key)
};
};

View File

@@ -4,15 +4,11 @@ angular
.factory('cryptoService', function ($sessionStorage) {
var _service = {},
_key,
_b64Key,
_aes,
_aesWithMac;
sjcl.beware["CBC mode is dangerous because it doesn't protect message integrity."]();
_b64Key;
_service.setKey = function (key) {
_key = key;
$sessionStorage.key = sjcl.codec.base64.fromBits(key);
$sessionStorage.key = forge.util.encode64(key);
};
_service.getKey = function (b64) {
@@ -24,11 +20,11 @@ angular
}
if ($sessionStorage.key) {
_key = sjcl.codec.base64.toBits($sessionStorage.key);
_key = forge.util.decode64($sessionStorage.key);
}
if (b64 && b64 === true) {
_b64Key = sjcl.codec.base64.fromBits(_key);
_b64Key = forge.util.encode64(_key);
return _b64Key;
}
@@ -37,24 +33,29 @@ angular
_service.getEncKey = function (key) {
key = key || _service.getKey();
return key.slice(0, 4);
var buffer = forge.util.createBuffer(key);
return buffer.getBytes(16);
};
_service.getMacKey = function (key) {
key = key || _service.getKey();
return key.slice(4);
var buffer = forge.util.createBuffer(key);
buffer.getBytes(16); // skip first half
return buffer.getBytes(16);
};
_service.clearKey = function () {
_key = _b64Key = _aes = _aesWithMac = null;
_key = _b64Key = null;
delete $sessionStorage.key;
};
_service.makeKey = function (password, salt, b64) {
var key = sjcl.misc.pbkdf2(password, salt, 5000, 256, null);
var key = forge.pbkdf2(password, salt, 5000, 256 / 8, 'sha256');
if (b64 && b64 === true) {
return sjcl.codec.base64.fromBits(key);
return forge.util.encode64(key);
}
return key;
@@ -69,24 +70,8 @@ angular
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.getAesWithMac = function () {
if (!_aesWithMac && _service.getKey()) {
_aesWithMac = new sjcl.cipher.aes(_service.getEncKey());
}
return _aesWithMac;
var hashBits = forge.pbkdf2(key, password, 1, 256 / 8, 'sha256');
return forge.util.encode64(hashBits);
};
_service.encrypt = function (plaintextValue, key) {
@@ -103,22 +88,21 @@ angular
encKey = key || _service.getKey();
}
var response = {};
var params = {
mode: 'cbc',
iv: sjcl.random.randomWords(4, 10)
};
var ctJson = sjcl.encrypt(encKey, plaintextValue, params, response);
var ct = ctJson.match(/"ct":"([^"]*)"/)[1];
var iv = sjcl.codec.base64.fromBits(response.iv);
var buffer = forge.util.createBuffer(plaintextValue, 'utf8');
var ivBytes = forge.random.getBytesSync(16);
var cipher = forge.cipher.createCipher('AES-CBC', encKey);
cipher.start({ iv: ivBytes });
cipher.update(buffer);
cipher.finish();
var iv = forge.util.encode64(ivBytes);
var ctBytes = cipher.output.getBytes();
var ct = forge.util.encode64(ctBytes);
var cipherString = iv + '|' + ct;
// TODO: Turn on whenever ready to support encrypt-then-mac
if (false) {
var mac = computeMac(ct, response.iv);
var mac = computeMac(ctBytes, ivBytes);
cipherString = cipherString + '|' + mac;
}
@@ -126,7 +110,7 @@ angular
};
_service.decrypt = function (encValue) {
if (!_service.getAes() || !_service.getAesWithMac()) {
if (!_service.getKey()) {
throw 'AES encryption unavailable.';
}
@@ -135,35 +119,33 @@ angular
return '';
}
var ivBits = sjcl.codec.base64.toBits(encPieces[0]);
var ctBits = sjcl.codec.base64.toBits(encPieces[1]);
var ivBytes = forge.util.decode64(encPieces[0]);
var ctBytes = forge.util.decode64(encPieces[1]);
var computedMac = null;
if (encPieces.length === 3) {
computedMac = computeMac(ctBits, ivBits);
computedMac = computeMac(ctBytes, ivBytes);
if (computedMac !== encPieces[2]) {
console.error('MAC failed.');
return '';
}
}
var decBits = sjcl.mode.cbc.decrypt(computedMac ? _service.getAesWithMac() : _service.getAes(), ctBits, ivBits, null);
return sjcl.codec.utf8String.fromBits(decBits);
var ctBuffer = forge.util.createBuffer(ctBytes);
var decipher = forge.cipher.createDecipher('AES-CBC', computedMac ? _service.getEncKey() : _service.getKey());
decipher.start({ iv: ivBytes });
decipher.update(ctBuffer);
decipher.finish();
return decipher.output.toString('utf8');
};
function computeMac(ct, iv) {
if (typeof ct === 'string') {
ct = sjcl.codec.base64.toBits(ct);
}
if (typeof iv === 'string') {
iv = sjcl.codec.base64.toBits(iv);
}
var macKey = _service.getMacKey();
var hmac = new sjcl.misc.hmac(macKey, sjcl.hash.sha256);
var bits = iv.concat(ct);
var mac = hmac.encrypt(bits);
return sjcl.codec.base64.fromBits(mac);
function computeMac(ct, iv, macKey) {
var hmac = forge.hmac.create();
hmac.start('sha256', macKey || _service.getMacKey());
hmac.update(iv + ct);
var mac = hmac.digest();
return forge.util.encode64(mac.getBytes());
}
return _service;

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,62 @@
angular
.module('bit.services')
.factory('tokenService', function ($sessionStorage) {
.factory('tokenService', function ($sessionStorage, jwtHelper) {
var _service = {},
_token;
_token = null,
_refreshToken = null;
_service.setToken = function (token) {
$sessionStorage.authBearer = token;
$sessionStorage.accessToken = token;
_token = token;
};
_service.getToken = function () {
if (!_token) {
_token = $sessionStorage.authBearer;
_token = $sessionStorage.accessToken;
}
return _token;
return _token ? _token : null;
};
_service.clearToken = function () {
_token = null;
delete $sessionStorage.authBearer;
delete $sessionStorage.accessToken;
};
_service.setRefreshToken = function (token) {
$sessionStorage.refreshToken = token;
_refreshToken = token;
};
_service.getRefreshToken = function () {
if (!_refreshToken) {
_refreshToken = $sessionStorage.refreshToken;
}
return _refreshToken ? _refreshToken : null;
};
_service.clearRefreshToken = function () {
_refreshToken = null;
delete $sessionStorage.refreshToken;
};
_service.tokenSecondsRemaining = function (token, offsetSeconds) {
var d = jwtHelper.getTokenExpirationDate(token);
offsetSeconds = offsetSeconds || 0;
if (d === null) {
return 0;
}
var msRemaining = d.valueOf() - (new Date().valueOf() + (offsetSeconds * 1000));
return Math.round(msRemaining / 1000);
};
_service.tokenNeedsRefresh = function (token, minutes) {
minutes = minutes || 5; // default 5 minutes
var sRemaining = _service.tokenSecondsRemaining(token);
return sRemaining < (60 * minutes);
};
return _service;

View File

@@ -14,6 +14,10 @@
return;
}
if (data && data.ErrorModel) {
data = data.ErrorModel;
}
if (!data.ValidationErrors) {
if (data.Message) {
form.$errors.push(data.Message);

View File

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

View File

@@ -0,0 +1,18 @@
angular
.module('bit.vault')
.controller('settingsAddEditEquivalentDomainController', function ($scope, $uibModalInstance, $analytics, domainIndex, domains) {
$analytics.eventTrack('settingsAddEditEquivalentDomainController', { category: 'Modal' });
$scope.domains = domains;
$scope.index = domainIndex;
$scope.submit = function (form) {
$analytics.eventTrack((domainIndex ? 'Edited' : 'Added') + ' Equivalent Domain');
$uibModalInstance.close({ domains: $scope.domains, index: domainIndex });
};
$scope.close = function () {
$uibModalInstance.dismiss('close');
};
});

View File

@@ -27,10 +27,10 @@
$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);
var reencryptedLogins = [];
var loginsPromise = apiService.logins.list({ dirty: false }, function (encryptedLogins) {
var unencryptedLogins = cipherService.decryptLogins(encryptedLogins.Data);
reencryptedLogins = cipherService.encryptLogins(unencryptedLogins, _newKey);
}).$promise;
var reencryptedFolders = [];
@@ -39,13 +39,13 @@
reencryptedFolders = cipherService.encryptFolders(unencryptedFolders, _newKey);
}).$promise;
$q.all([sitesPromise, foldersPromise]).then(function () {
$q.all([loginsPromise, foldersPromise]).then(function () {
var request = {
token: model.token,
newEmail: model.newEmail.toLowerCase(),
masterPasswordHash: _masterPasswordHash,
newMasterPasswordHash: _newMasterPasswordHash,
ciphers: reencryptedSites.concat(reencryptedFolders)
ciphers: reencryptedLogins.concat(reencryptedFolders)
};
$scope.confirmPromise = apiService.accounts.email(request, function () {

View File

@@ -27,10 +27,10 @@
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);
var reencryptedLogins = [];
var loginsPromise = apiService.logins.list({ dirty: false }, function (encryptedLogins) {
var unencryptedLogins = cipherService.decryptLogins(encryptedLogins.Data);
reencryptedLogins = cipherService.encryptLogins(unencryptedLogins, newKey);
}).$promise;
var reencryptedFolders = [];
@@ -39,11 +39,11 @@
reencryptedFolders = cipherService.encryptFolders(unencryptedFolders, newKey);
}).$promise;
$q.all([sitesPromise, foldersPromise]).then(function () {
$q.all([loginsPromise, foldersPromise]).then(function () {
var request = {
masterPasswordHash: cryptoService.hashPassword(model.masterPassword),
newMasterPasswordHash: cryptoService.hashPassword(model.newMasterPassword, newKey),
ciphers: reencryptedSites.concat(reencryptedFolders)
ciphers: reencryptedLogins.concat(reencryptedFolders)
};
$scope.savePromise = apiService.accounts.putPassword(request, function () {

View File

@@ -2,20 +2,33 @@
.module('bit.settings')
.controller('settingsController', function ($scope, $uibModal, apiService, toastr, authService) {
$scope.model = {};
$scope.model = {
profile: {},
twoFactorEnabled: false,
email: null
};
apiService.accounts.getProfile({}, function (user) {
$scope.model = {
name: user.Name,
profile: {
name: user.Name,
masterPasswordHint: user.MasterPasswordHint,
culture: user.Culture
},
email: user.Email,
masterPasswordHint: user.MasterPasswordHint,
culture: user.Culture,
twoFactorEnabled: user.TwoFactorEnabled
};
});
$scope.save = function (model) {
$scope.savePromise = apiService.accounts.putProfile({}, model, function (profile) {
$scope.generalSave = function () {
$scope.generalPromise = apiService.accounts.putProfile({}, $scope.model.profile, function (profile) {
authService.setUserProfile(profile);
toastr.success('Account has been updated.', 'Success!');
}).$promise;
};
$scope.passwordHintSave = function () {
$scope.passwordHintPromise = apiService.accounts.putProfile({}, $scope.model.profile, function (profile) {
authService.setUserProfile(profile);
toastr.success('Account has been updated.', 'Success!');
}).$promise;
@@ -29,10 +42,6 @@
});
};
$scope.$on('settingsChangePassword', function (event, args) {
$scope.changePassword();
});
$scope.changeEmail = function () {
$uibModal.open({
animation: true,
@@ -42,21 +51,21 @@
});
};
$scope.$on('settingsChangeEmail', function (event, args) {
$scope.changeEmail();
});
$scope.twoFactor = function () {
$uibModal.open({
var twoFactorModal = $uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsTwoFactor.html',
controller: 'settingsTwoFactorController'
});
};
$scope.$on('settingsTwoFactor', function (event, args) {
$scope.twoFactor();
});
twoFactorModal.result.then(function (enabled) {
if (enabled === null) {
return;
}
$scope.model.twoFactorEnabled = enabled;
});
};
$scope.sessions = function () {
$uibModal.open({
@@ -66,10 +75,6 @@
});
};
$scope.$on('settingsSessions', function (event, args) {
$scope.sessions();
});
$scope.delete = function () {
$uibModal.open({
animation: true,
@@ -78,8 +83,4 @@
size: 'sm'
});
};
$scope.$on('settingsDelete', function (event, args) {
$scope.delete();
});
});

View File

@@ -0,0 +1,101 @@
angular
.module('bit.settings')
.controller('settingsDomainsController', function ($scope, $state, apiService, toastr, $analytics, $uibModal) {
$scope.globalEquivalentDomains = [];
$scope.equivalentDomains = [];
apiService.settings.getDomains({}, function (response) {
var i;
if (response.EquivalentDomains) {
for (i = 0; i < response.EquivalentDomains.length; i++) {
$scope.equivalentDomains.push(response.EquivalentDomains[i].join(', '));
}
}
if (response.GlobalEquivalentDomains) {
for (i = 0; i < response.GlobalEquivalentDomains.length; i++) {
$scope.globalEquivalentDomains.push({
domains: response.GlobalEquivalentDomains[i].Domains.join(', '),
excluded: response.GlobalEquivalentDomains[i].Excluded,
key: response.GlobalEquivalentDomains[i].Type
});
}
}
});
$scope.toggleExclude = function (globalDomain) {
globalDomain.excluded = !globalDomain.excluded;
};
$scope.customize = function (globalDomain) {
globalDomain.excluded = true;
$scope.equivalentDomains.push(globalDomain.domains);
};
$scope.delete = function (i) {
$scope.equivalentDomains.splice(i, 1);
};
$scope.addEdit = function (i) {
var addEditModal = $uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsAddEditEquivalentDomain.html',
controller: 'settingsAddEditEquivalentDomainController',
resolve: {
domainIndex: function () { return i; },
domains: function () { return i !== null ? $scope.equivalentDomains[i] : null; }
}
});
addEditModal.result.then(function (returnObj) {
if (returnObj.domains) {
returnObj.domains = returnObj.domains.split(' ').join('').split(',').join(', ');
}
if (returnObj.index !== null) {
$scope.equivalentDomains[returnObj.index] = returnObj.domains;
}
else {
$scope.equivalentDomains.push(returnObj.domains);
}
});
};
$scope.saveGlobal = function () {
$scope.globalPromise = save();
};
$scope.saveCustom = function () {
$scope.customPromise = save();
};
var save = function () {
var request = {
ExcludedGlobalEquivalentDomains: [],
EquivalentDomains: []
};
for (var i = 0; i < $scope.globalEquivalentDomains.length; i++) {
if ($scope.globalEquivalentDomains[i].excluded) {
request.ExcludedGlobalEquivalentDomains.push($scope.globalEquivalentDomains[i].key);
}
}
for (i = 0; i < $scope.equivalentDomains.length; i++) {
request.EquivalentDomains.push($scope.equivalentDomains[i].split(' ').join('').split(','));
}
if (!request.EquivalentDomains.length) {
request.EquivalentDomains = null;
}
if (!request.ExcludedGlobalEquivalentDomains.length) {
request.ExcludedGlobalEquivalentDomains = null;
}
return apiService.settings.putDomains(request, function (domains) {
toastr.success('Domains have been updated.', 'Success!');
}).$promise;
};
});

View File

@@ -74,7 +74,15 @@
}).$promise;
};
$scope.print = function (printContent) {
var w = window.open();
w.document.write('<div style="font-size: 18px; text-align: center;"><p>bitwarden two-step login recovery code:</p>' +
'<pre>' + printContent + '</pre>');
w.print();
w.close();
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
$uibModalInstance.close(!_profile.extended ? null : _profile.extended.twoFactorEnabled);
};
});

View File

@@ -9,33 +9,28 @@
<div class="box-header with-border">
<h3 class="box-title">General</h3>
</div>
<form role="form" name="profileForm" ng-submit="profileForm.$valid && save(model)" api-form="savePromise">
<form role="form" name="generalForm" ng-submit="generalForm.$valid && generalSave()" api-form="generalPromise">
<div class="box-body">
<div class="row">
<div class="col-sm-9">
<div class="callout callout-danger validation-errors" ng-show="profileForm.$errors">
<div class="callout callout-danger validation-errors" ng-show="generalForm.$errors">
<h4>Errors have occured</h4>
<ul>
<li ng-repeat="e in profileForm.$errors">{{e}}</li>
<li ng-repeat="e in generalForm.$errors">{{e}}</li>
</ul>
</div>
<div class="form-group" show-errors>
<label for="name">Name</label>
<input type="text" id="name" name="Name" ng-model="model.name" class="form-control"
<input type="text" id="name" name="Name" ng-model="model.profile.name" class="form-control"
required api-field />
</div>
<div class="form-group">
<label for="email">Email - <a href="javascript:void(0)" ng-click="changeEmail()">change</a></label>
<input type="text" id="email" ng-model="model.email" class="form-control" readonly />
</div>
<div class="form-group" show-errors>
<label for="hint">Master Password Hint</label>
<input type="text" id="hint" name="MasterPasswordHint" ng-model="model.masterPasswordHint"
class="form-control" api-field />
</div>
<div class="form-group" show-errors>
<label for="culture">Language/Culture</label>
<select id="culture" name="Culture" ng-model="model.culture" class="form-control" api-field>
<select id="culture" name="Culture" ng-model="model.profile.culture" class="form-control" api-field>
<option value="en-US">English (US)</option>
</select>
</div>
@@ -51,10 +46,83 @@
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="profileForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="profileForm.$loading"></i>Save
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="generalForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="generalForm.$loading"></i>Save
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="changeEmail()">
Change Email
</button>
</div>
</form>
</div>
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Master Password</h3>
</div>
<form role="form" name="masterPasswordForm" ng-submit="masterPasswordForm.$valid && passwordHintSave()"
api-form="passwordHintPromise">
<div class="box-body">
<div class="row">
<div class="col-sm-9">
<div class="callout callout-danger validation-errors" ng-show="masterPasswordForm.$errors">
<h4>Errors have occured</h4>
<ul>
<li ng-repeat="e in masterPasswordForm.$errors">{{e}}</li>
</ul>
</div>
<div class="form-group" show-errors>
<label for="hint">Master Password Hint</label>
<input type="text" id="hint" name="MasterPasswordHint" ng-model="model.profile.masterPasswordHint"
class="form-control" api-field />
</div>
</div>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="masterPasswordForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="masterPasswordForm.$loading"></i>Save
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="changePassword()">
Change Master Password
</button>
</div>
</form>
</div>
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Two-step Log In</h3>
</div>
<div class="box-body">
<p>
Current Status:
<span class="label bg-green" ng-show="model.twoFactorEnabled">ENABLED</span>
<span class="label bg-gray" ng-show="!model.twoFactorEnabled">DISABLED</span>
</p>
<p>
Two-step login helps keep your account more secure by requiring a code provided by an app on your mobile device
while logging in (in addition to your master password).
</p>
</div>
<div class="box-footer">
<button type="button" class="btn btn-default btn-flat" ng-click="twoFactor()">
Manage Two-step Log In
</button>
</div>
</div>
<div class="box box-danger">
<div class="box-header with-border">
<h3 class="box-title">Danger Zone</h3>
</div>
<div class="box-body">
Careful, these actions are not reversible!
</div>
<div class="box-footer">
<button type="submit" class="btn btn-default btn-flat" ng-click="sessions()">
Deauthorize Sessions
</button>
<button type="submit" class="btn btn-default btn-flat" ng-click="delete()">
Delete Account
</button>
</div>
</div>
</section>

View File

@@ -0,0 +1,35 @@
<div class="modal-header">
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><i class="fa fa-globe"></i> {{index ? 'Edit Equivalent Domain' : 'Add Equivalent Domain'}}</h4>
</div>
<form name="domainAddEditForm" ng-submit="domainAddEditForm.$valid && submit(domainAddEditForm)">
<div class="modal-body">
<div class="callout callout-danger validation-errors" ng-show="domainAddEditForm.$errors">
<h4>Errors have occured</h4>
<ul>
<li ng-repeat="e in domainAddEditForm.$errors">{{e}}</li>
</ul>
</div>
<p>
Enter a list of domains separated by commas.
</p>
<div class="form-group" show-errors>
<label for="name">Domains</label> <span>*</span>
<textarea id="domains" name="Domains" ng-model="domains" class="form-control" placeholder="ex. google.com, gmail.com"
style="height: 100px;" required></textarea>
<p class="help-block">
Only "base" domains are allowed. Do not enter subdomains. For example, enter "google.com" instead of
"www.google.com".
</p>
<p class="help-block">
You can also enter "androidapp://package.name" to associate an android app with other website domains.
</p>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-flat">
Submit
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
</div>
</form>

View File

@@ -0,0 +1,94 @@
<section class="content-header">
<h1>Domain Rules</h1>
</section>
<section class="content">
<p>
If you have the same login across multiple different website domains, you can mark the website as "equivalent".
"Global" domains are ones already created for you by bitwarden.
</p>
<form name="customForm" ng-submit="customForm.$valid && saveCustom()" api-form="customPromise">
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Custom Equivalent Domains</h3>
<div class="box-tools pull-right">
<button type="button" class="btn btn-box-tool" ng-click="addEdit(null)">
<i class="fa fa-plus"></i> Add New
</button>
</div>
</div>
<div class="box-body no-padding">
<div class="table-responsive">
<table class="table table-striped table-hover">
<tbody ng-if="equivalentDomains.length">
<tr ng-repeat="customDomain in equivalentDomains track by $index">
<td style="width: 80px; min-width: 80px;">
<button type="button" class="btn btn-link btn-table" uib-tooltip="Edit" ng-click="addEdit($index)">
<i class="fa fa-lg fa-pencil"></i>
</button>
<button type="button" class="btn btn-link btn-table" uib-tooltip="Delete" ng-click="delete($index)">
<i class="fa fa-lg fa-trash"></i>
</button>
</td>
<td>{{customDomain}}</td>
</tr>
</tbody>
<tbody ng-if="!equivalentDomains.length">
<tr>
<td>No domains to list.</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="customForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="customForm.$loading"></i>Save
</button>
</div>
</div>
</form>
<form name="globalForm" ng-submit="globalForm.$valid && saveGlobal()" api-form="globalPromise">
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Global Equivalent Domains</h3>
</div>
<div class="box-body no-padding">
<div class="table-responsive">
<table class="table table-striped table-hover">
<tbody ng-if="globalEquivalentDomains.length">
<tr ng-repeat="globalDomain in globalEquivalentDomains">
<td style="width: 80px; min-width: 80px;">
<button type="button" class="btn btn-link btn-table" uib-tooltip="Exclude"
ng-if="!globalDomain.excluded" ng-click="toggleExclude(globalDomain)">
<i class="fa fa-lg fa-ban"></i>
</button>
<button type="button" class="btn btn-link btn-table" uib-tooltip="Include"
ng-if="globalDomain.excluded" ng-click="toggleExclude(globalDomain)">
<i class="fa fa-lg fa-plus"></i>
</button>
<button type="button" class="btn btn-link btn-table" uib-tooltip="Customize"
ng-click="customize(globalDomain)">
<i class="fa fa-lg fa-cut"></i>
</button>
</td>
<td ng-class="{strike: globalDomain.excluded}">{{globalDomain.domains}}</td>
</tr>
</tbody>
<tbody ng-if="!globalEquivalentDomains.length">
<tr>
<td>No domains to list.</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="globalForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="globalForm.$loading"></i>Save
</button>
</div>
</div>
</form>
</section>

View File

@@ -1,13 +1,11 @@
<div class="modal-header">
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="twoFactorModelLabel"><i class="fa fa-key"></i> Two-step Login</h4>
<h4 class="modal-title" id="twoFactorModelLabel"><i class="fa fa-key"></i> Two-step Log In</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>
<p ng-show="enabled()">Two-step log in 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 log in enter your master password below.</p>
<div class="callout callout-danger validation-errors" ng-show="authTwoStepForm.$errors">
<h4>Errors have occured</h4>
<ul>
@@ -29,7 +27,7 @@
<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>Two-step log in 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()">
@@ -58,17 +56,18 @@
</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 class="callout callout-danger">
<h4><i class="fa fa-warning"></i> Recovery Code <i class="fa fa-warning"></i></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>
<p><strong>Recovery Code:</strong> <code>{{twoFactorModel.recovery}}</code></p>
<button type="button" class="btn btn-default" ng-click="print(twoFactorModel.recovery)">
<i class="fa fa-print"></i> Print
</button>
</div>
</div>
<div class="callout callout-danger validation-errors" ng-show="updateTwoStepForm.$errors">
<h4>Errors have occured</h4>
@@ -77,12 +76,12 @@
</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>
<h4 style="margin-top: 30px;"><span ng-show="enabled()">Want to disable? </span><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>
<p ng-show="!enabled()">NOTE: After enabling two-step log in, 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">

View File

@@ -6,8 +6,7 @@
$uibModal.open({
animation: true,
templateUrl: 'app/tools/views/toolsImport.html',
controller: 'toolsImportController',
size: 'sm'
controller: 'toolsImportController'
});
};

View File

@@ -5,25 +5,25 @@
$analytics.eventTrack('toolsExportController', { category: 'Modal' });
$scope.export = function (model) {
$scope.startedExport = true;
apiService.sites.list({ expand: ['folder'] }, function (sites) {
apiService.logins.list({ expand: ['folder'] }, function (logins) {
try {
var decSites = cipherService.decryptSites(sites.Data);
var decLogins = cipherService.decryptLogins(logins.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
var exportLogins = [];
for (var i = 0; i < decLogins.length; i++) {
var login = {
name: decLogins[i].name,
uri: decLogins[i].uri,
username: decLogins[i].username,
password: decLogins[i].password,
notes: decLogins[i].notes,
folder: decLogins[i].folder ? decLogins[i].folder.name : null
};
exportSites.push(site);
exportLogins.push(login);
}
var csvString = Papa.unparse(exportSites);
var csvString = Papa.unparse(exportLogins);
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());

View File

@@ -1,9 +1,206 @@
angular
.module('bit.tools')
.controller('toolsImportController', function ($scope, $state, apiService, $uibModalInstance, cryptoService, cipherService, toastr, importService, $analytics) {
.controller('toolsImportController', function ($scope, $state, apiService, $uibModalInstance, cryptoService, cipherService, toastr, importService, $analytics, $sce) {
$analytics.eventTrack('toolsImportController', { category: 'Modal' });
$scope.model = { source: 'local' };
$scope.model = { source: 'bitwardencsv' };
$scope.source = {};
$scope.options = [
{
id: 'bitwardencsv',
name: 'bitwarden (csv)',
instructions: $sce.trustAsHtml('Export using the web vault (vault.bitwarden.com). ' +
'Log into the web vault and navigate to "Tools" > "Export".')
},
{
id: 'lastpass',
name: 'LastPass (csv)',
instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' +
'<a target="_blank" href="https://help.bitwarden.com/getting-started/import-from-lastpass/">' +
'https://help.bitwarden.com/getting-started/import-from-lastpass/</a>')
},
{
id: 'chromecsv',
name: 'Chrome (csv)',
instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' +
'<a target="_blank" href="https://help.bitwarden.com/getting-started/import-from-chrome/">' +
'https://help.bitwarden.com/getting-started/import-from-chrome/</a>')
},
{
id: 'firefoxpasswordexportercsvxml',
name: 'Firefox Password Exporter (xml)',
instructions: $sce.trustAsHtml('Use the ' +
'<a target="_blank" href="https://addons.mozilla.org/en-US/firefox/addon/password-exporter/">' +
'Password Exporter</a> addon for FireFox to export your passwords to a XML file. After installing ' +
'the addon, type <code>about:addons</code> in your FireFox navigation bar. Locate the Password Exporter ' +
'addon and click the "Options" button. In the dialog that pops up, click the "Export Passwords" button ' +
'to save the XML file.')
},
{
id: 'keepass2xml',
name: 'KeePass 2 (xml)',
instructions: $sce.trustAsHtml('Using the KeePass 2 desktop application, navigate to "File" > "Export" and ' +
'select the KeePass XML (2.x) option.')
},
{
id: 'keepassxcsv',
name: 'KeePassX (csv)',
instructions: $sce.trustAsHtml('Using the KeePassX desktop application, navigate to "Database" > ' +
'"Export to CSV file" and save the CSV file.')
},
{
id: 'dashlanecsv',
name: 'Dashlane (csv)',
instructions: $sce.trustAsHtml('Using the Dashlane desktop application, navigate to "File" > "Export" > ' +
'"Unsecured archive (readable) in CSV format" and save the CSV file.')
},
{
id: '1password1pif',
name: '1Password (1pif)',
instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' +
'<a target="_blank" href="https://help.bitwarden.com/getting-started/import-from-1password/">' +
'https://help.bitwarden.com/getting-started/import-from-1password/</a>')
},
{
id: '1password6wincsv',
name: '1Password 6 Windows (csv)',
instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' +
'<a target="_blank" href="https://help.bitwarden.com/getting-started/import-from-1password/">' +
'https://help.bitwarden.com/getting-started/import-from-1password/</a>')
},
{
id: 'roboformhtml',
name: 'RoboForm (html)',
instructions: $sce.trustAsHtml('Using the RoboForm Editor desktop application, navigate to "RoboForm" ' +
'(top left) > "Print List" > "Logins". When the following print dialog pops up click on the "Save" button ' +
'and save the HTML file.')
},
{
id: 'keepercsv',
name: 'Keeper (csv)',
instructions: $sce.trustAsHtml('Log into the Keeper web vault (keepersecurity.com/vault). Navigate to "Backup" ' +
'(top right) and find the "Export to Text File" option. Click "Export Now" to save the TXT/CSV file.')
},
{
id: 'enpasscsv',
name: 'Enpass (csv)',
instructions: $sce.trustAsHtml('Using the Enpass desktop application, navigate to "File" > "Export" > ' +
'"As CSV". Select "Yes" to the warning alert and save the CSV file.')
},
{
id: 'safeincloudxml',
name: 'SafeInCloud (xml)',
instructions: $sce.trustAsHtml('Using the SaveInCloud desktop application, navigate to "File" > "Export" > ' +
'"As XML" and save the XML file.')
},
{
id: 'pwsafexml',
name: 'Password Safe (xml)',
instructions: $sce.trustAsHtml('Using the Password Safe desktop application, navigate to "File" > ' +
'"Export To" > "XML format..." and save the XML file.')
},
{
id: 'stickypasswordxml',
name: 'Sticky Password (xml)',
instructions: $sce.trustAsHtml('Using the Sticky Password desktop application, navigate to "Menu" ' +
'(top right) > "Export" > "Export all". Select the unencrypted format XML option and then the ' +
'"Save to file" button. Save the XML file.')
},
{
id: 'msecurecsv',
name: 'mSecure (csv)',
instructions: $sce.trustAsHtml('Using the mSecure desktop application, navigate to "File" > ' +
'"Export" > "CSV File..." and save the CSV file.')
},
{
id: 'truekeycsv',
name: 'True Key (csv)',
instructions: $sce.trustAsHtml('Using the True Key desktop application, click the gear icon (top right) and ' +
'then navigate to "App Settings". Click the "Export" button, enter your password and save the CSV file.')
},
{
id: 'passwordbossjson',
name: 'Password Boss (json)',
instructions: $sce.trustAsHtml('Using the Password Boss desktop application, navigate to "File" > ' +
'"Export data" > "Password Boss JSON - not encrypted" and save the JSON file.')
},
{
id: 'zohovaultcsv',
name: 'Zoho Vault (csv)',
instructions: $sce.trustAsHtml('Log into the Zoho web vault (vault.zoho.com). Navigate to "Tools" > ' +
'"Export Secrets". Select "All Secrets" and click the "Zoho Vault Format CSV" button. Highlight ' +
'and copy the data from the textarea. Open a text editor like Notepad and paste the data. Save the ' +
'data from the text editor as <code>zoho_export.csv</code>.')
},
{
id: 'splashidcsv',
name: 'SplashID (csv)',
instructions: $sce.trustAsHtml('Using the SplashID Safe desktop application, click on the SplashID ' +
'blue lock logo in the top right corner. Navigate to "Export" > "Export as CSV" and save the CSV file.')
},
{
id: 'passworddragonxml',
name: 'Password Dragon (xml)',
instructions: $sce.trustAsHtml('Using the Password Dragon desktop application, navigate to "File" > ' +
'"Export" > "To XML". In the dialog that pops up select "All Rows" and check all fields. Click ' +
'the "Export" button and save the XML file.')
},
{
id: 'padlockcsv',
name: 'Padlock (csv)',
instructions: $sce.trustAsHtml('Using the Padlock desktop application, click the hamburger icon ' +
'in the top left corner and navigate to "Settings". Click the "Export Data" option. Ensure that ' +
'the "CSV" option is selected from the dropdown. Highlight and copy the data from the textarea. ' +
'Open a text editor like Notepad and paste the data. Save the data from the text editor as ' +
'<code>padlock_export.csv</code>.')
},
{
id: 'clipperzhtml',
name: 'Clipperz (html)',
instructions: $sce.trustAsHtml('Log into the Clipperz web application (clipperz.is/app). Click the ' +
'hamburger menu icon in the top right to expand the navigation bar. Navigate to "Data" > ' +
'"Export". Click the "download HTML+JSON" button to save the HTML file.')
},
{
id: 'avirajson',
name: 'Avira (json)',
instructions: $sce.trustAsHtml('Using the Avira browser extension, click your username in the top ' +
'right corner and navigate to "Settings". Locate the "Export Data" section and click "Export". ' +
'In the dialog that pops up, click the "Export Password Manager Data" button to save the ' +
'TXT/JSON file.')
},
{
id: 'saferpasscsv',
name: 'SaferPass (csv)',
instructions: $sce.trustAsHtml('Using the SaferPass browser extension, click the hamburger icon ' +
'in the top left corner and navigate to "Settings". Click the "Export accounts" button to ' +
'save the CSV file.')
},
{
id: 'upmcsv',
name: 'Universal Password Manager (csv)',
instructions: $sce.trustAsHtml('Using the Universal Password Manager desktop application, navigate ' +
'to "Database" > "Export" and save the CSV file.')
},
{
id: 'ascendocsv',
name: 'Ascendo DataVault (csv)',
instructions: $sce.trustAsHtml('Using the Ascendo DataVault desktop application, navigate ' +
'to "Tools" > "Export". In the dialog that pops up, select the "All Items (DVX, CSV)" ' +
'option. Click the "Ok" button to save the CSV file.')
}
];
$scope.setSource = function () {
for (var i = 0; i < $scope.options.length; i++) {
if ($scope.options[i].id === $scope.model.source) {
$scope.source = $scope.options[i];
break;
}
}
};
$scope.setSource();
$scope.import = function (model) {
$scope.processing = true;
@@ -11,16 +208,23 @@
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.');
function importSuccess(folders, logins, folderRelationships) {
if (!folders.length && !logins.length) {
importError('Nothing was imported.');
return;
}
else if (logins.length) {
var halfway = Math.floor(logins.length / 2);
var last = logins.length - 1;
if (loginIsBadData(logins[0]) && loginIsBadData(logins[halfway]) && loginIsBadData(logins[last])) {
importError('CSV data is not formatted correctly. Please check your import file and try again.');
return;
}
}
apiService.ciphers.import({
folders: cipherService.encryptFolders(folders, cryptoService.getKey()),
sites: cipherService.encryptSites(sites, cryptoService.getKey()),
logins: cipherService.encryptLogins(logins, cryptoService.getKey()),
folderRelationships: folderRelationships
}, function () {
$uibModalInstance.dismiss('cancel');
@@ -31,6 +235,10 @@
}, importError);
}
function loginIsBadData(login) {
return (login.name === null || login.name === '--') && (login.password === null || login.password === '');
}
function importError(error) {
$analytics.eventTrack('Import Data Failed', { label: $scope.model.source });
$uibModalInstance.dismiss('cancel');

View File

@@ -6,24 +6,14 @@
<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="dashlanecsv">Dashlane (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>
<option value="enpasscsv">Enpass (csv)</option>
<option value="pwsafexml">Password Safe (xml)</option>
<select id="source" name="source" class="form-control" ng-model="model.source" ng-change="setSource()">
<option ng-repeat="option in options" value="{{option.id}}">{{option.name}}</option>
</select>
</div>
<div class="callout callout-default">
<h4><i class="fa fa-info-circle"></i> {{source.name}} Instructions</h4>
<div ng-bind-html="source.instructions"></div>
</div>
<div class="form-group">
<label for="file">File</label>
<input type="file" id="file" name="file" required />

View File

@@ -1,27 +1,27 @@
angular
.module('bit.vault')
.controller('vaultAddSiteController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, passwordService, folders, selectedFolder, $analytics) {
$analytics.eventTrack('vaultAddSiteController', { category: 'Modal' });
.controller('vaultAddLoginController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, passwordService, folders, selectedFolder, $analytics) {
$analytics.eventTrack('vaultAddLoginController', { category: 'Modal' });
$scope.folders = folders;
$scope.site = {
$scope.login = {
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);
var login = cipherService.encryptLogin(model);
$scope.savePromise = apiService.logins.post(login, function (loginResponse) {
$analytics.eventTrack('Created Login');
var decLogin = cipherService.decryptLogin(loginResponse);
$uibModalInstance.close(decLogin);
}).$promise;
};
$scope.generatePassword = function () {
if (!$scope.site.password || confirm('Are you sure you want to overwrite the current password?')) {
if (!$scope.login.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.login.password = passwordService.generatePassword({ length: 12, special: true });
}
};

View File

@@ -2,35 +2,35 @@
.module('bit.vault')
.controller('vaultController', function ($scope, $uibModal, apiService, $filter, cryptoService, authService, toastr, cipherService) {
$scope.sites = [];
$scope.logins = [];
$scope.folders = [];
$scope.loadingSites = true;
apiService.sites.list({}, function (sites) {
$scope.loadingSites = false;
$scope.loadingLogins = true;
apiService.logins.list({}, function (logins) {
$scope.loadingLogins = 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
var decLogins = [];
for (var i = 0; i < logins.Data.length; i++) {
var decLogin = {
id: logins.Data[i].Id,
folderId: logins.Data[i].FolderId,
favorite: logins.Data[i].Favorite
};
try { decSite.name = cryptoService.decrypt(sites.Data[i].Name); }
catch (err) { decSite.name = '[error: cannot decrypt]'; }
try { decLogin.name = cryptoService.decrypt(logins.Data[i].Name); }
catch (err) { decLogin.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]'; }
if (logins.Data[i].Username) {
try { decLogin.username = cryptoService.decrypt(logins.Data[i].Username); }
catch (err) { decLogin.username = '[error: cannot decrypt]'; }
}
decSites.push(decSite);
decLogins.push(decLogin);
}
$scope.sites = decSites;
$scope.logins = decLogins;
}, function () {
$scope.loadingSites = false;
$scope.loadingLogins = false;
});
$scope.loadingFolders = true;
@@ -66,69 +66,69 @@
return item.name.toLowerCase();
};
$scope.editSite = function (site) {
$scope.editLogin = function (login) {
var editModel = $uibModal.open({
animation: true,
templateUrl: 'app/vault/views/vaultEditSite.html',
controller: 'vaultEditSiteController',
templateUrl: 'app/vault/views/vaultEditLogin.html',
controller: 'vaultEditLoginController',
resolve: {
siteId: function () { return site.id; },
loginId: function () { return login.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);
var loginToUpdate = $filter('filter')($scope.logins, { 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;
if (loginToUpdate && loginToUpdate.length > 0) {
loginToUpdate[0].folderId = returnVal.data.folderId;
loginToUpdate[0].name = returnVal.data.name;
loginToUpdate[0].username = returnVal.data.username;
loginToUpdate[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]);
var loginToDelete = $filter('filter')($scope.logins, { id: returnVal.data }, true);
if (loginToDelete && loginToDelete.length > 0) {
var index = $scope.logins.indexOf(loginToDelete[0]);
if (index > -1) {
$scope.sites.splice(index, 1);
$scope.logins.splice(index, 1);
}
}
}
});
};
$scope.$on('vaultAddSite', function (event, args) {
$scope.addSite();
$scope.$on('vaultAddLogin', function (event, args) {
$scope.addLogin();
});
$scope.addSite = function (folder) {
$scope.addLogin = function (folder) {
var addModel = $uibModal.open({
animation: true,
templateUrl: 'app/vault/views/vaultAddSite.html',
controller: 'vaultAddSiteController',
templateUrl: 'app/vault/views/vaultAddLogin.html',
controller: 'vaultAddLoginController',
resolve: {
folders: function () { return $scope.folders; },
selectedFolder: function () { return folder; }
}
});
addModel.result.then(function (addedSite) {
$scope.sites.push(addedSite);
addModel.result.then(function (addedLogin) {
$scope.logins.push(addedLogin);
});
};
$scope.deleteSite = function (site) {
if (!confirm('Are you sure you want to delete this site (' + site.name + ')?')) {
$scope.deleteLogin = function (login) {
if (!confirm('Are you sure you want to delete this login (' + login.name + ')?')) {
return;
}
apiService.sites.del({ id: site.id }, function () {
var index = $scope.sites.indexOf(site);
apiService.logins.del({ id: login.id }, function () {
var index = $scope.logins.indexOf(login);
if (index > -1) {
$scope.sites.splice(index, 1);
$scope.logins.splice(index, 1);
}
});
};
@@ -187,7 +187,7 @@
return false;
}
var sites = $filter('filter')($scope.sites, { folderId: folder.id });
return sites.length === 0;
var logins = $filter('filter')($scope.logins, { folderId: folder.id });
return logins.length === 0;
};
});

View File

@@ -1,31 +1,31 @@
angular
.module('bit.vault')
.controller('vaultEditSiteController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, passwordService, siteId, folders, $analytics) {
$analytics.eventTrack('vaultEditSiteController', { category: 'Modal' });
.controller('vaultEditLoginController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, passwordService, loginId, folders, $analytics) {
$analytics.eventTrack('vaultEditLoginController', { category: 'Modal' });
$scope.folders = folders;
$scope.site = {};
$scope.login = {};
apiService.sites.get({ id: siteId }, function (site) {
$scope.site = cipherService.decryptSite(site);
apiService.logins.get({ id: loginId }, function (login) {
$scope.login = cipherService.decryptLogin(login);
});
$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);
var login = cipherService.encryptLogin(model);
$scope.savePromise = apiService.logins.put({ id: loginId }, login, function (loginResponse) {
$analytics.eventTrack('Edited Login');
var decLogin = cipherService.decryptLogin(loginResponse);
$uibModalInstance.close({
action: 'edit',
data: decSite
data: decLogin
});
}).$promise;
};
$scope.generatePassword = function () {
if (!$scope.site.password || confirm('Are you sure you want to overwrite the current password?')) {
if (!$scope.login.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.login.password = passwordService.generatePassword({ length: 12, special: true });
}
};
@@ -53,14 +53,14 @@
}
$scope.delete = function () {
if (!confirm('Are you sure you want to delete this site (' + $scope.site.name + ')?')) {
if (!confirm('Are you sure you want to delete this login (' + $scope.login.name + ')?')) {
return;
}
apiService.sites.del({ id: $scope.site.id }, function () {
apiService.logins.del({ id: $scope.login.id }, function () {
$uibModalInstance.close({
action: 'delete',
data: $scope.site.id
data: $scope.login.id
});
});
};

View File

@@ -1,18 +1,18 @@
<section class="content-header">
<h1>
My Vault
<small>safe and secure</small>
<small>{{folders.length > 0 ? folders.length - 1 : 0}} folders, {{logins.length}} logins</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" ng-repeat="folder in folders | orderBy: folderSort" ng-show="folders.length && (!main.searchVaultText || folderLogins.length)">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-folder-open"></i> {{folder.name}}</h3>
<h3 class="box-title"><i class="fa fa-folder-open"></i> {{folder.name}} <small>{{folderLogins.length}} logins</small></h3>
<div class="box-tools pull-right">
<button type="button" class="btn btn-box-tool" ng-click="addSite(folder)" uib-tooltip="Add Site">
<button type="button" class="btn btn-box-tool" ng-click="addLogin(folder)" uib-tooltip="Add Login">
<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">
@@ -21,36 +21,36 @@
<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">
<button type="button" class="btn btn-box-tool" data-widget="collapse" uib-tooltip="Collapse/Expand">
<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 class="box-body" ng-class="{'no-padding': folderLogins.length}">
<div ng-show="loadingLogins && !folderLogins.length">
<p>Loading logins...</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 ng-show="!loadingLogins && !folderLogins.length">
<p>No logins in this folder.</p>
<button type="button" ng-click="addLogin(folder)" class="btn btn-default btn-flat">Add a Login</button>
</div>
<div class="table-responsive" ng-show="folderSites.length">
<div class="table-responsive" ng-show="folderLogins.length">
<table class="table table-striped table-hover">
<thead>
<tr>
<th style="width: 75px; min-width: 75px;"></th>
<th>Site</th>
<th>Name</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'])">
<tr ng-repeat="login in folderLogins = (logins | 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>
<button type="button" ng-click="deleteLogin(login)" class="btn btn-link btn-table" uib-tooltip="Delete"><i class="fa fa-lg fa-trash"></i></button>
<button type="button" ng-click="editLogin(login)" 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>
<td>{{login.name}} <i class="fa fa-star text-muted" uib-tooltip="Favorite" ng-show="login.favorite"></i></td>
<td>{{login.username}}</td>
</tr>
</tbody>
</table>

View File

@@ -11,7 +11,7 @@
</ul>
</div>
<div class="form-group" show-errors>
<label for="name">Name</label>
<label for="name">Name</label> <span>*</span>
<input type="text" id="name" name="Name" ng-model="folder.name" class="form-control" required api-field />
</div>
</div>

View File

@@ -1,19 +1,35 @@
<div class="modal-header">
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="addSiteModelLabel"><i class="fa fa-globe"></i> Add New Site</h4>
<h4 class="modal-title" id="addLoginModelLabel"><i class="fa fa-globe"></i> Add New Login</h4>
</div>
<form name="addSiteForm" ng-submit="addSiteForm.$valid && save(site)" api-form="savePromise">
<form name="addLoginForm" ng-submit="addLoginForm.$valid && save(login)" api-form="savePromise">
<div class="modal-body">
<div class="callout callout-danger validation-errors" ng-show="addSiteForm.$errors">
<div class="callout callout-danger validation-errors" ng-show="addLoginForm.$errors">
<h4>Errors have occured</h4>
<ul>
<li ng-repeat="e in addSiteForm.$errors">{{e}}</li>
<li ng-repeat="e in addLoginForm.$errors">{{e}}</li>
</ul>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group" show-errors>
<label for="name">Name</label> <span>*</span>
<input type="text" id="name" name="Name" ng-model="login.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="login.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="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 />
<input type="text" id="uri" ng-model="login.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)"
@@ -23,28 +39,12 @@
</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 />
<input type="text" id="username" name="Username" ng-model="login.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)"
@@ -63,8 +63,8 @@
</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 />
<input tabindex="-1" type="text" id="password-text" value="{{login.password}}" style="margin-left: -9999px;" />
<input type="password" id="password" name="Password" ng-model="login.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)"
@@ -75,23 +75,23 @@
</span>
</div>
</div>
<div style="margin: -10px 0 15px 0;" password-meter="site.password" password-meter-username="site.username" outer-class="xs"></div>
<div style="margin: -10px 0 15px 0;" password-meter="login.password" password-meter-username="login.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>
<textarea id="notes" name="Notes" class="form-control" ng-model="login.notes" api-field></textarea>
</div>
<div class="checkbox">
<label>
<input type="checkbox" ng-model="site.favorite" name="Favorite" />
<input type="checkbox" ng-model="login.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 type="submit" class="btn btn-primary btn-flat" ng-disabled="addLoginForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="addLoginForm.$loading"></i>Submit
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
</div>

View File

@@ -11,7 +11,7 @@
</ul>
</div>
<div class="form-group" show-errors>
<label for="name">Name</label>
<label for="name">Name</label> <span>*</span>
<input type="text" id="name" name="Name" ng-model="folder.name" class="form-control" required api-field />
</div>
</div>

View File

@@ -1,53 +1,53 @@
<div class="modal-header">
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="editSiteModelLabel"><i class="fa fa-globe"></i> Site Information <small>{{site.name}}</small></h4>
<h4 class="modal-title" id="editLoginModelLabel"><i class="fa fa-globe"></i> Login Information <small>{{login.name}}</small></h4>
</div>
<form name="editSiteForm" ng-submit="editSiteForm.$valid && save(site)" api-form="savePromise">
<form name="editLoginForm" ng-submit="editLoginForm.$valid && save(login)" api-form="savePromise">
<div class="modal-body">
<div class="callout callout-danger validation-errors" ng-show="editSiteForm.$errors">
<div class="callout callout-danger validation-errors" ng-show="editLoginForm.$errors">
<h4>Errors have occured</h4>
<ul>
<li ng-repeat="e in editSiteForm.$errors">{{e}}</li>
<li ng-repeat="e in editLoginForm.$errors">{{e}}</li>
</ul>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group" show-errors>
<label for="name">Name</label> <span>*</span>
<input type="text" id="name" name="Name" ng-model="login.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="login.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="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 />
<input type="text" id="uri" name="Uri" ng-model="login.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">
<a href="{{login.uri}}" target="_blank" class="btn btn-default btn-flat" uib-tooltip="Go To Login" 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 />
<input type="text" id="username" name="Username" ng-model="login.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)"
@@ -66,8 +66,8 @@
</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 />
<input type="text" id="password-text" value="{{login.password}}" style="margin-left: -9999px;" />
<input type="password" id="password" name="Password" ng-model="login.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)"
@@ -78,23 +78,23 @@
</span>
</div>
</div>
<div style="margin: -10px 0 15px 0;" password-meter="site.password" password-meter-username="site.username" outer-class="xs"></div>
<div style="margin: -10px 0 15px 0;" password-meter="login.password" password-meter-username="login.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>
<textarea id="notes" name="Notes" class="form-control" ng-model="login.notes" api-field></textarea>
</div>
<div class="checkbox">
<label>
<input type="checkbox" ng-model="site.favorite" name="Favorite" />
<input type="checkbox" ng-model="login.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 type="submit" class="btn btn-primary btn-flat" ng-disabled="editLoginForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="editLoginForm.$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">

View File

@@ -45,8 +45,8 @@
<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 href="javascript:void(0)" ng-click="addLogin()">
<i class="fa fa-plus"></i> New Login
</a>
</li>
<li>
@@ -56,32 +56,13 @@
</li>
</ul>
</li>
<li class="treeview" ng-class="{active: $state.includes('backend.settings')}">
<li class="treeview"
ng-class="{active: $state.includes('backend.settings') || $state.includes('backend.settingsDomains')}">
<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 ui-sref="backend.settingsDomains">
<i class="fa fa-circle-o"></i> Domain Rules
</a>
</li>
</ul>

View File

@@ -55,10 +55,7 @@
<script src="lib/bootstrap/js/bootstrap.js"></script>
<script src="lib/admin-lte/js/app.js"></script>
<script src="lib/sjcl/sjcl.js"></script>
<script src="lib/sjcl/cbc.js"></script>
<script src="lib/sjcl/bitArray.js"></script>
<script src="lib/forge/forge.js"></script>
<script src="lib/papaparse/papaparse.js"></script>
<script src="lib/clipboard/clipboard.js"></script>
@@ -114,8 +111,8 @@
<script src="app/vault/vaultModule.js"></script>
<script src="app/vault/vaultController.js"></script>
<script src="app/vault/vaultEditSiteController.js"></script>
<script src="app/vault/vaultAddSiteController.js"></script>
<script src="app/vault/vaultEditLoginController.js"></script>
<script src="app/vault/vaultAddLoginController.js"></script>
<script src="app/vault/vaultEditFolderController.js"></script>
<script src="app/vault/vaultAddFolderController.js"></script>
@@ -125,6 +122,8 @@
<script src="app/settings/settingsChangeEmailController.js"></script>
<script src="app/settings/settingsTwoFactorController.js"></script>
<script src="app/settings/settingsSessionsController.js"></script>
<script src="app/settings/settingsDomainsController.js"></script>
<script src="app/settings/settingsAddEditEquivalentDomainController.js"></script>
<script src="app/settings/settingsDeleteController.js"></script>
<script src="app/tools/toolsModule.js"></script>

View File

@@ -112,6 +112,23 @@ form .btn .loading-icon {
text-align: left;
}
/* Callouts */
.callout.callout-default {
&:extend(.bg-gray-light);
border-color: darken(@gray, 10%);
a {
color: @link-color;
}
a:hover,
a:active,
a:focus {
color: @link-hover-color;
}
}
/* Toastr */
#toast-container {
@@ -182,3 +199,10 @@ form .btn .loading-icon {
}
}
}
/* Misc */
.strike {
text-decoration: line-through;
color: @text-muted;
}