Initial
This commit is contained in:
10
backend/.gitignore
vendored
Normal file
10
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
data/
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
22
backend/Dockerfile
Executable file
22
backend/Dockerfile
Executable file
@@ -0,0 +1,22 @@
|
||||
FROM python:3.11-alpine
|
||||
|
||||
EXPOSE 8099
|
||||
|
||||
# install base system
|
||||
RUN apk add --no-cache gcc libc-dev linux-headers
|
||||
RUN pip install --upgrade pip
|
||||
RUN pip install uvicorn gunicorn
|
||||
|
||||
# install requirements
|
||||
WORKDIR /django_project
|
||||
COPY ./requirements.txt .
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
# then scripts (likely wont change often)
|
||||
ENV PATH="/scripts:/py/bin:$PATH"
|
||||
COPY --chmod=700 ./scripts /scripts
|
||||
|
||||
# finally copy app (likely will invalidate cache)
|
||||
COPY . .
|
||||
|
||||
CMD ["on-deploy.sh"]
|
||||
0
backend/app/__init__.py
Normal file
0
backend/app/__init__.py
Normal file
41
backend/app/admin.py
Normal file
41
backend/app/admin.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.models import Group # , User
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from .models import Organization, Member
|
||||
|
||||
admin.site.site_header = 'Ausweis-Verwaltung' # top-most title
|
||||
admin.site.index_title = 'Ausweis' # title at root
|
||||
admin.site.site_title = 'Ausweis-Verwaltung' # suffix to <title>
|
||||
|
||||
admin.site.unregister(Group)
|
||||
# admin.site.unregister(User)
|
||||
|
||||
|
||||
@admin.register(Organization)
|
||||
class OrganizationAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {'slug': ['name']}
|
||||
list_display = ('name', 'slug', 'member_count')
|
||||
list_display_links = ('name', 'slug')
|
||||
search_fields = ('name', 'slug')
|
||||
|
||||
@admin.display(description='Mitglieder')
|
||||
def member_count(self, obj: 'Organization'):
|
||||
return obj.members.count()
|
||||
|
||||
|
||||
@admin.register(Member)
|
||||
class MemberAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ('uuid', 'url_link_full')
|
||||
list_display = (
|
||||
'name', 'member_id', 'valid_since', 'valid_until', 'url_link_short')
|
||||
search_fields = ('name', 'member_id')
|
||||
|
||||
@admin.display(description='URL')
|
||||
def url_link_short(self, obj: 'Member'):
|
||||
return mark_safe(f'<a target="blank" href="{obj.export_url}">Link</a>')
|
||||
|
||||
@admin.display(description='URL')
|
||||
def url_link_full(self, obj: 'Member'):
|
||||
return mark_safe('<a target="blank" href="{0}">{0}</a>'.format(
|
||||
obj.export_url))
|
||||
6
backend/app/apps.py
Normal file
6
backend/app/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AppConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'app'
|
||||
38
backend/app/form/file_with_img_preview.py
Normal file
38
backend/app/form/file_with_img_preview.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from django.conf import settings
|
||||
# from django.contrib.admin.widgets import AdminFileWidget
|
||||
# from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.forms import ValidationError
|
||||
from django.forms.widgets import ClearableFileInput
|
||||
|
||||
# class ImageFileWidget(AdminFileWidget):
|
||||
|
||||
|
||||
class ImageFileWidget(ClearableFileInput):
|
||||
template_name = 'forms/img-file.html'
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
context = super().get_context(name, value, attrs)
|
||||
context['MEDIA_URL'] = settings.MEDIA_URL
|
||||
return context
|
||||
|
||||
|
||||
class FileWithImagePreview(models.FileField):
|
||||
def formfield(self, **kwargs):
|
||||
# if 'widget' not in kwargs: # only if no other is set (admin UI)
|
||||
kwargs['widget'] = ImageFileWidget
|
||||
return super().formfield(**kwargs)
|
||||
|
||||
|
||||
class ImageValidator:
|
||||
help_text = 'Ideal: 250 x 320 px (JPEG oder PNG)'
|
||||
|
||||
@staticmethod
|
||||
def validate(value: 'models.FieldFile') -> None:
|
||||
# TODO: make configurable
|
||||
if value.size > 512 * 1024:
|
||||
raise ValidationError('Datei darf maximal 512 KB groß sein.')
|
||||
|
||||
content_type = getattr(value.file, 'content_type', None)
|
||||
if content_type not in ['image/png', 'image/jpeg', None]:
|
||||
raise ValidationError('Nur JPEG und PNG Bilder werden unterstützt')
|
||||
49
backend/app/migrations/0001_initial.py
Normal file
49
backend/app/migrations/0001_initial.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# Generated by Django 4.2.5 on 2023-10-02 14:25
|
||||
|
||||
import app.form.file_with_img_preview
|
||||
import app.utils
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Organization',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, verbose_name='Name')),
|
||||
('slug', models.SlugField(unique=True, verbose_name='URL Slug')),
|
||||
('exportBaseUrl', models.URLField(verbose_name='Export Base-URL')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Organisation',
|
||||
'verbose_name_plural': 'Organisationen',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Member',
|
||||
fields=[
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('secret', models.CharField(default=app.utils.random_secret, editable=False, max_length=20)),
|
||||
('member_id', models.CharField(max_length=20, verbose_name='Mitglieder-Nr.')),
|
||||
('name', models.CharField(max_length=100, verbose_name='Name')),
|
||||
('valid_since', models.DateField(verbose_name='Mitglied seit')),
|
||||
('valid_until', models.DateField(blank=True, null=True, verbose_name='Mitglied bis')),
|
||||
('image', app.form.file_with_img_preview.FileWithImagePreview(blank=True, help_text='Ideal: 250 x 320 px (JPEG oder PNG)', null=True, upload_to=app.utils.overwrite_upload, validators=[app.form.file_with_img_preview.ImageValidator.validate], verbose_name='Bild')),
|
||||
('additional', models.JSONField(blank=True, null=True, verbose_name='Zusätzliche Daten (JSON)')),
|
||||
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='members', to='app.organization')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Mitglied',
|
||||
'verbose_name_plural': 'Mitglieder',
|
||||
},
|
||||
),
|
||||
]
|
||||
0
backend/app/migrations/__init__.py
Normal file
0
backend/app/migrations/__init__.py
Normal file
2
backend/app/models/__init__.py
Normal file
2
backend/app/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .organization import Organization # noqa: F401
|
||||
from .member import Member # noqa: F401
|
||||
97
backend/app/models/member.py
Normal file
97
backend/app/models/member.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from pathlib import PosixPath
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
from app.form.file_with_img_preview import FileWithImagePreview, ImageValidator
|
||||
from app.utils import encrypt, overwrite_upload, random_secret
|
||||
|
||||
import os
|
||||
import uuid
|
||||
import json
|
||||
import base64
|
||||
|
||||
|
||||
class Member(models.Model):
|
||||
# auto-generated ids
|
||||
uuid = models.UUIDField(
|
||||
primary_key=True, default=uuid.uuid4, editable=False)
|
||||
secret = models.CharField(
|
||||
max_length=20, default=random_secret, editable=False)
|
||||
|
||||
# member info
|
||||
organization = models.ForeignKey(
|
||||
'Organization', on_delete=models.CASCADE, related_name='members')
|
||||
member_id = models.CharField('Mitglieder-Nr.', max_length=20)
|
||||
name = models.CharField('Name', max_length=100)
|
||||
valid_since = models.DateField('Mitglied seit')
|
||||
valid_until = models.DateField('Mitglied bis', blank=True, null=True)
|
||||
image = FileWithImagePreview(
|
||||
'Bild', blank=True, null=True,
|
||||
upload_to=overwrite_upload, validators=[ImageValidator.validate],
|
||||
help_text=ImageValidator.help_text)
|
||||
additional = models.JSONField(
|
||||
'Zusätzliche Daten (JSON)', blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Mitglied'
|
||||
verbose_name_plural = 'Mitglieder'
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def export_url(self) -> str:
|
||||
return self.organization.exportBaseUrl + \
|
||||
f"#{self.organization.slug}/{self.uuid}/{self.secret}"
|
||||
|
||||
@property
|
||||
def export_os_path(self) -> PosixPath:
|
||||
return settings.EXPORT_PATH / self.organization.slug / str(self.uuid)
|
||||
|
||||
@property
|
||||
def image_save_url(self) -> str:
|
||||
return f'{self.organization.slug}/{self.uuid}'
|
||||
|
||||
@property
|
||||
def image_os_path(self) -> PosixPath:
|
||||
return settings.MEDIA_ROOT / self.organization.slug / str(self.uuid)
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
return json.dumps({
|
||||
'name': self.name,
|
||||
'org': self.organization.name,
|
||||
'id': self.member_id,
|
||||
'valid_since': str(self.valid_since or ''),
|
||||
'valid_until': str(self.valid_until or ''),
|
||||
'data': self.additional or '',
|
||||
'img': base64.b64encode(self.image.read()).decode(
|
||||
'utf-8') if self.image else None,
|
||||
})
|
||||
|
||||
@property
|
||||
def json_encrypted(self) -> bytes:
|
||||
return encrypt(self.json, self.secret)
|
||||
|
||||
def export(self):
|
||||
os.makedirs(self.export_os_path.parent, exist_ok=True)
|
||||
with open(self.export_os_path, 'wb') as fp:
|
||||
fp.write(self.json_encrypted)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.pk:
|
||||
try:
|
||||
prev = Member.objects.get(pk=self.pk)
|
||||
if prev.image != self.image:
|
||||
prev.image.delete(save=False)
|
||||
except Member.DoesNotExist:
|
||||
pass
|
||||
self.export()
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
if self.image:
|
||||
self.image.delete(save=False)
|
||||
if os.path.isfile(self.export_os_path):
|
||||
os.remove(self.export_os_path)
|
||||
return super().delete(*args, **kwargs)
|
||||
53
backend/app/models/organization.py
Normal file
53
backend/app/models/organization.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import typing
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from app.models.member import Member
|
||||
|
||||
|
||||
class Organization(models.Model):
|
||||
name = models.CharField('Name', max_length=100)
|
||||
slug = models.SlugField('URL Slug', unique=True, max_length=50)
|
||||
exportBaseUrl = models.URLField('Export Base-URL', max_length=200)
|
||||
members: 'models.QuerySet[Member]'
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Organisation'
|
||||
verbose_name_plural = 'Organisationen'
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.pk:
|
||||
try:
|
||||
prev = Organization.objects.get(pk=self.pk)
|
||||
if prev.slug != self.slug:
|
||||
renameSlug(prev.slug, self.slug)
|
||||
except Organization.DoesNotExist:
|
||||
pass
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
deleteSlug(self.slug)
|
||||
return super().delete(*args, **kwargs)
|
||||
|
||||
|
||||
def renameSlug(oldSlug: str, newSlug: str):
|
||||
if newSlug == oldSlug:
|
||||
return
|
||||
oldExportPath = settings.EXPORT_PATH / oldSlug
|
||||
oldMediaPath = settings.MEDIA_ROOT / oldSlug
|
||||
if os.path.exists(oldExportPath):
|
||||
os.rename(oldExportPath, settings.EXPORT_PATH / newSlug)
|
||||
if os.path.exists(oldMediaPath):
|
||||
os.rename(oldMediaPath, settings.MEDIA_ROOT / newSlug)
|
||||
|
||||
|
||||
def deleteSlug(oldSlug: str):
|
||||
shutil.rmtree(settings.EXPORT_PATH / oldSlug, ignore_errors=True)
|
||||
shutil.rmtree(settings.MEDIA_ROOT / oldSlug, ignore_errors=True)
|
||||
BIN
backend/app/static/favicon.ico
Normal file
BIN
backend/app/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
21
backend/app/static/favicon.svg
Normal file
21
backend/app/static/favicon.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1000 1000">
|
||||
<rect fill="#000" y="112.5" rx="108" width="1000" height="775"/>
|
||||
<g transform="translate(50,50)scale(.9)">
|
||||
<rect fill="#fff" y="125" rx="80" width="1000" height="750"/>
|
||||
<g fill="#43768F">
|
||||
<rect x="62" y="187" rx="17" width="876" height="63"/>
|
||||
<rect x="62" y="207" width="876" height="43"/>
|
||||
<circle cx="282" cy="435" r="94"/>
|
||||
<path d="M438.2,654.1c0-17.6-1.2-33.9-3.7-49.1c-2.5-15.1-6.4-29.8-11.9-44c-5.5
|
||||
-14.2-13.8-25.3-24.9-33.4c-11-8.1-24.3-12.2-39.6-12.2c-20.8,20.8-46.2,31.2
|
||||
-76.2,31.2s-55.4-10.4-76.2-31.2c-15.3,0-28.5,4.1-39.6,12.2c-11.1,8.1-19.4,19.3
|
||||
-24.9,33.4c-5.5,14.2-9.5,28.8-11.9,44c-2.4,15.2-3.7,31.5-3.7,49.1c0,17.9,5.1,
|
||||
33.1,15.4,45.6c10.3,12.5,22.5,18.8,36.9,18.8h207.9c14.3,0,26.6-6.2,36.9-18.8
|
||||
C433.2,687.2,438.2,672,438.2,654.1z"/>
|
||||
<rect x="500" y="375" width="375" height="63"/>
|
||||
<rect x="500" y="500" width="190" height="63"/>
|
||||
<rect x="750" y="500" width="125" height="63"/>
|
||||
<rect x="500" y="625" width="375" height="63"/>
|
||||
</g></g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
14
backend/app/templates/forms/img-file.html
Normal file
14
backend/app/templates/forms/img-file.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<p>
|
||||
{% if widget.is_initial %}
|
||||
<img src="{{MEDIA_URL}}{{widget.value}}" style="max-width: 150px; max-height: 150px; margin: 8px;" />
|
||||
{% if not widget.required %}
|
||||
<span class="clearable-file-input">
|
||||
<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}"{% if widget.attrs.disabled %} disabled{% endif %}>
|
||||
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>
|
||||
</span>
|
||||
{% endif %}
|
||||
<br>
|
||||
<span>{{ widget.input_text }}:</span>
|
||||
{% endif %}
|
||||
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>
|
||||
</p>
|
||||
24
backend/app/urls.py
Normal file
24
backend/app/urls.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import HttpRequest
|
||||
from django.urls import path
|
||||
from django.views.static import serve
|
||||
|
||||
from app.views import EncryptedJsonREST
|
||||
|
||||
|
||||
def ensure_authenticated(request: HttpRequest):
|
||||
if not request.user.is_authenticated:
|
||||
raise PermissionDenied()
|
||||
|
||||
|
||||
def protected_serve(request, **kwargs):
|
||||
ensure_authenticated(request)
|
||||
return serve(request, **kwargs)
|
||||
|
||||
|
||||
urlpatterns = []
|
||||
|
||||
if settings.API_ENABLED:
|
||||
urlpatterns.append(
|
||||
path('api/json/<str:org>/<uuid:uuid>', EncryptedJsonREST.as_view()))
|
||||
28
backend/app/utils.py
Normal file
28
backend/app/utils.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from django.utils.crypto import get_random_string
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
from cryptography.hazmat.primitives.hashes import Hash, SHA256
|
||||
|
||||
import os
|
||||
import typing
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from app.models.member import Member
|
||||
|
||||
|
||||
def random_secret():
|
||||
return get_random_string(16)
|
||||
|
||||
|
||||
def overwrite_upload(instance: 'Member', filename: str):
|
||||
if os.path.isfile(instance.image_os_path):
|
||||
os.remove(instance.image_os_path)
|
||||
return instance.image_save_url
|
||||
|
||||
|
||||
def encrypt(plaintext: str, key: str):
|
||||
digest = Hash(SHA256())
|
||||
digest.update(key.encode('utf8'))
|
||||
aesgcm = AESGCM(digest.finalize())
|
||||
iv = os.urandom(12)
|
||||
ct = aesgcm.encrypt(iv, plaintext.encode('utf8'), iv)
|
||||
return iv + ct
|
||||
32
backend/app/views.py
Normal file
32
backend/app/views.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from django.conf import settings
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.views import View
|
||||
from django.views.static import serve
|
||||
|
||||
from app.models import Member
|
||||
|
||||
|
||||
class EncryptedJsonREST(View):
|
||||
http_method_names = ['get']
|
||||
|
||||
def get(self, request: HttpRequest, *args, **kwargs):
|
||||
return serveStaticJson(request, **kwargs)
|
||||
# return serveDynamicJson(**kwargs)
|
||||
|
||||
|
||||
def memberOr404(kwargs: 'dict[str, str]'):
|
||||
org, uuid = kwargs['org'], kwargs['uuid']
|
||||
return get_object_or_404(Member, uuid=uuid, organization__slug=org)
|
||||
|
||||
|
||||
def serveStaticJson(request: HttpRequest, **kwargs):
|
||||
org, uuid = kwargs['org'], kwargs['uuid']
|
||||
return serve(
|
||||
request, path=f'{org}/{uuid}', document_root=settings.EXPORT_PATH)
|
||||
|
||||
|
||||
def serveDynamicJson(**kwargs):
|
||||
mem = memberOr404(kwargs)
|
||||
return HttpResponse(
|
||||
mem.json_encrypted, content_type='application/octet-stream')
|
||||
0
backend/config/__init__.py
Normal file
0
backend/config/__init__.py
Normal file
16
backend/config/asgi.py
Normal file
16
backend/config/asgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for 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/4.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
140
backend/config/settings.py
Normal file
140
backend/config/settings.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
Django settings for project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 4.2.4.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/4.2/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'insecure-$a^nh$d!tgut43^91@')
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = SECRET_KEY.startswith('insecure') or \
|
||||
os.environ.get('DEBUG', '0').lower() not in ['false', 'no', '0']
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
ALLOWED_HOSTS.extend(
|
||||
filter(
|
||||
None,
|
||||
os.environ.get('ALLOWED_HOSTS', '*').split(','),
|
||||
)
|
||||
)
|
||||
CSRF_TRUSTED_ORIGINS = ['https://' + x for x in ALLOWED_HOSTS]
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'app.apps.AppConfig',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'config.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [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',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'config.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'data' / 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.2/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/4.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'de'
|
||||
TIME_ZONE = 'Europe/Berlin'
|
||||
USE_I18N = True
|
||||
USE_TZ = False
|
||||
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
STATIC_ROOT = BASE_DIR / 'data' / 'static'
|
||||
|
||||
MEDIA_URL = 'upload/'
|
||||
MEDIA_ROOT = BASE_DIR / 'data' / 'upload'
|
||||
|
||||
# custom
|
||||
|
||||
API_ENABLED = False
|
||||
EXPORT_PATH = BASE_DIR / 'data' / 'export'
|
||||
|
||||
# enable sub-path URLs
|
||||
URL_SUBPATH = os.environ.get('URL_SUBPATH') or ''
|
||||
URL_SUBPATH = (URL_SUBPATH.strip('/') + '/').lstrip('/')
|
||||
if URL_SUBPATH:
|
||||
STATIC_URL = URL_SUBPATH + STATIC_URL
|
||||
MEDIA_URL = URL_SUBPATH + MEDIA_URL
|
||||
31
backend/config/urls.py
Normal file
31
backend/config/urls.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
URL configuration for project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/4.2/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.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.urls import include, path
|
||||
|
||||
from app.urls import protected_serve
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(settings.URL_SUBPATH, include('app.urls')),
|
||||
path(settings.URL_SUBPATH + 'edit/', admin.site.urls),
|
||||
# enable authenticated upload file access
|
||||
path(settings.MEDIA_URL.lstrip('/') + '<path:path>', protected_serve,
|
||||
{'document_root': settings.MEDIA_ROOT}),
|
||||
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
16
backend/config/wsgi.py
Normal file
16
backend/config/wsgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for 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/4.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
22
backend/manage.py
Executable file
22
backend/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', 'config.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()
|
||||
2
backend/requirements.txt
Normal file
2
backend/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Django==4.2.5
|
||||
cryptography==41.0.4
|
||||
7
backend/scripts/on-deploy.sh
Executable file
7
backend/scripts/on-deploy.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
python manage.py collectstatic --noinput
|
||||
python manage.py migrate
|
||||
|
||||
# python -m uvicorn --port 8099 config.asgi:application
|
||||
python -m gunicorn -b 0.0.0.0:8099 -k uvicorn.workers.UvicornWorker config.asgi:application
|
||||
4
backend/scripts/on-init.sh
Executable file
4
backend/scripts/on-init.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
|
||||
python manage.py migrate
|
||||
python manage.py createsuperuser
|
||||
26
backend/setup.py
Normal file
26
backend/setup.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
with open('README.md') as fp:
|
||||
longdesc = fp.read()
|
||||
|
||||
setup(
|
||||
name='ausweis',
|
||||
version='0.9.0',
|
||||
author='OWBA',
|
||||
description='Django app for digital membership cards',
|
||||
long_description=longdesc,
|
||||
long_description_content_type="text/markdown",
|
||||
url='https://github.com/owba/ausweis',
|
||||
license='MIT',
|
||||
classifiers=[
|
||||
'Programming Language :: Python :: 3',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Environment :: Web Environment',
|
||||
'Framework :: Django',
|
||||
'Operating System :: OS Independent',
|
||||
],
|
||||
python_requires='>=3.8',
|
||||
packages=find_packages(),
|
||||
scripts=['manage.py'],
|
||||
install_requires=[],
|
||||
)
|
||||
5
backend/templates/admin/base_site.html
Normal file
5
backend/templates/admin/base_site.html
Normal file
@@ -0,0 +1,5 @@
|
||||
{% extends "admin/base_site.html" %}
|
||||
{% load static %}
|
||||
{% block extrahead %}
|
||||
<link rel="shortcut icon" href="{% static 'favicon.ico' %}" />
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user