From 29611b15ca895e13747edf84636b8b31cb3071fb Mon Sep 17 00:00:00 2001 From: Nell Date: Sun, 31 Aug 2025 00:29:53 +0200 Subject: [PATCH] init --- .env.dist | 24 +++ .idea/.gitignore | 8 + .idea/inspectionProfiles/Project_Default.xml | 6 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 7 + .idea/modules.xml | 8 + .idea/oximg.iml | 32 ++++ .idea/vcs.xml | 6 + app/.dockerignore | 9 + app/Dockerfile | 72 +++++++ app/api/__init__.py | 0 app/api/apps.py | 6 + app/api/autoload.py | 19 ++ app/api/routers.py | 4 + app/api/tests.py | 3 + app/api/urls.py | 20 ++ app/api/utils.py | 14 ++ app/app/__init__.py | 0 app/app/asgi.py | 17 ++ app/app/settings.py | 179 ++++++++++++++++++ app/app/urls.py | 26 +++ app/app/wsgi.py | 16 ++ app/core/__init__.py | 0 app/core/admin.py | 3 + app/core/apps.py | 6 + app/core/migrations/__init__.py | 0 app/core/models.py | 3 + app/core/templates/core/index.html | 2 + app/core/templates/core/layout.html | 0 app/core/tests.py | 3 + app/core/urls.py | 5 + app/core/views.py | 5 + app/db.sqlite3 | Bin 0 -> 204800 bytes app/dev_run.sh | 12 ++ app/frontend/jsconfig.json | 8 + app/frontend/package.json | 23 +++ app/frontend/postcss.config.js | 7 + app/frontend/vite.config.js | 44 +++++ app/hypercorn.toml | 4 + app/hypercorn_dev.toml | 9 + app/manage.py | 22 +++ app/requirements/common.txt | 9 + app/requirements/dev.txt | 3 + app/requirements/prod.txt | 1 + app/templates/base.html | 38 ++++ app/upload/__init__.py | 0 app/upload/admin.py | 3 + app/upload/api/serializers.py | 10 + app/upload/api/viewsets.py | 19 ++ app/upload/apps.py | 6 + app/upload/forms.py | 26 +++ app/upload/migrations/0001_initial.py | 148 +++++++++++++++ app/upload/migrations/__init__.py | 0 app/upload/models.py | 172 +++++++++++++++++ app/upload/templates/upload/layout.html | 1 + app/upload/templates/upload/upload_form.html | 2 + app/upload/tests.py | 3 + app/upload/utils.py | 26 +++ app/upload/views.py | 25 +++ app/user/__init__.py | 0 app/user/admin.py | 3 + app/user/api/registry.py | 6 + app/user/api/serializers.py | 11 ++ app/user/api/viewsets.py | 17 ++ app/user/apps.py | 6 + app/user/forms.py | 17 ++ app/user/migrations/0001_initial.py | 45 +++++ .../0002_alter_user_max_upload_size.py | 18 ++ app/user/migrations/__init__.py | 0 app/user/models.py | 43 +++++ app/user/templates/user/login.html | 135 +++++++++++++ .../templates/user/password_change_done.html | 79 ++++++++ .../templates/user/password_change_form.html | 125 ++++++++++++ .../user/password_reset_complete.html | 79 ++++++++ .../user/password_reset_confirm.html | 149 +++++++++++++++ .../templates/user/password_reset_done.html | 88 +++++++++ .../templates/user/password_reset_email.html | 14 ++ .../templates/user/password_reset_form.html | 151 +++++++++++++++ .../templates/user/password_reset_subject.txt | 1 + app/user/templates/user/register.html | 143 ++++++++++++++ app/user/tests.py | 3 + app/user/urls.py | 41 ++++ app/user/views.py | 18 ++ docker-compose.dev.yml | 15 ++ docker-compose.yml | 60 ++++++ nginx/default.nginx | 29 +++ nginx/dev.nginx | 25 +++ 87 files changed, 2451 insertions(+) create mode 100644 .env.dist create mode 100644 .idea/.gitignore create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/oximg.iml create mode 100644 .idea/vcs.xml create mode 100644 app/.dockerignore create mode 100644 app/Dockerfile create mode 100644 app/api/__init__.py create mode 100644 app/api/apps.py create mode 100644 app/api/autoload.py create mode 100644 app/api/routers.py create mode 100644 app/api/tests.py create mode 100644 app/api/urls.py create mode 100644 app/api/utils.py create mode 100644 app/app/__init__.py create mode 100644 app/app/asgi.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/core/__init__.py create mode 100644 app/core/admin.py create mode 100644 app/core/apps.py create mode 100644 app/core/migrations/__init__.py create mode 100644 app/core/models.py create mode 100644 app/core/templates/core/index.html create mode 100644 app/core/templates/core/layout.html create mode 100644 app/core/tests.py create mode 100644 app/core/urls.py create mode 100644 app/core/views.py create mode 100644 app/db.sqlite3 create mode 100644 app/dev_run.sh create mode 100644 app/frontend/jsconfig.json create mode 100644 app/frontend/package.json create mode 100644 app/frontend/postcss.config.js create mode 100644 app/frontend/vite.config.js create mode 100644 app/hypercorn.toml create mode 100644 app/hypercorn_dev.toml 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/upload/__init__.py create mode 100644 app/upload/admin.py create mode 100644 app/upload/api/serializers.py create mode 100644 app/upload/api/viewsets.py create mode 100644 app/upload/apps.py create mode 100644 app/upload/forms.py create mode 100644 app/upload/migrations/0001_initial.py create mode 100644 app/upload/migrations/__init__.py create mode 100644 app/upload/models.py create mode 100644 app/upload/templates/upload/layout.html create mode 100644 app/upload/templates/upload/upload_form.html create mode 100644 app/upload/tests.py create mode 100644 app/upload/utils.py create mode 100644 app/upload/views.py create mode 100644 app/user/__init__.py create mode 100644 app/user/admin.py create mode 100644 app/user/api/registry.py create mode 100644 app/user/api/serializers.py create mode 100644 app/user/api/viewsets.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/0002_alter_user_max_upload_size.py create mode 100644 app/user/migrations/__init__.py create mode 100644 app/user/models.py create mode 100644 app/user/templates/user/login.html create mode 100644 app/user/templates/user/password_change_done.html create mode 100644 app/user/templates/user/password_change_form.html create mode 100644 app/user/templates/user/password_reset_complete.html create mode 100644 app/user/templates/user/password_reset_confirm.html create mode 100644 app/user/templates/user/password_reset_done.html create mode 100644 app/user/templates/user/password_reset_email.html create mode 100644 app/user/templates/user/password_reset_form.html create mode 100644 app/user/templates/user/password_reset_subject.txt create mode 100644 app/user/templates/user/register.html create mode 100644 app/user/tests.py create mode 100644 app/user/urls.py create mode 100644 app/user/views.py create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.yml create mode 100644 nginx/default.nginx create mode 100644 nginx/dev.nginx diff --git a/.env.dist b/.env.dist new file mode 100644 index 0000000..4b7d22a --- /dev/null +++ b/.env.dist @@ -0,0 +1,24 @@ +USER_ID=1000 # id user +GROUP_ID=1000 + +TRAEFIK_HOST_RULE="Host(`example.com`) || Host(`www.example.com`)" + +LISTEN_PORT=8000 # only for dev + +DEBUG=true +SECRET="change_me" # generate with "pwgen -y 50" +ALLOWED_HOSTS='["127.0.0.1", "localhost"]' +CSRF_TRUSTED_ORIGINS='["http://127.0.0.1:8000", "http://localhost:8000"]' +BASE_URL='http://127.0.0.1/' + +### DEV RELATED +# VITE +DEV_SERVER_HOST='127.0.0.1' +DEV_SERVER_PORT='8080' + +EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend # django.core.mail.backends.smtp.EmailBackend +EMAIL_HOST=example.test # +EMAIL_PORT=587 # 587 +EMAIL_USER=test # +EMAIL_PASSWORD=test # +EMAIL_SENDER=no-reply@example.test diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..ac21435 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..526a218 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..609c0f6 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/oximg.iml b/.idea/oximg.iml new file mode 100644 index 0000000..0e25da0 --- /dev/null +++ b/.idea/oximg.iml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.dockerignore b/app/.dockerignore new file mode 100644 index 0000000..6e95ae2 --- /dev/null +++ b/app/.dockerignore @@ -0,0 +1,9 @@ +media/ +static_collected/ +*.pyc +__pycache__/ +.env +.git/ +venv/ +*.sqlite3 +frontend/node_modules \ No newline at end of file diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000..ed44ccb --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,72 @@ +FROM python:3.12-slim + +ARG puid=1000 +ARG pgid=1000 +ARG debug=false +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + LANG=C.UTF-8 \ + LC_ALL=C.UTF-8 \ + DEBUG=${debug} + +# setup user +RUN groupadd -g ${pgid} -o custom_user && useradd -m -u ${puid} -g ${pgid} -o -s /bin/bash custom_user + +# setup system requirements +RUN apt-get update && apt-get install -y \ + gcc \ + libjpeg-dev \ + zlib1g-dev \ + curl \ + gnupg \ + ca-certificates \ + inotify-tools \ + libmagic1 \ + && mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ + && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \ + && apt-get update && apt-get install nodejs -y \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + && npm install -g yarn + +# setup node requirements +WORKDIR /app/frontend +COPY ./frontend/package.json ./package.json +RUN yarn install +RUN if [ "$debug" = "true" ] ; then \ + echo "Mode dev, pas de build frontend" ; \ + else \ + yarn build ; \ + fi +RUN chown -R custom_user:custom_user /app/frontend/node_modules + +# setup python requirements +WORKDIR /app +COPY requirements/ requirements/ +RUN if [ "$debug" = "true" ] ; then \ + pip install --no-cache-dir -r requirements/dev.txt ; \ + else \ + pip install --no-cache-dir -r requirements/prod.txt ; \ + fi + +COPY . . + +# collect static +RUN if [ "$debug" = "true" ] ; then \ + echo "Mode dev, pas de collectstatic" ; \ + else \ + python manage.py collectstatic --noinput ; \ + fi + +USER custom_user +# setup ipython +RUN mkdir -p ~/.ipython/profile_default/ && echo "c.InteractiveShellApp.extensions = ['autoreload']\nc.InteractiveShellApp.exec_lines = ['%autoreload 2']" > ~/.ipython/profile_default/ipython_config.py + +HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 CMD \ + pgrep hypercorn >/dev/null 2>&1 && curl -f http://127.0.0.1:8000/health/ || exit 0 + +EXPOSE 8000 + +# Utiliser Hypercorn pour lancer l'app ASGI +CMD ["hypercorn", "monprojet.asgi:application", "--bind", "0.0.0.0:8000"] diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/apps.py b/app/api/apps.py new file mode 100644 index 0000000..66656fd --- /dev/null +++ b/app/api/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'api' diff --git a/app/api/autoload.py b/app/api/autoload.py new file mode 100644 index 0000000..5674721 --- /dev/null +++ b/app/api/autoload.py @@ -0,0 +1,19 @@ +from django.conf import settings + +import importlib + + +def load_viewset_registries(router): + """ + Parcourt toutes les apps installées et appelle leur register_viewsets(router) si présent. + """ + for app in settings.INSTALLED_APPS: + module_path = f"{app}.api.registry" + try: + module = importlib.import_module(module_path) + if hasattr(module, "register_viewsets"): + module.register_viewsets(router) + except ModuleNotFoundError: + continue + except AttributeError as e: + print(f"[ERROR] Could not import {module_path}: {e}") diff --git a/app/api/routers.py b/app/api/routers.py new file mode 100644 index 0000000..fc89141 --- /dev/null +++ b/app/api/routers.py @@ -0,0 +1,4 @@ +from rest_framework import routers + + +router = routers.DefaultRouter() diff --git a/app/api/tests.py b/app/api/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/app/api/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/app/api/urls.py b/app/api/urls.py new file mode 100644 index 0000000..5a93802 --- /dev/null +++ b/app/api/urls.py @@ -0,0 +1,20 @@ +from django.urls import path, include + +from rest_framework.authtoken import views + +from .routers import router +from .autoload import load_viewset_registries + +load_viewset_registries(router) + +app_name = 'api' +urlpatterns = [ + # Endpoints d'API REST + path('', include(router.urls)), + + # Endpoint d'authentification par token + path('auth-token/', views.obtain_auth_token, name='api-token-auth'), + + # Endpoints d'authentification de l'API browsable + path('auth/', include('rest_framework.urls', namespace='rest_framework')), +] diff --git a/app/api/utils.py b/app/api/utils.py new file mode 100644 index 0000000..614a1e6 --- /dev/null +++ b/app/api/utils.py @@ -0,0 +1,14 @@ +import inspect + + +def register_in_app(router, prefix, viewset, basename=None): + # Trouve le module appelant pour déduire le nom de l'app + caller = inspect.stack()[1] + module = inspect.getmodule(caller.frame) + module_parts = module.__name__.split('.') + app_label = module_parts[0] # ex: "my_app" + + full_prefix = f"{app_label}/{prefix.strip('/')}" + if basename is None: + basename = f"{app_label}-{prefix}" + router.register(full_prefix, viewset, basename=basename) diff --git a/app/app/__init__.py b/app/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/app/asgi.py b/app/app/asgi.py new file mode 100644 index 0000000..0cb7099 --- /dev/null +++ b/app/app/asgi.py @@ -0,0 +1,17 @@ +""" +ASGI config for app project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/ +""" + +import os +from django.core.asgi import get_asgi_application +from asgiref.compatibility import guarantee_single_callable + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') + +django_application = get_asgi_application() +application = guarantee_single_callable(django_application) diff --git a/app/app/settings.py b/app/app/settings.py new file mode 100644 index 0000000..cabeead --- /dev/null +++ b/app/app/settings.py @@ -0,0 +1,179 @@ +""" +Django settings for app project. + +Generated by 'django-admin startproject' using Django 5.2. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.2/ref/settings/ +""" + +from pathlib import Path +from os import getenv +import ast + + +# 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.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = getenv("SECRET", "random_need_to_be_generated") + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = getenv("DEBUG", "false") == "true" + +ALLOWED_HOSTS = ["app", "127.0.0.1", "localhost"] +if getenv("ALLOWED_HOSTS"): + ALLOWED_HOSTS += ast.literal_eval(getenv("ALLOWED_HOSTS")) + +CSRF_TRUSTED_ORIGINS = [] +for allowed_host in ALLOWED_HOSTS: + CSRF_TRUSTED_ORIGINS += ast.literal_eval(getenv("CSRF_TRUSTED_ORIGINS")) + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + + "django_vite", + "rest_framework", + + 'api', + 'user', + 'upload', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] +if DEBUG: + MIDDLEWARE = ["whitenoise.middleware.WhiteNoiseMiddleware"] + MIDDLEWARE + +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.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.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.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/5.2/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.2/howto/static-files/ + +STATIC_URL = "static/" +STATIC_ROOT = BASE_DIR / "static_collected" +STATICFILES_DIRS = [ + BASE_DIR / "static", + BASE_DIR / "frontend/dist", +] + +MEDIA_URL = "media/" +MEDIA_ROOT = BASE_DIR / "media" + +# Default primary key field type +# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# User related +AUTH_USER_MODEL = "user.User" +LOGIN_REDIRECT_URL = "/" +LOGIN_URL = "/user/login/" +LOGOUT_REDIRECT_URL = "/user/login/" +SESSION_COOKIE_AGE = 30*24*60*60 # 30 days + +# Django-vite related +# https://github.com/MrBin99/django-vite/tree/master +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)), + } +} + +# Email related +EMAIL_BACKEND = getenv("EMAIL_BACKEND", 'django.core.mail.backends.console.EmailBackend') +EMAIL_HOST = getenv("EMAIL_HOST", None) +EMAIL_PORT = getenv("EMAIL_PORT", None) +EMAIL_HOST_USER = getenv("EMAIL_USER", None) +EMAIL_HOST_PASSWORD = getenv("EMAIL_PASSWORD", None) +EMAIL_SENDER = getenv("EMAIL_SENDER", None) +EMAIL_USE_TLS = True +DEFAULT_FROM_EMAIL = EMAIL_SENDER +EMAIL_SUBJECT_PREFIX = getenv("EMAIL_SUBJECT_PREFIX", None) +EMAIL_ADMIN = getenv("EMAIL_ADMIN", None) diff --git a/app/app/urls.py b/app/app/urls.py new file mode 100644 index 0000000..7cf37e4 --- /dev/null +++ b/app/app/urls.py @@ -0,0 +1,26 @@ +""" +URL configuration for app project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.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.urls import path, include +from django.http import HttpResponse + +urlpatterns = [ + path("health/", lambda request: HttpResponse("OK")), + path('admin/', admin.site.urls), + path('api/', include('api.urls', namespace='api')), + path('user/', include('user.urls', namespace='user')), +] diff --git a/app/app/wsgi.py b/app/app/wsgi.py new file mode 100644 index 0000000..121dd78 --- /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.2/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/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/admin.py b/app/core/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/app/core/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/app/core/apps.py b/app/core/apps.py new file mode 100644 index 0000000..8115ae6 --- /dev/null +++ b/app/core/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'core' diff --git a/app/core/migrations/__init__.py b/app/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/models.py b/app/core/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/app/core/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/app/core/templates/core/index.html b/app/core/templates/core/index.html new file mode 100644 index 0000000..9142dac --- /dev/null +++ b/app/core/templates/core/index.html @@ -0,0 +1,2 @@ +{% extends "core/layout.html" %} + diff --git a/app/core/templates/core/layout.html b/app/core/templates/core/layout.html new file mode 100644 index 0000000..e69de29 diff --git a/app/core/tests.py b/app/core/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/app/core/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/app/core/urls.py b/app/core/urls.py new file mode 100644 index 0000000..0150d4f --- /dev/null +++ b/app/core/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +urlspatterns = [ + +] \ No newline at end of file diff --git a/app/core/views.py b/app/core/views.py new file mode 100644 index 0000000..e0bcbc6 --- /dev/null +++ b/app/core/views.py @@ -0,0 +1,5 @@ +from django.views.generic import TemplateView + + +class IndexView(TemplateView): + template_name = "core/index.html" diff --git a/app/db.sqlite3 b/app/db.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..29692a7423d65395411a4b5951cbbe0c53217520 GIT binary patch literal 204800 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 literal 0 HcmV?d00001 diff --git a/app/dev_run.sh b/app/dev_run.sh new file mode 100644 index 0000000..538a4c5 --- /dev/null +++ b/app/dev_run.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +CORES=$(nproc) +WORKERS=$((2 * CORES + 1)) + +cd /app/frontend && yarn dev & +#cd /app && python manage.py runserver 0.0.0.0:8000 & +cd /app && hypercorn app.asgi:application -c hypercorn_dev.toml & + +wait -n + +exit $? \ No newline at end of file diff --git a/app/frontend/jsconfig.json b/app/frontend/jsconfig.json new file mode 100644 index 0000000..babf8fb --- /dev/null +++ b/app/frontend/jsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} \ No newline at end of file diff --git a/app/frontend/package.json b/app/frontend/package.json new file mode 100644 index 0000000..a98a110 --- /dev/null +++ b/app/frontend/package.json @@ -0,0 +1,23 @@ +{ + "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", + "autoprefixer": "^10.4.20", + "postcss": "^8.5.2", + "postcss-import": "^16.1.0", + "postcss-simple-vars": "^7.0.1", + "vite": "^6.1.0" + }, + "dependencies": { + "events": "^3.3.0", + "moment": "^2.30.1" + } +} diff --git a/app/frontend/postcss.config.js b/app/frontend/postcss.config.js new file mode 100644 index 0000000..058a505 --- /dev/null +++ b/app/frontend/postcss.config.js @@ -0,0 +1,7 @@ +export default { + plugins: { + 'postcss-import': {}, + 'postcss-simple-vars': {}, + 'autoprefixer': {} + } +} \ 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..e5dc52e --- /dev/null +++ b/app/frontend/vite.config.js @@ -0,0 +1,44 @@ +import {defineConfig, loadEnv} from "vite"; +import {resolve, join} from "path"; + +export default defineConfig((mode) => { + const env = loadEnv(mode, "..", ""), + SRC_DIR = resolve("./src"), + OUT_DIR = resolve("./dist") + + return { + plugins: [ + + ], + resolve: { + alias: { + "@": resolve(SRC_DIR), + "vue": "vue/dist/vue.esm-bundler.js" + } + }, + root: SRC_DIR, + base: "/static/", + css: { + postcss: "./postcss.config.js" + }, + 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, + reloadDelay: 500, + } + }, + build: { + manifest: "manifest.json", + emptyOutDir: true, + outDir: OUT_DIR, + rollupOptions: { + input: { + // admin: join(SRC_DIR, "app/admin-entrypoint.js"), + } + } + } + } +}) \ No newline at end of file diff --git a/app/hypercorn.toml b/app/hypercorn.toml new file mode 100644 index 0000000..62ff635 --- /dev/null +++ b/app/hypercorn.toml @@ -0,0 +1,4 @@ +bind = ["0.0.0.0:8000"] +loop = "uvloop" +worker_class = "asyncio" # asyncio ou trio +workers = 4 \ No newline at end of file diff --git a/app/hypercorn_dev.toml b/app/hypercorn_dev.toml new file mode 100644 index 0000000..8e50005 --- /dev/null +++ b/app/hypercorn_dev.toml @@ -0,0 +1,9 @@ +bind = ["0.0.0.0:8000"] +loop = "uvloop" +worker_class = "asyncio" # asyncio ou trio +workers = 4 +debug = true +reload = true +accesslog = "-" +errorlog = "-" +loglevel = "INFO" 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..a55e97e --- /dev/null +++ b/app/requirements/common.txt @@ -0,0 +1,9 @@ +Django +django-vite +Hypercorn[uvloop] +uvloop +python-magic +nanoid +djangorestframework +markdown +django-filter \ No newline at end of file diff --git a/app/requirements/dev.txt b/app/requirements/dev.txt new file mode 100644 index 0000000..48c5fb4 --- /dev/null +++ b/app/requirements/dev.txt @@ -0,0 +1,3 @@ +-r common.txt +whitenoise +django-stubs # temporaire le temps que pycharm sorte la version 2025.1.1 diff --git a/app/requirements/prod.txt b/app/requirements/prod.txt new file mode 100644 index 0000000..c3899b0 --- /dev/null +++ b/app/requirements/prod.txt @@ -0,0 +1 @@ +-r common.txt \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..90ef1f9 --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,38 @@ + + + + + + {% block base_title %}{% endblock %} + {% load django_vite %} + {% vite_hmr_client %} + + {% block extra_head %}{% endblock %} + + + +
+ {% block content %}{% endblock %} + {% vite_asset 'main.js' %} +
+ + diff --git a/app/upload/__init__.py b/app/upload/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/upload/admin.py b/app/upload/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/app/upload/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/app/upload/api/serializers.py b/app/upload/api/serializers.py new file mode 100644 index 0000000..936193e --- /dev/null +++ b/app/upload/api/serializers.py @@ -0,0 +1,10 @@ +from rest_framework import serializers + +from upload.models import Upload + + +class UploadSerializer(serializers.ModelSerializer): + class Meta: + model = Upload + fields = ['id', 'created_at', 'user', 'filename', 'slug'] + read_only_fields = ['id', 'created_at', 'slug'] \ No newline at end of file diff --git a/app/upload/api/viewsets.py b/app/upload/api/viewsets.py new file mode 100644 index 0000000..fdf3fcf --- /dev/null +++ b/app/upload/api/viewsets.py @@ -0,0 +1,19 @@ +from rest_framework import viewsets, permissions + +from upload.models import Upload +from upload.api.serializers import UploadSerializer + +class UploadViewSet(viewsets.ModelViewSet): + queryset = Upload.objects.all() + serializer_class = UploadSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + # Les utilisateurs ne peuvent voir que leurs propres uploads + if not self.request.user.is_staff: + return Upload.objects.filter(user=self.request.user) + # Les administrateurs peuvent voir tous les uploads + return Upload.objects.all() + + def perform_create(self, serializer): + serializer.save(user=self.request.user) \ No newline at end of file diff --git a/app/upload/apps.py b/app/upload/apps.py new file mode 100644 index 0000000..2dcd356 --- /dev/null +++ b/app/upload/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UploadConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'upload' diff --git a/app/upload/forms.py b/app/upload/forms.py new file mode 100644 index 0000000..97b8769 --- /dev/null +++ b/app/upload/forms.py @@ -0,0 +1,26 @@ +from django import forms + +from .models import Upload, Image + + +class UploadImageForm(forms.ModelForm): + class Meta: + model = Image + fields = ['file'] + + def save(self, commit=True): + image = super().save(commit=False) + + if not image.name: + image.name = image.file.name + + if not image.content_type: + from utils import get_mimetype + image.content_type = get_mimetype(image.file) + + image.size = image.file.size + + if commit: + image.save() + + return image diff --git a/app/upload/migrations/0001_initial.py b/app/upload/migrations/0001_initial.py new file mode 100644 index 0000000..09d1223 --- /dev/null +++ b/app/upload/migrations/0001_initial.py @@ -0,0 +1,148 @@ +# Generated by Django 5.2 on 2025-05-04 14:43 + +import django.db.models.deletion +import upload.models +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Archive', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField(upload_to=upload.models.upload_to)), + ('content_type', models.CharField(max_length=255)), + ('size', models.PositiveBigIntegerField()), + ('name', models.CharField(max_length=255)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Audio', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField(upload_to=upload.models.upload_to)), + ('content_type', models.CharField(max_length=255)), + ('size', models.PositiveBigIntegerField()), + ('name', models.CharField(max_length=255)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Code', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField(upload_to=upload.models.upload_to)), + ('content_type', models.CharField(max_length=255)), + ('size', models.PositiveBigIntegerField()), + ('name', models.CharField(max_length=255)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Document', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField(upload_to=upload.models.upload_to)), + ('content_type', models.CharField(max_length=255)), + ('size', models.PositiveBigIntegerField()), + ('name', models.CharField(max_length=255)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Image', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField(upload_to=upload.models.upload_to)), + ('content_type', models.CharField(max_length=255)), + ('size', models.PositiveBigIntegerField()), + ('name', models.CharField(max_length=255)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Other', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField(upload_to=upload.models.upload_to)), + ('content_type', models.CharField(max_length=255)), + ('size', models.PositiveBigIntegerField()), + ('name', models.CharField(max_length=255)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='StructuredData', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField(upload_to=upload.models.upload_to)), + ('content_type', models.CharField(max_length=255)), + ('size', models.PositiveBigIntegerField()), + ('name', models.CharField(max_length=255)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Text', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField(upload_to=upload.models.upload_to)), + ('content_type', models.CharField(max_length=255)), + ('size', models.PositiveBigIntegerField()), + ('name', models.CharField(max_length=255)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Video', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField(upload_to=upload.models.upload_to)), + ('content_type', models.CharField(max_length=255)), + ('size', models.PositiveBigIntegerField()), + ('name', models.CharField(max_length=255)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Upload', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('filename', models.CharField(max_length=255)), + ('object_id', models.UUIDField(blank=True, null=True)), + ('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/app/upload/migrations/__init__.py b/app/upload/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/upload/models.py b/app/upload/models.py new file mode 100644 index 0000000..aee6b3c --- /dev/null +++ b/app/upload/models.py @@ -0,0 +1,172 @@ +from django.utils import timezone +from django.db import models +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType + +import uuid +from pathlib import Path +from nanoid import generate + + +def generate_unique_slug_for_upload(size=10): + while True: + slug = generate(size=size) + if not Upload.objects.filter(slug=slug).exists(): + return slug + + + +def upload_to(instance, filename): + today = timezone.now().date() + dest_path = Path(f"uploads/{str(today.year)}/{str(today.month)}/{str(today.day)}") + if not dest_path.exists(): + dest_path.mkdir(parents=True) + + uid = uuid.uuid4().hex + file_path = Path(filename) + return str(dest_path / f"{uid}{file_path.suffix}") + + +class Upload(models.Model): + id = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4) + created_at = models.DateTimeField(auto_now_add=True) + user = models.ForeignKey('user.User', on_delete=models.CASCADE, null=True, blank=True) + filename = models.CharField(max_length=255) + slug = models.SlugField(max_length=255, unique=True, default=generate_unique_slug_for_upload, editable=False) + + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, blank=True) + object_id = models.UUIDField(null=True, blank=True) + content_object = GenericForeignKey('content_type', 'object_id') + + +class AbstractFile(models.Model): + file = models.FileField(upload_to=upload_to) + content_type = models.CharField(max_length=255) + size = models.PositiveBigIntegerField() + name = models.CharField(max_length=255) + + class Meta: + abstract = True + + def save(self, *args, **kwargs): + if self.file: + from utils import get_mimetype + self.size = self.file.size + self.content_type = get_mimetype(self.file) + self.name = self.file.name + + +class Image(AbstractFile): + allowed_mimetypes = [ + "image/jpeg", + "image/png", + "image/gif", + "image/webp", + "image/bmp", + "image/svg+xml", + "image/tiff", + "image/x-icon", # favicon + "image/vnd.microsoft.icon" + ] + pass + +class Video(AbstractFile): + allowed_mimetypes = [ + "video/mp4", + "video/quicktime", # .mov + "video/x-msvideo", # .avi + "video/x-matroska", # .mkv + "video/webm", + "video/mpeg", + "video/ogg" + ] + pass + +class Audio(AbstractFile): + allowed_mimetypes = [ + "audio/mpeg", # .mp3 + "audio/wav", # .wav + "audio/x-wav", + "audio/ogg", # .ogg + "audio/webm", # .webm audio + "audio/aac", + "audio/flac", + "audio/x-flac" + ] + pass + +class Document(AbstractFile): + allowed_mimetypes = [ + "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", # .docx + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", # .xlsx + "application/vnd.ms-powerpoint", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", # .pptx + "application/rtf", + "text/plain", + "text/csv" + ] + pass + +class Archive(AbstractFile): + allowed_mimetypes = [ + "application/zip", + "application/x-tar", + "application/x-gzip", + "application/x-bzip2", + "application/x-7z-compressed", + "application/x-rar-compressed" + ] + +class Text(AbstractFile): + allowed_mimetypes = [ + "text/plain", # .txt, .log + ] + pass + + +class Code(AbstractFile): + allowed_mimetypes = [ + "text/x-python", # .py + "text/x-shellscript", # .sh, .bash + "text/x-csrc", # .c + "text/x-c++src", # .cpp, .cc + "text/x-java-source", # .java + "text/x-go", # .go + "text/x-rustsrc", # .rs + "text/x-sql", # .sql + "text/x-markdown", # .md + "text/markdown", # .md (alternative) + "text/x-makefile", # Makefile + "text/x-php", # .php + "application/javascript", # .js (modern) + "text/javascript", # .js (legacy) + "text/css", # .css + "application/x-perl", # .pl + "application/x-ruby", # .rb + "application/x-lua", # .lua + ] + + +class StructuredData(AbstractFile): + allowed_mimetypes = [ + "application/json", # .json + "application/xml", # .xml + "text/xml", # .xml alternative + "text/csv", # .csv + "text/tab-separated-values", # .tsv + "application/x-yaml", # .yaml (rare) + "text/x-yaml", # .yaml, .yml (le plus courant) + "application/vnd.ms-excel", # .xls (Excel legacy) + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", # .xlsx + "application/x-hdf", # .hdf, .h5 + "application/x-parquet", # .parquet + "application/x-ndjson", # .ndjson (newline-delimited JSON) + "application/x-netcdf", # .nc + ] + +class Other(AbstractFile): + allowed_extensions = [] + pass diff --git a/app/upload/templates/upload/layout.html b/app/upload/templates/upload/layout.html new file mode 100644 index 0000000..63913c1 --- /dev/null +++ b/app/upload/templates/upload/layout.html @@ -0,0 +1 @@ +{% extends "base.html" %} \ No newline at end of file diff --git a/app/upload/templates/upload/upload_form.html b/app/upload/templates/upload/upload_form.html new file mode 100644 index 0000000..c964fdf --- /dev/null +++ b/app/upload/templates/upload/upload_form.html @@ -0,0 +1,2 @@ +{% extends "upload/layout.html" %} + diff --git a/app/upload/tests.py b/app/upload/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/app/upload/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/app/upload/utils.py b/app/upload/utils.py new file mode 100644 index 0000000..50f2d56 --- /dev/null +++ b/app/upload/utils.py @@ -0,0 +1,26 @@ +import magic +from upload.models import AbstractFile +from functools import lru_cache + +@lru_cache(maxsize=128) +def get_mimetype_class_map(): + mapping = {} + for cls in AbstractFile.__subclasses__(): + for mt in getattr(cls, 'allowed_mimetypes', []): + mapping[mt] = cls + return mapping + + +def get_mimetype(file_obj): + mimetype = magic.from_buffer(file_obj.read(2048), mime=True) + file_obj.seek(0) + return mimetype + + +def get_upload_class_for_mimetype(mimetype): + mimetype = mimetype.lower().replace('x-', '') # normalisation légère + return get_mimetype_class_map().get(mimetype) + + +def create_upload(request, file_obj): + pass diff --git a/app/upload/views.py b/app/upload/views.py new file mode 100644 index 0000000..627b90c --- /dev/null +++ b/app/upload/views.py @@ -0,0 +1,25 @@ +from django.shortcuts import render, redirect +from .forms import UploadImageForm +from .models import Upload +from django.contrib.contenttypes.models import ContentType + + +def upload_image_view(request): + if request.method == 'POST': + form = UploadImageForm(request.POST, request.FILES) + if form.is_valid(): + image = form.save() + + upload = Upload.objects.create( + filename=image.file.name, + content_type=ContentType.objects.get_for_model(image), + object_id=image.id, + user=request.user if request.user.is_authenticated else None + ) + + return redirect('upload_success', slug=upload.slug) + + else: + form = UploadImageForm() + + return render(request, 'upload/upload_form.html', {'form': form}) \ 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..8c38f3f --- /dev/null +++ b/app/user/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/app/user/api/registry.py b/app/user/api/registry.py new file mode 100644 index 0000000..78b6e28 --- /dev/null +++ b/app/user/api/registry.py @@ -0,0 +1,6 @@ +from api.utils import register_in_app +from .viewsets import UserViewSet + + +def register_viewsets(router): + register_in_app(router, 'users', UserViewSet) diff --git a/app/user/api/serializers.py b/app/user/api/serializers.py new file mode 100644 index 0000000..53bf4e2 --- /dev/null +++ b/app/user/api/serializers.py @@ -0,0 +1,11 @@ +from rest_framework import serializers + +from user.models import User + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ['id', 'username', 'email', 'first_name', 'last_name', 'max_upload_size'] + read_only_fields = ['id', 'max_upload_size'] + extra_kwargs = {'password': {'write_only': True}} diff --git a/app/user/api/viewsets.py b/app/user/api/viewsets.py new file mode 100644 index 0000000..2f2410e --- /dev/null +++ b/app/user/api/viewsets.py @@ -0,0 +1,17 @@ +from rest_framework import viewsets, permissions + +from user.models import User +from user.api.serializers import UserSerializer + + +class UserViewSet(viewsets.ModelViewSet): + queryset = User.objects.all() + serializer_class = UserSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + # Les utilisateurs normaux ne peuvent voir que leur propre profil + if not self.request.user.is_staff: + return User.objects.filter(id=self.request.user.id) + # Les administrateurs peuvent voir tous les utilisateurs + return User.objects.all() \ No newline at end of file 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..f1b2746 --- /dev/null +++ b/app/user/forms.py @@ -0,0 +1,17 @@ +from django import forms +from django.contrib.auth.forms import UserCreationForm +from .models import User + +class UserRegistrationForm(UserCreationForm): + email = forms.EmailField(required=True, help_text="Requis. Entrez une adresse email valide.") + + class Meta: + model = User + fields = ('username', 'email', 'password1', 'password2') + + def save(self, commit=True): + user = super().save(commit=False) + user.email = self.cleaned_data['email'] + if commit: + user.save() + return user \ No newline at end of file diff --git a/app/user/migrations/0001_initial.py b/app/user/migrations/0001_initial.py new file mode 100644 index 0000000..f6d8d8d --- /dev/null +++ b/app/user/migrations/0001_initial.py @@ -0,0 +1,45 @@ +# Generated by Django 5.2 on 2025-04-27 00:53 + +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)), + ('max_upload_size', models.PositiveBigIntegerField(default=1073741824)), + ('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.UsernameUserManager()), + ], + ), + ] diff --git a/app/user/migrations/0002_alter_user_max_upload_size.py b/app/user/migrations/0002_alter_user_max_upload_size.py new file mode 100644 index 0000000..89fc4c3 --- /dev/null +++ b/app/user/migrations/0002_alter_user_max_upload_size.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2 on 2025-05-04 14:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='max_upload_size', + field=models.PositiveBigIntegerField(default=104857600), + ), + ] 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..44284dc --- /dev/null +++ b/app/user/models.py @@ -0,0 +1,43 @@ +from django.db import models +from django.contrib.auth.models import AbstractUser, BaseUserManager + + +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) + + max_upload_size = models.PositiveBigIntegerField(default=100*1024*1024) # 100Mo + + objects = UsernameUserManager() + diff --git a/app/user/templates/user/login.html b/app/user/templates/user/login.html new file mode 100644 index 0000000..a1fd642 --- /dev/null +++ b/app/user/templates/user/login.html @@ -0,0 +1,135 @@ +{% extends 'base.html' %} + +{% block base_title %}Connexion{% endblock %} + +{% block content %} +
+

Connexion

+ +
+ {% csrf_token %} + + {% if form.non_field_errors %} +
+ {% for error in form.non_field_errors %} + {{ error }} + {% endfor %} +
+ {% endif %} + + {% for field in form %} +
+ + {{ field }} + {% if field.errors %} +
+ {% for error in field.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+ {% endfor %} + + +
+ + +
+ + +{% endblock %} \ No newline at end of file diff --git a/app/user/templates/user/password_change_done.html b/app/user/templates/user/password_change_done.html new file mode 100644 index 0000000..cfa8dc0 --- /dev/null +++ b/app/user/templates/user/password_change_done.html @@ -0,0 +1,79 @@ +{% extends 'base.html' %} + +{% block base_title %}Mot de passe modifié{% endblock %} + +{% block content %} +
+

Mot de passe modifié

+ +
+

Votre mot de passe a été modifié avec succès.

+
+ + +
+ + +{% endblock %} \ No newline at end of file diff --git a/app/user/templates/user/password_change_form.html b/app/user/templates/user/password_change_form.html new file mode 100644 index 0000000..517fd9d --- /dev/null +++ b/app/user/templates/user/password_change_form.html @@ -0,0 +1,125 @@ +{% extends 'base.html' %} + +{% block base_title %}Changer le mot de passe{% endblock %} + +{% block content %} +
+

Changer le mot de passe

+ +
+ {% csrf_token %} + + {% if form.non_field_errors %} +
+ {% for error in form.non_field_errors %} + {{ error }} + {% endfor %} +
+ {% endif %} + + {% for field in form %} +
+ + {{ field }} + {% if field.errors %} +
+ {% for error in field.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} + {% if field.help_text %} +
{{ field.help_text }}
+ {% endif %} +
+ {% endfor %} + + +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/app/user/templates/user/password_reset_complete.html b/app/user/templates/user/password_reset_complete.html new file mode 100644 index 0000000..bab700e --- /dev/null +++ b/app/user/templates/user/password_reset_complete.html @@ -0,0 +1,79 @@ +{% extends 'base.html' %} + +{% block base_title %}Mot de passe réinitialisé{% endblock %} + +{% block content %} +
+

Mot de passe réinitialisé

+ +
+

Votre mot de passe a été réinitialisé avec succès.

+
+ + +
+ + +{% endblock %} \ No newline at end of file diff --git a/app/user/templates/user/password_reset_confirm.html b/app/user/templates/user/password_reset_confirm.html new file mode 100644 index 0000000..998b29f --- /dev/null +++ b/app/user/templates/user/password_reset_confirm.html @@ -0,0 +1,149 @@ +{% extends 'base.html' %} + +{% block base_title %}Définir un nouveau mot de passe{% endblock %} + +{% block content %} +
+

Définir un nouveau mot de passe

+ + {% if validlink %} +
+ {% csrf_token %} + + {% if form.non_field_errors %} +
+ {% for error in form.non_field_errors %} + {{ error }} + {% endfor %} +
+ {% endif %} + + {% for field in form %} +
+ + {{ field }} + {% if field.errors %} +
+ {% for error in field.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} + {% if field.help_text %} +
{{ field.help_text }}
+ {% endif %} +
+ {% endfor %} + + +
+ {% else %} +
+

Le lien de réinitialisation du mot de passe n'est pas valide, probablement parce qu'il a déjà été utilisé.

+

Veuillez demander une nouvelle réinitialisation de mot de passe.

+
+ + + {% endif %} +
+ + +{% endblock %} \ No newline at end of file diff --git a/app/user/templates/user/password_reset_done.html b/app/user/templates/user/password_reset_done.html new file mode 100644 index 0000000..88ffd9f --- /dev/null +++ b/app/user/templates/user/password_reset_done.html @@ -0,0 +1,88 @@ +{% extends 'base.html' %} + +{% block base_title %}Email de réinitialisation envoyé{% endblock %} + +{% block content %} +
+

Email de réinitialisation envoyé

+ +
+

Nous vous avons envoyé par email les instructions pour réinitialiser votre mot de passe.

+

Si vous ne recevez pas d'email, vérifiez que vous avez saisi l'adresse avec laquelle vous vous êtes inscrit, et vérifiez votre dossier de spam.

+
+ + +
+ + +{% endblock %} \ No newline at end of file diff --git a/app/user/templates/user/password_reset_email.html b/app/user/templates/user/password_reset_email.html new file mode 100644 index 0000000..2c140c8 --- /dev/null +++ b/app/user/templates/user/password_reset_email.html @@ -0,0 +1,14 @@ +{% autoescape off %} +Bonjour, + +Vous recevez cet email car vous avez demandé la réinitialisation du mot de passe de votre compte sur {{ site_name }}. + +Veuillez suivre le lien ci-dessous pour définir un nouveau mot de passe : +{{ protocol }}://{{ domain }}{% url 'user:password_reset_confirm' uidb64=uid token=token %} + +Votre nom d'utilisateur, au cas où vous l'auriez oublié : {{ user.get_username }} + +Merci d'utiliser notre site ! + +L'équipe {{ site_name }} +{% endautoescape %} \ No newline at end of file diff --git a/app/user/templates/user/password_reset_form.html b/app/user/templates/user/password_reset_form.html new file mode 100644 index 0000000..eb535e5 --- /dev/null +++ b/app/user/templates/user/password_reset_form.html @@ -0,0 +1,151 @@ +{% extends 'base.html' %} + +{% block base_title %}Réinitialisation du mot de passe{% endblock %} + +{% block content %} +
+

Réinitialisation du mot de passe

+ +

Entrez votre adresse e-mail et nous vous enverrons un lien pour réinitialiser votre mot de passe.

+ +
+ {% csrf_token %} + + {% if form.non_field_errors %} +
+ {% for error in form.non_field_errors %} + {{ error }} + {% endfor %} +
+ {% endif %} + + {% for field in form %} +
+ + {{ field }} + {% if field.errors %} +
+ {% for error in field.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} + {% if field.help_text %} +
{{ field.help_text }}
+ {% endif %} +
+ {% endfor %} + + +
+ + +
+ + +{% endblock %} \ No newline at end of file diff --git a/app/user/templates/user/password_reset_subject.txt b/app/user/templates/user/password_reset_subject.txt new file mode 100644 index 0000000..31f2961 --- /dev/null +++ b/app/user/templates/user/password_reset_subject.txt @@ -0,0 +1 @@ +Réinitialisation de votre mot de passe sur {{ site_name }} \ No newline at end of file diff --git a/app/user/templates/user/register.html b/app/user/templates/user/register.html new file mode 100644 index 0000000..193c63e --- /dev/null +++ b/app/user/templates/user/register.html @@ -0,0 +1,143 @@ +{% extends 'base.html' %} + +{% block base_title %}Inscription{% endblock %} + +{% block content %} +
+

Inscription

+ +
+ {% csrf_token %} + + {% if form.non_field_errors %} +
+ {% for error in form.non_field_errors %} + {{ error }} + {% endfor %} +
+ {% endif %} + + {% for field in form %} +
+ + {{ field }} + {% if field.errors %} +
+ {% for error in field.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} + {% if field.help_text %} +
{{ field.help_text }}
+ {% endif %} +
+ {% endfor %} + + +
+ + +
+ + +{% endblock %} \ No newline at end of file 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/urls.py b/app/user/urls.py new file mode 100644 index 0000000..467176c --- /dev/null +++ b/app/user/urls.py @@ -0,0 +1,41 @@ +from django.urls import path +from django.contrib.auth import views as auth_views +from . import views + +app_name = 'user' + +urlpatterns = [ + # Registration + path('register/', views.register, name='register'), + + # Login and Logout + path('login/', auth_views.LoginView.as_view(template_name='user/login.html'), name='login'), + path('logout/', auth_views.LogoutView.as_view(), name='logout'), + + # Change password + path('password_change/', auth_views.PasswordChangeView.as_view( + template_name='user/password_change_form.html', + success_url='/user/password_change/done/' + ), name='password_change'), + path('password_change/done/', auth_views.PasswordChangeDoneView.as_view( + template_name='user/password_change_done.html' + ), name='password_change_done'), + + # Reset password + path('password_reset/', auth_views.PasswordResetView.as_view( + template_name='user/password_reset_form.html', + email_template_name='user/password_reset_email.html', + subject_template_name='user/password_reset_subject.txt', + success_url='/user/password_reset/done/' + ), name='password_reset'), + path('password_reset/done/', auth_views.PasswordResetDoneView.as_view( + template_name='user/password_reset_done.html' + ), name='password_reset_done'), + path('reset///', auth_views.PasswordResetConfirmView.as_view( + template_name='user/password_reset_confirm.html', + success_url='/user/reset/done/' + ), name='password_reset_confirm'), + path('reset/done/', auth_views.PasswordResetCompleteView.as_view( + template_name='user/password_reset_complete.html' + ), name='password_reset_complete'), +] \ No newline at end of file diff --git a/app/user/views.py b/app/user/views.py new file mode 100644 index 0000000..0605d04 --- /dev/null +++ b/app/user/views.py @@ -0,0 +1,18 @@ +from django.shortcuts import render, redirect +from django.contrib.auth import login +from django.contrib import messages +from .models import User +from .forms import UserRegistrationForm + +def register(request): + if request.method == 'POST': + form = UserRegistrationForm(request.POST) + if form.is_valid(): + user = form.save() + login(request, user) + messages.success(request, "Inscription réussie ! Vous êtes maintenant connecté.") + return redirect('/') + else: + form = UserRegistrationForm() + + return render(request, 'user/register.html', {'form': form}) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..d995c3a --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,15 @@ +services: + web: + restart: "no" + ports: + - "${LISTEN_PORT:-8000}:80" + volumes: + - ./nginx/dev.nginx:/etc/nginx/nginx.conf:ro + + app: + volumes: + - ./app:/app + restart: "no" + command: > + sh -c "python manage.py migrate --noinput && + ./dev_run.sh" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8883f1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,60 @@ +networks: + web: + external: true + +volumes: + static_volume: + +services: + web: + image: nginx:alpine + restart: unless-stopped + labels: + - "traefik.enable=true" + - "traefik.docker.network=web" + # Routeur HTTP (port 80) pour la redirection vers HTTPS + - "traefik.http.routers.web-http.rule=${TRAEFIK_HOST_RULE}" + - "traefik.http.routers.web-http.entrypoints=web" + - "traefik.http.routers.web-http.middlewares=redirect-to-https" + + # Middleware de redirection + - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" + + # Routeur HTTPS + - "traefik.http.routers.web.rule=${TRAEFIK_HOST_RULE}" + - "traefik.http.routers.web.entrypoints=websecure" + - "traefik.http.routers.web.tls.certresolver=le" + + # Port du container cible (le port d'écoute de Nginx à l’intérieur du conteneur) + - "traefik.http.services.web.loadbalancer.server.port=80" + volumes: + - ./nginx/default.nginx:/etc/nginx/nginx.conf:ro + - static_volume:/static:ro + - ./app/media:/media:ro + depends_on: + - app + + app: + build: + context: ./app + dockerfile: "Dockerfile" + args: + puid: ${USER_ID} + pgid: ${GROUP_ID} + debug: ${DEBUG:-false} + restart: unless-stopped + healthcheck: + test: [ "CMD", "curl", "-f", "http://127.0.0.1:8000/health/" ] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + env_file: + - .env + volumes: + - ./media:/app/media + - static_volume:/app/static_collected + - /app/frontend/node_modules + command: > + sh -c "python manage.py migrate --noinput && + hypercorn app.asgi:application -c hypercorn.toml" \ No newline at end of file diff --git a/nginx/default.nginx b/nginx/default.nginx new file mode 100644 index 0000000..1c7d1b0 --- /dev/null +++ b/nginx/default.nginx @@ -0,0 +1,29 @@ +events { + worker_connections 1024; +} + +http { + client_max_body_size 100M; + server { + listen 80; + + location /static/ { + alias /static/; + autoindex on; + } + + location /serve_media/ { + alias /media/; + autoindex on; + } + + location / { + proxy_redirect off; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass http://app:8000; + } + } +} diff --git a/nginx/dev.nginx b/nginx/dev.nginx new file mode 100644 index 0000000..847797c --- /dev/null +++ b/nginx/dev.nginx @@ -0,0 +1,25 @@ +events { + worker_connections 1024; +} + + +http { + client_max_body_size 100M; + server { + listen 80; + + location /serve_media/ { + alias /media/; + autoindex on; + } + + location / { + proxy_redirect off; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass http://app:8000; + } + } +} \ No newline at end of file