ソースを参照

feat: added field dependencies, fixed schema import, added schema 5

Kristian Vos 4 年 前
コミット
4752b4733b

+ 3 - 2
backend/logic/accountSchema.js

@@ -63,8 +63,9 @@ module.exports = class extends coreClass {
 		return new Promise(async (resolve, reject) => {
 			try { await this._validateHook(); } catch { return; }
 
-			this.accountSchemaModel.create(this.accountSchemaSchema, (err) => {
-				if (err) reject(new Error("Something went wrong."))
+			let schema = require(`../schemas/${name}`);
+			this.accountSchemaModel.create(schema, (err) => {
+				if (err) reject(new Error(err.message))
 				else resolve();
 			});
 		});

+ 2 - 1
backend/logic/io/namespaces/accountSchema.js

@@ -50,7 +50,8 @@ module.exports = {
 			});
 		}).catch(err => {
 			cb({
-				status: "failure"
+				status: "failure",
+				error: err.message
 			});
 		});
 	}

+ 2 - 1
backend/logic/mongo/schemas/accountSchema.js

@@ -2,5 +2,6 @@ module.exports = {
 	name: { type: String, required: true },
 	description: { type: String, required: true },
 	version: { type: Number, required: true, unique: true },
-	fields: [{ type: Object }]
+	fields: [{ type: Object }],
+	dependencies: { type: Object }
 };

+ 509 - 0
backend/schemas/accountSchemaV5.js

@@ -0,0 +1,509 @@
+module.exports = {
+	name: "Account",
+	description: "Account schema",
+	version: 5,
+	fields: [
+		{
+			name: "Name",
+			fieldId: "name",
+			fieldTypes: [
+				{
+					type: "text",
+					fill: true,
+					fieldTypeId: "name"
+				}
+			],
+			minEntries: 1,
+			maxEntries: 1
+		},
+		{
+			name: "Domain",
+			fieldId: "domain",
+			fieldTypes: [
+				{
+					type: "text",
+					fill: true,
+					fieldTypeId: "domain",
+					autosuggestGroup: "domain"
+				}
+			],
+			minEntries: 0,
+			maxEntries: 10
+		},
+		{
+			name: "App",
+			fieldId: "app",
+			fieldTypes: [
+				{
+					type: "select",
+					options: [
+						{
+							value: "android",
+							text: "Android"
+						},
+						{
+							value: "ios",
+							text: "iOS"
+						},
+						{
+							value: "windows",
+							text: "Windows"
+						}
+					],
+					fieldTypeId: "appType"
+				},
+				{
+					type: "text",
+					fill: true,
+					fieldTypeId: "appName"
+				}
+			],
+			minEntries: 0,
+			maxEntries: 10
+		},
+		{
+			name: "Account exists",
+			fieldId: "accountExists",
+			fieldTypes: [
+				{
+					type: "checkbox",
+					fieldTypeId: "accountExists"
+				}
+			],
+			minEntries: 0,
+			maxEntries: 1
+		},
+		{
+			name: "E-mail",
+			fieldId: "email",
+			fieldTypes: [
+				{
+					type: "text",
+					fieldTypeId: "email",
+					fill: true,
+					autosuggestGroup: "email"
+				}
+			],
+			minEntries: 0,
+			maxEntries: 10
+		},
+		{
+			name: "Username",
+			fieldId: "username",
+			fieldTypes: [
+				{
+					type: "text",
+					fieldTypeId: "username",
+					fill: true
+				}
+			],
+			minEntries: 0,
+			maxEntries: 10
+		},
+		{
+			name: "Login name",
+			fieldId: "loginName",
+			fieldTypes: [
+				{
+					type: "text",
+					fieldTypeId: "loginName",
+					fill: true
+				}
+			],
+			minEntries: 0,
+			maxEntries: 10
+		},
+		{
+			name: "Category",
+			fieldId: "category",
+			fieldTypes: [
+				{
+					type: "text",
+					fieldTypeId: "category",
+					fill: true
+				}
+			],
+			minEntries: 0,
+			maxEntries: 10
+		},
+		{
+			name: "Uses password",
+			fieldId: "usesPassword",
+			fieldTypes: [
+				{
+					type: "checkbox",
+					fieldTypeId: "usesPassword"
+				}
+			],
+			minEntries: 0,
+			maxEntries: 1
+		},
+		{
+			name: "Password last changed",
+			fieldId: "passwordLastChanged",
+			fieldTypes: [
+				{
+					type: "text",
+					fieldTypeId: "passwordLastChanged",
+					fill: true
+				}
+			],
+			minEntries: 0,
+			maxEntries: 1
+		},
+		{
+			name: "2FA possible",
+			fieldId: "twofaPossible",
+			fieldTypes: [
+				{
+					type: "select",
+					options: [
+						{
+							value: "otp",
+							text: "OTP"
+						},
+						{
+							value: "sms",
+							text: "SMS"
+						}
+					],
+					fieldTypeId: "twofaPossibleType",
+					fill: true
+				}
+			],
+			minEntries: 0,
+			maxEntries: 10
+		},
+		{
+			name: "2FA used",
+			fieldId: "twofaUsed",
+			fieldTypes: [
+				{
+					type: "select",
+					options: [
+						{
+							value: "otp",
+							text: "OTP"
+						},
+						{
+							value: "sms",
+							text: "SMS"
+						}
+					],
+					fieldTypeId: "twofaUsedType"
+				},
+				{
+					type: "text",
+					fieldTypeId: "twofaUsedValue",
+					fill: true
+				}
+			],
+			minEntries: 0,
+			maxEntries: 10
+		},
+		{
+			name: "2FA recovery method",
+			fieldId: "twofaRecovery",
+			fieldTypes: [
+				{
+					type: "select",
+					options: [
+						{
+							value: "backupCodes",
+							text: "Backup codes"
+						}
+					],
+					fieldTypeId: "twofaRecoveryMethod"
+				},
+				{
+					type: "text",
+					fieldTypeId: "twofaRecoveryValue",
+					fill: true
+				}
+			],
+			minEntries: 0,
+			maxEntries: 10
+		},
+		{
+			name: "Login service",
+			fieldId: "loginService",
+			fieldTypes: [
+				{
+					type: "text",
+					fieldTypeId: "loginService",
+					fill: true
+				}
+			],
+			minEntries: 0,
+			maxEntries: 10
+		},
+		{
+			name: "Service linked",
+			fieldId: "serviceLinked",
+			fieldTypes: [
+				{
+					type: "text",
+					fieldTypeId: "serviceLinked",
+					fill: true
+				}
+			],
+			minEntries: 0,
+			maxEntries: 10
+		},
+		{
+			name: "Uses security questions",
+			fieldId: "usesSecurityQuestions",
+			fieldTypes: [
+				{
+					type: "checkbox",
+					fieldTypeId: "usesSecurityQuestions",
+					fill: true
+				}
+			],
+			minEntries: 0,
+			maxEntries: 1
+		},
+		{
+			name: "Recovery e-mail",
+			fieldId: "recoveryEmail",
+			fieldTypes: [
+				{
+					type: "text",
+					fieldTypeId: "recoveryEmail",
+					fill: true,
+					autosuggestGroup: "email"
+				}
+			],
+			minEntries: 0,
+			maxEntries: 10
+		},
+		{
+			name: "Recovery phone number",
+			fieldId: "recoveryPhoneNumber",
+			fieldTypes: [
+				{
+					type: "text",
+					fieldTypeId: "recoveryPhoneNumber",
+					fill: true
+				}
+			],
+			minEntries: 0,
+			maxEntries: 10
+		},
+		{
+			name: "Comments",
+			fieldId: "comments",
+			fieldTypes: [
+				{
+					type: "text",
+					fieldTypeId: "comments",
+					fill: true
+				}
+			],
+			minEntries: 0,
+			maxEntries: 1
+		},
+		{
+			name: "In 1password",
+			fieldId: "in1password",
+			fieldTypes: [
+				{
+					type: "checkbox",
+					fieldTypeId: "in1password"
+				}
+			],
+			minEntries: 0,
+			maxEntries: 1
+		},
+		{
+			name: "Deleted",
+			fieldId: "deleted",
+			fieldTypes: [
+				{
+					type: "checkbox",
+					fieldTypeId: "deleted"
+				}
+			],
+			minEntries: 0,
+			maxEntries: 1
+		},
+		{
+			name: "Deleted at",
+			fieldId: "deletedAt",
+			fieldTypes: [
+				{
+					type: "text",
+					fieldTypeId: "deletedAt",
+					fill: true
+				}
+			],
+			minEntries: 0,
+			maxEntries: 1
+		},
+		{
+			name: "Service accessible",
+			fieldId: "serviceAccessible",
+			fieldTypes: [
+				{
+					type: "checkbox",
+					fieldTypeId: "serviceAccessible"
+				}
+			],
+			minEntries: 0,
+			maxEntries: 1
+		},
+		{
+			name: "Requested deletion",
+			fieldId: "requestedDeletion",
+			fieldTypes: [
+				{
+					type: "checkbox",
+					fieldTypeId: "requestedDeletion"
+				}
+			],
+			minEntries: 0,
+			maxEntries: 1
+		},
+		{
+			name: "Requested deletion at",
+			fieldId: "requestedDeletionAt",
+			fieldTypes: [
+				{
+					type: "text",
+					fieldTypeId: "requestedDeletionAt",
+					fill: true
+				}
+			],
+			minEntries: 0,
+			maxEntries: 1
+		},
+		{
+			name: "To delete",
+			fieldId: "toDelete",
+			fieldTypes: [
+				{
+					type: "checkbox",
+					fieldTypeId: "toDelete"
+				}
+			],
+			minEntries: 0,
+			maxEntries: 1
+		},
+		{
+			name: "Created at",
+			fieldId: "createdAt",
+			fieldTypes: [
+				{
+					type: "text",
+					fieldTypeId: "createdAt",
+					fill: true
+				}
+			],
+			minEntries: 0,
+			maxEntries: 1
+		}
+	],
+	dependencies: {
+		"twofaUsed": {
+			eval: "{fields}.twofaPossible.length > 0",
+			fieldId: "twofaPossible"
+		},
+		"twofaRecovery": {
+			eval: "{fields}.twofaUsed.length > 0",
+			fieldId: "twofaUsed"
+		}
+	}
+};
+
+/*
+[
+	{
+		name: "Domain",
+		fieldTypes: [
+			{
+				type: "checkbox",
+				extraButtons: []
+			},
+			{
+				type: "select",
+				options: [
+					{
+						value: "option1",
+						text: "Option 1"
+					},
+					{
+						value: "option2",
+						text: "Option 2"
+					},
+					{
+						value: "option3",
+						text: "Option 3"
+					}
+				],
+				extraButtons: [
+					{
+						icon: "~",
+						style: "red"
+					}
+				]
+			},
+			{
+				type: "text",
+				extraButtons: [],
+				fill: true
+			}
+		],
+		minEntries: 0,
+		maxEntries: 3,
+		initialEntries: [
+			[
+				true,
+				"option1",
+				"Hahaha value"
+			]
+		]
+	},
+	{
+		name: "Apps",
+		fieldTypes: [
+			{
+				type: "select",
+				options: [
+					{
+						value: "option1",
+						text: "Option 1"
+					},
+					{
+						value: "option2",
+						text: "Option 2"
+					},
+					{
+						value: "option3",
+						text: "Option 3"
+					}
+				],
+				extraButtons: [
+					{
+						icon: "~",
+						style: "red"
+					}
+				]
+			},
+			{
+				type: "text",
+				extraButtons: [],
+				fill: true
+			}
+		],
+		minEntries: 0,
+		maxEntries: 3,
+		initialEntries: [
+			[
+				true,
+				"option1",
+				"Hahaha value"
+			]
+		]
+	}
+]
+*/

+ 23 - 3
frontend/vue/components/AccountForm.vue

@@ -3,6 +3,7 @@
 		<p><b>Schema version</b>: {{account.version}}</p>
 		<field
 			v-for="field in fields"
+			v-if="dependencyChecksOut(field.fieldId)"
 			:name="field.name"
 			:minEntries="field.minEntries"
 			:maxEntries="field.maxEntries"
@@ -10,6 +11,7 @@
 			:autosuggest="autosuggest"
 			:key="field.fieldId"
 			:ref="field.fieldId"
+			:onChange="onFieldChange(field.fieldId)"
 			:fieldTypes="field.fieldTypes"/>
 			<button @click="submit()" type="button" class="button">
 				Submit
@@ -28,18 +30,32 @@ export default {
 		return {
 			fields: [],
 			account: {},
-			autosuggest: {}
+			autosuggest: {},
+			dependencies: {}
 		};
 	},
 	methods: {
 		submit() {
 			let account = JSON.parse(JSON.stringify(this.account));
-			let fields = {};
+			let fields = JSON.parse(JSON.stringify(this.templateAccount)).fields;
 			Object.keys(account.fields).forEach(fieldId => {
-				fields[fieldId] = this.$refs[fieldId][0].entries;
+				if (this.$refs[fieldId]) fields[fieldId] = this.$refs[fieldId][0].entries;
 			});
 			account.fields = fields;
 			this.onSubmit(account);
+		},
+		dependencyChecksOut(fieldId) {
+			if (!this.dependencies[fieldId]) return true;
+			let dependency = this.dependencies[fieldId];
+			let dependencyFieldId = dependency.fieldId;
+			if (!this.dependencyChecksOut(dependencyFieldId)) return false;
+			let dependencyEval = dependency.eval.replace("{fields}", "this.account.fields");
+			return eval(dependencyEval);
+		},
+		onFieldChange(fieldId) {
+			return () => {
+				this.account.fields[fieldId] = this.$refs[fieldId][0].entries;
+			};
 		}
 	},
 	props: {
@@ -52,6 +68,7 @@ export default {
 
 			socket.emit("accountSchema.getLatest", res => {
 				this.fields = res.schema.fields;
+				this.dependencies = res.schema.dependencies;
 				if (!this.initialAccount) {
 					this.account.fields = {};
 					this.account.version = res.schema.version;
@@ -69,8 +86,11 @@ export default {
 							this.account.fields[field.fieldId].push(defaultObject);
 						}
 					});
+
+					this.templateAccount = this.account;
 				} else {
 					this.account = this.initialAccount;
+					this.templateAccount = this.initialAccount;
 				}
 			});
 

+ 13 - 3
frontend/vue/components/Field.vue

@@ -4,8 +4,8 @@
 		<button v-if="entries.length === 0" @click="addEntry()" type="button">+</button>
 		<div class="control-row" v-for="(entry, entryIndex) in entries">
 			<div class="control-col" v-for="(fieldType, fieldIndex) in fieldTypes" :class="{ 'fill-remaining': fieldType.fill }">
-				<input name="name" type="text" v-if="fieldType.type === 'text'" v-model="entry[fieldType.fieldTypeId]" v-on:blur="blurInput(entryIndex, fieldType.fieldTypeId)" v-on:focus="focusInput(entryIndex, fieldType.fieldTypeId)" v-on:keydown="keydownInput(entryIndex, fieldType.fieldTypeId)" />
-				<select name="name" v-if="fieldType.type === 'select'" class="fill-remaining" v-model="entry[fieldType.fieldTypeId]">
+				<input name="name" type="text" v-if="fieldType.type === 'text'" v-model="entry[fieldType.fieldTypeId]" @change="onInputChange()" v-on:blur="blurInput(entryIndex, fieldType.fieldTypeId)" v-on:focus="focusInput(entryIndex, fieldType.fieldTypeId)" v-on:keydown="keydownInput(entryIndex, fieldType.fieldTypeId)" />
+				<select name="name" v-if="fieldType.type === 'select'" class="fill-remaining" v-model="entry[fieldType.fieldTypeId]" @change="onSelectChange()">
 					<option v-for="option in fieldType.options" :value="option.value">{{option.text}}</option>
 				</select>
 				<div tabindex="0" v-on:keyup.enter="toggleCheckbox(entryIndex, fieldType.fieldTypeId)" v-on:keyup.space="toggleCheckbox(entryIndex, fieldType.fieldTypeId)" name="name" class="checkbox" v-if="fieldType.type === 'checkbox'" :class="{ checked: entry[fieldType.fieldTypeId] }" @click="toggleCheckbox(entryIndex, fieldType.fieldTypeId)"></div>
@@ -45,7 +45,8 @@ export default {
 		minEntries: Number,
 		maxEntries: Number,
 		initialEntries: Array,
-		autosuggest: Object
+		autosuggest: Object,
+		onChange: Function
 	},
 	mounted() {
 		
@@ -58,12 +59,15 @@ export default {
 				else if (fieldType.type === "checkbox") emptyEntry[fieldType.fieldTypeId] = false;
 			});
 			this.entries.push(emptyEntry);
+			this.onChange();
 		},
 		removeEntry(index) {
 			this.entries.splice(index, 1);
+			this.onChange();
 		},
 		toggleCheckbox(entryIndex, fieldTypeId) {
 			this.entries[entryIndex][fieldTypeId] = !this.entries[entryIndex][fieldTypeId];
+			this.onChange();
 		},
 		selectAutosuggest(entryIndex, fieldTypeId, autosuggestItem) {
 			this.entries[entryIndex][fieldTypeId] = autosuggestItem;
@@ -83,6 +87,12 @@ export default {
 		blurAutosuggestContainer() {
 			this.autosuggestHover = "";
 		},
+		onSelectChange() {
+			this.onChange();
+		},
+		onInputChange() {
+			this.onChange();
+		},
 		filter(autosuggest, value) {
 			return autosuggest.filter(autosuggestItem => autosuggestItem.toLowerCase().startsWith(value.toLowerCase())).sort();
 		}