First commit

This commit is contained in:
Loïc Gremaud 2024-09-26 23:59:03 +02:00
commit f3116a6876
Signed by: Legrems
GPG Key ID: D4620E6DF3E0121D
53 changed files with 1822 additions and 0 deletions

182
.gitignore vendored Normal file
View File

@ -0,0 +1,182 @@
# Created by https://www.toptal.com/developers/gitignore/api/python
# Edit at https://www.toptal.com/developers/gitignore?templates=python
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# ruff
.ruff_cache/
# LSP config files
pyrightconfig.json
# End of https://www.toptal.com/developers/gitignore/api/python
cache
output
config.ini
settingsLocal.py
output.csv

0
k356/app/__init__.py Normal file
View File

16
k356/app/asgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
ASGI config for k356 project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')
application = get_asgi_application()

154
k356/app/settings.py Normal file
View File

@ -0,0 +1,154 @@
"""
Django settings for k356 project.
Generated by 'django-admin startproject' using Django 5.1.1.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
from pathlib import Path
from django.utils.module_loading import import_module
import os
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-$440wv7cqb$-umfo-x%w_@p3g5kuuk1(!rv#=7*gzndx4_h4ds'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'main',
'users',
'items',
'django_js_reverse',
]
STATIC_URL = "/static/"
STATICFILES_DIRS = (os.path.join(BASE_DIR, "static_source"),)
STATICFILES_FINDERS = (
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
# "djangobower.finders.BowerFinder",
# "compressor.finders.CompressorFinder",
)
STATIC_ROOT = os.path.join(BASE_DIR, "static")
STORAGES = {
"staticfiles": {
# "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
"BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
}
}
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'app.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(BASE_DIR, "templates"),
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'app.utils.extra_context.extra_context',
],
},
},
]
WSGI_APPLICATION = 'app.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
DATABASES = {}
# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
from app.settingsLocal import *
for extra_app in EXTRA_APPS:
INSTALLED_APPS.append(extra_app)
tmp_app = import_module(extra_app)

28
k356/app/urls.py Normal file
View File

@ -0,0 +1,28 @@
"""
URL configuration for k356 project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import include, path
from django_js_reverse.views import urls_js
urlpatterns = [
path("", include("main.urls")),
path("items/", include("items.urls")),
path("users/", include("users.urls")),
path("admin/", admin.site.urls),
path('reverse.js', urls_js, name='reverse_js'),
]

View File

View File

View File

@ -0,0 +1,21 @@
def header_for_table(model):
headers = model.objects.headers()
return [
{
"text": value,
"value": key,
}
for key, value in headers.items()
] + [
{
"text": "Actions",
"value": "actions",
"sortable": False,
},
]
def encrypted_fields(model):
return model.Encryption.fields

View File

@ -0,0 +1,37 @@
from django.conf import settings
from django.apps import apps
from users.models import UserSettings
from pathlib import Path
def extra_context(request):
if not request.user.is_anonymous:
user_settings, __ = UserSettings.objects.get_or_create(user=request.user)
else:
user_settings = None
components = []
for app in apps.get_app_configs():
p = Path(settings.BASE_DIR) / app.name / "templates/components/"
if p.exists():
for path in p.iterdir():
components.append(path.name)
return {
"user_settings": user_settings,
"templates": {
component: f"components/{component}/template.html"
for component in components
},
"components": {
component: {
"path": f"components/{component}/vue.js",
"flat_name": component.replace("-", "_").lower(),
}
for component in components
}
}

62
k356/app/utils/models.py Normal file
View File

@ -0,0 +1,62 @@
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
User = get_user_model()
class BaseQuerySet(models.QuerySet):
def headers(self):
"""Return the list of header for a list."""
fields = {}
for field in self.model._meta.fields:
if field.name in self.model.Serialization.excluded_fields:
continue
# if isinstance(field, RelatedField):
# fields[f"{field.name}__name"] = field.verbose_name.capitalize()
fields[field.name] = field.verbose_name.capitalize()
return fields
def serialize(self):
"""Serialize a queryset."""
fields = []
for field_name, _ in self.headers().items():
fields.append(field_name)
return self.values(*fields)
class BaseManager(models.Manager.from_queryset(BaseQuerySet)):
pass
class BaseModel(models.Model):
class Meta:
abstract = True
class Serialization:
# Exclude fields from serialization
excluded_fields = []
excluded_fields_edit = ["id", "created_at", "last_modified_at"]
class Encryption:
fields = ["name", "description", "custom_identifier"]
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
name = models.TextField(max_length=2048)
description = models.TextField(max_length=2048)
custom_identifier = models.TextField(max_length=2048, null=True)
created_at = models.DateTimeField(auto_now_add=True)
last_modified_at = models.DateTimeField(auto_now=True)
objects = BaseManager()

16
k356/app/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for k356 project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')
application = get_wsgi_application()

0
k356/items/__init__.py Normal file
View File

32
k356/items/admin.py Normal file
View File

@ -0,0 +1,32 @@
from django.contrib import admin
from items.models import ItemType, Item, ItemRelation, Property, LinkedProperty, RelationProperty
@admin.register(ItemType)
class ItemTypeAdmin(admin.ModelAdmin):
list_display = ("id", )
@admin.register(Item)
class ItemAdmin(admin.ModelAdmin):
list_display = ("id", )
@admin.register(ItemRelation)
class ItemRelationAdmin(admin.ModelAdmin):
list_display = ("id", )
@admin.register(Property)
class PropertyAdmin(admin.ModelAdmin):
list_display = ("id", "type")
@admin.register(LinkedProperty)
class LinkedPropertyAdmin(admin.ModelAdmin):
list_display = ("id", "property__type", "item")
@admin.register(RelationProperty)
class RelationPropertyAdmin(admin.ModelAdmin):
list_display = ("id", "property__type", "relation")

6
k356/items/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ItemsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'items'

View File

@ -0,0 +1,130 @@
# Generated by Django 5.1.1 on 2024-09-25 17:43
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('users', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Item',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('last_modified_at', models.DateTimeField(auto_now=True)),
('name', models.TextField(max_length=2048)),
('description', models.TextField(max_length=2048)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='users.usersettings')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='ItemRelation',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.TextField(max_length=2048)),
('description', models.TextField(max_length=2048)),
('created_at', models.DateTimeField(auto_now_add=True)),
('last_modified_at', models.DateTimeField(auto_now=True)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='users.usersettings')),
('child', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='parents', to='items.item')),
('parent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='children', to='items.item')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='item',
name='relations',
field=models.ManyToManyField(through='items.ItemRelation', to='items.item'),
),
migrations.CreateModel(
name='ItemType',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('last_modified_at', models.DateTimeField(auto_now=True)),
('name', models.TextField(max_length=2048)),
('description', models.TextField(max_length=2048)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='users.usersettings')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='item',
name='type',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='items.itemtype'),
),
migrations.CreateModel(
name='Property',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.TextField(max_length=2048)),
('description', models.TextField(max_length=2048)),
('created_at', models.DateTimeField(auto_now_add=True)),
('last_modified_at', models.DateTimeField(auto_now=True)),
('type', models.CharField(choices=[('text', 'Text'), ('date', 'Date'), ('datetime', 'Date & time')], default='text', max_length=32)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='users.usersettings')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='LinkedProperty',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.TextField(max_length=2048)),
('description', models.TextField(max_length=2048)),
('created_at', models.DateTimeField(auto_now_add=True)),
('last_modified_at', models.DateTimeField(auto_now=True)),
('value', models.TextField(max_length=2048)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='users.usersettings')),
('item', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='linked_properties', to='items.item')),
('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='items.property')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='item',
name='properties',
field=models.ManyToManyField(related_name='items', through='items.LinkedProperty', to='items.property'),
),
migrations.CreateModel(
name='RelationProperty',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.TextField(max_length=2048)),
('description', models.TextField(max_length=2048)),
('created_at', models.DateTimeField(auto_now_add=True)),
('last_modified_at', models.DateTimeField(auto_now=True)),
('value', models.TextField(max_length=2048)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='users.usersettings')),
('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='items.property')),
('relation', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='relation_properties', to='items.itemrelation')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='itemrelation',
name='properties',
field=models.ManyToManyField(related_name='relations', through='items.RelationProperty', to='items.property'),
),
]

View File

@ -0,0 +1,43 @@
# Generated by Django 5.1.1 on 2024-09-25 20:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('items', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='item',
name='custom_identifier',
field=models.TextField(max_length=2048, null=True),
),
migrations.AddField(
model_name='itemrelation',
name='custom_identifier',
field=models.TextField(max_length=2048, null=True),
),
migrations.AddField(
model_name='itemtype',
name='custom_identifier',
field=models.TextField(max_length=2048, null=True),
),
migrations.AddField(
model_name='linkedproperty',
name='custom_identifier',
field=models.TextField(max_length=2048, null=True),
),
migrations.AddField(
model_name='property',
name='custom_identifier',
field=models.TextField(max_length=2048, null=True),
),
migrations.AddField(
model_name='relationproperty',
name='custom_identifier',
field=models.TextField(max_length=2048, null=True),
),
]

View File

88
k356/items/models.py Normal file
View File

@ -0,0 +1,88 @@
from app.utils.models import BaseModel
from django.contrib.auth.forms import gettext as _
from django.db import models
from users.models import UserSettings
class ItemBase(BaseModel):
class Meta:
abstract = True
class Serialization(BaseModel.Serialization):
excluded_fields = BaseModel.Serialization.excluded_fields + ["author"]
excluded_fields_edit = BaseModel.Serialization.excluded_fields_edit + ["author"]
author = models.ForeignKey(UserSettings, on_delete=models.PROTECT)
class ItemType(ItemBase):
name = models.TextField(max_length=2048)
description = models.TextField(max_length=2048)
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"
)
properties = models.ManyToManyField(
"items.Property", through="items.RelationProperty", related_name="relations"
)
class Item(ItemBase):
name = models.TextField(max_length=2048)
description = models.TextField(max_length=2048)
type = models.ForeignKey(ItemType, on_delete=models.PROTECT)
relations = models.ManyToManyField(
"items.Item",
through=ItemRelation,
)
properties = models.ManyToManyField(
"items.Property",
through="LinkedProperty",
related_name="items",
)
class PropertyType(models.TextChoices):
TEXT = "text", _("Text")
DATE = "date", _("Date")
DATETIME = "datetime", _("Date & time")
# TODO: Add more property types (location, etc)
class Property(ItemBase):
type = models.CharField(
max_length=32, choices=PropertyType.choices, default=PropertyType.TEXT
)
class BaseLinkedProperty(ItemBase):
class Meta:
abstract = True
property = models.ForeignKey(Property, on_delete=models.CASCADE)
# Value is encrypted too
value = models.TextField(max_length=2048)
class LinkedProperty(BaseLinkedProperty):
item = models.ForeignKey(
Item, on_delete=models.CASCADE, null=True, related_name="linked_properties"
)
class RelationProperty(BaseLinkedProperty):
relation = models.ForeignKey(
ItemRelation,
on_delete=models.CASCADE,
null=True,
related_name="relation_properties",
)

View File

@ -0,0 +1,5 @@
{% load i18n %}
<div>
<input class="form-control" type="text" v-model="item.name">
</div>

View File

@ -0,0 +1,4 @@
item = {
template: "#item",
props: ["crypto_key", "item"],
}

View File

@ -0,0 +1,88 @@
{% load i18n %}
<div>
<div class="card mt-4 pt-2 ps-lg-2">
<h5 class="card-header">{% trans "Your items" %}</h5>
<div class="card-body">
<v-data-table
:headers="data.items_headers"
:items="data.items"
:items-per-page="50"
:search="search"
group-by="type"
loading
dense>
<template v-slot:top>
<v-toolbar flat>
<v-text-field v-model="search" append-icon="mdi-magnify" label="Search" single-line hide-details></v-text-field>
<v-divider class="mx-4" insert vertical></v-divider>
<v-spacer></v-spacer>
<v-dialog v-model="dialog" max-width="500px">
<template v-slot:activator="{ on, attrs }">
<v-btn color="primary" dark class="mb-2" v-bind="attrs" v-on="on">{% trans "New item" %}</v-btn>
</template>
<v-card>
<v-card-text>
<v-container>
<v-row>
<v-text-field v-model="editedItem.name" label="Name"></v-text-field>
<v-select
v-model="editedItem.type"
:items="data.types"
label="Type"
item-text="name"
item-value="id"
persistent-hint
>
<template slot="item" slot-scope="data">
[[ data.item.name ]] - [[ data.item.custom_identifier ]]
</template>
</v-select>
<v-textarea v-model="editedItem.description" label="Description"></v-textarea>
<v-text-field v-model="editedItem.custom_identifier" label="Identifier"></v-text-field>
</v-row>
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text @click="close">{% trans "Cancel" %}</v-btn>
<v-btn color="blue darken-1" text @click="save">{% trans "Save" %}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="dialogDelete" max-width="500px">
<v-card>
<v-card-title class="text-h5">{% trans "Are you sure you want to delete this item?" %}</v-card-title>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text @click="closeDelete">{% trans "Cancel" %}</v-btn>
<v-btn color="blue darken-1" text @click="deleteItemConfirm">{% trans "OK" %}</v-btn>
<v-spacer></v-spacer>
</v-card-actions>
</v-card>
</v-dialog>
</v-toolbar>
</template>
<template v-slot:item.actions="{ item }">
<v-icon small class="mr-2" @click="editItem(item)">mdi-pencil</v-icon>
<v-icon small @click="deleteItem(item)">mdi-delete</v-icon>
</template>
<template v-slot:no-data>
<v-btn color="primary" @click="initialize">{% trans "Reset" %}</v-btn>
</template>
</v-data-table>
</div>
</div>
</div>

View File

@ -0,0 +1,191 @@
item_list = {
template: "#item_list",
delimiters: ["[[", "]]"],
props: ["crypto_key", "locked"],
data: function() {
return {
dialog: false,
dialogDelete: false,
editedIndex: -1,
defaultItem: {},
editedItem: {},
search: null,
data: {},
}
},
mounted: function() {
var self = this;
this.$http.get(Urls["items:list"]()).then(response => {
Object.keys(response.data.result).forEach(name => {
self.$set(self.data, name, response.data.result[name]);
});
self.data.items.forEach(item => {
self.decryptItem(item);
});
}).catch(err => {
Swal.fire({title: "{{_('Error during loading of items.') | escapejs}}", icon: "error", position:"top-end", showConfirmButton: false, toast: true, timer: 1000});
});
},
computed: {
formTitle () {
return this.editedIndex === -1 ? "{{_('New item') | escapejs}}" : "{{_('Edit item') | escapejs}}"
},
},
watch: {
dialog (val) {
val || this.close()
},
dialogDelete (val) {
val || this.closeDelete()
},
},
methods: {
decryptItem (item) {
this.data.items_encrypted.forEach(field => {
decryptWithKey(this.crypto_key, item[field]).then(dec => {
item[field] = dec;
})
});
return item;
},
editItem (item) {
this.editedIndex = this.data.items.indexOf(item)
this.editedItem = Object.assign({}, item)
this.dialog = true
},
deleteItem (item) {
this.editedIndex = this.data.items.indexOf(item)
this.editedItem = Object.assign({}, item)
this.dialogDelete = true
},
deleteItemConfirm () {
var item = this.data.items[this.editedIndex];
this.item_edition("delete", item).then(response => {
this.data.items.splice(this.data.items.indexOf(item), 1)
Swal.fire({title: "{{_('Item successfully deleted!') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000});
});
this.closeDelete()
},
close () {
this.dialog = false
this.$nextTick(() => {
this.editedItem = Object.assign({}, this.defaultItem)
this.editedIndex = -1
})
},
closeDelete () {
this.dialogDelete = false
this.$nextTick(() => {
this.editedItem = Object.assign({}, this.defaultItem)
this.editedIndex = -1
})
},
item_edition (method, item) {
// Return a Promise
var self = this;
return new Promise((resolve) => {
let url = Urls["items:edit"](item.id)
if (item.id == undefined || item.id == null) {
url = Urls["items:create"]()
}
let promises = self.data.items_encrypted.map(field => {
// Encrypt all necessary fields
if (item[field] == null) {
return null;
}
return new Promise((resolve) => {
return encryptWithKey(self.crypto_key, item[field]).then(enc => {
resolve({field: field, value: enc});
});
});
}).filter(e => e != null);
Promise.all(promises).then(values => {
values.forEach(value => {
item[value.field] = value.value;
});
self.$http[method](url, item).then(response => {
resolve(response.data);
}).catch(err => {
let msg = "{{_('Error during edition of item') | escapejs}}";
if (method == "delete") {
msg = "{{_('Error during deletion of item') | escapejs}}";
}
Swal.fire({title: msg, icon: "error", position:"top-end", showConfirmButton: false, toast: true, timer: 1000});
});
});
});
},
save () {
if (this.editedIndex > -1) {
var self = this;
this.item_edition("post", this.editedItem).then(data => {
self.data.items.splice(this.data.items.indexOf(self.editedItem), 1)
console.log('pre edit', data.item)
new_item = self.decryptItem(data.item);
console.log('edited item', new_item);
// self.data.items.push(self.decryptItem(data.item));
Swal.fire({title: "{{_('Item successfully edited') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000});
});
} else {
var self = this;
this.item_edition("post", this.editedItem).then(data => {
new_item = self.decryptItem(data.item);
console.log('new item', new_item);
// self.data.items.push(self.decryptItem(data.item));
// console.log(data.item);
Swal.fire({title: "{{_('Item successfully created!') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000});
});
}
this.close()
},
}
}

3
k356/items/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

10
k356/items/urls.py Normal file
View File

@ -0,0 +1,10 @@
from django.urls import path
from items.views import item as item_view
app_name = "items"
urlpatterns = [
path("", item_view.item_list, name="list"),
path("<uuid:id>", item_view.item_edit, name="edit"),
path("create", item_view.item_edit, {"id": None}, name="create"),
]

View File

90
k356/items/views/item.py Normal file
View File

@ -0,0 +1,90 @@
import json
from app.utils.api.api_list import encrypted_fields, 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
@login_required
def item_list(request):
items = Item.objects.filter(author=request.user.setting)
types = ItemType.objects.filter(author=request.user.setting)
return JsonResponse(
{
"result": {
"items": list(items.serialize()),
"types": list(types.serialize()),
"items_headers": header_for_table(Item),
"types_headers": header_for_table(ItemType),
"items_encrypted": encrypted_fields(Item),
"types_encrypted": encrypted_fields(ItemType),
},
"count": items.count(),
}
)
@login_required
def item_edit(request, id=None):
"""Create/edit item view."""
if id:
item = Item.objects.filter(id=id, author=request.user.setting).first()
else:
item = Item(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(
{
"item": Item.objects.filter(id=item.id).serialize().first(),
}
)

0
k356/main/__init__.py Normal file
View File

3
k356/main/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
k356/main/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class MainConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'main'

View File

3
k356/main/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

@ -0,0 +1,21 @@
{% load i18n %}
<div>
<div class="card mt-4 pt-2 ps-lg-2">
<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="encrypted" disabled></textarea>
</div>
</div>
<div class="card mt-4 pt-2 ps-lg-2">
<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="decrypted" disabled></textarea>
</div>
</div>
</div>

View File

@ -0,0 +1,34 @@
encryption_testing = {
template: "#encryption-testing",
props: ["crypto_key"],
data: function() {
return {
text: '',
encrypted: '',
encrypted_text: '',
decrypted: '',
}
},
methods: {
encrypt: function(data) {
var self = this;
encryptWithKey(this.crypto_key, data).then(e => {
self.encrypted = e;
navigator.clipboard.writeText(self.encrypted);
})
},
decrypt: function(data) {
var self = this;
decryptWithKey(this.crypto_key, data).then(e => {
self.decrypted = e;
navigator.clipboard.writeText(self.encrypted);
})
},
}
}

View File

@ -0,0 +1,18 @@
{% 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>
</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>

View File

@ -0,0 +1,109 @@
const rvalidate = Vue.resource(Urls["users:k356.validate"]);
k356_loading = {
template: "#k356-loading",
props: ["crypto_key"],
data: function() {
return {
password: '',
k356_fingerprint: "{{ user_settings.k356_fingerprint }}",
}
},
mounted: function() {},
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 = "";
},
}
}

View File

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans "Home" %}{% endblock %}
{% block body %}
<k356-loading></k356-loading>
{% endblock %}

3
k356/main/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

12
k356/main/urls.py Normal file
View File

@ -0,0 +1,12 @@
from django.urls import path
from . import views
app_name = "main"
urlpatterns = [
path("", views.home, name="home"),
]

7
k356/main/views.py Normal file
View File

@ -0,0 +1,7 @@
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
@login_required
def home(request):
return render(request, "base.html", {})

22
k356/manage.py Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

163
k356/templates/base.html Normal file
View File

@ -0,0 +1,163 @@
{% load i18n static %}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{% endblock %}</title>
<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://cdnjs.cloudflare.com/ajax/libs/vue-resource/0.6.1/vue-resource.min.js"></script> -->
<script src="https://cdn.jsdelivr.net/npm/vue-resource@1.5.3"></script>
<script src="https://unpkg.com/htmx.org@2.0.2"></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>
<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="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="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css" rel="stylesheet">
</head>
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
<v-app>
<div id="main" data-app>
<nav class="navbar navbar-expand-lg bg-primary" data-bs-theme="dark">
<div class="container-fluid">
<a href="#" @click="lock_me" class="navbar-brand">{% trans "Lock" %}</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarColor01" aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<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' %}">
{% trans "K356" %}
<span class="visually-hidden"></span>
</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" to="/item_list">{% trans "Items" %}</router-link>
</li>
<li class="nav-item">
<a class="nav-link" href="#">{% trans "Properties" %}</a>
</li>
<li class="nav-item">
<router-link class="nav-link" to="/encryption-testing">{% trans "Encryption" %}</router-link>
</li>
<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">
<a class="dropdown-item" href="#">Action</a>
<a class="dropdown-item" href="#">Another action</a>
<a class="dropdown-item" href="#">Something else here</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/admin/">{% trans "Admin" %}</a>
</div>
</li>
</ul>
<form class="d-flex">
<input class="form-control me-sm-2" type="search" placeholder="Search">
<button class="btn btn-secondary my-2 my-sm-0" type="submit">Search</button>
</form>
</div>
</div>
</nav>
<div id="app" class="container">
<k356-loading :crypto_key="key" @update_key="update_key" v-if="locked"></k356-loading>
<template v-if="!locked">
<router-view :crypto_key="key"></router-view>
</template>
</div>
</div>
</v-app>
<script type="text/javascript">
{% include 'scripts.js' %}
</script>
{% for name, path in templates.items %}
<script type="text/x-template" id="{{ name }}">
{% include path %}
</script>
{% endfor %}
<script type="text/javascript">
Vue.use(VueRouter);
Vue.config.delimiters = ["[[", "]]"];
{% for name, value in components.items %}
{% include value.path %}
Vue.component("{{ name }}", {{ value.flat_name }});
{% endfor %}
const routes = [
{ path: '/', component: null },
{% for name, value in components.items %}
{
path: "/{{ name }}",
component: {{ value.flat_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) {
console.log(key);
this.key.key = key;
this.locked = key == null;
},
lock_me: function() {
this.locked = true;
this.key.key = null;
}
}
});
router.beforeEach((to, from) => {
// Prevent from routing if key is not present.
return approuter.key.key != null;
});
</script>
<script type="text/javascript">
function refresh_csrftoken() {
var csrftoken = Cookies.get('csrftoken');
Vue.http.headers.common['X-CSRFTOKEN'] = csrftoken;
}
refresh_csrftoken();
</script>
</body>
</html>

56
k356/templates/scripts.js Normal file
View File

@ -0,0 +1,56 @@
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 encryptWithKey(key, data) {
// Encrypt data with key. Return a Promise
return new Promise((resolve) => {
window.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: stringToArrayBuffer(key.uuid),
},
key.key,
stringToArrayBuffer(data)
).then(text => {
resolve(btoa(arrayBufferToString(text)));
});
});
}
function decryptWithKey(key, data) {
// Decrypt data with key. Return a Promise
return new Promise((resolve) => {
window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: stringToArrayBuffer(key.uuid),
},
key.key,
stringToArrayBuffer(atob(data))
).then(text => {
resolve(arrayBufferToString(text));
});
})
}

0
k356/users/__init__.py Normal file
View File

16
k356/users/admin.py Normal file
View File

@ -0,0 +1,16 @@
from django.contrib import admin
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)
@admin.register(UserSettings)
class UserSettingsAdmin(admin.ModelAdmin):
list_display = ("user", "k356_key")
actions = [remove_key]

6
k356/users/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'users'

View File

@ -0,0 +1,34 @@
# Generated by Django 5.1.1 on 2024-09-25 17:40
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='UserSettings',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.TextField(max_length=2048)),
('description', models.TextField(max_length=2048)),
('created_at', models.DateTimeField(auto_now_add=True)),
('last_modified_at', models.DateTimeField(auto_now=True)),
('k356_key', models.BooleanField(default=False)),
('k356_key_fingerprint', models.CharField(null=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='setting', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.1 on 2024-09-25 20:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='usersettings',
name='custom_identifier',
field=models.TextField(max_length=2048, null=True),
),
]

View File

14
k356/users/models.py Normal file
View File

@ -0,0 +1,14 @@
from uuid import uuid4
from django.contrib.auth import get_user_model
from django.db import models
from app.utils.models import BaseModel
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)

3
k356/users/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

10
k356/users/urls.py Normal file
View File

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

48
k356/users/views.py Normal file
View File

@ -0,0 +1,48 @@
import json
from django.contrib.auth.decorators import login_required
from django.utils.translation import gettext as _
from django.http import JsonResponse
login_required()
def k356_validate(request):
us = request.user.setting
if request.method == "POST":
try:
fingerprint = json.loads(request.body)["fingerprint"]
except Exception as e:
return JsonResponse(
{
"ok": False,
"error": str(e),
}
)
if us.k356_key_fingerprint:
if us.k356_key_fingerprint != fingerprint:
return JsonResponse(
{
"ok": False,
"error": _("Unable to verify key."),
}
)
else:
us.k356_key = True
us.k356_key_fingerprint = fingerprint
us.save()
return JsonResponse(
{
"ok": True
}
)
return JsonResponse(
{
"key": us.k356_key,
"fingerprint": us.k356_key_fingerprint,
}
)