First commit
This commit is contained in:
commit
f3116a6876
182
.gitignore
vendored
Normal file
182
.gitignore
vendored
Normal 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
0
k356/app/__init__.py
Normal file
16
k356/app/asgi.py
Normal file
16
k356/app/asgi.py
Normal 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
154
k356/app/settings.py
Normal 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
28
k356/app/urls.py
Normal 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'),
|
||||||
|
]
|
||||||
0
k356/app/utils/__init__.py
Normal file
0
k356/app/utils/__init__.py
Normal file
0
k356/app/utils/api/__init__.py
Normal file
0
k356/app/utils/api/__init__.py
Normal file
21
k356/app/utils/api/api_list.py
Normal file
21
k356/app/utils/api/api_list.py
Normal 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
|
||||||
37
k356/app/utils/extra_context.py
Normal file
37
k356/app/utils/extra_context.py
Normal 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
62
k356/app/utils/models.py
Normal 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
16
k356/app/wsgi.py
Normal 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
0
k356/items/__init__.py
Normal file
32
k356/items/admin.py
Normal file
32
k356/items/admin.py
Normal 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
6
k356/items/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ItemsConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'items'
|
||||||
130
k356/items/migrations/0001_initial.py
Normal file
130
k356/items/migrations/0001_initial.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
k356/items/migrations/__init__.py
Normal file
0
k356/items/migrations/__init__.py
Normal file
88
k356/items/models.py
Normal file
88
k356/items/models.py
Normal 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",
|
||||||
|
)
|
||||||
5
k356/items/templates/components/item/template.html
Normal file
5
k356/items/templates/components/item/template.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input class="form-control" type="text" v-model="item.name">
|
||||||
|
</div>
|
||||||
4
k356/items/templates/components/item/vue.js
Normal file
4
k356/items/templates/components/item/vue.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
item = {
|
||||||
|
template: "#item",
|
||||||
|
props: ["crypto_key", "item"],
|
||||||
|
}
|
||||||
88
k356/items/templates/components/item_list/template.html
Normal file
88
k356/items/templates/components/item_list/template.html
Normal 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>
|
||||||
191
k356/items/templates/components/item_list/vue.js
Normal file
191
k356/items/templates/components/item_list/vue.js
Normal 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
3
k356/items/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
10
k356/items/urls.py
Normal file
10
k356/items/urls.py
Normal 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"),
|
||||||
|
]
|
||||||
0
k356/items/views/__init__.py
Normal file
0
k356/items/views/__init__.py
Normal file
90
k356/items/views/item.py
Normal file
90
k356/items/views/item.py
Normal 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
0
k356/main/__init__.py
Normal file
3
k356/main/admin.py
Normal file
3
k356/main/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
k356/main/apps.py
Normal file
6
k356/main/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class MainConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'main'
|
||||||
0
k356/main/migrations/__init__.py
Normal file
0
k356/main/migrations/__init__.py
Normal file
3
k356/main/models.py
Normal file
3
k356/main/models.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
@ -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>
|
||||||
34
k356/main/templates/components/encryption-testing/vue.js
Normal file
34
k356/main/templates/components/encryption-testing/vue.js
Normal 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);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
18
k356/main/templates/components/k356-loading/template.html
Normal file
18
k356/main/templates/components/k356-loading/template.html
Normal 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>
|
||||||
109
k356/main/templates/components/k356-loading/vue.js
Normal file
109
k356/main/templates/components/k356-loading/vue.js
Normal 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 = "";
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
10
k356/main/templates/main/home.html
Normal file
10
k356/main/templates/main/home.html
Normal 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
3
k356/main/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
12
k356/main/urls.py
Normal file
12
k356/main/urls.py
Normal 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
7
k356/main/views.py
Normal 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
22
k356/manage.py
Executable 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()
|
||||||
BIN
k356/static_source/img/favicon.png
Normal file
BIN
k356/static_source/img/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
163
k356/templates/base.html
Normal file
163
k356/templates/base.html
Normal 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
56
k356/templates/scripts.js
Normal 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
0
k356/users/__init__.py
Normal file
16
k356/users/admin.py
Normal file
16
k356/users/admin.py
Normal 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
6
k356/users/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class UsersConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'users'
|
||||||
34
k356/users/migrations/0001_initial.py
Normal file
34
k356/users/migrations/0001_initial.py
Normal 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,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
18
k356/users/migrations/0002_usersettings_custom_identifier.py
Normal file
18
k356/users/migrations/0002_usersettings_custom_identifier.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
k356/users/migrations/__init__.py
Normal file
0
k356/users/migrations/__init__.py
Normal file
14
k356/users/models.py
Normal file
14
k356/users/models.py
Normal 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
3
k356/users/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
10
k356/users/urls.py
Normal file
10
k356/users/urls.py
Normal 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
48
k356/users/views.py
Normal 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,
|
||||||
|
}
|
||||||
|
)
|
||||||
Loading…
Reference in New Issue
Block a user