V0.1
This commit is contained in:
parent
8dfafa9404
commit
fb4d566007
@ -10,11 +10,13 @@ For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/5.1/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from django.utils.module_loading import import_module
|
||||
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
@ -46,23 +48,6 @@ INSTALLED_APPS = [
|
||||
"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/"
|
||||
|
||||
STATICFILES_DIRS = (os.path.join(BASE_DIR, "static_source"),)
|
||||
@ -78,11 +63,29 @@ STATIC_ROOT = os.path.join(BASE_DIR, "static")
|
||||
|
||||
STORAGES = {
|
||||
"staticfiles": {
|
||||
# "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
||||
"BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
|
||||
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
||||
# "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 = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
@ -161,6 +164,7 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
from app.settingsLocal import *
|
||||
|
||||
|
||||
for extra_app in EXTRA_APPS:
|
||||
INSTALLED_APPS.append(extra_app)
|
||||
|
||||
|
||||
16
k356/app/utils/helpers.py
Normal file
16
k356/app/utils/helpers.py
Normal 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))
|
||||
@ -1,9 +1,14 @@
|
||||
from uuid import uuid4
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
from django.db.models.fields.related import RelatedField
|
||||
|
||||
|
||||
from app.utils.helpers import recursive_getattr
|
||||
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
@ -17,9 +22,9 @@ class BaseQuerySet(models.QuerySet):
|
||||
"text": field.verbose_name.capitalize(),
|
||||
"align": "",
|
||||
"encrypted": field.name in self.model.Encryption.fields,
|
||||
"editable": field.name
|
||||
not in self.model.Serialization.excluded_fields_edit,
|
||||
"editable": field.name not in self.model.Serialization.excluded_fields_edit,
|
||||
"field_widget": "v-textarea",
|
||||
"choices": None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,6 +46,19 @@ class BaseQuerySet(models.QuerySet):
|
||||
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
|
||||
|
||||
fields = {}
|
||||
@ -78,12 +96,25 @@ class BaseModel(models.Model):
|
||||
class Encryption:
|
||||
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)
|
||||
|
||||
name = models.TextField(max_length=2048)
|
||||
description = models.TextField(max_length=2048)
|
||||
name = models.TextField(max_length=2048, blank=True, null=True)
|
||||
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)
|
||||
last_modified_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
35
k356/items/migrations/0004_alter_property_type.py
Normal file
35
k356/items/migrations/0004_alter_property_type.py
Normal 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,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -1,6 +1,8 @@
|
||||
from app.utils.models import BaseModel
|
||||
from django.contrib.auth.forms import gettext as _
|
||||
from django.db import models
|
||||
|
||||
|
||||
from app.utils.models import BaseModel
|
||||
from users.models import UserSettings
|
||||
|
||||
|
||||
@ -16,21 +18,14 @@ class ItemBase(BaseModel):
|
||||
|
||||
|
||||
class ItemType(ItemBase):
|
||||
name = models.TextField(max_length=2048)
|
||||
description = models.TextField(max_length=2048)
|
||||
pass
|
||||
|
||||
|
||||
class ItemRelation(ItemBase):
|
||||
parent = models.ForeignKey(
|
||||
"items.Item", on_delete=models.CASCADE, related_name="children"
|
||||
)
|
||||
child = models.ForeignKey(
|
||||
"items.Item", on_delete=models.CASCADE, related_name="parents"
|
||||
)
|
||||
parent = models.ForeignKey("items.Item", on_delete=models.CASCADE, related_name="children")
|
||||
child = models.ForeignKey("items.Item", on_delete=models.CASCADE, related_name="parents")
|
||||
|
||||
properties = models.ManyToManyField(
|
||||
"items.Property", through="items.RelationProperty", related_name="relations"
|
||||
)
|
||||
properties = models.ManyToManyField("items.Property", through="items.RelationProperty", related_name="relations")
|
||||
|
||||
|
||||
class Item(ItemBase):
|
||||
@ -54,13 +49,21 @@ class PropertyType(models.TextChoices):
|
||||
TEXT = "text", _("Text")
|
||||
DATE = "date", _("Date")
|
||||
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)
|
||||
|
||||
|
||||
class Property(ItemBase):
|
||||
type = models.CharField(
|
||||
max_length=32, choices=PropertyType.choices, default=PropertyType.TEXT
|
||||
)
|
||||
type = models.CharField(max_length=32, choices=PropertyType.choices, default=PropertyType.TEXT)
|
||||
|
||||
|
||||
class BaseLinkedProperty(ItemBase):
|
||||
@ -74,9 +77,7 @@ class BaseLinkedProperty(ItemBase):
|
||||
|
||||
|
||||
class LinkedProperty(BaseLinkedProperty):
|
||||
item = models.ForeignKey(
|
||||
Item, on_delete=models.CASCADE, null=True, related_name="linked_properties"
|
||||
)
|
||||
item = models.ForeignKey(Item, on_delete=models.CASCADE, null=True, related_name="linked_properties")
|
||||
|
||||
|
||||
class RelationProperty(BaseLinkedProperty):
|
||||
|
||||
51
k356/items/templates/components/ItemDetail/template.html
Normal file
51
k356/items/templates/components/ItemDetail/template.html
Normal 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>
|
||||
97
k356/items/templates/components/ItemDetail/vue.js
Normal file
97
k356/items/templates/components/ItemDetail/vue.js
Normal 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() {
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -3,5 +3,6 @@
|
||||
|
||||
{% block component %}
|
||||
{% define 'items' 'items' %}
|
||||
{% define 'show_url' 'ItemDetail' %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
@ -0,0 +1 @@
|
||||
<div>[[ this.$route.params.id ]]</div>
|
||||
26
k356/items/templates/components/ItemRelationDetail/vue.js
Normal file
26
k356/items/templates/components/ItemRelationDetail/vue.js
Normal 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))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
{% extends "base_components/glist/template.html" %}
|
||||
9
k356/items/templates/components/ItemRelationList/vue.js
Normal file
9
k356/items/templates/components/ItemRelationList/vue.js
Normal 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 %}
|
||||
@ -6,10 +6,10 @@
|
||||
<div class="card-body">
|
||||
|
||||
<ItemList
|
||||
:crypto_key="crypto_key"
|
||||
:items="items"
|
||||
:items_headers="items_headers"
|
||||
:items_relations="{'type': types}"
|
||||
:hidden_fields="[]"
|
||||
group_by="type__name"
|
||||
@deleteItem="deleteItem"
|
||||
@createItem="createItem"
|
||||
@ -24,10 +24,10 @@
|
||||
<div class="card-body">
|
||||
|
||||
<ItemList
|
||||
:crypto_key="crypto_key"
|
||||
:items="types"
|
||||
:items_headers="types_headers"
|
||||
:items_relations="{}"
|
||||
:hidden_fields="[]"
|
||||
group_by="[]"
|
||||
@deleteItem="deleteType"
|
||||
@createItem="createType"
|
||||
|
||||
@ -1,17 +1,15 @@
|
||||
ItemView = {
|
||||
template: "#ItemView",
|
||||
router_path: "/ItemView",
|
||||
delimiters: ["[[", "]]"],
|
||||
props: ["crypto_key"],
|
||||
props: [],
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
search: "",
|
||||
items: [],
|
||||
items_headers: [],
|
||||
items_relations: [],
|
||||
types: [],
|
||||
types_headers: [],
|
||||
types_relations: [],
|
||||
}
|
||||
},
|
||||
|
||||
@ -20,158 +18,67 @@ ItemView = {
|
||||
},
|
||||
|
||||
computed: {
|
||||
items_encrypted_fields: function() {
|
||||
items_efields: function() {
|
||||
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)
|
||||
},
|
||||
},
|
||||
|
||||
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 => {
|
||||
self.$set(self, name, response.data.result[name])
|
||||
// Decrypt all item the push
|
||||
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 => {
|
||||
self.decryptObject(self.items_encrypted_fields, item)
|
||||
// Decrypt all type the push
|
||||
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 => {
|
||||
self.decryptObject(self.types_encrypted_fields, type)
|
||||
})
|
||||
|
||||
}).catch(err => {
|
||||
} catch (err) {
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
let url = null
|
||||
|
||||
if (obj.id == undefined || obj.id == null) {
|
||||
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 {
|
||||
|
||||
self.decryptObject(encrypted_fields, response.data.object).then(new_obj => {
|
||||
resolve(new_obj)
|
||||
})
|
||||
|
||||
url = Urls[url_edit](obj.id)
|
||||
}
|
||||
|
||||
}).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}}"
|
||||
if (method == "delete") {
|
||||
@ -180,102 +87,130 @@ ItemView = {
|
||||
|
||||
Swal.fire({title: msg, icon: "error", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
|
||||
|
||||
})
|
||||
throw err
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
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) {
|
||||
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 => {
|
||||
|
||||
self.items.push(new_item)
|
||||
const new_item = await this.item_edition("post", item)
|
||||
this.items.push(new_item)
|
||||
|
||||
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
|
||||
self.items.splice(index, 1)
|
||||
self.items.push(new_item)
|
||||
const new_item = await this.item_edition("post", 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})
|
||||
|
||||
})
|
||||
} catch (err) {
|
||||
|
||||
this.items.push(item)
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
deleteItem (index) {
|
||||
async deleteItem (index) {
|
||||
|
||||
var self = this
|
||||
var item = this.items[index]
|
||||
|
||||
this.item_edition("delete", item).then(() => {
|
||||
self.items.splice(this.items.indexOf(item), 1)
|
||||
try {
|
||||
|
||||
// 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})
|
||||
|
||||
})
|
||||
} catch (err) {
|
||||
|
||||
this.items.push(item)
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
createType (type) {
|
||||
async createType (type) {
|
||||
|
||||
var self = this
|
||||
try {
|
||||
|
||||
this.type_edition("post", type).then(new_type => {
|
||||
|
||||
self.types.push(new_type)
|
||||
const new_type = await this.type_edition("post", type)
|
||||
this.types.push(new_type)
|
||||
|
||||
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
|
||||
self.types.splice(index, 1)
|
||||
self.types.push(new_type)
|
||||
const new_type = await this.type_edition("post", type)
|
||||
|
||||
this.types.push(new_type)
|
||||
|
||||
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]
|
||||
|
||||
this.type_edition("delete", type).then(() => {
|
||||
self.types.splice(this.types.indexOf(type), 1)
|
||||
try {
|
||||
|
||||
// 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})
|
||||
|
||||
})
|
||||
} catch (err) {
|
||||
|
||||
this.types.push(type)
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
@ -0,0 +1 @@
|
||||
<div>PropertyDetail</div>
|
||||
4
k356/items/templates/components/PropertyDetail/vue.js
Normal file
4
k356/items/templates/components/PropertyDetail/vue.js
Normal file
@ -0,0 +1,4 @@
|
||||
PropertyDetail = {
|
||||
template: "#PropertyDetail",
|
||||
router_path: "/PropertyDetail/:id",
|
||||
},
|
||||
@ -0,0 +1 @@
|
||||
{% extends "base_components/glist/template.html" %}
|
||||
8
k356/items/templates/components/PropertyList/vue.js
Normal file
8
k356/items/templates/components/PropertyList/vue.js
Normal 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 %}
|
||||
22
k356/items/templates/components/PropertyView/template.html
Normal file
22
k356/items/templates/components/PropertyView/template.html
Normal 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>
|
||||
183
k356/items/templates/components/PropertyView/vue.js
Normal file
183
k356/items/templates/components/PropertyView/vue.js
Normal 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)
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
@ -1,12 +1,27 @@
|
||||
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"
|
||||
|
||||
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>/details", item_view.item_details, name="details"),
|
||||
path("create", item_view.item_edit, {"id": None}, name="create"),
|
||||
# Type
|
||||
path("type/<uuid:id>", type_view.type_edit, name="type.edit"),
|
||||
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
65
k356/items/views/base.py
Normal 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(),
|
||||
}
|
||||
)
|
||||
@ -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 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 Item, ItemType
|
||||
from items.models import Item, ItemRelation, ItemType, LinkedProperty, Property
|
||||
from items.views.base import generic_edit
|
||||
|
||||
|
||||
@login_required
|
||||
@ -31,57 +30,27 @@ def item_list(request):
|
||||
def item_edit(request, id=None):
|
||||
"""Create/edit item view."""
|
||||
|
||||
if id:
|
||||
item = Item.objects.filter(id=id, author=request.user.setting).first()
|
||||
return generic_edit(Item, request, id)
|
||||
|
||||
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:
|
||||
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.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),
|
||||
}
|
||||
)
|
||||
|
||||
30
k356/items/views/property_view.py
Normal file
30
k356/items/views/property_view.py
Normal 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)
|
||||
29
k356/items/views/relation_view.py
Normal file
29
k356/items/views/relation_view.py
Normal 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),
|
||||
}
|
||||
)
|
||||
@ -1,67 +1,12 @@
|
||||
import json
|
||||
|
||||
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.views.base import generic_edit
|
||||
|
||||
|
||||
@login_required
|
||||
def type_edit(request, id=None):
|
||||
"""Create/edit type view."""
|
||||
|
||||
if 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(),
|
||||
}
|
||||
)
|
||||
return generic_edit(ItemType, request, id)
|
||||
|
||||
@ -5,7 +5,8 @@
|
||||
<h5 class="card-header">{% trans "Encryption testing" %}</h5>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -14,7 +15,8 @@
|
||||
<h5 class="card-header">{% trans "Decryption testing" %}</h5>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,34 +1,36 @@
|
||||
EncryptionTesting = {
|
||||
template: "#EncryptionTesting",
|
||||
props: ["crypto_key"],
|
||||
router_path: "/EncryptionTesting",
|
||||
delimiters: ["[[", "]]"],
|
||||
props: [],
|
||||
data: function() {
|
||||
return {
|
||||
text: '',
|
||||
encrypted: '',
|
||||
encrypted_text: '',
|
||||
decrypted: '',
|
||||
decryption_error: '',
|
||||
encryption_error: '',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
encrypt: function(data) {
|
||||
var self = this;
|
||||
|
||||
encryptWithKey(this.crypto_key, data).then(e => {
|
||||
self.encrypted = e;
|
||||
|
||||
navigator.clipboard.writeText(self.encrypted);
|
||||
})
|
||||
async encrypt_text (data) {
|
||||
try {
|
||||
this.encrypted = await this.encrypt(data)
|
||||
this.encryption_error = ""
|
||||
} catch (err) {
|
||||
this.encryption_error = "{{_('Error while encryption of message.') | escapejs}}"
|
||||
}
|
||||
},
|
||||
|
||||
decrypt: function(data) {
|
||||
var self = this;
|
||||
|
||||
decryptWithKey(this.crypto_key, data).then(e => {
|
||||
self.decrypted = e;
|
||||
|
||||
navigator.clipboard.writeText(self.encrypted);
|
||||
})
|
||||
async decrypt_text (data) {
|
||||
try {
|
||||
this.decrypted = await this.decrypt(data)
|
||||
this.decryption_error = ""
|
||||
} catch (err) {
|
||||
this.decryption_error = "{{_('Error while decryption of message.') | escapejs}}"
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,18 +1,10 @@
|
||||
{% load i18n %}
|
||||
|
||||
<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>
|
||||
<div class="card-body">
|
||||
<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>
|
||||
<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>
|
||||
{% 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>
|
||||
|
||||
@ -1,108 +1,25 @@
|
||||
const rvalidate = Vue.resource(Urls["users:k356.validate"]);
|
||||
|
||||
Loading = {
|
||||
template: "#Loading",
|
||||
router_path: "/",
|
||||
|
||||
props: ["crypto_key"],
|
||||
props: [],
|
||||
data: function() {
|
||||
return {
|
||||
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: {
|
||||
|
||||
generate_import_key: function(password) {
|
||||
|
||||
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 = "";
|
||||
async generate_aes_key (password) {
|
||||
const key = await this.deriveKeyFromPassphrase(password, "{{ user_setting.id }}--aes")
|
||||
|
||||
this.$emit("update_key", key)
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
@ -1,9 +1,16 @@
|
||||
from django import template
|
||||
from django.utils.html import mark_safe
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def define(context, key, value):
|
||||
context.dicts[0][key] = value
|
||||
def define(context, key, *args):
|
||||
if len(args) == 1:
|
||||
context.dicts[0][key] = args[0]
|
||||
|
||||
else:
|
||||
context.dicts[0][key] = [mark_safe(arg) for arg in args]
|
||||
|
||||
return ""
|
||||
|
||||
19
k356/node_modules/.package-lock.json
generated
vendored
Normal file
19
k356/node_modules/.package-lock.json
generated
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
12
k356/node_modules/@jamescoyle/svg-icon/.github/FUNDING.yml
generated
vendored
Normal file
12
k356/node_modules/@jamescoyle/svg-icon/.github/FUNDING.yml
generated
vendored
Normal 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
21
k356/node_modules/@jamescoyle/svg-icon/LICENSE
generated
vendored
Normal 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
42
k356/node_modules/@jamescoyle/svg-icon/README.md
generated
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
# SVG Icon WebComponent
|
||||
|
||||
[](https://www.npmjs.com/package/@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
29
k356/node_modules/@jamescoyle/svg-icon/package.json
generated
vendored
Normal 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"
|
||||
}
|
||||
15
k356/node_modules/@jamescoyle/svg-icon/tests/index.html
generated
vendored
Normal file
15
k356/node_modules/@jamescoyle/svg-icon/tests/index.html
generated
vendored
Normal 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
20
k356/node_modules/@mdi/js/LICENSE
generated
vendored
Normal 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
34
k356/node_modules/@mdi/js/README.md
generated
vendored
Normal 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
18
k356/node_modules/@mdi/js/build.js
generated
vendored
Normal 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
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
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
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
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
37
k356/node_modules/@mdi/js/package.json
generated
vendored
Normal 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
25
k356/package-lock.json
generated
Normal 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
6
k356/package.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@jamescoyle/svg-icon": "^0.2.1",
|
||||
"@mdi/js": "^7.4.47"
|
||||
}
|
||||
}
|
||||
391
k356/poetry.lock
generated
Normal file
391
k356/poetry.lock
generated
Normal 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
109
k356/pyproject.toml
Normal 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__.:"
|
||||
]
|
||||
|
||||
@ -8,25 +8,26 @@
|
||||
|
||||
<link rel="shortcut icon" href="{% static 'img/favicon.png' %}">
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue-resource@1.5.3"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/js-cookie@3.0.5/dist/js.cookie.min.js"></script>
|
||||
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
|
||||
<script src="{% static "vue/index.js" %}"></script>
|
||||
<script src="{% static "sweetalert2/index" %}"></script>
|
||||
<script src="{% static "vue-resource/index" %}"></script>
|
||||
<script src="{% static "js.cookie.min/index.js" %}"></script>
|
||||
<script src="{% static "vue-router/index.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">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||
<link href="{% static "bootstrap.min/index.css" %}" rel="stylesheet">
|
||||
<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>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>
|
||||
<link href="{% static "vuetify.min/index.css" %}" rel="stylesheet">
|
||||
<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://fonts.cdnfonts.com/css/jetbrains-mono" rel="stylesheet">
|
||||
<link href="{% static "jetbrains-mono/index" %}">
|
||||
<style>
|
||||
.font {
|
||||
font-family: 'JetBrains Mono', sans-serif;
|
||||
@ -49,7 +50,7 @@
|
||||
<div class="collapse navbar-collapse" id="navbarColor01">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<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" %}
|
||||
<span class="visually-hidden"></span>
|
||||
</router-link>
|
||||
@ -58,11 +59,12 @@
|
||||
<router-link class="nav-link" to="/ItemView">{% trans "Items" %}</router-link>
|
||||
</li>
|
||||
<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 class="nav-item">
|
||||
<router-link class="nav-link" to="/EncryptionTesting">{% trans "Encryption" %}</router-link>
|
||||
</li>
|
||||
{% if request.user.is_superuser %}
|
||||
<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>
|
||||
<div class="dropdown-menu">
|
||||
@ -73,6 +75,7 @@
|
||||
<a class="dropdown-item" href="/admin/">{% trans "Admin" %}</a>
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<form class="d-flex">
|
||||
<input class="form-control me-sm-2" type="search" placeholder="Search">
|
||||
@ -83,9 +86,9 @@
|
||||
</nav>
|
||||
|
||||
<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">
|
||||
<router-view :crypto_key="key"></router-view>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@ -103,56 +106,7 @@
|
||||
{% endfor %}
|
||||
|
||||
<script type="text/javascript">
|
||||
Vue.use(VueRouter);
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
{% include "vue/index.js" %}
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
@ -164,16 +118,5 @@
|
||||
refresh_csrftoken();
|
||||
</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>
|
||||
</html>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
{% block component %}
|
||||
<v-data-table
|
||||
:headers="items_headers"
|
||||
:headers="citems_headers"
|
||||
:items="items"
|
||||
:items-per-page="50"
|
||||
:search="search"
|
||||
@ -34,6 +34,17 @@
|
||||
<v-row>
|
||||
<template v-for="field in editable_fields">
|
||||
<template v-if="field.field_widget == 'v-select'">
|
||||
<template v-if="field.choices">
|
||||
<v-select
|
||||
v-model="editedItem[field.value]"
|
||||
:items="field.choices"
|
||||
:label="field.text"
|
||||
item-text="text"
|
||||
item-value="value"
|
||||
persistent-hint>
|
||||
</v-select>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-select
|
||||
v-model="editedItem[field.value]"
|
||||
:items="items_relations[field.value]"
|
||||
@ -46,6 +57,7 @@
|
||||
</template>
|
||||
</v-select>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<component :is="field.field_widget" v-model="editedItem[field.value]" :label="field.text"></component>
|
||||
</template>
|
||||
@ -91,7 +103,23 @@
|
||||
<template v-slot:item.id="{ item }">
|
||||
[[ item.id.slice(0, 8) ]]...
|
||||
</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 }">
|
||||
<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 @click="deleteItem(item)">mdi-delete</v-icon>
|
||||
</template>
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
{% block component %}
|
||||
{{ name }} = {
|
||||
template: "#{{ name }}",
|
||||
router_path: "/{{ name }}",
|
||||
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() {
|
||||
return {
|
||||
@ -12,6 +13,8 @@
|
||||
defaultItem: {},
|
||||
editedItem: {},
|
||||
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() {
|
||||
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: {
|
||||
@ -32,6 +39,14 @@
|
||||
|
||||
methods: {
|
||||
|
||||
formatDate (date) {
|
||||
return formatDate(date)
|
||||
},
|
||||
|
||||
showItem (item) {
|
||||
this.$router.replace({ name: this.show_url, params: { id: item.id }})
|
||||
},
|
||||
|
||||
editItem (item) {
|
||||
this.editedIndex = this.items.indexOf(item)
|
||||
this.editedItem = Object.assign({}, item)
|
||||
@ -45,9 +60,6 @@
|
||||
},
|
||||
|
||||
deleteItemConfirm () {
|
||||
var self = this
|
||||
var item = this.items[this.editedIndex]
|
||||
|
||||
this.$emit("deleteItem", this.editedIndex)
|
||||
this.closeDelete()
|
||||
|
||||
@ -74,7 +86,6 @@
|
||||
|
||||
} else {
|
||||
|
||||
console.log('createItem emit', this.editedItem)
|
||||
this.$emit("createItem", this.editedItem)
|
||||
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
var buf = new ArrayBuffer(str.length);
|
||||
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) {
|
||||
// Encrypt data with key. Return a Promise
|
||||
return new Promise((resolve) => {
|
||||
|
||||
117
k356/templates/vue/index.js
Normal file
117
k356/templates/vue/index.js
Normal 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)
|
||||
})
|
||||
160
k356/templates/vue/plugins.js
Normal file
160
k356/templates/vue/plugins.js
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -6,11 +6,12 @@ from users.models import UserSettings
|
||||
|
||||
@admin.action(description="Remove the key from the users")
|
||||
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)
|
||||
class UserSettingsAdmin(admin.ModelAdmin):
|
||||
list_display = ("user", "k356_key")
|
||||
list_display = ("user",)
|
||||
|
||||
actions = [remove_key]
|
||||
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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",
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -1,6 +1,7 @@
|
||||
from uuid import uuid4
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
|
||||
|
||||
from app.utils.models import BaseModel
|
||||
|
||||
|
||||
@ -10,5 +11,6 @@ User = get_user_model()
|
||||
class UserSettings(BaseModel):
|
||||
user = models.OneToOneField(User, on_delete=models.PROTECT, related_name="setting")
|
||||
|
||||
k356_key = models.BooleanField(default=False)
|
||||
k356_key_fingerprint = models.CharField(null=True)
|
||||
# The private and public key are wrapped with the AES key from the front-end
|
||||
public_key = models.TextField(max_length=2048, null=True)
|
||||
private_key = models.TextField(max_length=2048, null=True)
|
||||
|
||||
@ -3,8 +3,10 @@ from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
app_name = "users"
|
||||
|
||||
urlpatterns = [
|
||||
path("keys", views.keys, name="keys"),
|
||||
path("k356/validate", views.k356_validate, name="k356.validate"),
|
||||
]
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import json
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils.translation import gettext as _
|
||||
from django.http import JsonResponse
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
|
||||
login_required()
|
||||
import json
|
||||
|
||||
|
||||
@login_required
|
||||
def k356_validate(request):
|
||||
|
||||
us = request.user.setting
|
||||
@ -36,7 +38,7 @@ def k356_validate(request):
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"ok": True
|
||||
"ok": True,
|
||||
}
|
||||
)
|
||||
|
||||
@ -46,3 +48,30 @@ def k356_validate(request):
|
||||
"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
109
k356/wrap.js
Normal 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))
|
||||
Loading…
Reference in New Issue
Block a user