Add more components + base for history

This commit is contained in:
2024-10-01 15:42:18 +02:00
parent fb4d566007
commit 8f9c83dc2a
38 changed files with 1374 additions and 665 deletions
+4 -7
View File
@@ -41,7 +41,10 @@
<nav class="navbar navbar-expand-lg bg-primary" data-bs-theme="dark">
<div class="container-fluid">
<a href="#" @click="lock_me" class="navbar-brand">{% trans "Lock" %}</a>
<v-btn color="navbar-brand" text @click="lock_me">
<v-icon small class="mr-2">mdi-lock</v-icon>
{% trans "Lock" %}
</v-btn>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarColor01" aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
@@ -49,12 +52,6 @@
<div class="collapse navbar-collapse" id="navbarColor01">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<router-link class="nav-link active" to="{% url 'main:home' %}">
{% trans "K356" %}
<span class="visually-hidden"></span>
</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" to="/ItemView">{% trans "Items" %}</router-link>
</li>
@@ -32,36 +32,56 @@
<v-card-text>
<v-container>
<v-row>
<template v-for="field in editable_fields">
<template v-if="field.field_widget == 'v-select'">
<template v-if="field.choices">
<v-select
v-model="editedItem[field.value]"
:items="field.choices"
:label="field.text"
item-text="text"
item-value="value"
persistent-hint>
</v-select>
<v-form ref="form" v-model="valid">
<template v-for="field in editable_fields">
<template v-if="field.field_widget == 'v-select'">
<template v-if="field.choices">
<v-select
v-model="editedItem[field.value]"
:items="field.choices"
:label="field.text"
:rules="[rules.required]"
item-text="text"
item-value="value"
persistent-hint>
</v-select>
</template>
<template v-else>
<v-select
v-model="editedItem[field.value]"
:items="items_relations[field.value]"
:label="field.text"
:rules="[rules.required]"
item-text="name"
item-value="id"
return-object
persistent-hint>
<template slot="item" slot-scope="data">
<template v-if="data.item.custom_identifier">
[[ data.item.custom_identifier ]] - [[ data.item.name ]]
</template>
<template v-else>
[[ data.item.name ]]
</template>
</template>
</v-select>
</template>
</template>
<template v-else-if="field?.dynamic_field_type">
<DynField :item="editedItem" :field="field"></DynField>
</template>
<template v-else>
<v-select
v-model="editedItem[field.value]"
:items="items_relations[field.value]"
:label="field.text"
item-text="name"
item-value="id"
persistent-hint>
<template slot="item" slot-scope="data">
[[ data.item.name ]] - [[ data.item.custom_identifier ]]
</template>
</v-select>
<component :is="field.field_widget" v-model="editedItem[field.value]" :label="field.text"></component>
</template>
</template>
<template v-else>
<component :is="field.field_widget" v-model="editedItem[field.value]" :label="field.text"></component>
</template>
</template>
</v-form>
</v-row>
</v-container>
</v-card-text>
+40 -14
View File
@@ -3,24 +3,51 @@
template: "#{{ name }}",
router_path: "/{{ name }}",
delimiters: ["[[", "]]"],
props: ["crypto_key", "items", "items_headers", "items_relations", "group_by", "hidden_fields"],
props: {
items: Array,
items_headers: Array,
items_relations: {
default: function () {
return {}
},
},
group_by: String,
hidden_fields: {
default: function () {
return []
},
},
non_editable_fields: {
default: function () {
return []
},
},
show_item: {
default: "id",
},
show_url: {
default: "{{ show_url|default:'' }}",
},
},
data: function() {
return {
valid: false,
dialog: false,
dialogDelete: false,
editedIndex: -1,
defaultItem: {},
editedItem: {},
search: null,
show_url: "{{ show_url|default:'' }}",
default_hidden_fields: [{% for field in default_hidden_fields %}"{{ field }}",{% endfor %}]
default_hidden_fields: [{% for field in default_hidden_fields %}"{{ field }}",{% endfor %}],
rules: {
required: value => !!value || "{{_('Required') | escapejs}}",
},
}
},
computed: {
editable_fields: function() {
return this.items_headers.filter(e => e.editable)
return this.items_headers.filter(e => e.editable).filter(e => !this.non_editable_fields.includes(e.value))
},
citems_headers: function() {
@@ -44,23 +71,21 @@
},
showItem (item) {
this.$router.replace({ name: this.show_url, params: { id: item.id }})
this.$router.push({ name: this.show_url, params: { id: item[this.show_item] }})
},
editItem (item) {
this.editedIndex = this.items.indexOf(item)
this.editedItem = Object.assign({}, item)
this.dialog = true
},
deleteItem (item) {
this.editedIndex = this.items.indexOf(item)
this.editedItem = Object.assign({}, item)
this.dialogDelete = true
},
deleteItemConfirm () {
this.$emit("deleteItem", this.editedIndex)
this.$emit("deleteItem", this.editedItem)
this.closeDelete()
},
@@ -68,25 +93,26 @@
close () {
this.dialog = false
this.editedItem = Object.assign({}, this.defaultItem)
this.editedIndex = -1
},
closeDelete () {
this.dialogDelete = false
this.editedItem = Object.assign({}, this.defaultItem)
this.editedIndex = -1
},
save () {
if (this.editedIndex > -1) {
const isValid = this.$refs.form.validate()
if (!isValid) return
this.$emit("editItem", this.editedIndex, this.editedItem)
if (this.editedItem.id == null) {
this.$emit("createItem", this.editedItem)
} else {
this.$emit("createItem", this.editedItem)
this.$emit("editItem", this.editedItem)
}
-104
View File
@@ -1,51 +1,3 @@
async function init() {
const operations = crypto.subtle
const KEY_LEN = 256
const ENCRYPTION_ALGO = "AES-GCM"
const ENCRYPTION_USAGES = ["encrypt", "decrypt"]
const ENCRYPTION_PARAMS = { name: ENCRYPTION_ALGO, length: KEY_LEN }
const WRAPPING_ALGO = "AES-KW"
const WRAPPING_USAGES = ["wrapKey", "unwrapKey"]
const DERIVATION_ALGO = "ECDH"
const { publicKey, privateKey } = await operations.generateKey(
{
name: DERIVATION_ALGO,
namedCurve: "P-384",
},
false,
["derivekey"]
)
const encryptionKey = await operations.deriveKey(
{
name: DERIVATION_ALGO,
public: publicKey,
},
privateKey,
ENCRYPTION_PARAMS,
false,
["encrypt"]
)
const decryptionKey = await operations.deriveKey(
{ name: DERIVATION_ALGO, public: publicKey },
privateKey,
ENCRYPTION_PARAMS,
false,
['decrypt']
)
return { encryptionKey, decryptionKey }
}
function stringToArrayBuffer(str) {
var buf = new ArrayBuffer(str.length);
var bufView = new Uint8Array(buf);
@@ -64,59 +16,3 @@ function arrayBufferToString(str) {
}
return byteString;
}
async function aEncryptWithKey(key, data) {
// Encrypt data with key. Return a Promise
return new Promise((resolve) => {
window.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: stringToArrayBuffer(key.uuid),
},
key.key,
stringToArrayBuffer(data)
).then(text => {
resolve(btoa(arrayBufferToString(text)));
});
});
}
function encryptWithKey(key, data) {
// Encrypt data with key. Return a Promise
return new Promise((resolve) => {
window.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: stringToArrayBuffer(key.uuid),
},
key.key,
stringToArrayBuffer(data)
).then(text => {
resolve(btoa(arrayBufferToString(text)));
});
});
}
function decryptWithKey(key, data) {
// Decrypt data with key. Return a Promise
return new Promise((resolve) => {
window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: stringToArrayBuffer(key.uuid),
},
key.key,
stringToArrayBuffer(atob(data))
).then(text => {
resolve(arrayBufferToString(text));
});
})
}
+91 -49
View File
@@ -1,6 +1,7 @@
{% include "vue/plugins.js" %}
Vue.config.devtools = true
Vue.use(VueRouter)
Vue.use(Vuex)
Vue.use(EncryptionPlugin)
@@ -24,29 +25,13 @@ const routes = [
{% endfor %}
]
const encryptionStore = new Vuex.Store({
state: {
aes_key: null,
keyPair: null,
},
mutations: {
update_aes_key (state, key) {
state.aes_key = key
},
update_keyPair (state, keyPair) {
state.keyPair = keyPair
},
}
})
{% include "vue/stores.js" %}
const router = new VueRouter({routes})
const approuter = new Vue({
router,
vuetify: new Vuetify(),
store: encryptionStore,
store: store,
el: "#main",
data: {
uuid: "{{ user_settings.id }}",
@@ -54,7 +39,7 @@ const approuter = new Vue({
computed: {
locked: function() {
return this.$store.state.aes_key == null || this.$store.state.keyPair?.privateKey == null
return this.$store.state.encryption.aes_key == null || this.$store.state.encryption.keyPair?.privateKey == null
}
},
@@ -62,56 +47,113 @@ const approuter = new Vue({
methods: {
async load_keys (aes_key) {
const response = await this.$http.get(Urls["users:keys"]())
try {
const iv_private = `${this.uuid}--private`
const iv_public = `${this.uuid}--public`
const response = await this.$http.get(Urls["users:keys"]())
if (response.data.privateKey != null) {
const iv_private = `${this.uuid}--private`
const iv_public = `${this.uuid}--public`
if (response.data.privateKey != null) {
const keyPair = {
privateKey: await this.unwrapKey(aes_key, response.data.privateKey, iv_private, ["decrypt"]),
publicKey: await this.unwrapKey(aes_key, response.data.publicKey, iv_public, ["encrypt"]),
}
this.$store.commit('encryption/updateKeyPair', keyPair)
Swal.fire({title: "Successfully loaded K356!", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000});
} else {
const keyPair = await this.generateKeyPair()
await this.$http.post(
Urls["users:keys"](),
{
privateKey: await this.wrapKey(this.keyPair.privateKey, aes_key, iv_private),
publicKey: await this.wrapKey(this.keyPair.publicKey, aes_key, iv_public),
}
)
this.$store.commit('encryption/updateKeyPair', keyPair)
Swal.fire({title: "Successfully created K356!", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000});
const keyPair = {
privateKey: await this.unwrapKey(aes_key, response.data.privateKey, iv_private, ["decrypt"]),
publicKey: await this.unwrapKey(aes_key, response.data.publicKey, iv_public, ["encrypt"]),
}
this.$store.commit('update_keyPair', keyPair)
this.load_items()
this.load_properties()
this.load_relations()
Swal.fire({title: "Successfully loaded K356!", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000});
} catch (err) {
} else {
const keyPair = await this.generateKeyPair()
await this.$http.post(
Urls["users:keys"](),
{
privateKey: await this.wrapKey(this.keyPair.privateKey, aes_key, iv_private),
publicKey: await this.wrapKey(this.keyPair.publicKey, aes_key, iv_public),
}
)
this.$store.commit('update_keyPair', keyPair)
Swal.fire({title: "Successfully created K356!", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000});
Swal.fire({title: "{{_('Error during unwrapping of private key.<br><br>Maybe your password is wrong?') | escapejs}}", icon: "error", showConfirmButton: false})
}
},
update_key: function(key) {
this.$store.commit('update_aes_key', key)
this.$store.commit('encryption/updateAesKey', key)
this.load_keys(key)
},
lock_me: function() {
this.$store.commit('update_keyPair', null)
this.$store.commit('update_aes_key', null)
}
this.$store.commit('encryption/updateKeyPair', null)
this.$store.commit('encryption/updateAesKey', null)
// Lock all stores
this.$store.commit('encryption/lock')
this.$store.commit('items/lock')
this.$store.commit('types/lock')
this.$store.commit('relations/lock')
this.$store.commit('properties/lock')
this.$store.commit('linkedProperties/lock')
this.$store.commit('relationProperties/lock')
},
async load_items () {
const response = await this.$http.get(Urls["items:list"]())
this.$store.state.items.headers = response.data.result.items_headers
this.$store.state.types.headers = response.data.result.types_headers
this.$store.dispatch("items/setItems", { self: this, items: response.data.result.items })
this.$store.dispatch("types/setItems", { self: this, items: response.data.result.types })
},
async load_properties () {
const response = await this.$http.get(Urls["items:property.list"]())
this.$store.state.properties.headers = response.data.result.properties_headers
this.$store.state.linkedProperties.headers = response.data.result.linked_properties_headers
this.$store.state.relationProperties.headers = response.data.result.relation_properties_headers
this.$store.dispatch("properties/setItems", { self: this, items: response.data.result.properties })
this.$store.dispatch("linkedProperties/setItems", { self: this, items: response.data.result.linked_properties })
this.$store.dispatch("relationProperties/setItems", { self: this, items: response.data.result.relation_properties })
},
async load_relations () {
const response = await this.$http.get(Urls["items:relation.list"]())
this.$store.state.relations.headers = response.data.result.headers
this.$store.dispatch("relations/setItems", { self: this, items: response.data.result.relations })
},
}
})
router.beforeEach((to, from, next) => {
// Prevent from routing if key is not present.
// Prevent from routing when locked or loading.
next(!approuter.locked)
})
+38 -2
View File
@@ -105,7 +105,7 @@ const EncryptionPlugin = {
return btoa(arrayBufferToString(await operations.encrypt(
{ name: "RSA-OAEP" },
this.$store.state.keyPair.publicKey,
this.$store.state.encryption.keyPair.publicKey,
stringToArrayBuffer(data),
)))
@@ -115,7 +115,7 @@ const EncryptionPlugin = {
return arrayBufferToString(await operations.decrypt(
{ name: "RSA-OAEP" },
this.$store.state.keyPair.privateKey,
this.$store.state.encryption.keyPair.privateKey,
stringToArrayBuffer(atob(armored_data))
))
@@ -153,6 +153,42 @@ const EncryptionPlugin = {
}))
return newobj
},
Vue.prototype.object_edit = async function (url_edit, url_create, type, method, obj) {
let url = null
if (obj.id == undefined || obj.id == null) {
url = Urls[url_create]()
} else {
url = Urls[url_edit](obj.id)
}
const efields = this.$store.getters[`${type}/encryptedFields`]
try {
const newobj = await this.encryptObject(efields, obj)
const response = await this.$http[method](url, newobj)
if (method != "delete") {
return await this.decryptObject(efields, response.data.object)
}
} catch (err) {
let msg = "{{_('Error during edition') | escapejs}}"
if (method == "delete") {
msg = "{{_('Error during deletion') | escapejs}}"
}
Swal.fire({title: msg, icon: "error", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
throw err
}
}
}
+108
View File
@@ -0,0 +1,108 @@
const encryptionStore = {
namespaced: true,
state: () => (
{
aes_key: null,
keyPair: null,
}
),
mutations: {
lock (state) {
state.aes_key = null
state.keyPair = null
},
updateAesKey (state, key) {
state.aes_key = key
},
updateKeyPair (state, keyPair) {
state.keyPair = keyPair
},
},
}
const storeMixin = {
state: () => (
{
items: [],
headers: [],
}
),
mutations: {
lock (state) {
state.items = []
},
addItem (state, item) {
state.items.push(item)
},
removeItem (state, id) {
state.items = state.items.filter(i => i.id != id)
Swal.fire({title: "{{_('Successfully deleted!') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
},
editItem (state, item_edited) {
state.items = state.items.map(i => {
if (i.id == item_edited.id) {
return item_edited
}
return i
})
Swal.fire({title: "{{_('Successfully edited') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
},
},
getters: {
encryptedFields (state) {
return state.headers.filter(e => e.encrypted).map(e => e.value)
},
getById: (state) => (id) => {
return state.items.find(e => e.id == id)
},
},
actions: {
async setItems ({ commit, getters }, payload) {
return payload.items.map(async item => {
const new_item = await payload.self.decryptObject(getters.encryptedFields, item)
return await commit('addItem', new_item)
})
},
async addItem ({ getters, commit }, payload) {
const new_item = await payload.self.decryptObject(getters.encryptedFields, payload.item)
return await commit('addItem', new_item)
},
}
}
const itemStore = {__proto__: storeMixin, namespaced: true}
const typeStore = {__proto__: storeMixin, namespaced: true}
const relationStore = {__proto__: storeMixin, namespaced: true}
const propertyStore = {__proto__: storeMixin, namespaced: true}
const linkedPropertyStore = {__proto__: storeMixin, namespaced: true}
const relationPropertyStore = {__proto__: storeMixin, namespaced: true}
const store = new Vuex.Store({
modules: {
encryption: encryptionStore,
items: itemStore,
types: typeStore,
relations: relationStore,
properties: propertyStore,
linkedProperties: linkedPropertyStore,
relationProperties: relationPropertyStore,
}
})