This commit is contained in:
Loïc Gremaud 2024-09-28 17:37:29 +02:00
parent 8dfafa9404
commit fb4d566007
Signed by: Legrems
GPG Key ID: D4620E6DF3E0121D
64 changed files with 32325 additions and 573 deletions

View File

@ -10,11 +10,13 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/ https://docs.djangoproject.com/en/5.1/ref/settings/
""" """
import os
from pathlib import Path
from django.utils.module_loading import import_module from django.utils.module_loading import import_module
from pathlib import Path
import os
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
@ -46,23 +48,6 @@ INSTALLED_APPS = [
"items", "items",
] ]
BOWER_COMPONENT_ROOT = os.path.join(BASE_DIR, "components")
BOWER_INSTALLED_APPS = [
"https://code.jquery.com/jquery-3.7.1.min.js",
"https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js",
"https://cdn.jsdelivr.net/npm/sweetalert2@11",
"https://cdn.jsdelivr.net/npm/vue-resource@1.5.3",
"https://cdn.jsdelivr.net/npm/js-cookie@3.0.5/dist/js.cookie.min.js",
"https://unpkg.com/vue-router@3/dist/vue-router.js",
"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css",
"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js",
"https://cdn.jsdelivr.net/npm/bootswatch@5.3.3/dist/lux/bootstrap.min.css",
"https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css",
"https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js",
"https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css",
"https://fonts.cdnfonts.com/css/jetbrains-mono",
]
STATIC_URL = "/static/" STATIC_URL = "/static/"
STATICFILES_DIRS = (os.path.join(BASE_DIR, "static_source"),) STATICFILES_DIRS = (os.path.join(BASE_DIR, "static_source"),)
@ -78,11 +63,29 @@ STATIC_ROOT = os.path.join(BASE_DIR, "static")
STORAGES = { STORAGES = {
"staticfiles": { "staticfiles": {
# "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
"BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage", # "BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
} }
} }
BOWER_COMPONENT_ROOT = os.path.join(BASE_DIR, "components")
BOWER_INSTALLED_APPS = [
"https://code.jquery.com/jquery-3.7.1.min.js",
"https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js",
"https://cdn.jsdelivr.net/npm/sweetalert2",
"https://cdn.jsdelivr.net/npm/vue-resource",
"https://unpkg.com/vuex@3.6.2/dist/vuex.js",
"https://cdn.jsdelivr.net/npm/js-cookie@3.0.5/dist/js.cookie.min.js",
"https://unpkg.com/vue-router@3/dist/vue-router.js",
"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css",
"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js",
"bootswatch.min.css=https://cdn.jsdelivr.net/npm/bootswatch@5.3.3/dist/lux/bootstrap.min.css",
"https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css",
"https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js",
"https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css",
"https://fonts.cdnfonts.com/css/jetbrains-mono",
]
MIDDLEWARE = [ MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
@ -161,6 +164,7 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
from app.settingsLocal import * from app.settingsLocal import *
for extra_app in EXTRA_APPS: for extra_app in EXTRA_APPS:
INSTALLED_APPS.append(extra_app) INSTALLED_APPS.append(extra_app)

16
k356/app/utils/helpers.py Normal file
View File

@ -0,0 +1,16 @@
from functools import reduce
def recursive_getattr(obj, attr, *args, delimiter="__"):
"""Recursive getattr on a object, based on the attr name, delimited with the delimiter."""
def _getattr(obj, attr):
rg = getattr(obj, attr, *args)
# Usefull for getting attribute like `datetime__date` => will result in datetime.date()
if callable(rg):
return rg()
return rg
return reduce(_getattr, [obj] + attr.split(delimiter))

View File

@ -1,9 +1,14 @@
from uuid import uuid4
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import models from django.db import models
from django.db.models.fields.related import RelatedField from django.db.models.fields.related import RelatedField
from app.utils.helpers import recursive_getattr
from uuid import uuid4
User = get_user_model() User = get_user_model()
@ -17,9 +22,9 @@ class BaseQuerySet(models.QuerySet):
"text": field.verbose_name.capitalize(), "text": field.verbose_name.capitalize(),
"align": "", "align": "",
"encrypted": field.name in self.model.Encryption.fields, "encrypted": field.name in self.model.Encryption.fields,
"editable": field.name "editable": field.name not in self.model.Serialization.excluded_fields_edit,
not in self.model.Serialization.excluded_fields_edit,
"field_widget": "v-textarea", "field_widget": "v-textarea",
"choices": None,
} }
} }
@ -41,6 +46,19 @@ class BaseQuerySet(models.QuerySet):
field_widget="v-select", field_widget="v-select",
) )
if field.choices:
ret[field.name].update(
text="",
field_widget="v-select",
choices=[
{
"value": c[0],
"text": c[1],
}
for c in field.choices
],
)
return ret return ret
fields = {} fields = {}
@ -78,12 +96,25 @@ class BaseModel(models.Model):
class Encryption: class Encryption:
fields = ["name", "description", "custom_identifier"] fields = ["name", "description", "custom_identifier"]
def serialize(self):
ret = {}
for field_name in self._meta.model.objects.headers().keys():
value = recursive_getattr(self, field_name)
if isinstance(value, BaseModel):
value = value.id
ret[field_name] = value
return ret
id = models.UUIDField(primary_key=True, default=uuid4, editable=False) id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
name = models.TextField(max_length=2048) name = models.TextField(max_length=2048, blank=True, null=True)
description = models.TextField(max_length=2048) description = models.TextField(max_length=2048, blank=True, null=True)
custom_identifier = models.TextField(max_length=2048, null=True) custom_identifier = models.TextField(max_length=2048, null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
last_modified_at = models.DateTimeField(auto_now=True) last_modified_at = models.DateTimeField(auto_now=True)

View File

@ -0,0 +1,93 @@
# Generated by Django 5.1.1 on 2024-09-28 07:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("items", "0002_item_custom_identifier_and_more"),
]
operations = [
migrations.AlterField(
model_name="item",
name="custom_identifier",
field=models.TextField(blank=True, max_length=2048, null=True),
),
migrations.AlterField(
model_name="itemrelation",
name="custom_identifier",
field=models.TextField(blank=True, max_length=2048, null=True),
),
migrations.AlterField(
model_name="itemrelation",
name="description",
field=models.TextField(blank=True, max_length=2048, null=True),
),
migrations.AlterField(
model_name="itemrelation",
name="name",
field=models.TextField(blank=True, max_length=2048, null=True),
),
migrations.AlterField(
model_name="itemtype",
name="custom_identifier",
field=models.TextField(blank=True, max_length=2048, null=True),
),
migrations.AlterField(
model_name="itemtype",
name="description",
field=models.TextField(blank=True, max_length=2048, null=True),
),
migrations.AlterField(
model_name="itemtype",
name="name",
field=models.TextField(blank=True, max_length=2048, null=True),
),
migrations.AlterField(
model_name="linkedproperty",
name="custom_identifier",
field=models.TextField(blank=True, max_length=2048, null=True),
),
migrations.AlterField(
model_name="linkedproperty",
name="description",
field=models.TextField(blank=True, max_length=2048, null=True),
),
migrations.AlterField(
model_name="linkedproperty",
name="name",
field=models.TextField(blank=True, max_length=2048, null=True),
),
migrations.AlterField(
model_name="property",
name="custom_identifier",
field=models.TextField(blank=True, max_length=2048, null=True),
),
migrations.AlterField(
model_name="property",
name="description",
field=models.TextField(blank=True, max_length=2048, null=True),
),
migrations.AlterField(
model_name="property",
name="name",
field=models.TextField(blank=True, max_length=2048, null=True),
),
migrations.AlterField(
model_name="relationproperty",
name="custom_identifier",
field=models.TextField(blank=True, max_length=2048, null=True),
),
migrations.AlterField(
model_name="relationproperty",
name="description",
field=models.TextField(blank=True, max_length=2048, null=True),
),
migrations.AlterField(
model_name="relationproperty",
name="name",
field=models.TextField(blank=True, max_length=2048, null=True),
),
]

View File

@ -0,0 +1,35 @@
# Generated by Django 5.1.1 on 2024-09-28 11:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("items", "0003_alter_item_custom_identifier_and_more"),
]
operations = [
migrations.AlterField(
model_name="property",
name="type",
field=models.CharField(
choices=[
("text", "Text"),
("date", "Date"),
("datetime", "Date & time"),
("time", "Time"),
("duration", "Duration"),
("uuid", "UUID"),
("number", "Number"),
("float", "Float"),
("boolean", "Boolean"),
("email", "Email"),
("ip", "IP address"),
("json", "JSON"),
],
default="text",
max_length=32,
),
),
]

View File

@ -1,6 +1,8 @@
from app.utils.models import BaseModel
from django.contrib.auth.forms import gettext as _ from django.contrib.auth.forms import gettext as _
from django.db import models from django.db import models
from app.utils.models import BaseModel
from users.models import UserSettings from users.models import UserSettings
@ -16,21 +18,14 @@ class ItemBase(BaseModel):
class ItemType(ItemBase): class ItemType(ItemBase):
name = models.TextField(max_length=2048) pass
description = models.TextField(max_length=2048)
class ItemRelation(ItemBase): class ItemRelation(ItemBase):
parent = models.ForeignKey( parent = models.ForeignKey("items.Item", on_delete=models.CASCADE, related_name="children")
"items.Item", on_delete=models.CASCADE, related_name="children" child = models.ForeignKey("items.Item", on_delete=models.CASCADE, related_name="parents")
)
child = models.ForeignKey(
"items.Item", on_delete=models.CASCADE, related_name="parents"
)
properties = models.ManyToManyField( properties = models.ManyToManyField("items.Property", through="items.RelationProperty", related_name="relations")
"items.Property", through="items.RelationProperty", related_name="relations"
)
class Item(ItemBase): class Item(ItemBase):
@ -54,13 +49,21 @@ class PropertyType(models.TextChoices):
TEXT = "text", _("Text") TEXT = "text", _("Text")
DATE = "date", _("Date") DATE = "date", _("Date")
DATETIME = "datetime", _("Date & time") DATETIME = "datetime", _("Date & time")
TIME = "time", _("Time")
DURATION = "duration", _("Duration")
UUID = "uuid", _("UUID")
NUMBER = "number", _("Number")
FLOAT = "float", _("Float")
BOOLEAN = "boolean", _("Boolean")
EMAIL = "email", _("Email")
IP = "ip", _("IP address")
JSON = "json", _("JSON")
# TODO: Add more property types (location, etc) # TODO: Add more property types (location, etc)
class Property(ItemBase): class Property(ItemBase):
type = models.CharField( type = models.CharField(max_length=32, choices=PropertyType.choices, default=PropertyType.TEXT)
max_length=32, choices=PropertyType.choices, default=PropertyType.TEXT
)
class BaseLinkedProperty(ItemBase): class BaseLinkedProperty(ItemBase):
@ -74,9 +77,7 @@ class BaseLinkedProperty(ItemBase):
class LinkedProperty(BaseLinkedProperty): class LinkedProperty(BaseLinkedProperty):
item = models.ForeignKey( item = models.ForeignKey(Item, on_delete=models.CASCADE, null=True, related_name="linked_properties")
Item, on_delete=models.CASCADE, null=True, related_name="linked_properties"
)
class RelationProperty(BaseLinkedProperty): class RelationProperty(BaseLinkedProperty):

View File

@ -0,0 +1,51 @@
{% load i18n %}
<div>
<div class="card mt-4 pt-2 ps-lg-2">
<h5 class="card-header">{% trans "Properties" %} [[ this.$route.params.id ]]</h5>
<div class="card-body">
<PropertyList
:items="properties"
:items_headers="properties_headers"
:items_relations="{}"
:hidden_fields="[]"
group_by="type"
@deleteItem="deleteItem"
@createItem="createItem"
@editItem="editItem"
></PropertyList>
</div>
</div>
<div class="card mt-4 pt-2 ps-lg-2">
<h5 class="card-header">{% trans "Children" %}</h5>
<div class="card-body">
<ItemRelationList
:items="children"
:items_headers="children_headers"
:items_relations="{}"
:hidden_fields="['parent__name']"
group_by="type__name"
@deleteItem="deleteItem"
@createItem="createItem"
@editItem="editItem"
></ItemRelationList>
</div>
</div>
<div class="card mt-4 pt-2 ps-lg-2">
<h5 class="card-header">{% trans "Parents" %}</h5>
<div class="card-body">
<ItemRelationList
:items="parents"
:items_headers="parents_headers"
:items_relations="{}"
:hidden_fields="['child__name']"
group_by="type__name"
@deleteItem="deleteItem"
@createItem="createItem"
@editItem="editItem"
></ItemRelationList>
</div>
</div>
</div>

View File

@ -0,0 +1,97 @@
ItemDetail = {
template: "#ItemDetail",
router_path: "/ItemDetail/:id",
delimiters: ["[[", "]]"],
data: function() {
return {
properties: [],
linked_properties: [],
children: [],
parents: [],
// TODO: Also remove this tedious things
properties_headers: [],
linked_properties_headers: [],
children_headers: [],
parents_headers: [],
}
},
computed: {
// TODO: Remove this by a generic things at some points, this become tedious and repetitive
properties_efields: function() {
return this.properties_headers.filter(e => e.encrypted).map(e => e.value)
},
linked_properties_efields: function() {
return this.linked_properties_headers.filter(e => e.encrypted).map(e => e.value)
},
children_efields: function() {
return this.children_headers.filter(e => e.encrypted).map(e => e.value)
},
parents_efields: function() {
return this.parents_headers.filter(e => e.encrypted).map(e => e.value)
},
},
mounted: function() {
this.reload()
},
methods: {
async reload () {
try {
const response = await this.$http.get(Urls["items:details"](this.$route.params.id))
this.properties_headers = response.data.properties_headers
this.linked_properties_headers = response.data.linked_properties_headers
this.children_headers = response.data.children_headers
this.parents_headers = response.data.parents_headers
// TODO: TEDIOUUUUUS
response.data.parents.forEach(async e => {
this.parents.push(await this.decryptObject(this.parents_efields, e))
})
response.data.children.forEach(async e => {
this.children.push(await this.decryptObject(this.children_efields, e))
})
response.data.properties.forEach(async e => {
this.properties.push(await this.decryptObject(this.properties_efields, e))
})
response.data.linked_properties.forEach(async e => {
this.linked_properties.push(await this.decryptObject(this.linked_properties_efields, e))
})
} catch (err) {
Swal.fire({title: "{{_('Error during loading of items') | escapejs}}", icon: "error", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
throw err
}
},
async deleteItem () {
},
async createItem () {
},
async editItem() {
},
}
}

View File

@ -3,5 +3,6 @@
{% block component %} {% block component %}
{% define 'items' 'items' %} {% define 'items' 'items' %}
{% define 'show_url' 'ItemDetail' %}
{{ block.super }} {{ block.super }}
{% endblock %} {% endblock %}

View File

@ -0,0 +1 @@
<div>[[ this.$route.params.id ]]</div>

View File

@ -0,0 +1,26 @@
ItemRelationDetail = {
template: "#ItemRelationDetail",
router_path: "/ItemRelationDetail/:id",
delimiters: ["[[", "]]"],
data: function() {
return {
data: null
}
},
mounted: function() {
this.reload()
},
methods: {
async reload () {
const response = await this.$http.get(Urls["items:relation.details"](this.$route.params.id))
}
}
}

View File

@ -0,0 +1 @@
{% extends "base_components/glist/template.html" %}

View File

@ -0,0 +1,9 @@
{% extends "base_components/glist/vue.js" %}
{% load main %}
{% block component %}
{% define 'items' 'items' %}
{% define 'default_hidden_fields' 'name' 'description' %}
{% define 'show_url' 'ItemRelationDetail' %}
{{ block.super }}
{% endblock %}

View File

@ -6,10 +6,10 @@
<div class="card-body"> <div class="card-body">
<ItemList <ItemList
:crypto_key="crypto_key"
:items="items" :items="items"
:items_headers="items_headers" :items_headers="items_headers"
:items_relations="{'type': types}" :items_relations="{'type': types}"
:hidden_fields="[]"
group_by="type__name" group_by="type__name"
@deleteItem="deleteItem" @deleteItem="deleteItem"
@createItem="createItem" @createItem="createItem"
@ -24,10 +24,10 @@
<div class="card-body"> <div class="card-body">
<ItemList <ItemList
:crypto_key="crypto_key"
:items="types" :items="types"
:items_headers="types_headers" :items_headers="types_headers"
:items_relations="{}" :items_relations="{}"
:hidden_fields="[]"
group_by="[]" group_by="[]"
@deleteItem="deleteType" @deleteItem="deleteType"
@createItem="createType" @createItem="createType"

View File

@ -1,17 +1,15 @@
ItemView = { ItemView = {
template: "#ItemView", template: "#ItemView",
router_path: "/ItemView",
delimiters: ["[[", "]]"], delimiters: ["[[", "]]"],
props: ["crypto_key"], props: [],
data: function() { data: function() {
return { return {
search: "",
items: [], items: [],
items_headers: [], items_headers: [],
items_relations: [],
types: [], types: [],
types_headers: [], types_headers: [],
types_relations: [],
} }
}, },
@ -20,158 +18,67 @@ ItemView = {
}, },
computed: { computed: {
items_encrypted_fields: function() { items_efields: function() {
return this.items_headers.filter(e => e.encrypted).map(e => e.value) return this.items_headers.filter(e => e.encrypted).map(e => e.value)
}, },
types_encrypted_fields: function() { types_efields: function() {
return this.types_headers.filter(e => e.encrypted).map(e => e.value) return this.types_headers.filter(e => e.encrypted).map(e => e.value)
}, },
}, },
methods: { methods: {
reload () { async reload () {
var self = this try {
const response = await this.$http.get(Urls["items:list"]())
this.$http.get(Urls["items:list"]()).then(response => { this.items_headers = response.data.result.items_headers
this.types_headers = response.data.result.types_headers
Object.keys(response.data.result).forEach(name => { // Decrypt all item the push
self.$set(self, name, response.data.result[name]) response.data.result.items.forEach(async item => {
const new_item = await this.decryptObject(this.items_efields, item)
this.items.push(new_item)
}) })
self.items.forEach(item => { // Decrypt all type the push
self.decryptObject(self.items_encrypted_fields, item) response.data.result.types.forEach(async type => {
const new_type = await this.decryptObject(this.types_efields, type)
this.types.push(new_type)
}) })
self.types.forEach(type => { } catch (err) {
self.decryptObject(self.types_encrypted_fields, type)
})
}).catch(err => {
Swal.fire({title: "{{_('Error during loading of items') | escapejs}}", icon: "error", position:"top-end", showConfirmButton: false, toast: true, timer: 1000}) Swal.fire({title: "{{_('Error during loading of items') | escapejs}}", icon: "error", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
})
},
async aDecryptObject (encrypted_fields, obj) {
var self = this
return new Promise((resolve) => {
let promises = encrypted_fields.map(field => {
// Encrypt all necessary fields
if (obj[field] == null) {
return null
} }
return new Promise((resolve) => {
return decryptWithKey(self.crypto_key, obj[field]).then(dec => {
resolve({field: field, value: dec})
})
})
}).filter(e => e != null)
Promise.all(promises).then(values => {
values.forEach(value => {
obj[value.field] = value.value
})
resolve(obj)
})
})
}, },
decryptObject (encrypted_fields, obj) { async object_edit(url_edit, url_create, encrypted_fields, method, obj) {
var self = this let url = null
return new Promise((resolve) => {
let promises = encrypted_fields.map(field => {
// Encrypt all necessary fields
if (obj[field] == null) {
return null
}
return new Promise((resolve) => {
return decryptWithKey(self.crypto_key, obj[field]).then(dec => {
resolve({field: field, value: dec})
})
})
}).filter(e => e != null)
Promise.all(promises).then(values => {
values.forEach(value => {
obj[value.field] = value.value
})
resolve(obj)
})
})
},
object_edition (url_edit, url_create, encrypted_fields, method, obj) {
// Return a Promise
var self = this
return new Promise((resolve) => {
let url = Urls[url_edit](obj.id)
if (obj.id == undefined || obj.id == null) { if (obj.id == undefined || obj.id == null) {
url = Urls[url_create]() url = Urls[url_create]()
}
let promises = encrypted_fields.map(field => {
// Encrypt all necessary fields
if (obj[field] == null) {
return null
}
return new Promise((resolve) => {
return encryptWithKey(self.crypto_key, obj[field]).then(enc => {
resolve({field: field, value: enc})
})
})
}).filter(e => e != null)
Promise.all(promises).then(values => {
values.forEach(value => {
obj[value.field] = value.value
})
self.$http[method](url, obj).then(response => {
if (method == "delete") {
resolve()
} else { } else {
url = Urls[url_edit](obj.id)
self.decryptObject(encrypted_fields, response.data.object).then(new_obj => {
resolve(new_obj)
})
} }
}).catch(err => { try {
const newobj = await this.encryptObject(encrypted_fields, obj)
const response = await this.$http[method](url, newobj)
if (method != "delete") {
return await this.decryptObject(encrypted_fields, response.data.object)
}
} catch (err) {
let msg = "{{_('Error during edition') | escapejs}}" let msg = "{{_('Error during edition') | escapejs}}"
if (method == "delete") { if (method == "delete") {
@ -180,102 +87,130 @@ ItemView = {
Swal.fire({title: msg, icon: "error", position:"top-end", showConfirmButton: false, toast: true, timer: 1000}) Swal.fire({title: msg, icon: "error", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
}) throw err
}
})
})
}, },
item_edition (method, item) { item_edition (method, item) {
return this.object_edition("items:edit", "items:create", this.items_encrypted_fields, method, item) return this.object_edit("items:edit", "items:create", this.items_efields, method, item)
}, },
type_edition (method, item) { type_edition (method, item) {
return this.object_edition("items:type.edit", "items:type.create", this.items_encrypted_fields, method, item) return this.object_edit("items:type.edit", "items:type.create", this.types_efields, method, item)
}, },
createItem (item) { async createItem (item) {
var self = this try {
this.item_edition("post", item).then(new_item => { const new_item = await this.item_edition("post", item)
this.items.push(new_item)
self.items.push(new_item)
Swal.fire({title: "{{_('Item successfully created!') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000}) Swal.fire({title: "{{_('Item successfully created!') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
}) } catch (err) {
}
}, },
editItem (index, item) { async editItem (index, item) {
var self = this try {
this.item_edition("post", item).then(new_item => { // Remove the item
this.items.splice(index, 1)
// Remove the 'current' (non edited) item from the list const new_item = await this.item_edition("post", item)
self.items.splice(index, 1)
self.items.push(new_item) // Add the new item
this.items.push(new_item)
Swal.fire({title: "{{_('Item successfully edited') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000}) Swal.fire({title: "{{_('Item successfully edited') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
}) } catch (err) {
this.items.push(item)
}
}, },
deleteItem (index) { async deleteItem (index) {
var self = this
var item = this.items[index] var item = this.items[index]
this.item_edition("delete", item).then(() => { try {
self.items.splice(this.items.indexOf(item), 1)
// Remove the item
this.items.splice(index, 1)
await this.item_edition("delete", item)
Swal.fire({title: "{{_('Item successfully deleted!') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000}) Swal.fire({title: "{{_('Item successfully deleted!') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
}) } catch (err) {
this.items.push(item)
}
}, },
createType (type) { async createType (type) {
var self = this try {
this.type_edition("post", type).then(new_type => { const new_type = await this.type_edition("post", type)
this.types.push(new_type)
self.types.push(new_type)
Swal.fire({title: "{{_('Type successfully created!') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000}) Swal.fire({title: "{{_('Type successfully created!') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
}) } catch (err) {
}
}, },
editType (index, type) { async editType (index, type) {
var self = this try {
this.type_edition("post", type).then(new_type => { this.types.splice(index, 1)
// Remove the 'current' (non edited) item from the list const new_type = await this.type_edition("post", type)
self.types.splice(index, 1)
self.types.push(new_type) this.types.push(new_type)
Swal.fire({title: "{{_('Type successfully edited') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000}) Swal.fire({title: "{{_('Type successfully edited') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
}) } catch (err) {
this.types.push(type)
}
}, },
deleteType (index) { async deleteType (index) {
var self = this
var type = this.types[index] var type = this.types[index]
this.type_edition("delete", type).then(() => { try {
self.types.splice(this.types.indexOf(type), 1)
// Remove the type
this.types.splice(index, 1)
await this.type_edition("delete", type)
Swal.fire({title: "{{_('Type successfully deleted!') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000}) Swal.fire({title: "{{_('Type successfully deleted!') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
}) } catch (err) {
this.types.push(type)
}
}, },
}, },

View File

@ -0,0 +1 @@
<div>PropertyDetail</div>

View File

@ -0,0 +1,4 @@
PropertyDetail = {
template: "#PropertyDetail",
router_path: "/PropertyDetail/:id",
},

View File

@ -0,0 +1 @@
{% extends "base_components/glist/template.html" %}

View File

@ -0,0 +1,8 @@
{% extends "base_components/glist/vue.js" %}
{% load main %}
{% block component %}
{% define 'items' 'items' %}
{% define 'show_url' 'PropertyDetail' %}
{{ block.super }}
{% endblock %}

View File

@ -0,0 +1,22 @@
{% load i18n %}
<div>
<div class="card mt-4 pt-2 ps-lg-2">
<h5 class="card-header">{% trans "Your properties" %}</h5>
<div class="card-body">
<PropertyList
:items="properties"
:items_headers="properties_headers"
:items_relations="{}"
:hidden_fields="[]"
group_by="type"
@deleteItem="deleteItem"
@createItem="createItem"
@editItem="editItem"
></PropertyList>
</div>
</div>
</div>

View File

@ -0,0 +1,183 @@
PropertyView = {
template: "#PropertyView",
router_path: "/PropertyView",
delimiters: ["[[", "]]"],
props: [],
data: function() {
return {
properties: [],
properties_headers: [],
}
},
mounted: function() {
this.reload()
},
computed: {
properties_encrypted_fields: function() {
return this.properties_headers.filter(e => e.encrypted).map(e => e.value)
},
properties_relations: function() {
return this.properties_headers.filter(e => e.choices != null)
},
},
methods: {
async reload () {
try {
const response = await this.$http.get(Urls["items:property.list"]())
this.properties_headers = response.data.result.properties_headers
// Decrypt all item the push
response.data.result.properties.forEach(async item => {
const new_item = await this.decryptObject(this.properties_encrypted_fields, item)
this.properties.push(new_item)
})
} catch (err) {
Swal.fire({title: "{{_('Error during loading of properties') | escapejs}}", icon: "error", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
throw err
}
},
async decryptObject (encrypted_fields, obj) {
// Decrypt all fields and return a new object
var newobj = {}
await Promise.all(Object.keys(obj).map(async field => {
if (encrypted_fields.includes(field) && obj[field] != null) {
newobj[field] = await this.decrypt(obj[field])
} else {
newobj[field] = obj[field]
}
}))
return newobj
},
async encryptObject (encrypted_fields, obj) {
// Encrypt all fields and return a new object
var newobj = {}
await Promise.all(Object.keys(obj).map(async field => {
if (encrypted_fields.includes(field) && obj[field] != null) {
newobj[field] = await this.encrypt(obj[field])
} else {
newobj[field] = obj[field]
}
}))
return newobj
},
async object_edit(url_edit, url_create, encrypted_fields, method, obj) {
let url = null
if (obj.id == undefined || obj.id == null) {
url = Urls[url_create]()
} else {
url = Urls[url_edit](obj.id)
}
try {
const newobj = await this.encryptObject(encrypted_fields, obj)
const response = await this.$http[method](url, newobj)
if (method != "delete") {
return await this.decryptObject(encrypted_fields, 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
}
},
property_edition (method, item) {
return this.object_edit("items:property.edit", "items:property.create", this.properties_encrypted_fields, method, item)
},
async createItem (item) {
try {
const new_item = await this.property_edition("post", item)
this.properties.push(new_item)
Swal.fire({title: "{{_('Item successfully created!') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
} catch (err) {
}
},
async editItem (index, item) {
try {
// Remove the item
this.properties.splice(index, 1)
const new_item = await this.property_edition("post", item)
// Add the new item
this.properties.push(new_item)
Swal.fire({title: "{{_('Item successfully edited') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
} catch (err) {
this.properties.push(item)
}
},
async deleteItem (index) {
var item = this.properties[index]
try {
// Remove the item
this.properties.splice(index, 1)
await this.property_edition("delete", item)
Swal.fire({title: "{{_('Item successfully deleted!') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
} catch (err) {
this.properties.push(item)
}
},
},
}

View File

@ -1,12 +1,27 @@
from django.urls import path from django.urls import path
from items.views import item_view, type_view
from items.views import item_view, property_view, relation_view, type_view
app_name = "items" app_name = "items"
urlpatterns = [ urlpatterns = [
path("", item_view.item_list, name="list"), # Item
path("list", item_view.item_list, name="list"),
path("<uuid:id>", item_view.item_edit, name="edit"), path("<uuid:id>", item_view.item_edit, name="edit"),
path("<uuid:id>/details", item_view.item_details, name="details"),
path("create", item_view.item_edit, {"id": None}, name="create"), path("create", item_view.item_edit, {"id": None}, name="create"),
# Type
path("type/<uuid:id>", type_view.type_edit, name="type.edit"), path("type/<uuid:id>", type_view.type_edit, name="type.edit"),
path("type/create", type_view.type_edit, {"id": None}, name="type.create"), path("type/create", type_view.type_edit, {"id": None}, name="type.create"),
# Property
path("property/list", property_view.property_list, name="property.list"),
path("property/<uuid:id>", property_view.property_edit, name="property.edit"),
path("property/create", property_view.property_edit, {"id": None}, name="property.create"),
# Relations
# path("relation/list", relation_view.relation_list, name="relation.list"),
# path("relation/<uuid:id>", relation_view.relation_edit, name="relation.edit"),
path("relation/<uuid:id>/details", relation_view.relation_details, name="relation.details"),
# path("relation/create", relation_view.relation_edit, {"id": None}, name="relation.create"),
] ]

65
k356/items/views/base.py Normal file
View File

@ -0,0 +1,65 @@
from django.db import models
from django.db.models.fields.related import RelatedField
from django.http.response import JsonResponse
import json
def generic_edit(model, request, id=None):
"""Create/edit generic object view."""
if id:
item = model.objects.filter(id=id, author=request.user.setting).first()
else:
item = model(author=request.user.setting)
if not item:
return JsonResponse({}, status=404)
if request.method == "DELETE":
try:
item.delete()
except Exception:
return JsonResponse({"error": "INVALID_DELETE"}, status=401)
return JsonResponse({})
if request.method != "POST":
return JsonResponse({}, status=405)
try:
data = json.loads(request.body)
except Exception:
return JsonResponse({"error": "INVALID_DATA"}, status=401)
for field in item._meta.fields:
if field.name in item.Serialization.excluded_fields_edit:
continue
if isinstance(field, RelatedField):
# For now, disregard related field (fk, m2m, 1-1)
if isinstance(field, models.ForeignKey):
setattr(item, f"{field.name}_id", data[field.name])
continue
if field.name not in data:
continue
setattr(item, field.name, data[field.name])
try:
item.save()
except Exception:
return JsonResponse({"error": "DATA_INVALID"}, status=401)
return JsonResponse(
{
"object": item.serialize(),
}
)

View File

@ -1,11 +1,10 @@
import json from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from app.utils.api.api_list import header_for_table from app.utils.api.api_list import header_for_table
from django.contrib.auth.decorators import login_required from items.models import Item, ItemRelation, ItemType, LinkedProperty, Property
from django.db import models from items.views.base import generic_edit
from django.db.models.fields.related import RelatedField
from django.http import JsonResponse
from items.models import Item, ItemType
@login_required @login_required
@ -31,57 +30,27 @@ def item_list(request):
def item_edit(request, id=None): def item_edit(request, id=None):
"""Create/edit item view.""" """Create/edit item view."""
if id: return generic_edit(Item, request, id)
item = Item.objects.filter(id=id, author=request.user.setting).first()
else:
item = Item(author=request.user.setting) @login_required
def item_details(request, id):
item = Item.objects.filter(author=request.user.setting, id=id).first()
if not item: if not item:
return JsonResponse({}, status=404) return JsonResponse({}, status=404)
if request.method == "DELETE":
try:
item.delete()
except Exception:
return JsonResponse({"error": "INVALID_DELETE"}, status=401)
return JsonResponse({})
if request.method != "POST":
return JsonResponse({}, status=405)
try:
data = json.loads(request.body)
except Exception:
return JsonResponse({"error": "INVALID_DATA"}, status=401)
for field in item._meta.fields:
if field.name in item.Serialization.excluded_fields_edit:
continue
if isinstance(field, RelatedField):
# For now, disregard related field (fk, m2m, 1-1)
if isinstance(field, models.ForeignKey):
setattr(item, f"{field.name}_id", data[field.name])
continue
if field.name not in data:
continue
setattr(item, field.name, data[field.name])
try:
item.save()
except Exception:
return JsonResponse({"error": "DATA_INVALID"}, status=401)
return JsonResponse( return JsonResponse(
{ {
"object": Item.objects.filter(id=item.id).serialize().first(), "object": item.serialize(),
"parents": list(item.parents.serialize()),
"parents_headers": header_for_table(ItemRelation),
"children": list(item.children.serialize()),
"children_headers": header_for_table(ItemRelation),
"properties": list(item.properties.serialize()),
"properties_headers": header_for_table(Property),
"linked_properties": list(item.linked_properties.serialize()),
"linked_properties_headers": header_for_table(LinkedProperty),
} }
) )

View File

@ -0,0 +1,30 @@
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from app.utils.api.api_list import header_for_table
from items.models import Property
from items.views.base import generic_edit
@login_required
def property_list(request):
items = Property.objects.filter(author=request.user.setting)
return JsonResponse(
{
"result": {
"properties": list(items.serialize()),
"properties_headers": header_for_table(Property),
},
"count": items.count(),
}
)
@login_required
def property_edit(request, id=None):
"""Create/edit property view."""
return generic_edit(Property, request, id)

View File

@ -0,0 +1,29 @@
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from app.utils.api.api_list import header_for_table
from items.models import Item, ItemRelation, Property, RelationProperty
@login_required
def relation_details(request, id):
relation = ItemRelation.objects.filter(author=request.user.setting, id=id).first()
if not relation:
return JsonResponse({}, status=404)
return JsonResponse(
{
"object": relation.serialize(),
"parent": relation.parent.serialize(),
"parent_headers": header_for_table(Item),
"child": relation.child.serialize(),
"child_headers": header_for_table(Item),
"properties": list(relation.properties.serialize()),
"properties_headers": header_for_table(Property),
"relation_properties": list(relation.relation_properties.serialize()),
"relation_properties_headers": header_for_table(RelationProperty),
}
)

View File

@ -1,67 +1,12 @@
import json
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db import models
from django.db.models.fields.related import RelatedField
from django.http import JsonResponse
from items.models import ItemType from items.models import ItemType
from items.views.base import generic_edit
@login_required @login_required
def type_edit(request, id=None): def type_edit(request, id=None):
"""Create/edit type view.""" """Create/edit type view."""
if id: return generic_edit(ItemType, request, id)
item = ItemType.objects.filter(id=id, author=request.user.setting).first()
else:
item = ItemType(author=request.user.setting)
if not item:
return JsonResponse({}, status=404)
if request.method == "DELETE":
try:
item.delete()
except Exception:
return JsonResponse({"error": "INVALID_DELETE"}, status=401)
return JsonResponse({})
if request.method != "POST":
return JsonResponse({}, status=405)
try:
data = json.loads(request.body)
except Exception:
return JsonResponse({"error": "INVALID_DATA"}, status=401)
for field in item._meta.fields:
if field.name in item.Serialization.excluded_fields_edit:
continue
if isinstance(field, RelatedField):
# For now, disregard related field (fk, m2m, 1-1)
if isinstance(field, models.ForeignKey):
setattr(item, f"{field.name}_id", data[field.name])
continue
if field.name not in data:
continue
setattr(item, field.name, data[field.name])
try:
item.save()
except Exception:
return JsonResponse({"error": "DATA_INVALID"}, status=401)
return JsonResponse(
{
"object": ItemType.objects.filter(id=item.id).serialize().first(),
}
)

View File

@ -5,7 +5,8 @@
<h5 class="card-header">{% trans "Encryption testing" %}</h5> <h5 class="card-header">{% trans "Encryption testing" %}</h5>
<div class="card-body d-flex flex-column"> <div class="card-body d-flex flex-column">
<p class="card-text">{% trans "The text will be automatically copied to your clipboard." %}</p> <p class="card-text">{% trans "The text will be automatically copied to your clipboard." %}</p>
<textarea class="form-control m-2" cols="50" rows="4" v-model="text" @keyup="encrypt(text)"></textarea> <textarea class="form-control m-2" cols="50" rows="4" v-model="text" @keyup="encrypt_text(text)"></textarea>
<p class="card-text text-danger" v-if="encryption_error">[[ encryption_error ]]</p>
<textarea class="form-control m-2" cols="50" rows="4" v-model="encrypted" disabled></textarea> <textarea class="form-control m-2" cols="50" rows="4" v-model="encrypted" disabled></textarea>
</div> </div>
</div> </div>
@ -14,7 +15,8 @@
<h5 class="card-header">{% trans "Decryption testing" %}</h5> <h5 class="card-header">{% trans "Decryption testing" %}</h5>
<div class="card-body d-flex flex-column"> <div class="card-body d-flex flex-column">
<p class="card-text">{% trans "The text will be automatically copied to your clipboard." %}</p> <p class="card-text">{% trans "The text will be automatically copied to your clipboard." %}</p>
<textarea class="form-control m-2" cols="50" rows="4" v-model="encrypted_text" @keyup="decrypt(encrypted_text)"></textarea> <textarea class="form-control m-2" cols="50" rows="4" v-model="encrypted_text" @keyup="decrypt_text(encrypted_text)"></textarea>
<p class="card-text text-danger" v-if="decryption_error">[[ decryption_error ]]</p>
<textarea class="form-control m-2" cols="50" rows="4" v-model="decrypted" disabled></textarea> <textarea class="form-control m-2" cols="50" rows="4" v-model="decrypted" disabled></textarea>
</div> </div>
</div> </div>

View File

@ -1,34 +1,36 @@
EncryptionTesting = { EncryptionTesting = {
template: "#EncryptionTesting", template: "#EncryptionTesting",
props: ["crypto_key"], router_path: "/EncryptionTesting",
delimiters: ["[[", "]]"],
props: [],
data: function() { data: function() {
return { return {
text: '', text: '',
encrypted: '', encrypted: '',
encrypted_text: '', encrypted_text: '',
decrypted: '', decrypted: '',
decryption_error: '',
encryption_error: '',
} }
}, },
methods: { methods: {
encrypt: function(data) { async encrypt_text (data) {
var self = this; try {
this.encrypted = await this.encrypt(data)
encryptWithKey(this.crypto_key, data).then(e => { this.encryption_error = ""
self.encrypted = e; } catch (err) {
this.encryption_error = "{{_('Error while encryption of message.') | escapejs}}"
navigator.clipboard.writeText(self.encrypted); }
})
}, },
decrypt: function(data) { async decrypt_text (data) {
var self = this; try {
this.decrypted = await this.decrypt(data)
decryptWithKey(this.crypto_key, data).then(e => { this.decryption_error = ""
self.decrypted = e; } catch (err) {
this.decryption_error = "{{_('Error while decryption of message.') | escapejs}}"
navigator.clipboard.writeText(self.encrypted); }
})
}, },
} }
} }

View File

@ -1,18 +1,10 @@
{% load i18n %} {% load i18n %}
<div class="card bg-warning mt-4 pt-2 ps-lg-2"> <div class="card bg-warning mt-4 pt-2 ps-lg-2">
{% if user_settings.k356_key %}
<h5 class="card-header">{% trans "K356 is locked" %}</h5> <h5 class="card-header">{% trans "K356 is locked" %}</h5>
<div class="card-body"> <div class="card-body">
<h5 class="card-title">{% trans "K356 needs an unlock..." %}</h5> <h5 class="card-title">{% trans "K356 needs an unlock..." %}</h5>
<p class="card-text">{% trans "Enter your personnal password in order to unlock your K356." %}</p> <p class="card-text">{% trans "Enter your personnal password in order to unlock your K356." %}</p>
<input class="form-control" type="password" v-model="password" @keyup.enter="generate_import_key(password)" autofocus> <input class="form-control" type="password" v-model="password" @keyup.enter="generate_aes_key(password)" autofocus>
</div> </div>
{% else %}
<h5 class="card-header">{% trans "K356 creation" %}</h5>
<div class="card-body">
<p class="card-text">{% trans "Enter your personnal password in order to create your K356." %}</p>
<input class="form-control" type="password" v-model="password" @keyup.enter="generate_import_key(password)" autofocus>
</div>
{% endif %}
</div> </div>

View File

@ -1,108 +1,25 @@
const rvalidate = Vue.resource(Urls["users:k356.validate"]);
Loading = { Loading = {
template: "#Loading", template: "#Loading",
router_path: "/",
props: ["crypto_key"], props: [],
data: function() { data: function() {
return { return {
password: '', password: '',
k356_fingerprint: "{{ user_settings.k356_fingerprint }}",
} }
}, },
mounted: function() {}, mounted: function() {
// FIX: Remove this, this is the key for debugging
this.generate_aes_key('asd')
},
methods: { methods: {
generate_import_key: function(password) { async generate_aes_key (password) {
const key = await this.deriveKeyFromPassphrase(password, "{{ user_setting.id }}--aes")
if (password != null && password != "") {
var self = this;
window.crypto.subtle.importKey(
"raw",
(new TextEncoder()).encode(password),
"PBKDF2",
true,
["deriveKey"]
).then(pkey => {
window.crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: stringToArrayBuffer("salt"),
iterations: 250000,
hash: "SHA-256",
},
pkey,
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
).then(key => {
self.set_key(key);
}).catch(function(err) {
self.set_key(null);
});
}).catch(function(err) {
self.set_key(null);
});
} else {
this.set_key(null);
}
},
set_key: function(key) {
if (key) {
var self = this;
encryptWithKey({key: key, uuid: this.crypto_key.uuid}, this.crypto_key.uuid).then(efinger => {
self.$http.post(Urls["users:k356.validate"](), {fingerprint: efinger}).then(response => {
if (!response.data.ok) {
Swal.fire({title: response.data.error, icon: "error", showConfirmButton: false, toast: false});
} else {
self.$emit("update_key", key);
Swal.fire({title: "Successfully loaded K356!", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000});
}
}).catch(err => {
Swal.fire({title: "{{_('Error while verifying encryption check') | escapejs}}", icon: "error", showConfirmButton: false, toast: false});
});
}).catch(err => {
Swal.fire({title: "{{_('Error while encrypting verification') | escapejs}}", icon: "error", showConfirmButton: false, toast: false});
});
}
if (key == null) {
Swal.fire({title: "Error while loading K356!", icon: "error", showConfirmButton: false});
this.$emit("update_key", null);
}
this.password = "";
this.$emit("update_key", key)
}, },
} }

View File

@ -1,9 +1,16 @@
from django import template from django import template
from django.utils.html import mark_safe
register = template.Library() register = template.Library()
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def define(context, key, value): def define(context, key, *args):
context.dicts[0][key] = value if len(args) == 1:
context.dicts[0][key] = args[0]
else:
context.dicts[0][key] = [mark_safe(arg) for arg in args]
return "" return ""

19
k356/node_modules/.package-lock.json generated vendored Normal file
View File

@ -0,0 +1,19 @@
{
"name": "k356",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/@jamescoyle/svg-icon": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@jamescoyle/svg-icon/-/svg-icon-0.2.1.tgz",
"integrity": "sha512-ZGth9/uM02L7hxKEPmpVYheviVM1R0P6pxQp4M96xmtqTcFVe22UQIGTfyGWrkFMJtztTgMq2K12++Zv5aXZIw==",
"license": "MIT"
},
"node_modules/@mdi/js": {
"version": "7.4.47",
"resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz",
"integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==",
"license": "Apache-2.0"
}
}
}

View File

@ -0,0 +1,12 @@
# These are supported funding model platforms
github: [JamesCoyle] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

21
k356/node_modules/@jamescoyle/svg-icon/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 James Coyle
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

42
k356/node_modules/@jamescoyle/svg-icon/README.md generated vendored Normal file
View File

@ -0,0 +1,42 @@
# SVG Icon WebComponent
[![](https://chips.james-coyle.now.sh/npm/version/@jamescoyle/svg-icon)](https://www.npmjs.com/package/@jamescoyle/svg-icon)
[![](https://chips.james-coyle.now.sh/npm/downloads/@jamescoyle/svg-icon)](https://www.npmjs.com/package/@jamescoyle/svg-icon)
A basic webcomponent for rendering a single path SVG icon. This component makes it easy to use SVG path based icon packs such as [MaterialDesignIcons](https://materialdesignicons.com/) and [SimpleIcons](https://simpleicons.org/).
# Usage
1. Install the package from NPM
```
npm install @jamescoyle/svg-icon
```
2. Import the component into your application
```
import '@jamescoyle/svg-icon'
```
3. Use the icon in your markup
```
<svg-icon type="mdi" path="M...z"></svg-icon>
```
# Attributes
| Name | Default | Description |
| ------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| type | null | This sets the size and viewbox to match the recommended size for the icon pack specified. |
| path | null | Required. An SVG path to render as an icon |
| size | 24 | The width and height of the SVG element |
| viewbox | "0 0 24 24" | The `viewBox` of the SVG element |
| flip | null | One of "horizontal", "vertical", or "both". Flips the icon in the specified direction(s). |
| rotate | 0deg | Rotates the icon by the specified value. Can be any valid [CSS angle](https://developer.mozilla.org/en-US/docs/Web/CSS/angle) value. |
# Styling
By default the icon will inherit the current font color of the container it is placed within. You can easily provide a specific color using an inline style on the element (`style="color: red"`) or can target the tag as normal with CSS rules.
# Accessibility
You should make use of aria attributes to improve accessibility for users that use screen reading technology. You can use `aria-labelledby` to create a link between an icon and its label. A descriptive `aria-label` can be used to allow screen readers to announce an icon if there is no visual label to accompany it.

29
k356/node_modules/@jamescoyle/svg-icon/package.json generated vendored Normal file
View File

@ -0,0 +1,29 @@
{
"name": "@jamescoyle/svg-icon",
"version": "0.2.1",
"description": "Icon webcomponent",
"main": "lib/svg-icon.js",
"directories": {
"lib": "lib",
"test": "tests"
},
"scripts": {
"test": "jest"
},
"repository": {
"type": "git",
"url": "git+https://github.com/JamesCoyle/svg-icon.git"
},
"keywords": [
"webcomponents",
"icon",
"mdi",
"pictogrammers"
],
"author": "Pictogrammers, James Coyle",
"license": "MIT",
"bugs": {
"url": "https://github.com/JamesCoyle/svg-icon/issues"
},
"homepage": "https://github.com/JamesCoyle/svg-icon#readme"
}

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script type="module">
import '../lib/svg-icon.js'
</script>
</head>
<body>
<svg-icon type="mdi" path="M15,14C12.33,14 7,15.33 7,18V20H23V18C23,15.33 17.67,14 15,14M15,12A4,4 0 0,0 19,8A4,4 0 0,0 15,4A4,4 0 0,0 11,8A4,4 0 0,0 15,12M5,13.28L7.45,14.77L6.8,11.96L9,10.08L6.11,9.83L5,7.19L3.87,9.83L1,10.08L3.18,11.96L2.5,14.77L5,13.28Z"></svg-icon>
</body>
</html>

20
k356/node_modules/@mdi/js/LICENSE generated vendored Normal file
View File

@ -0,0 +1,20 @@
Pictogrammers Free License
--------------------------
This icon collection is released as free, open source, and GPL friendly by
the [Pictogrammers](http://pictogrammers.com/) icon group. You may use it
for commercial projects, open source projects, or anything really.
# Icons: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
Some of the icons are redistributed under the Apache 2.0 license. All other
icons are either redistributed under their respective licenses or are
distributed under the Apache 2.0 license.
# Fonts: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
All web and desktop fonts are distributed under the Apache 2.0 license. Web
and desktop fonts contain some icons that are redistributed under the Apache
2.0 license. All other icons are either redistributed under their respective
licenses or are distributed under the Apache 2.0 license.
# Code: MIT (https://opensource.org/licenses/MIT)
The MIT license applies to all non-font and non-icon files.

34
k356/node_modules/@mdi/js/README.md generated vendored Normal file
View File

@ -0,0 +1,34 @@
> *Note:* Please use the main [MaterialDesign](https://github.com/Templarian/MaterialDesign/issues) repo to report issues. This repo is for distribution of the JavaScript files only.
# JavaScript/TypeScript - Material Design Icons
JavaScript and TypeScript distribution for the [Material Design Icons](https://materialdesignicons.com). This module contains all the path data for all icons.
```
npm install @mdi/js
```
## Usage
```js
import { mdiAccount } from '@mdi/js'
console.log(mdiAccount); // "M...Z"
```
> Note: [WebPack](https://webpack.js.org) 4 or [Rollup](https://rollupjs.org) will tree shake unused icons.
## Related Packages
[NPM @MDI Organization](https://npmjs.com/org/mdi)
- React: [MaterialDesign-React](https://github.com/Templarian/MaterialDesign-React)
- SVG: [MaterialDesign-SVG](https://github.com/Templarian/MaterialDesign-SVG)
- Webfont: [MaterialDesign-Webfont](https://github.com/Templarian/MaterialDesign-Webfont)
- Font-Build: [MaterialDesign-Font-Build](https://github.com/Templarian/MaterialDesign-Font-Build)
- Desktop Font: [MaterialDesign-Font](https://github.com/Templarian/MaterialDesign-Font)
## Learn More
- [MaterialDesignIcons.com](https://materialdesignicons.com)
- https://github.com/Templarian/MaterialDesign

18
k356/node_modules/@mdi/js/build.js generated vendored Normal file
View File

@ -0,0 +1,18 @@
const util = require('@mdi/util');
const meta = util.getMeta(true);
const find = /(\-\w)/g;
const convert = function(matches){
return matches[1].toUpperCase();
};
const lines = meta.map(icon => {
let name = icon.name.replace(find, convert);
name = `${name[0].toUpperCase()}${name.slice(1)}`;
return `export const mdi${name}: string = "${icon.path}";`;
});
const output = `// Material Design Icons v${util.getVersion()}\n${lines.join('\n')}`;
util.write("mdi.ts", output);

7447
k356/node_modules/@mdi/js/commonjs/mdi.d.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

7599
k356/node_modules/@mdi/js/commonjs/mdi.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

7447
k356/node_modules/@mdi/js/mdi.d.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

7448
k356/node_modules/@mdi/js/mdi.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

37
k356/node_modules/@mdi/js/package.json generated vendored Normal file
View File

@ -0,0 +1,37 @@
{
"name": "@mdi/js",
"version": "7.4.47",
"description": "Dist for Material Design Icons for JS/TypeScript",
"main": "commonjs/mdi.js",
"module": "mdi.js",
"types": "mdi.d.ts",
"sideEffects": false,
"scripts": {
"build": "npm update && npm install && npm run buildjs && npm run es5 && npm run commonjs",
"buildjs": "node build.js",
"es5": "tsc -d mdi.ts --target es5 --module es2015",
"commonjs": "tsc -d mdi.ts --outDir commonjs",
"umd": "tsc -d mdi.ts --module UMD --outDir umd",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Templarian/MaterialDesign-JS.git"
},
"keywords": [
"Material",
"Design",
"Icons",
"mdi"
],
"author": "Austin Andrews",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/Templarian/MaterialDesign-JS/issues"
},
"homepage": "https://github.com/Templarian/MaterialDesign-JS#readme",
"devDependencies": {
"@mdi/svg": "^7.4.47",
"@mdi/util": "^0.3.2"
}
}

25
k356/package-lock.json generated Normal file
View File

@ -0,0 +1,25 @@
{
"name": "k356",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@jamescoyle/svg-icon": "^0.2.1",
"@mdi/js": "^7.4.47"
}
},
"node_modules/@jamescoyle/svg-icon": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@jamescoyle/svg-icon/-/svg-icon-0.2.1.tgz",
"integrity": "sha512-ZGth9/uM02L7hxKEPmpVYheviVM1R0P6pxQp4M96xmtqTcFVe22UQIGTfyGWrkFMJtztTgMq2K12++Zv5aXZIw==",
"license": "MIT"
},
"node_modules/@mdi/js": {
"version": "7.4.47",
"resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz",
"integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==",
"license": "Apache-2.0"
}
}
}

6
k356/package.json Normal file
View File

@ -0,0 +1,6 @@
{
"dependencies": {
"@jamescoyle/svg-icon": "^0.2.1",
"@mdi/js": "^7.4.47"
}
}

391
k356/poetry.lock generated Normal file
View File

@ -0,0 +1,391 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "asgiref"
version = "3.8.1"
description = "ASGI specs, helper code, and adapters"
optional = false
python-versions = ">=3.8"
files = [
{file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"},
{file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"},
]
[package.extras]
tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"]
[[package]]
name = "black"
version = "24.8.0"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.8"
files = [
{file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"},
{file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"},
{file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"},
{file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"},
{file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"},
{file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"},
{file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"},
{file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"},
{file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"},
{file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"},
{file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"},
{file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"},
{file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"},
{file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"},
{file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"},
{file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"},
{file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"},
{file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"},
{file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"},
{file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"},
{file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"},
{file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"},
]
[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
packaging = ">=22.0"
pathspec = ">=0.9.0"
platformdirs = ">=2"
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "cfgv"
version = "3.4.0"
description = "Validate configuration and produce human readable error messages."
optional = false
python-versions = ">=3.8"
files = [
{file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
{file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
]
[[package]]
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "distlib"
version = "0.3.8"
description = "Distribution utilities"
optional = false
python-versions = "*"
files = [
{file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
{file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
]
[[package]]
name = "django"
version = "5.1.1"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
optional = false
python-versions = ">=3.10"
files = [
{file = "Django-5.1.1-py3-none-any.whl", hash = "sha256:71603f27dac22a6533fb38d83072eea9ddb4017fead6f67f2562a40402d61c3f"},
{file = "Django-5.1.1.tar.gz", hash = "sha256:021ffb7fdab3d2d388bc8c7c2434eb9c1f6f4d09e6119010bbb1694dda286bc2"},
]
[package.dependencies]
asgiref = ">=3.8.1,<4"
sqlparse = ">=0.3.1"
tzdata = {version = "*", markers = "sys_platform == \"win32\""}
[package.extras]
argon2 = ["argon2-cffi (>=19.1.0)"]
bcrypt = ["bcrypt"]
[[package]]
name = "django-bower"
version = "5.2.1-rc2"
description = "Integrate django with bower"
optional = false
python-versions = "*"
files = []
develop = false
[package.dependencies]
django = "*"
six = "*"
[package.source]
type = "git"
url = "https://github.com/ArcaniteSolutions/django-bower.git"
reference = "HEAD"
resolved_reference = "3f85ca87da022f73a2f10356659d8178a71f1752"
[[package]]
name = "filelock"
version = "3.16.1"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.8"
files = [
{file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"},
{file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"},
]
[package.extras]
docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"]
typing = ["typing-extensions (>=4.12.2)"]
[[package]]
name = "identify"
version = "2.6.1"
description = "File identification library for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"},
{file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"},
]
[package.extras]
license = ["ukkonen"]
[[package]]
name = "isort"
version = "5.13.2"
description = "A Python utility / library to sort Python imports."
optional = false
python-versions = ">=3.8.0"
files = [
{file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
{file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
]
[package.extras]
colors = ["colorama (>=0.4.6)"]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.5"
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
[[package]]
name = "nodeenv"
version = "1.9.1"
description = "Node.js virtual environment builder"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"},
{file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
]
[[package]]
name = "packaging"
version = "24.1"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
]
[[package]]
name = "pathspec"
version = "0.12.1"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.8"
files = [
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
]
[[package]]
name = "platformdirs"
version = "4.3.6"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.8"
files = [
{file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
{file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
]
[package.extras]
docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"]
type = ["mypy (>=1.11.2)"]
[[package]]
name = "pre-commit"
version = "3.8.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false
python-versions = ">=3.9"
files = [
{file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"},
{file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"},
]
[package.dependencies]
cfgv = ">=2.0.0"
identify = ">=1.0.0"
nodeenv = ">=0.11.1"
pyyaml = ">=5.1"
virtualenv = ">=20.10.0"
[[package]]
name = "pyyaml"
version = "6.0.2"
description = "YAML parser and emitter for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"},
{file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"},
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"},
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"},
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"},
{file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"},
{file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"},
{file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"},
{file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"},
{file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"},
{file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"},
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"},
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"},
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"},
{file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"},
{file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"},
{file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"},
{file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"},
{file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"},
{file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"},
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"},
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"},
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"},
{file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"},
{file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"},
{file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"},
{file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"},
{file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"},
{file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"},
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"},
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"},
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"},
{file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"},
{file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"},
{file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"},
{file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"},
{file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"},
{file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"},
{file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"},
{file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"},
{file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"},
{file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"},
{file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"},
{file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"},
{file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"},
{file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"},
{file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"},
{file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"},
{file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"},
{file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"},
{file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"},
{file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"},
{file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"},
]
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
[[package]]
name = "sqlparse"
version = "0.5.1"
description = "A non-validating SQL parser."
optional = false
python-versions = ">=3.8"
files = [
{file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"},
{file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"},
]
[package.extras]
dev = ["build", "hatch"]
doc = ["sphinx"]
[[package]]
name = "tzdata"
version = "2024.2"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
files = [
{file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"},
{file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"},
]
[[package]]
name = "virtualenv"
version = "20.26.6"
description = "Virtual Python Environment builder"
optional = false
python-versions = ">=3.7"
files = [
{file = "virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2"},
{file = "virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48"},
]
[package.dependencies]
distlib = ">=0.3.7,<1"
filelock = ">=3.12.2,<4"
platformdirs = ">=3.9.1,<5"
[package.extras]
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "60769459dbf139704b035dd2ea3e1de8dc0fe36bc730e452db7676ea9d5344b3"

109
k356/pyproject.toml Normal file
View File

@ -0,0 +1,109 @@
[tool.black]
line-length = 150
extend-exclude = ""
[tool.isort]
# When imports are broken into multi-line, use the "Vertical Hanging Indent" layout.
multi_line_output = 3
# Always add a trailing comma to import lists (default: False).
include_trailing_comma = true
# Always put imports lists into vertical mode (0 = none allowed on first line)
force_grid_wrap = 0
# When multi-lining imports, use parentheses for line-continuation instead of default \.
use_parentheses = true
# Max import line length.
line_length = 150
# Put the django package into its own named section so we can rearrange it.
known_django = "django"
# All apps from this project
known_first_party = [
"main",
"items",
"users",
]
# projects have Django as the first imports.
sections = ["FUTURE","DJANGO","FIRSTPARTY","LOCALFOLDER","STDLIB","THIRDPARTY"]
# projects treat STDLIB the same as THIRDPARTY, so remove the blank line between them.
# projects want FIRSTPARTY and LOCALFOLDER in the same section.
no_lines_before = ["THIRDPARTY", "LOCALFOLDER"]
# Regardless of what follows the imports, force 2 blank lines after the import list
lines_after_imports = 2
# Insert 2 blank lines between each section
lines_between_sections = 2
# Alphabetical sort in sections (inside a line or in ())
force_alphabetical_sort_within_sections = true
# Sort by lexicographical
lexicographical = true
# Put all from before import
from_first = true
ensure_newline_before_comments = true
[tool.poetry]
name = "k356"
version = "1.0.0"
description = ""
authors = []
[tool.poetry.dependencies]
python = "^3.11"
Django = "^5.0"
django-bower = {git = "https://github.com/ArcaniteSolutions/django-bower.git"}
[tool.poetry.dev-dependencies]
black = "^24.4.0"
isort = "^5.10.1"
[tool.poetry.group.dev.dependencies]
pre-commit = "^3.8.0"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.coverage.run]
include = "*/k356/*"
omit = [
"*env*",
"*migrations*",
"*test*",
"admin.py",
"*settings*",
"*wsgi.py",
"*manage.py"
]
[tool.coverage.report]
# Regexes for lines to exclude from consideration
exclude_lines = [
# Have to re-enable the standard pragma
"pragma: no cover",
# Don't complain about missing debug-only code:
"def __repr__",
"if self.debug",
# Don't complain if tests don't hit defensive assertion code:
"raise AssertionError",
"raise NotImplementedError",
# Don't complain if non-runnable code isn't run:
"if 0:",
"if __name__ == .__main__.:"
]

View File

@ -8,25 +8,26 @@
<link rel="shortcut icon" href="{% static 'img/favicon.png' %}"> <link rel="shortcut icon" href="{% static 'img/favicon.png' %}">
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> <script src="{% static "vue/index.js" %}"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script> <script src="{% static "sweetalert2/index" %}"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="{% static "vue-resource/index" %}"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-resource@1.5.3"></script> <script src="{% static "js.cookie.min/index.js" %}"></script>
<script src="https://cdn.jsdelivr.net/npm/js-cookie@3.0.5/dist/js.cookie.min.js"></script> <script src="{% static "vue-router/index.js" %}"></script>
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous"> <link href="{% static "bootstrap.min/index.css" %}" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script> <script src="{% static "bootstrap.bundle.min/index.js" %}"></script>
<link href="{% static "bootswatch.min.css/index.css" %}" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.3/dist/lux/bootstrap.min.css " rel="stylesheet">
<script src="{% url 'reverse_js' %}" type="text/javascript"></script> <script src="{% url 'reverse_js' %}" type="text/javascript"></script>
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet"> <link href="{% static "vuetify.min/index.css" %}" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script> <script src="{% static "vuetify/index.js" %}"></script>
<script src="{% static "vuex/index.js" %}"></script>
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css" rel="stylesheet">
<link href="https://fonts.cdnfonts.com/css/jetbrains-mono" rel="stylesheet"> <link href="{% static "jetbrains-mono/index" %}">
<style> <style>
.font { .font {
font-family: 'JetBrains Mono', sans-serif; font-family: 'JetBrains Mono', sans-serif;
@ -49,7 +50,7 @@
<div class="collapse navbar-collapse" id="navbarColor01"> <div class="collapse navbar-collapse" id="navbarColor01">
<ul class="navbar-nav me-auto"> <ul class="navbar-nav me-auto">
<li class="nav-item"> <li class="nav-item">
<router-link href="#" to="{% url 'main:home' %}" class="nav-link active" to="{% url 'main:home' %}"> <router-link class="nav-link active" to="{% url 'main:home' %}">
{% trans "K356" %} {% trans "K356" %}
<span class="visually-hidden"></span> <span class="visually-hidden"></span>
</router-link> </router-link>
@ -58,11 +59,12 @@
<router-link class="nav-link" to="/ItemView">{% trans "Items" %}</router-link> <router-link class="nav-link" to="/ItemView">{% trans "Items" %}</router-link>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="#">{% trans "Properties" %}</a> <router-link class="nav-link" to="/PropertyView">{% trans "Properties" %}</router-link>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<router-link class="nav-link" to="/EncryptionTesting">{% trans "Encryption" %}</router-link> <router-link class="nav-link" to="/EncryptionTesting">{% trans "Encryption" %}</router-link>
</li> </li>
{% if request.user.is_superuser %}
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">{% trans "More" %}</a> <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">{% trans "More" %}</a>
<div class="dropdown-menu"> <div class="dropdown-menu">
@ -73,6 +75,7 @@
<a class="dropdown-item" href="/admin/">{% trans "Admin" %}</a> <a class="dropdown-item" href="/admin/">{% trans "Admin" %}</a>
</div> </div>
</li> </li>
{% endif %}
</ul> </ul>
<form class="d-flex"> <form class="d-flex">
<input class="form-control me-sm-2" type="search" placeholder="Search"> <input class="form-control me-sm-2" type="search" placeholder="Search">
@ -83,9 +86,9 @@
</nav> </nav>
<div id="app" class="container"> <div id="app" class="container">
<Loading :crypto_key="key" @update_key="update_key" v-if="locked"></Loading> <Loading @update_key="update_key" v-if="locked"></Loading>
<template v-if="!locked"> <template v-if="!locked">
<router-view :crypto_key="key"></router-view> <router-view></router-view>
</template> </template>
</div> </div>
@ -103,56 +106,7 @@
{% endfor %} {% endfor %}
<script type="text/javascript"> <script type="text/javascript">
Vue.use(VueRouter); {% include "vue/index.js" %}
Vue.config.delimiters = ["[[", "]]"];
{% for name, path in components.items %}
{% include path %}
Vue.component("{{ name }}", {{ name }});
{% endfor %}
const routes = [
{ path: '/', component: null },
{% for name, path in components.items %}
{
path: "/{{ name }}",
component: {{ name }},
},
{% endfor %}
];
const router = new VueRouter({routes});
const approuter = new Vue({
router,
vuetify: new Vuetify(),
el: "#main",
data: {
key: {
key: null,
uuid: "{{ user_settings.id }}",
},
locked: true,
},
methods: {
update_key: function(key) {
this.key.key = key;
this.locked = key == null;
},
lock_me: function() {
this.locked = true;
this.key.key = null;
}
}
});
router.beforeEach((to, from, next) => {
// Prevent from routing if key is not present.
next(approuter.key.key != null);
});
</script> </script>
<script type="text/javascript"> <script type="text/javascript">
@ -164,16 +118,5 @@
refresh_csrftoken(); refresh_csrftoken();
</script> </script>
<script type="module">
import { useDate } from './vuetify.js'
const date = useDate()
const formatted = date.format('2010-04-13', 'fullDateWithWeekday')
console.log(formatted) // Tuesday, April 13, 2010
</script>
</body> </body>
</html> </html>

View File

@ -2,7 +2,7 @@
{% block component %} {% block component %}
<v-data-table <v-data-table
:headers="items_headers" :headers="citems_headers"
:items="items" :items="items"
:items-per-page="50" :items-per-page="50"
:search="search" :search="search"
@ -34,6 +34,17 @@
<v-row> <v-row>
<template v-for="field in editable_fields"> <template v-for="field in editable_fields">
<template v-if="field.field_widget == 'v-select'"> <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>
</template>
<template v-else>
<v-select <v-select
v-model="editedItem[field.value]" v-model="editedItem[field.value]"
:items="items_relations[field.value]" :items="items_relations[field.value]"
@ -46,6 +57,7 @@
</template> </template>
</v-select> </v-select>
</template> </template>
</template>
<template v-else> <template v-else>
<component :is="field.field_widget" v-model="editedItem[field.value]" :label="field.text"></component> <component :is="field.field_widget" v-model="editedItem[field.value]" :label="field.text"></component>
</template> </template>
@ -91,7 +103,23 @@
<template v-slot:item.id="{ item }"> <template v-slot:item.id="{ item }">
[[ item.id.slice(0, 8) ]]... [[ item.id.slice(0, 8) ]]...
</template> </template>
<template v-slot:item.description="{ item }">
<template v-if="item.description && item.description.length > 15">
[[ item.description.slice(0, 15) ]]...
</template>
<template v-else>
[[ item.description ]]
</template>
</template>
<template v-slot:item.last_modified_at="{ item }">
[[ formatDate(item.last_modified_at) ]]
</template>
<template v-slot:item.created_at="{ item }">
[[ formatDate(item.created_at) ]]
</template>
<template v-slot:item.actions="{ item }"> <template v-slot:item.actions="{ item }">
<v-icon v-if="show_url" small class="mr-2" @click="showItem(item)">mdi-eye</v-icon>
<v-icon small class="mr-2" @click="editItem(item)">mdi-pencil</v-icon> <v-icon small class="mr-2" @click="editItem(item)">mdi-pencil</v-icon>
<v-icon small @click="deleteItem(item)">mdi-delete</v-icon> <v-icon small @click="deleteItem(item)">mdi-delete</v-icon>
</template> </template>

View File

@ -1,8 +1,9 @@
{% block component %} {% block component %}
{{ name }} = { {{ name }} = {
template: "#{{ name }}", template: "#{{ name }}",
router_path: "/{{ name }}",
delimiters: ["[[", "]]"], delimiters: ["[[", "]]"],
props: ["crypto_key", "items", "items_headers", "items_relations", "group_by"], props: ["crypto_key", "items", "items_headers", "items_relations", "group_by", "hidden_fields"],
data: function() { data: function() {
return { return {
@ -12,6 +13,8 @@
defaultItem: {}, defaultItem: {},
editedItem: {}, editedItem: {},
search: null, search: null,
show_url: "{{ show_url|default:'' }}",
default_hidden_fields: [{% for field in default_hidden_fields %}"{{ field }}",{% endfor %}]
} }
}, },
@ -19,6 +22,10 @@
editable_fields: function() { editable_fields: function() {
return this.items_headers.filter(e => e.editable) return this.items_headers.filter(e => e.editable)
}, },
citems_headers: function() {
return this.items_headers.filter(e => !this.hidden_fields.includes(e.value) && !this.default_hidden_fields.includes(e.value))
},
}, },
watch: { watch: {
@ -32,6 +39,14 @@
methods: { methods: {
formatDate (date) {
return formatDate(date)
},
showItem (item) {
this.$router.replace({ name: this.show_url, params: { id: item.id }})
},
editItem (item) { editItem (item) {
this.editedIndex = this.items.indexOf(item) this.editedIndex = this.items.indexOf(item)
this.editedItem = Object.assign({}, item) this.editedItem = Object.assign({}, item)
@ -45,9 +60,6 @@
}, },
deleteItemConfirm () { deleteItemConfirm () {
var self = this
var item = this.items[this.editedIndex]
this.$emit("deleteItem", this.editedIndex) this.$emit("deleteItem", this.editedIndex)
this.closeDelete() this.closeDelete()
@ -74,7 +86,6 @@
} else { } else {
console.log('createItem emit', this.editedItem)
this.$emit("createItem", this.editedItem) this.$emit("createItem", this.editedItem)
} }

View File

@ -1,3 +1,51 @@
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) { function stringToArrayBuffer(str) {
var buf = new ArrayBuffer(str.length); var buf = new ArrayBuffer(str.length);
var bufView = new Uint8Array(buf); var bufView = new Uint8Array(buf);
@ -18,6 +66,24 @@ function arrayBufferToString(str) {
} }
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) { function encryptWithKey(key, data) {
// Encrypt data with key. Return a Promise // Encrypt data with key. Return a Promise
return new Promise((resolve) => { return new Promise((resolve) => {

117
k356/templates/vue/index.js Normal file
View File

@ -0,0 +1,117 @@
{% include "vue/plugins.js" %}
Vue.use(VueRouter)
Vue.use(Vuex)
Vue.use(EncryptionPlugin)
Vue.config.delimiters = ["[[", "]]"];
{% for name, path in components.items %}
{% include path %}
Vue.component("{{ name }}", {{ name }})
{% endfor %}
const routes = [
{ path: '/', component: null },
{% for name, path in components.items %}
{
path: {{ name }}.router_path,
name: "{{ name }}",
component: {{ name }},
},
{% 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
},
}
})
const router = new VueRouter({routes})
const approuter = new Vue({
router,
vuetify: new Vuetify(),
store: encryptionStore,
el: "#main",
data: {
uuid: "{{ user_settings.id }}",
},
computed: {
locked: function() {
return this.$store.state.aes_key == null || this.$store.state.keyPair?.privateKey == null
}
},
mounted: function() {},
methods: {
async load_keys (aes_key) {
const response = await this.$http.get(Urls["users:keys"]())
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('update_keyPair', 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('update_keyPair', keyPair)
Swal.fire({title: "Successfully created K356!", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000});
}
},
update_key: function(key) {
this.$store.commit('update_aes_key', key)
this.load_keys(key)
},
lock_me: function() {
this.$store.commit('update_keyPair', null)
this.$store.commit('update_aes_key', null)
}
}
})
router.beforeEach((to, from, next) => {
// Prevent from routing if key is not present.
next(!approuter.locked)
})

View File

@ -0,0 +1,160 @@
const operations = crypto.subtle
const pbkdf2_iterations = 250000
function stringToArrayBuffer(str) {
var buf = new ArrayBuffer(str.length);
var bufView = new Uint8Array(buf);
for (var i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
function arrayBufferToString(str) {
var byteArray = new Uint8Array(str);
var byteString = '';
for (var i = 0; i < byteArray.byteLength; i++) {
byteString += String.fromCodePoint(byteArray[i]);
}
return byteString;
}
function formatDate (date) {
const d = new Date(date)
const hours = d.getHours().toString().padStart(2, '0')
const minutes = d.getMinutes().toString().padStart(2, '0')
const formattedTime = `${hours}:${minutes}`
return `${d.toLocaleDateString()} ${formattedTime}`
}
const EncryptionPlugin = {
install(Vue, options) {
Vue.prototype.deriveKeyFromPassphrase = async (passphrase, salt) => {
const encoder = new TextEncoder();
const keyFromPassword = await operations.importKey(
"raw",
encoder.encode(passphrase),
"PBKDF2",
false,
["deriveKey"]
)
return await operations.deriveKey(
{
name: "PBKDF2",
salt: stringToArrayBuffer(salt),
iterations: pbkdf2_iterations,
hash: "SHA-256",
},
keyFromPassword,
{
name: "AES-GCM",
length: 256
},
true,
["wrapKey", "unwrapKey"]
)
},
Vue.prototype.generateKeyPair = async () => {
return await operations.generateKey(
{
name: "RSA-OAEP",
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256"
},
true,
["encrypt", "decrypt"]
);
},
Vue.prototype.wrapKey = async (key, wrappingKey, iv) => {
return btoa(arrayBufferToString(await operations.wrapKey(
"jwk",
key,
wrappingKey,
{name: "AES-GCM", iv: stringToArrayBuffer(iv)}
)))
},
Vue.prototype.unwrapKey = async (unwrappingKey, armored_jwk_data, iv, args) => {
return await operations.unwrapKey(
"jwk",
stringToArrayBuffer(atob(armored_jwk_data)),
unwrappingKey,
{name: "AES-GCM", iv: stringToArrayBuffer(iv)},
{
name: "RSA-OAEP",
hash: "SHA-256",
},
true,
args,
)
},
Vue.prototype.encrypt = async function(data) {
return btoa(arrayBufferToString(await operations.encrypt(
{ name: "RSA-OAEP" },
this.$store.state.keyPair.publicKey,
stringToArrayBuffer(data),
)))
},
Vue.prototype.decrypt = async function(armored_data) {
return arrayBufferToString(await operations.decrypt(
{ name: "RSA-OAEP" },
this.$store.state.keyPair.privateKey,
stringToArrayBuffer(atob(armored_data))
))
},
Vue.prototype.decryptObject = async function(efields, obj) {
// Decrypt all fields and return a new object
var newobj = {}
await Promise.all(Object.keys(obj).map(async field => {
if (efields.includes(field) && obj[field] != null && obj[field] != "") {
// TODO: Catch error
newobj[field] = await this.decrypt(obj[field])
} else {
newobj[field] = obj[field]
}
}))
return newobj
},
Vue.prototype.encryptObject = async function(efields, obj) {
// Encrypt all fields and return a new object
var newobj = {}
await Promise.all(Object.keys(obj).map(async field => {
if (efields.includes(field) && obj[field] != null) {
// TODO: Catch error
newobj[field] = await this.encrypt(obj[field])
} else {
newobj[field] = obj[field]
}
}))
return newobj
}
}
}

View File

@ -6,11 +6,12 @@ from users.models import UserSettings
@admin.action(description="Remove the key from the users") @admin.action(description="Remove the key from the users")
def remove_key(modeladmin, request, queryset): def remove_key(modeladmin, request, queryset):
queryset.update(k356_key=False, k356_key_fingerprint=None) # FIX: This is only for debugging first, this should *never* be used in production
queryset.update(public_key=None, private_key=None)
@admin.register(UserSettings) @admin.register(UserSettings)
class UserSettingsAdmin(admin.ModelAdmin): class UserSettingsAdmin(admin.ModelAdmin):
list_display = ("user", "k356_key") list_display = ("user",)
actions = [remove_key] actions = [remove_key]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.1.1 on 2024-09-28 04:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("users", "0002_usersettings_custom_identifier"),
]
operations = [
migrations.AddField(
model_name="usersettings",
name="private_key",
field=models.TextField(max_length=2048, null=True),
),
migrations.AddField(
model_name="usersettings",
name="public_key",
field=models.TextField(max_length=2048, null=True),
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 5.1.1 on 2024-09-28 06:34
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("users", "0003_usersettings_private_key_usersettings_public_key"),
]
operations = [
migrations.RemoveField(
model_name="usersettings",
name="k356_key",
),
migrations.RemoveField(
model_name="usersettings",
name="k356_key_fingerprint",
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 5.1.1 on 2024-09-28 07:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("users", "0004_remove_usersettings_k356_key_and_more"),
]
operations = [
migrations.AlterField(
model_name="usersettings",
name="custom_identifier",
field=models.TextField(blank=True, max_length=2048, null=True),
),
migrations.AlterField(
model_name="usersettings",
name="description",
field=models.TextField(blank=True, max_length=2048, null=True),
),
migrations.AlterField(
model_name="usersettings",
name="name",
field=models.TextField(blank=True, max_length=2048, null=True),
),
]

View File

@ -1,6 +1,7 @@
from uuid import uuid4
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import models from django.db import models
from app.utils.models import BaseModel from app.utils.models import BaseModel
@ -10,5 +11,6 @@ User = get_user_model()
class UserSettings(BaseModel): class UserSettings(BaseModel):
user = models.OneToOneField(User, on_delete=models.PROTECT, related_name="setting") user = models.OneToOneField(User, on_delete=models.PROTECT, related_name="setting")
k356_key = models.BooleanField(default=False) # The private and public key are wrapped with the AES key from the front-end
k356_key_fingerprint = models.CharField(null=True) public_key = models.TextField(max_length=2048, null=True)
private_key = models.TextField(max_length=2048, null=True)

View File

@ -3,8 +3,10 @@ from django.urls import path
from . import views from . import views
app_name = "users" app_name = "users"
urlpatterns = [ urlpatterns = [
path("keys", views.keys, name="keys"),
path("k356/validate", views.k356_validate, name="k356.validate"), path("k356/validate", views.k356_validate, name="k356.validate"),
] ]

View File

@ -1,10 +1,12 @@
import json
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.utils.translation import gettext as _
from django.http import JsonResponse from django.http import JsonResponse
from django.utils.translation import gettext as _
login_required() import json
@login_required
def k356_validate(request): def k356_validate(request):
us = request.user.setting us = request.user.setting
@ -36,7 +38,7 @@ def k356_validate(request):
return JsonResponse( return JsonResponse(
{ {
"ok": True "ok": True,
} }
) )
@ -46,3 +48,30 @@ def k356_validate(request):
"fingerprint": us.k356_key_fingerprint, "fingerprint": us.k356_key_fingerprint,
} }
) )
@login_required
def keys(request):
us = request.user.setting
if request.method == "POST":
try:
data = json.loads(request.body)
except Exception:
return JsonResponse({"error": "INVALID_DATA"}, status=401)
if us.private_key and not request.GET("force", "false") == "true":
return JsonResponse({"error": "KEY_EXISTS"}, status=401)
us.private_key = data.get("privateKey", None)
us.public_key = data.get("publicKey", None)
us.save()
return JsonResponse(
{
"privateKey": us.private_key,
"publicKey": us.public_key,
}
)

109
k356/wrap.js Normal file
View File

@ -0,0 +1,109 @@
const operations = crypto.subtle
var iv = crypto.getRandomValues(new Uint8Array(24))
function stringToArrayBuffer(str) {
var buf = new ArrayBuffer(str.length);
var bufView = new Uint8Array(buf);
for (var i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
function arrayBufferToString(str) {
var byteArray = new Uint8Array(str);
var byteString = '';
for (var i = 0; i < byteArray.byteLength; i++) {
byteString += String.fromCodePoint(byteArray[i]);
}
return byteString;
}
const encoder = new TextEncoder();
const passwordAsKeyData = encoder.encode('superSecretPassword');
const keyFromPassword = await operations.importKey(
"raw",
passwordAsKeyData,
"PBKDF2",
false,
["deriveKey"]
)
const aes = await operations.deriveKey(
{
name: "PBKDF2",
salt: stringToArrayBuffer("salt"),
iterations: 250000,
hash: "SHA-256",
},
keyFromPassword,
{
name: "AES-GCM",
length: 256
},
true,
["wrapKey", "unwrapKey"]
)
var keyPair = await crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256",
},
true,
["encrypt", "decrypt"]
)
var wrappedRSAKey = await crypto.subtle.wrapKey(
"jwk",
keyPair.privateKey,
aes,
{name: "AES-GCM", iv: iv}
)
const kk = btoa(arrayBufferToString(wrappedRSAKey))
const siv = btoa(arrayBufferToString(iv))
console.log(wrappedRSAKey)
console.log('wrapped key:', kk)
console.log('iv:', siv)
console.log("=================================")
console.log("=================================")
console.log("=================================")
var unwrapped = await crypto.subtle.unwrapKey(
"jwk",
stringToArrayBuffer(atob(kk)),
aes,
{name: "AES-GCM", iv: stringToArrayBuffer(atob(siv))},
{
name: "RSA-OAEP",
hash: "SHA-256",
},
true,
["decrypt"]
)
console.log(unwrapped)
const enc = await operations.encrypt(
{ name: "RSA-OAEP" },
keyPair.publicKey,
stringToArrayBuffer("asd"),
)
console.log(btoa(arrayBufferToString(enc)))
const dec = await operations.decrypt(
{ name: "RSA-OAEP" },
unwrapped,
enc,
)
console.log(arrayBufferToString(dec))