From e899252f31f705e1db063721499649efc4955b8c Mon Sep 17 00:00:00 2001 From: Nell Date: Tue, 4 Jun 2024 22:08:15 +0200 Subject: [PATCH] Init --- .env.sample | 35 + .gitignore | 5 + README.md | 0 app/.gitignore | 5 + app/Dockerfile | 46 ++ app/app/__init__.py | 3 + app/app/asgi.py | 16 + app/app/celery.py | 9 + app/app/settings.py | 216 +++++++ app/app/urls.py | 31 + app/app/wsgi.py | 16 + app/app_site/__init__.py | 0 app/app_site/admin.py | 3 + app/app_site/apps.py | 6 + app/app_site/migrations/__init__.py | 0 app/app_site/models.py | 3 + app/app_site/templates/app_site/home.html | 2 + app/app_site/templates/app_site/layout.html | 2 + app/app_site/tests.py | 3 + app/app_site/urls.py | 8 + app/app_site/views.py | 5 + app/db/__init__.py | 0 app/db/admin.py | 18 + app/db/apps.py | 13 + app/db/backup_engine.py | 98 +++ app/db/credentials/.gitkeep | 0 app/db/migrations/0001_initial.py | 55 ++ ...name_alter_db_db_type_alter_dbbackup_db.py | 30 + app/db/migrations/__init__.py | 0 app/db/models.py | 79 +++ app/db/serializers.py | 0 app/db/signals.py | 12 + app/db/tasks.py | 53 ++ app/db/templates/db/index.html | 6 + app/db/templates/db/layout.html | 5 + app/db/tests.py | 3 + app/db/urls.py | 8 + app/db/views.py | 5 + app/dev_run.sh | 6 + app/frontend/.gitignore | 24 + app/frontend/package.json | 25 + app/frontend/postcss.config.cjs | 7 + app/frontend/src/components/Base.vue | 29 + app/frontend/src/components/db/DBBackups.vue | 27 + app/frontend/src/components/db/DBs.vue | 27 + app/frontend/src/components/db/Index.vue | 13 + .../components/db/forms/CredentialForm.vue | 0 .../src/components/db/forms/DBForm.vue | 0 app/frontend/src/entrypoint/app.js | 3 + app/frontend/src/entrypoint/db.js | 17 + app/frontend/src/index.js | 0 app/frontend/src/plugins/vue-loader.js | 6 + app/frontend/src/plugins/vuetify.js | 30 + app/frontend/src/style/main.css | 3 + app/frontend/src/style/main.css.js | 1 + app/frontend/vite.config.js | 56 ++ app/frontend/yarn.lock | 596 ++++++++++++++++++ app/manage.py | 22 + app/requirements/common.txt | 8 + app/requirements/dev.txt | 3 + app/requirements/prod.txt | 4 + app/templates/base.html | 23 + app/user/__init__.py | 0 app/user/admin.py | 27 + app/user/apps.py | 6 + app/user/forms.py | 11 + app/user/migrations/0001_initial.py | 44 ++ app/user/migrations/__init__.py | 0 app/user/models.py | 72 +++ app/user/tests.py | 3 + app/user/views.py | 3 + backups/.gitkeep | 0 docker-compose.dev.yml | 18 + docker-compose.yml | 56 ++ 74 files changed, 1969 insertions(+) create mode 100644 .env.sample create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/.gitignore create mode 100644 app/Dockerfile create mode 100644 app/app/__init__.py create mode 100644 app/app/asgi.py create mode 100644 app/app/celery.py create mode 100644 app/app/settings.py create mode 100644 app/app/urls.py create mode 100644 app/app/wsgi.py create mode 100644 app/app_site/__init__.py create mode 100644 app/app_site/admin.py create mode 100644 app/app_site/apps.py create mode 100644 app/app_site/migrations/__init__.py create mode 100644 app/app_site/models.py create mode 100644 app/app_site/templates/app_site/home.html create mode 100644 app/app_site/templates/app_site/layout.html create mode 100644 app/app_site/tests.py create mode 100644 app/app_site/urls.py create mode 100644 app/app_site/views.py create mode 100644 app/db/__init__.py create mode 100644 app/db/admin.py create mode 100644 app/db/apps.py create mode 100644 app/db/backup_engine.py create mode 100644 app/db/credentials/.gitkeep create mode 100644 app/db/migrations/0001_initial.py create mode 100644 app/db/migrations/0002_db_db_name_alter_db_db_type_alter_dbbackup_db.py create mode 100644 app/db/migrations/__init__.py create mode 100644 app/db/models.py create mode 100644 app/db/serializers.py create mode 100644 app/db/signals.py create mode 100644 app/db/tasks.py create mode 100644 app/db/templates/db/index.html create mode 100644 app/db/templates/db/layout.html create mode 100644 app/db/tests.py create mode 100644 app/db/urls.py create mode 100644 app/db/views.py create mode 100644 app/dev_run.sh create mode 100644 app/frontend/.gitignore create mode 100644 app/frontend/package.json create mode 100644 app/frontend/postcss.config.cjs create mode 100644 app/frontend/src/components/Base.vue create mode 100644 app/frontend/src/components/db/DBBackups.vue create mode 100644 app/frontend/src/components/db/DBs.vue create mode 100644 app/frontend/src/components/db/Index.vue create mode 100644 app/frontend/src/components/db/forms/CredentialForm.vue create mode 100644 app/frontend/src/components/db/forms/DBForm.vue create mode 100644 app/frontend/src/entrypoint/app.js create mode 100644 app/frontend/src/entrypoint/db.js create mode 100644 app/frontend/src/index.js create mode 100644 app/frontend/src/plugins/vue-loader.js create mode 100644 app/frontend/src/plugins/vuetify.js create mode 100644 app/frontend/src/style/main.css create mode 100644 app/frontend/src/style/main.css.js create mode 100644 app/frontend/vite.config.js create mode 100644 app/frontend/yarn.lock create mode 100644 app/manage.py create mode 100644 app/requirements/common.txt create mode 100644 app/requirements/dev.txt create mode 100644 app/requirements/prod.txt create mode 100644 app/templates/base.html create mode 100644 app/user/__init__.py create mode 100644 app/user/admin.py create mode 100644 app/user/apps.py create mode 100644 app/user/forms.py create mode 100644 app/user/migrations/0001_initial.py create mode 100644 app/user/migrations/__init__.py create mode 100644 app/user/models.py create mode 100644 app/user/tests.py create mode 100644 app/user/views.py create mode 100644 backups/.gitkeep create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.yml diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..d83ed11 --- /dev/null +++ b/.env.sample @@ -0,0 +1,35 @@ +USER_ID=1000 +GROUP_ID=1000 + +LISTEN_PORT=8000 + +# should be prod or dev +ENV='dev' + +# secret +SECRET='CHANGE_ME' + +# Allowed hosts, domain's allowed, comma separated +ALLOWED_HOSTS='127.0.0.1, localhost' + +# CSRF Trusted origins +CSRF_TRUSTED_ORIGINS='http://127.0.0.1, http://localhost' + +### DEV RELATED +# VITE +DEV_SERVER_HOST='127.0.0.1' +DEV_SERVER_PORT='8080' + +### DB related +# db engine, should be 'sqlite', 'pgsql' (psycopg[binary] required) +DB_ENGINE='sqlite3' + +# SQLITE +SQLITE_REL_PATH='db.sqlite3' + +# PostgreSQL, env logic for postgresql docker image +#POSTGRES_HOST=localhost +#POSTGRES_PORT=5432 +#POSTGRES_DB=changeme +#POSTGRES_USER=changeme +#POSTGRES_PASSWORD=changeme diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..68d4c13 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea/ +.env + +backups/* +!backups/.gitkeep \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..9d9b5b1 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,5 @@ +db.sqlite3 +celerybeat-schedule + +db/credentials/* +!db/credentials/.gitkeep \ No newline at end of file diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000..376d1c6 --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,46 @@ +FROM python:3.12-slim + +ARG PUID +ARG PGID +ARG ENV + +ENV PYTHONUNBUFFERED 1 + +#RUN groupadd -g ${PGID} -o custom_user +#RUN useradd -m -u ${PUID} -g ${PGID} -o -s /bin/bash custom_user + +# install requirements debian dependencies +RUN apt update && apt install -y curl inotify-tools mariadb-client ca-certificates gnupg wget lsb-release + +# install lastest version of postgresql-client +RUN install -d /usr/share/postgresql-common/pgdg +RUN curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc +RUN echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list +RUN apt update && apt install -y postgresql-client-16 + +# install node 20 +RUN mkdir -p /etc/apt/keyrings +RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg +RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list +RUN apt update && apt install nodejs -y +RUN npm install -g yarn + +# clean apt cache +RUN find /var/cache/apt/archives /var/lib/apt/lists -not -name lock -type f -delete + +# Setup python requirements +WORKDIR /app/requirements +COPY ./requirements/* ./ +RUN python -m pip install --upgrade pip +RUN pip install -r ${ENV}.txt + +#USER custom_user + +# Setup node dependencies +WORKDIR /app/frontend +COPY ./frontend/package.json ./package.json +RUN yarn install + +WORKDIR /app + + diff --git a/app/app/__init__.py b/app/app/__init__.py new file mode 100644 index 0000000..53f4ccb --- /dev/null +++ b/app/app/__init__.py @@ -0,0 +1,3 @@ +from .celery import app as celery_app + +__all__ = ("celery_app",) diff --git a/app/app/asgi.py b/app/app/asgi.py new file mode 100644 index 0000000..54f589c --- /dev/null +++ b/app/app/asgi.py @@ -0,0 +1,16 @@ +import os + +from django.core.asgi import get_asgi_application +from channels.auth import AuthMiddlewareStack +from channels.routing import ProtocolTypeRouter, URLRouter + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') +django_asgi_app = get_asgi_application() + +from .urls import websocket_urlpatterns + + +application = ProtocolTypeRouter({ + "http": django_asgi_app, + "websocket": AuthMiddlewareStack(websocket_urlpatterns) +}) diff --git a/app/app/celery.py b/app/app/celery.py new file mode 100644 index 0000000..382b245 --- /dev/null +++ b/app/app/celery.py @@ -0,0 +1,9 @@ +import os + +from celery import Celery + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") +app = Celery("app") + +app.config_from_object("django.conf:settings", namespace="CELERY") +app.autodiscover_tasks() diff --git a/app/app/settings.py b/app/app/settings.py new file mode 100644 index 0000000..c0992dd --- /dev/null +++ b/app/app/settings.py @@ -0,0 +1,216 @@ +""" +Django settings for app project. + +Generated by 'django-admin startproject' using Django 5.0.4. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.0/ref/settings/ +""" + +from pathlib import Path +from os import getenv + +# 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/5.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = getenv("SECRET", "not_secure") + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = getenv("ENV", "prod") == "dev" + +ALLOWED_HOSTS = [host.strip() for host in getenv("ALLOWED_HOSTS").split(",") if host] +CSRF_TRUSTED_ORIGINS = [host.strip() for host in getenv("CSRF_TRUSTED_ORIGINS").split(",") if host] +CORS_ALLOWED_ORIGINS = CSRF_TRUSTED_ORIGINS + + +# Application definition +INSTALLED_APPS = [] +if DEBUG: + INSTALLED_APPS += ["daphne"] + +INSTALLED_APPS += [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + + "corsheaders", + "django_vite", + "rest_framework", + "django_celery_beat", + + "user", + "app_site", + "db" +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + "corsheaders.middleware.CorsMiddleware", + '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': [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 = 'app.wsgi.application' +ASGI_APPLICATION = "app.asgi.application" + + +# Database +# https://docs.djangoproject.com/en/5.0/ref/settings/#databases + +match getenv("DB_ENGINE"): + case "pgsql": + DB = { + "ENGINE": "django.db.backends.postgresql", + "NAME": getenv("DB_NAME"), + "USER": getenv("DB_USER"), + "PASSWORD": getenv("DB_PASSWORD"), + "HOST": getenv("DB_HOST", "localhost"), + "PORT": int(getenv("DB_PORT", 5432)), + } + case _: + DB = { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / getenv("SQLITE_REL_PATH", "db.sqlite3"), + } + +DATABASES = { + 'default': DB +} + + +# Password validation +# https://docs.djangoproject.com/en/5.0/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.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.0/howto/static-files/ + +STATIC_URL = 'static/' +STATICFILES_DIRS = [] + +# Default primary key field type +# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +AUTH_USER_MODEL = "user.User" +LOGIN_REDIRECT_URL = "/" +LOGIN_URL = "/user/login/" +LOGOUT_REDIRECT_URL = "/user/login/" +SESSION_COOKIE_AGE = 60 * 60 * 24 * 30 + +DJANGO_VITE = { + "default": { + "dev_mode": DEBUG, + "manifest_path": BASE_DIR / "frontend/dist/manifest.json", + "dev_server_host": getenv("DEV_SERVER_HOST", "localhost"), + "dev_server_port": int(getenv("DEV_SERVER_PORT", 8080)), + } +} +STATICFILES_DIRS += BASE_DIR / "frontend/dist", + +BACKUPS_PATH = Path("/backups") + +CELERY_BROKER_URL = "redis://redis:6379/1" +CELERY_TIMEZONE = "Europe/Paris" +CELERY_TASK_TRACK_STARTED = True +# CELERY_TASK_TIME_LIMIT = 30 * 60 +CELERY_TASK_SERIALIZER = "json" +CELERY_ACCEPT_CONTENT = ['json'] +CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True +CELERY_BEAT_SCHEDULE = { + "db_job_check_daily_backup": { + "task": "db.tasks.job_check_daily_backup", + "schedule": 1*60 + } +} + +CHANNEL_LAYERS = { + "default": { + "BACKEND": "channels_redis.pubsub.RedisPubSubChannelLayer", + "CONFIG": { + "hosts": ["redis://redis:6379/2"], + }, + }, +} + +# REST_FRAMEWORK = { +# 'DEFAULT_FILTER_BACKENDS': [ +# 'rest_framework.filters.OrderingFilter' +# ], +# # 'DEFAULT_METADATA_CLASS': 'api.metadatas.CustomMetaData', +# 'DEFAULT_AUTHENTICATION_CLASSES': [ +# 'rest_framework.authentication.SessionAuthentication', +# # "api.authentication.CsrfExemptSessionAuthentication", +# # 'rest_framework.authentication.BasicAuthentication', +# # 'rest_framework_simplejwt.authentication.JWTAuthentication', +# ], +# 'DEFAULT_PERMISSION_CLASSES': [ +# # 'api.permissions.IsSuperUser' +# ], +# 'DEFAULT_RENDERER_CLASSES': [ +# 'rest_framework.renderers.JSONRenderer', +# 'rest_framework.renderers.BrowsableAPIRenderer', +# # 'rest_framework_csv.renderers.CSVRenderer', +# ], +# } + diff --git a/app/app/urls.py b/app/app/urls.py new file mode 100644 index 0000000..04aaac1 --- /dev/null +++ b/app/app/urls.py @@ -0,0 +1,31 @@ +""" +URL configuration for app project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.0/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 path, include +from django.conf import settings +from django.views import static + +urlpatterns = [ + path('admin/', admin.site.urls), + + path("", include("app_site.urls", namespace="app_site")), + path("db/", include("db.urls", namespace="db")), +] + +websocket_urlpatterns = [ + # path("ws/torrent/", TorrentConsumer.as_asgi()), +] diff --git a/app/app/wsgi.py b/app/app/wsgi.py new file mode 100644 index 0000000..eb514ab --- /dev/null +++ b/app/app/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for app 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.0/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() diff --git a/app/app_site/__init__.py b/app/app_site/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/app_site/admin.py b/app/app_site/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/app/app_site/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/app/app_site/apps.py b/app/app_site/apps.py new file mode 100644 index 0000000..2c8e02b --- /dev/null +++ b/app/app_site/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AppSiteConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'app_site' diff --git a/app/app_site/migrations/__init__.py b/app/app_site/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/app_site/models.py b/app/app_site/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/app/app_site/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/app/app_site/templates/app_site/home.html b/app/app_site/templates/app_site/home.html new file mode 100644 index 0000000..fa00360 --- /dev/null +++ b/app/app_site/templates/app_site/home.html @@ -0,0 +1,2 @@ +{% extends "app_site/layout.html" %} + diff --git a/app/app_site/templates/app_site/layout.html b/app/app_site/templates/app_site/layout.html new file mode 100644 index 0000000..5cb6467 --- /dev/null +++ b/app/app_site/templates/app_site/layout.html @@ -0,0 +1,2 @@ +{% extends "base.html" %} + diff --git a/app/app_site/tests.py b/app/app_site/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/app/app_site/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/app/app_site/urls.py b/app/app_site/urls.py new file mode 100644 index 0000000..3647a72 --- /dev/null +++ b/app/app_site/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from .views import HomeView + +app_name = "app_site" +urlpatterns = [ + path('', HomeView.as_view(), name='home'), +] diff --git a/app/app_site/views.py b/app/app_site/views.py new file mode 100644 index 0000000..c2e455d --- /dev/null +++ b/app/app_site/views.py @@ -0,0 +1,5 @@ +from django.views.generic import TemplateView + + +class HomeView(TemplateView): + template_name = "app_site/home.html" diff --git a/app/db/__init__.py b/app/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/db/admin.py b/app/db/admin.py new file mode 100644 index 0000000..9529380 --- /dev/null +++ b/app/db/admin.py @@ -0,0 +1,18 @@ +from django.contrib import admin + +from .models import DB, DBBackup, DBCredential + + +@admin.register(DB) +class DBAdmin(admin.ModelAdmin): + pass + + +@admin.register(DBCredential) +class DBCredentialAdmin(admin.ModelAdmin): + pass + + +@admin.register(DBBackup) +class BackupDBAdmin(admin.ModelAdmin): + readonly_fields = ("task_id", "status", "rel_path",) diff --git a/app/db/apps.py b/app/db/apps.py new file mode 100644 index 0000000..4198102 --- /dev/null +++ b/app/db/apps.py @@ -0,0 +1,13 @@ +from django.apps import AppConfig +from django.db.models import signals as db_signals + + +class DbConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'db' + + def ready(self): + from . import signals, models + + db_signals.post_save.connect(signals.on_backupdb_save, sender=models.DBBackup) + db_signals.post_delete.connect(signals.on_backupdb_delete, sender=models.DBBackup) diff --git a/app/db/backup_engine.py b/app/db/backup_engine.py new file mode 100644 index 0000000..ebefbce --- /dev/null +++ b/app/db/backup_engine.py @@ -0,0 +1,98 @@ +import time +from abc import ABC, abstractmethod +import gzip +import subprocess +import uuid +from typing import Any + +from .models import DB, DBBackup +# https://medium.com/poka-techblog/5-different-ways-to-backup-your-postgresql-database-using-python-3f06cea4f51 + + +class BackupEngineBase(ABC): + def __init__(self, backupdb: DBBackup): + self.backup_instance = backupdb + self.db_instance = self.backup_instance.db + + # self.db_instance = db_instance + # self.backup_instance = BackupDB.objects.create( + # db=self.db_instance, + # rel_path="{db_id}-{timestamp}-{rand_gen}.{db_type}.{ext}".format( + # db_id=self.db_instance.id, + # timestamp=int(time.time()), + # rand_gen=uuid.uuid4().hex[:5], + # db_type=self.db_instance.db_type, + # ext='sql.gz' if self.gzip else 'sql' + # ), + # ) + self.credentials = {"database": self.db_instance.db_name, **self.db_instance.credential.credentials} + + @property + @abstractmethod + def cmd_kwargs(self) -> dict[str, Any]: + ... + + @property + def format_cmd_kwargs(self): + return " ".join([ + key if isinstance(value, bool) else f"{key} {value}" + for key, value in self.cmd_kwargs.items() + if value + ]) + + @property + @abstractmethod + def cmd(self): + ... + + @property + def cmd_format(self): + return self.cmd.format(cmd_kwargs=self.format_cmd_kwargs, **self.credentials) + + def popen(self): + return subprocess.Popen(self.cmd_format, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + shell=True, + universal_newlines=True) + + def file_obj(self, path): + return gzip.open(path, "wb") + + def run(self, task_id=None): + self.backup_instance.update_task_id(task_id) + try: + with self.file_obj(self.backup_instance.abs_path) as f: + with self.popen() as proc: + self.backup_instance.update_status("running") + for line in iter(proc.stdout.readline, ""): + f.write(line.encode()) + except Exception as e: + self.backup_instance.update_status("finished") + raise e + + +class MySQLBackupEngine(BackupEngineBase): + @property + def cmd_kwargs(self) -> dict[str, Any]: + return { + "--no-tablespaces": True + } + + @property + def cmd(self): + return "mysqldump {cmd_kwargs} -h {host} -P {port} -u {user} -p{password} {database}" + + +class PostgreSQLBackupEngine(BackupEngineBase): + @property + def cmd_kwargs(self) -> dict[str, Any]: + return { + "--no-owner": True, + "--no-privileges": True, + "--clean": False + } + + @property + def cmd(self): + return "pg_dump {cmd_kwargs} --dbname=postgresql://{user}:{password}@{host}:{port}/{database}" diff --git a/app/db/credentials/.gitkeep b/app/db/credentials/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/db/migrations/0001_initial.py b/app/db/migrations/0001_initial.py new file mode 100644 index 0000000..3716ed8 --- /dev/null +++ b/app/db/migrations/0001_initial.py @@ -0,0 +1,55 @@ +# Generated by Django 5.0.6 on 2024-05-28 09:06 + +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='DB', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('date_modified', models.DateTimeField(auto_now=True)), + ('db_type', models.CharField(max_length=100)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='DBBackup', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('date_modified', models.DateTimeField(auto_now=True)), + ('status', models.CharField(choices=[('waiting', 'waiting'), ('running', 'running'), ('ok', 'Ok'), ('error', 'Error'), ('expired', 'Expired')], default='waiting', max_length=20)), + ('task_id', models.CharField(max_length=100, null=True)), + ('rel_path', models.CharField(max_length=100)), + ('db', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='db.db')), + ], + ), + migrations.CreateModel( + name='DBCredential', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('credentials', models.JSONField(default=dict)), + ('create_db_perm', models.BooleanField(default=False)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='db', + name='credential', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='db.dbcredential'), + ), + ] diff --git a/app/db/migrations/0002_db_db_name_alter_db_db_type_alter_dbbackup_db.py b/app/db/migrations/0002_db_db_name_alter_db_db_type_alter_dbbackup_db.py new file mode 100644 index 0000000..74caec1 --- /dev/null +++ b/app/db/migrations/0002_db_db_name_alter_db_db_type_alter_dbbackup_db.py @@ -0,0 +1,30 @@ +# Generated by Django 5.0.6 on 2024-06-04 10:27 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='db', + name='db_name', + field=models.CharField(default='test', max_length=255), + preserve_default=False, + ), + migrations.AlterField( + model_name='db', + name='db_type', + field=models.CharField(choices=[('mysql', 'MySQL/MariaDB'), ('postgres', 'PostgreSQL')], max_length=100), + ), + migrations.AlterField( + model_name='dbbackup', + name='db', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='backups', to='db.db'), + ), + ] diff --git a/app/db/migrations/__init__.py b/app/db/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/db/models.py b/app/db/models.py new file mode 100644 index 0000000..4ece221 --- /dev/null +++ b/app/db/models.py @@ -0,0 +1,79 @@ +from django.db import models +from django.core.files.storage import FileSystemStorage +from django.conf import settings + +import pathlib +import uuid +import time + +credentials_storage = FileSystemStorage(pathlib.Path(__file__).parent / 'credentials') + + +class DBCredential(models.Model): + # credentials = models.FileField(storage=credentials_storage) + credentials = models.JSONField(default=dict) + create_db_perm = models.BooleanField(default=False) + user = models.ForeignKey("user.User", on_delete=models.CASCADE) + + +class DB(models.Model): + name = models.CharField(max_length=100) + db_name = models.CharField(max_length=255) + date_created = models.DateTimeField(auto_now_add=True) + date_modified = models.DateTimeField(auto_now=True) + credential = models.ForeignKey("DBCredential", on_delete=models.CASCADE) + user = models.ForeignKey("user.User", on_delete=models.CASCADE) + db_types = ( + ("mysql", "MySQL/MariaDB"), + ("postgres", "PostgreSQL"), + ) + db_type = models.CharField(max_length=100, choices=db_types) + + +class DBBackup(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + date_created = models.DateTimeField(auto_now_add=True) + date_modified = models.DateTimeField(auto_now=True) + db = models.ForeignKey("DB", on_delete=models.CASCADE, related_name="backups") + status_choices = [ + ("waiting", "waiting"), + ("running", "running"), + ("ok", "Ok"), + ("error", "Error"), + ("expired", "Expired") + ] + status = models.CharField(max_length=20, default="waiting", choices=status_choices) + task_id = models.CharField(max_length=100, null=True) + rel_path = models.CharField(max_length=100) + + def save(self, *args, **kwargs): + if not self.rel_path: + self.rel_path = "{db_id}-{timestamp}-{rand_gen}.{db_type}.{ext}".format( + db_id=self.db.id, + timestamp=int(time.time()), + rand_gen=uuid.uuid4().hex[:5], + db_type=self.db.db_type, + ext='sql.gz' + ) + return super().save(*args, **kwargs) + + def update_status(self, status): + self.status = status + self.save() + + def update_task_id(self, task_id): + if task_id != self.task_id: + self.task_id = task_id + self.save() + + def set_expired(self): + self.status = "expired" + self.abs_path.unlink(True) + self.save() + + @property + def abs_path(self): + return settings.BACKUPS_PATH / self.rel_path + + + diff --git a/app/db/serializers.py b/app/db/serializers.py new file mode 100644 index 0000000..e69de29 diff --git a/app/db/signals.py b/app/db/signals.py new file mode 100644 index 0000000..7d9f4c6 --- /dev/null +++ b/app/db/signals.py @@ -0,0 +1,12 @@ +from .models import DBBackup + +from .tasks import start_backup + + +def on_backupdb_save(instance: DBBackup, created, **kwargs): + if created: + start_backup.delay(instance.id) + + +def on_backupdb_delete(instance: DBBackup, **kwargs): + instance.abs_path.unlink(missing_ok=True) diff --git a/app/db/tasks.py b/app/db/tasks.py new file mode 100644 index 0000000..284185a --- /dev/null +++ b/app/db/tasks.py @@ -0,0 +1,53 @@ +from django.db.models import Prefetch, Q +from django.utils import timezone + +import celery +from datetime import timedelta + +from .models import DBBackup, DB +from .backup_engine import MySQLBackupEngine, PostgreSQLBackupEngine + + +@celery.shared_task +def start_backup(backup_id): + backupdb = DBBackup.objects.get(id=backup_id) + # task_id can be none if it's not a celery task (for ex : if running the function without apply it) + task_id = start_backup.request.id + + klass = None + match backupdb.db.db_type: + case "mysql": + klass = MySQLBackupEngine + + case "postgres": + klass = PostgreSQLBackupEngine + + case _: + pass + + if klass: + be = klass(backupdb=backupdb) + be.run(task_id) + + # if start_backup.request.id: + # # running from celery task + # pass + # else: + # # running from standard function + # pass + + +@celery.shared_task +def job_check_daily_backup(): + now = timezone.now() + + for db in DB.objects.all(): + # check if need backup + last_backup: DBBackup = (db.backups.order_by("-date_created") + .filter(Q(status="ok") | Q(status="running") | Q(status="waiting")) + .first()) + if not last_backup or last_backup.date_created < (now - timedelta(hours=24)): + DBBackup.objects.create(db=db) + + for backup in db.backups.filter(date_created__lt=now - timedelta(days=30)): + backup.set_expired() diff --git a/app/db/templates/db/index.html b/app/db/templates/db/index.html new file mode 100644 index 0000000..4f8dc05 --- /dev/null +++ b/app/db/templates/db/index.html @@ -0,0 +1,6 @@ +{% extends "db/layout.html" %} +{% load django_vite %} + +{% block js %} + {% vite_asset "entrypoint/db.js" %} +{% endblock %} \ No newline at end of file diff --git a/app/db/templates/db/layout.html b/app/db/templates/db/layout.html new file mode 100644 index 0000000..932dc26 --- /dev/null +++ b/app/db/templates/db/layout.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} + +{% block js %} + +{% endblock %} \ No newline at end of file diff --git a/app/db/tests.py b/app/db/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/app/db/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/app/db/urls.py b/app/db/urls.py new file mode 100644 index 0000000..f07fa72 --- /dev/null +++ b/app/db/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from .views import IndexView + +app_name = 'db' +urlpatterns = [ + path("", IndexView.as_view(), name='index'), +] diff --git a/app/db/views.py b/app/db/views.py new file mode 100644 index 0000000..d3bd8c2 --- /dev/null +++ b/app/db/views.py @@ -0,0 +1,5 @@ +from django.views.generic import TemplateView + + +class IndexView(TemplateView): + template_name = "db/index.html" diff --git a/app/dev_run.sh b/app/dev_run.sh new file mode 100644 index 0000000..3058129 --- /dev/null +++ b/app/dev_run.sh @@ -0,0 +1,6 @@ +cd /app/frontend && yarn dev & +cd /app && python manage.py runserver 0.0.0.0:8000 & + +wait -n + +exit $? \ No newline at end of file diff --git a/app/frontend/.gitignore b/app/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/app/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/app/frontend/package.json b/app/frontend/package.json new file mode 100644 index 0000000..1416dff --- /dev/null +++ b/app/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@mdi/font": "^7.4.47", + "@vitejs/plugin-vue": "^5.0.4", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.38", + "postcss-import": "^16.1.0", + "postcss-simple-vars": "^7.0.1", + "vite": "^5.2.0" + }, + "dependencies": { + "vite-plugin-vuetify": "^2.0.3", + "vue": "^3.4.27", + "vuetify": "^3.6.7" + } +} diff --git a/app/frontend/postcss.config.cjs b/app/frontend/postcss.config.cjs new file mode 100644 index 0000000..5bf2493 --- /dev/null +++ b/app/frontend/postcss.config.cjs @@ -0,0 +1,7 @@ +module.exports = (ctx) => ({ + plugins: [ + require('postcss-import')(), + require('postcss-simple-vars')(), + require("autoprefixer")(), + ] +}) \ No newline at end of file diff --git a/app/frontend/src/components/Base.vue b/app/frontend/src/components/Base.vue new file mode 100644 index 0000000..7d61db6 --- /dev/null +++ b/app/frontend/src/components/Base.vue @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/app/frontend/src/components/db/DBBackups.vue b/app/frontend/src/components/db/DBBackups.vue new file mode 100644 index 0000000..99af122 --- /dev/null +++ b/app/frontend/src/components/db/DBBackups.vue @@ -0,0 +1,27 @@ + + + \ No newline at end of file diff --git a/app/frontend/src/components/db/DBs.vue b/app/frontend/src/components/db/DBs.vue new file mode 100644 index 0000000..77d41f4 --- /dev/null +++ b/app/frontend/src/components/db/DBs.vue @@ -0,0 +1,27 @@ + + + \ No newline at end of file diff --git a/app/frontend/src/components/db/Index.vue b/app/frontend/src/components/db/Index.vue new file mode 100644 index 0000000..4dcb02a --- /dev/null +++ b/app/frontend/src/components/db/Index.vue @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/app/frontend/src/components/db/forms/CredentialForm.vue b/app/frontend/src/components/db/forms/CredentialForm.vue new file mode 100644 index 0000000..e69de29 diff --git a/app/frontend/src/components/db/forms/DBForm.vue b/app/frontend/src/components/db/forms/DBForm.vue new file mode 100644 index 0000000..e69de29 diff --git a/app/frontend/src/entrypoint/app.js b/app/frontend/src/entrypoint/app.js new file mode 100644 index 0000000..381652f --- /dev/null +++ b/app/frontend/src/entrypoint/app.js @@ -0,0 +1,3 @@ +// add the beginning of your app entry +import 'vite/modulepreload-polyfill' + diff --git a/app/frontend/src/entrypoint/db.js b/app/frontend/src/entrypoint/db.js new file mode 100644 index 0000000..a86ac2e --- /dev/null +++ b/app/frontend/src/entrypoint/db.js @@ -0,0 +1,17 @@ +// add the beginning of your app entry +import 'vite/modulepreload-polyfill' + +// import {createVue} from "../utils.js" +import App from "@/components/db/Index.vue" + +import("@/plugins/vue-loader.js").then(utils => { + utils.createVue(App, "#app"); +}) + +// (async function(){ +// const a = () => import("../utils.js"); +// const b = await a(); +// console.log(b) +// b.default.createVue(App, "#app") +// // b.createVue(App, "#app") +// })() diff --git a/app/frontend/src/index.js b/app/frontend/src/index.js new file mode 100644 index 0000000..e69de29 diff --git a/app/frontend/src/plugins/vue-loader.js b/app/frontend/src/plugins/vue-loader.js new file mode 100644 index 0000000..06ed476 --- /dev/null +++ b/app/frontend/src/plugins/vue-loader.js @@ -0,0 +1,6 @@ +import vuetify from "./vuetify.js"; +import { createApp } from 'vue'; + +export function createVue(component, dom_id){ + return createApp(component).use(vuetify).mount(dom_id) +} \ No newline at end of file diff --git a/app/frontend/src/plugins/vuetify.js b/app/frontend/src/plugins/vuetify.js new file mode 100644 index 0000000..faa4e64 --- /dev/null +++ b/app/frontend/src/plugins/vuetify.js @@ -0,0 +1,30 @@ +import '@mdi/font/css/materialdesignicons.css' // Ensure you are using css-loader +import 'vuetify/styles' +import { createVuetify } from 'vuetify' +import colors from 'vuetify/lib/util/colors.mjs' +// import { aliases, mdi } from 'vuetify/iconsets/mdi-svg' + +export default createVuetify({ + // components, + // directives, + icons: { + defaultSet: 'mdi', + }, + // ssr: false, + theme: { + defaultTheme: "dark", + themes: { + dark: { + colors: { + primary: colors.blue.darken2, + accent: colors.grey.darken3, + secondary: colors.amber.darken3, + info: colors.teal.lighten1, + warning: colors.amber.base, + error: colors.deepOrange.accent4, + success: colors.green.accent3 + } + } + } + } +}); \ No newline at end of file diff --git a/app/frontend/src/style/main.css b/app/frontend/src/style/main.css new file mode 100644 index 0000000..a9705e9 --- /dev/null +++ b/app/frontend/src/style/main.css @@ -0,0 +1,3 @@ +/*body {*/ +/* background-color: grey;*/ +/*}*/ \ No newline at end of file diff --git a/app/frontend/src/style/main.css.js b/app/frontend/src/style/main.css.js new file mode 100644 index 0000000..720cec1 --- /dev/null +++ b/app/frontend/src/style/main.css.js @@ -0,0 +1 @@ +import "@/style/main.css" \ No newline at end of file diff --git a/app/frontend/vite.config.js b/app/frontend/vite.config.js new file mode 100644 index 0000000..912c83b --- /dev/null +++ b/app/frontend/vite.config.js @@ -0,0 +1,56 @@ +import {defineConfig, loadEnv} from "vite"; +import {resolve, join} from "path"; +import vue from "@vitejs/plugin-vue"; +import vuetify from "vite-plugin-vuetify"; + +// const postcssConfig = { +// plugins: [ +// require('postcss-import')(), +// require('postcss-simple-vars')(), +// require('autoprefixer')(), +// ], +// }; + +export default defineConfig((mode) => { + const env = loadEnv(mode, "..", ""), + SRC_DIR = resolve("./src"), + OUT_DIR = resolve("./dist") + + return { + plugins: [ + vue(), + vuetify() + ], + resolve: { + alias: { + "@": resolve(SRC_DIR), + "vue": "vue/dist/vue.esm-bundler.js" + } + }, + root: SRC_DIR, + base: "/static/", + // css: { + // postcss: postcssConfig + // }, + server: { + host: "0.0.0.0", + port: env.DEV_SERVER_PORT, + origin: `http://${env.DEV_SERVER_HOST}:${env.DEV_SERVER_PORT}`, // hotfix, webfont was loaded from wrong url + watch: { + usePolling: true + } + }, + build: { + manifest: "manifest.json", + emptyOutDir: true, + outDir: OUT_DIR, + rollupOptions: { + input: { + app: join(SRC_DIR, "entrypoint/app.js"), + db: join(SRC_DIR, "entrypoint/db.js"), + style: join(SRC_DIR, "style/main.css.js") + }, + } + } + } +}) \ No newline at end of file diff --git a/app/frontend/yarn.lock b/app/frontend/yarn.lock new file mode 100644 index 0000000..f452999 --- /dev/null +++ b/app/frontend/yarn.lock @@ -0,0 +1,596 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/parser@^7.24.4": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.5.tgz#4a4d5ab4315579e5398a82dcf636ca80c3392790" + integrity sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg== + +"@esbuild/aix-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" + integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g== + +"@esbuild/android-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9" + integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg== + +"@esbuild/android-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995" + integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w== + +"@esbuild/android-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98" + integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg== + +"@esbuild/darwin-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb" + integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA== + +"@esbuild/darwin-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0" + integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA== + +"@esbuild/freebsd-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911" + integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw== + +"@esbuild/freebsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c" + integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw== + +"@esbuild/linux-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5" + integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A== + +"@esbuild/linux-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c" + integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg== + +"@esbuild/linux-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa" + integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig== + +"@esbuild/linux-loong64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5" + integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ== + +"@esbuild/linux-mips64el@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa" + integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA== + +"@esbuild/linux-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20" + integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg== + +"@esbuild/linux-riscv64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300" + integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg== + +"@esbuild/linux-s390x@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685" + integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ== + +"@esbuild/linux-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff" + integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw== + +"@esbuild/netbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6" + integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ== + +"@esbuild/openbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf" + integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ== + +"@esbuild/sunos-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f" + integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w== + +"@esbuild/win32-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90" + integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ== + +"@esbuild/win32-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23" + integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ== + +"@esbuild/win32-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc" + integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ== + +"@jridgewell/sourcemap-codec@^1.4.15": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@mdi/font@^7.4.47": + version "7.4.47" + resolved "https://registry.yarnpkg.com/@mdi/font/-/font-7.4.47.tgz#2ae522867da3a5c88b738d54b403eb91471903af" + integrity sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw== + +"@rollup/rollup-android-arm-eabi@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz#1a32112822660ee104c5dd3a7c595e26100d4c2d" + integrity sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ== + +"@rollup/rollup-android-arm64@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz#5aeef206d65ff4db423f3a93f71af91b28662c5b" + integrity sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw== + +"@rollup/rollup-darwin-arm64@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz#6b66aaf003c70454c292cd5f0236ebdc6ffbdf1a" + integrity sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw== + +"@rollup/rollup-darwin-x64@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz#f64fc51ed12b19f883131ccbcea59fc68cbd6c0b" + integrity sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ== + +"@rollup/rollup-linux-arm-gnueabihf@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz#1a7641111be67c10111f7122d1e375d1226cbf14" + integrity sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A== + +"@rollup/rollup-linux-arm-musleabihf@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz#c93fd632923e0fee25aacd2ae414288d0b7455bb" + integrity sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg== + +"@rollup/rollup-linux-arm64-gnu@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz#fa531425dd21d058a630947527b4612d9d0b4a4a" + integrity sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A== + +"@rollup/rollup-linux-arm64-musl@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz#8acc16f095ceea5854caf7b07e73f7d1802ac5af" + integrity sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA== + +"@rollup/rollup-linux-powerpc64le-gnu@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz#94e69a8499b5cf368911b83a44bb230782aeb571" + integrity sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ== + +"@rollup/rollup-linux-riscv64-gnu@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz#7ef1c781c7e59e85a6ce261cc95d7f1e0b56db0f" + integrity sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg== + +"@rollup/rollup-linux-s390x-gnu@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz#f15775841c3232fca9b78cd25a7a0512c694b354" + integrity sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g== + +"@rollup/rollup-linux-x64-gnu@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz#b521d271798d037ad70c9f85dd97d25f8a52e811" + integrity sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ== + +"@rollup/rollup-linux-x64-musl@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz#9254019cc4baac35800991315d133cc9fd1bf385" + integrity sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q== + +"@rollup/rollup-win32-arm64-msvc@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz#27f65a89f6f52ee9426ec11e3571038e4671790f" + integrity sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA== + +"@rollup/rollup-win32-ia32-msvc@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz#a2fbf8246ed0bb014f078ca34ae6b377a90cb411" + integrity sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ== + +"@rollup/rollup-win32-x64-msvc@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz#5a2d08b81e8064b34242d5cc9973ef8dd1e60503" + integrity sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w== + +"@types/estree@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + +"@vitejs/plugin-vue@^5.0.4": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.0.4.tgz#508d6a0f2440f86945835d903fcc0d95d1bb8a37" + integrity sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ== + +"@vue/compiler-core@3.4.27": + version "3.4.27" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.27.tgz#e69060f4b61429fe57976aa5872cfa21389e4d91" + integrity sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg== + dependencies: + "@babel/parser" "^7.24.4" + "@vue/shared" "3.4.27" + entities "^4.5.0" + estree-walker "^2.0.2" + source-map-js "^1.2.0" + +"@vue/compiler-dom@3.4.27": + version "3.4.27" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.27.tgz#d51d35f40d00ce235d7afc6ad8b09dfd92b1cc1c" + integrity sha512-kUTvochG/oVgE1w5ViSr3KUBh9X7CWirebA3bezTbB5ZKBQZwR2Mwj9uoSKRMFcz4gSMzzLXBPD6KpCLb9nvWw== + dependencies: + "@vue/compiler-core" "3.4.27" + "@vue/shared" "3.4.27" + +"@vue/compiler-sfc@3.4.27": + version "3.4.27" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.27.tgz#399cac1b75c6737bf5440dc9cf3c385bb2959701" + integrity sha512-nDwntUEADssW8e0rrmE0+OrONwmRlegDA1pD6QhVeXxjIytV03yDqTey9SBDiALsvAd5U4ZrEKbMyVXhX6mCGA== + dependencies: + "@babel/parser" "^7.24.4" + "@vue/compiler-core" "3.4.27" + "@vue/compiler-dom" "3.4.27" + "@vue/compiler-ssr" "3.4.27" + "@vue/shared" "3.4.27" + estree-walker "^2.0.2" + magic-string "^0.30.10" + postcss "^8.4.38" + source-map-js "^1.2.0" + +"@vue/compiler-ssr@3.4.27": + version "3.4.27" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.27.tgz#2a8ecfef1cf448b09be633901a9c020360472e3d" + integrity sha512-CVRzSJIltzMG5FcidsW0jKNQnNRYC8bT21VegyMMtHmhW3UOI7knmUehzswXLrExDLE6lQCZdrhD4ogI7c+vuw== + dependencies: + "@vue/compiler-dom" "3.4.27" + "@vue/shared" "3.4.27" + +"@vue/reactivity@3.4.27": + version "3.4.27" + resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.4.27.tgz#6ece72331bf719953f5eaa95ec60b2b8d49e3791" + integrity sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA== + dependencies: + "@vue/shared" "3.4.27" + +"@vue/runtime-core@3.4.27": + version "3.4.27" + resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.4.27.tgz#1b6e1d71e4604ba7442dd25ed22e4a1fc6adbbda" + integrity sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA== + dependencies: + "@vue/reactivity" "3.4.27" + "@vue/shared" "3.4.27" + +"@vue/runtime-dom@3.4.27": + version "3.4.27" + resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.4.27.tgz#fe8d1ce9bbe8921d5dd0ad5c10df0e04ef7a5ee7" + integrity sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q== + dependencies: + "@vue/runtime-core" "3.4.27" + "@vue/shared" "3.4.27" + csstype "^3.1.3" + +"@vue/server-renderer@3.4.27": + version "3.4.27" + resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.4.27.tgz#3306176f37e648ba665f97dda3ce705687be63d2" + integrity sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA== + dependencies: + "@vue/compiler-ssr" "3.4.27" + "@vue/shared" "3.4.27" + +"@vue/shared@3.4.27": + version "3.4.27" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.27.tgz#f05e3cd107d157354bb4ae7a7b5fc9cf73c63b50" + integrity sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA== + +"@vuetify/loader-shared@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@vuetify/loader-shared/-/loader-shared-2.0.3.tgz#11451c717e4a352ec311da52a79c857cd256c92f" + integrity sha512-Ss3GC7eJYkp2SF6xVzsT7FAruEmdihmn4OCk2+UocREerlXKWgOKKzTN5PN3ZVN5q05jHHrsNhTuWbhN61Bpdg== + dependencies: + upath "^2.0.1" + +autoprefixer@^10.4.19: + version "10.4.19" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.19.tgz#ad25a856e82ee9d7898c59583c1afeb3fa65f89f" + integrity sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew== + dependencies: + browserslist "^4.23.0" + caniuse-lite "^1.0.30001599" + fraction.js "^4.3.7" + normalize-range "^0.1.2" + picocolors "^1.0.0" + postcss-value-parser "^4.2.0" + +browserslist@^4.23.0: + version "4.23.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab" + integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== + dependencies: + caniuse-lite "^1.0.30001587" + electron-to-chromium "^1.4.668" + node-releases "^2.0.14" + update-browserslist-db "^1.0.13" + +caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001599: + version "1.0.30001617" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001617.tgz#809bc25f3f5027ceb33142a7d6c40759d7a901eb" + integrity sha512-mLyjzNI9I+Pix8zwcrpxEbGlfqOkF9kM3ptzmKNw5tizSyYwMe+nGLTqMK9cO+0E+Bh6TsBxNAaHWEM8xwSsmA== + +csstype@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + +debug@^4.3.3: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +electron-to-chromium@^1.4.668: + version "1.4.762" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.762.tgz#c29c9d47cf7cc128a9c364baa28adbadde95a47c" + integrity sha512-rrFvGweLxPwwSwJOjIopy3Vr+J3cIPtZzuc74bmlvmBIgQO3VYJDvVrlj94iKZ3ukXUH64Ex31hSfRTLqvjYJQ== + +entities@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + +esbuild@^0.20.1: + version "0.20.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.2.tgz#9d6b2386561766ee6b5a55196c6d766d28c87ea1" + integrity sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g== + optionalDependencies: + "@esbuild/aix-ppc64" "0.20.2" + "@esbuild/android-arm" "0.20.2" + "@esbuild/android-arm64" "0.20.2" + "@esbuild/android-x64" "0.20.2" + "@esbuild/darwin-arm64" "0.20.2" + "@esbuild/darwin-x64" "0.20.2" + "@esbuild/freebsd-arm64" "0.20.2" + "@esbuild/freebsd-x64" "0.20.2" + "@esbuild/linux-arm" "0.20.2" + "@esbuild/linux-arm64" "0.20.2" + "@esbuild/linux-ia32" "0.20.2" + "@esbuild/linux-loong64" "0.20.2" + "@esbuild/linux-mips64el" "0.20.2" + "@esbuild/linux-ppc64" "0.20.2" + "@esbuild/linux-riscv64" "0.20.2" + "@esbuild/linux-s390x" "0.20.2" + "@esbuild/linux-x64" "0.20.2" + "@esbuild/netbsd-x64" "0.20.2" + "@esbuild/openbsd-x64" "0.20.2" + "@esbuild/sunos-x64" "0.20.2" + "@esbuild/win32-arm64" "0.20.2" + "@esbuild/win32-ia32" "0.20.2" + "@esbuild/win32-x64" "0.20.2" + +escalade@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +fraction.js@^4.3.7: + version "4.3.7" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" + integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== + +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +is-core-module@^2.13.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + +magic-string@^0.30.10: + version "0.30.10" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.10.tgz#123d9c41a0cb5640c892b041d4cfb3bd0aa4b39e" + integrity sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + +node-releases@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" + integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + +postcss-import@^16.1.0: + version "16.1.0" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-16.1.0.tgz#258732175518129667fe1e2e2a05b19b5654b96a" + integrity sha512-7hsAZ4xGXl4MW+OKEWCnF6T5jqBw80/EE9aXg1r2yyn1RsVEU8EtKXbijEODa+rg7iih4bKf7vlvTGYR4CnPNg== + dependencies: + postcss-value-parser "^4.0.0" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-simple-vars@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/postcss-simple-vars/-/postcss-simple-vars-7.0.1.tgz#836b3097a54dcd13dbd3c36a5dbdd512fad2954c" + integrity sha512-5GLLXaS8qmzHMOjVxqkk1TZPf1jMqesiI7qLhnlyERalG0sMbHIbJqrcnrpmZdKCLglHnRHoEBB61RtGTsj++A== + +postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss@^8.4.38: + version "8.4.38" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" + integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.2.0" + +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" + integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== + dependencies: + pify "^2.3.0" + +resolve@^1.1.7: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +rollup@^4.13.0: + version "4.17.2" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.17.2.tgz#26d1785d0144122277fdb20ab3a24729ae68301f" + integrity sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ== + dependencies: + "@types/estree" "1.0.5" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.17.2" + "@rollup/rollup-android-arm64" "4.17.2" + "@rollup/rollup-darwin-arm64" "4.17.2" + "@rollup/rollup-darwin-x64" "4.17.2" + "@rollup/rollup-linux-arm-gnueabihf" "4.17.2" + "@rollup/rollup-linux-arm-musleabihf" "4.17.2" + "@rollup/rollup-linux-arm64-gnu" "4.17.2" + "@rollup/rollup-linux-arm64-musl" "4.17.2" + "@rollup/rollup-linux-powerpc64le-gnu" "4.17.2" + "@rollup/rollup-linux-riscv64-gnu" "4.17.2" + "@rollup/rollup-linux-s390x-gnu" "4.17.2" + "@rollup/rollup-linux-x64-gnu" "4.17.2" + "@rollup/rollup-linux-x64-musl" "4.17.2" + "@rollup/rollup-win32-arm64-msvc" "4.17.2" + "@rollup/rollup-win32-ia32-msvc" "4.17.2" + "@rollup/rollup-win32-x64-msvc" "4.17.2" + fsevents "~2.3.2" + +source-map-js@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" + integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +upath@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b" + integrity sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w== + +update-browserslist-db@^1.0.13: + version "1.0.15" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.15.tgz#60ed9f8cba4a728b7ecf7356f641a31e3a691d97" + integrity sha512-K9HWH62x3/EalU1U6sjSZiylm9C8tgq2mSvshZpqc7QE69RaA2qjhkW2HlNA0tFpEbtyFz7HTqbSdN4MSwUodA== + dependencies: + escalade "^3.1.2" + picocolors "^1.0.0" + +vite-plugin-vuetify@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/vite-plugin-vuetify/-/vite-plugin-vuetify-2.0.3.tgz#b65ee4e05cfc6bf2b478a32b6d58b42398519f1e" + integrity sha512-HbYajgGgb/noaVKNRhnnXIiQZrNXfNIeanUGAwXgOxL6h/KULS40Uf51Kyz8hNmdegF+DwjgXXI/8J1PNS83xw== + dependencies: + "@vuetify/loader-shared" "^2.0.3" + debug "^4.3.3" + upath "^2.0.1" + +vite@^5.2.0: + version "5.2.11" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.11.tgz#726ec05555431735853417c3c0bfb36003ca0cbd" + integrity sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ== + dependencies: + esbuild "^0.20.1" + postcss "^8.4.38" + rollup "^4.13.0" + optionalDependencies: + fsevents "~2.3.3" + +vue@^3.4.27: + version "3.4.27" + resolved "https://registry.yarnpkg.com/vue/-/vue-3.4.27.tgz#40b7d929d3e53f427f7f5945386234d2854cc2a1" + integrity sha512-8s/56uK6r01r1icG/aEOHqyMVxd1bkYcSe9j8HcKtr/xTOFWvnzIVTehNW+5Yt89f+DLBe4A569pnZLS5HzAMA== + dependencies: + "@vue/compiler-dom" "3.4.27" + "@vue/compiler-sfc" "3.4.27" + "@vue/runtime-dom" "3.4.27" + "@vue/server-renderer" "3.4.27" + "@vue/shared" "3.4.27" + +vuetify@^3.6.7: + version "3.6.7" + resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.6.7.tgz#f120799ad35b156c48a877aac4633c2250e79903" + integrity sha512-oj7zrNbjKFlSS38449K9/ad9mc/o25bxuTFT9rs2AtSNnOA+BOLIVJCAPlmLGWoQGslPh80RzckXDedoifs+TQ== diff --git a/app/manage.py b/app/manage.py new file mode 100644 index 0000000..4931389 --- /dev/null +++ b/app/manage.py @@ -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() diff --git a/app/requirements/common.txt b/app/requirements/common.txt new file mode 100644 index 0000000..1be7327 --- /dev/null +++ b/app/requirements/common.txt @@ -0,0 +1,8 @@ +Django + +django-cors-headers +django-vite +celery +channels_redis +djangorestframework +django-celery-beat \ No newline at end of file diff --git a/app/requirements/dev.txt b/app/requirements/dev.txt new file mode 100644 index 0000000..cb7bfa7 --- /dev/null +++ b/app/requirements/dev.txt @@ -0,0 +1,3 @@ +-r common.txt + +channels[daphne] \ No newline at end of file diff --git a/app/requirements/prod.txt b/app/requirements/prod.txt new file mode 100644 index 0000000..2b8afdb --- /dev/null +++ b/app/requirements/prod.txt @@ -0,0 +1,4 @@ +-r common.txt + +uvicorn +channels \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..a5e48bb --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,23 @@ +{% load django_vite %} + + + + + + + Document + {% block base_css %} + {% block css %}{% endblock %} + + {% endblock %} + + + {% block content %}

hello world

{% endblock %} + {% block base_js %} + {% vite_hmr_client %} + {% vite_asset "entrypoint/app.js" %} + {% block js %}{% endblock %} + {% endblock %} + + \ No newline at end of file diff --git a/app/user/__init__.py b/app/user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/user/admin.py b/app/user/admin.py new file mode 100644 index 0000000..dfd024b --- /dev/null +++ b/app/user/admin.py @@ -0,0 +1,27 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin + +from .models import User +from .forms import UserCreationForm, UserChangeForm + + +@admin.register(User) +class UserAdmin(BaseUserAdmin): + add_form = UserCreationForm + form = UserChangeForm + model = User + list_display = ["email", "is_staff", "is_superuser", "is_active"] + list_filter = ["email", "is_staff", "is_superuser", "is_active"] + fieldsets = [ + [None, {"fields": ["email", "password"]}], + ("permissions", {"fields": ["is_staff", "is_active", "is_superuser"]}) + ] + add_fieldsets = [ + [None, { + "classes": ["wide"], + "fields": ["email", "password1", "password2", "is_staff", "is_active", "is_superuser"] + }] + ] + search_fields = ["email"] + ordering = ["email"] + diff --git a/app/user/apps.py b/app/user/apps.py new file mode 100644 index 0000000..36cce4c --- /dev/null +++ b/app/user/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UserConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'user' diff --git a/app/user/forms.py b/app/user/forms.py new file mode 100644 index 0000000..004b517 --- /dev/null +++ b/app/user/forms.py @@ -0,0 +1,11 @@ +from django.contrib.auth import forms as BaseAuthForms + +from .models import User + + +class UserCreationForm(BaseAuthForms.UserCreationForm): + pass + + +class UserChangeForm(BaseAuthForms.UserChangeForm): + pass diff --git a/app/user/migrations/0001_initial.py b/app/user/migrations/0001_initial.py new file mode 100644 index 0000000..977f1e1 --- /dev/null +++ b/app/user/migrations/0001_initial.py @@ -0,0 +1,44 @@ +# Generated by Django 5.0.6 on 2024-05-15 14:10 + +import django.contrib.auth.validators +import django.utils.timezone +import user.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('email', models.EmailField(max_length=254, unique=True)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', user.models.EmailUserManager()), + ], + ), + ] diff --git a/app/user/migrations/__init__.py b/app/user/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/user/models.py b/app/user/models.py new file mode 100644 index 0000000..336c5a9 --- /dev/null +++ b/app/user/models.py @@ -0,0 +1,72 @@ +from django.db import models +from django.contrib.auth.models import AbstractUser, BaseUserManager + + +class EmailUserManager(BaseUserManager): + use_in_migrations = True + + def _create_user(self, email, password, **extra_fields): + if not email: + raise ValueError("Un email doit être défini") + + email = self.normalize_email(email) + user = self.model(email=email, **extra_fields) + user.set_password(password) + user.save(using=self.db) + return user + + def create_user(self, username, email, password=None, **extra_fields): + extra_fields.setdefault("is_staff", False) + extra_fields.setdefault("is_superuser", False) + return self._create_user(username, email, password, **extra_fields) + + def create_superuser(self, email, password, **extra_fields): + extra_fields.setdefault("is_staff", True) + extra_fields.setdefault("is_superuser", True) + + if extra_fields.get("is_staff") is not True: + raise ValueError("Superuser doit être staff à True") + if extra_fields.get("is_superuser") is not True: + raise ValueError("SuperUser doit être is_superuser à True") + + return self._create_user(email, password, **extra_fields) + + +# class UsernameUserManager(BaseUserManager): +# use_in_migrations = True +# +# def _create_user(self, username, email, password, **extra_fields): +# if not username: +# raise ValueError("Un username doit être défini") +# if not email: +# raise ValueError("Un email doit être défini") +# +# email = self.normalize_email(email) +# user = self.model(username=username, email=email, **extra_fields) +# user.set_password(password) +# user.save(using=self.db) +# return user +# +# def create_user(self, username, email, password=None, **extra_fields): +# extra_fields.setdefault("is_staff", False) +# extra_fields.setdefault("is_superuser", False) +# return self._create_user(username, email, password, **extra_fields) +# +# def create_superuser(self, username, email, password, **extra_fields): +# extra_fields.setdefault("is_staff", True) +# extra_fields.setdefault("is_superuser", True) +# +# if extra_fields.get("is_staff") is not True: +# raise ValueError("Superuser doit être staff à True") +# if extra_fields.get("is_superuser") is not True: +# raise ValueError("SuperUser doit être is_superuser à True") +# +# return self._create_user(username, email, password, **extra_fields) + + +class User(AbstractUser): + email = models.EmailField(unique=True) + USERNAME_FIELD = 'email' + + REQUIRED_FIELDS = [] + objects = EmailUserManager() diff --git a/app/user/tests.py b/app/user/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/app/user/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/app/user/views.py b/app/user/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/app/user/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/backups/.gitkeep b/backups/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..206c2f9 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,18 @@ +services: + redis: + restart: "no" + + app: + restart: "no" + command: > + bash -c "sleep 2 + && python manage.py migrate + && ./dev_run.sh" + ports: + - "${DEV_SERVER_PORT:-8080}:${DEV_SERVER_PORT:-8080}" + + tasks: + restart: "no" + + jobs: + restart: "no" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9ad4c96 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,56 @@ +services: + redis: + image: redis:alpine + restart: unless-stopped + + app: + image: app + build: + context: ./app + args: + ENV: ${ENV} + PUID: ${USER_ID} + PGID: ${GROUP_ID} + env_file: + - .env +# user: "${USER_ID}:${GROUP_ID}" + volumes: + - ./app:/app + - ./backups:/backups + - /app/frontend/node_modules + restart: unless-stopped + ports: + - "${LISTEN_PORT:-8000}:8000" + command: > + bash -c "cd frontend && yarn build && cd .. + && python manage.py collectstatic --noinput + && python manage.py migrate + && uvicorn app.asgi:application --workers 3 --host 0.0.0.0 --port 8000" + + tasks: + image: app + volumes_from: + - app + env_file: + - .env +# user: "${USER_ID}:${GROUP_ID}" + restart: unless-stopped + depends_on: + - app + command: > + bash -c "sleep 5 + && celery -A app worker --loglevel=info" + + jobs: + image: app + volumes_from: + - app + env_file: + - .env + # user: "${USER_ID}:${GROUP_ID}" + restart: unless-stopped + depends_on: + - app + command: > + bash -c "sleep 5 + && celery -A app beat --scheduler django_celery_beat.schedulers:DatabaseScheduler --loglevel=info" \ No newline at end of file