diff --git a/app/scripts/collections/external-entry-collection.js b/app/scripts/collections/external-entry-collection.js
deleted file mode 100644
index 2eee76bd..00000000
--- a/app/scripts/collections/external-entry-collection.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { Collection } from 'framework/collection';
-import { ExternalEntryModel } from 'models/external/external-entry-model';
-
-class ExternalEntryCollection extends Collection {
- static model = ExternalEntryModel;
-}
-
-export { ExternalEntryCollection };
diff --git a/app/scripts/models/app-model.js b/app/scripts/models/app-model.js
index 7203326b..60f62ab9 100644
--- a/app/scripts/models/app-model.js
+++ b/app/scripts/models/app-model.js
@@ -12,7 +12,7 @@ import { EntryModel } from 'models/entry-model';
import { FileInfoModel } from 'models/file-info-model';
import { FileModel } from 'models/file-model';
import { GroupModel } from 'models/group-model';
-import { YubiKeyOtpModel } from 'models/external/yubikey-otp-model';
+import { YubiKeyOtpModel } from 'models/otp-device/yubikey-otp-model';
import { MenuModel } from 'models/menu/menu-model';
import { PluginManager } from 'plugins/plugin-manager';
import { Features } from 'util/features';
@@ -307,12 +307,12 @@ class AppModel {
const preparedFilter = this.prepareFilter(filter);
const entries = new SearchResultCollection();
- const devicesToMatchOtpEntries = this.files.filter((file) => file.external);
+ const devicesToMatchOtpEntries = this.files.filter((file) => file.backend === 'otp-device');
const matchedOtpEntrySet = this.settings.yubiKeyMatchEntries ? new Set() : undefined;
this.files
- .filter((file) => !file.external)
+ .filter((file) => file.backend !== 'otp-device')
.forEach((file) => {
file.forEachEntry(preparedFilter, (entry) => {
if (matchedOtpEntrySet) {
@@ -827,7 +827,10 @@ class AppModel {
this.scheduleBackupFile(file, data);
}
if (this.settings.yubiKeyAutoOpen) {
- if (this.attachedYubiKeysCount > 0 && !this.files.some((f) => f.external)) {
+ if (
+ this.attachedYubiKeysCount > 0 &&
+ !this.files.some((f) => f.backend === 'otp-device')
+ ) {
this.tryOpenOtpDeviceInBackground();
}
}
@@ -1271,7 +1274,9 @@ class AppModel {
}
const isNewYubiKey = UsbListener.attachedYubiKeys > attachedYubiKeysCount;
- const hasOpenFiles = this.files.some((file) => file.active && !file.external);
+ const hasOpenFiles = this.files.some(
+ (file) => file.active && file.backend !== 'otp-device'
+ );
if (isNewYubiKey && hasOpenFiles && !this.openingOtpDevice) {
this.tryOpenOtpDeviceInBackground();
@@ -1303,7 +1308,7 @@ class AppModel {
return null;
}
for (const file of this.files) {
- if (file.external) {
+ if (file.backend === 'otp-device') {
const matchingEntry = file.getMatchingEntry(entry);
if (matchingEntry) {
return matchingEntry;
diff --git a/app/scripts/models/entry-model.js b/app/scripts/models/entry-model.js
index 20537bd4..ddd11ea8 100644
--- a/app/scripts/models/entry-model.js
+++ b/app/scripts/models/entry-model.js
@@ -505,7 +505,7 @@ class EntryModel extends Model {
}
try {
this.otpGenerator = Otp.parseUrl(otpUrl);
- } catch (e) {
+ } catch {
this.otpGenerator = null;
}
} else {
diff --git a/app/scripts/models/external/external-otp-device-model.js b/app/scripts/models/external/external-otp-device-model.js
deleted file mode 100644
index 9e6e654e..00000000
--- a/app/scripts/models/external/external-otp-device-model.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import { ExternalDeviceModel } from 'models/external/external-device-model';
-
-class ExternalOtpDeviceModel extends ExternalDeviceModel {
- open(callback) {
- throw 'Not implemented';
- }
-
- cancelOpen() {
- throw 'Not implemented';
- }
-
- close(callback) {
- throw 'Not implemented';
- }
-
- getOtp(callback) {
- throw 'Not implemented';
- }
-}
-
-export { ExternalOtpDeviceModel };
diff --git a/app/scripts/models/file-model.js b/app/scripts/models/file-model.js
index 06eaeccb..608cc0dc 100644
--- a/app/scripts/models/file-model.js
+++ b/app/scripts/models/file-model.js
@@ -766,7 +766,11 @@ FileModel.defineModelProperties({
oldKeyFileHash: null,
oldKeyChangeDate: null,
encryptedPassword: null,
- encryptedPasswordDate: null
+ encryptedPasswordDate: null,
+ supportsTags: true,
+ supportsColors: true,
+ supportsIcons: true,
+ supportsExpiration: true
});
export { FileModel };
diff --git a/app/scripts/models/otp-device/otp-device-entry-collection.js b/app/scripts/models/otp-device/otp-device-entry-collection.js
new file mode 100644
index 00000000..29b9d0b4
--- /dev/null
+++ b/app/scripts/models/otp-device/otp-device-entry-collection.js
@@ -0,0 +1,8 @@
+import { Collection } from 'framework/collection';
+import { OtpDeviceEntryModel } from './otp-device-entry-model';
+
+class OtpDeviceEntryCollection extends Collection {
+ static model = OtpDeviceEntryModel;
+}
+
+export { OtpDeviceEntryCollection };
diff --git a/app/scripts/models/external/external-entry-model.js b/app/scripts/models/otp-device/otp-device-entry-model.js
similarity index 88%
rename from app/scripts/models/external/external-entry-model.js
rename to app/scripts/models/otp-device/otp-device-entry-model.js
index 03232def..63bbeea1 100644
--- a/app/scripts/models/external/external-entry-model.js
+++ b/app/scripts/models/otp-device/otp-device-entry-model.js
@@ -1,7 +1,7 @@
import { Model } from 'framework/model';
import { EntrySearch } from 'util/entry-search';
-class ExternalEntryModel extends Model {
+class OtpDeviceEntryModel extends Model {
tags = [];
fields = {};
@@ -44,9 +44,10 @@ class ExternalEntryModel extends Model {
}
}
-ExternalEntryModel.defineModelProperties({
+OtpDeviceEntryModel.defineModelProperties({
id: '',
- external: true,
+ file: null,
+ entry: true,
readOnly: true,
device: undefined,
deviceSubId: null,
@@ -59,4 +60,4 @@ ExternalEntryModel.defineModelProperties({
_search: undefined
});
-export { ExternalEntryModel };
+export { OtpDeviceEntryModel };
diff --git a/app/scripts/models/external/external-device-model.js b/app/scripts/models/otp-device/otp-device-model.js
similarity index 63%
rename from app/scripts/models/external/external-device-model.js
rename to app/scripts/models/otp-device/otp-device-model.js
index 96222c36..6a4ffd55 100644
--- a/app/scripts/models/external/external-device-model.js
+++ b/app/scripts/models/otp-device/otp-device-model.js
@@ -1,13 +1,11 @@
import { Model } from 'framework/model';
-import { ExternalEntryCollection } from 'collections/external-entry-collection';
+import { OtpDeviceEntryCollection } from './otp-device-entry-collection';
-class ExternalDeviceModel extends Model {
- entries = new ExternalEntryCollection();
+class OtpDeviceModel extends Model {
+ entries = new OtpDeviceEntryCollection();
groups = [];
entryMap = {};
- close() {}
-
forEachEntry(filter, callback) {
if (filter.trash || filter.group || filter.tag) {
return;
@@ -32,11 +30,28 @@ class ExternalDeviceModel extends Model {
this.entryMap[entry.id.toLowerCase()] = entry;
}
}
+
+ open(callback) {
+ throw 'Not implemented';
+ }
+
+ cancelOpen() {
+ throw 'Not implemented';
+ }
+
+ close(callback) {
+ throw 'Not implemented';
+ }
+
+ getOtp(callback) {
+ throw 'Not implemented';
+ }
}
-ExternalDeviceModel.defineModelProperties({
+OtpDeviceModel.defineModelProperties({
id: '',
- external: true,
+ backend: 'otp-device',
+ skipOpenList: true,
readOnly: true,
active: false,
entries: undefined,
@@ -47,4 +62,4 @@ ExternalDeviceModel.defineModelProperties({
entryMap: undefined
});
-export { ExternalDeviceModel };
+export { OtpDeviceModel };
diff --git a/app/scripts/models/external/external-otp-entry-model.js b/app/scripts/models/otp-device/otp-entry-model.js
similarity index 89%
rename from app/scripts/models/external/external-otp-entry-model.js
rename to app/scripts/models/otp-device/otp-entry-model.js
index 7d8292ed..05d4e8e1 100644
--- a/app/scripts/models/external/external-otp-entry-model.js
+++ b/app/scripts/models/otp-device/otp-entry-model.js
@@ -1,6 +1,6 @@
-import { ExternalEntryModel } from 'models/external/external-entry-model';
+import { OtpDeviceEntryModel } from './otp-device-entry-model';
-class ExternalOtpEntryModel extends ExternalEntryModel {
+class OtpEntryModel extends OtpDeviceEntryModel {
constructor(props) {
super({
...props,
@@ -59,10 +59,11 @@ class ExternalOtpEntryModel extends ExternalEntryModel {
}
}
-ExternalOtpEntryModel.defineModelProperties({
+OtpEntryModel.defineModelProperties({
user: undefined,
+ backend: 'otp-device',
otpGenerator: undefined,
needsTouch: false
});
-export { ExternalOtpEntryModel };
+export { OtpEntryModel };
diff --git a/app/scripts/models/external/yubikey-otp-model.js b/app/scripts/models/otp-device/yubikey-otp-model.js
similarity index 92%
rename from app/scripts/models/external/yubikey-otp-model.js
rename to app/scripts/models/otp-device/yubikey-otp-model.js
index def68321..d10066b2 100644
--- a/app/scripts/models/external/yubikey-otp-model.js
+++ b/app/scripts/models/otp-device/yubikey-otp-model.js
@@ -1,13 +1,13 @@
import { Events } from 'framework/events';
-import { ExternalOtpDeviceModel } from 'models/external/external-otp-device-model';
-import { ExternalOtpEntryModel } from 'models/external/external-otp-entry-model';
+import { OtpDeviceModel } from './otp-device-model';
+import { OtpEntryModel } from './otp-entry-model';
import { Logger } from 'util/logger';
import { UsbListener } from 'comp/app/usb-listener';
import { YubiKey } from 'comp/app/yubikey';
const logger = new Logger('yubikey');
-class YubiKeyOtpModel extends ExternalOtpDeviceModel {
+class YubiKeyOtpModel extends OtpDeviceModel {
constructor(props) {
super({
id: 'yubikey',
@@ -67,8 +67,9 @@ class YubiKeyOtpModel extends ExternalOtpDeviceModel {
for (const code of codes) {
this.entries.push(
- new ExternalOtpEntryModel({
+ new OtpEntryModel({
id: this.entryId(code.title, code.user),
+ file: this,
device: this,
deviceSubId: serial,
icon: 'clock',
diff --git a/app/scripts/presenters/entry-presenter.js b/app/scripts/presenters/entry-presenter.js
index 44444146..6bc8ef9f 100644
--- a/app/scripts/presenters/entry-presenter.js
+++ b/app/scripts/presenters/entry-presenter.js
@@ -14,9 +14,6 @@ EntryPresenter.prototype = {
this.entry = item;
} else if (item.group) {
this.group = item;
- } else if (item.external) {
- this.entry = item;
- this.external = true;
}
return this;
},
@@ -71,7 +68,7 @@ EntryPresenter.prototype = {
if (!this.entry) {
return '[' + Locale.listGroup + ']';
}
- if (this.external) {
+ if (this.entry.backend === 'otp-device') {
return this.entry.description;
}
switch (this.descField) {
diff --git a/app/scripts/views/app-view.js b/app/scripts/views/app-view.js
index 6dcb231e..78689cca 100644
--- a/app/scripts/views/app-view.js
+++ b/app/scripts/views/app-view.js
@@ -616,7 +616,7 @@ class AppView extends View {
closeAllFilesAndShowFirst() {
let fileToShow = this.model.files.find(
- (file) => !file.demo && !file.created && !file.external
+ (file) => !file.demo && !file.created && !file.skipOpenList
);
this.model.closeAllFiles();
if (!fileToShow) {
diff --git a/app/scripts/views/details/details-fields.js b/app/scripts/views/details/details-fields.js
index d10e909f..520a2ae6 100644
--- a/app/scripts/views/details/details-fields.js
+++ b/app/scripts/views/details/details-fields.js
@@ -21,7 +21,7 @@ function createDetailsFields(detailsView) {
const fieldViews = [];
const fieldViewsAside = [];
- if (model.external) {
+ if (model.backend === 'otp-device') {
fieldViewsAside.push(
new FieldViewReadOnly({
name: 'Device',
@@ -73,15 +73,27 @@ function createDetailsFields(detailsView) {
})
);
} else {
- fieldViewsAside.push(
- new FieldViewReadOnly({
- name: 'File',
- title: StringFormat.capFirst(Locale.file),
- value() {
- return model.fileName;
- }
- })
- );
+ if (model.backend) {
+ fieldViewsAside.push(
+ new FieldViewReadOnly({
+ name: 'Storage',
+ title: StringFormat.capFirst(Locale.storage),
+ value() {
+ return model.fileName;
+ }
+ })
+ );
+ } else {
+ fieldViewsAside.push(
+ new FieldViewReadOnly({
+ name: 'File',
+ title: StringFormat.capFirst(Locale.file),
+ value() {
+ return model.fileName;
+ }
+ })
+ );
+ }
}
fieldViews.push(
new FieldViewAutocomplete({
@@ -127,26 +139,30 @@ function createDetailsFields(detailsView) {
sequence: '{NOTES}'
})
);
- fieldViews.push(
- new FieldViewTags({
- name: 'Tags',
- title: StringFormat.capFirst(Locale.tags),
- tags: AppModel.instance.tags,
- value() {
- return model.tags;
- }
- })
- );
- fieldViews.push(
- new FieldViewDate({
- name: 'Expires',
- title: Locale.detExpires,
- lessThanNow: '(' + Locale.detExpired + ')',
- value() {
- return model.expires;
- }
- })
- );
+ if (model.file.supportsTags) {
+ fieldViews.push(
+ new FieldViewTags({
+ name: 'Tags',
+ title: StringFormat.capFirst(Locale.tags),
+ tags: AppModel.instance.tags,
+ value() {
+ return model.tags;
+ }
+ })
+ );
+ }
+ if (model.file.supportsExpiration) {
+ fieldViews.push(
+ new FieldViewDate({
+ name: 'Expires',
+ title: Locale.detExpires,
+ lessThanNow: '(' + Locale.detExpired + ')',
+ value() {
+ return model.expires;
+ }
+ })
+ );
+ }
fieldViewsAside.push(
new FieldViewReadOnly({
name: 'Group',
@@ -159,33 +175,39 @@ function createDetailsFields(detailsView) {
}
})
);
- fieldViewsAside.push(
- new FieldViewReadOnly({
- name: 'Created',
- title: Locale.detCreated,
- value() {
- return DateFormat.dtStr(model.created);
- }
- })
- );
- fieldViewsAside.push(
- new FieldViewReadOnly({
- name: 'Updated',
- title: Locale.detUpdated,
- value() {
- return DateFormat.dtStr(model.updated);
- }
- })
- );
- fieldViewsAside.push(
- new FieldViewHistory({
- name: 'History',
- title: StringFormat.capFirst(Locale.history),
- value() {
- return { length: model.historyLength, unsaved: model.unsaved };
- }
- })
- );
+ if (model.created) {
+ fieldViewsAside.push(
+ new FieldViewReadOnly({
+ name: 'Created',
+ title: Locale.detCreated,
+ value() {
+ return DateFormat.dtStr(model.created);
+ }
+ })
+ );
+ }
+ if (model.updated) {
+ fieldViewsAside.push(
+ new FieldViewReadOnly({
+ name: 'Updated',
+ title: Locale.detUpdated,
+ value() {
+ return DateFormat.dtStr(model.updated);
+ }
+ })
+ );
+ }
+ if (!model.backend) {
+ fieldViewsAside.push(
+ new FieldViewHistory({
+ name: 'History',
+ title: StringFormat.capFirst(Locale.history),
+ value() {
+ return { length: model.historyLength, unsaved: model.unsaved };
+ }
+ })
+ );
+ }
if (otpEntry) {
fieldViews.push(
new FieldViewOtp({
@@ -207,7 +229,7 @@ function createDetailsFields(detailsView) {
fieldViews.push(
new FieldViewOtp({
name: '$' + field,
- title: field,
+ title: Locale.detOtpField,
value() {
return model.otpGenerator;
},
diff --git a/app/scripts/views/details/details-view.js b/app/scripts/views/details/details-view.js
index 79873f08..1017a7ea 100644
--- a/app/scripts/views/details/details-view.js
+++ b/app/scripts/views/details/details-view.js
@@ -121,6 +121,9 @@ class DetailsView extends View {
}
const model = {
deleted: this.appModel.filter.trash,
+ canEditColor: this.model.file.supportsColors && !this.model.readOnly,
+ canEditIcon: this.model.file.supportsIcons && !this.model.readOnly,
+ showButtons: !this.model.backend && !this.model.readOnly,
...this.model
};
this.template = template;
@@ -185,7 +188,7 @@ class DetailsView extends View {
this.fieldViews = fieldViews.concat(fieldViewsAside);
- if (!this.model.external) {
+ if (!this.model.backend) {
this.moreView = new DetailsAddFieldView();
this.moreView.render();
this.moreView.on('add-field', this.addNewField.bind(this));
@@ -363,7 +366,7 @@ class DetailsView extends View {
}
toggleIcons() {
- if (this.model.external) {
+ if (this.model.backend) {
return;
}
if (this.views.sub && this.views.sub instanceof IconSelectView) {
@@ -464,7 +467,7 @@ class DetailsView extends View {
}
this.model.initOtpGenerator?.();
- if (this.model.external) {
+ if (this.model.backend === 'otp-device') {
return;
}
@@ -496,7 +499,7 @@ class DetailsView extends View {
if (!this.model) {
return;
}
- if (this.model.external) {
+ if (this.model.backend === 'otp-device') {
this.copyOtp();
e.preventDefault();
}
@@ -520,7 +523,7 @@ class DetailsView extends View {
copyOtp() {
const otpField = this.getFieldView('$otp');
- if (this.model.external) {
+ if (this.model.backend === 'otp-device') {
if (!otpField) {
return false;
}
@@ -715,7 +718,7 @@ class DetailsView extends View {
}
editTitle() {
- if (this.model.external) {
+ if (this.model.backend === 'otp-device') {
return;
}
const input = $('')
@@ -875,7 +878,7 @@ class DetailsView extends View {
const canCopy = document.queryCommandSupported('copy');
const options = [];
if (canCopy) {
- if (this.model.external) {
+ if (this.model.backend === 'otp-device') {
options.push({
value: 'det-copy-otp',
icon: 'copy',
@@ -894,7 +897,7 @@ class DetailsView extends View {
text: Locale.detMenuCopyUser
});
}
- if (!this.model.external) {
+ if (!this.model.backend) {
options.push({ value: 'det-add-new', icon: 'plus', text: Locale.detMenuAddNewField });
options.push({ value: 'det-clone', icon: 'clone', text: Locale.detClone });
if (canCopy) {
@@ -985,7 +988,8 @@ class DetailsView extends View {
autoType(sequence) {
const entry = this.model;
- const hasOtp = sequence?.includes('{TOTP}') || (entry.external && !sequence);
+ const hasOtp =
+ sequence?.includes('{TOTP}') || (entry.backend === 'otp-device' && !sequence);
if (hasOtp) {
const otpField = this.getFieldView('$otp');
otpField.refreshOtp((err) => {
diff --git a/app/scripts/views/fields/field-view-read-only.js b/app/scripts/views/fields/field-view-read-only.js
index e4536a21..9a78aecd 100644
--- a/app/scripts/views/fields/field-view-read-only.js
+++ b/app/scripts/views/fields/field-view-read-only.js
@@ -5,7 +5,7 @@ class FieldViewReadOnly extends FieldView {
readonly = true;
renderValue(value) {
- value = value.isProtected ? new Array(value.textLength + 1).join('•') : escape(value);
+ value = value?.isProtected ? new Array(value.textLength + 1).join('•') : escape(value);
value = value.replace(/\n/g, '
');
return value;
}
diff --git a/app/scripts/views/settings/settings-file-external-view.js b/app/scripts/views/settings/settings-file-otp-device-view.js
similarity index 78%
rename from app/scripts/views/settings/settings-file-external-view.js
rename to app/scripts/views/settings/settings-file-otp-device-view.js
index 47ab3763..b11a61d4 100644
--- a/app/scripts/views/settings/settings-file-external-view.js
+++ b/app/scripts/views/settings/settings-file-otp-device-view.js
@@ -1,8 +1,8 @@
import { View } from 'framework/views/view';
import { Events } from 'framework/events';
-import template from 'templates/settings/settings-file-external.hbs';
+import template from 'templates/settings/settings-file-otp-device.hbs';
-class SettingsFileExternalView extends View {
+class SettingsFileOtpDeviceView extends View {
template = template;
events = {
@@ -26,4 +26,4 @@ class SettingsFileExternalView extends View {
}
}
-export { SettingsFileExternalView };
+export { SettingsFileOtpDeviceView };
diff --git a/app/scripts/views/settings/settings-view.js b/app/scripts/views/settings/settings-view.js
index be77ef45..3db99a56 100644
--- a/app/scripts/views/settings/settings-view.js
+++ b/app/scripts/views/settings/settings-view.js
@@ -33,8 +33,8 @@ class SettingsView extends View {
setPage(e) {
let { page, section, file } = e;
- if (page === 'file' && file && file.external) {
- page = 'file-external';
+ if (page === 'file' && file && file.backend === 'otp-device') {
+ page = 'file-otp-device';
}
const module = require('./settings-' + page + '-view');
const viewName = StringFormat.pascalCase(page);
diff --git a/app/templates/details/details.hbs b/app/templates/details/details.hbs
index cbbbb78e..98529b5b 100644
--- a/app/templates/details/details.hbs
+++ b/app/templates/details/details.hbs
@@ -4,11 +4,11 @@