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

Compare commits

...

57 Commits

Author SHA1 Message Date
Kyle Spearrin
16930aa422 version bump 2017-10-26 22:12:42 -04:00
Kyle Spearrin
263f5ba147 monospaced fonts on certain input fields 2017-10-26 11:37:38 -04:00
Kyle Spearrin
6a60c00e22 added note about english for enpass 2017-10-26 11:24:53 -04:00
Kyle Spearrin
f3eaf644b0 purge vault 2017-10-25 21:46:35 -04:00
Kyle Spearrin
a57110b935 lint fixes 2017-10-25 16:01:04 -04:00
Kyle Spearrin
cae8beaa8f default cipher type data objects 2017-10-25 15:45:33 -04:00
Kyle Spearrin
df94d81d07 handle null condition 2017-10-25 12:38:55 -04:00
Kyle Spearrin
f03c22cc07 tax information 2017-10-25 12:21:46 -04:00
Kyle Spearrin
5b31fe37f2 border same as bg 2017-10-25 00:49:49 -04:00
Kyle Spearrin
c60a596995 invoice link for charges 2017-10-25 00:47:07 -04:00
Kyle Spearrin
b52ecd8085 icons url for self hosted instances 2017-10-23 18:11:29 -04:00
Kyle Spearrin
4323341d19 attachments indicator in org vault 2017-10-23 16:23:32 -04:00
Kyle Spearrin
e13992ba27 web vault options 2017-10-23 16:07:41 -04:00
Kyle Spearrin
52a4317d09 add option to disable website icons in web vault 2017-10-23 16:06:55 -04:00
Kyle Spearrin
d53187935b only use icon images if not self hosted 2017-10-23 15:35:46 -04:00
Kyle Spearrin
0d6c96e38b update importers for cipher types & fields 2017-10-23 14:50:19 -04:00
Kyle Spearrin
b0832578a4 handle logins & notes for generic export/import 2017-10-23 12:40:42 -04:00
Kyle Spearrin
805393b4db null check refresh promise 2017-10-19 21:20:32 -04:00
Kyle Spearrin
c3653577c6 fix bug with only showing selected collections 2017-10-19 21:18:45 -04:00
Kyle Spearrin
1eb5a99ba3 make sure uri has . in it before prefixing http 2017-10-18 15:54:42 -04:00
Kyle Spearrin
a035d73545 max-height 2017-10-17 11:27:58 -04:00
Kyle Spearrin
79fc3056a6 re-order car brands 2017-10-12 23:37:05 -04:00
Kyle Spearrin
e44cf6e7ee return error when rejecting 2017-10-12 23:35:58 -04:00
Kyle Spearrin
641c76ae62 overflow y on control-sidebar sections 2017-10-12 22:42:12 -04:00
Kyle Spearrin
1efcd69148 dont hide overflow 2017-10-12 17:18:13 -04:00
Kyle Spearrin
49ee41f7d3 process notes for cards and identity from lastpass 2017-10-12 17:01:34 -04:00
Kyle Spearrin
598c7ea068 update listing when cipher is edited 2017-10-12 15:48:30 -04:00
Kyle Spearrin
001a116c8b generic notes fix 2017-10-12 14:27:45 -04:00
Kyle Spearrin
106e71fe54 import updates
- converted logins to ciphers up to 1password csv
- started secure notes support for lastpasss
2017-10-12 14:24:08 -04:00
Kyle Spearrin
cd93d6cc32 icons for filters 2017-10-12 10:59:01 -04:00
Kyle Spearrin
d63c89bae7 new icon path 2017-10-12 10:23:03 -04:00
Yash Shah
fb3a7733a3 Add semicolon and remove unneeded comma (#108) 2017-10-12 08:29:32 -04:00
Kyle Spearrin
852363cb77 import/export/updatekey fixes for ciphers 2017-10-11 16:41:09 -04:00
Kyle Spearrin
7f6ee21a8e renaming org vault logins to ciphers 2017-10-11 15:54:47 -04:00
Kyle Spearrin
2963516d5c more logins to cipher renames 2017-10-11 09:57:18 -04:00
Kyle Spearrin
1f26ff5c80 round the icons 2017-10-11 09:46:04 -04:00
Kyle Spearrin
de3f310082 renaming logins to ciphers in move 2017-10-11 09:45:52 -04:00
Kyle Spearrin
4af2edafd3 set login icon function 2017-10-11 09:35:59 -04:00
Kyle Spearrin
4de08f2e71 bitwarden vault 2017-10-11 09:26:18 -04:00
Kyle Spearrin
d978e1dfa3 favicon updates 2017-10-10 22:56:04 -04:00
Kyle Spearrin
f828288b84 icons for vault listing 2017-10-10 21:55:58 -04:00
Kyle Spearrin
7a36f13034 convert share from logins to ciphers 2017-10-09 15:54:21 -04:00
Kyle Spearrin
422b48fa36 added additional fields to identity 2017-10-09 11:00:41 -04:00
Kyle Spearrin
fe9e29a057 cipher type icons 2017-10-09 10:42:26 -04:00
Kyle Spearrin
88c302ca2e cipher type forms 2017-10-09 10:06:44 -04:00
Kyle Spearrin
52f3032483 fixes for item filtering in vault 2017-10-09 08:20:58 -04:00
Kyle Spearrin
b13edfeeae adjust height of notes field 2017-10-07 21:57:00 -04:00
Kyle Spearrin
4046339569 filter cipher list by type 2017-10-07 21:48:02 -04:00
Kyle Spearrin
52f4a9d961 show all types in listing 2017-10-07 21:28:15 -04:00
Kyle Spearrin
ca0fb6d66a convert add login to ciphers 2017-10-07 14:20:28 -04:00
Kyle Spearrin
7c93c82d24 shared vault listing conversion to ciphers 2017-10-07 13:45:33 -04:00
Kyle Spearrin
3b71760f9e convert vault listing to ciphers 2017-10-06 22:01:17 -04:00
Kyle Spearrin
c4d2045884 convert edit to generic ciphers 2017-10-06 21:24:04 -04:00
Kyle Spearrin
d28c59544f encrypt/decrypt ciphers 2017-10-06 21:23:14 -04:00
Kyle Spearrin
acff0b19d6 adjusted build script 2017-10-04 16:17:00 -04:00
Kyle Spearrin
94bfcb2865 version bump 2017-10-03 22:29:42 -04:00
Kyle Spearrin
1bb6244337 on alter token header if not self hosted 2017-10-03 22:29:01 -04:00
73 changed files with 3363 additions and 1673 deletions

View File

@@ -5,18 +5,12 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
echo ""
if [ $# -gt 0 -a "$1" == "push" ]
if [ $# -gt 1 -a "$1" == "push" ]
then
echo "# Pushing Web"
TAG=$2
echo "# Pushing Web ($TAG)"
echo ""
if [ $# -gt 1 ]
then
TAG=$2
docker push bitwarden/web:$TAG
else
docker push bitwarden/web
fi
docker push bitwarden/web:$TAG
elif [ $# -gt 1 -a "$1" == "tag" ]
then
TAG=$2

1
dist/.publish vendored Submodule

Submodule dist/.publish added at 856b11bf95

View File

@@ -25,7 +25,7 @@ var gulp = require('gulp'),
var paths = {};
paths.dist = './dist/';
paths.webroot = './src/'
paths.webroot = './src/';
paths.js = paths.webroot + 'js/**/*.js';
paths.minJs = paths.webroot + 'js/**/*.min.js';
paths.concatJsDest = paths.webroot + 'js/bw.min.js';
@@ -304,7 +304,7 @@ gulp.task('dist:move', function () {
src: [
paths.npmDir + 'bootstrap/dist/**/bootstrap.min.js',
paths.npmDir + 'bootstrap/dist/**/bootstrap.min.css',
paths.npmDir + 'bootstrap/dist/**/fonts/**/*',
paths.npmDir + 'bootstrap/dist/**/fonts/**/*'
],
dest: paths.dist + 'lib/bootstrap'
},

View File

@@ -1,6 +1,6 @@
{
"name": "bitwarden",
"version": "1.17.3",
"version": "1.19.0",
"env": "Production",
"devDependencies": {
"connect": "3.6.3",

View File

@@ -2,6 +2,7 @@
"appSettings": {
"apiUri": "https://preview-api.bitwarden.com",
"identityUri": "https://preview-identity.bitwarden.com",
"iconsUri": "https://icons.bitwarden.com",
"stripeKey": "pk_test_KPoCfZXu7mznb9uSCPZ2JpTD",
"braintreeKey": "sandbox_r72q8jq6_9pnxkwm75f87sdc2",
"whitelistDomains": [

View File

@@ -2,6 +2,7 @@
"appSettings": {
"apiUri": "https://api.bitwarden.com",
"identityUri": "https://identity.bitwarden.com",
"iconsUri": "https://icons.bitwarden.com",
"stripeKey": "pk_live_bpN0P37nMxrMQkcaHXtAybJk",
"braintreeKey": "production_qfbsv8kc_njj2zjtyngtjmbjd",
"whitelistDomains": [

View File

@@ -2,6 +2,7 @@
"appSettings": {
"apiUri": "http://localhost:4000",
"identityUri": "http://localhost:33656",
"iconsUri": "https://icons.bitwarden.com",
"stripeKey": "pk_test_KPoCfZXu7mznb9uSCPZ2JpTD",
"braintreeKey": "sandbox_r72q8jq6_9pnxkwm75f87sdc2",
"whitelistDomains": [

View File

@@ -4,7 +4,9 @@ angular
.config(function ($stateProvider, $urlRouterProvider, $httpProvider, jwtInterceptorProvider, jwtOptionsProvider,
$uibTooltipProvider, toastrConfig, $locationProvider, $qProvider, appSettings
// @if !selfHosted
/* jshint ignore:start */
, stripeProvider
/* jshint ignore:end */
// @endif
) {
angular.extend(appSettings, window.bitwardenAppSettings);
@@ -13,19 +15,20 @@ angular
$locationProvider.hashPrefix('');
var jwtConfig = {
// Using Content-Language header since it is unused and is a CORS-safelisted header. This avoids pre-flights.
authHeader: 'Content-Language',
whiteListedDomains: appSettings.whitelistDomains
};
// Safari doesn't work with unconventional "Content-Language" header for CORS.
// See notes here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
var userAgent = navigator.userAgent.toLowerCase();
if (userAgent.indexOf('safari') > -1 && userAgent.indexOf('chrome') === -1) {
jwtConfig = {
urlParam: 'access_token',
whiteListedDomains: appSettings.whitelistDomains
};
if (!appSettings.selfHosted) {
var userAgent = navigator.userAgent.toLowerCase();
if (userAgent.indexOf('safari') > -1 && userAgent.indexOf('chrome') === -1) {
// Safari doesn't work with unconventional "Content-Language" header for CORS.
// See notes here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
jwtConfig.urlParam = 'access_token';
}
else {
// Using Content-Language header since it is unused and is a CORS-safelisted header. This avoids pre-flights.
jwtConfig.authHeader = 'Content-Language';
}
}
jwtOptionsProvider.config(jwtConfig);
@@ -48,7 +51,12 @@ angular
return token;
}
refreshPromise = authService.refreshAccessToken().then(function (newToken) {
var p = authService.refreshAccessToken();
if (!p) {
return;
}
refreshPromise = p.then(function (newToken) {
refreshPromise = null;
return newToken || token;
});
@@ -367,7 +375,7 @@ angular
// user is guaranteed to be authenticated becuase of previous check
if (toState.name.indexOf('backend.org.') > -1 && toParams.orgId) {
// clear vault rootScope when visiting org admin section
$rootScope.vaultLogins = $rootScope.vaultFolders = null;
$rootScope.vaultCiphers = $rootScope.vaultFolders = null;
authService.getUserProfile().then(function (profile) {
var orgs = profile.organizations;

View File

@@ -28,6 +28,17 @@ angular.module('bit')
email: 1,
remember: 5
},
cipherType: {
login: 1,
secureNote: 2,
card: 3,
identity: 4
},
fieldType: {
text: 0,
hidden: 1,
boolean: 2
},
twoFactorProviderInfo: [
{
type: 0,

View File

@@ -0,0 +1,11 @@
angular
.module('bit.directives')
.directive('fallbackSrc', function () {
return function (scope, element, attrs) {
var el = $(element);
el.bind('error', function (event) {
el.attr('src', attrs.fallbackSrc);
});
};
});

View File

@@ -49,16 +49,16 @@ angular
vm.openControlSidebar = vm.usingControlSidebar && $document.width() > 768;
});
$scope.addLogin = function () {
$scope.$broadcast('vaultAddLogin');
$scope.addCipher = function () {
$scope.$broadcast('vaultAddCipher');
};
$scope.addFolder = function () {
$scope.$broadcast('vaultAddFolder');
};
$scope.addOrganizationLogin = function () {
$scope.$broadcast('organizationVaultAddLogin');
$scope.addOrganizationCipher = function () {
$scope.$broadcast('organizationVaultAddCipher');
};
$scope.addOrganizationCollection = function () {

View File

@@ -4,7 +4,9 @@
.controller('organizationBillingChangePaymentController', function ($scope, $state, $uibModalInstance, apiService,
$analytics, toastr, existingPaymentMethod
// @if !selfHosted
/* jshint ignore:start */
, stripe
/* jshint ignore:end */
// @endif
) {
$analytics.eventTrack('organizationBillingChangePaymentController', { category: 'Modal' });

View File

@@ -2,7 +2,7 @@
.module('bit.organization')
.controller('organizationBillingController', function ($scope, apiService, $state, $uibModal, toastr, $analytics,
appSettings) {
appSettings, tokenService, $window) {
$scope.selfHosted = appSettings.selfHosted;
$scope.charges = [];
$scope.paymentSource = null;
@@ -208,6 +208,15 @@
});
};
$scope.viewInvoice = function (charge) {
if ($scope.selfHosted) {
return;
}
var url = appSettings.apiUri + '/organizations/' + $state.params.orgId +
'/billing-invoice/' + charge.invoiceId + '?access_token=' + tokenService.getToken();
$window.open(url);
};
function load() {
apiService.organizations.getBilling({ id: $state.params.orgId }, function (org) {
$scope.loading = false;

View File

@@ -10,7 +10,12 @@
$scope.model = {
name: org.Name,
billingEmail: org.BillingEmail,
businessName: org.BusinessName
businessName: org.BusinessName,
businessAddress1: org.BusinessAddress1,
businessAddress2: org.BusinessAddress2,
businessAddress3: org.BusinessAddress3,
businessCountry: org.BusinessCountry,
businessTaxNumber: org.BusinessTaxNumber
};
});
});

View File

@@ -2,11 +2,11 @@
.module('bit.organization')
.controller('organizationSettingsExportController', function ($scope, apiService, $uibModalInstance, cipherService,
$q, toastr, $analytics, $state) {
$q, toastr, $analytics, $state, constants) {
$analytics.eventTrack('organizationSettingsExportController', { category: 'Modal' });
$scope.export = function (model) {
$scope.startedExport = true;
var decLogins = [],
var decCiphers = [],
decCollections = [];
var collectionsPromise = apiService.collections.listOrganization({ orgId: $state.params.orgId },
@@ -14,18 +14,13 @@
decCollections = cipherService.decryptCollections(collections.Data, $state.params.orgId, true);
}).$promise;
var loginsPromise = apiService.ciphers.listOrganizationDetails({ organizationId: $state.params.orgId },
var ciphersPromise = apiService.ciphers.listOrganizationDetails({ organizationId: $state.params.orgId },
function (ciphers) {
for (var i = 0; i < ciphers.Data.length; i++) {
if (ciphers.Data[i].Type === 1) {
var decLogin = cipherService.decryptLogin(ciphers.Data[i]);
decLogins.push(decLogin);
}
}
decCiphers = cipherService.decryptCiphers(ciphers.Data);
}).$promise;
$q.all([collectionsPromise, loginsPromise]).then(function () {
if (!decLogins.length) {
$q.all([collectionsPromise, ciphersPromise]).then(function () {
if (!decCiphers.length) {
toastr.error('Nothing to export.', 'Error!');
$scope.close();
return;
@@ -37,45 +32,68 @@
}
try {
var exportLogins = [];
for (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,
totp: decLogins[i].totp,
var exportCiphers = [];
for (i = 0; i < decCiphers.length; i++) {
// only export logins and secure notes
if (decCiphers[i].type !== constants.cipherType.login &&
decCiphers[i].type !== constants.cipherType.secureNote) {
continue;
}
var cipher = {
collections: [],
fields: null
type: null,
name: decCiphers[i].name,
notes: decCiphers[i].notes,
fields: null,
// Login props
login_uri: null,
login_username: null,
login_password: null,
login_totp: null
};
var j;
if (decLogins[i].fields) {
for (j = 0; j < decLogins[i].fields.length; j++) {
if (!login.fields) {
login.fields = '';
if (decCiphers[i].collectionIds) {
for (j = 0; j < decCiphers[i].collectionIds.length; j++) {
if (collectionsDict.hasOwnProperty(decCiphers[i].collectionIds[j])) {
cipher.collections.push(collectionsDict[decCiphers[i].collectionIds[j]].name);
}
}
}
if (decCiphers[i].fields) {
for (j = 0; j < decCiphers[i].fields.length; j++) {
if (!cipher.fields) {
cipher.fields = '';
}
else {
login.fields += '\n';
cipher.fields += '\n';
}
login.fields += ((decLogins[i].fields[j].name || '') + ': ' + decLogins[i].fields[j].value);
cipher.fields += ((decCiphers[i].fields[j].name || '') + ': ' + decCiphers[i].fields[j].value);
}
}
if (decLogins[i].collectionIds) {
for (j = 0; j < decLogins[i].collectionIds.length; j++) {
if (collectionsDict.hasOwnProperty(decLogins[i].collectionIds[j])) {
login.collections.push(collectionsDict[decLogins[i].collectionIds[j]].name);
}
}
switch (decCiphers[i].type) {
case constants.cipherType.login:
cipher.type = 'login';
cipher.login_uri = decCiphers[i].login.uri;
cipher.login_username = decCiphers[i].login.username;
cipher.login_password = decCiphers[i].login.password;
cipher.login_totp = decCiphers[i].login.totp;
break;
case constants.cipherType.secureNote:
cipher.type = 'note';
break;
default:
continue;
}
exportLogins.push(login);
exportCiphers.push(cipher);
}
var csvString = Papa.unparse(exportLogins);
var csvString = Papa.unparse(exportCiphers);
var csvBlob = new Blob([csvString]);
// IE hack. ref http://msdn.microsoft.com/en-us/library/ie/hh779016.aspx

View File

@@ -55,15 +55,15 @@
importService.importOrg(model.source, file || model.fileContents, importSuccess, importError);
};
function importSuccess(collections, logins, collectionRelationships) {
if (!collections.length && !logins.length) {
function importSuccess(collections, ciphers, collectionRelationships) {
if (!collections.length && !ciphers.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])) {
else if (ciphers.length) {
var halfway = Math.floor(ciphers.length / 2);
var last = ciphers.length - 1;
if (cipherIsBadData(ciphers[0]) && cipherIsBadData(ciphers[halfway]) && cipherIsBadData(ciphers[last])) {
importError('CSV data is not formatted correctly. Please check your import file and try again.');
return;
}
@@ -71,7 +71,7 @@
apiService.ciphers.importOrg({ orgId: $state.params.orgId }, {
collections: cipherService.encryptCollections(collections, $state.params.orgId),
ciphers: cipherService.encryptLogins(logins, cryptoService.getOrgKey($state.params.orgId)),
ciphers: cipherService.encryptCiphers(ciphers, cryptoService.getOrgKey($state.params.orgId)),
collectionRelationships: collectionRelationships
}, function () {
$uibModalInstance.dismiss('cancel');
@@ -82,8 +82,9 @@
}, importError);
}
function loginIsBadData(login) {
return (login.name === null || login.name === '--') && (login.password === null || login.password === '');
function cipherIsBadData(cipher) {
return (cipher.name === null || cipher.name === '--') &&
(cipher.login && (cipher.login.password === null || cipher.login.password === ''));
}
function importError(error) {

View File

@@ -1,10 +1,20 @@
angular
.module('bit.organization')
.controller('organizationVaultAddLoginController', function ($scope, apiService, $uibModalInstance, cryptoService,
cipherService, passwordService, $analytics, authService, orgId, $uibModal) {
$analytics.eventTrack('organizationVaultAddLoginController', { category: 'Modal' });
$scope.login = {};
.controller('organizationVaultAddCipherController', function ($scope, apiService, $uibModalInstance, cryptoService,
cipherService, passwordService, $analytics, authService, orgId, $uibModal, constants) {
$analytics.eventTrack('organizationVaultAddCipherController', { category: 'Modal' });
$scope.constants = constants;
$scope.selectedType = constants.cipherType.login.toString();
$scope.cipher = {
type: constants.cipherType.login,
login: {},
identity: {},
card: {},
secureNote: {
type: '0'
}
};
$scope.hideFolders = $scope.hideFavorite = $scope.fromOrg = true;
authService.getUserProfile().then(function (userProfile) {
@@ -12,40 +22,44 @@
$scope.useTotp = orgProfile.useTotp;
});
$scope.typeChanged = function () {
$scope.cipher.type = parseInt($scope.selectedType);
};
$scope.savePromise = null;
$scope.save = function (model) {
model.organizationId = orgId;
var login = cipherService.encryptLogin(model);
$scope.savePromise = apiService.ciphers.postAdmin(login, function (loginResponse) {
$analytics.eventTrack('Created Organization Login');
var decLogin = cipherService.decryptLogin(loginResponse);
$uibModalInstance.close(decLogin);
$scope.save = function () {
$scope.cipher.organizationId = orgId;
var cipher = cipherService.encryptCipher($scope.cipher);
$scope.savePromise = apiService.ciphers.postAdmin(cipher, function (cipherResponse) {
$analytics.eventTrack('Created Organization Cipher');
var decCipher = cipherService.decryptCipherPreview(cipherResponse);
$uibModalInstance.close(decCipher);
}).$promise;
};
$scope.generatePassword = function () {
if (!$scope.login.password || confirm('Are you sure you want to overwrite the current password?')) {
if (!$scope.cipher.login.password || confirm('Are you sure you want to overwrite the current password?')) {
$analytics.eventTrack('Generated Password From Add');
$scope.login.password = passwordService.generatePassword({ length: 12, special: true });
$scope.cipher.login.password = passwordService.generatePassword({ length: 12, special: true });
}
};
$scope.addField = function () {
if (!$scope.login.fields) {
$scope.login.fields = [];
if (!$scope.cipher.fields) {
$scope.cipher.fields = [];
}
$scope.login.fields.push({
type: '0',
$scope.cipher.fields.push({
type: constants.fieldType.text.toString(),
name: null,
value: null
});
};
$scope.removeField = function (field) {
var index = $scope.login.fields.indexOf(field);
var index = $scope.cipher.fields.indexOf(field);
if (index > -1) {
$scope.login.fields.splice(index, 1);
$scope.cipher.fields.splice(index, 1);
}
};

View File

@@ -2,16 +2,16 @@
.module('bit.organization')
.controller('organizationVaultAttachmentsController', function ($scope, apiService, $uibModalInstance, cryptoService,
cipherService, loginId, $analytics, validationService, toastr, $timeout) {
cipherService, cipherId, $analytics, validationService, toastr, $timeout) {
$analytics.eventTrack('organizationVaultAttachmentsController', { category: 'Modal' });
$scope.login = {};
$scope.cipher = {};
$scope.loading = true;
$scope.isPremium = true;
$scope.canUseAttachments = true;
var closing = false;
apiService.ciphers.getAdmin({ id: loginId }, function (login) {
$scope.login = cipherService.decryptLogin(login);
apiService.ciphers.getAdmin({ id: cipherId }, function (cipher) {
$scope.cipher = cipherService.decryptCipher(cipher);
$scope.loading = false;
}, function () {
$scope.loading = false;
@@ -24,12 +24,12 @@
return;
}
var key = cryptoService.getOrgKey($scope.login.organizationId);
var key = cryptoService.getOrgKey($scope.cipher.organizationId);
$scope.savePromise = cipherService.encryptAttachmentFile(key, files[0]).then(function (encValue) {
var fd = new FormData();
var blob = new Blob([encValue.data], { type: 'application/octet-stream' });
fd.append('data', blob, encValue.fileName);
return apiService.ciphers.postAttachment({ id: loginId }, fd).$promise;
return apiService.ciphers.postAttachment({ id: cipherId }, fd).$promise;
}).then(function (response) {
$analytics.eventTrack('Added Attachment');
toastr.success('The attachment has been added.');
@@ -47,7 +47,7 @@
$scope.download = function (attachment) {
attachment.loading = true;
var key = cryptoService.getOrgKey($scope.login.organizationId);
var key = cryptoService.getOrgKey($scope.cipher.organizationId);
cipherService.downloadAndDecryptAttachment(key, attachment, true).then(function (res) {
$timeout(function () {
attachment.loading = false;
@@ -65,12 +65,12 @@
}
attachment.loading = true;
apiService.ciphers.delAttachment({ id: loginId, attachmentId: attachment.id }).$promise.then(function () {
apiService.ciphers.delAttachment({ id: cipherId, attachmentId: attachment.id }).$promise.then(function () {
attachment.loading = false;
$analytics.eventTrack('Deleted Organization Attachment');
var index = $scope.login.attachments.indexOf(attachment);
var index = $scope.cipher.attachments.indexOf(attachment);
if (index > -1) {
$scope.login.attachments.splice(index, 1);
$scope.cipher.attachments.splice(index, 1);
}
}, function () {
toastr.error('Cannot delete attachment.');
@@ -89,6 +89,6 @@
e.preventDefault();
closing = true;
$uibModalInstance.close(!!$scope.login.attachments && $scope.login.attachments.length > 0);
$uibModalInstance.close(!!$scope.cipher.attachments && $scope.cipher.attachments.length > 0);
});
});

View File

@@ -1,9 +1,9 @@
angular
.module('bit.organization')
.controller('organizationVaultLoginCollectionsController', function ($scope, apiService, $uibModalInstance, cipherService,
.controller('organizationVaultCipherCollectionsController', function ($scope, apiService, $uibModalInstance, cipherService,
cipher, $analytics, collections) {
$analytics.eventTrack('organizationVaultLoginCollectionsController', { category: 'Modal' });
$analytics.eventTrack('organizationVaultCipherCollectionsController', { category: 'Modal' });
$scope.cipher = {};
$scope.collections = [];
$scope.selectedCollections = {};
@@ -69,7 +69,7 @@
$scope.submitPromise = apiService.ciphers.putCollectionsAdmin({ id: cipher.id }, request)
.$promise.then(function (response) {
$analytics.eventTrack('Edited Login Collections');
$analytics.eventTrack('Edited Cipher Collections');
$uibModalInstance.close({
action: 'collectionsEdit',
collectionIds: request.collectionIds

View File

@@ -3,7 +3,7 @@
.controller('organizationVaultController', function ($scope, apiService, cipherService, $analytics, $q, $state,
$localStorage, $uibModal, $filter, authService) {
$scope.logins = [];
$scope.ciphers = [];
$scope.collections = [];
$scope.loading = true;
@@ -27,16 +27,14 @@
var cipherPromise = apiService.ciphers.listOrganizationDetails({ organizationId: $state.params.orgId },
function (ciphers) {
var decLogins = [];
var decCiphers = [];
for (var i = 0; i < ciphers.Data.length; i++) {
if (ciphers.Data[i].Type === 1) {
var decLogin = cipherService.decryptLoginPreview(ciphers.Data[i]);
decLogins.push(decLogin);
}
var decCipher = cipherService.decryptCipherPreview(ciphers.Data[i]);
decCiphers.push(decCipher);
}
$scope.logins = decLogins;
$scope.ciphers = decCiphers;
}).$promise;
$q.all([collectionPromise, cipherPromise]).then(function () {
@@ -77,55 +75,59 @@
}
};
$scope.editLogin = function (login) {
$scope.editCipher = function (cipher) {
var editModel = $uibModal.open({
animation: true,
templateUrl: 'app/vault/views/vaultEditLogin.html',
controller: 'organizationVaultEditLoginController',
templateUrl: 'app/vault/views/vaultEditCipher.html',
controller: 'organizationVaultEditCipherController',
resolve: {
loginId: function () { return login.id; },
cipherId: function () { return cipher.id; },
orgId: function () { return $state.params.orgId; }
}
});
editModel.result.then(function (returnVal) {
var index;
if (returnVal.action === 'edit') {
login.name = returnVal.data.name;
login.username = returnVal.data.username;
index = $scope.ciphers.indexOf(cipher);
if (index > -1) {
returnVal.data.collectionIds = $scope.ciphers[index].collectionIds;
$scope.ciphers[index] = returnVal.data;
}
}
else if (returnVal.action === 'delete') {
var index = $scope.logins.indexOf(login);
index = $scope.ciphers.indexOf(cipher);
if (index > -1) {
$scope.logins.splice(index, 1);
$scope.ciphers.splice(index, 1);
}
}
});
};
$scope.$on('organizationVaultAddLogin', function (event, args) {
$scope.addLogin();
$scope.$on('organizationVaultAddCipher', function (event, args) {
$scope.addCipher();
});
$scope.addLogin = function () {
$scope.addCipher = function () {
var addModel = $uibModal.open({
animation: true,
templateUrl: 'app/vault/views/vaultAddLogin.html',
controller: 'organizationVaultAddLoginController',
templateUrl: 'app/vault/views/vaultAddCipher.html',
controller: 'organizationVaultAddCipherController',
resolve: {
orgId: function () { return $state.params.orgId; }
}
});
addModel.result.then(function (addedLogin) {
$scope.logins.push(addedLogin);
addModel.result.then(function (addedCipher) {
$scope.ciphers.push(addedCipher);
});
};
$scope.editCollections = function (cipher) {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationVaultLoginCollections.html',
controller: 'organizationVaultLoginCollectionsController',
templateUrl: 'app/organization/views/organizationVaultCipherCollections.html',
controller: 'organizationVaultCipherCollectionsController',
resolve: {
cipher: function () { return cipher; },
collections: function () { return $scope.collections; }
@@ -139,9 +141,9 @@
});
};
$scope.attachments = function (login) {
$scope.attachments = function (cipher) {
authService.getUserProfile().then(function (profile) {
return !!profile.organizations[login.organizationId].maxStorageGb;
return !!profile.organizations[cipher.organizationId].maxStorageGb;
}).then(function (useStorage) {
if (!useStorage) {
$uibModal.open({
@@ -149,7 +151,7 @@
templateUrl: 'app/views/paidOrgRequired.html',
controller: 'paidOrgRequiredController',
resolve: {
orgId: function () { return login.organizationId; }
orgId: function () { return cipher.organizationId; }
}
});
return;
@@ -160,18 +162,18 @@
templateUrl: 'app/vault/views/vaultAttachments.html',
controller: 'organizationVaultAttachmentsController',
resolve: {
loginId: function () { return login.id; }
cipherId: function () { return cipher.id; }
}
});
attachmentModel.result.then(function (hasAttachments) {
login.hasAttachments = hasAttachments;
cipher.hasAttachments = hasAttachments;
});
});
};
$scope.removeLogin = function (login, collection) {
if (!confirm('Are you sure you want to remove this login (' + login.name + ') from the ' +
$scope.removeCipher = function (cipher, collection) {
if (!confirm('Are you sure you want to remove this item (' + cipher.name + ') from the ' +
'collection (' + collection.name + ') ?')) {
return;
}
@@ -180,28 +182,28 @@
collectionIds: []
};
for (var i = 0; i < login.collectionIds.length; i++) {
if (login.collectionIds[i] !== collection.id) {
request.collectionIds.push(login.collectionIds[i]);
for (var i = 0; i < cipher.collectionIds.length; i++) {
if (cipher.collectionIds[i] !== collection.id) {
request.collectionIds.push(cipher.collectionIds[i]);
}
}
apiService.ciphers.putCollections({ id: login.id }, request).$promise.then(function (response) {
$analytics.eventTrack('Removed Login From Collection');
login.collectionIds = request.collectionIds;
apiService.ciphers.putCollections({ id: cipher.id }, request).$promise.then(function (response) {
$analytics.eventTrack('Removed Cipher From Collection');
cipher.collectionIds = request.collectionIds;
});
};
$scope.deleteLogin = function (login) {
if (!confirm('Are you sure you want to delete this login (' + login.name + ')?')) {
$scope.deleteCipher = function (cipher) {
if (!confirm('Are you sure you want to delete this item (' + cipher.name + ')?')) {
return;
}
apiService.ciphers.delAdmin({ id: login.id }, function () {
$analytics.eventTrack('Deleted Login');
var index = $scope.logins.indexOf(login);
apiService.ciphers.delAdmin({ id: cipher.id }, function () {
$analytics.eventTrack('Deleted Cipher');
var index = $scope.ciphers.indexOf(cipher);
if (index > -1) {
$scope.logins.splice(index, 1);
$scope.ciphers.splice(index, 1);
}
});
};

View File

@@ -1,52 +1,53 @@
angular
.module('bit.organization')
.controller('organizationVaultEditLoginController', function ($scope, apiService, $uibModalInstance, cryptoService,
cipherService, passwordService, loginId, $analytics, orgId, $uibModal) {
$analytics.eventTrack('organizationVaultEditLoginController', { category: 'Modal' });
$scope.login = {};
.controller('organizationVaultEditCipherController', function ($scope, apiService, $uibModalInstance, cryptoService,
cipherService, passwordService, cipherId, $analytics, orgId, $uibModal, constants) {
$analytics.eventTrack('organizationVaultEditCipherController', { category: 'Modal' });
$scope.cipher = {};
$scope.hideFolders = $scope.hideFavorite = $scope.fromOrg = true;
$scope.constants = constants;
apiService.ciphers.getAdmin({ id: loginId }, function (login) {
$scope.login = cipherService.decryptLogin(login);
$scope.useTotp = $scope.login.organizationUseTotp;
apiService.ciphers.getAdmin({ id: cipherId }, function (cipher) {
$scope.cipher = cipherService.decryptCipher(cipher);
$scope.useTotp = $scope.cipher.organizationUseTotp;
});
$scope.save = function (model) {
var login = cipherService.encryptLogin(model);
$scope.savePromise = apiService.ciphers.putAdmin({ id: loginId }, login, function (loginResponse) {
$analytics.eventTrack('Edited Organization Login');
var decLogin = cipherService.decryptLogin(loginResponse);
var cipher = cipherService.encryptCipher(model, $scope.cipher.type);
$scope.savePromise = apiService.ciphers.putAdmin({ id: cipherId }, cipher, function (cipherResponse) {
$analytics.eventTrack('Edited Organization Cipher');
var decCipher = cipherService.decryptCipherPreview(cipherResponse);
$uibModalInstance.close({
action: 'edit',
data: decLogin
data: decCipher
});
}).$promise;
};
$scope.generatePassword = function () {
if (!$scope.login.password || confirm('Are you sure you want to overwrite the current password?')) {
if (!$scope.cipher.login.password || confirm('Are you sure you want to overwrite the current password?')) {
$analytics.eventTrack('Generated Password From Edit');
$scope.login.password = passwordService.generatePassword({ length: 12, special: true });
$scope.cipher.login.password = passwordService.generatePassword({ length: 12, special: true });
}
};
$scope.addField = function () {
if (!$scope.login.fields) {
$scope.login.fields = [];
if (!$scope.cipher.login.fields) {
$scope.cipher.login.fields = [];
}
$scope.login.fields.push({
type: '0',
$scope.cipher.fields.push({
type: constants.fieldType.text.toString(),
name: null,
value: null
});
};
$scope.removeField = function (field) {
var index = $scope.login.fields.indexOf(field);
var index = $scope.cipher.fields.indexOf(field);
if (index > -1) {
$scope.login.fields.splice(index, 1);
$scope.cipher.fields.splice(index, 1);
}
};
@@ -70,15 +71,15 @@
}
$scope.delete = function () {
if (!confirm('Are you sure you want to delete this login (' + $scope.login.name + ')?')) {
if (!confirm('Are you sure you want to delete this item (' + $scope.cipher.name + ')?')) {
return;
}
apiService.ciphers.delAdmin({ id: $scope.login.id }, function () {
$analytics.eventTrack('Deleted Organization Login From Edit');
apiService.ciphers.delAdmin({ id: $scope.cipher.id }, function () {
$analytics.eventTrack('Deleted Organization Cipher From Edit');
$uibModalInstance.close({
action: 'delete',
data: $scope.login.id
data: $scope.cipher.id
});
});
};

View File

@@ -201,6 +201,11 @@
<table class="table">
<tbody>
<tr ng-repeat="charge in charges">
<td style="width: 30px">
<a href="#" stop-click ng-click="viewInvoice(charge)" title="Invoice">
<i class="fa fa-file-pdf-o"></i>
</a>
</td>
<td style="width: 200px">
{{charge.date | date: 'mediumDate'}}
</td>

View File

@@ -25,15 +25,28 @@
<input type="text" id="name" name="Name" ng-model="model.name" class="form-control"
required api-field ng-readonly="selfHosted" />
</div>
<div class="form-group" show-errors>
<label for="name">Billing Email</label>
<input type="email" id="billingEmail" name="BillingEmail" ng-model="model.billingEmail"
class="form-control" required api-field ng-readonly="selfHosted" />
</div>
<div class="form-group" show-errors>
<label for="name">Business Name</label>
<input type="text" id="businessName" name="BusinessName" ng-model="model.businessName"
class="form-control" api-field ng-readonly="selfHosted" />
</div>
<div class="form-group" show-errors>
<label for="name">Billing Email</label>
<input type="email" id="billingEmail" name="BillingEmail" ng-model="model.billingEmail"
class="form-control" required api-field ng-readonly="selfHosted" />
<div ng-if="!selfHosted">
<hr />
<strong>Tax Information</strong>
<div>{{model.businessAddress1}}</div>
<div>{{model.businessAddress2}}</div>
<div>{{model.businessAddress3}}</div>
<div>{{model.businessCountry}}</div>
<div>{{model.businessTaxNumber}}</div>
<p class="help-block">
Please <a href="https://bitwarden.com/contact/" target="_blank">contact support</a>
to provide (or update) tax information for your invoices.
</p>
</div>
</div>
<div class="col-sm-3 settings-photo">

View File

@@ -5,7 +5,7 @@
<span ng-pluralize
count="collections.length > 0 ? collections.length - 1 : 0"
when="{'1': '{} collection', 'other': '{} collections'}"></span>,
<span ng-pluralize count="logins.length" when="{'1': '{} login', 'other': '{} logins'}"></span>
<span ng-pluralize count="ciphers.length" when="{'1': '{} item', 'other': '{} items'}"></span>
</small>
</h1>
</section>
@@ -13,12 +13,12 @@
<p ng-show="loading && !collections.length">Loading...</p>
<div class="box" ng-class="{'collapsed-box': collection.collapsed}" ng-repeat="collection in collections |
orderBy: collectionSort track by collection.id"
ng-show="collections.length && (!main.searchVaultText || collectionLogins.length)">
ng-show="collections.length && (!main.searchVaultText || collectionCiphers.length)">
<div class="box-header with-border">
<h3 class="box-title">
<i class="fa" ng-class="{'fa-cubes': collection.id, 'fa-sitemap': !collection.id}"></i>
{{collection.name}}
<small ng-pluralize count="collectionLogins.length" when="{'1': '{} login', 'other': '{} logins'}"></small>
<small ng-pluralize count="collectionCiphers.length" when="{'1': '{} item', 'other': '{} items'}"></small>
</h3>
<div class="box-tools">
<button type="button" class="btn btn-box-tool" data-widget="collapse" title="Collapse/Expand"
@@ -27,14 +27,14 @@
</button>
</div>
</div>
<div class="box-body" ng-class="{'no-padding': collectionLogins.length}">
<div ng-show="!collectionLogins.length && collection.id">No logins in this collection.</div>
<div ng-show="!collectionLogins.length && !collection.id">No unassigned logins.</div>
<div class="table-responsive" ng-show="collectionLogins.length">
<div class="box-body" ng-class="{'no-padding': collectionCiphers.length}">
<div ng-show="!collectionCiphers.length && collection.id">No items in this collection.</div>
<div ng-show="!collectionCiphers.length && !collection.id">No unassigned items.</div>
<div class="table-responsive" ng-show="collectionCiphers.length">
<table class="table table-striped table-hover table-vmiddle">
<tbody>
<tr ng-repeat="login in collectionLogins = (logins | filter: filterByCollection(collection) |
filter: (main.searchVaultText || '') | orderBy: ['name', 'username']) track by login.id">
<tr ng-repeat="cipher in collectionCiphers = (ciphers | filter: filterByCollection(collection) |
filter: (main.searchVaultText || '') | orderBy: ['name', 'subTitle']) track by cipher.id">
<td style="width: 70px;">
<div class="btn-group" data-append-to="body">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
@@ -42,37 +42,44 @@
</button>
<ul class="dropdown-menu">
<li>
<a href="#" stop-click ng-click="editLogin(login)">
<a href="#" stop-click ng-click="editCipher(cipher)">
<i class="fa fa-fw fa-pencil"></i> Edit
</a>
</li>
<li>
<a href="#" stop-click ng-click="attachments(login)">
<a href="#" stop-click ng-click="attachments(cipher)">
<i class="fa fa-fw fa-paperclip"></i> Attachments
</a>
</li>
<li>
<a href="#" stop-click ng-click="editCollections(login)">
<a href="#" stop-click ng-click="editCollections(cipher)">
<i class="fa fa-fw fa-cubes"></i> Collections
</a>
</li>
<li>
<a href="#" stop-click ng-click="removeLogin(login, collection)" class="text-red"
<a href="#" stop-click ng-click="removeCipher(cipher, collection)" class="text-red"
ng-if="collection.id">
<i class="fa fa-fw fa-remove"></i> Remove
</a>
</li>
<li>
<a href="#" stop-click ng-click="deleteLogin(login)" class="text-red">
<a href="#" stop-click ng-click="deleteCipher(cipher)" class="text-red">
<i class="fa fa-fw fa-trash"></i> Delete
</a>
</li>
</ul>
</div>
</td>
<td class="vault-icon">
<i class="fa fa-fw fa-lg {{::cipher.icon}}" ng-if="!cipher.meta.image"></i>
<img alt="" ng-if="cipher.meta.image" ng-src="{{cipher.meta.image}}"
fallback-src="images/fa-globe.png" />
</td>
<td>
<a href="#" stop-click ng-click="editLogin(login)">{{login.name}}</a>
<div class="text-sm text-muted">{{login.username}}</div>
<a href="#" stop-click ng-click="editCipher(cipher)">{{cipher.name}}</a>
<i class="fa fa-paperclip text-muted" title="Attachments" ng-if="cipher.hasAttachments"
stop-prop></i>
<div class="text-sm text-muted">{{cipher.subTitle}}</div>
</td>
</tr>
</tbody>

View File

@@ -4,7 +4,7 @@
</div>
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise" autocomplete="off">
<div class="modal-body">
<p>Edit the collections that this login is being shared with.</p>
<p>Edit the collections that this item is being shared with.</p>
<div class="callout callout-danger validation-errors" ng-show="form.$errors">
<h4>Errors have occurred</h4>
<ul>

View File

@@ -35,6 +35,7 @@
delAdmin: { url: _apiUri + '/ciphers/:id/delete-admin', method: 'POST', params: { id: '@id' } },
delMany: { url: _apiUri + '/ciphers/delete', method: 'POST' },
moveMany: { url: _apiUri + '/ciphers/move', method: 'POST' },
purge: { url: _apiUri + '/ciphers/purge', method: 'POST' },
postAttachment: {
url: _apiUri + '/ciphers/:id/attachment',
method: 'POST',

View File

@@ -95,7 +95,7 @@ angular
_service.logOut = function () {
tokenService.clearTokens();
cryptoService.clearKeys();
$rootScope.vaultFolders = $rootScope.vaultLogins = null;
$rootScope.vaultFolders = $rootScope.vaultCiphers = null;
_userProfile = null;
};
@@ -158,8 +158,8 @@ angular
cryptoService.setOrgKeys(orgs);
_setDeferred.resolve(_userProfile);
}
}, function () {
_setDeferred.reject();
}, function (error) {
_setDeferred.reject(error);
});
return _setDeferred.promise;
@@ -233,7 +233,9 @@ angular
_service.refreshAccessToken = function () {
var refreshToken = tokenService.getRefreshToken();
if (!refreshToken) {
return null;
return $q(function (resolve, reject) {
resolve(null);
});
}
return apiService.identity.token({

View File

@@ -1,64 +1,23 @@
angular
.module('bit.services')
.factory('cipherService', function (cryptoService, apiService, $q, $window) {
var _service = {};
_service.decryptLogins = function (encryptedLogins) {
if (!encryptedLogins) throw "encryptedLogins is undefined or null";
var unencryptedLogins = [];
for (var i = 0; i < encryptedLogins.length; i++) {
unencryptedLogins.push(_service.decryptLogin(encryptedLogins[i]));
}
return unencryptedLogins;
.factory('cipherService', function (cryptoService, apiService, $q, $window, constants, appSettings, $localStorage) {
var _service = {
disableWebsiteIcons: $localStorage.disableWebsiteIcons
};
_service.decryptLogin = function (encryptedLogin, isCipher) {
if (!encryptedLogin) throw "encryptedLogin is undefined or null";
_service.decryptCiphers = function (encryptedCiphers) {
if (!encryptedCiphers) throw "encryptedCiphers is undefined or null";
var key = null;
if (encryptedLogin.OrganizationId) {
key = cryptoService.getOrgKey(encryptedLogin.OrganizationId);
var unencryptedCiphers = [];
for (var i = 0; i < encryptedCiphers.length; i++) {
unencryptedCiphers.push(_service.decryptCipher(encryptedCiphers[i]));
}
var login = {
id: encryptedLogin.Id,
organizationId: encryptedLogin.OrganizationId,
collectionIds: encryptedLogin.CollectionIds || [],
'type': 1,
folderId: encryptedLogin.FolderId,
favorite: encryptedLogin.Favorite,
edit: encryptedLogin.Edit,
organizationUseTotp: encryptedLogin.OrganizationUseTotp,
attachments: null
};
var loginData = encryptedLogin.Data;
if (loginData) {
login.name = cryptoService.decrypt(loginData.Name, key);
login.uri = loginData.Uri && loginData.Uri !== '' ? cryptoService.decrypt(loginData.Uri, key) : null;
login.username = loginData.Username && loginData.Username !== '' ? cryptoService.decrypt(loginData.Username, key) : null;
login.password = loginData.Password && loginData.Password !== '' ? cryptoService.decrypt(loginData.Password, key) : null;
login.notes = loginData.Notes && loginData.Notes !== '' ? cryptoService.decrypt(loginData.Notes, key) : null;
login.totp = loginData.Totp && loginData.Totp !== '' ? cryptoService.decrypt(loginData.Totp, key) : null;
login.fields = _service.decryptFields(key, loginData.Fields);
}
if (!encryptedLogin.Attachments) {
return login;
}
login.attachments = [];
for (var i = 0; i < encryptedLogin.Attachments.length; i++) {
login.attachments.push(_service.decryptAttachment(key, encryptedLogin.Attachments[i]));
}
return login;
return unencryptedCiphers;
};
_service.decryptLoginPreview = function (encryptedCipher) {
_service.decryptCipher = function (encryptedCipher) {
if (!encryptedCipher) throw "encryptedCipher is undefined or null";
var key = null;
@@ -66,27 +25,202 @@ angular
key = cryptoService.getOrgKey(encryptedCipher.OrganizationId);
}
var login = {
var cipher = {
id: encryptedCipher.Id,
organizationId: encryptedCipher.OrganizationId,
collectionIds: encryptedCipher.CollectionIds || [],
'type': encryptedCipher.Type,
folderId: encryptedCipher.FolderId,
favorite: encryptedCipher.Favorite,
edit: encryptedCipher.Edit,
organizationUseTotp: encryptedCipher.OrganizationUseTotp,
hasAttachments: !!encryptedCipher.Attachments && encryptedCipher.Attachments.length > 0
attachments: null,
icon: null
};
var loginData = encryptedCipher.Data;
if (loginData) {
login.name = _service.decryptProperty(loginData.Name, key, false);
login.username = _service.decryptProperty(loginData.Username, key, true);
login.password = _service.decryptProperty(loginData.Password, key, true);
var cipherData = encryptedCipher.Data;
if (cipherData) {
cipher.name = cryptoService.decrypt(cipherData.Name, key);
cipher.notes = _service.decryptProperty(cipherData.Notes, key, true, false);
cipher.fields = _service.decryptFields(key, cipherData.Fields);
var dataObj = {};
switch (cipher.type) {
case constants.cipherType.login:
dataObj.uri = _service.decryptProperty(cipherData.Uri, key, true, false);
dataObj.username = _service.decryptProperty(cipherData.Username, key, true, false);
dataObj.password = _service.decryptProperty(cipherData.Password, key, true, false);
dataObj.totp = _service.decryptProperty(cipherData.Totp, key, true, false);
cipher.login = dataObj;
cipher.icon = 'fa-globe';
break;
case constants.cipherType.secureNote:
dataObj.type = cipherData.Type;
cipher.secureNote = dataObj;
cipher.icon = 'fa-sticky-note-o';
break;
case constants.cipherType.card:
dataObj.cardholderName = _service.decryptProperty(cipherData.CardholderName, key, true, false);
dataObj.number = _service.decryptProperty(cipherData.Number, key, true, false);
dataObj.brand = _service.decryptProperty(cipherData.Brand, key, true, false);
dataObj.expMonth = _service.decryptProperty(cipherData.ExpMonth, key, true, false);
dataObj.expYear = _service.decryptProperty(cipherData.ExpYear, key, true, false);
dataObj.code = _service.decryptProperty(cipherData.Code, key, true, false);
cipher.card = dataObj;
cipher.icon = 'fa-credit-card';
break;
case constants.cipherType.identity:
dataObj.title = _service.decryptProperty(cipherData.Title, key, true, false);
dataObj.firstName = _service.decryptProperty(cipherData.FirstName, key, true, false);
dataObj.middleName = _service.decryptProperty(cipherData.MiddleName, key, true, false);
dataObj.lastName = _service.decryptProperty(cipherData.LastName, key, true, false);
dataObj.address1 = _service.decryptProperty(cipherData.Address1, key, true, false);
dataObj.address2 = _service.decryptProperty(cipherData.Address2, key, true, false);
dataObj.address3 = _service.decryptProperty(cipherData.Address3, key, true, false);
dataObj.city = _service.decryptProperty(cipherData.City, key, true, false);
dataObj.state = _service.decryptProperty(cipherData.State, key, true, false);
dataObj.postalCode = _service.decryptProperty(cipherData.PostalCode, key, true, false);
dataObj.country = _service.decryptProperty(cipherData.Country, key, true, false);
dataObj.company = _service.decryptProperty(cipherData.Company, key, true, false);
dataObj.email = _service.decryptProperty(cipherData.Email, key, true, false);
dataObj.phone = _service.decryptProperty(cipherData.Phone, key, true, false);
dataObj.ssn = _service.decryptProperty(cipherData.SSN, key, true, false);
dataObj.username = _service.decryptProperty(cipherData.Username, key, true, false);
dataObj.passportNumber = _service.decryptProperty(cipherData.PassportNumber, key, true, false);
dataObj.licenseNumber = _service.decryptProperty(cipherData.LicenseNumber, key, true, false);
cipher.identity = dataObj;
cipher.icon = 'fa-id-card-o';
break;
default:
break;
}
}
return login;
if (!encryptedCipher.Attachments) {
return cipher;
}
cipher.attachments = [];
for (var i = 0; i < encryptedCipher.Attachments.length; i++) {
cipher.attachments.push(_service.decryptAttachment(key, encryptedCipher.Attachments[i]));
}
return cipher;
};
_service.decryptCipherPreview = function (encryptedCipher) {
if (!encryptedCipher) throw "encryptedCipher is undefined or null";
var key = null;
if (encryptedCipher.OrganizationId) {
key = cryptoService.getOrgKey(encryptedCipher.OrganizationId);
}
var cipher = {
id: encryptedCipher.Id,
organizationId: encryptedCipher.OrganizationId,
collectionIds: encryptedCipher.CollectionIds || [],
'type': encryptedCipher.Type,
folderId: encryptedCipher.FolderId,
favorite: encryptedCipher.Favorite,
edit: encryptedCipher.Edit,
organizationUseTotp: encryptedCipher.OrganizationUseTotp,
hasAttachments: !!encryptedCipher.Attachments && encryptedCipher.Attachments.length > 0,
meta: {},
icon: null
};
var cipherData = encryptedCipher.Data;
if (cipherData) {
cipher.name = _service.decryptProperty(cipherData.Name, key, false, true);
var dataObj = {};
switch (cipher.type) {
case constants.cipherType.login:
cipher.subTitle = _service.decryptProperty(cipherData.Username, key, true, true);
cipher.meta.password = _service.decryptProperty(cipherData.Password, key, true, true);
cipher.meta.uri = _service.decryptProperty(cipherData.Uri, key, true, true);
setLoginIcon(cipher, cipher.meta.uri, true);
break;
case constants.cipherType.secureNote:
cipher.subTitle = null;
cipher.icon = 'fa-sticky-note-o';
break;
case constants.cipherType.card:
cipher.subTitle = '';
cipher.meta.number = _service.decryptProperty(cipherData.Number, key, true, true);
var brand = _service.decryptProperty(cipherData.Brand, key, true, true);
if (brand) {
cipher.subTitle = brand;
}
if (cipher.meta.number && cipher.meta.number.length >= 4) {
if (cipher.subTitle !== '') {
cipher.subTitle += ', ';
}
cipher.subTitle += ('*' + cipher.meta.number.substr(cipher.meta.number.length - 4));
}
cipher.icon = 'fa-credit-card';
break;
case constants.cipherType.identity:
var firstName = _service.decryptProperty(cipherData.FirstName, key, true, true);
var lastName = _service.decryptProperty(cipherData.LastName, key, true, true);
cipher.subTitle = '';
if (firstName) {
cipher.subTitle = firstName;
}
if (lastName) {
if (cipher.subTitle !== '') {
cipher.subTitle += ' ';
}
cipher.subTitle += lastName;
}
cipher.icon = 'fa-id-card-o';
break;
default:
break;
}
if (cipher.subTitle === '') {
cipher.subTitle = null;
}
}
return cipher;
};
function setLoginIcon(cipher, uri, setImage) {
if (!_service.disableWebsiteIcons && uri) {
var hostnameUri = uri,
isWebsite = false;
if (hostnameUri.indexOf('androidapp://') === 0) {
cipher.icon = 'fa-android';
}
else if (hostnameUri.indexOf('iosapp://') === 0) {
cipher.icon = 'fa-apple';
}
else if (hostnameUri.indexOf('://') === -1 && hostnameUri.indexOf('.') > -1) {
hostnameUri = "http://" + hostnameUri;
isWebsite = true;
}
else {
isWebsite = hostnameUri.indexOf('http') === 0 && hostnameUri.indexOf('.') > -1;
}
if (setImage && isWebsite) {
try {
var url = new URL(hostnameUri);
cipher.meta.image = appSettings.iconsUri + '/' + url.hostname + '/icon.png';
}
catch (e) { }
}
}
if (!cipher.icon) {
cipher.icon = 'fa-globe';
}
}
_service.decryptAttachment = function (key, encryptedAttachment) {
if (!encryptedAttachment) throw "encryptedAttachment is undefined or null";
@@ -182,7 +316,7 @@ angular
return {
id: encryptedFolder.Id,
name: _service.decryptProperty(encryptedFolder.Name, null, false)
name: _service.decryptProperty(encryptedFolder.Name, null, false, true)
};
};
@@ -206,12 +340,12 @@ angular
return {
id: encryptedCollection.Id,
name: catchError ? _service.decryptProperty(encryptedCollection.Name, key, false) :
name: catchError ? _service.decryptProperty(encryptedCollection.Name, key, false, true) :
cryptoService.decrypt(encryptedCollection.Name, key)
};
};
_service.decryptProperty = function (property, key, checkEmpty) {
_service.decryptProperty = function (property, key, checkEmpty, showError) {
if (checkEmpty && (!property || property === '')) {
return null;
}
@@ -223,53 +357,100 @@ angular
property = null;
}
return property || '[error: cannot decrypt]';
return property || (showError ? '[error: cannot decrypt]' : null);
};
_service.encryptLogins = function (unencryptedLogins, key) {
if (!unencryptedLogins) throw "unencryptedLogins is undefined or null";
_service.encryptCiphers = function (unencryptedCiphers, key) {
if (!unencryptedCiphers) throw "unencryptedCiphers is undefined or null";
var encryptedLogins = [];
for (var i = 0; i < unencryptedLogins.length; i++) {
encryptedLogins.push(_service.encryptLogin(unencryptedLogins[i], key));
var encryptedCiphers = [];
for (var i = 0; i < unencryptedCiphers.length; i++) {
encryptedCiphers.push(_service.encryptCipher(unencryptedCiphers[i], null, key));
}
return encryptedLogins;
return encryptedCiphers;
};
_service.encryptLogin = function (unencryptedLogin, key, attachments) {
if (!unencryptedLogin) throw "unencryptedLogin is undefined or null";
_service.encryptCipher = function (unencryptedCipher, type, key, attachments) {
if (!unencryptedCipher) throw "unencryptedCipher is undefined or null";
if (unencryptedLogin.organizationId) {
key = key || cryptoService.getOrgKey(unencryptedLogin.organizationId);
if (unencryptedCipher.organizationId) {
key = key || cryptoService.getOrgKey(unencryptedCipher.organizationId);
}
var login = {
id: unencryptedLogin.id,
'type': 1,
organizationId: unencryptedLogin.organizationId || null,
folderId: unencryptedLogin.folderId === '' ? null : unencryptedLogin.folderId,
favorite: unencryptedLogin.favorite !== null ? unencryptedLogin.favorite : false,
name: cryptoService.encrypt(unencryptedLogin.name, key),
notes: !unencryptedLogin.notes || unencryptedLogin.notes === '' ? null : cryptoService.encrypt(unencryptedLogin.notes, key),
login: {
uri: !unencryptedLogin.uri || unencryptedLogin.uri === '' ? null : cryptoService.encrypt(unencryptedLogin.uri, key),
username: !unencryptedLogin.username || unencryptedLogin.username === '' ? null : cryptoService.encrypt(unencryptedLogin.username, key),
password: !unencryptedLogin.password || unencryptedLogin.password === '' ? null : cryptoService.encrypt(unencryptedLogin.password, key),
totp: !unencryptedLogin.totp || unencryptedLogin.totp === '' ? null : cryptoService.encrypt(unencryptedLogin.totp, key)
},
fields: _service.encryptFields(unencryptedLogin.fields, key)
var cipher = {
id: unencryptedCipher.id,
'type': type || unencryptedCipher.type,
organizationId: unencryptedCipher.organizationId || null,
folderId: unencryptedCipher.folderId === '' ? null : unencryptedCipher.folderId,
favorite: unencryptedCipher.favorite !== null ? unencryptedCipher.favorite : false,
name: cryptoService.encrypt(unencryptedCipher.name, key),
notes: encryptProperty(unencryptedCipher.notes, key),
fields: _service.encryptFields(unencryptedCipher.fields, key)
};
if (unencryptedLogin.attachments && attachments) {
login.attachments = {};
for (var i = 0; i < unencryptedLogin.attachments.length; i++) {
login.attachments[unencryptedLogin.attachments[i].id] =
cryptoService.encrypt(unencryptedLogin.attachments[i].fileName, key);
switch (cipher.type) {
case constants.cipherType.login:
var loginData = unencryptedCipher.login;
cipher.login = {
uri: encryptProperty(loginData.uri, key),
username: encryptProperty(loginData.username, key),
password: encryptProperty(loginData.password, key),
totp: encryptProperty(loginData.totp, key)
};
break;
case constants.cipherType.secureNote:
cipher.secureNote = {
type: unencryptedCipher.secureNote.type
};
break;
case constants.cipherType.card:
var cardData = unencryptedCipher.card;
cipher.card = {
cardholderName: encryptProperty(cardData.cardholderName, key),
brand: encryptProperty(cardData.brand, key),
number: encryptProperty(cardData.number, key),
expMonth: encryptProperty(cardData.expMonth, key),
expYear: encryptProperty(cardData.expYear, key),
code: encryptProperty(cardData.code, key)
};
break;
case constants.cipherType.identity:
var identityData = unencryptedCipher.identity;
cipher.identity = {
title: encryptProperty(identityData.title, key),
firstName: encryptProperty(identityData.firstName, key),
middleName: encryptProperty(identityData.middleName, key),
lastName: encryptProperty(identityData.lastName, key),
address1: encryptProperty(identityData.address1, key),
address2: encryptProperty(identityData.address2, key),
address3: encryptProperty(identityData.address3, key),
city: encryptProperty(identityData.city, key),
state: encryptProperty(identityData.state, key),
postalCode: encryptProperty(identityData.postalCode, key),
country: encryptProperty(identityData.country, key),
company: encryptProperty(identityData.company, key),
email: encryptProperty(identityData.email, key),
phone: encryptProperty(identityData.phone, key),
ssn: encryptProperty(identityData.ssn, key),
username: encryptProperty(identityData.username, key),
passportNumber: encryptProperty(identityData.passportNumber, key),
licenseNumber: encryptProperty(identityData.licenseNumber, key)
};
break;
default:
break;
}
if (unencryptedCipher.attachments && attachments) {
cipher.attachments = {};
for (var i = 0; i < unencryptedCipher.attachments.length; i++) {
cipher.attachments[unencryptedCipher.attachments[i].id] =
cryptoService.encrypt(unencryptedCipher.attachments[i].fileName, key);
}
}
return login;
return cipher;
};
_service.encryptAttachmentFile = function (key, unencryptedFile) {
@@ -365,5 +546,9 @@ angular
};
};
function encryptProperty(property, key) {
return !property || property === '' ? null : cryptoService.encrypt(property, key);
}
return _service;
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +1,2 @@
angular.module("bit")
.constant("appSettings", {"apiUri":"https://api.bitwarden.com","identityUri":"https://identity.bitwarden.com","stripeKey":"pk_live_bpN0P37nMxrMQkcaHXtAybJk","braintreeKey":"production_qfbsv8kc_njj2zjtyngtjmbjd","whitelistDomains":["api.bitwarden.com"],"selfHosted":false,"version":"1.17.3","environment":"Production"});
.constant("appSettings", {"apiUri":"https://api.bitwarden.com","identityUri":"https://identity.bitwarden.com","iconsUri":"https://icons.bitwarden.com","stripeKey":"pk_live_bpN0P37nMxrMQkcaHXtAybJk","braintreeKey":"production_qfbsv8kc_njj2zjtyngtjmbjd","whitelistDomains":["api.bitwarden.com"],"selfHosted":false,"version":"1.18.0","environment":"Production"});

View File

@@ -4,7 +4,9 @@
.controller('settingsBillingChangePaymentController', function ($scope, $state, $uibModalInstance, apiService,
$analytics, toastr, existingPaymentMethod, appSettings, $timeout
// @if !selfHosted
/* jshint ignore:start */
, stripe
/* jshint ignore:end */
// @endif
) {
$analytics.eventTrack('settingsBillingChangePaymentController', { category: 'Modal' });

View File

@@ -1,11 +1,12 @@
angular
.module('bit.settings')
.controller('settingsController', function ($scope, $state, $uibModal, apiService, toastr, authService) {
.controller('settingsController', function ($scope, $state, $uibModal, apiService, toastr, authService, $localStorage,
$rootScope, cipherService) {
$scope.model = {
profile: {},
twoFactorEnabled: false,
email: null
email: null,
disableWebsiteIcons: false
};
$scope.$on('$viewContentLoaded', function () {
@@ -17,7 +18,7 @@
culture: user.Culture
},
email: user.Email,
twoFactorEnabled: user.TwoFactorEnabled
disableWebsiteIcons: $localStorage.disableWebsiteIcons
};
if (user.Organizations) {
@@ -58,6 +59,13 @@
}).$promise;
};
$scope.optionsSave = function () {
$localStorage.disableWebsiteIcons = cipherService.disableWebsiteIcons = $scope.model.disableWebsiteIcons;
$rootScope.vaultCiphers = null;
toastr.success('Options have been updated.', 'Success!');
};
$scope.changePassword = function () {
$uibModal.open({
animation: true,
@@ -121,6 +129,14 @@
});
};
$scope.purge = function () {
$uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsPurge.html',
controller: 'settingsPurgeController'
});
};
function scrollToTop() {
$('html, body').animate({ scrollTop: 0 }, 200);
}

View File

@@ -4,7 +4,9 @@
.controller('settingsCreateOrganizationController', function ($scope, $state, apiService, cryptoService,
toastr, $analytics, authService, constants, appSettings, validationService
// @if !selfHosted
/* jshint ignore:start */
, stripe
/* jshint ignore:end */
// @endif
) {
$scope.plans = constants.plans;
@@ -113,6 +115,7 @@
additionalStorageGb: model.additionalStorageGb,
billingEmail: model.billingEmail,
businessName: model.ownedBusiness ? model.businessName : null,
country: $scope.paymentMethod === 'card' ? model.card.address_country : null,
collectionName: defaultCollectionCt
};

View File

@@ -4,7 +4,9 @@
.controller('settingsPremiumController', function ($scope, $state, apiService, toastr, $analytics, authService,
constants, $timeout, appSettings, validationService
// @if !selfHosted
/* jshint ignore:start */
, stripe
/* jshint ignore:end */
// @endif
) {
var profile = null;

View File

@@ -0,0 +1,24 @@
angular
.module('bit.settings')
.controller('settingsPurgeController', function ($scope, $state, apiService, $uibModalInstance, cryptoService,
authService, toastr, $analytics, tokenService) {
$analytics.eventTrack('settingsPurgeController', { category: 'Modal' });
$scope.submit = function (model) {
$scope.submitPromise = cryptoService.hashPassword(model.masterPassword).then(function (hash) {
return apiService.ciphers.purge({
masterPasswordHash: hash
}).$promise;
}).then(function () {
$uibModalInstance.dismiss('cancel');
$analytics.eventTrack('Purged Vault');
return $state.go('backend.user.vault', { refreshFromServer: true });
}).then(function () {
toastr.success('All items in your vault have been deleted.', 'Vault Purged');
});
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@@ -31,19 +31,19 @@
function updateKey(masterPasswordHash) {
var madeEncKey = cryptoService.makeEncKey(null);
var reencryptedLogins = [];
var loginsPromise = apiService.ciphers.list({}, function (encryptedLogins) {
var filteredEncryptedLogins = [];
for (var i = 0; i < encryptedLogins.Data.length; i++) {
if (encryptedLogins.Data[i].OrganizationId) {
var reencryptedCiphers = [];
var ciphersPromise = apiService.ciphers.list({}, function (encryptedCiphers) {
var filteredEncryptedCiphers = [];
for (var i = 0; i < encryptedCiphers.Data.length; i++) {
if (encryptedCiphers.Data[i].OrganizationId) {
continue;
}
filteredEncryptedLogins.push(encryptedLogins.Data[i]);
filteredEncryptedCiphers.push(encryptedCiphers.Data[i]);
}
var unencryptedLogins = cipherService.decryptLogins(filteredEncryptedLogins);
reencryptedLogins = cipherService.encryptLogins(unencryptedLogins, madeEncKey.encKey);
var unencryptedCiphers = cipherService.decryptCiphers(filteredEncryptedCiphers);
reencryptedCiphers = cipherService.encryptCiphers(unencryptedCiphers, madeEncKey.encKey);
}).$promise;
var reencryptedFolders = [];
@@ -58,10 +58,10 @@
reencryptedPrivateKey = cryptoService.encrypt(privateKey, madeEncKey.encKey, 'raw');
}
return $q.all([loginsPromise, foldersPromise]).then(function () {
return $q.all([ciphersPromise, foldersPromise]).then(function () {
var request = {
masterPasswordHash: masterPasswordHash,
ciphers: reencryptedLogins,
ciphers: reencryptedCiphers,
folders: reencryptedFolders,
privateKey: reencryptedPrivateKey,
key: madeEncKey.encKeyEnc

View File

@@ -86,6 +86,27 @@
</div>
</form>
</div>
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Web Vault Options</h3>
</div>
<form role="form" name="optionsForm" ng-submit="optionsForm.$valid && optionsSave()" autocomplete="off">
<div class="box-body">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="model.disableWebsiteIcons">
Disable Website Icons
</label>
<p class="help-block">Website Icons provide a recognizable image next to each login item in your vault.</p>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="optionsForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="optionsForm.$loading"></i>Save
</button>
</div>
</form>
</div>
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Organizations</h3>
@@ -129,10 +150,13 @@
Careful, these actions are not reversible!
</div>
<div class="box-footer">
<button type="submit" class="btn btn-default btn-flat" ng-click="sessions()">
<button type="button" class="btn btn-default btn-flat" ng-click="sessions()">
Deauthorize Sessions
</button>
<button type="submit" class="btn btn-default btn-flat" ng-click="delete()">
<button type="button" class="btn btn-default btn-flat" ng-click="purge()">
Purge Vault
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="delete()">
Delete Account
</button>
</div>

View File

@@ -0,0 +1,33 @@
<div class="modal-header">
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><i class="fa fa-trash"></i> Purge Vault</h4>
</div>
<form name="form" ng-submit="form.$valid && submit(model)" api-form="submitPromise">
<div class="modal-body">
<p>
Continue below to delete all items in your vault. Items that belong to an organization that you share
with will not be deleted.
</p>
<div class="callout callout-warning">
<h4><i class="fa fa-warning"></i> Warning</h4>
Purging your vault is permanent. It cannot be undone.
</div>
<div class="callout callout-danger validation-errors" ng-show="form.$errors">
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in form.$errors">{{e}}</li>
</ul>
</div>
<div class="form-group" show-errors>
<label for="masterPassword">Master Password</label>
<input type="password" id="masterPassword" name="MasterPasswordHash" ng-model="model.masterPassword"
class="form-control" required api-field />
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="form.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="form.$loading"></i>Purge
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
</div>
</form>

View File

@@ -2,23 +2,23 @@
.module('bit.tools')
.controller('toolsExportController', function ($scope, apiService, $uibModalInstance, cipherService, $q,
toastr, $analytics) {
toastr, $analytics, constants) {
$analytics.eventTrack('toolsExportController', { category: 'Modal' });
$scope.export = function (model) {
$scope.startedExport = true;
var decLogins = [],
var decCiphers = [],
decFolders = [];
var folderPromise = apiService.folders.list({}, function (folders) {
decFolders = cipherService.decryptFolders(folders.Data);
}).$promise;
var loginsPromise = apiService.ciphers.list({}, function (logins) {
decLogins = cipherService.decryptLogins(logins.Data);
var ciphersPromise = apiService.ciphers.list({}, function (ciphers) {
decCiphers = cipherService.decryptCiphers(ciphers.Data);
}).$promise;
$q.all([folderPromise, loginsPromise]).then(function () {
if (!decLogins.length) {
$q.all([folderPromise, ciphersPromise]).then(function () {
if (!decCiphers.length) {
toastr.error('Nothing to export.', 'Error!');
$scope.close();
return;
@@ -30,38 +30,61 @@
}
try {
var exportLogins = [];
for (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].folderId && (decLogins[i].folderId in foldersDict) ?
foldersDict[decLogins[i].folderId].name : null,
favorite: decLogins[i].favorite ? 1 : null,
totp: decLogins[i].totp,
fields: null
var exportCiphers = [];
for (i = 0; i < decCiphers.length; i++) {
// only export logins and secure notes
if (decCiphers[i].type !== constants.cipherType.login &&
decCiphers[i].type !== constants.cipherType.secureNote) {
continue;
}
var cipher = {
folder: decCiphers[i].folderId && (decCiphers[i].folderId in foldersDict) ?
foldersDict[decCiphers[i].folderId].name : null,
favorite: decCiphers[i].favorite ? 1 : null,
type: null,
name: decCiphers[i].name,
notes: decCiphers[i].notes,
fields: null,
// Login props
login_uri: null,
login_username: null,
login_password: null,
login_totp: null
};
if (decLogins[i].fields) {
for (var j = 0; j < decLogins[i].fields.length; j++) {
if (!login.fields) {
login.fields = '';
if (decCiphers[i].fields) {
for (var j = 0; j < decCiphers[i].fields.length; j++) {
if (!cipher.fields) {
cipher.fields = '';
}
else {
login.fields += '\n';
cipher.fields += '\n';
}
login.fields += ((decLogins[i].fields[j].name || '') + ': ' + decLogins[i].fields[j].value);
cipher.fields += ((decCiphers[i].fields[j].name || '') + ': ' + decCiphers[i].fields[j].value);
}
}
exportLogins.push(login);
switch (decCiphers[i].type) {
case constants.cipherType.login:
cipher.type = 'login';
cipher.login_uri = decCiphers[i].login.uri;
cipher.login_username = decCiphers[i].login.username;
cipher.login_password = decCiphers[i].login.password;
cipher.login_totp = decCiphers[i].login.totp;
break;
case constants.cipherType.secureNote:
cipher.type = 'note';
break;
default:
continue;
}
exportCiphers.push(cipher);
}
var csvString = Papa.unparse(exportLogins);
var csvString = Papa.unparse(exportCiphers);
var csvBlob = new Blob([csvString]);
// IE hack. ref http://msdn.microsoft.com/en-us/library/ie/hh779016.aspx

View File

@@ -102,7 +102,8 @@
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.')
'"As CSV". Select "Yes" to the warning alert and save the CSV file. Note that the importer only fully ' +
'supports files exported while Enpass is set to the English language, so adjust your settings accordingly.')
},
{
id: 'safeincloudxml',
@@ -237,7 +238,7 @@
{
id: 'gnomejson',
name: 'GNOME Passwords and Keys/Seahorse (json)',
instructions: $sce.trustAsHtml('Make sure you have python-keyring and python-gnomekeyring installed. ' +
instructions: $sce.trustAsHtml('Make sure you have python-keyring and python-gnomekeyring installed. ' +
'Save the <a target="_blank" href="http://bit.ly/2sMldAI">GNOME Keyring Import/Export</a> ' +
'python script by Luke Plant to your desktop as <code>pw_helper.py</code>. Open terminal and run ' +
'<code>chmod +rx Desktop/pw_helper.py</code> and then ' +
@@ -272,15 +273,15 @@
importService.import(model.source, file || model.fileContents, importSuccess, importError);
};
function importSuccess(folders, logins, folderRelationships) {
if (!folders.length && !logins.length) {
function importSuccess(folders, ciphers, folderRelationships) {
if (!folders.length && !ciphers.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])) {
else if (ciphers.length) {
var halfway = Math.floor(ciphers.length / 2);
var last = ciphers.length - 1;
if (cipherIsBadData(ciphers[0]) && cipherIsBadData(ciphers[halfway]) && cipherIsBadData(ciphers[last])) {
importError('CSV data is not formatted correctly. Please check your import file and try again.');
return;
}
@@ -288,7 +289,7 @@
apiService.ciphers.import({
folders: cipherService.encryptFolders(folders),
ciphers: cipherService.encryptLogins(logins),
ciphers: cipherService.encryptCiphers(ciphers),
folderRelationships: folderRelationships
}, function () {
$uibModalInstance.dismiss('cancel');
@@ -299,8 +300,9 @@
}, importError);
}
function loginIsBadData(login) {
return (login.name === null || login.name === '--') && (login.password === null || login.password === '');
function cipherIsBadData(cipher) {
return (cipher.name === null || cipher.name === '--') &&
(cipher.login && (cipher.login.password === null || cipher.login.password === ''));
}
function importError(error) {

View File

@@ -1,57 +1,70 @@
angular
.module('bit.vault')
.controller('vaultAddLoginController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService,
passwordService, selectedFolder, $analytics, checkedFavorite, $rootScope, authService, $uibModal) {
$analytics.eventTrack('vaultAddLoginController', { category: 'Modal' });
.controller('vaultAddCipherController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService,
passwordService, selectedFolder, $analytics, checkedFavorite, $rootScope, authService, $uibModal, constants) {
$analytics.eventTrack('vaultAddCipherController', { category: 'Modal' });
$scope.folders = $rootScope.vaultFolders;
$scope.login = {
$scope.constants = constants;
$scope.selectedType = constants.cipherType.login.toString();
$scope.cipher = {
folderId: selectedFolder ? selectedFolder.id : null,
favorite: checkedFavorite === true
favorite: checkedFavorite === true,
type: constants.cipherType.login,
login: {},
identity: {},
card: {},
secureNote: {
type: '0'
}
};
authService.getUserProfile().then(function (profile) {
$scope.useTotp = profile.premium;
});
$scope.typeChanged = function () {
$scope.cipher.type = parseInt($scope.selectedType);
};
$scope.savePromise = null;
$scope.save = function (model) {
var login = cipherService.encryptLogin(model);
$scope.savePromise = apiService.ciphers.post(login, function (loginResponse) {
$analytics.eventTrack('Created Login');
var decLogin = cipherService.decryptLogin(loginResponse);
$uibModalInstance.close(decLogin);
$scope.save = function () {
var cipher = cipherService.encryptCipher($scope.cipher);
$scope.savePromise = apiService.ciphers.post(cipher, function (cipherResponse) {
$analytics.eventTrack('Created Cipher');
var decCipher = cipherService.decryptCipherPreview(cipherResponse);
$uibModalInstance.close(decCipher);
}).$promise;
};
$scope.generatePassword = function () {
if (!$scope.login.password || confirm('Are you sure you want to overwrite the current password?')) {
if (!$scope.cipher.login.password || confirm('Are you sure you want to overwrite the current password?')) {
$analytics.eventTrack('Generated Password From Add');
$scope.login.password = passwordService.generatePassword({ length: 12, special: true });
$scope.cipher.login.password = passwordService.generatePassword({ length: 12, special: true });
}
};
$scope.addField = function () {
if (!$scope.login.fields) {
$scope.login.fields = [];
if (!$scope.cipher.fields) {
$scope.cipher.fields = [];
}
$scope.login.fields.push({
type: '0',
$scope.cipher.fields.push({
type: constants.fieldType.text.toString(),
name: null,
value: null
});
};
$scope.removeField = function (field) {
var index = $scope.login.fields.indexOf(field);
var index = $scope.cipher.fields.indexOf(field);
if (index > -1) {
$scope.login.fields.splice(index, 1);
$scope.cipher.fields.splice(index, 1);
}
};
$scope.toggleFavorite = function () {
$scope.login.favorite = !$scope.login.favorite;
$scope.cipher.favorite = !$scope.cipher.favorite;
};
$scope.clipboardSuccess = function (e) {

View File

@@ -2,9 +2,9 @@
.module('bit.vault')
.controller('vaultAttachmentsController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService,
loginId, $analytics, validationService, toastr, $timeout, authService, $uibModal) {
cipherId, $analytics, validationService, toastr, $timeout, authService, $uibModal) {
$analytics.eventTrack('vaultAttachmentsController', { category: 'Modal' });
$scope.login = {};
$scope.cipher = {};
$scope.readOnly = true;
$scope.loading = true;
$scope.isPremium = true;
@@ -13,11 +13,11 @@
authService.getUserProfile().then(function (profile) {
$scope.isPremium = profile.premium;
return apiService.ciphers.get({ id: loginId }).$promise;
return apiService.ciphers.get({ id: cipherId }).$promise;
}).then(function (cipher) {
$scope.login = cipherService.decryptLogin(cipher);
$scope.readOnly = !$scope.login.edit;
$scope.canUseAttachments = $scope.isPremium || $scope.login.organizationId;
$scope.cipher = cipherService.decryptCipher(cipher);
$scope.readOnly = !$scope.cipher.edit;
$scope.canUseAttachments = $scope.isPremium || $scope.cipher.organizationId;
$scope.loading = false;
}, function () {
$scope.loading = false;
@@ -31,14 +31,14 @@
return;
}
$scope.savePromise = cipherService.encryptAttachmentFile(getKeyForLogin(), files[0]).then(function (encValue) {
$scope.savePromise = cipherService.encryptAttachmentFile(getKeyForCipher(), files[0]).then(function (encValue) {
var fd = new FormData();
var blob = new Blob([encValue.data], { type: 'application/octet-stream' });
fd.append('data', blob, encValue.fileName);
return apiService.ciphers.postAttachment({ id: loginId }, fd).$promise;
return apiService.ciphers.postAttachment({ id: cipherId }, fd).$promise;
}).then(function (response) {
$analytics.eventTrack('Added Attachment');
$scope.login = cipherService.decryptLogin(response);
$scope.cipher = cipherService.decryptCipher(response);
// reset file input
// ref: https://stackoverflow.com/a/20552042
@@ -64,7 +64,7 @@
return;
}
cipherService.downloadAndDecryptAttachment(getKeyForLogin(), attachment, true).then(function (res) {
cipherService.downloadAndDecryptAttachment(getKeyForCipher(), attachment, true).then(function (res) {
$timeout(function () {
attachment.loading = false;
});
@@ -75,9 +75,9 @@
});
};
function getKeyForLogin() {
if ($scope.login.organizationId) {
return cryptoService.getOrgKey($scope.login.organizationId);
function getKeyForCipher() {
if ($scope.cipher.organizationId) {
return cryptoService.getOrgKey($scope.cipher.organizationId);
}
return null;
@@ -89,12 +89,12 @@
}
attachment.loading = true;
apiService.ciphers.delAttachment({ id: loginId, attachmentId: attachment.id }).$promise.then(function () {
apiService.ciphers.delAttachment({ id: cipherId, attachmentId: attachment.id }).$promise.then(function () {
attachment.loading = false;
$analytics.eventTrack('Deleted Attachment');
var index = $scope.login.attachments.indexOf(attachment);
var index = $scope.cipher.attachments.indexOf(attachment);
if (index > -1) {
$scope.login.attachments.splice(index, 1);
$scope.cipher.attachments.splice(index, 1);
}
}, function () {
toastr.error('Cannot delete attachment.');
@@ -113,7 +113,7 @@
e.preventDefault();
closing = true;
$uibModalInstance.close(!!$scope.login.attachments && $scope.login.attachments.length > 0);
$uibModalInstance.close(!!$scope.cipher.attachments && $scope.cipher.attachments.length > 0);
});
$scope.showUpgrade = function () {

View File

@@ -1,24 +1,25 @@
angular
.module('bit.vault')
.controller('vaultLoginCollectionsController', function ($scope, apiService, $uibModalInstance, cipherService,
loginId, $analytics) {
$analytics.eventTrack('vaultLoginCollectionsController', { category: 'Modal' });
$scope.login = {};
.controller('vaultCipherCollectionsController', function ($scope, apiService, $uibModalInstance, cipherService,
cipherId, $analytics) {
$analytics.eventTrack('vaultCipherCollectionsController', { category: 'Modal' });
$scope.cipher = {};
$scope.readOnly = false;
$scope.loadingLogin = true;
$scope.loadingCipher = true;
$scope.loadingCollections = true;
$scope.selectedCollections = {};
$scope.collections = [];
var cipherAndCols = null;
$uibModalInstance.opened.then(function () {
apiService.ciphers.getDetails({ id: loginId }).$promise.then(function (cipher) {
$scope.loadingLogin = false;
apiService.ciphers.getDetails({ id: cipherId }).$promise.then(function (cipher) {
$scope.loadingCipher = false;
$scope.readOnly = !cipher.Edit;
if (cipher.Edit && cipher.OrganizationId) {
if (cipher.Type === 1) {
$scope.login = cipherService.decryptLoginPreview(cipher);
$scope.cipher = cipherService.decryptCipherPreview(cipher);
}
var collections = {};
@@ -35,34 +36,40 @@
}
return null;
}).then(function (cipherAndCols) {
if (!cipherAndCols) {
}).then(function (result) {
if (!result) {
$scope.loadingCollections = false;
return false;
}
cipherAndCols = result;
return apiService.collections.listMe({ writeOnly: true }).$promise;
}).then(function (response) {
if (response === false) {
return;
}
apiService.collections.listMe({ writeOnly: true }, function (response) {
var collections = [];
var selectedCollections = {};
var collections = [];
var selectedCollections = {};
var writeableCollections = response.Data;
for (var i = 0; i < response.Data.length; i++) {
// clean out selectCollections that aren't from this organization or read only
if (response.Data[i].Id in cipherAndCols.cipherCollections &&
response.Data[i].OrganizationId === cipherAndCols.cipher.OrganizationId) {
selectedCollections[response.Data[i].Id] = true;
}
else {
continue;
}
var decCollection = cipherService.decryptCollection(response.Data[i]);
collections.push(decCollection);
for (var i = 0; i < writeableCollections.length; i++) {
// clean out selectCollections that aren't from this organization
if (writeableCollections[i].OrganizationId !== cipherAndCols.cipher.OrganizationId) {
continue;
}
$scope.loadingCollections = false;
$scope.collections = collections;
$scope.selectedCollections = selectedCollections;
});
if (writeableCollections[i].Id in cipherAndCols.cipherCollections) {
selectedCollections[writeableCollections[i].Id] = true;
}
var decCollection = cipherService.decryptCollection(writeableCollections[i]);
collections.push(decCollection);
}
$scope.loadingCollections = false;
$scope.collections = collections;
$scope.selectedCollections = selectedCollections;
});
});
@@ -105,9 +112,9 @@
}
}
$scope.submitPromise = apiService.ciphers.putCollections({ id: loginId }, request)
$scope.submitPromise = apiService.ciphers.putCollections({ id: cipherId }, request)
.$promise.then(function (response) {
$analytics.eventTrack('Edited Login Collections');
$analytics.eventTrack('Edited Cipher Collections');
$uibModalInstance.close({
action: 'collectionsEdit',
collectionIds: request.collectionIds

View File

@@ -2,21 +2,23 @@
.module('bit.vault')
.controller('vaultController', function ($scope, $uibModal, apiService, $filter, cryptoService, authService, toastr,
cipherService, $q, $localStorage, $timeout, $rootScope, $state, $analytics) {
cipherService, $q, $localStorage, $timeout, $rootScope, $state, $analytics, constants) {
$scope.loading = true;
$scope.logins = [];
$scope.ciphers = [];
$scope.constants = constants;
$scope.favoriteCollapsed = $localStorage.collapsedFolders && 'favorite' in $localStorage.collapsedFolders;
$scope.folderIdFilter = undefined;
$scope.typeFilter = undefined;
if ($state.params.refreshFromServer) {
$rootScope.vaultFolders = $rootScope.vaultLogins = null;
$rootScope.vaultFolders = $rootScope.vaultCiphers = null;
}
$scope.$on('$viewContentLoaded', function () {
if ($rootScope.vaultFolders && $rootScope.vaultLogins) {
if ($rootScope.vaultFolders && $rootScope.vaultCiphers) {
$scope.loading = false;
loadFolderData($rootScope.vaultFolders);
loadLoginData($rootScope.vaultLogins);
loadCipherData($rootScope.vaultCiphers);
return;
}
@@ -39,17 +41,15 @@
}).$promise;
var cipherPromise = apiService.ciphers.list({}, function (ciphers) {
var decLogins = [];
var decCiphers = [];
for (var i = 0; i < ciphers.Data.length; i++) {
if (ciphers.Data[i].Type === 1) {
var decLogin = cipherService.decryptLoginPreview(ciphers.Data[i]);
decLogins.push(decLogin);
}
var decCipher = cipherService.decryptCipherPreview(ciphers.Data[i]);
decCiphers.push(decCipher);
}
$q.when(folderPromise).then(function () {
loadLoginData(decLogins);
folderPromise.then(function () {
loadCipherData(decCiphers);
});
}).$promise;
@@ -62,26 +62,26 @@
$rootScope.vaultFolders = $filter('orderBy')(decFolders, folderSort);
}
function loadLoginData(decLogins) {
function loadCipherData(decCiphers) {
angular.forEach($rootScope.vaultFolders, function (folderValue, folderIndex) {
folderValue.collapsed = $localStorage.collapsedFolders &&
(folderValue.id || 'none') in $localStorage.collapsedFolders;
angular.forEach(decLogins, function (loginValue) {
if (loginValue.favorite) {
loginValue.sort = -1;
angular.forEach(decCiphers, function (cipherValue) {
if (cipherValue.favorite) {
cipherValue.sort = -1;
}
else if (loginValue.folderId == folderValue.id) {
loginValue.sort = folderIndex;
else if (cipherValue.folderId == folderValue.id) {
cipherValue.sort = folderIndex;
}
});
});
$rootScope.vaultLogins = $scope.logins = $filter('orderBy')(decLogins, ['sort', 'name', 'username']);
$rootScope.vaultCiphers = $scope.ciphers = $filter('orderBy')(decCiphers, ['sort', 'name', 'subTitle']);
var chunks = chunk($rootScope.vaultLogins, 400);
var chunks = chunk($rootScope.vaultCiphers, 400);
if (chunks.length > 0) {
$scope.logins = chunks[0];
$scope.ciphers = chunks[0];
var delay = 200;
angular.forEach(chunks, function (value, index) {
delay += 200;
@@ -89,15 +89,15 @@
// skip the first chuck
if (index > 0) {
$timeout(function () {
Array.prototype.push.apply($scope.logins, value);
Array.prototype.push.apply($scope.ciphers, value);
}, delay);
}
});
}
}
function sortScopedLoginData() {
$rootScope.vaultLogins = $scope.logins = $filter('orderBy')($rootScope.vaultLogins, ['name', 'username']);
function sortScopedCipherData() {
$rootScope.vaultCiphers = $scope.ciphers = $filter('orderBy')($rootScope.vaultCiphers, ['name', 'subTitle']);
}
function chunk(arr, len) {
@@ -120,7 +120,7 @@
$scope.clipboardError = function (e) {
alert('Your web browser does not support easy clipboard copying. ' +
'Edit the login and copy it manually instead.');
'Edit the item and copy it manually instead.');
};
$scope.collapseExpand = function (folder, favorite) {
@@ -137,89 +137,87 @@
}
};
$scope.editLogin = function (login) {
$scope.editCipher = function (cipher) {
var editModel = $uibModal.open({
animation: true,
templateUrl: 'app/vault/views/vaultEditLogin.html',
controller: 'vaultEditLoginController',
templateUrl: 'app/vault/views/vaultEditCipher.html',
controller: 'vaultEditCipherController',
resolve: {
loginId: function () { return login.id; }
cipherId: function () { return cipher.id; }
}
});
editModel.result.then(function (returnVal) {
if (returnVal.action === 'edit') {
login.folderId = returnVal.data.folderId;
login.name = returnVal.data.name;
login.username = returnVal.data.username;
login.password = returnVal.data.password;
login.favorite = returnVal.data.favorite;
sortScopedLoginData();
var index = $scope.ciphers.indexOf(cipher);
if (index > -1) {
$rootScope.vaultCiphers[index] = returnVal.data;
}
sortScopedCipherData();
}
else if (returnVal.action === 'partialEdit') {
login.folderId = returnVal.data.folderId;
login.favorite = returnVal.data.favorite;
cipher.folderId = returnVal.data.folderId;
cipher.favorite = returnVal.data.favorite;
}
else if (returnVal.action === 'delete') {
removeLoginFromScopes(login);
removeCipherFromScopes(cipher);
}
});
};
$scope.$on('vaultAddLogin', function (event, args) {
$scope.addLogin();
$scope.$on('vaultAddCipher', function (event, args) {
$scope.addCipher();
});
$scope.addLogin = function (folder, favorite) {
$scope.addCipher = function (folder, favorite) {
var addModel = $uibModal.open({
animation: true,
templateUrl: 'app/vault/views/vaultAddLogin.html',
controller: 'vaultAddLoginController',
templateUrl: 'app/vault/views/vaultAddCipher.html',
controller: 'vaultAddCipherController',
resolve: {
selectedFolder: function () { return folder; },
checkedFavorite: function () { return favorite; }
}
});
addModel.result.then(function (addedLogin) {
$rootScope.vaultLogins.push(addedLogin);
sortScopedLoginData();
addModel.result.then(function (addedCipher) {
$rootScope.vaultCiphers.push(addedCipher);
sortScopedCipherData();
});
};
$scope.deleteLogin = function (login) {
if (!confirm('Are you sure you want to delete this login (' + login.name + ')?')) {
$scope.deleteCipher = function (cipher) {
if (!confirm('Are you sure you want to delete this item (' + cipher.name + ')?')) {
return;
}
apiService.ciphers.del({ id: login.id }, function () {
$analytics.eventTrack('Deleted Login');
removeLoginFromScopes(login);
apiService.ciphers.del({ id: cipher.id }, function () {
$analytics.eventTrack('Deleted Item');
removeCipherFromScopes(cipher);
});
};
$scope.attachments = function (login) {
$scope.attachments = function (cipher) {
authService.getUserProfile().then(function (profile) {
return {
isPremium: profile.premium,
orgUseStorage: login.organizationId && !!profile.organizations[login.organizationId].maxStorageGb
orgUseStorage: cipher.organizationId && !!profile.organizations[cipher.organizationId].maxStorageGb
};
}).then(function (perms) {
if (!login.hasAttachments) {
if (login.organizationId && !perms.orgUseStorage) {
if (!cipher.hasAttachments) {
if (cipher.organizationId && !perms.orgUseStorage) {
$uibModal.open({
animation: true,
templateUrl: 'app/views/paidOrgRequired.html',
controller: 'paidOrgRequiredController',
resolve: {
orgId: function () { return login.organizationId; }
orgId: function () { return cipher.organizationId; }
}
});
return;
}
if (!login.organizationId && !perms.isPremium) {
if (!cipher.organizationId && !perms.isPremium) {
$uibModal.open({
animation: true,
templateUrl: 'app/views/premiumRequired.html',
@@ -229,7 +227,7 @@
}
}
if (!login.organizationId && !cryptoService.getEncKey()) {
if (!cipher.organizationId && !cryptoService.getEncKey()) {
toastr.error('You cannot use this feature until you update your encryption key.', 'Feature Unavailable');
return;
}
@@ -239,12 +237,12 @@
templateUrl: 'app/vault/views/vaultAttachments.html',
controller: 'vaultAttachmentsController',
resolve: {
loginId: function () { return login.id; }
cipherId: function () { return cipher.id; }
}
});
attachmentModel.result.then(function (hasAttachments) {
login.hasAttachments = hasAttachments;
cipher.hasAttachments = hasAttachments;
});
});
};
@@ -298,42 +296,42 @@
};
$scope.canDeleteFolder = function (folder) {
if (!folder || !folder.id || !$rootScope.vaultLogins) {
if (!folder || !folder.id || !$rootScope.vaultCiphers) {
return false;
}
var logins = $filter('filter')($rootScope.vaultLogins, { folderId: folder.id });
return logins && logins.length === 0;
var ciphers = $filter('filter')($rootScope.vaultCiphers, { folderId: folder.id });
return ciphers && ciphers.length === 0;
};
$scope.share = function (login) {
$scope.share = function (cipher) {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/vault/views/vaultShareLogin.html',
controller: 'vaultShareLoginController',
templateUrl: 'app/vault/views/vaultShareCipher.html',
controller: 'vaultShareCipherController',
resolve: {
loginId: function () { return login.id; }
cipherId: function () { return cipher.id; }
}
});
modal.result.then(function (orgId) {
login.organizationId = orgId;
cipher.organizationId = orgId;
});
};
$scope.collections = function (login) {
$scope.collections = function (cipher) {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/vault/views/vaultLoginCollections.html',
controller: 'vaultLoginCollectionsController',
templateUrl: 'app/vault/views/vaultCipherCollections.html',
controller: 'vaultCipherCollectionsController',
resolve: {
loginId: function () { return login.id; }
cipherId: function () { return cipher.id; }
}
});
modal.result.then(function (response) {
if (response.collectionIds && !response.collectionIds.length) {
removeLoginFromScopes(login);
removeCipherFromScopes(cipher);
}
});
};
@@ -348,8 +346,19 @@
}
};
$scope.filterType = function (type) {
$scope.typeFilter = type;
if ($.AdminLTE && $.AdminLTE.layout) {
$timeout(function () {
$.AdminLTE.layout.fix();
}, 0);
}
};
$scope.clearFilters = function () {
$scope.folderIdFilter = undefined;
$scope.typeFilter = undefined;
if ($.AdminLTE && $.AdminLTE.layout) {
$timeout(function () {
@@ -362,17 +371,21 @@
return $scope.folderIdFilter === undefined || folder.id === $scope.folderIdFilter;
};
$scope.cipherFilter = function (cipher) {
return $scope.typeFilter === undefined || cipher.type === $scope.typeFilter;
};
$scope.unselectAll = function () {
selectAll(false);
};
$scope.selectFolder = function (folder, $event) {
var checkbox = $($event.currentTarget).closest('.box').find('input[name="loginSelection"]');
var checkbox = $($event.currentTarget).closest('.box').find('input[name="cipherSelection"]');
checkbox.prop('checked', true);
};
$scope.select = function ($event) {
var checkbox = $($event.currentTarget).closest('tr').find('input[name="loginSelection"]');
var checkbox = $($event.currentTarget).closest('tr').find('input[name="cipherSelection"]');
checkbox.prop('checked', !checkbox.prop('checked'));
};
@@ -380,18 +393,18 @@
return self.indexOf(value) === index;
}
function getSelectedLogins() {
return $('input[name="loginSelection"]:checked').map(function () {
function getSelectedCiphers() {
return $('input[name="cipherSelection"]:checked').map(function () {
return $(this).val();
}).get().filter(distinct);
}
function selectAll(select) {
$('input[name="loginSelection"]').prop('checked', select);
$('input[name="cipherSelection"]').prop('checked', select);
}
$scope.bulkMove = function () {
var ids = getSelectedLogins();
var ids = getSelectedCiphers();
if (ids.length === 0) {
alert('You have not selected anything.');
return;
@@ -399,8 +412,8 @@
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/vault/views/vaultMoveLogins.html',
controller: 'vaultMoveLoginsController',
templateUrl: 'app/vault/views/vaultMoveCiphers.html',
controller: 'vaultMoveCiphersController',
size: 'sm',
resolve: {
ids: function () { return ids; }
@@ -409,37 +422,37 @@
modal.result.then(function (folderId) {
for (var i = 0; i < ids.length; i++) {
var login = $filter('filter')($rootScope.vaultLogins, { id: ids[i] });
if (login.length) {
login[0].folderId = folderId;
var cipher = $filter('filter')($rootScope.vaultCiphers, { id: ids[i] });
if (cipher.length) {
cipher[0].folderId = folderId;
}
}
selectAll(false);
sortScopedLoginData();
sortScopedCipherData();
toastr.success('Items have been moved!');
});
};
$scope.bulkDelete = function () {
var ids = getSelectedLogins();
var ids = getSelectedCiphers();
if (ids.length === 0) {
alert('You have not selected anything.');
return;
}
if (!confirm('Are you sure you want to delete the selected logins (total: ' + ids.length + ')?')) {
if (!confirm('Are you sure you want to delete the selected items (total: ' + ids.length + ')?')) {
return;
}
$scope.bulkActionLoading = true;
apiService.ciphers.delMany({ ids: ids }, function () {
$analytics.eventTrack('Bulk Deleted Logins');
$analytics.eventTrack('Bulk Deleted Items');
for (var i = 0; i < ids.length; i++) {
var login = $filter('filter')($rootScope.vaultLogins, { id: ids[i] });
if (login.length && login[0].edit) {
removeLoginFromScopes(login[0]);
var cipher = $filter('filter')($rootScope.vaultCiphers, { id: ids[i] });
if (cipher.length && cipher[0].edit) {
removeCipherFromScopes(cipher[0]);
}
}
@@ -452,15 +465,15 @@
});
};
function removeLoginFromScopes(login) {
var index = $rootScope.vaultLogins.indexOf(login);
function removeCipherFromScopes(cipher) {
var index = $rootScope.vaultCiphers.indexOf(cipher);
if (index > -1) {
$rootScope.vaultLogins.splice(index, 1);
$rootScope.vaultCiphers.splice(index, 1);
}
index = $scope.logins.indexOf(login);
index = $scope.ciphers.indexOf(cipher);
if (index > -1) {
$scope.logins.splice(index, 1);
$scope.ciphers.splice(index, 1);
}
}
});

View File

@@ -1,33 +1,34 @@
angular
.module('bit.vault')
.controller('vaultEditLoginController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService,
passwordService, loginId, $analytics, $rootScope, authService, $uibModal) {
$analytics.eventTrack('vaultEditLoginController', { category: 'Modal' });
.controller('vaultEditCipherController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService,
passwordService, cipherId, $analytics, $rootScope, authService, $uibModal, constants) {
$analytics.eventTrack('vaultEditCipherController', { category: 'Modal' });
$scope.folders = $rootScope.vaultFolders;
$scope.login = {};
$scope.cipher = {};
$scope.readOnly = false;
$scope.constants = constants;
authService.getUserProfile().then(function (profile) {
$scope.useTotp = profile.premium;
return apiService.ciphers.get({ id: loginId }).$promise;
}).then(function (login) {
$scope.login = cipherService.decryptLogin(login);
$scope.readOnly = !$scope.login.edit;
$scope.useTotp = $scope.useTotp || $scope.login.organizationUseTotp;
return apiService.ciphers.get({ id: cipherId }).$promise;
}).then(function (cipher) {
$scope.cipher = cipherService.decryptCipher(cipher);
$scope.readOnly = !$scope.cipher.edit;
$scope.useTotp = $scope.useTotp || $scope.cipher.organizationUseTotp;
});
$scope.save = function (model) {
if ($scope.readOnly) {
$scope.savePromise = apiService.ciphers.putPartial({ id: loginId }, {
$scope.savePromise = apiService.ciphers.putPartial({ id: cipherId }, {
folderId: model.folderId,
favorite: model.favorite
}, function (response) {
$analytics.eventTrack('Partially Edited Login');
$analytics.eventTrack('Partially Edited Cipher');
$uibModalInstance.close({
action: 'partialEdit',
data: {
id: loginId,
id: cipherId,
favorite: model.favorite,
folderId: model.folderId && model.folderId !== '' ? model.folderId : null
}
@@ -35,46 +36,46 @@
}).$promise;
}
else {
var login = cipherService.encryptLogin(model);
$scope.savePromise = apiService.ciphers.put({ id: loginId }, login, function (loginResponse) {
$analytics.eventTrack('Edited Login');
var decLogin = cipherService.decryptLogin(loginResponse);
var cipher = cipherService.encryptCipher(model, $scope.cipher.type);
$scope.savePromise = apiService.ciphers.put({ id: cipherId }, cipher, function (cipherResponse) {
$analytics.eventTrack('Edited Cipher');
var decCipher = cipherService.decryptCipherPreview(cipherResponse);
$uibModalInstance.close({
action: 'edit',
data: decLogin
data: decCipher
});
}).$promise;
}
};
$scope.generatePassword = function () {
if (!$scope.login.password || confirm('Are you sure you want to overwrite the current password?')) {
if (!$scope.cipher.login.password || confirm('Are you sure you want to overwrite the current password?')) {
$analytics.eventTrack('Generated Password From Edit');
$scope.login.password = passwordService.generatePassword({ length: 12, special: true });
$scope.cipher.login.password = passwordService.generatePassword({ length: 12, special: true });
}
};
$scope.addField = function () {
if (!$scope.login.fields) {
$scope.login.fields = [];
if (!$scope.cipher.fields) {
$scope.cipher.fields = [];
}
$scope.login.fields.push({
type: '0',
$scope.cipher.fields.push({
type: constants.fieldType.text.toString(),
name: null,
value: null
});
};
$scope.removeField = function (field) {
var index = $scope.login.fields.indexOf(field);
var index = $scope.cipher.fields.indexOf(field);
if (index > -1) {
$scope.login.fields.splice(index, 1);
$scope.cipher.fields.splice(index, 1);
}
};
$scope.toggleFavorite = function () {
$scope.login.favorite = !$scope.login.favorite;
$scope.cipher.favorite = !$scope.cipher.favorite;
};
$scope.clipboardSuccess = function (e) {
@@ -105,15 +106,15 @@
}
$scope.delete = function () {
if (!confirm('Are you sure you want to delete this login (' + $scope.login.name + ')?')) {
if (!confirm('Are you sure you want to delete this item (' + $scope.cipher.name + ')?')) {
return;
}
apiService.ciphers.del({ id: $scope.login.id }, function () {
$analytics.eventTrack('Deleted Login From Edit');
apiService.ciphers.del({ id: $scope.cipher.id }, function () {
$analytics.eventTrack('Deleted Cipher From Edit');
$uibModalInstance.close({
action: 'delete',
data: $scope.login.id
data: $scope.cipher.id
});
});
};

View File

@@ -1,15 +1,15 @@
angular
.module('bit.vault')
.controller('vaultMoveLoginsController', function ($scope, apiService, $uibModalInstance, ids, $analytics,
.controller('vaultMoveCiphersController', function ($scope, apiService, $uibModalInstance, ids, $analytics,
$rootScope) {
$analytics.eventTrack('vaultMoveLoginsController', { category: 'Modal' });
$analytics.eventTrack('vaultMoveCiphersController', { category: 'Modal' });
$scope.folders = $rootScope.vaultFolders;
$scope.count = ids.length;
$scope.save = function () {
$scope.savePromise = apiService.ciphers.moveMany({ ids: ids, folderId: $scope.folderId }, function () {
$analytics.eventTrack('Bulk Moved Logins');
$analytics.eventTrack('Bulk Moved Ciphers');
$uibModalInstance.close($scope.folderId || null);
}).$promise;
};

View File

@@ -1,11 +1,11 @@
angular
.module('bit.vault')
.controller('vaultShareLoginController', function ($scope, apiService, $uibModalInstance, authService, cipherService,
loginId, $analytics, $state, cryptoService, $q, toastr) {
$analytics.eventTrack('vaultShareLoginController', { category: 'Modal' });
.controller('vaultShareCipherController', function ($scope, apiService, $uibModalInstance, authService, cipherService,
cipherId, $analytics, $state, cryptoService, $q, toastr) {
$analytics.eventTrack('vaultShareCipherController', { category: 'Modal' });
$scope.model = {};
$scope.login = {};
$scope.cipher = {};
$scope.collections = [];
$scope.selectedCollections = {};
$scope.organizations = [];
@@ -14,13 +14,13 @@
$scope.loading = true;
$scope.readOnly = false;
apiService.ciphers.get({ id: loginId }).$promise.then(function (login) {
$scope.readOnly = !login.Edit;
if (login.Edit) {
$scope.login = cipherService.decryptLogin(login);
apiService.ciphers.get({ id: cipherId }).$promise.then(function (cipher) {
$scope.readOnly = !cipher.Edit;
if (cipher.Edit) {
$scope.cipher = cipherService.decryptCipher(cipher);
}
return login.Edit;
return cipher.Edit;
}).then(function (canEdit) {
$scope.loading = false;
if (!canEdit) {
@@ -110,8 +110,9 @@
var errorOnUpload = false;
var attachmentSharePromises = [];
if ($scope.login.attachments) {
for (var i = 0; i < $scope.login.attachments.length; i++) {
if ($scope.cipher.attachments) {
for (var i = 0; i < $scope.cipher.attachments.length; i++) {
/* jshint ignore:start */
(function (attachment) {
var promise = cipherService.downloadAndDecryptAttachment(null, attachment, false)
.then(function (decData) {
@@ -127,7 +128,7 @@
fd.append('data', blob, encFilename);
return apiService.ciphers.postShareAttachment({
id: loginId,
id: cipherId,
attachmentId: attachment.id,
orgId: model.organizationId
}, fd).$promise;
@@ -135,7 +136,8 @@
errorOnUpload = true;
});
attachmentSharePromises.push(promise);
})($scope.login.attachments[i]);
})($scope.cipher.attachments[i]);
/* jshint ignore:end */
}
}
@@ -144,11 +146,11 @@
return;
}
$scope.login.organizationId = model.organizationId;
$scope.cipher.organizationId = model.organizationId;
var request = {
collectionIds: [],
cipher: cipherService.encryptLogin($scope.login, null, true)
cipher: cipherService.encryptCipher($scope.cipher, $scope.cipher.type, null, true)
};
for (var id in $scope.selectedCollections) {
@@ -157,10 +159,10 @@
}
}
return apiService.ciphers.putShare({ id: loginId }, request).$promise;
return apiService.ciphers.putShare({ id: cipherId }, request).$promise;
}).then(function (response) {
$analytics.eventTrack('Shared Login');
toastr.success('Login has been shared.');
$analytics.eventTrack('Shared Cipher');
toastr.success('Item has been shared.');
$uibModalInstance.close(model.organizationId);
});
};

View File

@@ -3,7 +3,7 @@
.controller('vaultSharedController', function ($scope, apiService, cipherService, $analytics, $q, $localStorage,
$uibModal, $filter, $rootScope, authService, cryptoService) {
$scope.logins = [];
$scope.ciphers = [];
$scope.collections = [];
$scope.loading = true;
@@ -22,16 +22,14 @@
}).$promise;
var cipherPromise = apiService.ciphers.listDetails({}, function (ciphers) {
var decLogins = [];
var decCiphers = [];
for (var i = 0; i < ciphers.Data.length; i++) {
if (ciphers.Data[i].Type === 1) {
var decLogin = cipherService.decryptLoginPreview(ciphers.Data[i]);
decLogins.push(decLogin);
}
var decCipher = cipherService.decryptCipherPreview(ciphers.Data[i]);
decCiphers.push(decCipher);
}
if (decLogins.length) {
if (decCiphers.length) {
$scope.collections.push({
id: null,
name: 'Unassigned',
@@ -39,7 +37,7 @@
});
}
$scope.logins = decLogins;
$scope.ciphers = decCiphers;
}).$promise;
$q.all([collectionPromise, cipherPromise]).then(function () {
@@ -49,29 +47,29 @@
$scope.clipboardError = function (e) {
alert('Your web browser does not support easy clipboard copying. ' +
'Edit the login and copy it manually instead.');
'Edit the item and copy it manually instead.');
};
$scope.attachments = function (login) {
$scope.attachments = function (cipher) {
authService.getUserProfile().then(function (profile) {
return {
isPremium: profile.premium,
orgUseStorage: login.organizationId && !!profile.organizations[login.organizationId].maxStorageGb
orgUseStorage: cipher.organizationId && !!profile.organizations[cipher.organizationId].maxStorageGb
};
}).then(function (perms) {
if (login.organizationId && !perms.orgUseStorage) {
if (cipher.organizationId && !perms.orgUseStorage) {
$uibModal.open({
animation: true,
templateUrl: 'app/views/paidOrgRequired.html',
controller: 'paidOrgRequiredController',
resolve: {
orgId: function () { return login.organizationId; }
orgId: function () { return cipher.organizationId; }
}
});
return;
}
if (!login.organizationId && !perms.isPremium) {
if (!cipher.organizationId && !perms.isPremium) {
$uibModal.open({
animation: true,
templateUrl: 'app/views/premiumRequired.html',
@@ -80,7 +78,7 @@
return;
}
if (!login.organizationId && !cryptoService.getEncKey()) {
if (!cipher.organizationId && !cryptoService.getEncKey()) {
toastr.error('You cannot use this feature until you update your encryption key.', 'Feature Unavailable');
return;
}
@@ -90,12 +88,12 @@
templateUrl: 'app/vault/views/vaultAttachments.html',
controller: 'vaultAttachmentsController',
resolve: {
loginId: function () { return login.id; }
cipherId: function () { return cipher.id; }
}
});
attachmentModel.result.then(function (hasAttachments) {
login.hasAttachments = hasAttachments;
cipher.hasAttachments = hasAttachments;
});
});
};
@@ -133,62 +131,70 @@
}
};
$scope.editLogin = function (login) {
$scope.editCipher = function (cipher) {
var editModel = $uibModal.open({
animation: true,
templateUrl: 'app/vault/views/vaultEditLogin.html',
controller: 'vaultEditLoginController',
templateUrl: 'app/vault/views/vaultEditCipher.html',
controller: 'vaultEditCipherController',
resolve: {
loginId: function () { return login.id; }
cipherId: function () { return cipher.id; }
}
});
editModel.result.then(function (returnVal) {
var rootLogin = findRootLogin(login) || {};
var rootCipher = findRootCipher(cipher) || { meta: {} },
index;
if (returnVal.action === 'edit') {
login.folderId = rootLogin.folderId = returnVal.data.folderId;
login.name = rootLogin.name = returnVal.data.name;
login.username = rootLogin.username = returnVal.data.username;
login.password = rootLogin.password = returnVal.data.password;
login.favorite = rootLogin.favorite = returnVal.data.favorite;
index = $scope.ciphers.indexOf(cipher);
if (index > -1) {
returnVal.data.collectionIds = $scope.ciphers[index].collectionIds;
$scope.ciphers[index] = returnVal.data;
if ($rootScope.vaultCiphers) {
index = $rootScope.vaultCiphers.indexOf(rootCipher);
if (index > -1) {
$rootScope.vaultCiphers[index] = returnVal.data;
}
}
}
}
else if (returnVal.action === 'partialEdit') {
login.folderId = rootLogin.folderId = returnVal.data.folderId;
login.favorite = rootLogin.favorite = returnVal.data.favorite;
cipher.folderId = rootCipher.folderId = returnVal.data.folderId;
cipher.favorite = rootCipher.favorite = returnVal.data.favorite;
}
else if (returnVal.action === 'delete') {
var index = $scope.logins.indexOf(login);
index = $scope.ciphers.indexOf(cipher);
if (index > -1) {
$scope.logins.splice(index, 1);
$scope.ciphers.splice(index, 1);
}
removeRootLogin(rootLogin);
removeRootCipher(rootCipher);
}
});
};
$scope.editCollections = function (login) {
$scope.editCollections = function (cipher) {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/vault/views/vaultLoginCollections.html',
controller: 'vaultLoginCollectionsController',
templateUrl: 'app/vault/views/vaultCipherCollections.html',
controller: 'vaultCipherCollectionsController',
resolve: {
loginId: function () { return login.id; }
cipherId: function () { return cipher.id; }
}
});
modal.result.then(function (response) {
if (response.collectionIds) {
login.collectionIds = response.collectionIds;
// TODO: if there are no collectionIds now, it is possible that the user no longer has access to this login
// which means it should be removed by calling removeRootLogin(findRootLogin(login))
cipher.collectionIds = response.collectionIds;
// TODO: if there are no collectionIds now, it is possible that the user no longer has access to this cipher
// which means it should be removed by calling removeRootCipher(findRootCipher(cipher))
}
});
};
$scope.removeLogin = function (login, collection) {
if (!confirm('Are you sure you want to remove this login (' + login.name + ') from the ' +
$scope.removeCipher = function (cipher, collection) {
if (!confirm('Are you sure you want to remove this item (' + cipher.name + ') from the ' +
'collection (' + collection.name + ') ?')) {
return;
}
@@ -197,36 +203,36 @@
collectionIds: []
};
for (var i = 0; i < login.collectionIds.length; i++) {
if (login.collectionIds[i] !== collection.id) {
request.collectionIds.push(login.collectionIds[i]);
for (var i = 0; i < cipher.collectionIds.length; i++) {
if (cipher.collectionIds[i] !== collection.id) {
request.collectionIds.push(cipher.collectionIds[i]);
}
}
apiService.ciphers.putCollections({ id: login.id }, request).$promise.then(function (response) {
apiService.ciphers.putCollections({ id: cipher.id }, request).$promise.then(function (response) {
$analytics.eventTrack('Removed From Collection');
login.collectionIds = request.collectionIds;
// TODO: if there are no collectionIds now, it is possible that the user no longer has access to this login
// which means it should be removed by calling removeRootLogin(findRootLogin(login))
cipher.collectionIds = request.collectionIds;
// TODO: if there are no collectionIds now, it is possible that the user no longer has access to this cipher
// which means it should be removed by calling removeRootCipher(findRootCipher(cipher))
});
};
function findRootLogin(login) {
if ($rootScope.vaultLogins) {
var rootLogins = $filter('filter')($rootScope.vaultLogins, { id: login.id });
if (rootLogins && rootLogins.length) {
return rootLogins[0];
function findRootCipher(cipher) {
if ($rootScope.vaultCiphers) {
var rootCiphers = $filter('filter')($rootScope.vaultCiphers, { id: cipher.id });
if (rootCiphers && rootCiphers.length) {
return rootCiphers[0];
}
}
return null;
}
function removeRootLogin(rootLogin) {
if (rootLogin && rootLogin.id) {
var index = $rootScope.vaultLogins.indexOf(rootLogin);
function removeRootCipher(rootCipher) {
if (rootCipher && rootCipher.id) {
var index = $rootScope.vaultCiphers.indexOf(rootCipher);
if (index > -1) {
$rootScope.vaultLogins.splice(index, 1);
$rootScope.vaultCiphers.splice(index, 1);
}
}
}

View File

@@ -30,7 +30,7 @@
<span ng-pluralize
count="vaultFolders.length > 0 ? vaultFolders.length - 1 : 0"
when="{'1': '{} folder', 'other': '{} folders'}"></span>,
<span ng-pluralize count="logins.length" when="{'1': '{} login', 'other': '{} logins'}"></span>
<span ng-pluralize count="ciphers.length" when="{'1': '{} item', 'other': '{} items'}"></span>
</small>
</h1>
</section>
@@ -39,12 +39,12 @@
<p>Loading...</p>
</div>
<div class="box box-primary" ng-class="{'collapsed-box': favoriteCollapsed}" style="margin-bottom: 40px;"
ng-show="vaultFolders.length && folderIdFilter === undefined && (!main.searchVaultText || favoriteLogins.length)">
ng-show="vaultFolders.length && folderIdFilter === undefined && (!main.searchVaultText || favoriteCiphers.length)">
<div class="box-header with-border">
<h3 class="box-title">
<i class="fa fa-star"></i>
Favorites
<small ng-pluralize count="favoriteLogins.length" when="{'1': '{} login', 'other': '{} logins'}"></small>
<small ng-pluralize count="favoriteCiphers.length" when="{'1': '{} item', 'other': '{} items'}"></small>
</h3>
<div class="box-tools">
<div class="btn-group">
@@ -53,8 +53,8 @@
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li>
<a href="#" stop-click ng-click="addLogin(null, true)">
<i class="fa fa-fw fa-plus-circle"></i> Add Login
<a href="#" stop-click ng-click="addCipher(null, true)">
<i class="fa fa-fw fa-plus-circle"></i> New Item
</a>
</li>
</ul>
@@ -65,16 +65,16 @@
</button>
</div>
</div>
<div class="box-body" ng-class="{'no-padding': favoriteLogins.length}">
<div ng-show="!favoriteLogins.length">
<p>No favorite logins.</p>
<button type="button" ng-click="addLogin(null, true)" class="btn btn-default btn-flat">Add a Login</button>
<div class="box-body" ng-class="{'no-padding': favoriteCiphers.length}">
<div ng-show="!favoriteCiphers.length">
<p>No favorite items.</p>
<button type="button" ng-click="addCipher(null, true)" class="btn btn-default btn-flat">Add an Item</button>
</div>
<div class="table-responsive" ng-show="favoriteLogins.length">
<div class="table-responsive" ng-show="favoriteCiphers.length">
<table class="table table-striped table-hover table-vmiddle">
<tbody>
<tr ng-repeat="login in favoriteLogins = (logins | filter: { favorite: true } |
filter: (main.searchVaultText || '')) track by login.id">
<tr ng-repeat="cipher in favoriteCiphers = (ciphers | filter: { favorite: true } |
filter: cipherFilter | filter: (main.searchVaultText || '')) track by cipher.id">
<td style="width: 70px;">
<div class="btn-group" data-append-to="body">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
@@ -82,49 +82,54 @@
</button>
<ul class="dropdown-menu">
<li>
<a href="#" stop-click ng-click="editLogin(login)">
<a href="#" stop-click ng-click="editCipher(cipher)">
<i class="fa fa-fw fa-pencil"></i> Edit
</a>
</li>
<li>
<a href="#" stop-click ng-click="attachments(login)">
<a href="#" stop-click ng-click="attachments(cipher)">
<i class="fa fa-fw fa-paperclip"></i> Attachments
</a>
</li>
<li ng-show="!login.organizationId">
<a href="#" stop-click ng-click="share(login)">
<li ng-show="!cipher.organizationId">
<a href="#" stop-click ng-click="share(cipher)">
<i class="fa fa-fw fa-share-alt"></i> Share
</a>
</li>
<li ng-show="login.organizationId && login.edit">
<a href="#" stop-click ng-click="collections(login)">
<li ng-show="cipher.organizationId && cipher.edit">
<a href="#" stop-click ng-click="collections(cipher)">
<i class="fa fa-fw fa-cubes"></i> Collections
</a>
</li>
<li ng-show="login.password">
<li ng-show="cipher.meta.password">
<a href="#" stop-click ngclipboard ngclipboard-error="clipboardError(e)"
data-clipboard-text="{{login.password}}">
data-clipboard-text="{{cipher.meta.password}}">
<i class="fa fa-fw fa-clipboard"></i> Copy Password
</a>
</li>
<li ng-show="login.edit">
<a href="#" stop-click ng-click="deleteLogin(login)" class="text-red">
<li ng-show="cipher.edit">
<a href="#" stop-click ng-click="deleteCipher(cipher)" class="text-red">
<i class="fa fa-fw fa-trash"></i> Delete
</a>
</li>
</ul>
</div>
</td>
<td class="action-select">
<input type="checkbox" value="{{::login.id}}" name="loginSelection" />
<td class="action-select" ng-click="select($event)">
<input type="checkbox" value="{{::cipher.id}}" name="cipherSelection" stop-prop />
</td>
<td class="vault-icon" ng-click="select($event)">
<i class="fa fa-fw fa-lg {{::cipher.icon}}" ng-if="!cipher.meta.image"></i>
<img alt="" ng-if="cipher.meta.image" ng-src="{{cipher.meta.image}}"
fallback-src="images/fa-globe.png" />
</td>
<td ng-click="select($event)">
<a href="#" stop-click ng-click="editLogin(login)" stop-prop>{{login.name}}</a>
<i class="fa fa-share-alt text-muted" title="Shared" ng-if="login.organizationId"
<a href="#" stop-click ng-click="editCipher(cipher)" stop-prop>{{cipher.name}}</a>
<i class="fa fa-share-alt text-muted" title="Shared" ng-if="cipher.organizationId"
stop-prop></i>
<i class="fa fa-paperclip text-muted" title="Attachments" ng-if="login.hasAttachments"
<i class="fa fa-paperclip text-muted" title="Attachments" ng-if="cipher.hasAttachments"
stop-prop></i><br />
<span class="text-sm text-muted" stop-prop>{{login.username}}</span>
<span class="text-sm text-muted" stop-prop>{{cipher.subTitle}}</span>
</td>
</tr>
</tbody>
@@ -134,12 +139,12 @@
</div>
<div class="box" ng-class="{'collapsed-box': folder.collapsed}"
ng-repeat="folder in filteredVaultFolders = (vaultFolders | filter: folderFilter) track by folder.id"
ng-show="vaultFolders.length && (!main.searchVaultText || folderLogins.length)">
ng-show="vaultFolders.length && (!main.searchVaultText || folderCiphers.length)">
<div class="box-header with-border">
<h3 class="box-title">
<i class="fa" ng-class="{'fa-folder-open': folder.id !== null, 'fa-folder-open-o': folder.id === null}"></i>
{{folder.name}}
<small ng-pluralize count="folderLogins.length" when="{'1': '{} login', 'other': '{} logins'}"></small>
<small ng-pluralize count="folderCiphers.length" when="{'1': '{} item', 'other': '{} items'}"></small>
</h3>
<div class="box-tools">
<div class="btn-group">
@@ -148,8 +153,8 @@
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li>
<a href="#" stop-click ng-click="addLogin(folder)">
<i class="fa fa-fw fa-plus-circle"></i> Add Login
<a href="#" stop-click ng-click="addCipher(folder)">
<i class="fa fa-fw fa-plus-circle"></i> New Item
</a>
</li>
<li ng-show="folder.id">
@@ -175,16 +180,16 @@
</button>
</div>
</div>
<div class="box-body" ng-class="{'no-padding': folderLogins.length}">
<div ng-show="!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 class="box-body" ng-class="{'no-padding': folderCiphers.length}">
<div ng-show="!folderCiphers.length">
<p>No items in this folder.</p>
<button type="button" ng-click="addCipher(folder)" class="btn btn-default btn-flat">Add an Item</button>
</div>
<div class="table-responsive" ng-show="folderLogins.length">
<div class="table-responsive" ng-show="folderCiphers.length">
<table class="table table-striped table-hover table-vmiddle">
<tbody>
<tr ng-repeat="login in folderLogins = (logins | filter: { folderId: folder.id } |
filter: (main.searchVaultText || '')) track by login.id">
<tr ng-repeat="cipher in folderCiphers = (ciphers | filter: { folderId: folder.id } |
filter: cipherFilter | filter: (main.searchVaultText || '')) track by cipher.id">
<td style="width: 70px;">
<div class="btn-group" data-append-to="body">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
@@ -192,33 +197,33 @@
</button>
<ul class="dropdown-menu">
<li>
<a href="#" stop-click ng-click="editLogin(login)">
<a href="#" stop-click ng-click="editCipher(cipher)">
<i class="fa fa-fw fa-pencil"></i> Edit
</a>
</li>
<li>
<a href="#" stop-click ng-click="attachments(login)">
<a href="#" stop-click ng-click="attachments(cipher)">
<i class="fa fa-fw fa-paperclip"></i> Attachments
</a>
</li>
<li ng-show="!login.organizationId">
<a href="#" stop-click ng-click="share(login)">
<li ng-show="!cipher.organizationId">
<a href="#" stop-click ng-click="share(cipher)">
<i class="fa fa-fw fa-share-alt"></i> Share
</a>
</li>
<li ng-show="login.organizationId && login.edit">
<a href="#" stop-click ng-click="collections(login)">
<li ng-show="cipher.organizationId && cipher.edit">
<a href="#" stop-click ng-click="collections(cipher)">
<i class="fa fa-fw fa-cubes"></i> Collections
</a>
</li>
<li ng-show="login.password">
<li ng-show="cipher.meta.password">
<a href="#" stop-click ngclipboard ngclipboard-error="clipboardError(e)"
data-clipboard-text="{{login.password}}">
data-clipboard-text="{{cipher.meta.password}}">
<i class="fa fa-fw fa-clipboard"></i> Copy Password
</a>
</li>
<li ng-show="login.edit">
<a href="#" stop-click ng-click="deleteLogin(login)" class="text-red">
<li ng-show="cipher.edit">
<a href="#" stop-click ng-click="deleteCipher(cipher)" class="text-red">
<i class="fa fa-fw fa-trash"></i> Delete
</a>
</li>
@@ -226,17 +231,22 @@
</div>
</td>
<td class="action-select" ng-click="select($event)">
<input type="checkbox" value="{{::login.id}}" name="loginSelection" stop-prop />
<input type="checkbox" value="{{::cipher.id}}" name="cipherSelection" stop-prop />
</td>
<td class="vault-icon" ng-click="select($event)">
<i class="fa fa-fw fa-lg {{::cipher.icon}}" ng-if="!cipher.meta.image"></i>
<img alt="" ng-if="cipher.meta.image" ng-src="{{cipher.meta.image}}"
fallback-src="images/fa-globe.png" />
</td>
<td ng-click="select($event)">
<a href="#" stop-click ng-click="editLogin(login)" stop-prop>{{login.name}}</a>
<i class="fa fa-star text-muted" title="Favorite" ng-show="login.favorite" stop-prop></i>
<i class="fa fa-share-alt text-muted" title="Shared" ng-show="login.organizationId"
<a href="#" stop-click ng-click="editCipher(cipher)" stop-prop>{{cipher.name}}</a>
<i class="fa fa-star text-muted" title="Favorite" ng-show="cipher.favorite" stop-prop></i>
<i class="fa fa-share-alt text-muted" title="Shared" ng-show="cipher.organizationId"
stop-prop></i>
<i class="fa fa-paperclip text-muted" title="Attachments" ng-if="login.hasAttachments"
<i class="fa fa-paperclip text-muted" title="Attachments" ng-if="cipher.hasAttachments"
stop-prop></i>
<br />
<span class="text-sm text-muted" stop-prop>{{login.username}}</span>
<span class="text-sm text-muted" stop-prop>{{cipher.subTitle}}</span>
</td>
</tr>
</tbody>
@@ -254,19 +264,53 @@
</a>
</li>
</ul>
<h3 class="control-sidebar-heading">
<i class="fa fa-tag fa-fw"></i> Types
</h3>
<div class="control-sidebar-section">
<ul class="control-sidebar-menu">
<li>
<a href="#" stop-click ng-click="filterType(constants.cipherType.login)">
<i class="fa fa-check fa-fw" ng-if="constants.cipherType.login === typeFilter"></i>
<i class="fa fa-globe fa-fw" ng-if="constants.cipherType.login !== typeFilter"></i> Login
</a>
</li>
<li>
<a href="#" stop-click ng-click="filterType(constants.cipherType.card)">
<i class="fa fa-check fa-fw" ng-if="constants.cipherType.card === typeFilter"></i>
<i class="fa fa-credit-card fa-fw" ng-if="constants.cipherType.card !== typeFilter"></i> Card
</a>
</li>
<li>
<a href="#" stop-click ng-click="filterType(constants.cipherType.identity)">
<i class="fa fa-check fa-fw" ng-if="constants.cipherType.identity === typeFilter"></i>
<i class="fa fa-id-card-o fa-fw" ng-if="constants.cipherType.identity !== typeFilter"></i> Identity
</a>
</li>
<li>
<a href="#" stop-click ng-click="filterType(constants.cipherType.secureNote)">
<i class="fa fa-check fa-fw" ng-if="constants.cipherType.secureNote === typeFilter"></i>
<i class="fa fa-sticky-note-o fa-fw" ng-if="constants.cipherType.secureNote !== typeFilter"></i> Secure Note
</a>
</li>
</ul>
</div>
<h3 class="control-sidebar-heading">
<i class="fa fa-folder fa-fw"></i> Folders
</h3>
<div ng-show="loading && !vaultFolders.length">
<p>Loading...</p>
</div>
<ul class="control-sidebar-menu" ng-show="!loading && vaultFolders.length">
<li ng-repeat="folder in vaultFolders track by folder.id">
<a href="#" stop-click ng-click="filterFolder(folder)">
<i class="fa fa-check" ng-if="folder.id === folderIdFilter"></i>
{{folder.name}}
</a>
</li>
</ul>
<div class="control-sidebar-section">
<ul class="control-sidebar-menu" ng-show="!loading && vaultFolders.length">
<li ng-repeat="folder in vaultFolders track by folder.id">
<a href="#" stop-click ng-click="filterFolder(folder)">
<i class="fa fa-check fa-fw" ng-if="folder.id === folderIdFilter"></i>
<i class="fa fa-caret-right fa-fw" ng-if="folder.id !== folderIdFilter"></i>
{{folder.name}}
</a>
</li>
</ul>
</div>
</div>
</aside>

View File

@@ -0,0 +1,583 @@
<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-plus-circle"></i> Add New Item</h4>
</div>
<form name="form" ng-submit="form.$valid && save()" api-form="savePromise" autocomplete="off">
<div class="modal-body">
<div class="callout callout-danger validation-errors" ng-show="form.$errors">
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in form.$errors">{{e}}</li>
</ul>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<label for="type">What type of item is this?</label>
<select id="type" name="Type" ng-model="selectedType" class="form-control" ng-change="typeChanged()">
<option value="{{constants.cipherType.login}}">Login</option>
<option value="{{constants.cipherType.card}}">Card</option>
<option value="{{constants.cipherType.identity}}">Identity</option>
<option value="{{constants.cipherType.secureNote}}">Secure Note</option>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="name">Name</label> <span>*</span>
<input type="text" id="name" name="Name" ng-model="cipher.name" class="form-control" required api-field />
</div>
</div>
<div class="col-sm-6" ng-if="!hideFolders">
<div class="form-group" show-errors>
<label for="folder">Folder</label>
<select id="folder" name="FolderId" ng-model="cipher.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 ng-if="cipher.type === constants.cipherType.login">
<div class="form-group" show-errors>
<label for="uri">URI</label>
<div class="input-group">
<input type="text" id="uri" name="Login.Uri" ng-model="cipher.login.uri" class="form-control"
placeholder="http://..." ng-readonly="readOnly" api-field />
<span class="input-group-btn">
<button 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="{{cipher.login.uri}}" target="_blank" class="btn btn-default btn-flat"
uib-tooltip="Go To Website" tooltip-placement="left">
<i class="fa fa-share"></i>
</a>
</span>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="username">Username</label>
<div class="input-group">
<input type="text" id="username" name="Login.Username" ng-model="cipher.login.username"
class="form-control monospaced" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Username" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)"
data-clipboard-target="#username">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group" show-errors style="margin-bottom: 5px;">
<div class="pull-right password-options">
<i class="fa fa-lg fa-refresh" uib-tooltip="Generate Password" tooltip-placement="left"
ng-click="generatePassword()" ng-show="!readOnly"></i>
<i class="fa fa-lg fa-eye" uib-tooltip="Toggle Password" tooltip-placement="left"
password-viewer="#password"></i>
</div>
<label for="password">Password</label>
<div class="input-group">
<input type="password" id="password" name="Login.Password" ng-model="cipher.login.password"
class="form-control monospaced" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Password" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-success="clipboardSuccess(e)" ngclipboard-error="clipboardError(e, true)"
data-clipboard-text="{{cipher.login.password}}">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
<div password-meter="cipher.login.password" password-meter-username="cipher.login.username"
outer-class="xs" class="password-meter"></div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="totp">Authenticator Key (TOTP)</label>
<input type="text" id="totp" name="Login.Totp" ng-model="cipher.login.totp"
class="form-control monospaced" ng-readonly="readOnly" api-field />
</div>
</div>
<div class="col-sm-6 totp-col">
<div totp="cipher.login.totp" id="verification-code" ng-if="useTotp"></div>
<div ng-if="!useTotp">
<a href="#" stop-click ng-click="showUpgrade()"><img src="images/totp-countdown.png" alt="" /></a>
<span class="label label-info clickable" ng-click="showUpgrade()">
{{fromOrg ? 'UPGRADE' : 'PREMIUM'}}
</span>
</div>
</div>
</div>
</div>
<div ng-if="cipher.type === constants.cipherType.card">
<div class="row">
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="name">Cardholder Name</label>
<input type="text" id="cardholderName" name="Card.CarholderName" ng-readonly="readOnly"
ng-model="cipher.card.cardholderName" class="form-control" api-field />
</div>
</div>
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="cardBrand">Brand</label>
<select id="cardBrand" name="Card.Brand" ng-model="cipher.card.brand" class="form-control"
ng-readonly="readOnly" api-field>
<option value="">-- Select --</option>
<option value="Visa">Visa</option>
<option value="Mastercard">Mastercard</option>
<option value="Amex">American Express</option>
<option value="Discover">Discover</option>
<option value="Diners Club">Diners Club</option>
<option value="JCB">JCB</option>
<option value="Maestro">Maestro</option>
<option value="UnionPay">UnionPay</option>
<option value="Other">Other</option>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="cardNumber">Number</label>
<div class="input-group">
<input type="text" id="cardNumber" name="Card.Number" ng-model="cipher.card.number"
class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Number" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#cardNumber">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
<div class="col-sm-3">
<div class="form-group" show-errors>
<label for="cardExpMonth">Expiration Month</label>
<select id="cardExpMonth" name="Card.ExpMonth" ng-model="cipher.card.expMonth"
ng-readonly="readOnly" class="form-control" api-field>
<option value="">-- Select --</option>
<option value="1">01 - January</option>
<option value="2">02 - February</option>
<option value="3">03 - March</option>
<option value="4">04 - April</option>
<option value="5">05 - May</option>
<option value="6">06 - June</option>
<option value="7">07 - July</option>
<option value="8">08 - August</option>
<option value="9">09 - September</option>
<option value="10">10 - October</option>
<option value="11">11 - November</option>
<option value="12">12 - December</option>
</select>
</div>
</div>
<div class="col-sm-3">
<div class="form-group" show-errors>
<label for="cardExpYear">Expiration Year</label>
<input type="text" id="cardExpYear" name="Card.ExpYear" ng-readonly="readOnly"
ng-model="cipher.card.expYear" class="form-control" api-field placeholder="ex. 2019" />
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="cardCode">Security Code</label>
<div class="input-group">
<input type="text" id="cardCode" name="Card.Code" ng-model="cipher.card.code"
class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Code" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#cardCode">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
</div>
</div>
<div ng-if="cipher.type === constants.cipherType.identity">
<div class="row">
<div class="col-sm-4">
<div class="form-group" show-errors>
<label for="identityTitle">Title</label>
<select id="identityTitle" name="Identity.Title" ng-model="cipher.identity.title" class="form-control"
ng-readonly="readOnly" api-field>
<option value="">-- Select --</option>
<option value="Mr">Mr</option>
<option value="Mrs">Mrs</option>
<option value="Ms">Ms</option>
<option value="Dr">Dr</option>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-4">
<div class="form-group" show-errors>
<label for="identityFirstName">First Name</label>
<div class="input-group">
<input type="text" id="identityFirstName" name="Identity.FirstName"
ng-model="cipher.identity.firstName" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy First Name" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityFirstName">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="form-group" show-errors>
<label for="identityMiddleName">Middle Name</label>
<div class="input-group">
<input type="text" id="identityMiddleName" name="Identity.FirstName"
ng-model="cipher.identity.middleName" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Middle Name" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityMiddleName">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="form-group" show-errors>
<label for="identityLastName">Last Name</label>
<div class="input-group">
<input type="text" id="identityLastName" name="Identity.LastName"
ng-model="cipher.identity.lastName" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Last Name" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityLastName">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-4">
<div class="form-group" show-errors>
<label for="identityUsername">Username</label>
<div class="input-group">
<input type="text" id="identityUsername" name="Identity.Username"
ng-model="cipher.identity.username" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Username" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityUsername">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="form-group" show-errors>
<label for="identityCompany">Company</label>
<div class="input-group">
<input type="text" id="identityCompany" name="Identity.Company"
ng-model="cipher.identity.company" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Company" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityCompany">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-4">
<div class="form-group" show-errors>
<label for="identitySSN">Social Security Number</label>
<div class="input-group">
<input type="text" id="identitySSN" name="Identity.SSN"
ng-model="cipher.identity.ssn" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy SSN" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identitySSN">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="form-group" show-errors>
<label for="identityPassportNumber">Passport Number</label>
<div class="input-group">
<input type="text" id="identityPassportNumber" name="Identity.PassportNumber"
ng-model="cipher.identity.passportNumber" class="form-control" ng-readonly="readOnly"
api-field />
<span class="input-group-btn" uib-tooltip="Copy Passport Number" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityPassportNumber">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="form-group" show-errors>
<label for="identityLicenseNumber">License Number</label>
<div class="input-group">
<input type="text" id="identityLicenseNumber" name="Identity.LicenseNumber"
ng-model="cipher.identity.licenseNumber" class="form-control" ng-readonly="readOnly"
api-field />
<span class="input-group-btn" uib-tooltip="Copy License Number" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityLicenseNumber">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="identityEmail">Email</label>
<div class="input-group">
<input type="text" id="identityEmail" name="Identity.Email"
ng-model="cipher.identity.email" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Email" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityEmail">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="identityPhone">Phone</label>
<div class="input-group">
<input type="text" id="identityPhone" name="Identity.Phone"
ng-model="cipher.identity.phone" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Phone" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityPhone">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="identityAddress1">Address 1</label>
<div class="input-group">
<input type="text" id="identityAddress1" name="Identity.Address1"
ng-model="cipher.identity.address1" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Address 1" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityAddress1">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="identityAddress2">Address 2</label>
<div class="input-group">
<input type="text" id="identityAddress2" name="Identity.Address2"
ng-model="cipher.identity.address2" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Address 2" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityAddress2">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="identityAddress3">Address 3</label>
<div class="input-group">
<input type="text" id="identityAddress3" name="Identity.Address3"
ng-model="cipher.identity.address3" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Address 3" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityAddress3">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="identityCity">City / Town</label>
<div class="input-group">
<input type="text" id="identityCity" name="Identity.City"
ng-model="cipher.identity.city" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy City/Town" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityCity">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="identityState">State / Province</label>
<div class="input-group">
<input type="text" id="identityState" name="Identity.State"
ng-model="cipher.identity.state" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy State/Province" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityState">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="identityPostalCode">Zip / Postal Code</label>
<div class="input-group">
<input type="text" id="identityPostalCode" name="Identity.PostalCode"
ng-model="cipher.identity.postalCode" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Zip/Postal Code" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityPostalCode">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="identityCountry">Country</label>
<div class="input-group">
<input type="text" id="identityCountry" name="Identity.Country"
ng-model="cipher.identity.country" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Country" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityCountry">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
</div>
</div>
<div ng-if="cipher.type === constants.cipherType.secureNote">
<!-- Nothing for now -->
</div>
<div class="form-group" show-errors>
<label for="notes">Notes</label>
<textarea id="notes" name="Notes" class="form-control" ng-model="cipher.notes" api-field
ng-class="{'big-textarea': cipher.type === constants.cipherType.secureNote}"></textarea>
</div>
<hr />
<h4><i class="fa fa-list-ul"></i> Custom Fields</h4>
<div ng-repeat="field in cipher.fields">
<div class="row">
<div class="col-sm-3">
<div class="form-group">
<label for="field_name{{$index}}">Name</label>
<input type="text" id="field_name{{$index}}" name="Field.Name{{$index}}" class="form-control" ng-model="field.name" />
</div>
</div>
<div class="col-sm-3">
<div class="form-group">
<label for="field_type{{$index}}">Type</label>
<select id="field_type{{$index}}" name="Field.Type{{$index}}" class="form-control" ng-model="field.type">
<option value="0">Text</option>
<option value="1">Hidden</option>
<option value="2">Boolean</option>
</select>
</div>
</div>
<div class="col-sm-5">
<div class="form-group">
<div class="pull-right password-options" ng-if="field.type === '1'">
<i class="fa fa-lg fa-eye" uib-tooltip="Toggle Visibility" tooltip-placement="left"
password-viewer="#field_value{{$index}}"></i>
</div>
<label for="field_value{{$index}}">Value</label>
<div class="input-group" ng-if="field.type !== '2'">
<input ng-attr-type="{{field.type === '0' ? 'text' : 'password'}}" id="field_value{{$index}}"
name="Field.Value{{$index}}" class="form-control"
ng-class="{'monospaced': field.type !== '0'}" ng-model="field.value" />
<span class="input-group-btn" uib-tooltip="Copy Value" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-success="clipboardSuccess(e)" ngclipboard-error="clipboardError(e, true)"
data-clipboard-text="{{field.value}}">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
<div ng-if="field.type === '2'">
<input type="checkbox" id="field_value{{$index}}" name="Field.Value{{$index}}" ng-model="field.value"
data-ng-true-value="'true'" />
</div>
</div>
</div>
<div class="col-sm-1">
<br class="hidden-xs" />
<a href="#" ng-click="removeField(field)" stop-click>
<i class="fa fa-window-close-o fa-lg"></i>
<span class="visible-xs-inline">Remove Custom Field</span>
</a>
</div>
</div>
<hr class="visible-xs-block" />
</div>
<a href="#" ng-click="addField()" stop-click>
<i class="fa fa-plus-circle"></i> New Custom Field
</a>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="form.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="form.$loading"></i>Submit
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
<button type="button" ng-if="!hideFavorite" class="btn btn-link pull-right" ng-click="toggleFavorite()"
uib-tooltip="Toggle Favorite" tooltip-placement="left">
<i class="fa fa-lg" ng-class="cipher.favorite ? 'fa-star' : 'fa-star-o'"></i>
<span class="sr-only">Toggle Favorite</span>
</button>
</div>
</form>

View File

@@ -1,171 +0,0 @@
<div class="modal-header">
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="addLoginModelLabel"><i class="fa fa-globe"></i> Add New Login</h4>
</div>
<form name="addLoginForm" ng-submit="addLoginForm.$valid && save(login)" api-form="savePromise" autocomplete="off">
<div class="modal-body">
<div class="callout callout-danger validation-errors" ng-show="addLoginForm.$errors">
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in addLoginForm.$errors">{{e}}</li>
</ul>
</div>
<div class="row">
<div class="col-sm-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-sm-6" ng-if="!hideFolders">
<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="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)"
data-clipboard-target="#uri">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
<div class="row">
<div class="col-sm-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="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)"
data-clipboard-target="#username">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group" show-errors style="margin-bottom: 5px;">
<div class="pull-right password-options">
<i class="fa fa-lg fa-refresh" uib-tooltip="Generate Password" tooltip-placement="left" ng-click="generatePassword()"></i>
<i class="fa fa-lg fa-eye" uib-tooltip="Toggle Password" tooltip-placement="left" password-viewer="#password"></i>
</div>
<label for="password">Password</label>
<div class="input-group">
<input tabindex="-1" type="text" id="password-text" value="{{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)"
ngclipboard-error="clipboardError(e, true)"
data-clipboard-target="#password-text">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
<div password-meter="login.password" password-meter-username="login.username"
outer-class="xs" class="password-meter"></div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="totp">Authenticator Key (TOTP)</label>
<input type="text" id="totp" name="Totp" ng-model="login.totp" class="form-control"
ng-readonly="readOnly" api-field />
</div>
</div>
<div class="col-sm-6 totp-col">
<div totp="login.totp" id="verification-code" ng-if="useTotp"></div>
<div ng-if="!useTotp">
<a href="#" stop-click ng-click="showUpgrade()"><img src="images/totp-countdown.png" alt="" /></a>
<span class="label label-info clickable" ng-click="showUpgrade()">{{fromOrg ? 'UPGRADE' : 'PREMIUM'}}</span>
</div>
</div>
</div>
<div class="form-group" show-errors>
<label for="notes">Notes</label>
<textarea id="notes" name="Notes" class="form-control" ng-model="login.notes" api-field></textarea>
</div>
<hr />
<h4><i class="fa fa-list-ul"></i> Custom Fields</h4>
<div ng-repeat="field in login.fields">
<div class="row">
<div class="col-sm-3">
<div class="form-group">
<label for="field_name{{$index}}">Name</label>
<input type="text" id="field_name{{$index}}" name="Field.Name{{$index}}" class="form-control" ng-model="field.name" />
</div>
</div>
<div class="col-sm-3">
<div class="form-group">
<label for="field_type{{$index}}">Type</label>
<select id="field_type{{$index}}" name="Field.Type{{$index}}" class="form-control" ng-model="field.type">
<option value="0">Text</option>
<option value="1">Hidden</option>
<option value="2">Boolean</option>
</select>
</div>
</div>
<div class="col-sm-5">
<div class="form-group">
<div class="pull-right password-options" ng-if="field.type === '1'">
<i class="fa fa-lg fa-eye" uib-tooltip="Toggle Visibility" tooltip-placement="left"
password-viewer="#field_value{{$index}}"></i>
</div>
<label for="field_value{{$index}}">Value</label>
<div class="input-group" ng-if="field.type !== '2'">
<input ng-attr-type="{{field.type === '0' ? 'text' : 'password'}}" id="field_value{{$index}}"
name="Field.Value{{$index}}" class="form-control" ng-model="field.value" />
<span class="input-group-btn" uib-tooltip="Copy Value" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-success="clipboardSuccess(e)" ngclipboard-error="clipboardError(e, true)"
data-clipboard-text="{{field.value}}">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
<div ng-if="field.type === '2'">
<input type="checkbox" id="field_value{{$index}}" name="Field.Value{{$index}}" ng-model="field.value"
data-ng-true-value="'true'" />
</div>
</div>
</div>
<div class="col-sm-1">
<br class="hidden-xs" />
<a href="#" ng-click="removeField(field)" stop-click>
<i class="fa fa-window-close-o fa-lg"></i>
<span class="visible-xs-inline">Remove Custom Field</span>
</a>
</div>
</div>
<hr class="visible-xs-block" />
</div>
<a href="#" ng-click="addField()" stop-click>
<i class="fa fa-plus-circle"></i> New Custom Field
</a>
</div>
<div class="modal-footer">
<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>
<button type="button" ng-if="!hideFavorite" class="btn btn-link pull-right" ng-click="toggleFavorite()"
uib-tooltip="Toggle Favorite" tooltip-placement="left">
<i class="fa fa-lg" ng-class="login.favorite ? 'fa-star' : 'fa-star-o'"></i>
<span class="sr-only">Toggle Favorite</span>
</button>
</div>
</form>

View File

@@ -1,7 +1,7 @@
<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-paperclip"></i> Secure Attachments <small>{{login.name}}</small>
<i class="fa fa-paperclip"></i> Secure Attachments <small>{{cipher.name}}</small>
</h4>
</div>
<form name="form" ng-submit="form.$valid && save(form)" api-form="savePromise" autocomplete="off">
@@ -9,13 +9,13 @@
<div ng-if="loading">
Loading...
</div>
<div ng-if="!loading && !login.attachments.length">
There are no attachments for this login.
<div ng-if="!loading && !cipher.attachments.length">
There are no attachments for this item.
</div>
<div class="table-responsive" ng-if="login.attachments.length" style="margin: 0;">
<div class="table-responsive" ng-if="cipher.attachments.length" style="margin: 0;">
<table class="table table-striped table-hover table-vmiddle" style="margin: 0;">
<tbody>
<tr ng-repeat="attachment in login.attachments | orderBy: ['fileName']">
<tr ng-repeat="attachment in cipher.attachments | orderBy: ['fileName']">
<td style="width: 70px;">
<div class="btn-group" data-append-to=".modal">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">

View File

@@ -1,10 +1,10 @@
<div class="modal-header">
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><i class="fa fa-cubes"></i> Collections <small>{{login.name}}</small></h4>
<h4 class="modal-title"><i class="fa fa-cubes"></i> Collections <small>{{cipher.name}}</small></h4>
</div>
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise" autocomplete="off">
<div class="modal-body">
<p>Edit the collections that this login is being shared with.</p>
<p>Edit the collections that this item is being shared with.</p>
<div class="callout callout-danger validation-errors" ng-show="form.$errors">
<h4>Errors have occurred</h4>
<ul>

View File

@@ -0,0 +1,584 @@
<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 {{cipher.icon}}"></i> Item Information <small>{{cipher.name}}</small>
</h4>
</div>
<form name="form" ng-submit="form.$valid && save(cipher)" api-form="savePromise" autocomplete="off">
<div class="modal-body">
<div class="callout callout-danger validation-errors" ng-show="form.$errors">
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in form.$errors">{{e}}</li>
</ul>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="name">Name</label> <span>*</span>
<input type="text" id="name" name="Name" ng-model="cipher.name" class="form-control"
ng-readonly="readOnly" required api-field />
</div>
</div>
<div class="col-sm-6" ng-if="!hideFolders">
<div class="form-group" show-errors>
<label for="folder">Folder</label>
<select id="folder" name="FolderId" ng-model="cipher.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 ng-if="cipher.type === constants.cipherType.login">
<div class="form-group" show-errors>
<label for="uri">URI</label>
<div class="input-group">
<input type="text" id="uri" name="Login.Uri" ng-model="cipher.login.uri" class="form-control"
placeholder="http://..." ng-readonly="readOnly" api-field />
<span class="input-group-btn">
<button 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="{{cipher.login.uri}}" target="_blank" class="btn btn-default btn-flat"
uib-tooltip="Go To Website" tooltip-placement="left">
<i class="fa fa-share"></i>
</a>
</span>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="username">Username</label>
<div class="input-group">
<input type="text" id="username" name="Login.Username" ng-model="cipher.login.username"
class="form-control monospaced" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Username" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)"
data-clipboard-target="#username">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group" show-errors style="margin-bottom: 5px;">
<div class="pull-right password-options">
<i class="fa fa-lg fa-refresh" uib-tooltip="Generate Password" tooltip-placement="left"
ng-click="generatePassword()" ng-show="!readOnly"></i>
<i class="fa fa-lg fa-eye" uib-tooltip="Toggle Password" tooltip-placement="left"
password-viewer="#password"></i>
</div>
<label for="password">Password</label>
<div class="input-group">
<input type="password" id="password" name="Login.Password" ng-model="cipher.login.password"
class="form-control monospaced" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Password" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-success="clipboardSuccess(e)" ngclipboard-error="clipboardError(e, true)"
data-clipboard-text="{{cipher.login.password}}">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
<div password-meter="cipher.login.password" password-meter-username="cipher.login.username"
outer-class="xs" class="password-meter"></div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="totp">Authenticator Key (TOTP)</label>
<input type="text" id="totp" name="Login.Totp" ng-model="cipher.login.totp"
class="form-control monospaced" ng-readonly="readOnly" api-field />
</div>
</div>
<div class="col-sm-6 totp-col">
<div totp="cipher.login.totp" id="verification-code" ng-if="useTotp"></div>
<div ng-if="!useTotp">
<a href="#" stop-click ng-click="showUpgrade()"><img src="images/totp-countdown.png" alt="" /></a>
<span class="label label-info clickable" ng-click="showUpgrade()">
{{fromOrg ? 'UPGRADE' : 'PREMIUM'}}
</span>
</div>
</div>
</div>
</div>
<div ng-if="cipher.type === constants.cipherType.card">
<div class="row">
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="name">Cardholder Name</label>
<input type="text" id="cardholderName" name="Card.CarholderName" ng-readonly="readOnly"
ng-model="cipher.card.cardholderName" class="form-control" api-field />
</div>
</div>
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="cardBrand">Brand</label>
<select id="cardBrand" name="Card.Brand" ng-model="cipher.card.brand" class="form-control"
ng-readonly="readOnly" api-field>
<option value="">-- Select --</option>
<option value="Visa">Visa</option>
<option value="Mastercard">Mastercard</option>
<option value="Amex">American Express</option>
<option value="Discover">Discover</option>
<option value="Diners Club">Diners Club</option>
<option value="JCB">JCB</option>
<option value="Maestro">Maestro</option>
<option value="UnionPay">UnionPay</option>
<option value="Other">Other</option>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="cardNumber">Number</label>
<div class="input-group">
<input type="text" id="cardNumber" name="Card.Number" ng-model="cipher.card.number"
class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Number" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#cardNumber">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
<div class="col-sm-3">
<div class="form-group" show-errors>
<label for="cardExpMonth">Expiration Month</label>
<select id="cardExpMonth" name="Card.ExpMonth" ng-model="cipher.card.expMonth"
ng-readonly="readOnly" class="form-control" api-field>
<option value="">-- Select --</option>
<option value="1">01 - January</option>
<option value="2">02 - February</option>
<option value="3">03 - March</option>
<option value="4">04 - April</option>
<option value="5">05 - May</option>
<option value="6">06 - June</option>
<option value="7">07 - July</option>
<option value="8">08 - August</option>
<option value="9">09 - September</option>
<option value="10">10 - October</option>
<option value="11">11 - November</option>
<option value="12">12 - December</option>
</select>
</div>
</div>
<div class="col-sm-3">
<div class="form-group" show-errors>
<label for="cardExpYear">Expiration Year</label>
<input type="text" id="cardExpYear" name="Card.ExpYear" ng-readonly="readOnly"
ng-model="cipher.card.expYear" class="form-control" api-field placeholder="ex. 2019" />
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="cardCode">Security Code</label>
<div class="input-group">
<input type="text" id="cardCode" name="Card.Code" ng-model="cipher.card.code"
class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Code" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#cardCode">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
</div>
</div>
<div ng-if="cipher.type === constants.cipherType.identity">
<div class="row">
<div class="col-sm-4">
<div class="form-group" show-errors>
<label for="identityTitle">Title</label>
<select id="identityTitle" name="Identity.Title" ng-model="cipher.identity.title" class="form-control"
ng-readonly="readOnly" api-field>
<option value="">-- Select --</option>
<option value="Mr">Mr</option>
<option value="Mrs">Mrs</option>
<option value="Ms">Ms</option>
<option value="Dr">Dr</option>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-4">
<div class="form-group" show-errors>
<label for="identityFirstName">First Name</label>
<div class="input-group">
<input type="text" id="identityFirstName" name="Identity.FirstName"
ng-model="cipher.identity.firstName" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy First Name" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityFirstName">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="form-group" show-errors>
<label for="identityMiddleName">Middle Name</label>
<div class="input-group">
<input type="text" id="identityMiddleName" name="Identity.FirstName"
ng-model="cipher.identity.middleName" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Middle Name" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityMiddleName">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="form-group" show-errors>
<label for="identityLastName">Last Name</label>
<div class="input-group">
<input type="text" id="identityLastName" name="Identity.LastName"
ng-model="cipher.identity.lastName" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Last Name" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityLastName">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-4">
<div class="form-group" show-errors>
<label for="identityUsername">Username</label>
<div class="input-group">
<input type="text" id="identityUsername" name="Identity.Username"
ng-model="cipher.identity.username" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Username" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityUsername">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="form-group" show-errors>
<label for="identityCompany">Company</label>
<div class="input-group">
<input type="text" id="identityCompany" name="Identity.Company"
ng-model="cipher.identity.company" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Company" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityCompany">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-4">
<div class="form-group" show-errors>
<label for="identitySSN">Social Security Number</label>
<div class="input-group">
<input type="text" id="identitySSN" name="Identity.SSN"
ng-model="cipher.identity.ssn" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy SSN" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identitySSN">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="form-group" show-errors>
<label for="identityPassportNumber">Passport Number</label>
<div class="input-group">
<input type="text" id="identityPassportNumber" name="Identity.PassportNumber"
ng-model="cipher.identity.passportNumber" class="form-control" ng-readonly="readOnly"
api-field />
<span class="input-group-btn" uib-tooltip="Copy Passport Number" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityPassportNumber">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="form-group" show-errors>
<label for="identityLicenseNumber">License Number</label>
<div class="input-group">
<input type="text" id="identityLicenseNumber" name="Identity.LicenseNumber"
ng-model="cipher.identity.licenseNumber" class="form-control" ng-readonly="readOnly"
api-field />
<span class="input-group-btn" uib-tooltip="Copy License Number" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityLicenseNumber">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="identityEmail">Email</label>
<div class="input-group">
<input type="text" id="identityEmail" name="Identity.Email"
ng-model="cipher.identity.email" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Email" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityEmail">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="identityPhone">Phone</label>
<div class="input-group">
<input type="text" id="identityPhone" name="Identity.Phone"
ng-model="cipher.identity.phone" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Phone" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityPhone">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="identityAddress1">Address 1</label>
<div class="input-group">
<input type="text" id="identityAddress1" name="Identity.Address1"
ng-model="cipher.identity.address1" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Address 1" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityAddress1">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="identityAddress2">Address 2</label>
<div class="input-group">
<input type="text" id="identityAddress2" name="Identity.Address2"
ng-model="cipher.identity.address2" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Address 2" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityAddress2">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="identityAddress3">Address 3</label>
<div class="input-group">
<input type="text" id="identityAddress3" name="Identity.Address3"
ng-model="cipher.identity.address3" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Address 3" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityAddress3">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="identityCity">City / Town</label>
<div class="input-group">
<input type="text" id="identityCity" name="Identity.City"
ng-model="cipher.identity.city" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy City/Town" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityCity">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="identityState">State / Province</label>
<div class="input-group">
<input type="text" id="identityState" name="Identity.State"
ng-model="cipher.identity.state" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy State/Province" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityState">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="identityPostalCode">Zip / Postal Code</label>
<div class="input-group">
<input type="text" id="identityPostalCode" name="Identity.PostalCode"
ng-model="cipher.identity.postalCode" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Zip/Postal Code" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityPostalCode">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="identityCountry">Country</label>
<div class="input-group">
<input type="text" id="identityCountry" name="Identity.Country"
ng-model="cipher.identity.country" class="form-control" ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Country" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityCountry">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
</div>
</div>
<div ng-if="cipher.type === constants.cipherType.secureNote">
<!-- Nothing for now -->
</div>
<div class="form-group" show-errors>
<label for="notes">Notes</label>
<textarea id="notes" name="Notes" class="form-control" ng-model="cipher.notes"
ng-class="{'big-textarea': cipher.type === constants.cipherType.secureNote}"
ng-readonly="readOnly" api-field></textarea>
</div>
<div ng-if="!readOnly || (cipher.fields && cipher.fields.length)">
<hr />
<h4><i class="fa fa-list-ul"></i> Custom Fields</h4>
</div>
<div ng-repeat="field in cipher.fields">
<div class="row">
<div class="col-sm-3">
<div class="form-group">
<label for="field_name{{$index}}">Name</label>
<input type="text" id="field_name{{$index}}" class="form-control" ng-model="field.name"
ng-readonly="readOnly" />
</div>
</div>
<div class="col-sm-3">
<div class="form-group">
<label for="field_type{{$index}}">Type</label>
<select id="field_type{{$index}}" class="form-control" ng-model="field.type" ng-disabled="readOnly">
<option value="0">Text</option>
<option value="1">Hidden</option>
<option value="2">Boolean</option>
</select>
</div>
</div>
<div class="col-sm-5">
<div class="form-group">
<div class="pull-right password-options" ng-if="field.type === '1'">
<i class="fa fa-lg fa-eye" uib-tooltip="Toggle Visibility" tooltip-placement="left"
password-viewer="#field_value{{$index}}"></i>
</div>
<label for="field_value{{$index}}">Value</label>
<div class="input-group" ng-if="field.type !== '2'">
<input ng-attr-type="{{field.type === '0' ? 'text' : 'password'}}" id="field_value{{$index}}"
class="form-control" ng-class="{'monospaced': field.type !== '0'}" ng-model="field.value"
ng-readonly="readOnly" />
<span class="input-group-btn" uib-tooltip="Copy Value" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-success="clipboardSuccess(e)" ngclipboard-error="clipboardError(e, true)"
data-clipboard-text="{{field.value}}">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
<div ng-if="field.type === '2'">
<input type="checkbox" id="field_value{{$index}}" ng-model="field.value"
data-ng-true-value="'true'" ng-disabled="readOnly" />
</div>
</div>
</div>
<div class="col-sm-1">
<br class="hidden-xs" />
<a href="#" ng-click="removeField(field)" stop-click ng-if="!readOnly">
<i class="fa fa-window-close-o fa-lg"></i>
<span class="visible-xs-inline">Remove Custom Field</span>
</a>
</div>
</div>
<hr class="visible-xs-block" />
</div>
<a href="#" ng-click="addField()" stop-click ng-if="!readOnly">
<i class="fa fa-plus-circle"></i> New Custom Field
</a>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="form.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="form.$loading"></i>Save
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
<button type="button" class="btn btn-link pull-right" ng-click="delete()" uib-tooltip="Delete"
tooltip-placement="left" ng-disabled="readOnly">
<i class="fa fa-trash fa-lg"></i>
<span class="sr-only">Delete</span>
</button>
<button type="button" ng-if="!hideFavorite" class="btn btn-link pull-right" ng-click="toggleFavorite()"
uib-tooltip="Toggle Favorite" tooltip-placement="left">
<i class="fa fa-lg" ng-class="cipher.favorite ? 'fa-star' : 'fa-star-o'"></i>
<span class="sr-only">Toggle Favorite</span>
</button>
</div>
</form>

View File

@@ -1,192 +0,0 @@
<div class="modal-header">
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="editLoginModelLabel">
<i class="fa fa-globe"></i> Login Information <small>{{login.name}}</small>
</h4>
</div>
<form name="editLoginForm" ng-submit="editLoginForm.$valid && save(login)" api-form="savePromise" autocomplete="off">
<div class="modal-body">
<div class="callout callout-danger validation-errors" ng-show="editLoginForm.$errors">
<h4>Errors have occurred</h4>
<ul>
<li ng-repeat="e in editLoginForm.$errors">{{e}}</li>
</ul>
</div>
<div class="row">
<div class="col-sm-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"
ng-readonly="readOnly" required api-field />
</div>
</div>
<div class="col-sm-6" ng-if="!hideFolders">
<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="login.uri" class="form-control" placeholder="http://..."
ng-readonly="readOnly" api-field />
<span class="input-group-btn">
<button 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="{{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-sm-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="login.username" class="form-control"
ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Username" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-error="clipboardError(e)"
data-clipboard-target="#username">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group" show-errors style="margin-bottom: 5px;">
<div class="pull-right password-options">
<i class="fa fa-lg fa-refresh" uib-tooltip="Generate Password" tooltip-placement="left"
ng-click="generatePassword()" ng-show="!readOnly"></i>
<i class="fa fa-lg fa-eye" uib-tooltip="Toggle Password" tooltip-placement="left"
password-viewer="#password"></i>
</div>
<label for="password">Password</label>
<div class="input-group">
<input type="password" id="password" name="Password" ng-model="login.password" class="form-control"
ng-readonly="readOnly" api-field />
<span class="input-group-btn" uib-tooltip="Copy Password" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-success="clipboardSuccess(e)" ngclipboard-error="clipboardError(e, true)"
data-clipboard-text="{{login.password}}">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
</div>
<div password-meter="login.password" password-meter-username="login.username"
outer-class="xs" class="password-meter"></div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group" show-errors>
<label for="totp">Authenticator Key (TOTP)</label>
<input type="text" id="totp" name="Totp" ng-model="login.totp" class="form-control"
ng-readonly="readOnly" api-field />
</div>
</div>
<div class="col-sm-6 totp-col">
<div totp="login.totp" id="verification-code" ng-if="useTotp"></div>
<div ng-if="!useTotp">
<a href="#" stop-click ng-click="showUpgrade()"><img src="images/totp-countdown.png" alt="" /></a>
<span class="label label-info clickable" ng-click="showUpgrade()">{{fromOrg ? 'UPGRADE' : 'PREMIUM'}}</span>
</div>
</div>
</div>
<div class="form-group" show-errors>
<label for="notes">Notes</label>
<textarea id="notes" name="Notes" class="form-control" ng-model="login.notes"
ng-readonly="readOnly" api-field></textarea>
</div>
<div ng-if="!readOnly || (login.fields && login.fields.length)">
<hr />
<h4><i class="fa fa-list-ul"></i> Custom Fields</h4>
</div>
<div ng-repeat="field in login.fields">
<div class="row">
<div class="col-sm-3">
<div class="form-group">
<label for="field_name{{$index}}">Name</label>
<input type="text" id="field_name{{$index}}" class="form-control" ng-model="field.name"
ng-readonly="readOnly" />
</div>
</div>
<div class="col-sm-3">
<div class="form-group">
<label for="field_type{{$index}}">Type</label>
<select id="field_type{{$index}}" class="form-control" ng-model="field.type" ng-disabled="readOnly">
<option value="0">Text</option>
<option value="1">Hidden</option>
<option value="2">Boolean</option>
</select>
</div>
</div>
<div class="col-sm-5">
<div class="form-group">
<div class="pull-right password-options" ng-if="field.type === '1'">
<i class="fa fa-lg fa-eye" uib-tooltip="Toggle Visibility" tooltip-placement="left"
password-viewer="#field_value{{$index}}"></i>
</div>
<label for="field_value{{$index}}">Value</label>
<div class="input-group" ng-if="field.type !== '2'">
<input ng-attr-type="{{field.type === '0' ? 'text' : 'password'}}" id="field_value{{$index}}"
class="form-control" ng-model="field.value" ng-readonly="readOnly" />
<span class="input-group-btn" uib-tooltip="Copy Value" tooltip-placement="left">
<button class="btn btn-default btn-flat" type="button" ngclipboard
ngclipboard-success="clipboardSuccess(e)" ngclipboard-error="clipboardError(e, true)"
data-clipboard-text="{{field.value}}">
<i class="fa fa-clipboard"></i>
</button>
</span>
</div>
<div ng-if="field.type === '2'">
<input type="checkbox" id="field_value{{$index}}" ng-model="field.value"
data-ng-true-value="'true'" ng-disabled="readOnly" />
</div>
</div>
</div>
<div class="col-sm-1">
<br class="hidden-xs" />
<a href="#" ng-click="removeField(field)" stop-click ng-if="!readOnly">
<i class="fa fa-window-close-o fa-lg"></i>
<span class="visible-xs-inline">Remove Custom Field</span>
</a>
</div>
</div>
<hr class="visible-xs-block" />
</div>
<a href="#" ng-click="addField()" stop-click ng-if="!readOnly">
<i class="fa fa-plus-circle"></i> New Custom Field
</a>
</div>
<div class="modal-footer">
<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"
tooltip-placement="left" ng-disabled="readOnly">
<i class="fa fa-trash fa-lg"></i>
<span class="sr-only">Delete</span>
</button>
<button type="button" ng-if="!hideFavorite" class="btn btn-link pull-right" ng-click="toggleFavorite()"
uib-tooltip="Toggle Favorite" tooltip-placement="left">
<i class="fa fa-lg" ng-class="login.favorite ? 'fa-star' : 'fa-star-o'"></i>
<span class="sr-only">Toggle Favorite</span>
</button>
</div>
</form>

View File

@@ -1,7 +1,7 @@
<div class="modal-header">
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">
<i class="fa fa-share"></i> Move Logins
<i class="fa fa-share"></i> Move Items
</h4>
</div>
<form name="form" ng-submit="form.$valid && save()" api-form="savePromise" autocomplete="off">
@@ -14,7 +14,7 @@
</div>
<p>
Select a folder that you would like to move the <b>{{count}}</b> selected
<span ng-pluralize count="count" when="{'1': 'login', 'other': 'logins'}"></span> to.
<span ng-pluralize count="count" when="{'1': 'item', 'other': 'items'}"></span> to.
</p>
<div class="form-group" show-errors>
<label for="folder">Folder</label>

View File

@@ -1,10 +1,10 @@
<div class="modal-header">
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><i class="fa fa-share-alt"></i> Share Login <small>{{login.name}}</small></h4>
<h4 class="modal-title"><i class="fa fa-share-alt"></i> Share Item <small>{{cipher.name}}</small></h4>
</div>
<form name="form" ng-submit="form.$valid && submit(model)" api-form="submitPromise" autocomplete="off">
<div class="modal-body">
<p>Choose an organization that you wish to share this login with.</p>
<p>Choose an organization that you wish to share this item with.</p>
<div class="callout callout-danger validation-errors" ng-show="form.$errors">
<h4>Errors have occurred</h4>
<ul>
@@ -14,7 +14,7 @@
<p ng-show="loading">Loading...</p>
<div ng-show="!loading && !organizations.length" class="callout callout-default">
<h4><i class="fa fa-info-circle"></i> No Organizations</h4>
<p>You do not belong to any organizations. Organizations allow you to share logins with other bitwarden users.</p>
<p>You do not belong to any organizations. Organizations allow you to share items with other bitwarden users.</p>
<a ng-click="createOrg()" class="btn btn-default btn-flat">
Create an Organization
</a>
@@ -72,4 +72,4 @@
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
</div>
</form>
</form>

View File

@@ -3,18 +3,18 @@
Shared
<small>
<span ng-pluralize
count="collections.length > 0 && logins.length ? collections.length - 1 : collections.length"
count="collections.length > 0 && ciphers.length ? collections.length - 1 : collections.length"
when="{'1': '{} collection', 'other': '{} collections'}"></span>,
<span ng-pluralize count="logins.length" when="{'1': '{} login', 'other': '{} logins'}"></span>
<span ng-pluralize count="ciphers.length" when="{'1': '{} item', 'other': '{} items'}"></span>
</small>
</h1>
</section>
<section class="content">
<p ng-show="loading && !collections.length">Loading...</p>
<div class="callout callout-default" style="background: #fff;" ng-show="!loading && !collections.length && !logins.length">
<div class="callout callout-default" style="background: #fff;" ng-show="!loading && !collections.length && !ciphers.length">
<h4>Nothing shared <i class="fa fa-frown-o"></i></h4>
<p>
You do not have any logins or collections being shared with you.
You do not have any items or collections being shared with you.
To start sharing, create an organization or ask an existing organization to invite you.
</p>
<a ui-sref="backend.user.settingsCreateOrg" class="btn btn-default btn-flat">
@@ -28,7 +28,7 @@
<h3 class="box-title">
<i class="fa" ng-class="{'fa-cubes': collection.id, 'fa-sitemap': !collection.id}"></i>
{{collection.name}}
<small ng-pluralize count="collectionLogins.length" when="{'1': '{} login', 'other': '{} logins'}"></small>
<small ng-pluralize count="collectionCiphers.length" when="{'1': '{} item', 'other': '{} items'}"></small>
</h3>
<div class="box-tools">
<button type="button" class="btn btn-box-tool" data-widget="collapse" title="Collapse/Expand"
@@ -37,20 +37,20 @@
</button>
</div>
</div>
<div class="box-body" ng-class="{'no-padding': collectionLogins.length}">
<div ng-show="!collectionLogins.length && collection.id">
<p>No logins in this collection.</p>
<div class="box-body" ng-class="{'no-padding': collectionCiphers.length}">
<div ng-show="!collectionCiphers.length && collection.id">
<p>No items in this collection.</p>
<p>
Share a login to this collection by selecting <i class="fa fa-share-alt"></i> <b>Share</b> or
<i class="fa fa-cubes"></i> <b>Collections</b> from the login's options (<i class="fa fa-cog"></i>) menu.
Share an item to this collection by selecting <i class="fa fa-share-alt"></i> <b>Share</b> or
<i class="fa fa-cubes"></i> <b>Collections</b> from the item's options (<i class="fa fa-cog"></i>) menu.
</p>
</div>
<div ng-show="!collectionLogins.length && !collection.id">No unassigned logins.</div>
<div class="table-responsive" ng-show="collectionLogins.length">
<div ng-show="!collectionCiphers.length && !collection.id">No unassigned items.</div>
<div class="table-responsive" ng-show="collectionCiphers.length">
<table class="table table-striped table-hover table-vmiddle">
<tbody>
<tr ng-repeat="login in collectionLogins = (logins | filter: filterByCollection(collection) |
orderBy: ['name', 'username']) track by login.id">
<tr ng-repeat="cipher in collectionCiphers = (ciphers | filter: filterByCollection(collection) |
orderBy: ['name', 'subTitle']) track by cipher.id">
<td style="width: 70px;">
<div class="btn-group" data-append-to="body">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
@@ -58,28 +58,28 @@
</button>
<ul class="dropdown-menu">
<li>
<a href="#" stop-click ng-click="editLogin(login)">
<a href="#" stop-click ng-click="editCipher(cipher)">
<i class="fa fa-fw fa-pencil"></i> Edit
</a>
</li>
<li>
<a href="#" stop-click ng-click="attachments(login)">
<a href="#" stop-click ng-click="attachments(cipher)">
<i class="fa fa-fw fa-paperclip"></i> Attachments
</a>
</li>
<li ng-show="login.edit">
<a href="#" stop-click ng-click="editCollections(login)">
<li ng-show="cipher.edit">
<a href="#" stop-click ng-click="editCollections(cipher)">
<i class="fa fa-fw fa-cubes"></i> Collections
</a>
</li>
<li ng-show="login.password">
<li ng-show="cipher.meta.password">
<a href="#" stop-click ngclipboard ngclipboard-error="clipboardError(e)"
data-clipboard-text="{{login.password}}">
data-clipboard-text="{{cipher.meta.password}}">
<i class="fa fa-fw fa-clipboard"></i> Copy Password
</a>
</li>
<li ng-show="login.edit">
<a href="#" stop-click ng-click="removeLogin(login, collection)"
<li ng-show="cipher.edit">
<a href="#" stop-click ng-click="removeCipher(cipher, collection)"
ng-if="collection.id" class="text-red">
<i class="fa fa-fw fa-remove"></i> Remove
</a>
@@ -87,12 +87,17 @@
</ul>
</div>
</td>
<td class="vault-icon">
<i class="fa fa-fw fa-lg {{::cipher.icon}}" ng-if="!cipher.meta.image"></i>
<img alt="" ng-if="cipher.meta.image" ng-src="{{cipher.meta.image}}"
fallback-src="images/fa-globe.png" />
</td>
<td>
<a href="#" stop-click ng-click="editLogin(login)">{{login.name}}</a>
<i class="fa fa-star text-muted" title="Favorite" ng-show="login.favorite"></i>
<i class="fa fa-paperclip text-muted" title="Attachments" ng-if="login.hasAttachments"
<a href="#" stop-click ng-click="editCipher(cipher)">{{cipher.name}}</a>
<i class="fa fa-star text-muted" title="Favorite" ng-show="cipher.favorite"></i>
<i class="fa fa-paperclip text-muted" title="Attachments" ng-if="cipher.hasAttachments"
stop-prop></i><br />
<div class="text-sm text-muted">{{login.username}}</div>
<div class="text-sm text-muted">{{cipher.subTitle}}</div>
</td>
</tr>
</tbody>

View File

@@ -53,8 +53,8 @@
</a>
<ul class="treeview-menu" ng-class="{'menu-open': $state.includes('backend.org.vault')}">
<li>
<a href="#" stop-click ng-click="addOrganizationLogin()">
<i class="fa fa-plus-circle fa-fw"></i> New Login
<a href="#" stop-click ng-click="addOrganizationCipher()">
<i class="fa fa-plus-circle fa-fw"></i> New Item
</a>
</li>
</ul>

View File

@@ -46,8 +46,8 @@
<a ui-sref="backend.user.vault"><i class="fa fa-lock fa-fw"></i> <span>My Vault</span></a>
<ul class="treeview-menu" ng-class="{'menu-open': $state.includes('backend.user.vault')}">
<li>
<a href="#" stop-click ng-click="addLogin()">
<i class="fa fa-plus-circle fa-fw"></i> New Login
<a href="#" stop-click ng-click="addCipher()">
<i class="fa fa-plus-circle fa-fw"></i> New Item
</a>
</li>
<li>

9
src/browserconfig.xml Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="images/icons/mstile-150x150.png"/>
<TileColor>#3c8dbc</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 18 KiB

BIN
src/images/fa-globe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M824 4945 c-92 -16 -133 -30 -218 -71 -91 -45 -150 -90 -229 -173
-112 -119 -182 -263 -207 -427 -7 -47 -10 -598 -8 -1759 l3 -1690 22 -70 c32
-103 58 -160 108 -234 88 -132 222 -243 367 -300 157 -63 87 -61 1892 -61
1797 0 1750 -1 1900 59 239 96 408 289 483 551 17 62 18 145 18 1790 0 1645
-1 1728 -18 1790 -44 155 -111 272 -214 372 -106 105 -218 170 -362 211 l-76
22 -1695 2 c-1357 1 -1709 -1 -1766 -12z m3196 -617 c18 -13 41 -39 51 -58 18
-33 19 -79 19 -1000 -1 -866 -4 -1026 -25 -1131 -26 -136 -150 -397 -251 -534
-81 -109 -302 -330 -439 -441 -105 -85 -296 -216 -390 -270 -11 -6 -60 -34
-108 -62 -146 -86 -272 -142 -316 -142 -102 0 -530 247 -811 469 -143 112
-385 359 -462 471 -102 148 -175 300 -226 475 l-26 90 -4 1010 c-2 719 0 1021
8 1048 7 25 24 47 52 67 l41 30 1427 0 1427 0 33 -22z"/>
<path d="M2560 2540 l0 -1350 38 19 c231 118 489 307 683 500 181 180 265 309
321 488 21 66 21 87 25 881 l4 812 -535 0 -536 0 0 -1350z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -25,6 +25,7 @@
img-src
'self'
data:
https://icons.bitwarden.com
https://*.paypal.com
https://www.paypalobjects.com
https://q.stripe.com
@@ -76,6 +77,7 @@
*;">
<!-- @endif -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#3c8dbc">
<base href="/" />
<title page-title>bitwarden Web Vault</title>
@@ -102,6 +104,11 @@
<link rel="stylesheet" href="css/vault.css" />
<!-- @endexclude -->
<link rel="apple-touch-icon" sizes="180x180" href="images/icons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="images/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="images/icons/favicon-16x16.png">
<link rel="mask-icon" href="images/icons/safari-pinned-tab.svg" color="#3c8dbc">
<link rel="manifest" href="manifest.json">
</head>
<body ng-controller="mainController as main" class="layout-boxed {{main.skinClass}} sidebar-mini {{main.bodyClass}}"
ng-class="{'using-control-sidebar': main.usingControlSidebar,
@@ -173,6 +180,7 @@
<script src="app/directives/stopClickDirective.js"></script>
<script src="app/directives/stopPropDirective.js"></script>
<script src="app/directives/totpDirective.js"></script>
<script src="app/directives/fallbackSrcDirective.js"></script>
<script src="app/filters/filtersModule.js"></script>
<script src="app/filters/enumNameFilter.js"></script>
@@ -210,14 +218,14 @@
<script src="app/vault/vaultModule.js"></script>
<script src="app/vault/vaultController.js"></script>
<script src="app/vault/vaultEditLoginController.js"></script>
<script src="app/vault/vaultAddLoginController.js"></script>
<script src="app/vault/vaultEditCipherController.js"></script>
<script src="app/vault/vaultAddCipherController.js"></script>
<script src="app/vault/vaultEditFolderController.js"></script>
<script src="app/vault/vaultAddFolderController.js"></script>
<script src="app/vault/vaultShareLoginController.js"></script>
<script src="app/vault/vaultShareCipherController.js"></script>
<script src="app/vault/vaultSharedController.js"></script>
<script src="app/vault/vaultLoginCollectionsController.js"></script>
<script src="app/vault/vaultMoveLoginsController.js"></script>
<script src="app/vault/vaultCipherCollectionsController.js"></script>
<script src="app/vault/vaultMoveCiphersController.js"></script>
<script src="app/vault/vaultAttachmentsController.js"></script>
<script src="app/organization/organizationModule.js"></script>
@@ -242,9 +250,9 @@
<script src="app/organization/organizationBillingChangePlanController.js"></script>
<script src="app/organization/organizationBillingVerifyBankController.js"></script>
<script src="app/organization/organizationVaultController.js"></script>
<script src="app/organization/organizationVaultAddLoginController.js"></script>
<script src="app/organization/organizationVaultEditLoginController.js"></script>
<script src="app/organization/organizationVaultLoginCollectionsController.js"></script>
<script src="app/organization/organizationVaultAddCipherController.js"></script>
<script src="app/organization/organizationVaultEditCipherController.js"></script>
<script src="app/organization/organizationVaultCipherCollectionsController.js"></script>
<script src="app/organization/organizationVaultAttachmentsController.js"></script>
<script src="app/organization/organizationGroupsController.js"></script>
<script src="app/organization/organizationGroupsAddController.js"></script>
@@ -266,6 +274,7 @@
<script src="app/settings/settingsTwoStepController.js"></script>
<script src="app/settings/settingsAddEditEquivalentDomainController.js"></script>
<script src="app/settings/settingsDeleteController.js"></script>
<script src="app/settings/settingsPurgeController.js"></script>
<script src="app/settings/settingsCreateOrganizationController.js"></script>
<script src="app/settings/settingsBillingController.js"></script>
<script src="app/settings/settingsBillingAdjustStorageController.js"></script>

View File

@@ -53,7 +53,7 @@ body.skin-blue {
}
.content-wrapper, .main-footer {
border-left: 1px solid transparent;
border-left: 1px solid @sidebar-dark-bg;
}
.sidebar-menu > li {
@@ -159,6 +159,7 @@ body.skin-blue {
li a {
padding-top: 5px;
padding-bottom: 5px;
white-space: nowrap;
}
li.active a {
@@ -166,6 +167,13 @@ body.skin-blue {
}
}
.control-sidebar-section {
max-height: 350px;
overflow-y: auto;
padding-right: 15px;
margin-right: -15px;
}
.control-sidebar-open {
@media (min-width: @screen-sm) {
.main-footer {
@@ -193,10 +201,6 @@ body.skin-blue {
position: absolute;
}
.layout-boxed .wrapper {
overflow: hidden;
}
.content-wrapper, .control-sidebar, .main-sidebar, .main-header .logo, .main-header .navbar, .main-footer {
.transition(initial);
}
@@ -432,6 +436,24 @@ form .btn .loading-icon {
.table td.action-select {
text-align: center;
width: 30px;
max-width: 30px;
@media (max-width: @screen-sm-max) {
display: none;
}
}
.table td.vault-icon {
text-align: center;
width: 41px;
max-width: 41px;
color: @text-muted;
img {
max-width: 24px;
max-height: 24px;
border-radius: 3px;
}
@media (max-width: @screen-sm-max) {
display: none;
@@ -785,3 +807,13 @@ h1, h2, h3, h4, h5, h6 {
}
}
}
textarea {
&#notes {
height: 100px;
}
&.big-textarea {
height: 200px !important;
}
}

17
src/manifest.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "bitwarden vault",
"icons": [
{
"src": "images/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "images/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#3c8dbc",
"background_color": "#3c8dbc"
}