mirror of
https://github.com/bitwarden/web
synced 2025-12-06 00:03:28 +00:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9b5426f6f | ||
|
|
bf885c184f | ||
|
|
0d2bf4f7a1 | ||
|
|
01ffc68fc2 | ||
|
|
16892239fb | ||
|
|
d5765d8814 | ||
|
|
8d6a96074d | ||
|
|
f54884eb79 | ||
|
|
828149b2d6 | ||
|
|
501c4fc263 | ||
|
|
1d0b45e17d | ||
|
|
a0f7ed68fb | ||
|
|
7bd0c17188 | ||
|
|
1ea9d28523 | ||
|
|
8a3fb92bbe | ||
|
|
de3a9b9903 | ||
|
|
9834f3d2aa | ||
|
|
ac079b9d88 | ||
|
|
9e96906f32 | ||
|
|
90c079e743 | ||
|
|
4ecf307285 | ||
|
|
6cf4c453d9 | ||
|
|
d2899d14c7 | ||
|
|
f3b438d514 | ||
|
|
2997f694f8 | ||
|
|
b78ab4db27 | ||
|
|
37dddea515 | ||
|
|
e307d1e87d | ||
|
|
62e1dbb642 | ||
|
|
b8a425f530 | ||
|
|
cafb6fa694 | ||
|
|
0482ddea2c | ||
|
|
b411176c8d | ||
|
|
2f13449cb6 | ||
|
|
b0c1b7b683 | ||
|
|
7e8978c7fc | ||
|
|
d58b422bd0 | ||
|
|
3563601382 | ||
|
|
d42e6ca3fd | ||
|
|
7f0d8c99e3 | ||
|
|
48a67dc2b3 | ||
|
|
8d0b42492d |
@@ -1,4 +1,4 @@
|
||||
[](https://ci.appveyor.com/project/bitwarden/web) [](https://gitter.im/bitwarden/Lobby)
|
||||
[](https://ci.appveyor.com/project/bitwarden/web) [](https://hub.docker.com/u/bitwarden/) [](https://gitter.im/bitwarden/Lobby)
|
||||
|
||||
# bitwarden Web
|
||||
|
||||
|
||||
2
dist/.publish
vendored
2
dist/.publish
vendored
Submodule dist/.publish updated: 9d2a53eca0...1990d717ea
@@ -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'
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
34
package.json
34
package.json
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
100
src/app/organization/organizationEventsController.js
Normal file
100
src/app/organization/organizationEventsController.js
Normal 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;
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
75
src/app/organization/organizationPeopleEventsController.js
Normal file
75
src/app/organization/organizationPeopleEventsController.js
Normal 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');
|
||||
};
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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.');
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
104
src/app/organization/organizationVaultCipherEventsController.js
Normal file
104
src/app/organization/organizationVaultCipherEventsController.js
Normal 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');
|
||||
};
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<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>
|
||||
|
||||
67
src/app/organization/views/organizationEvents.html
Normal file
67
src/app/organization/views/organizationEvents.html
Normal 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">
|
||||
|
||||
<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>
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<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>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<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
|
||||
|
||||
56
src/app/organization/views/organizationPeopleEvents.html
Normal file
56
src/app/organization/views/organizationPeopleEvents.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title"><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>
|
||||
@@ -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">
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title"><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>
|
||||
@@ -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: {}
|
||||
|
||||
@@ -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;
|
||||
|
||||
269
src/app/services/eventService.js
Normal file
269
src/app/services/eventService.js
Normal 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;
|
||||
});
|
||||
@@ -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 = [];
|
||||
|
||||
47
src/app/services/utilsService.js
Normal file
47
src/app/services/utilsService.js
Normal 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;
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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"});
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
<label>
|
||||
<input type="radio" ng-model="model.plan" name="PlanType" value="families">
|
||||
Families
|
||||
<span>For personal users such as families & friends.</span>
|
||||
<span>For personal use, to share with family & friends.</span>
|
||||
<span>- Add and share with up to 5 users</span>
|
||||
<span>- Create unlimited collections</span>
|
||||
<span>- 1 GB encrypted file storage</span>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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.');
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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>, &
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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 & Licensing
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -524,6 +524,10 @@ form .btn .loading-icon {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 5px;
|
||||
|
||||
input {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.box-body p:last-child {
|
||||
|
||||
Reference in New Issue
Block a user