encrypt, upload, and view attachments
This commit is contained in:
parent
9c7b47c277
commit
7b4cf53ec4
|
@ -40,7 +40,13 @@
|
||||||
del: { url: _apiUri + '/ciphers/:id/delete', method: 'POST', params: { id: '@id' } },
|
del: { url: _apiUri + '/ciphers/:id/delete', method: 'POST', params: { id: '@id' } },
|
||||||
delAdmin: { url: _apiUri + '/ciphers/:id/delete-admin', method: 'POST', params: { id: '@id' } },
|
delAdmin: { url: _apiUri + '/ciphers/:id/delete-admin', method: 'POST', params: { id: '@id' } },
|
||||||
delMany: { url: _apiUri + '/ciphers/delete', method: 'POST' },
|
delMany: { url: _apiUri + '/ciphers/delete', method: 'POST' },
|
||||||
moveMany: { url: _apiUri + '/ciphers/move', method: 'POST' }
|
moveMany: { url: _apiUri + '/ciphers/move', method: 'POST' },
|
||||||
|
postAttachment: {
|
||||||
|
url: _apiUri + '/ciphers/:id/attachment',
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': undefined },
|
||||||
|
params: { id: '@id' }
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
_service.organizations = $resource(_apiUri + '/organizations/:id', {}, {
|
_service.organizations = $resource(_apiUri + '/organizations/:id', {}, {
|
||||||
|
|
|
@ -35,9 +35,19 @@ angular
|
||||||
uri: encryptedLogin.Uri && encryptedLogin.Uri !== '' ? cryptoService.decrypt(encryptedLogin.Uri, key) : null,
|
uri: encryptedLogin.Uri && encryptedLogin.Uri !== '' ? cryptoService.decrypt(encryptedLogin.Uri, key) : null,
|
||||||
username: encryptedLogin.Username && encryptedLogin.Username !== '' ? cryptoService.decrypt(encryptedLogin.Username, key) : null,
|
username: encryptedLogin.Username && encryptedLogin.Username !== '' ? cryptoService.decrypt(encryptedLogin.Username, key) : null,
|
||||||
password: encryptedLogin.Password && encryptedLogin.Password !== '' ? cryptoService.decrypt(encryptedLogin.Password, key) : null,
|
password: encryptedLogin.Password && encryptedLogin.Password !== '' ? cryptoService.decrypt(encryptedLogin.Password, key) : null,
|
||||||
notes: encryptedLogin.Notes && encryptedLogin.Notes !== '' ? cryptoService.decrypt(encryptedLogin.Notes, key) : null
|
notes: encryptedLogin.Notes && encryptedLogin.Notes !== '' ? cryptoService.decrypt(encryptedLogin.Notes, key) : null,
|
||||||
|
attachments: null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!encryptedLogin.Attachments) {
|
||||||
|
return login;
|
||||||
|
}
|
||||||
|
|
||||||
|
login.attachments = [];
|
||||||
|
for (var i = 0; i < encryptedLogin.Attachments.length; i++) {
|
||||||
|
login.attachments.push(_service.decryptAttachment(key, encryptedLogin.Attachments[i]));
|
||||||
|
}
|
||||||
|
|
||||||
return login;
|
return login;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -58,12 +68,23 @@ angular
|
||||||
edit: encryptedCipher.Edit,
|
edit: encryptedCipher.Edit,
|
||||||
name: _service.decryptProperty(encryptedCipher.Data.Name, key, false),
|
name: _service.decryptProperty(encryptedCipher.Data.Name, key, false),
|
||||||
username: _service.decryptProperty(encryptedCipher.Data.Username, key, true),
|
username: _service.decryptProperty(encryptedCipher.Data.Username, key, true),
|
||||||
password: _service.decryptProperty(encryptedCipher.Data.Password, key, true)
|
password: _service.decryptProperty(encryptedCipher.Data.Password, key, true),
|
||||||
|
hasAttachments: !!encryptedCipher.Attachments
|
||||||
};
|
};
|
||||||
|
|
||||||
return login;
|
return login;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_service.decryptAttachment = function (key, encryptedAttachment) {
|
||||||
|
if (!encryptedAttachment) throw "encryptedAttachment is undefined or null";
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: encryptedAttachment.Id,
|
||||||
|
fileName: cryptoService.decrypt(encryptedAttachment.FileName, key),
|
||||||
|
size: encryptedAttachment.SizeName
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
_service.decryptFolders = function (encryptedFolders) {
|
_service.decryptFolders = function (encryptedFolders) {
|
||||||
if (!encryptedFolders) throw "encryptedFolders is undefined or null";
|
if (!encryptedFolders) throw "encryptedFolders is undefined or null";
|
||||||
|
|
||||||
|
|
|
@ -295,6 +295,41 @@ angular
|
||||||
};
|
};
|
||||||
|
|
||||||
_service.encrypt = function (plainValue, key, plainValueEncoding) {
|
_service.encrypt = function (plainValue, key, plainValueEncoding) {
|
||||||
|
var encValue = aesEncrypt(plainValue, key, plainValueEncoding);
|
||||||
|
|
||||||
|
var iv = forge.util.encode64(encValue.iv);
|
||||||
|
var ct = forge.util.encode64(encValue.ct);
|
||||||
|
var cipherString = iv + '|' + ct;
|
||||||
|
|
||||||
|
if (encValue.mac) {
|
||||||
|
var mac = forge.util.encode64(encValue.mac)
|
||||||
|
cipherString = cipherString + '|' + mac;
|
||||||
|
}
|
||||||
|
|
||||||
|
return encValue.key.encType + '.' + cipherString;
|
||||||
|
};
|
||||||
|
|
||||||
|
_service.encryptToBytes = function (plainValue, key) {
|
||||||
|
return aesEncryptWC(plainValue, key).then(function (encValue) {
|
||||||
|
var macLen = 0;
|
||||||
|
if (encValue.mac) {
|
||||||
|
macLen = encValue.mac.length
|
||||||
|
}
|
||||||
|
|
||||||
|
var encBytes = new Uint8Array(1 + encValue.iv.length + macLen + encValue.ct.length);
|
||||||
|
|
||||||
|
encBytes.set([encValue.key.encType]);
|
||||||
|
encBytes.set(encValue.iv, 1);
|
||||||
|
if (encValue.mac) {
|
||||||
|
encBytes.set(encValue.mac, 1 + encValue.iv.length);
|
||||||
|
}
|
||||||
|
encBytes.set(encValue.ct, 1 + encValue.iv.length + macLen);
|
||||||
|
|
||||||
|
return encBytes.buffer;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function aesEncrypt(plainValue, key, plainValueEncoding) {
|
||||||
key = key || _service.getEncKey() || _service.getKey();
|
key = key || _service.getEncKey() || _service.getKey();
|
||||||
|
|
||||||
if (!key) {
|
if (!key) {
|
||||||
|
@ -309,18 +344,55 @@ angular
|
||||||
cipher.update(buffer);
|
cipher.update(buffer);
|
||||||
cipher.finish();
|
cipher.finish();
|
||||||
|
|
||||||
var iv = forge.util.encode64(ivBytes);
|
|
||||||
var ctBytes = cipher.output.getBytes();
|
var ctBytes = cipher.output.getBytes();
|
||||||
var ct = forge.util.encode64(ctBytes);
|
|
||||||
var cipherString = iv + '|' + ct;
|
|
||||||
|
|
||||||
|
var macBytes = null;
|
||||||
if (key.macKey) {
|
if (key.macKey) {
|
||||||
var mac = computeMac(ivBytes + ctBytes, key.macKey, true);
|
macBytes = computeMac(ivBytes + ctBytes, key.macKey, false);
|
||||||
cipherString = cipherString + '|' + mac;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return key.encType + '.' + cipherString;
|
return {
|
||||||
};
|
iv: ivBytes,
|
||||||
|
ct: ctBytes,
|
||||||
|
mac: macBytes,
|
||||||
|
key: key,
|
||||||
|
plainValueEncoding: plainValueEncoding
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function aesEncryptWC(plainValue, key) {
|
||||||
|
key = key || _service.getEncKey() || _service.getKey();
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
throw 'Encryption key unavailable.';
|
||||||
|
}
|
||||||
|
|
||||||
|
var obj = {
|
||||||
|
iv: new Uint8Array(16),
|
||||||
|
ct: null,
|
||||||
|
mac: null,
|
||||||
|
key: key
|
||||||
|
};
|
||||||
|
|
||||||
|
var keyBuf = key.getBuffers();
|
||||||
|
window.crypto.getRandomValues(obj.iv);
|
||||||
|
|
||||||
|
return window.crypto.subtle.importKey('raw', keyBuf.encKey, { name: 'AES-CBC' }, false, ['encrypt'])
|
||||||
|
.then(function (encKey) {
|
||||||
|
return window.crypto.subtle.encrypt({ name: 'AES-CBC', iv: obj.iv }, encKey, plainValue);
|
||||||
|
}).then(function (encValue) {
|
||||||
|
obj.ct = new Uint8Array(encValue);
|
||||||
|
if (!keyBuf.macKey) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return computeMacWC(encValue, keyBuf.macKey);
|
||||||
|
}).then(function (mac) {
|
||||||
|
if (mac) {
|
||||||
|
obj.mac = new Uint8Array(mac);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_service.rsaEncrypt = function (plainValue, publicKey, key) {
|
_service.rsaEncrypt = function (plainValue, publicKey, key) {
|
||||||
publicKey = publicKey || _service.getPublicKey();
|
publicKey = publicKey || _service.getPublicKey();
|
||||||
|
@ -513,6 +585,13 @@ angular
|
||||||
return b64Output ? forge.util.encode64(mac.getBytes()) : mac.getBytes();
|
return b64Output ? forge.util.encode64(mac.getBytes()) : mac.getBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function computeMacWC(data, macKey) {
|
||||||
|
return window.crypto.subtle.importKey('raw', macKey, { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign'])
|
||||||
|
.then(function (key) {
|
||||||
|
return window.crypto.subtle.sign({ name: 'HMAC' }, key, data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Safely compare two MACs in a way that protects against timing attacks (Double HMAC Verification).
|
// Safely compare two MACs in a way that protects against timing attacks (Double HMAC Verification).
|
||||||
// ref: https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/
|
// ref: https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/
|
||||||
function macsEqual(macKey, mac1, mac2) {
|
function macsEqual(macKey, mac1, mac2) {
|
||||||
|
@ -577,5 +656,33 @@ angular
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SymmetricCryptoKey.prototype.getBuffers = function () {
|
||||||
|
var key = b64ToArray(this.keyB64);
|
||||||
|
|
||||||
|
var keys = {
|
||||||
|
key: key.buffer
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.macKey) {
|
||||||
|
keys.encKey = key.slice(0, key.length / 2).buffer;
|
||||||
|
keys.macKey = key.slice(key.length / 2).buffer;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
keys.encKey = key.buffer;
|
||||||
|
keys.macKey = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys;
|
||||||
|
};
|
||||||
|
|
||||||
|
function b64ToArray(b64Str) {
|
||||||
|
var binaryString = window.atob(b64Str);
|
||||||
|
var arr = new Uint8Array(binaryString.length);
|
||||||
|
for (var i = 0; i < binaryString.length; i++) {
|
||||||
|
arr[i] = binaryString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
return _service;
|
return _service;
|
||||||
});
|
});
|
|
@ -0,0 +1,53 @@
|
||||||
|
angular
|
||||||
|
.module('bit.vault')
|
||||||
|
|
||||||
|
.controller('vaultAttachmentsController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService,
|
||||||
|
loginId, $analytics, validationService) {
|
||||||
|
$analytics.eventTrack('vaultAttachmentsController', { category: 'Modal' });
|
||||||
|
$scope.login = {};
|
||||||
|
$scope.readOnly = false;
|
||||||
|
$scope.loading = true;
|
||||||
|
|
||||||
|
apiService.logins.get({ id: loginId }, function (login) {
|
||||||
|
$scope.login = cipherService.decryptLogin(login);
|
||||||
|
$scope.readOnly = !login.Edit;
|
||||||
|
$scope.loading = false;
|
||||||
|
}, function () {
|
||||||
|
$scope.loading = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.save = function (form) {
|
||||||
|
var files = document.getElementById('file').files;
|
||||||
|
if (!files || !files.length) {
|
||||||
|
validationService.addError(form, 'file', 'Select a file.', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var file = files[0];
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
reader.onload = function (evt) {
|
||||||
|
var key = null;
|
||||||
|
var encFilename = cryptoService.encrypt(file.name, key);
|
||||||
|
cryptoService.encryptToBytes(evt.target.result, key).then(function (encData) {
|
||||||
|
var fd = new FormData();
|
||||||
|
var blob = new Blob([encData], { type: 'application/octet-stream' });
|
||||||
|
fd.append('data', blob, encFilename);
|
||||||
|
return apiService.ciphers.postAttachment({ id: loginId }, fd).$promise;
|
||||||
|
}).then(function (response) {
|
||||||
|
$analytics.eventTrack('Added Attachment');
|
||||||
|
$uibModalInstance.close({
|
||||||
|
action: 'attach',
|
||||||
|
data: $scope.login
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
reader.onerror = function (evt) {
|
||||||
|
validationService.addError(form, 'file', 'Error reading file.', true);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.close = function () {
|
||||||
|
$uibModalInstance.dismiss('cancel');
|
||||||
|
};
|
||||||
|
});
|
|
@ -199,6 +199,21 @@
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.attachments = function (login) {
|
||||||
|
var addModel = $uibModal.open({
|
||||||
|
animation: true,
|
||||||
|
templateUrl: 'app/vault/views/vaultAttachments.html',
|
||||||
|
controller: 'vaultAttachmentsController',
|
||||||
|
resolve: {
|
||||||
|
loginId: function () { return login.id; }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
addModel.result.then(function (data) {
|
||||||
|
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
$scope.editFolder = function (folder) {
|
$scope.editFolder = function (folder) {
|
||||||
var editModel = $uibModal.open({
|
var editModel = $uibModal.open({
|
||||||
animation: true,
|
animation: true,
|
||||||
|
|
|
@ -86,6 +86,11 @@
|
||||||
<i class="fa fa-fw fa-pencil"></i> Edit
|
<i class="fa fa-fw fa-pencil"></i> Edit
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li ng-show="login.edit">
|
||||||
|
<a href="#" stop-click ng-click="attachments(login)">
|
||||||
|
<i class="fa fa-fw fa-paperclip"></i> Attachments
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li ng-show="!login.organizationId">
|
<li ng-show="!login.organizationId">
|
||||||
<a href="#" stop-click ng-click="share(login)">
|
<a href="#" stop-click ng-click="share(login)">
|
||||||
<i class="fa fa-fw fa-share-alt"></i> Share
|
<i class="fa fa-fw fa-share-alt"></i> Share
|
||||||
|
@ -115,7 +120,9 @@
|
||||||
</td>
|
</td>
|
||||||
<td ng-click="select($event)">
|
<td ng-click="select($event)">
|
||||||
<a href="#" stop-click ng-click="editLogin(login)" stop-prop>{{login.name}}</a>
|
<a href="#" stop-click ng-click="editLogin(login)" stop-prop>{{login.name}}</a>
|
||||||
<i class="fa fa-share-alt text-muted" title="Shared" ng-show="login.organizationId"
|
<i class="fa fa-share-alt text-muted" title="Shared" ng-if="login.organizationId"
|
||||||
|
stop-prop></i>
|
||||||
|
<i class="fa fa-paperclip text-muted" title="Attachments" ng-if="login.hasAttachments"
|
||||||
stop-prop></i><br />
|
stop-prop></i><br />
|
||||||
<span class="text-sm text-muted" stop-prop>{{login.username}}</span>
|
<span class="text-sm text-muted" stop-prop>{{login.username}}</span>
|
||||||
</td>
|
</td>
|
||||||
|
@ -189,6 +196,11 @@
|
||||||
<i class="fa fa-fw fa-pencil"></i> Edit
|
<i class="fa fa-fw fa-pencil"></i> Edit
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li ng-show="login.edit">
|
||||||
|
<a href="#" stop-click ng-click="attachments(login)">
|
||||||
|
<i class="fa fa-fw fa-paperclip"></i> Attachments
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li ng-show="!login.organizationId">
|
<li ng-show="!login.organizationId">
|
||||||
<a href="#" stop-click ng-click="share(login)">
|
<a href="#" stop-click ng-click="share(login)">
|
||||||
<i class="fa fa-fw fa-share-alt"></i> Share
|
<i class="fa fa-fw fa-share-alt"></i> Share
|
||||||
|
@ -219,7 +231,10 @@
|
||||||
<td ng-click="select($event)">
|
<td ng-click="select($event)">
|
||||||
<a href="#" stop-click ng-click="editLogin(login)" stop-prop>{{login.name}}</a>
|
<a href="#" stop-click ng-click="editLogin(login)" stop-prop>{{login.name}}</a>
|
||||||
<i class="fa fa-star text-muted" title="Favorite" ng-show="login.favorite" stop-prop></i>
|
<i class="fa fa-star text-muted" title="Favorite" ng-show="login.favorite" stop-prop></i>
|
||||||
<i class="fa fa-share-alt text-muted" title="Shared" ng-show="login.organizationId" stop-prop></i>
|
<i class="fa fa-share-alt text-muted" title="Shared" ng-show="login.organizationId"
|
||||||
|
stop-prop></i>
|
||||||
|
<i class="fa fa-paperclip text-muted" title="Attachments" ng-if="login.hasAttachments"
|
||||||
|
stop-prop></i>
|
||||||
<br />
|
<br />
|
||||||
<span class="text-sm text-muted" stop-prop>{{login.username}}</span>
|
<span class="text-sm text-muted" stop-prop>{{login.username}}</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
<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-paperclip"></i> Secure Attachments <small>{{login.name}}</small>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<form name="form" ng-submit="form.$valid && save(form)" api-form="savePromise">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div ng-if="loading">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
<div ng-if="!loading && !login.attachments.length">
|
||||||
|
There are no attachments for this login.
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive" ng-if="login.attachments.length" style="margin: 0;">
|
||||||
|
<table class="table table-striped table-hover table-vmiddle" style="margin: 0;">
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat="attachment in login.attachments | orderBy: ['fileName']">
|
||||||
|
<td style="width: 70px;">
|
||||||
|
<div class="btn-group" data-append-to=".modal">
|
||||||
|
<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="download(attachment)">
|
||||||
|
<i class="fa fa-fw fa-download"></i> Download
|
||||||
|
</a>
|
||||||
|
<a href="#" stop-click ng-click="remove(attachment)" class="text-red"
|
||||||
|
ng-show="!readOnly">
|
||||||
|
<i class="fa fa-fw fa-trash"></i> Delete
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="#" stop-click ng-click="download(attachment)">{{attachment.fileName}}</a>
|
||||||
|
</td>
|
||||||
|
<td style="width: 80px; min-width: 80px;">
|
||||||
|
{{attachment.size}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<h4>Add New Attachment</h4>
|
||||||
|
<div class="callout callout-danger validation-errors" ng-show="form.$errors">
|
||||||
|
<h4>Errors have occurred</h4>
|
||||||
|
<ul>
|
||||||
|
<li ng-repeat="e in form.$errors">{{e}}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" show-error>
|
||||||
|
<label for="file" class="sr-only">File</label>
|
||||||
|
<input type="file" id="file" name="file" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="form.$loading">
|
||||||
|
<i class="fa fa-refresh fa-spin loading-icon" ng-show="form.$loading"></i>Save
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
|
@ -163,6 +163,7 @@
|
||||||
<script src="app/vault/vaultSharedController.js"></script>
|
<script src="app/vault/vaultSharedController.js"></script>
|
||||||
<script src="app/vault/vaultLoginCollectionsController.js"></script>
|
<script src="app/vault/vaultLoginCollectionsController.js"></script>
|
||||||
<script src="app/vault/vaultMoveLoginsController.js"></script>
|
<script src="app/vault/vaultMoveLoginsController.js"></script>
|
||||||
|
<script src="app/vault/vaultAttachmentsController.js"></script>
|
||||||
|
|
||||||
<script src="app/organization/organizationModule.js"></script>
|
<script src="app/organization/organizationModule.js"></script>
|
||||||
<script src="app/organization/organizationDashboardController.js"></script>
|
<script src="app/organization/organizationDashboardController.js"></script>
|
||||||
|
|
Loading…
Reference in New Issue