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

Compare commits

..

42 Commits

Author SHA1 Message Date
Kyle Spearrin
c9b5426f6f version bump 2017-12-28 12:56:00 -05:00
Kyle Spearrin
bf885c184f lint fixes 2017-12-19 12:15:24 -05:00
Kyle Spearrin
0d2bf4f7a1 update libs 2017-12-19 12:13:33 -05:00
Kyle Spearrin
01ffc68fc2 focus vault search on $viewContentLoaded 2017-12-19 11:30:52 -05:00
Kyle Spearrin
16892239fb cross navigation for event subject ids 2017-12-19 11:14:15 -05:00
Kyle Spearrin
d5765d8814 display app/device info on events 2017-12-18 13:56:38 -05:00
Kyle Spearrin
8d6a96074d send device type header 2017-12-18 13:37:06 -05:00
Kyle Spearrin
f54884eb79 event logs for users. ip address. useEvents checks 2017-12-18 13:17:49 -05:00
Kyle Spearrin
828149b2d6 eventService and cipher event logs page 2017-12-18 11:52:42 -05:00
Kyle Spearrin
501c4fc263 serve CSP from proxy 2017-12-16 23:44:35 -05:00
Kyle Spearrin
1d0b45e17d whiteListedDomains only on dev builds 2017-12-16 23:23:17 -05:00
Kyle Spearrin
a0f7ed68fb content-type doesn't need to be text anymore 2017-12-16 23:14:43 -05:00
Kyle Spearrin
7bd0c17188 switch to fork for gh-pages fix 2017-12-16 23:10:40 -05:00
Kyle Spearrin
1ea9d28523 local api/identity uri paths 2017-12-16 22:08:23 -05:00
Kyle Spearrin
8a3fb92bbe paging 2017-12-15 15:02:27 -05:00
Kyle Spearrin
de3a9b9903 date range filtering 2017-12-15 12:42:21 -05:00
Kyle Spearrin
9834f3d2aa use events check 2017-12-14 18:04:18 -05:00
Kyle Spearrin
ac079b9d88 audit logs icon 2017-12-14 15:24:18 -05:00
Kyle Spearrin
9e96906f32 compute counts on every load scenario 2017-12-14 15:20:18 -05:00
Kyle Spearrin
90c079e743 org events page setup 2017-12-14 15:03:46 -05:00
Kyle Spearrin
4ecf307285 properly flag new folder as type folder
resolves #149
2017-12-09 08:28:52 -05:00
Kyle Spearrin
6cf4c453d9 Update README.md 2017-12-05 11:12:34 -05:00
Philipp Hug
d2899d14c7 vaultAddCipherController.js: secureNote Type is int not string (#144) 2017-12-04 07:59:28 -05:00
Kyle Spearrin
f3b438d514 null ref on keeper import 2017-12-03 21:27:49 -05:00
Kyle Spearrin
2997f694f8 import notes for form fills 2017-11-30 23:45:06 -05:00
Kyle Spearrin
b78ab4db27 import form fill csv for lastpass 2017-11-30 23:40:05 -05:00
Kyle Spearrin
37dddea515 simplify collapse/expand logic 2017-11-30 22:47:16 -05:00
Kyle Spearrin
e307d1e87d init storage 2017-11-29 22:47:21 -05:00
Kyle Spearrin
62e1dbb642 expand/collapse all boxes 2017-11-29 22:43:58 -05:00
Kyle Spearrin
b8a425f530 version bump 2017-11-29 22:12:46 -05:00
Kyle Spearrin
cafb6fa694 not always CSV data 2017-11-28 10:07:21 -05:00
Kyle Spearrin
0482ddea2c store large items in notes for import 2017-11-28 10:02:41 -05:00
Kyle Spearrin
b411176c8d better error message handling 2017-11-28 09:27:44 -05:00
Kyle Spearrin
2f13449cb6 fix null ref 2017-11-22 12:29:30 -05:00
Kyle Spearrin
b0c1b7b683 default password generated is 14 length 2017-11-22 12:28:06 -05:00
Kyle Spearrin
7e8978c7fc single collection icon is a cube 2017-11-22 12:24:21 -05:00
Kyle Spearrin
d58b422bd0 no items in folder/collection 2017-11-22 12:21:55 -05:00
Kyle Spearrin
3563601382 no collections message 2017-11-22 12:17:40 -05:00
Kyle Spearrin
d42e6ca3fd show collection and folder groupings together 2017-11-22 12:08:31 -05:00
Kyle Spearrin
7f0d8c99e3 version bump 2017-11-13 12:31:23 -05:00
Kyle Spearrin
48a67dc2b3 remove amazon app 2017-11-13 12:28:11 -05:00
Kyle Spearrin
8d0b42492d families plan desc 2017-11-08 22:05:53 -05:00
52 changed files with 1530 additions and 647 deletions

1
CNAME
View File

@@ -1 +0,0 @@
vault.bitwarden.com

View File

@@ -1,4 +1,4 @@
[![appveyor build](https://ci.appveyor.com/api/projects/status/github/bitwarden/web?branch=master&svg=true)](https://ci.appveyor.com/project/bitwarden/web) [![Join the chat at https://gitter.im/bitwarden/Lobby](https://badges.gitter.im/bitwarden/Lobby.svg)](https://gitter.im/bitwarden/Lobby)
[![appveyor build](https://ci.appveyor.com/api/projects/status/github/bitwarden/web?branch=master&svg=true)](https://ci.appveyor.com/project/bitwarden/web) [![DockerHub](https://img.shields.io/docker/pulls/bitwarden/web.svg)](https://hub.docker.com/u/bitwarden/) [![Join the chat at https://gitter.im/bitwarden/Lobby](https://badges.gitter.im/bitwarden/Lobby.svg)](https://gitter.im/bitwarden/Lobby)
# bitwarden Web

2
dist/.publish vendored

Submodule dist/.publish updated: 9d2a53eca0...1990d717ea

View File

@@ -465,7 +465,7 @@ gulp.task('deploy-preview', ['dist'], function () {
return gulp.src(paths.dist + '**/*')
.pipe(ghPages({
cacheDir: paths.dist + '.publish',
remoteUrl: 'git@github.com:kspearrin/bitwarden-web-preview.git'
remoteUrl: 'git@github.com:bitwarden/web-preview.git'
}));
});

View File

@@ -1,9 +1,9 @@
{
"name": "bitwarden",
"version": "1.20.0",
"version": "1.22.0",
"env": "Production",
"devDependencies": {
"connect": "3.6.3",
"connect": "3.6.5",
"lodash": "4.17.4",
"gulp": "3.9.1",
"gulp-concat": "2.6.1",
@@ -11,41 +11,41 @@
"gulp-less": "3.3.2",
"gulp-rename": "1.2.2",
"gulp-uglify": "3.0.0",
"gulp-gh-pages": "0.5.4",
"gulp-gh-pages": "git@github.com:tekd/gulp-gh-pages.git#update-dependency",
"gulp-preprocess": "2.0.0",
"gulp-ng-annotate": "2.0.0",
"gulp-ng-config": "1.4.0",
"gulp-ng-config": "1.5.0",
"gulp-connect": "5.0.0",
"jshint": "2.9.5",
"gulp-jshint": "2.0.4",
"rimraf": "2.6.1",
"run-sequence": "2.1.0",
"rimraf": "2.6.2",
"run-sequence": "2.2.0",
"merge-stream": "1.0.1",
"jquery": "2.2.4",
"jquery": "3.2.1",
"font-awesome": "4.7.0",
"bootstrap": "3.3.7",
"angular": "1.6.6",
"angular-resource": "1.6.6",
"angular-sanitize": "1.6.6",
"angular-ui-bootstrap": "2.5.0",
"angular": "1.6.7",
"angular-resource": "1.6.7",
"angular-sanitize": "1.6.7",
"angular-ui-bootstrap": "2.5.6",
"angular-ui-router": "0.4.2",
"angular-jwt": "0.1.9",
"angular-cookies": "1.6.6",
"angular-cookies": "1.6.7",
"admin-lte": "2.3.11",
"angular-toastr": "2.1.1",
"angular-bootstrap-show-errors": "2.3.0",
"angular-messages": "1.6.6",
"angular-messages": "1.6.7",
"ngstorage": "0.3.11",
"papaparse": "4.3.5",
"papaparse": "4.3.6",
"clipboard": "1.7.1",
"ngclipboard": "1.1.1",
"angulartics": "1.4.0",
"ngclipboard": "1.1.2",
"angulartics": "1.5.0",
"angulartics-google-analytics": "0.4.0",
"node-forge": "0.7.1",
"webpack-stream": "4.0.0",
"angular-stripe": "5.0.0",
"angular-credit-cards": "3.1.6",
"browserify": "14.4.0",
"browserify": "14.5.0",
"vinyl-source-stream": "1.1.0",
"gulp-derequire": "2.1.0",
"exposify": "0.5.0",

View File

@@ -1,12 +1,9 @@
{
"appSettings": {
"apiUri": "https://preview-api.bitwarden.com",
"identityUri": "https://preview-identity.bitwarden.com",
"apiUri": "/api",
"identityUri": "/identity",
"iconsUri": "https://icons.bitwarden.com",
"stripeKey": "pk_test_KPoCfZXu7mznb9uSCPZ2JpTD",
"braintreeKey": "sandbox_r72q8jq6_9pnxkwm75f87sdc2",
"whitelistDomains": [
"preview-api.bitwarden.com"
]
"braintreeKey": "sandbox_r72q8jq6_9pnxkwm75f87sdc2"
}
}

View File

@@ -1,12 +1,9 @@
{
"appSettings": {
"apiUri": "https://api.bitwarden.com",
"identityUri": "https://identity.bitwarden.com",
"apiUri": "/api",
"identityUri": "/identity",
"iconsUri": "https://icons.bitwarden.com",
"stripeKey": "pk_live_bpN0P37nMxrMQkcaHXtAybJk",
"braintreeKey": "production_qfbsv8kc_njj2zjtyngtjmbjd",
"whitelistDomains": [
"api.bitwarden.com"
]
"braintreeKey": "production_qfbsv8kc_njj2zjtyngtjmbjd"
}
}

View File

@@ -4,9 +4,6 @@
"identityUri": "http://localhost:33656",
"iconsUri": "https://icons.bitwarden.com",
"stripeKey": "pk_test_KPoCfZXu7mznb9uSCPZ2JpTD",
"braintreeKey": "sandbox_r72q8jq6_9pnxkwm75f87sdc2",
"whitelistDomains": [
"localhost"
]
"braintreeKey": "sandbox_r72q8jq6_9pnxkwm75f87sdc2"
}
}

View File

@@ -1,9 +1,13 @@
angular
.module('bit')
.factory('apiInterceptor', function ($injector, $q, toastr) {
.factory('apiInterceptor', function ($injector, $q, toastr, appSettings, utilsService) {
return {
request: function (config) {
if (config.url.indexOf(appSettings.apiUri + '/') > -1) {
config.headers['Device-Type'] = utilsService.getDeviceType();
}
return config;
},
response: function (response) {

View File

@@ -14,27 +14,15 @@ angular
$qProvider.errorOnUnhandledRejections(false);
$locationProvider.hashPrefix('');
var jwtConfig = {
whiteListedDomains: appSettings.whitelistDomains
};
// @if false
jwtOptionsProvider.config({
whiteListedDomains: ['localhost', 'api.bitwarden.com', 'vault.bitwarden.com']
});
// @endif
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);
var refreshPromise;
jwtInterceptorProvider.tokenGetter = /*@ngInject*/ function (options, tokenService, authService) {
if (options.url.indexOf(appSettings.apiUri) !== 0) {
if (options.url.indexOf(appSettings.apiUri + '/') === -1) {
return;
}
@@ -79,12 +67,6 @@ angular
appendToBody: true
});
if ($httpProvider.defaults.headers.post) {
$httpProvider.defaults.headers.post = {};
}
$httpProvider.defaults.headers.post['Content-Type'] = 'text/plain; charset=utf-8';
// stop IE from caching get requests
if (navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > 0) {
if (!$httpProvider.defaults.headers.get) {
@@ -124,12 +106,6 @@ angular
refreshFromServer: false
}
})
.state('backend.user.shared', {
url: '^/shared',
templateUrl: 'app/vault/views/vaultShared.html',
controller: 'vaultSharedController',
data: { pageTitle: 'Shared' }
})
.state('backend.user.settings', {
url: '^/settings',
templateUrl: 'app/settings/views/settings.html',
@@ -195,13 +171,13 @@ angular
data: { pageTitle: 'Organization Dashboard' }
})
.state('backend.org.people', {
url: '/organization/:orgId/people',
url: '/organization/:orgId/people?viewEvents&search',
templateUrl: 'app/organization/views/organizationPeople.html',
controller: 'organizationPeopleController',
data: { pageTitle: 'Organization People' }
})
.state('backend.org.collections', {
url: '/organization/:orgId/collections',
url: '/organization/:orgId/collections?search',
templateUrl: 'app/organization/views/organizationCollections.html',
controller: 'organizationCollectionsController',
data: { pageTitle: 'Organization Collections' }
@@ -219,17 +195,23 @@ angular
data: { pageTitle: 'Organization Billing' }
})
.state('backend.org.vault', {
url: '/organization/:orgId/vault',
url: '/organization/:orgId/vault?viewEvents&search',
templateUrl: 'app/organization/views/organizationVault.html',
controller: 'organizationVaultController',
data: { pageTitle: 'Organization Vault' }
})
.state('backend.org.groups', {
url: '/organization/:orgId/groups',
url: '/organization/:orgId/groups?search',
templateUrl: 'app/organization/views/organizationGroups.html',
controller: 'organizationGroupsController',
data: { pageTitle: 'Organization Groups' }
})
.state('backend.org.events', {
url: '/organization/:orgId/events',
templateUrl: 'app/organization/views/organizationEvents.html',
controller: 'organizationEventsController',
data: { pageTitle: 'Organization Events' }
})
// Frontend
.state('frontend', {
@@ -375,7 +357,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.vaultCiphers = $rootScope.vaultFolders = null;
$rootScope.vaultCiphers = $rootScope.vaultGroupings = null;
authService.getUserProfile().then(function (profile) {
var orgs = profile.organizations;

View File

@@ -39,6 +39,60 @@ angular.module('bit')
hidden: 1,
boolean: 2
},
deviceType: {
android: 0,
ios: 1,
chromeExt: 2,
firefoxExt: 3,
operaExt: 4,
edgeExt: 5,
windowsDesktop: 6,
macOsDesktop: 7,
linuxDesktop: 8,
chrome: 9,
firefox: 10,
opera: 11,
edge: 12,
ie: 13,
unknown: 14,
uwp: 16,
safari: 17,
vivaldi: 18,
vivaldiExt: 19
},
eventType: {
User_LoggedIn: 1000,
User_ChangedPassword: 1001,
User_Enabled2fa: 1002,
User_Disabled2fa: 1003,
User_Recovered2fa: 1004,
User_FailedLogIn: 1005,
User_FailedLogIn2fa: 1006,
Cipher_Created: 1100,
Cipher_Updated: 1101,
Cipher_Deleted: 1102,
Cipher_AttachmentCreated: 1103,
Cipher_AttachmentDeleted: 1104,
Cipher_Shared: 1105,
Cipher_UpdatedCollections: 1106,
Collection_Created: 1300,
Collection_Updated: 1301,
Collection_Deleted: 1302,
Group_Created: 1400,
Group_Updated: 1401,
Group_Deleted: 1402,
OrganizationUser_Invited: 1500,
OrganizationUser_Confirmed: 1501,
OrganizationUser_Updated: 1502,
OrganizationUser_Removed: 1503,
OrganizationUser_UpdatedGroups: 1504,
Organization_Updated: 1600
},
twoFactorProviderInfo: [
{
type: 0,

View File

@@ -49,6 +49,10 @@ angular
vm.openControlSidebar = vm.usingControlSidebar && $document.width() > 768;
});
$scope.$on('setSearchVaultText', function (event, val) {
vm.searchVaultText = val;
});
$scope.addCipher = function () {
$scope.$broadcast('vaultAddCipher');
};

View File

@@ -2,7 +2,7 @@
.module('bit.organization')
.controller('organizationCollectionsController', function ($scope, $state, apiService, $uibModal, cipherService, $filter,
toastr, $analytics) {
toastr, $analytics, $uibModalStack) {
$scope.collections = [];
$scope.loading = true;
$scope.$on('$viewContentLoaded', function () {
@@ -96,6 +96,12 @@
apiService.collections.listOrganization({ orgId: $state.params.orgId }, function (list) {
$scope.collections = cipherService.decryptCollections(list.Data, $state.params.orgId, true);
$scope.loading = false;
if ($state.params.search) {
$uibModalStack.dismissAll();
$scope.filterSearch = $state.params.search;
$('#filterSearch').focus();
}
});
}
});

View File

@@ -0,0 +1,100 @@
angular
.module('bit.organization')
.controller('organizationEventsController', function ($scope, $state, apiService, $uibModal, $filter,
toastr, $analytics, constants, eventService, $compile, $sce) {
$scope.events = [];
$scope.orgUsers = [];
$scope.loading = true;
$scope.continuationToken = null;
var defaultFilters = eventService.getDefaultDateFilters();
$scope.filterStart = defaultFilters.start;
$scope.filterEnd = defaultFilters.end;
$scope.$on('$viewContentLoaded', function () {
load();
});
$scope.refresh = function () {
loadEvents(true);
};
$scope.next = function () {
loadEvents(false);
};
var i = 0,
orgUsersUserIdDict = {},
orgUsersIdDict = {};
function load() {
apiService.organizationUsers.list({ orgId: $state.params.orgId }).$promise.then(function (list) {
var users = [];
for (i = 0; i < list.Data.length; i++) {
var user = {
id: list.Data[i].Id,
userId: list.Data[i].UserId,
name: list.Data[i].Name,
email: list.Data[i].Email
};
users.push(user);
var displayName = user.name || user.email;
orgUsersUserIdDict[user.userId] = displayName;
orgUsersIdDict[user.id] = displayName;
}
$scope.orgUsers = users;
return loadEvents(true);
});
}
function loadEvents(clearExisting) {
var filterResult = eventService.formatDateFilters($scope.filterStart, $scope.filterEnd);
if (filterResult.error) {
alert(filterResult.error);
return;
}
if (clearExisting) {
$scope.continuationToken = null;
$scope.events = [];
}
$scope.loading = true;
return apiService.events.listOrganization({
orgId: $state.params.orgId,
start: filterResult.start,
end: filterResult.end,
continuationToken: $scope.continuationToken
}).$promise.then(function (list) {
$scope.continuationToken = list.ContinuationToken;
var events = [];
for (i = 0; i < list.Data.length; i++) {
var userId = list.Data[i].ActingUserId || list.Data[i].UserId;
var eventInfo = eventService.getEventInfo(list.Data[i]);
var htmlMessage = $compile('<span>' + eventInfo.message + '</span>')($scope);
events.push({
message: $sce.trustAsHtml(htmlMessage[0].outerHTML),
appIcon: eventInfo.appIcon,
appName: eventInfo.appName,
userId: userId,
userName: userId ? (orgUsersUserIdDict[userId] || '-') : '-',
date: list.Data[i].Date,
ip: list.Data[i].IpAddress
});
}
if ($scope.events && $scope.events.length > 0) {
$scope.events = $scope.events.concat(events);
}
else {
$scope.events = events;
}
$scope.loading = false;
});
}
});

View File

@@ -2,7 +2,7 @@
.module('bit.organization')
.controller('organizationGroupsController', function ($scope, $state, apiService, $uibModal, $filter,
toastr, $analytics) {
toastr, $analytics, $uibModalStack) {
$scope.groups = [];
$scope.loading = true;
$scope.$on('$viewContentLoaded', function () {
@@ -88,6 +88,12 @@
}
$scope.groups = groups;
$scope.loading = false;
if ($state.params.search) {
$uibModalStack.dismissAll();
$scope.filterSearch = $state.params.search;
$('#filterSearch').focus();
}
});
}
});

View File

@@ -2,9 +2,10 @@
.module('bit.organization')
.controller('organizationPeopleController', function ($scope, $state, $uibModal, cryptoService, apiService, authService,
toastr, $analytics) {
toastr, $analytics, $filter, $uibModalStack) {
$scope.users = [];
$scope.useGroups = false;
$scope.useEvents = false;
$scope.$on('$viewContentLoaded', function () {
loadList();
@@ -13,6 +14,7 @@
if (profile.organizations) {
var org = profile.organizations[$state.params.orgId];
$scope.useGroups = !!org.useGroups;
$scope.useEvents = !!org.useEvents;
}
});
});
@@ -110,6 +112,18 @@
});
};
$scope.events = function (user) {
$uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationPeopleEvents.html',
controller: 'organizationPeopleEventsController',
resolve: {
orgUser: function () { return user; },
orgId: function () { return $state.params.orgId; }
}
});
};
function loadList() {
apiService.organizationUsers.list({ orgId: $state.params.orgId }, function (list) {
var users = [];
@@ -129,6 +143,20 @@
}
$scope.users = users;
if ($state.params.search) {
$uibModalStack.dismissAll();
$scope.filterSearch = $state.params.search;
$('#filterSearch').focus();
}
if ($state.params.viewEvents) {
$uibModalStack.dismissAll();
var eventUser = $filter('filter')($scope.users, { id: $state.params.viewEvents });
if (eventUser && eventUser.length) {
$scope.events(eventUser[0]);
}
}
});
}
});

View File

@@ -0,0 +1,75 @@
angular
.module('bit.organization')
.controller('organizationPeopleEventsController', function ($scope, apiService, $uibModalInstance,
orgUser, $analytics, eventService, orgId, $compile, $sce) {
$analytics.eventTrack('organizationPeopleEventsController', { category: 'Modal' });
$scope.email = orgUser.email;
$scope.events = [];
$scope.loading = true;
$scope.continuationToken = null;
var defaultFilters = eventService.getDefaultDateFilters();
$scope.filterStart = defaultFilters.start;
$scope.filterEnd = defaultFilters.end;
$uibModalInstance.opened.then(function () {
loadEvents(true);
});
$scope.refresh = function () {
loadEvents(true);
};
$scope.next = function () {
loadEvents(false);
};
function loadEvents(clearExisting) {
var filterResult = eventService.formatDateFilters($scope.filterStart, $scope.filterEnd);
if (filterResult.error) {
alert(filterResult.error);
return;
}
if (clearExisting) {
$scope.continuationToken = null;
$scope.events = [];
}
$scope.loading = true;
return apiService.events.listOrganizationUser({
orgId: orgId,
id: orgUser.id,
start: filterResult.start,
end: filterResult.end,
continuationToken: $scope.continuationToken
}).$promise.then(function (list) {
$scope.continuationToken = list.ContinuationToken;
var events = [];
for (var i = 0; i < list.Data.length; i++) {
var eventInfo = eventService.getEventInfo(list.Data[i]);
var htmlMessage = $compile('<span>' + eventInfo.message + '</span>')($scope);
events.push({
message: $sce.trustAsHtml(htmlMessage[0].outerHTML),
appIcon: eventInfo.appIcon,
appName: eventInfo.appName,
date: list.Data[i].Date,
ip: list.Data[i].IpAddress
});
}
if ($scope.events && $scope.events.length > 0) {
$scope.events = $scope.events.concat(events);
}
else {
$scope.events = events;
}
$scope.loading = false;
});
}
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@@ -64,7 +64,7 @@
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.');
importError('Data is not formatted correctly. Please check your import file and try again.');
return;
}
}

View File

@@ -40,7 +40,7 @@
$scope.generatePassword = function () {
if (!$scope.cipher.login.password || confirm('Are you sure you want to overwrite the current password?')) {
$analytics.eventTrack('Generated Password From Add');
$scope.cipher.login.password = passwordService.generatePassword({ length: 12, special: true });
$scope.cipher.login.password = passwordService.generatePassword({ length: 14, special: true });
}
};

View File

@@ -35,13 +35,9 @@
toastr.success('The attachment has been added.');
closing = true;
$uibModalInstance.close(true);
}, function (err) {
if (err) {
validationService.addError(form, 'file', err, true);
}
else {
validationService.addError(form, 'file', 'Something went wrong.', true);
}
}, function (e) {
var errors = validationService.parseErrors(e);
toastr.error(errors.length ? errors[0] : 'An error occurred.');
});
};

View File

@@ -0,0 +1,104 @@
angular
.module('bit.organization')
.controller('organizationVaultCipherEventsController', function ($scope, apiService, $uibModalInstance,
cipher, $analytics, eventService) {
$analytics.eventTrack('organizationVaultCipherEventsController', { category: 'Modal' });
$scope.cipher = cipher;
$scope.events = [];
$scope.loading = true;
$scope.continuationToken = null;
var defaultFilters = eventService.getDefaultDateFilters();
$scope.filterStart = defaultFilters.start;
$scope.filterEnd = defaultFilters.end;
$uibModalInstance.opened.then(function () {
load();
});
$scope.refresh = function () {
loadEvents(true);
};
$scope.next = function () {
loadEvents(false);
};
var i = 0,
orgUsersUserIdDict = {},
orgUsersIdDict = {};
function load() {
apiService.organizationUsers.list({ orgId: cipher.organizationId }).$promise.then(function (list) {
var users = [];
for (i = 0; i < list.Data.length; i++) {
var user = {
id: list.Data[i].Id,
userId: list.Data[i].UserId,
name: list.Data[i].Name,
email: list.Data[i].Email
};
users.push(user);
var displayName = user.name || user.email;
orgUsersUserIdDict[user.userId] = displayName;
orgUsersIdDict[user.id] = displayName;
}
$scope.orgUsers = users;
return loadEvents(true);
});
}
function loadEvents(clearExisting) {
var filterResult = eventService.formatDateFilters($scope.filterStart, $scope.filterEnd);
if (filterResult.error) {
alert(filterResult.error);
return;
}
if (clearExisting) {
$scope.continuationToken = null;
$scope.events = [];
}
$scope.loading = true;
return apiService.events.listCipher({
id: cipher.id,
start: filterResult.start,
end: filterResult.end,
continuationToken: $scope.continuationToken
}).$promise.then(function (list) {
$scope.continuationToken = list.ContinuationToken;
var events = [];
for (i = 0; i < list.Data.length; i++) {
var userId = list.Data[i].ActingUserId || list.Data[i].UserId;
var eventInfo = eventService.getEventInfo(list.Data[i], { cipherInfo: false });
events.push({
message: eventInfo.message,
appIcon: eventInfo.appIcon,
appName: eventInfo.appName,
userId: userId,
userName: userId ? (orgUsersUserIdDict[userId] || '-') : '-',
date: list.Data[i].Date,
ip: list.Data[i].IpAddress
});
}
if ($scope.events && $scope.events.length > 0) {
$scope.events = $scope.events.concat(events);
}
else {
$scope.events = events;
}
$scope.loading = false;
});
}
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@@ -2,12 +2,20 @@
.module('bit.organization')
.controller('organizationVaultController', function ($scope, apiService, cipherService, $analytics, $q, $state,
$localStorage, $uibModal, $filter, authService) {
$localStorage, $uibModal, $filter, authService, $uibModalStack) {
$scope.ciphers = [];
$scope.collections = [];
$scope.loading = true;
$scope.useEvents = false;
$scope.$on('$viewContentLoaded', function () {
authService.getUserProfile().then(function (profile) {
if (profile.organizations) {
var org = profile.organizations[$state.params.orgId];
$scope.useEvents = !!org.useEvents;
}
});
var collectionPromise = apiService.collections.listOrganization({ orgId: $state.params.orgId }, function (collections) {
var decCollections = [{
id: null,
@@ -39,6 +47,20 @@
$q.all([collectionPromise, cipherPromise]).then(function () {
$scope.loading = false;
$("#search").focus();
if ($state.params.search) {
$uibModalStack.dismissAll();
$scope.$emit('setSearchVaultText', $state.params.search);
}
if ($state.params.viewEvents) {
$uibModalStack.dismissAll();
var cipher = $filter('filter')($scope.ciphers, { id: $state.params.viewEvents });
if (cipher && cipher.length) {
$scope.viewEvents(cipher[0]);
}
}
});
});
@@ -141,6 +163,17 @@
});
};
$scope.viewEvents = function (cipher) {
$uibModal.open({
animation: true,
templateUrl: 'app/organization/views/organizationVaultCipherEvents.html',
controller: 'organizationVaultCipherEventsController',
resolve: {
cipher: function () { return cipher; }
}
});
};
$scope.attachments = function (cipher) {
authService.getUserProfile().then(function (profile) {
return !!profile.organizations[cipher.organizationId].maxStorageGb;

View File

@@ -28,7 +28,7 @@
$scope.generatePassword = function () {
if (!$scope.cipher.login.password || confirm('Are you sure you want to overwrite the current password?')) {
$analytics.eventTrack('Generated Password From Edit');
$scope.cipher.login.password = passwordService.generatePassword({ length: 12, special: true });
$scope.cipher.login.password = passwordService.generatePassword({ length: 14, special: true });
}
};

View File

@@ -10,7 +10,7 @@
&nbsp;
<div class="box-filters hidden-xs">
<div class="form-group form-group-sm has-feedback has-feedback-left">
<input type="text" id="search" class="form-control" placeholder="Search collections..."
<input type="text" id="filterSearch" class="form-control" placeholder="Search collections..."
style="width: 200px;" ng-model="filterSearch">
<span class="fa fa-search form-control-feedback text-muted" aria-hidden="true"></span>
</div>

View File

@@ -0,0 +1,67 @@
<section class="content-header">
<h1>
Events
<small>audit your organization</small>
</h1>
</section>
<section class="content">
<div class="box">
<div class="box-header with-border">
&nbsp;
<div class="box-filters hidden-xs hidden-sm">
<input type="datetime-local" ng-model="filterStart" required
class="form-control input-sm" style="width:initial;" />
-
<input type="datetime-local" ng-model="filterEnd" required
class="form-control input-sm" style="width:initial;" />
</div>
<div class="box-tools">
<button type="button" class="btn btn-primary btn-sm btn-flat" ng-click="refresh()">
<i class="fa fa-fw fa-refresh" ng-class="{'fa-spin': loading}"></i> Refresh
</button>
</div>
</div>
<div class="box-body" ng-class="{'no-padding': filteredEvents.length}">
<div ng-show="loading && !events.length">
Loading...
</div>
<div ng-show="!loading && !events.length">
<p>There are no events to list.</p>
</div>
<div class="table-responsive" ng-show="events.length">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Timestamp</th>
<th><span class="sr-only">App</span></th>
<th>User</th>
<th>Event</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="event in filteredEvents = (events)">
<td style="width: 210px; min-width: 100px;">
{{event.date | date:'medium'}}
</td>
<td style="width: 20px;" class="text-center">
<i class="text-muted fa fa-lg {{event.appIcon}}" title="{{event.appName}}, {{event.ip}}"></i>
</td>
<td style="width: 150px; min-width: 100px;">
{{event.userName}}
</td>
<td>
<div ng-bind-html="event.message"></div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="box-footer text-center" ng-show="continuationToken">
<button class="btn btn-link btn-block" ng-click="next()" ng-if="!loading">
Load more...
</button>
<i class="fa fa-fw fa-refresh fa-spin text-muted" ng-if="loading"></i>
</div>
</div>
</section>

View File

@@ -10,7 +10,7 @@
&nbsp;
<div class="box-filters hidden-xs">
<div class="form-group form-group-sm has-feedback has-feedback-left">
<input type="text" id="search" class="form-control" placeholder="Search groups..."
<input type="text" id="filterSearch" class="form-control" placeholder="Search groups..."
style="width: 200px;" ng-model="filterSearch">
<span class="fa fa-search form-control-feedback text-muted" aria-hidden="true"></span>
</div>

View File

@@ -10,7 +10,7 @@
&nbsp;
<div class="box-filters hidden-xs">
<div class="form-group form-group-sm has-feedback has-feedback-left">
<input type="text" id="search" class="form-control" placeholder="Search people..."
<input type="text" id="filterSearch" class="form-control" placeholder="Search people..."
style="width: 200px;" ng-model="filterSearch">
<span class="fa fa-search form-control-feedback text-muted" aria-hidden="true"></span>
</div>
@@ -46,6 +46,12 @@
<i class="fa fa-fw fa-sitemap"></i> Groups
</a>
</li>
<li>
<a href="#" stop-click ng-click="events(user)"
ng-if="useEvents && user.status === 2">
<i class="fa fa-fw fa-file-text-o"></i> Event Logs
</a>
</li>
<li ng-show="user.status === 1">
<a href="#" stop-click ng-click="confirm(user)">
<i class="fa fa-fw fa-check"></i> Confirm

View File

@@ -0,0 +1,56 @@
<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-file-text-o"></i> User Event Logs <small>{{email}}</small></h4>
</div>
<div class="modal-body">
<div class="hidden-xs">
<input type="datetime-local" ng-model="filterStart" required
class="form-control input-sm" style="width:initial; display: inline;" />
-
<input type="datetime-local" ng-model="filterEnd" required
class="form-control input-sm" style="width:initial; display: inline;" />
<button type="button" class="btn btn-primary btn-sm btn-flat" ng-click="refresh()">
<i class="fa fa-fw fa-refresh" ng-class="{'fa-spin': loading}"></i> Refresh
</button>
<hr />
</div>
<div ng-show="loading && !events.length">
Loading...
</div>
<div ng-show="!loading && !events.length">
<p>There are no events to list.</p>
</div>
<div class="table-responsive" ng-show="events.length" style="margin: 0;">
<table class="table table-striped table-hover" style="{{ !continuationToken ? 'margin: 0;' : '' }}">
<thead>
<tr>
<th>Timestamp</th>
<th><span class="sr-only">App</span></th>
<th>Event</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="event in filteredEvents = (events)">
<td style="width: 210px; min-width: 100px;">
{{event.date | date:'medium'}}
</td>
<td style="width: 20px;" class="text-center">
<i class="text-muted fa fa-lg {{event.appIcon}}" title="{{event.appName}}, {{event.ip}}"></i>
</td>
<td>
<div ng-bind-html="event.message"></div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="text-center" ng-show="continuationToken">
<button class="btn btn-link btn-block" ng-click="next()" ng-if="!loading">
Load more...
</button>
<i class="fa fa-fw fa-refresh fa-spin text-muted" ng-if="loading"></i>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
</div>

View File

@@ -16,7 +16,7 @@
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>
<i class="fa" ng-class="{'fa-cube': collection.id, 'fa-sitemap': !collection.id}"></i>
{{collection.name}}
<small ng-pluralize count="collectionCiphers.length" when="{'1': '{} item', 'other': '{} items'}"></small>
</h3>
@@ -56,6 +56,11 @@
<i class="fa fa-fw fa-cubes"></i> Collections
</a>
</li>
<li>
<a href="#" stop-click ng-click="viewEvents(cipher)" ng-if="useEvents">
<i class="fa fa-fw fa-file-text-o"></i> Event Logs
</a>
</li>
<li>
<a href="#" stop-click ng-click="removeCipher(cipher, collection)" class="text-red"
ng-if="collection.id">

View File

@@ -0,0 +1,60 @@
<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-file-text-o"></i> Event Logs <small>{{cipher.name}}</small></h4>
</div>
<div class="modal-body">
<div class="hidden-xs">
<input type="datetime-local" ng-model="filterStart" required
class="form-control input-sm" style="width:initial; display: inline;" />
-
<input type="datetime-local" ng-model="filterEnd" required
class="form-control input-sm" style="width:initial; display: inline;" />
<button type="button" class="btn btn-primary btn-sm btn-flat" ng-click="refresh()">
<i class="fa fa-fw fa-refresh" ng-class="{'fa-spin': loading}"></i> Refresh
</button>
<hr />
</div>
<div ng-show="loading && !events.length">
Loading...
</div>
<div ng-show="!loading && !events.length">
<p>There are no events to list.</p>
</div>
<div class="table-responsive" ng-show="events.length" style="margin: 0;">
<table class="table table-striped table-hover" style="{{ !continuationToken ? 'margin: 0;' : '' }}">
<thead>
<tr>
<th>Timestamp</th>
<th><span class="sr-only">App</span></th>
<th>User</th>
<th>Event</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="event in filteredEvents = (events)">
<td style="width: 210px; min-width: 100px;">
{{event.date | date:'medium'}}
</td>
<td style="width: 20px;" class="text-center">
<i class="text-muted fa fa-lg {{event.appIcon}}" title="{{event.appName}}, {{event.ip}}"></i>
</td>
<td style="width: 150px; min-width: 100px;">
{{event.userName}}
</td>
<td>
{{event.message}}
</td>
</tr>
</tbody>
</table>
</div>
<div class="text-center" ng-show="continuationToken">
<button class="btn btn-link btn-block" ng-click="next()" ng-if="!loading">
Load more...
</button>
<i class="fa fa-fw fa-refresh fa-spin text-muted" ng-if="loading"></i>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
</div>

View File

@@ -1,7 +1,7 @@
angular
.module('bit.services')
.factory('apiService', function ($resource, tokenService, appSettings, $httpParamSerializer) {
.factory('apiService', function ($resource, tokenService, appSettings, $httpParamSerializer, utilsService) {
var _service = {},
_apiUri = appSettings.apiUri,
_identityUri = appSettings.identityUri;
@@ -179,11 +179,21 @@
getPublicKey: { url: _apiUri + '/users/:id/public-key', method: 'GET', params: { id: '@id' } }
});
_service.events = $resource(_apiUri + '/events', {}, {
list: { method: 'GET', params: {} },
listOrganization: { url: _apiUri + '/organizations/:orgId/events', method: 'GET', params: { id: '@orgId' } },
listCipher: { url: _apiUri + '/ciphers/:id/events', method: 'GET', params: { id: '@id' } },
listOrganizationUser: { url: _apiUri + '/organizations/:orgId/users/:id/events', method: 'GET', params: { orgId: '@orgId', id: '@id' } }
});
_service.identity = $resource(_identityUri + '/connect', {}, {
token: {
url: _identityUri + '/connect/token',
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' },
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
'Device-Type': utilsService.getDeviceType()
},
transformRequest: transformUrlEncoded,
skipAuthorization: true,
params: {}

View File

@@ -95,7 +95,7 @@ angular
_service.logOut = function () {
tokenService.clearTokens();
cryptoService.clearKeys();
$rootScope.vaultFolders = $rootScope.vaultCiphers = null;
$rootScope.vaultGroupings = $rootScope.vaultCiphers = null;
_userProfile = null;
};
@@ -150,6 +150,8 @@ angular
maxStorageGb: profile.Organizations[i].MaxStorageGb,
seats: profile.Organizations[i].Seats,
useGroups: profile.Organizations[i].UseGroups,
useDirectory: profile.Organizations[i].UseDirectory,
useEvents: profile.Organizations[i].UseEvents,
useTotp: profile.Organizations[i].UseTotp
};
}
@@ -183,6 +185,8 @@ angular
maxStorageGb: org.MaxStorageGb,
seats: org.Seats,
useGroups: org.UseGroups,
useDirectory: org.UseDirectory,
useEvents: org.UseEvents,
useTotp: org.UseTotp
};
profile.organizations[o.id] = o;

View File

@@ -0,0 +1,269 @@
angular
.module('bit.services')
.factory('eventService', function (constants, $filter) {
var _service = {};
_service.getDefaultDateFilters = function () {
var d = new Date();
var filterEnd = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59);
d.setDate(d.getDate() - 30);
var filterStart = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0);
return {
start: filterStart,
end: filterEnd
};
};
_service.formatDateFilters = function (filterStart, filterEnd) {
var result = {
start: null,
end: null,
error: null
};
try {
var format = 'yyyy-MM-ddTHH:mm';
result.start = $filter('date')(filterStart, format + 'Z', 'UTC');
result.end = $filter('date')(filterEnd, format + ':59.999Z', 'UTC');
} catch (e) { }
if (!result.start || !result.end || result.end < result.start) {
result.error = 'Invalid date range.';
}
return result;
};
_service.getEventInfo = function (ev, options) {
options = options || {
cipherInfo: true
};
var appInfo = getAppInfo(ev);
return {
message: getEventMessage(ev, options),
appIcon: appInfo.icon,
appName: appInfo.name
};
};
function getEventMessage(ev, options) {
var msg = '';
switch (ev.Type) {
// User
case constants.eventType.User_LoggedIn:
msg = 'Logged in.';
break;
case constants.eventType.User_ChangedPassword:
msg = 'Changed account password.';
break;
case constants.eventType.User_Enabled2fa:
msg = 'Enabled two-step login.';
break;
case constants.eventType.User_Disabled2fa:
msg = 'Disabled two-step login.';
break;
case constants.eventType.User_Recovered2fa:
msg = 'Recovered account from two-step login.';
break;
case constants.eventType.User_FailedLogIn:
msg = 'Login attempt failed with incorrect password.';
break;
case constants.eventType.User_FailedLogIn2fa:
msg = 'Login attempt failed with incorrect two-step login.';
break;
// Cipher
case constants.eventType.Cipher_Created:
msg = options.cipherInfo ? 'Created item ' + formatCipherId(ev) + '.' : 'Created.';
break;
case constants.eventType.Cipher_Updated:
msg = options.cipherInfo ? 'Edited item ' + formatCipherId(ev) + '.' : 'Edited.';
break;
case constants.eventType.Cipher_Deleted:
msg = options.cipherInfo ? 'Deleted item ' + formatCipherId(ev) + '.' : 'Deleted';
break;
case constants.eventType.Cipher_AttachmentCreated:
msg = options.cipherInfo ? 'Created attachment for item ' + formatCipherId(ev) + '.' :
'Created attachment.';
break;
case constants.eventType.Cipher_AttachmentDeleted:
msg = options.cipherInfo ? 'Deleted attachment for item ' + formatCipherId(ev) + '.' :
'Deleted attachment.';
break;
case constants.eventType.Cipher_Shared:
msg = options.cipherInfo ? 'Shared item ' + formatCipherId(ev) + '.' : 'Shared.';
break;
case constants.eventType.Cipher_UpdatedCollections:
msg = options.cipherInfo ? 'Update collections for item ' + formatCipherId(ev) + '.' :
'Updated collections.';
break;
// Collection
case constants.eventType.Collection_Created:
msg = 'Created collection ' + formatCollectionId(ev) + '.';
break;
case constants.eventType.Collection_Updated:
msg = 'Edited collection ' + formatCollectionId(ev) + '.';
break;
case constants.eventType.Collection_Deleted:
msg = 'Deleted collection ' + formatCollectionId(ev) + '.';
break;
// Group
case constants.eventType.Group_Created:
msg = 'Created group ' + formatGroupId(ev) + '.';
break;
case constants.eventType.Group_Updated:
msg = 'Edited group ' + formatGroupId(ev) + '.';
break;
case constants.eventType.Group_Deleted:
msg = 'Deleted group ' + formatGroupId(ev) + '.';
break;
// Org user
case constants.eventType.OrganizationUser_Invited:
msg = 'Invited user ' + formatOrgUserId(ev) + '.';
break;
case constants.eventType.OrganizationUser_Confirmed:
msg = 'Confirmed user ' + formatOrgUserId(ev) + '.';
break;
case constants.eventType.OrganizationUser_Updated:
msg = 'Edited user ' + formatOrgUserId(ev) + '.';
break;
case constants.eventType.OrganizationUser_Removed:
msg = 'Removed user ' + formatOrgUserId(ev) + '.';
break;
case constants.eventType.OrganizationUser_UpdatedGroups:
msg = 'Edited groups for user ' + formatOrgUserId(ev) + '.';
break;
// Org
case constants.eventType.Organization_Updated:
msg = 'Edited organization settings.';
break;
default:
break;
}
return msg === '' ? null : msg;
}
function getAppInfo(ev) {
var appInfo = {
icon: 'fa-globe',
name: 'Unknown'
};
switch (ev.DeviceType) {
case constants.deviceType.android:
appInfo.icon = 'fa-android';
appInfo.name = 'Mobile App - Android';
break;
case constants.deviceType.ios:
appInfo.icon = 'fa-apple';
appInfo.name = 'Mobile App - iOS';
break;
case constants.deviceType.uwp:
appInfo.icon = 'fa-windows';
appInfo.name = 'Mobile App - Windows';
break;
case constants.deviceType.chromeExt:
appInfo.icon = 'fa-chrome';
appInfo.name = 'Extension - Chrome';
break;
case constants.deviceType.firefoxExt:
appInfo.icon = 'fa-firefox';
appInfo.name = 'Extension - Firefox';
break;
case constants.deviceType.operaExt:
appInfo.icon = 'fa-opera';
appInfo.name = 'Extension - Opera';
break;
case constants.deviceType.edgeExt:
appInfo.icon = 'fa-edge';
appInfo.name = 'Extension - Edge';
break;
case constants.deviceType.vivaldiExt:
appInfo.icon = 'fa-puzzle-piece';
appInfo.name = 'Extension - Vivaldi';
break;
case constants.deviceType.windowsDesktop:
appInfo.icon = 'fa-windows';
appInfo.name = 'Desktop - Windows';
break;
case constants.deviceType.macOsDesktop:
appInfo.icon = 'fa-apple';
appInfo.name = 'Desktop - macOS';
break;
case constants.deviceType.linuxDesktop:
appInfo.icon = 'fa-linux';
appInfo.name = 'Desktop - Linux';
break;
case constants.deviceType.chrome:
appInfo.icon = 'fa-globe';
appInfo.name = 'Web Vault - Chrome';
break;
case constants.deviceType.firefox:
appInfo.icon = 'fa-globe';
appInfo.name = 'Web Vault - Firefox';
break;
case constants.deviceType.opera:
appInfo.icon = 'fa-globe';
appInfo.name = 'Web Vault - Opera';
break;
case constants.deviceType.safari:
appInfo.icon = 'fa-globe';
appInfo.name = 'Web Vault - Safari';
break;
case constants.deviceType.vivaldi:
appInfo.icon = 'fa-globe';
appInfo.name = 'Web Vault - Vivaldi';
break;
case constants.deviceType.edge:
appInfo.icon = 'fa-globe';
appInfo.name = 'Web Vault - Edge';
break;
case constants.deviceType.ie:
appInfo.icon = 'fa-globe';
appInfo.name = 'Web Vault - IE';
break;
case constants.deviceType.unknown:
appInfo.icon = 'fa-globe';
appInfo.name = 'Web Vault - Unknown';
break;
default:
break;
}
return appInfo;
}
function formatCipherId(ev) {
var shortId = ev.CipherId.substring(0, 8);
if (!ev.OrganizationId) {
return '<code>' + shortId + '</code>';
}
return '<a title="View item ' + ev.CipherId + '" ui-sref="backend.org.vault({orgId:\'' + ev.OrganizationId + '\',search:\'' + shortId + '\',viewEvents:\'' + ev.CipherId + '\'})">' +
'<code>' + shortId + '</code></a>';
}
function formatGroupId(ev) {
var shortId = ev.GroupId.substring(0, 8);
return '<a title="View group ' + ev.GroupId + '" ui-sref="backend.org.groups({orgId:\'' + ev.OrganizationId + '\',search:\'' + shortId + '\'})">' +
'<code>' + shortId + '</code></a>';
}
function formatCollectionId(ev) {
var shortId = ev.CollectionId.substring(0, 8);
return '<a title="View collection ' + ev.CollectionId + '" ui-sref="backend.org.collections({orgId:\'' + ev.OrganizationId + '\',search:\'' + shortId + '\'})">' +
'<code>' + shortId + '</code></a>';
}
function formatOrgUserId(ev) {
var shortId = ev.OrganizationUserId.substring(0, 8);
return '<a title="View user ' + ev.OrganizationUserId + '" ui-sref="backend.org.people({orgId:\'' + ev.OrganizationId + '\',search:\'' + shortId + '\'})">' +
'<code>' + shortId + '</code></a>';
}
return _service;
});

View File

@@ -236,6 +236,63 @@
}, errorCallback);
}
// ref https://stackoverflow.com/a/5911300
function getCardType(number) {
if (!number) {
return null;
}
// Visa
var re = new RegExp('^4');
if (number.match(re) != null) {
return 'Visa';
}
// Mastercard
// Updated for Mastercard 2017 BINs expansion
if (/^(5[1-5][0-9]{14}|2(22[1-9][0-9]{12}|2[3-9][0-9]{13}|[3-6][0-9]{14}|7[0-1][0-9]{13}|720[0-9]{12}))$/.test(number)) {
return 'Mastercard';
}
// AMEX
re = new RegExp('^3[47]');
if (number.match(re) != null) {
return 'Amex';
}
// Discover
re = new RegExp('^(6011|622(12[6-9]|1[3-9][0-9]|[2-8][0-9]{2}|9[0-1][0-9]|92[0-5]|64[4-9])|65)');
if (number.match(re) != null) {
return 'Discover';
}
// Diners
re = new RegExp('^36');
if (number.match(re) != null) {
return 'Diners Club';
}
// Diners - Carte Blanche
re = new RegExp('^30[0-5]');
if (number.match(re) != null) {
return 'Diners Club';
}
// JCB
re = new RegExp('^35(2[89]|[3-8][0-9])');
if (number.match(re) != null) {
return 'JCB';
}
// Visa Electron
re = new RegExp('^(4026|417500|4508|4844|491(3|7))');
if (number.match(re) != null) {
return 'Visa';
}
return null;
}
// importers
function importBitwardenCsv(file, success, error) {
@@ -557,6 +614,28 @@
return obj;
}
function parseCard(value) {
var cardData = {
cardholderName: value.ccname && value.ccname !== '' ? value.ccname : null,
number: value.ccnum && value.ccnum !== '' ? value.ccnum : null,
brand: value.ccnum && value.ccnum !== '' ? getCardType(value.ccnum) : null,
code: value.cccsc && value.cccsc !== '' ? value.cccsc : null
};
if (value.ccexp && value.ccexp !== '' && value.ccexp.indexOf('-') > -1) {
var ccexpParts = value.ccexp.split('-');
if (ccexpParts.length > 1) {
cardData.expYear = ccexpParts[0];
cardData.expMonth = ccexpParts[1];
if (cardData.expMonth.length === 2 && cardData.expMonth[0] === '0') {
cardData.expMonth = cardData.expMonth[1];
}
}
}
return cardData;
}
function parseData(data) {
var folders = [],
ciphers = [],
@@ -579,11 +658,29 @@
}
}
var cipher = {
favorite: org ? false : value.fav === '1',
name: value.name && value.name !== '' ? value.name : '--',
type: value.url === 'http://sn' ? constants.cipherType.secureNote : constants.cipherType.login
};
var cipher;
if (value.hasOwnProperty('profilename') && value.hasOwnProperty('profilelanguage')) {
// form fill
cipher = {
favorite: false,
name: value.profilename && value.profilename !== '' ? value.profilename : '--',
type: constants.cipherType.card
};
if (value.title !== '' || value.firstname !== '' || value.lastname !== '' ||
value.address1 !== '' || value.phone !== '' || value.username !== '' ||
value.email !== '') {
cipher.type = constants.cipherType.identity;
}
}
else {
// site or secure note
cipher = {
favorite: org ? false : value.fav === '1',
name: value.name && value.name !== '' ? value.name : '--',
type: value.url === 'http://sn' ? constants.cipherType.secureNote : constants.cipherType.login
};
}
if (cipher.type === constants.cipherType.login) {
cipher.login = {
@@ -644,6 +741,46 @@
cipher.notes = value.extra && value.extra !== '' ? value.extra : null;
}
}
else if (cipher.type === constants.cipherType.card) {
cipher.card = parseCard(value);
cipher.notes = value.notes && value.notes !== '' ? value.notes : null;
}
else if (cipher.type === constants.cipherType.identity) {
cipher.identity = {
title: value.title && value.title !== '' ? value.title : null,
firstName: value.firstname && value.firstname !== '' ? value.firstname : null,
middleName: value.middlename && value.middlename !== '' ? value.middlename : null,
lastName: value.lastname && value.lastname !== '' ? value.lastname : null,
username: value.username && value.username !== '' ? value.username : null,
company: value.company && value.company !== '' ? value.company : null,
ssn: value.ssn && value.ssn !== '' ? value.ssn : null,
address1: value.address1 && value.address1 !== '' ? value.address1 : null,
address2: value.address2 && value.address2 !== '' ? value.address2 : null,
address3: value.address3 && value.address3 !== '' ? value.address3 : null,
city: value.city && value.city !== '' ? value.city : null,
state: value.state && value.state !== '' ? value.state : null,
postalCode: value.zip && value.zip !== '' ? value.zip : null,
country: value.country && value.country !== '' ? value.country : null,
email: value.email && value.email !== '' ? value.email : null,
phone: value.phone && value.phone !== '' ? value.phone : null
};
cipher.notes = value.notes && value.notes !== '' ? value.notes : null;
if (cipher.identity.title) {
cipher.identity.title = cipher.identity.title.charAt(0).toUpperCase() +
cipher.identity.title.slice(1);
}
if (value.ccnum && value.ccnum !== '') {
// there is a card on this identity too
var cardCipher = JSON.parse(JSON.stringify(cipher)); // cloned
cardCipher.identity = null;
cardCipher.type = constants.cipherType.card;
cardCipher.card = parseCard(value);
ciphers.push(cardCipher);
}
}
ciphers.push(cipher);
@@ -738,6 +875,9 @@
else if (type === 'weblogin' || type === 'website') {
cipher.login.uri = trimUri(text);
}
else if (text.length > 200) {
cipher.notes += (name + ': ' + text + '\n');
}
else {
if (!cipher.fields) {
cipher.fields = [];
@@ -970,16 +1110,25 @@
cipher.notes = cipher.notes === null ? value + '\n' : cipher.notes + value + '\n';
break;
default:
if (!cipher.fields) {
cipher.fields = [];
}
if (value.length > 200 || value.indexOf('\n') > -1) {
if (!cipher.notes) {
cipher.notes = '';
}
// other custom fields
cipher.fields.push({
name: key,
value: value,
type: constants.fieldType.text
});
cipher.notes += (key + ': ' + value + '\n');
}
else {
if (!cipher.fields) {
cipher.fields = [];
}
// other custom fields
cipher.fields.push({
name: key,
value: value,
type: constants.fieldType.text
});
}
break;
}
}
@@ -1106,7 +1255,7 @@
}
else if (fieldValue) {
var fieldName = (field[nameKey] || 'no_name');
if (fieldValue.indexOf('\\n') > -1) {
if (fieldValue.indexOf('\\n') > -1 || fieldValue.length > 200) {
if (cipher.notes === null) {
cipher.notes = '';
}
@@ -1434,14 +1583,25 @@
if (value.length > 6) {
// we have some custom fields.
cipher.fields = [];
for (i = 6; i < value.length; i = i + 2) {
cipher.fields.push({
name: value[i],
value: value[i + 1],
type: constants.fieldType.text
});
if (value[i + 1] && value[i + 1].length > 200) {
if (!cipher.notes) {
cipher.notes = '';
}
cipher.notes += (value[i] + ': ' + value[i + 1] + '\n');
}
else {
if (!cipher.fields) {
cipher.fields = [];
}
cipher.fields.push({
name: value[i],
value: value[i + 1],
type: constants.fieldType.text
});
}
}
}
@@ -1549,15 +1709,24 @@
continue;
}
if (!cipher.fields) {
cipher.fields = [];
}
if (attrValue.length > 200) {
if (!cipher.notes) {
cipher.notes = '';
}
cipher.fields.push({
name: attrName,
value: attrValue,
type: constants.fieldType.text
});
cipher.notes += (attrName + ': ' + attrValue + '\n');
}
else {
if (!cipher.fields) {
cipher.fields = [];
}
cipher.fields.push({
name: attrName,
value: attrValue,
type: constants.fieldType.text
});
}
}
}
@@ -1642,6 +1811,13 @@
else if (fieldLower === 'totp' && !cipher.login.totp) {
cipher.login.totp = value;
}
else if (value.length > 200) {
if (!cipher.notes) {
cipher.notes = '';
}
cipher.notes += (field + ': ' + value + '\n');
}
else {
// other fields
if (!cipher.fields) {
@@ -2132,16 +2308,25 @@
for (var property in value) {
if (value.hasOwnProperty(property) && propsToIgnore.indexOf(property.toLowerCase()) < 0 &&
value[property] && value[property] !== '') {
if (!cipher.fields) {
cipher.fields = [];
}
if (value[property].length > 200) {
if (!cipher.notes) {
cipher.notes = '';
}
// other custom fields
cipher.fields.push({
name: property,
value: value[property],
type: constants.fieldType.text
});
cipher.notes += (property + ': ' + value[property] + '\n');
}
else {
if (!cipher.fields) {
cipher.fields = [];
}
// other custom fields
cipher.fields.push({
name: property,
value: value[property],
type: constants.fieldType.text
});
}
}
}
}
@@ -2213,6 +2398,13 @@
else if (!cipher.login.password && isField(field.label, _passwordFieldNames)) {
cipher.login.password = field.value;
}
else if (field.value.length > 200) {
if (!cipher.notes) {
cipher.notes = '';
}
cipher.notes += (field.label + ': ' + field.value + '\n');
}
else {
if (!cipher.fields) {
cipher.fields = [];
@@ -2340,6 +2532,13 @@
else if (!cipher.login.username && isField(field.replace(':', ''), _usernameFieldNames)) {
cipher.login.username = fieldValue;
}
else if (fieldValue.length > 200) {
if (!cipher.notes) {
cipher.notes = '';
}
cipher.notes += (field + ': ' + fieldValue + '\n');
}
else {
if (!cipher.fields) {
cipher.fields = [];
@@ -2452,6 +2651,13 @@
else if (!cipher.login.password && isField(field, _passwordFieldNames)) {
cipher.login.password = value;
}
else if (value.length > 200) {
if (!cipher.notes) {
cipher.notes = '';
}
cipher.notes += (field + ': ' + value + '\n');
}
else {
if (!cipher.fields) {
cipher.fields = [];
@@ -2522,6 +2728,13 @@
else if (property === 'password') {
cipher.login.password = value;
}
else if (value.length > 200) {
if (!cipher.notes) {
cipher.notes = '';
}
cipher.notes += (property + ': ' + value + '\n');
}
else {
if (!cipher.fields) {
cipher.fields = [];
@@ -2575,6 +2788,13 @@
else if (fieldLower === 'password') {
cipher.login.password = value;
}
else if (value.length > 200) {
if (!cipher.notes) {
cipher.notes = '';
}
cipher.notes += (field + ': ' + value + '\n');
}
else {
if (!cipher.fields) {
cipher.fields = [];

View File

@@ -0,0 +1,47 @@
angular
.module('bit.services')
.factory('utilsService', function (constants) {
var _service = {};
var _browserCache;
_service.getDeviceType = function (token) {
if (_browserCache) {
return _browserCache;
}
if (navigator.userAgent.indexOf(' Vivaldi/') >= 0) {
_browserCache = constants.deviceType.vivaldi;
}
else if (!!window.chrome && !!window.chrome.webstore) {
_browserCache = constants.deviceType.chrome;
}
else if (typeof InstallTrigger !== 'undefined') {
_browserCache = constants.deviceType.firefox;
}
else if ((!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0) {
_browserCache = constants.deviceType.firefox;
}
else if (/constructor/i.test(window.HTMLElement) ||
safariCheck(!window.safari || (typeof safari !== 'undefined' && safari.pushNotification))) {
_browserCache = constants.deviceType.opera;
}
else if (!!document.documentMode) {
_browserCache = constants.deviceType.ie;
}
else if (!!window.StyleMedia) {
_browserCache = constants.deviceType.edge;
}
else {
_browserCache = constants.deviceType.unknown;
}
return _browserCache;
};
function safariCheck(p) {
return p.toString() === '[object SafariRemoteNotification]';
}
return _service;
});

View File

@@ -62,5 +62,41 @@
}
};
_service.parseErrors = function (reason) {
var data = reason.data;
var defaultErrorMessage = 'An unexpected error has occurred.';
var errors = [];
if (!data || !angular.isObject(data)) {
errors.push(defaultErrorMessage);
return errors;
}
if (data && data.ErrorModel) {
data = data.ErrorModel;
}
if (!data.ValidationErrors) {
if (data.Message) {
errors.push(data.Message);
}
else {
errors.push(defaultErrorMessage);
}
}
for (var key in data.ValidationErrors) {
if (!data.ValidationErrors.hasOwnProperty(key)) {
continue;
}
for (var i = 0; i < data.ValidationErrors[key].length; i++) {
errors.push(data.ValidationErrors[key][i]);
}
}
return errors;
};
return _service;
});

View File

@@ -1,2 +1,2 @@
angular.module("bit")
.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.20.0","environment":"Production"});
.constant("appSettings", {"apiUri":"/api","identityUri":"/identity","iconsUri":"https://icons.bitwarden.com","stripeKey":"pk_live_bpN0P37nMxrMQkcaHXtAybJk","braintreeKey":"production_qfbsv8kc_njj2zjtyngtjmbjd","selfHosted":false,"version":"1.22.0","environment":"Production"});

View File

@@ -95,7 +95,7 @@
<label>
<input type="radio" ng-model="model.plan" name="PlanType" value="families">
Families
<span>For personal users such as families &amp; friends.</span>
<span>For personal use, to share with family &amp; friends.</span>
<span>- Add and share with up to 5 users</span>
<span>- Create unlimited collections</span>
<span>- 1 GB encrypted file storage</span>

View File

@@ -282,7 +282,7 @@
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.');
importError('Data is not formatted correctly. Please check your import file and try again.');
return;
}
}

View File

@@ -2,9 +2,9 @@
.module('bit.vault')
.controller('vaultAddCipherController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService,
passwordService, selectedFolder, $analytics, checkedFavorite, $rootScope, authService, $uibModal, constants) {
passwordService, selectedFolder, $analytics, checkedFavorite, $rootScope, authService, $uibModal, constants, $filter) {
$analytics.eventTrack('vaultAddCipherController', { category: 'Modal' });
$scope.folders = $rootScope.vaultFolders;
$scope.folders = $filter('filter')($rootScope.vaultGroupings, { folder: true });
$scope.constants = constants;
$scope.selectedType = constants.cipherType.login.toString();
$scope.cipher = {
@@ -15,7 +15,7 @@
identity: {},
card: {},
secureNote: {
type: '0'
type: 0
}
};
@@ -40,7 +40,7 @@
$scope.generatePassword = function () {
if (!$scope.cipher.login.password || confirm('Are you sure you want to overwrite the current password?')) {
$analytics.eventTrack('Generated Password From Add');
$scope.cipher.login.password = passwordService.generatePassword({ length: 12, special: true });
$scope.cipher.login.password = passwordService.generatePassword({ length: 14, special: true });
}
};

View File

@@ -45,13 +45,9 @@
fileEl.type = '';
fileEl.type = 'file';
fileEl.value = '';
}, function (err) {
if (err) {
validationService.addError(form, 'file', err, true);
}
else {
validationService.addError(form, 'file', 'Something went wrong.', true);
}
}, function (e) {
var errors = validationService.parseErrors(e);
toastr.error(errors.length ? errors[0] : 'An error occurred.');
});
};

View File

@@ -2,22 +2,27 @@
.module('bit.vault')
.controller('vaultController', function ($scope, $uibModal, apiService, $filter, cryptoService, authService, toastr,
cipherService, $q, $localStorage, $timeout, $rootScope, $state, $analytics, constants) {
cipherService, $q, $localStorage, $timeout, $rootScope, $state, $analytics, constants, validationService) {
$scope.loading = true;
$scope.ciphers = [];
$scope.folderCount = 0;
$scope.collectionCount = 0;
$scope.firstCollectionId = null;
$scope.constants = constants;
$scope.favoriteCollapsed = $localStorage.collapsedFolders && 'favorite' in $localStorage.collapsedFolders;
$scope.folderIdFilter = undefined;
$scope.groupingIdFilter = undefined;
$scope.typeFilter = undefined;
if ($state.params.refreshFromServer) {
$rootScope.vaultFolders = $rootScope.vaultCiphers = null;
$rootScope.vaultGroupings = $rootScope.vaultCiphers = null;
}
$scope.$on('$viewContentLoaded', function () {
if ($rootScope.vaultFolders && $rootScope.vaultCiphers) {
$("#search").focus();
if ($rootScope.vaultGroupings && $rootScope.vaultCiphers) {
$scope.loading = false;
loadFolderData($rootScope.vaultFolders);
loadGroupingData($rootScope.vaultGroupings);
loadCipherData($rootScope.vaultCiphers);
return;
}
@@ -26,20 +31,32 @@
});
function loadDataFromServer() {
var folderPromise = apiService.folders.list({}, function (folders) {
var decFolders = [{
id: null,
name: 'No Folder'
}];
var decGroupings = [{
id: null,
name: 'No Folder',
folder: true
}];
var collectionPromise = apiService.collections.listMe({ writeOnly: false }, function (collections) {
for (var i = 0; i < collections.Data.length; i++) {
var decCollection = cipherService.decryptCollection(collections.Data[i], null, true);
decCollection.collection = true;
decGroupings.push(decCollection);
}
}).$promise;
var folderPromise = apiService.folders.list({}, function (folders) {
for (var i = 0; i < folders.Data.length; i++) {
var decFolder = cipherService.decryptFolderPreview(folders.Data[i]);
decFolders.push(decFolder);
decFolder.folder = true;
decGroupings.push(decFolder);
}
loadFolderData(decFolders);
}).$promise;
var groupingPromise = $q.all([collectionPromise, folderPromise]).then(function () {
loadGroupingData(decGroupings);
});
var cipherPromise = apiService.ciphers.list({}, function (ciphers) {
var decCiphers = [];
@@ -48,31 +65,40 @@
decCiphers.push(decCipher);
}
folderPromise.then(function () {
groupingPromise.then(function () {
loadCipherData(decCiphers);
});
}).$promise;
$q.all([cipherPromise, folderPromise]).then(function () {
$q.all([cipherPromise, groupingPromise]).then(function () {
$scope.loading = false;
});
}
function loadFolderData(decFolders) {
$rootScope.vaultFolders = $filter('orderBy')(decFolders, folderSort);
function loadGroupingData(decGroupings) {
$rootScope.vaultGroupings = $filter('orderBy')(decGroupings, ['folder', groupingSort]);
var collections = $filter('filter')($rootScope.vaultGroupings, { collection: true });
$scope.collectionCount = collections.length;
$scope.folderCount = decGroupings.length - collections.length - 1;
if (collections && collections.length) {
$scope.firstCollectionId = collections[0].id;
}
}
function loadCipherData(decCiphers) {
angular.forEach($rootScope.vaultFolders, function (folderValue, folderIndex) {
folderValue.collapsed = $localStorage.collapsedFolders &&
(folderValue.id || 'none') in $localStorage.collapsedFolders;
angular.forEach($rootScope.vaultGroupings, function (grouping, groupingIndex) {
grouping.collapsed = $localStorage.collapsedFolders &&
(grouping.id || 'none') in $localStorage.collapsedFolders;
angular.forEach(decCiphers, function (cipherValue) {
if (cipherValue.favorite) {
cipherValue.sort = -1;
}
else if (cipherValue.folderId == folderValue.id) {
cipherValue.sort = folderIndex;
else if (grouping.folder && cipherValue.folderId == grouping.id) {
cipherValue.sort = groupingIndex;
}
else if (grouping.collection && cipherValue.collectionIds.indexOf(grouping.id) > -1) {
cipherValue.sort = groupingIndex;
}
});
});
@@ -110,7 +136,7 @@
return chunks;
}
function folderSort(item) {
function groupingSort(item) {
if (!item.id) {
return '';
}
@@ -123,12 +149,12 @@
'Edit the item and copy it manually instead.');
};
$scope.collapseExpand = function (folder, favorite) {
$scope.collapseExpand = function (grouping, favorite) {
if (!$localStorage.collapsedFolders) {
$localStorage.collapsedFolders = {};
}
var id = favorite ? 'favorite' : (folder.id || 'none');
var id = favorite ? 'favorite' : (grouping.id || 'none');
if (id in $localStorage.collapsedFolders) {
delete $localStorage.collapsedFolders[id];
}
@@ -137,6 +163,34 @@
}
};
$scope.collapseAll = function () {
if (!$localStorage.collapsedFolders) {
$localStorage.collapsedFolders = {};
}
$localStorage.collapsedFolders.none = true;
$localStorage.collapsedFolders.favorite = true;
if ($rootScope.vaultGroupings) {
for (var i = 0; i < $rootScope.vaultGroupings.length; i++) {
$localStorage.collapsedFolders[$rootScope.vaultGroupings[i].id] = true;
}
}
$('.box').addClass('collapsed-box');
$('.box .box-header button i.fa-minus').removeClass('fa-minus').addClass('fa-plus');
};
$scope.expandAll = function () {
if ($localStorage.collapsedFolders) {
delete $localStorage.collapsedFolders;
}
$('.box').removeClass('collapsed-box');
$('.box-body').show();
$('.box .box-header button i.fa-plus').removeClass('fa-plus').addClass('fa-minus');
};
$scope.editCipher = function (cipher) {
var editModel = $uibModal.open({
animation: true,
@@ -169,13 +223,13 @@
$scope.addCipher();
});
$scope.addCipher = function (folder, favorite) {
$scope.addCipher = function (grouping, favorite) {
var addModel = $uibModal.open({
animation: true,
templateUrl: 'app/vault/views/vaultAddCipher.html',
controller: 'vaultAddCipherController',
resolve: {
selectedFolder: function () { return folder; },
selectedFolder: function () { return grouping && grouping.folder ? grouping : null; },
checkedFavorite: function () { return favorite; }
}
});
@@ -276,8 +330,9 @@
});
addModel.result.then(function (addedFolder) {
$rootScope.vaultFolders.push(addedFolder);
loadFolderData($rootScope.vaultFolders);
addedFolder.folder = true;
$rootScope.vaultGroupings.push(addedFolder);
loadGroupingData($rootScope.vaultGroupings);
});
};
@@ -288,9 +343,10 @@
apiService.folders.del({ id: folder.id }, function () {
$analytics.eventTrack('Deleted Folder');
var index = $rootScope.vaultFolders.indexOf(folder);
var index = $rootScope.vaultGroupings.indexOf(folder);
if (index > -1) {
$rootScope.vaultFolders.splice(index, 1);
$rootScope.vaultGroupings.splice(index, 1);
$scope.folderCount--;
}
});
};
@@ -319,7 +375,7 @@
});
};
$scope.collections = function (cipher) {
$scope.editCollections = function (cipher) {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/vault/views/vaultCipherCollections.html',
@@ -333,11 +389,14 @@
if (response.collectionIds && !response.collectionIds.length) {
removeCipherFromScopes(cipher);
}
else if (response.collectionIds) {
cipher.collectionIds = response.collectionIds;
}
});
};
$scope.filterFolder = function (folder) {
$scope.folderIdFilter = folder.id;
$scope.filterGrouping = function (grouping) {
$scope.groupingIdFilter = grouping.id;
if ($.AdminLTE && $.AdminLTE.layout) {
$timeout(function () {
@@ -357,7 +416,7 @@
};
$scope.clearFilters = function () {
$scope.folderIdFilter = undefined;
$scope.groupingIdFilter = undefined;
$scope.typeFilter = undefined;
if ($.AdminLTE && $.AdminLTE.layout) {
@@ -367,12 +426,22 @@
}
};
$scope.folderFilter = function (folder) {
return $scope.folderIdFilter === undefined || folder.id === $scope.folderIdFilter;
$scope.groupingFilter = function (grouping) {
return $scope.groupingIdFilter === undefined || grouping.id === $scope.groupingIdFilter;
};
$scope.cipherFilter = function (cipher) {
return $scope.typeFilter === undefined || cipher.type === $scope.typeFilter;
$scope.cipherFilter = function (grouping) {
return function (cipher) {
var matchesGrouping = grouping === null;
if (!matchesGrouping && grouping.folder && cipher.folderId === grouping.id) {
matchesGrouping = true;
}
else if (!matchesGrouping && grouping.collection && cipher.collectionIds.indexOf(grouping.id) > -1) {
matchesGrouping = true;
}
return matchesGrouping && ($scope.typeFilter === undefined || cipher.type === $scope.typeFilter);
};
};
$scope.unselectAll = function () {
@@ -445,7 +514,7 @@
return;
}
$scope.bulkActionLoading = true;
$scope.actionLoading = true;
apiService.ciphers.delMany({ ids: ids }, function () {
$analytics.eventTrack('Bulk Deleted Items');
@@ -457,11 +526,12 @@
}
selectAll(false);
$scope.bulkActionLoading = false;
$scope.actionLoading = false;
toastr.success('Items have been deleted!');
}, function () {
toastr.error('An error occurred.');
$scope.bulkActionLoading = false;
}, function (e) {
var errors = validationService.parseErrors(e);
toastr.error(errors.length ? errors[0] : 'An error occurred.');
$scope.actionLoading = false;
});
};

View File

@@ -2,9 +2,9 @@
.module('bit.vault')
.controller('vaultEditCipherController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService,
passwordService, cipherId, $analytics, $rootScope, authService, $uibModal, constants) {
passwordService, cipherId, $analytics, $rootScope, authService, $uibModal, constants, $filter) {
$analytics.eventTrack('vaultEditCipherController', { category: 'Modal' });
$scope.folders = $rootScope.vaultFolders;
$scope.folders = $filter('filter')($rootScope.vaultGroupings, { folder: true });
$scope.cipher = {};
$scope.readOnly = false;
$scope.constants = constants;
@@ -51,7 +51,7 @@
$scope.generatePassword = function () {
if (!$scope.cipher.login.password || confirm('Are you sure you want to overwrite the current password?')) {
$analytics.eventTrack('Generated Password From Edit');
$scope.cipher.login.password = passwordService.generatePassword({ length: 12, special: true });
$scope.cipher.login.password = passwordService.generatePassword({ length: 14, special: true });
}
};

View File

@@ -2,9 +2,9 @@
.module('bit.vault')
.controller('vaultMoveCiphersController', function ($scope, apiService, $uibModalInstance, ids, $analytics,
$rootScope) {
$rootScope, $filter) {
$analytics.eventTrack('vaultMoveCiphersController', { category: 'Modal' });
$scope.folders = $rootScope.vaultFolders;
$scope.folders = $filter('filter')($rootScope.vaultGroupings, { folder: true });
$scope.count = ids.length;
$scope.save = function () {

View File

@@ -1,239 +0,0 @@
angular
.module('bit.vault')
.controller('vaultSharedController', function ($scope, apiService, cipherService, $analytics, $q, $localStorage,
$uibModal, $filter, $rootScope, authService, cryptoService) {
$scope.ciphers = [];
$scope.collections = [];
$scope.loading = true;
$scope.$on('$viewContentLoaded', function () {
var collectionPromise = apiService.collections.listMe({ writeOnly: false }, function (collections) {
var decCollections = [];
for (var i = 0; i < collections.Data.length; i++) {
var decCollection = cipherService.decryptCollection(collections.Data[i], null, true);
decCollection.collapsed = $localStorage.collapsedCollections &&
decCollection.id in $localStorage.collapsedCollections;
decCollections.push(decCollection);
}
$scope.collections = decCollections;
}).$promise;
var cipherPromise = apiService.ciphers.listDetails({}, function (ciphers) {
var decCiphers = [];
for (var i = 0; i < ciphers.Data.length; i++) {
var decCipher = cipherService.decryptCipherPreview(ciphers.Data[i]);
decCiphers.push(decCipher);
}
if (decCiphers.length) {
$scope.collections.push({
id: null,
name: 'Unassigned',
collapsed: $localStorage.collapsedCollections && 'unassigned' in $localStorage.collapsedCollections
});
}
$scope.ciphers = decCiphers;
}).$promise;
$q.all([collectionPromise, cipherPromise]).then(function () {
$scope.loading = false;
});
});
$scope.clipboardError = function (e) {
alert('Your web browser does not support easy clipboard copying. ' +
'Edit the item and copy it manually instead.');
};
$scope.attachments = function (cipher) {
authService.getUserProfile().then(function (profile) {
return {
isPremium: profile.premium,
orgUseStorage: cipher.organizationId && !!profile.organizations[cipher.organizationId].maxStorageGb
};
}).then(function (perms) {
if (cipher.organizationId && !perms.orgUseStorage) {
$uibModal.open({
animation: true,
templateUrl: 'app/views/paidOrgRequired.html',
controller: 'paidOrgRequiredController',
resolve: {
orgId: function () { return cipher.organizationId; }
}
});
return;
}
if (!cipher.organizationId && !perms.isPremium) {
$uibModal.open({
animation: true,
templateUrl: 'app/views/premiumRequired.html',
controller: 'premiumRequiredController'
});
return;
}
if (!cipher.organizationId && !cryptoService.getEncKey()) {
toastr.error('You cannot use this feature until you update your encryption key.', 'Feature Unavailable');
return;
}
var attachmentModel = $uibModal.open({
animation: true,
templateUrl: 'app/vault/views/vaultAttachments.html',
controller: 'vaultAttachmentsController',
resolve: {
cipherId: function () { return cipher.id; }
}
});
attachmentModel.result.then(function (hasAttachments) {
cipher.hasAttachments = hasAttachments;
});
});
};
$scope.filterByCollection = function (collection) {
return function (cipher) {
if (!cipher.collectionIds || !cipher.collectionIds.length) {
return collection.id === null;
}
return cipher.collectionIds.indexOf(collection.id) > -1;
};
};
$scope.collectionSort = function (item) {
if (!item.id) {
return '';
}
return item.name.toLowerCase();
};
$scope.collapseExpand = function (collection) {
if (!$localStorage.collapsedCollections) {
$localStorage.collapsedCollections = {};
}
var id = collection.id || 'unassigned';
if (id in $localStorage.collapsedCollections) {
delete $localStorage.collapsedCollections[id];
}
else {
$localStorage.collapsedCollections[id] = true;
}
};
$scope.editCipher = function (cipher) {
var editModel = $uibModal.open({
animation: true,
templateUrl: 'app/vault/views/vaultEditCipher.html',
controller: 'vaultEditCipherController',
resolve: {
cipherId: function () { return cipher.id; }
}
});
editModel.result.then(function (returnVal) {
var rootCipher = findRootCipher(cipher) || { meta: {} },
index;
if (returnVal.action === 'edit') {
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') {
cipher.folderId = rootCipher.folderId = returnVal.data.folderId;
cipher.favorite = rootCipher.favorite = returnVal.data.favorite;
}
else if (returnVal.action === 'delete') {
index = $scope.ciphers.indexOf(cipher);
if (index > -1) {
$scope.ciphers.splice(index, 1);
}
removeRootCipher(rootCipher);
}
});
};
$scope.editCollections = function (cipher) {
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/vault/views/vaultCipherCollections.html',
controller: 'vaultCipherCollectionsController',
resolve: {
cipherId: function () { return cipher.id; }
}
});
modal.result.then(function (response) {
if (response.collectionIds) {
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.removeCipher = function (cipher, collection) {
if (!confirm('Are you sure you want to remove this item (' + cipher.name + ') from the ' +
'collection (' + collection.name + ') ?')) {
return;
}
var request = {
collectionIds: []
};
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: cipher.id }, request).$promise.then(function (response) {
$analytics.eventTrack('Removed From Collection');
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 findRootCipher(cipher) {
if ($rootScope.vaultCiphers) {
var rootCiphers = $filter('filter')($rootScope.vaultCiphers, { id: cipher.id });
if (rootCiphers && rootCiphers.length) {
return rootCiphers[0];
}
}
return null;
}
function removeRootCipher(rootCipher) {
if (rootCipher && rootCipher.id) {
var index = $rootScope.vaultCiphers.indexOf(rootCipher);
if (index > -1) {
$rootScope.vaultCiphers.splice(index, 1);
}
}
}
});

View File

@@ -1,9 +1,8 @@
<section class="content-header">
<div class="btn-group pull-right">
<button type="button" class="btn btn-link dropdown-toggle" data-toggle="dropdown"
ng-disabled="bulkActionLoading">
<i class="fa fa-refresh fa-spin" ng-show="bulkActionLoading"></i>
Bulk Actions <span class="caret"></span>
<button type="button" class="btn btn-link dropdown-toggle" data-toggle="dropdown" ng-disabled="actionLoading">
<i class="fa fa-refresh fa-spin" ng-show="actionLoading"></i>
Actions <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li>
@@ -19,27 +18,36 @@
<li role="separator" class="divider"></li>
<li>
<a href="#" stop-click ng-click="unselectAll()">
<i class="fa fa-fw fa-minus-square-o"></i> Unselect All
<i class="fa fa-fw fa-minus-circle"></i> Unselect All
</a>
</li>
<li>
<a href="#" stop-click ng-click="collapseAll()">
<i class="fa fa-fw fa-minus-square-o"></i> Collapse All
</a>
</li>
<li>
<a href="#" stop-click ng-click="expandAll()">
<i class="fa fa-fw fa-plus-square-o"></i> Expand All
</a>
</li>
</ul>
</div>
<h1>
My Vault
<small>
<span ng-pluralize
count="vaultFolders.length > 0 ? vaultFolders.length - 1 : 0"
when="{'1': '{} folder', 'other': '{} folders'}"></span>,
<small class="visible-md-inline visible-lg-inline">
<span ng-pluralize count="folderCount" when="{'1': '{} folder', 'other': '{} folders'}"></span>,
<span ng-pluralize count="collectionCount" when="{'1': '{} collection', 'other': '{} collections'}"></span>, &amp;
<span ng-pluralize count="ciphers.length" when="{'1': '{} item', 'other': '{} items'}"></span>
</small>
</h1>
</section>
<section class="content">
<div ng-show="loading && !vaultFolders.length">
<div ng-show="loading && !vaultGroupings.length">
<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 || favoriteCiphers.length)">
ng-show="vaultGroupings.length && groupingIdFilter === undefined && (!main.searchVaultText || favoriteCiphers.length)">
<div class="box-header with-border">
<h3 class="box-title">
<i class="fa fa-star"></i>
@@ -74,7 +82,7 @@
<table class="table table-striped table-hover table-vmiddle">
<tbody>
<tr ng-repeat="cipher in favoriteCiphers = (ciphers | filter: { favorite: true } |
filter: cipherFilter | filter: (main.searchVaultText || '')) track by cipher.id">
filter: cipherFilter(null) | 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">
@@ -97,7 +105,7 @@
</a>
</li>
<li ng-show="cipher.organizationId && cipher.edit">
<a href="#" stop-click ng-click="collections(cipher)">
<a href="#" stop-click ng-click="editCollections(cipher)">
<i class="fa fa-fw fa-cubes"></i> Collections
</a>
</li>
@@ -137,59 +145,75 @@
</div>
</div>
</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 || folderCiphers.length)">
<div class="box" ng-class="{'collapsed-box': grouping.collapsed}"
ng-repeat="grouping in filteredVaultGroupings = (vaultGroupings | filter: groupingFilter) track by grouping.id"
ng-show="vaultGroupings.length && (!main.searchVaultText || groupingCiphers.length)"
ng-style="firstCollectionId && grouping.id === firstCollectionId &&
groupingIdFilter !== grouping.id && {'margin-top': '40px'}">
<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="folderCiphers.length" when="{'1': '{} item', 'other': '{} items'}"></small>
<i class="fa" ng-if="grouping.folder"
ng-class="{'fa-folder-open': grouping.id !== null, 'fa-folder-open-o': grouping.id === null}"></i>
<i class="fa fa-cube" ng-if="grouping.collection"></i>
{{grouping.name}}
<small ng-pluralize count="groupingCiphers.length" when="{'1': '{} item', 'other': '{} items'}"></small>
</h3>
<div class="box-tools">
<div class="btn-group">
<button type="button" class="btn btn-box-tool dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-cog"></i> <span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<ul class="dropdown-menu dropdown-menu-right" ng-if="grouping.folder">
<li>
<a href="#" stop-click ng-click="addCipher(folder)">
<a href="#" stop-click ng-click="addCipher(grouping)">
<i class="fa fa-fw fa-plus-circle"></i> New Item
</a>
</li>
<li ng-show="folder.id">
<a href="#" stop-click ng-click="editFolder(folder)">
<li ng-show="grouping.id">
<a href="#" stop-click ng-click="editFolder(grouping)">
<i class="fa fa-fw fa-pencil"></i> Edit Folder
</a>
</li>
<li ng-show="canDeleteFolder(grouping)">
<a href="#" stop-click ng-click="deleteFolder(grouping)" class="text-red">
<i class="fa fa-fw fa-trash"></i> Delete Folder
</a>
</li>
<li>
<a href="#" stop-click ng-click="selectFolder(folder, $event)">
<a href="#" stop-click ng-click="selectFolder(grouping, $event)">
<i class="fa fa-fw fa-check-square-o"></i> Select All
</a>
</li>
<li ng-show="canDeleteFolder(folder)">
<a href="#" stop-click ng-click="deleteFolder(folder)" class="text-red">
<i class="fa fa-fw fa-trash"></i> Delete Folder
</ul>
<ul class="dropdown-menu dropdown-menu-right" ng-if="grouping.collection">
<li>
<a href="#" stop-click ng-click="selectFolder(grouping, $event)">
<i class="fa fa-fw fa-check-square-o"></i> Select All
</a>
</li>
</ul>
</div>
<button type="button" class="btn btn-box-tool" data-widget="collapse" title="Collapse/Expand"
ng-click="collapseExpand(folder)">
<i class="fa" ng-class="{'fa-minus': !folder.collapsed, 'fa-plus': folder.collapsed}"></i>
ng-click="collapseExpand(grouping)">
<i class="fa" ng-class="{'fa-minus': !grouping.collapsed, 'fa-plus': grouping.collapsed}"></i>
</button>
</div>
</div>
<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 class="box-body" ng-class="{'no-padding': groupingCiphers.length}">
<div ng-show="!groupingCiphers.length">
<div ng-if="grouping.folder">
<p>No items in this folder.</p>
<button type="button" ng-click="addCipher(grouping)" class="btn btn-default btn-flat">Add an Item</button>
</div>
<div ng-if="!grouping.folder">
<p>No items in this collection.</p>
</div>
</div>
<div class="table-responsive" ng-show="folderCiphers.length">
<div class="table-responsive" ng-show="groupingCiphers.length">
<table class="table table-striped table-hover table-vmiddle">
<tbody>
<tr ng-repeat="cipher in folderCiphers = (ciphers | filter: { folderId: folder.id } |
filter: cipherFilter | filter: (main.searchVaultText || '')) track by cipher.id">
<tr ng-repeat="cipher in groupingCiphers = (ciphers | filter: cipherFilter(grouping) |
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">
@@ -212,7 +236,7 @@
</a>
</li>
<li ng-show="cipher.organizationId && cipher.edit">
<a href="#" stop-click ng-click="collections(cipher)">
<a href="#" stop-click ng-click="editCollections(cipher)">
<i class="fa fa-fw fa-cubes"></i> Collections
</a>
</li>
@@ -298,19 +322,43 @@
<h3 class="control-sidebar-heading">
<i class="fa fa-folder fa-fw"></i> Folders
</h3>
<div ng-show="loading && !vaultFolders.length">
<div ng-show="loading && !vaultGroupings.length">
<p>Loading...</p>
</div>
<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>
<ul class="control-sidebar-menu" ng-show="!loading && folders.length">
<li ng-repeat="folder in folders = (vaultGroupings | filter: {folder: true}) track by folder.id">
<a href="#" stop-click ng-click="filterGrouping(folder)">
<i class="fa fa-check fa-fw" ng-if="folder.id === groupingIdFilter"></i>
<i class="fa fa-caret-right fa-fw" ng-if="folder.id !== groupingIdFilter"></i>
{{folder.name}}
</a>
</li>
</ul>
</div>
<h3 class="control-sidebar-heading">
<i class="fa fa-cubes fa-fw"></i> Collections
</h3>
<div ng-show="loading && !vaultGroupings.length">
<p>Loading...</p>
</div>
<div ng-show="!loading && !collections.length">
<p>No collections are being shared with you. <i class="fa fa-frown-o"></i></p>
<a ui-sref="backend.user.settingsCreateOrg" class="btn btn-default btn-lint">
Create an Organization
</a>
</div>
<div class="control-sidebar-section">
<ul class="control-sidebar-menu" ng-show="!loading && collections.length">
<li ng-repeat="collection in collections =
(vaultGroupings | filter: {collection: true}) track by collection.id">
<a href="#" stop-click ng-click="filterGrouping(collection)">
<i class="fa fa-check fa-fw" ng-if="collection.id === groupingIdFilter"></i>
<i class="fa fa-caret-right fa-fw" ng-if="collection.id !== groupingIdFilter"></i>
{{collection.name}}
</a>
</li>
</ul>
</div>
</div>
</aside>

View File

@@ -1,108 +0,0 @@
<section class="content-header">
<h1>
Shared
<small>
<span ng-pluralize
count="collections.length > 0 && ciphers.length ? collections.length - 1 : collections.length"
when="{'1': '{} collection', 'other': '{} collections'}"></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 && !ciphers.length">
<h4>Nothing shared <i class="fa fa-frown-o"></i></h4>
<p>
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">
Create an Organization
</a>
</div>
<div class="box" ng-class="{'collapsed-box': collection.collapsed}" ng-repeat="collection in collections |
orderBy: collectionSort track by collection.id"
ng-show="collections.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="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"
ng-click="collapseExpand(collection)">
<i class="fa" ng-class="{'fa-minus': !collection.collapsed, 'fa-plus': collection.collapsed}"></i>
</button>
</div>
</div>
<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 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="!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="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">
<i class="fa fa-cog"></i> <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li>
<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(cipher)">
<i class="fa fa-fw fa-paperclip"></i> Attachments
</a>
</li>
<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="cipher.meta.password">
<a href="#" stop-click ngclipboard ngclipboard-error="clipboardError(e)"
data-clipboard-text="{{cipher.meta.password}}">
<i class="fa fa-fw fa-clipboard"></i> Copy Password
</a>
</li>
<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>
</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="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">{{cipher.subTitle}}</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>

View File

@@ -80,11 +80,6 @@
</div>
<div class="col-sm-6">
<ul class="fa-ul">
<li>
<a href="https://www.amazon.com/dp/B06XMYGPMV" target="_blank">
<i class="fa fa-amazon fa-lg fa-fw fa-li"></i> Amazon
</a>
</li>
<li>
<a href="#" stop-click>
<i class="fa fa-windows fa-lg fa-fw fa-li"></i> Windows

View File

@@ -95,6 +95,11 @@
</li>
</ul>
</li>
<li ng-class="{active: $state.is('backend.org.events')}" ng-if="orgProfile.useEvents">
<a ui-sref="backend.org.events({orgId: params.orgId})">
<i class="fa fa-file-text-o fa-fw"></i> <span>Event Logs</span>
</a>
</li>
<li ng-class="{active: $state.is('backend.org.billing')}" ng-if="isOrgOwner(orgProfile)">
<a ui-sref="backend.org.billing({orgId: params.orgId})">
<i class="fa fa-credit-card fa-fw"></i> Billing &amp; Licensing

View File

@@ -36,7 +36,7 @@
<label for="search" class="sr-only">Search</label>
<div class="form-group has-feedback">
<input type="text" id="search" class="form-control" placeholder="Search my vault..."
ng-focus="searchVault()" ng-model="main.searchVaultText" autofocus />
ng-focus="searchVault()" ng-model="main.searchVaultText" />
<span class="fa fa-search form-control-feedback" aria-hidden="true"></span>
</div>
</form>
@@ -57,11 +57,6 @@
</li>
</ul>
</li>
<li class="treeview" ng-class="{active: $state.is('backend.user.shared')}">
<a ui-sref="backend.user.shared">
<i class="fa fa-share-alt fa-fw"></i> <span>Shared</span>
</a>
</li>
<li class="treeview" ng-class="{active: $state.is('backend.user.tools') ||
$state.is('backend.user.reportsBreach')}">
<a ui-sref="backend.user.tools"><i class="fa fa-wrench fa-fw"></i> <span>Tools</span></a>

View File

@@ -2,55 +2,6 @@
<html ng-app="bit" ng-csp>
<head>
<meta charset="utf-8" />
<!-- @if !selfHosted -->
<meta http-equiv="Content-Security-Policy" content="
default-src
'self';
script-src
'self'
'sha256-ryoU+5+IUZTuUyTElqkrQGBJXr1brEv6r2CA62WUw8w='
https://www.google-analytics.com
https://js.stripe.com
https://js.braintreegateway.com
https://www.paypalobjects.com
https://maxcdn.bootstrapcdn.com
https://ajax.googleapis.com;
style-src
'self'
'unsafe-inline'
https://maxcdn.bootstrapcdn.com
https://assets.braintreegateway.com
https://*.paypal.com
https://fonts.googleapis.com;
img-src
'self'
data:
https://icons.bitwarden.com
https://*.paypal.com
https://www.paypalobjects.com
https://q.stripe.com
https://haveibeenpwned.com
https://chart.googleapis.com
https://www.google-analytics.com;
font-src
'self'
https://maxcdn.bootstrapcdn.com
https://fonts.gstatic.com;
child-src
'self'
https://js.stripe.com
https://assets.braintreegateway.com
https://*.paypal.com
https://*.duosecurity.com;
frame-src
'self'
https://js.stripe.com
https://assets.braintreegateway.com
https://*.paypal.com
https://*.duosecurity.com;
connect-src
*;">
<!-- @endif -->
<!-- @if selfHosted !>
<meta http-equiv="Content-Security-Policy" content="
default-src
@@ -195,6 +146,8 @@
<script src="app/services/validationService.js"></script>
<script src="app/services/passwordService.js"></script>
<script src="app/services/importService.js"></script>
<script src="app/services/eventService.js"></script>
<script src="app/services/utilsService.js"></script>
<script src="app/global/globalModule.js"></script>
<script src="app/global/mainController.js"></script>
@@ -223,7 +176,6 @@
<script src="app/vault/vaultEditFolderController.js"></script>
<script src="app/vault/vaultAddFolderController.js"></script>
<script src="app/vault/vaultShareCipherController.js"></script>
<script src="app/vault/vaultSharedController.js"></script>
<script src="app/vault/vaultCipherCollectionsController.js"></script>
<script src="app/vault/vaultMoveCiphersController.js"></script>
<script src="app/vault/vaultAttachmentsController.js"></script>
@@ -234,6 +186,7 @@
<script src="app/organization/organizationPeopleInviteController.js"></script>
<script src="app/organization/organizationPeopleEditController.js"></script>
<script src="app/organization/organizationPeopleGroupsController.js"></script>
<script src="app/organization/organizationPeopleEventsController.js"></script>
<script src="app/organization/organizationCollectionsController.js"></script>
<script src="app/organization/organizationCollectionsAddController.js"></script>
<script src="app/organization/organizationCollectionsEditController.js"></script>
@@ -253,11 +206,13 @@
<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/organizationVaultCipherEventsController.js"></script>
<script src="app/organization/organizationVaultAttachmentsController.js"></script>
<script src="app/organization/organizationGroupsController.js"></script>
<script src="app/organization/organizationGroupsAddController.js"></script>
<script src="app/organization/organizationGroupsEditController.js"></script>
<script src="app/organization/organizationGroupsUsersController.js"></script>
<script src="app/organization/organizationEventsController.js"></script>
<script src="app/settings/settingsModule.js"></script>
<script src="app/settings/settingsController.js"></script>

View File

@@ -524,6 +524,10 @@ form .btn .loading-icon {
position: absolute;
left: 10px;
top: 5px;
input {
display: inline;
}
}
.box-body p:last-child {