Merge branch 'import-export-improvements'

Signed-off-by: binsky <timo@binsky.org>
This commit is contained in:
binsky 2023-07-16 23:29:40 +02:00
commit 0e09b4f579
No known key found for this signature in database
GPG key ID: B438F7FA2E3AC98F
11 changed files with 234 additions and 151 deletions

View file

@ -1276,10 +1276,10 @@ h3 {
.setting-group input[type="text"], .setting-group input[type="password"], .setting-group textarea { .setting-group input[type="text"], .setting-group input[type="password"], .setting-group textarea {
width: 100%; } width: 100%; }
.setting-group.margin-bottom-25 { .setting-group.margin-bottom-25, .margin-bottom-25 {
margin-bottom: 25px; } margin-bottom: 25px; }
.setting-group.margin-bottom-10 { .setting-group.margin-bottom-10, .margin-bottom-10 {
margin-bottom: 10px; } margin-bottom: 10px; }
.display-grid { .display-grid {
@ -1295,7 +1295,7 @@ h3 {
label[for=confirmVaultPWChange] { label[for=confirmVaultPWChange] {
margin-bottom: 10px; } margin-bottom: 10px; }
label[for=confirmVaultDelete] { label[for=confirmVaultDelete], label[for=skipFirstRow] {
margin-top: 10px; margin-top: 10px;
margin-bottom: 10px; } margin-bottom: 10px; }

File diff suppressed because one or more lines are too long

View file

@ -32,8 +32,8 @@
* Controller of the passmanApp * Controller of the passmanApp
*/ */
angular.module('passmanApp') angular.module('passmanApp')
.controller('GenericCsvImportCtrl', ['$scope', 'CredentialService', 'FileService', 'EncryptService', '$translate', '$q', .controller('GenericCsvImportCtrl', ['$scope', '$rootScope', 'CredentialService', 'FileService', 'EncryptService', '$translate', '$q',
function ($scope, CredentialService, FileService, EncryptService, $translate, $q) { function ($scope, $rootScope, CredentialService, FileService, EncryptService, $translate, $q) {
$scope.hello = 'world'; $scope.hello = 'world';
$scope.credentialProperties = [ $scope.credentialProperties = [
@ -45,17 +45,27 @@
{ {
label: 'Username', label: 'Username',
prop: 'username', prop: 'username',
matching: ['username', 'user', 'login', 'login name'] matching: ['username', 'user', 'login', 'login name', 'login_username']
}, },
{ {
label: 'Password', label: 'Password',
prop: 'password', prop: 'password',
matching: ['password', 'pass', 'pw'] matching: ['password', 'pass', 'pw', 'login_password']
}, },
{ {
label: 'TOTP Secret', label: 'TOTP Secret or Object',
prop: 'otp', prop: 'otp',
matching: ['totp'] matching: ['otp', 'otp_object', 'totp', 'login_totp']
},
{
label: 'Email',
prop: 'email',
matching: ['email', 'mail']
},
{
label: 'Notes',
prop: 'description',
matching: ['notes', 'description', 'comments']
}, },
{ {
label: 'Custom field', label: 'Custom field',
@ -71,24 +81,45 @@
prop: 'files', prop: 'files',
matching: ['files'] matching: ['files']
}, },
{
label: 'Notes',
prop: 'description',
matching: ['notes', 'description', 'comments']
},
{
label: 'Email',
prop: 'email',
matching: ['email', 'mail']
},
{ {
label: 'URL', label: 'URL',
prop: 'url', prop: 'url',
matching: ['website', 'url', 'fulladdress', 'site', 'web site'] matching: ['website', 'url', 'fulladdress', 'site', 'web site', 'login_uri']
}, },
{ {
label: 'Tags', label: 'Tags',
prop: 'tags' prop: 'tags',
matching: ['tags', 'folder']
},
{
label: 'Created',
prop: 'created',
matching: ['created', 'creation']
},
{
label: 'Changed',
prop: 'changed',
matching: ['changed', 'edited']
},
{
label: 'Expire time',
prop: 'expire_time',
matching: ['expire_time', 'expire', 'expires', 'expires_at']
},
{
label: 'Delete time',
prop: 'delete_time',
matching: ['delete_time', 'delete', 'deleted_at']
},
{
label: 'Icon',
prop: 'icon',
matching: ['icon', 'favicon']
},
{
label: 'Compromised',
prop: 'compromised',
matching: ['compromised']
}, },
{ {
label: 'Ignored', label: 'Ignored',
@ -99,14 +130,23 @@
return {text: t}; return {text: t};
}; };
var rowToCredential = async function (row) { var rowToCredential = async function (row) {
var _credential = PassmanImporter.newCredential(); let _credential = PassmanImporter.newCredential();
for(var k = 0; k < $scope.import_fields.length; k++){ for(let k = 0; k < $scope.import_fields.length; k++){
var field = $scope.import_fields[k]; const field = $scope.import_fields[k];
if(field){ if(field){
if(field === 'otp'){ if(field === 'otp'){
_credential.otp.secret = row[k]; if (typeof row[k] === 'object' || row[k].includes('{"')) {
const otpobj = JSON.parse(row[k]);
if (typeof otpobj === 'object' && otpobj.secret !== undefined && otpobj.algorithm !== undefined && otpobj.period !== undefined && otpobj.digits !== undefined) {
_credential.otp = otpobj;
} else if (otpobj.secret !== undefined) {
_credential.otp.secret = otpobj.secret;
}
} else if (row[k] !== '{}') {
_credential.otp.secret = row[k];
}
} else if(field === 'custom_field'){ } else if(field === 'custom_field'){
var key = ($scope.matched) ? $scope.parsed_csv[0][k] : 'Custom field '+ k; const key = ($scope.matched) ? $scope.parsed_csv[0][k] : 'Custom field '+ k;
_credential.custom_fields.push({ _credential.custom_fields.push({
'label': key, 'label': key,
'value': row[k], 'value': row[k],
@ -116,46 +156,44 @@
if (row[k] !== undefined && (typeof row[k] === 'string' || row[k] instanceof String) && row[k].length > 1){ if (row[k] !== undefined && (typeof row[k] === 'string' || row[k] instanceof String) && row[k].length > 1){
try { try {
row[k] = JSON.parse(row[k]); row[k] = JSON.parse(row[k]);
for(let i = 0; k < row[k].length; i++){
_credential.custom_fields.push({
'label': row[k][i].label,
'secret': row[k][i].secret,
'field_type': row[k][i].field_type,
});
}
} catch (e) { } catch (e) {
// ignore row[k], it contains no valid json data // ignore row[k], it contains no valid json data
// console.error(e); console.error(e);
continue;
} }
} else { }
for(let j = 0; j < row[k].length; j++){ for(let j = 0; j < row[k].length; j++){
if (row[k][j].field_type === 'file'){ if (row[k][j].field_type === 'file'){
var _file = { const _file = {
filename: row[k][j].value.filename, filename: row[k][j].value.filename,
size: row[k][j].value.size, size: row[k][j].value.size,
mimetype: row[k][j].value.mimetype, mimetype: row[k][j].value.mimetype,
data: row[k][j].value.file_data data: row[k][j].value.file_data ? row[k][j].value.file_data : row[k][j].value.data
}; };
if (_file.data === undefined) {
row[k][j].value = await FileService.uploadFile(_file).then(FileService.getEmptyFileWithDecryptedFilename); console.error('Unable to parse file data from ', row[k][j]);
$scope.log.push('Unable to parse file data from file ' + _file.filename);
continue;
} }
_credential.custom_fields.push(row[k][j]); row[k][j].value = await FileService.uploadFile(_file).then(FileService.getEmptyFileWithDecryptedFilename);
} }
_credential.custom_fields.push(row[k][j]);
} }
} else if(field === 'files'){ } else if(field === 'files'){
if (row[k] !== undefined && (typeof row[k] === 'string' || row[k] instanceof String) && row[k].length > 1){ if (row[k] !== undefined && (typeof row[k] === 'string' || row[k] instanceof String) && row[k].length > 1){
try { try {
row[k] = JSON.parse(row[k]); row[k] = JSON.parse(row[k]);
for(let i = 0; k < row[k].length; i++){ for(let i = 0; i < row[k].length; i++){
_credential.files.push({ _credential.files.push(await FileService.uploadFile({
filename: row[k][i].filename, filename: row[k][i].filename,
size: row[k][i].size, size: row[k][i].size,
mimetype: row[k][i].mimetype mimetype: row[k][i].mimetype,
}); data: row[k][i].file_data ? row[k][i].file_data : row[k][i].data
}).then(FileService.getEmptyFileWithDecryptedFilename));
} }
} catch (e) { } catch (e) {
// ignore row[k], it contains no valid json data // ignore row[k], it contains no valid json data
// console.error(e); console.error(e);
} }
} else { } else {
for(let j = 0; j < row[k].length; j++){ for(let j = 0; j < row[k].length; j++){
@ -163,15 +201,25 @@
filename: row[k][j].filename, filename: row[k][j].filename,
size: row[k][j].size, size: row[k][j].size,
mimetype: row[k][j].mimetype, mimetype: row[k][j].mimetype,
data: row[k][j].file_data data: row[k][j].file_data ? row[k][j].file_data : row[k][j].data
}).then(FileService.getEmptyFileWithDecryptedFilename)); }).then(FileService.getEmptyFileWithDecryptedFilename));
} }
} }
} else if(field === 'tags'){ } else if(field === 'tags'){
if( row[k]) { if(row[k] && row[k] !== '' && row[k] !== '[]') {
var tags = row[k].split(','); if (row[k].startsWith('[') && row[k].endsWith(']')) {
row[k] = row[k].substring(1, row[k].length - 1);
}
const tags = row[k].split(',');
_credential.tags = tags.map(tagMapper); _credential.tags = tags.map(tagMapper);
} }
} else if(field === 'compromised'){
_credential[field] = (row[k] === 'true' || row[k] === '1');
} else if (field === 'created' || field === 'changed' || field === 'expire_time' || field === 'delete_time') {
const num = parseInt(row[k]);
if (!isNaN(num)) {
_credential[field] = num;
}
} else{ } else{
_credential[field] = row[k]; _credential[field] = row[k];
} }
@ -181,19 +229,19 @@
}; };
$scope.inspectCredential = function (row) { $scope.inspectCredential = async function (row) {
$scope.inspected_credential = rowToCredential(row); $scope.inspected_credential = await rowToCredential(row);
}; };
$scope.csvLoaded = function (file) { $scope.csvLoaded = function (file) {
$scope.import_fields = []; $scope.import_fields = [];
$scope.inspected_credential = false; $scope.inspected_credential = {};
$scope.matched = false; $scope.atLeastlabelMatched = false;
var file_data = file.data.split(','); var file_data = file.data.split(',');
file_data = decodeURIComponent(escape(window.atob(file_data[1]))); file_data = decodeURIComponent(escape(window.atob(file_data[1])));
/** global: Papa */ /** global: Papa */
Papa.parse(file_data, { Papa.parse(file_data, {
complete: function(results) { complete: async function(results) {
if(results.data) { if(results.data) {
for(var i = 0; i < results.data[0].length; i++){ for(var i = 0; i < results.data[0].length; i++){
var propName = results.data[0][i]; var propName = results.data[0][i];
@ -203,13 +251,15 @@
if(credentialProperty.matching){ if(credentialProperty.matching){
if(credentialProperty.matching.indexOf(propName.toLowerCase()) !== -1){ if(credentialProperty.matching.indexOf(propName.toLowerCase()) !== -1){
$scope.import_fields[i] = credentialProperty.prop; $scope.import_fields[i] = credentialProperty.prop;
$scope.matched = true; if (credentialProperty.prop === 'label') {
$scope.atLeastlabelMatched = true;
}
} }
} }
} }
} }
if($scope.matched){ if($scope.atLeastlabelMatched){
$scope.inspectCredential(results.data[1]); await $scope.inspectCredential(results.data[1]);
} }
for(var j = 0; j < results.data.length; j++){ for(var j = 0; j < results.data.length; j++){
@ -242,6 +292,7 @@
}; };
$scope.log.push($translate.instant('done')); $scope.log.push($translate.instant('done'));
$scope.importing = false; $scope.importing = false;
$rootScope.refresh();
} }
} }
@ -274,5 +325,12 @@
var start = ($scope.skipFirstRow) ? 1 : 0; var start = ($scope.skipFirstRow) ? 1 : 0;
$scope.inspectCredential($scope.parsed_csv[start]); $scope.inspectCredential($scope.parsed_csv[start]);
}; };
$scope.fileLoadError = function (file) {
console.error($translate.instant('error.loading.file'), file);
};
$scope.fileSelectProgress = function () {
};
}]); }]);
}()); }());

View file

@ -36,7 +36,7 @@
replace: true, replace: true,
restrict: 'A', restrict: 'A',
scope: { scope: {
credential: '=credentialTemplate' credential: '='
}, },
link: function (scope, element, attrs) { link: function (scope, element, attrs) {

View file

@ -34,33 +34,50 @@ PassmanExporter.csv.export = function (credentials, FileService, EncryptService,
/** global: C_Promise */ /** global: C_Promise */
return new C_Promise(function () { return new C_Promise(function () {
PassmanExporter.getCredentialsWithFiles(credentials, FileService, EncryptService, _log, $translate).then((function(){ PassmanExporter.getCredentialsWithFiles(credentials, FileService, EncryptService, _log, $translate).then((function(){
var headers = ['label', 'username', 'password', 'email', 'description', 'tags', 'url', 'custom_fields', 'files']; const headers = [
var file_data = '"' + headers.join('","') + '"\n'; 'label',
for (var i = 0; i < credentials.length; i++) { 'description',
var _credential = credentials[i]; 'created',
var row_data = []; 'changed',
for (var h = 0; h < headers.length; h++) { 'tags',
var field = headers[h]; 'email',
'icon',
'username',
'password',
'url',
'renew_interval',
'expire_time',
'delete_time',
'files',
'custom_fields',
'otp',
'compromised',
'hidden'
];
let file_data = '"' + headers.join('","') + '"\n';
for (let i = 0; i < credentials.length; i++) {
const _credential = credentials[i];
let row_data = [];
for (const field of headers) {
if (field === 'tags') { if (field === 'tags') {
var _tags = []; let _tags = [];
for (var t = 0; t < _credential[field].length; t++) { for (const tag_field of _credential[field]) {
_tags.push(_credential[field][t].text); _tags.push(tag_field.text);
} }
var tag_data = '[' + _tags.join(",") + ']'; const tag_data = '[' + _tags.join(",") + ']';
row_data.push('"' + tag_data.replaceAll('"', '""') + '"'); row_data.push('"' + tag_data.replaceAll('"', '""') + '"');
} } else if (field === 'custom_fields' || field === 'files' || field === 'otp' || field === 'icon') {
else if (field == 'custom_fields' || field == 'files') { let _fields = JSON.stringify(_credential[field]);
var _fields = JSON.stringify(_credential[field]); _fields = _fields.replaceAll('"', '""');
_fields = _fields.replaceAll('"', '""'); row_data.push('"' + _fields + '"');
row_data.push('"' + _fields + '"'); } else {
} let data = _credential[field];
else { data = typeof data === 'number' || typeof data === 'boolean' ? "" + data : data;
var data = _credential[field], const value = (data === null || data === undefined) ? '' : data.replaceAll('"', '""');
value = data === null ? '':data.replaceAll('"', '""'); row_data.push('"' + value + '"');
row_data.push('"' + value + '"');
} }
} }
var progress = { let progress = {
percent: i / credentials.length * 100, percent: i / credentials.length * 100,
loaded: i, loaded: i,
total: credentials.length total: credentials.length

View file

@ -34,9 +34,9 @@ PassmanExporter.json.export = function (credentials, FileService, EncryptService
/** global: C_Promise */ /** global: C_Promise */
return new C_Promise(function () { return new C_Promise(function () {
PassmanExporter.getCredentialsWithFiles(credentials, FileService, EncryptService, _log, $translate).then((function(){ PassmanExporter.getCredentialsWithFiles(credentials, FileService, EncryptService, _log, $translate).then((function(){
var _output = []; let _output = [];
for (var i = 0; i < credentials.length; i++) { for (let i = 0; i < credentials.length; i++) {
var _credential = angular.copy(credentials[i]); let _credential = angular.copy(credentials[i]);
delete _credential.vault_key; delete _credential.vault_key;
delete _credential.vault_id; delete _credential.vault_id;
@ -44,14 +44,14 @@ PassmanExporter.json.export = function (credentials, FileService, EncryptService
_output.push(_credential); _output.push(_credential);
var progress = { let progress = {
percent: i / credentials.length * 100, percent: i / credentials.length * 100,
loaded: i, loaded: i,
total: credentials.length total: credentials.length
}; };
this.call_progress(progress); this.call_progress(progress);
} }
var file_data = JSON.stringify(_output); let file_data = JSON.stringify(_output);
this.call_then(); this.call_then();
download(file_data, 'passman-export.json'); download(file_data, 'passman-export.json');
}).bind(this)).progress(function() { }).bind(this)).progress(function() {

View file

@ -75,44 +75,49 @@ if (!window['PassmanExporter']) {
} }
}).bind(this); }).bind(this);
for (var i = 0; i < credentials.length; i++) { for (let i = 0; i < credentials.length; i++) {
const credential = credentials[i];
var item = credentials[i];
// Custom fields // Custom fields
for (c = 0; c < item.custom_fields.length; c++) { for (let c = 0; c < credential.custom_fields.length; c++) {
var cf = item.custom_fields[c]; const cf = credential.custom_fields[c];
if (cf.field_type === 'file') { if (cf.field_type === 'file') {
const file = cf.value;
if (file !== "undefined" && file !== undefined && file !== null && file.file_id !== undefined) {
this.parent.total++;
this.parent.fileGUID_cred[file.guid] = {
cred_pos: i,
on: 'custom_fields',
at: c
};
this.parent.FS.getFile(file).then((function (data) {
this.parent.step(data);
}).bind(this), (function (error) {
this.parent.stepFailed(error);
}).bind(this));
}
}
}
// Also get all files
for (let c = 0; c < credential.files.length; c++) {
const file = credential.files[c];
if (file !== "undefined" && file !== undefined && file !== null && file.file_id !== undefined) {
this.parent.total++; this.parent.total++;
this.parent.fileGUID_cred[cf.value.guid] = { this.parent.fileGUID_cred[file.guid] = {
cred_pos: i, cred_pos: i,
on: 'custom_fields', on: 'files',
at: c at: c
}; };
this.parent.FS.getFile(cf.value).then((function (data) { this.parent.FS.getFile(file).then((function (data) {
this.parent.step(data); this.parent.step(data);
}).bind(this), (function (error) { }).bind(this), (function (error) {
this.parent.stepFailed(error); this.parent.stepFailed(error);
}).bind(this)); }).bind(this));
} }
} }
// Also get all files
for (var c = 0; c < item.files.length; c++) {
this.parent.total++;
this.parent.fileGUID_cred[item.files[c].guid] = {
cred_pos: i,
on: 'files',
at: c
};
this.parent.FS.getFile(item.files[c]).then((function (data) {
this.parent.step(data);
}).bind(this), (function (error) {
this.parent.stepFailed(error);
}).bind(this));
}
} }
// We have finished downloading everything, so let's hand over job to somewhere else! // We have finished downloading everything, so let's hand over job to somewhere else!

File diff suppressed because one or more lines are too long

View file

@ -98,10 +98,10 @@ h3 {
width: 100%; width: 100%;
} }
} }
.setting-group.margin-bottom-25 { .setting-group.margin-bottom-25, .margin-bottom-25 {
margin-bottom: 25px; margin-bottom: 25px;
} }
.setting-group.margin-bottom-10 { .setting-group.margin-bottom-10, .margin-bottom-10 {
margin-bottom: 10px; margin-bottom: 10px;
} }
.display-grid { .display-grid {
@ -117,7 +117,7 @@ h3 {
label[for=confirmVaultPWChange] { label[for=confirmVaultPWChange] {
margin-bottom: 10px; margin-bottom: 10px;
} }
label[for=confirmVaultDelete] { label[for=confirmVaultDelete], label[for=skipFirstRow] {
margin-top: 10px; margin-top: 10px;
margin-bottom: 10px; margin-bottom: 10px;
} }

View file

@ -1,9 +1,9 @@
<div ng-controller="GenericCsvImportCtrl"> <div ng-controller="GenericCsvImportCtrl">
<div class="row"> <div class="row margin-bottom-25">
<div class="col-xs-12 col-md-3"> <div class="col-xs-12 col-md-3">
<div>{{ 'select.csv' | translate}} <div>{{ 'select.csv' | translate}}
<input type="file" file-select accept=".csv" <input type="file" file-select accept=".csv"
success="csvLoaded"> success="csvLoaded" error="fileLoadError" progress="fileSelectProgress">
</div> </div>
<div ng-show="parsed_csv"> <div ng-show="parsed_csv">
<span translate="parsed.csv.rows" translate-value-rows="{{ parsed_csv.length }}"> <span translate="parsed.csv.rows" translate-value-rows="{{ parsed_csv.length }}">
@ -11,7 +11,8 @@
</span> </span>
</div> </div>
<div ng-show="parsed_csv"> <div ng-show="parsed_csv">
<input type="checkbox" ng-model="skipFirstRow"> {{ 'skip.first.row' | translate}} <input id="skipFirstRow" class="checkbox" type="checkbox" ng-model="skipFirstRow">
<label for="skipFirstRow">{{'skip.first.row' | translate}}</label>
</div> </div>
<div ng-show="import_fields.indexOf('label') === -1 && parsed_csv"> <div ng-show="import_fields.indexOf('label') === -1 && parsed_csv">
<b>{{ 'import.csv.label.req' | translate}}</b> <b>{{ 'import.csv.label.req' | translate}}</b>
@ -25,40 +26,42 @@
<div progress-bar="import_progress.progress" index="import_progress.loaded" total="import_progress.total"></div> <div progress-bar="import_progress.progress" index="import_progress.loaded" total="import_progress.total"></div>
</div> </div>
</div> </div>
<div>
<div ng-if="log" class="import_log">
<textarea id="import_log" auto-scroll="log">{{log.join('\n')}}</textarea>
</div>
</div>
</div> </div>
<div class="col-xs-12 col-md-9" ng-show="parsed_csv"> <div class="col-xs-12 col-md-9">
<div ng-if="log" class="import_log">
<textarea id="import_log" auto-scroll="log">{{log.join('\n')}}</textarea>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12 display-grid" ng-show="parsed_csv">
<b>{{ 'first.five.lines' | translate }}</b><br /> <b>{{ 'first.five.lines' | translate }}</b><br />
{{ 'assign.column' | translate }} {{ 'assign.column' | translate }}
<div class="import-table-outter"> <div class="import-table-outter margin-bottom-25">
<table class="import-table"> <table class="import-table">
<tr ng-repeat="line in parsed_csv | limitTo:5"> <tr ng-repeat="line in parsed_csv | limitTo:5">
<td class="inspect"><i class="fa fa-search" <td class="inspect"><i class="fa fa-search"
ng-click="inspectCredential(line)" ng-click="inspectCredential(line)"
ng-if="($index > 0 && matched && import_fields.length > 0) || ($index >= 0 && !matched && import_fields.length > 0)"></i> ng-if="($index > 0 && matched && import_fields.length > 0) || ($index >= 0 && !matched && import_fields.length > 0)"></i>
</td> </td>
<td ng-repeat="prop in line track by $index"> <td ng-repeat="prop in line track by $index">
{{line[$index]}} {{"" + prop | limitTo: 100}}
</td> </td>
</tr> </tr>
<tr ng-repeat="line in parsed_csv | limitTo:1"> <tr ng-repeat="line in parsed_csv | limitTo:1">
<td></td> <td></td>
<td ng-repeat="prop in line track by $index"> <td ng-repeat="prop in line track by $index">
<select ng-model="import_fields[$index]" ng-change="updateExample()" <select ng-model="import_fields[$index]" ng-change="updateExample()"
ng-options="property.prop as property.label for property in credentialProperties"> ng-options="property.prop as property.label for property in credentialProperties">
</select> </select>
</td> </td>
</tr> </tr>
</table> </table>
</div> </div>
<div ng-show="inspected_credential && import_fields.length > 0"> <div ng-show="inspected_credential && import_fields.length > 0">
<b>{{ 'example.credential' | translate}}</b> <b>{{ 'example.credential' | translate}}</b>
<div credential-template="inspected_credential" show-label> <div credential-template credential="inspected_credential" show-label>
</div> </div>
</div> </div>

View file

@ -158,7 +158,7 @@
<h2 class="sidebar-label">{{selectedCredential.label}}</h2> <h2 class="sidebar-label">{{selectedCredential.label}}</h2>
</div> </div>
<div credential-template="selectedCredential"></div> <div credential-template credential="selectedCredential"></div>
<div ng-show="selectedCredential"> <div ng-show="selectedCredential">
<div> <div>