feat: publish CodexPi contest snapshot
Some checks are pending
Qualité / tests (push) Waiting to run
Some checks are pending
Qualité / tests (push) Waiting to run
This commit is contained in:
1
django_ui/__init__.py
Normal file
1
django_ui/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
6
django_ui/asgi.py
Normal file
6
django_ui/asgi.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_ui.settings")
|
||||
application = get_asgi_application()
|
||||
1
django_ui/identity/__init__.py
Normal file
1
django_ui/identity/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Application permissions attached to the existing Django identity."""
|
||||
37
django_ui/identity/migrations/0001_initial.py
Normal file
37
django_ui/identity/migrations/0001_initial.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
APPLICATIONS = (
|
||||
"auto-care", "autonomous-dev-studio", "codex-commander", "knowledge-graph",
|
||||
"loutre-brain", "loutre-finance", "loutre-home", "loutre-os",
|
||||
"nasflix-ai-media-curator", "nasflix-media-health", "network-guardian",
|
||||
"pi-doctor", "print-pilot", "pwa-factory", "reverse-proxy-manager",
|
||||
"smart-inventory", "trip-organizer",
|
||||
)
|
||||
ROLES = ("reader", "operator", "admin")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ApplicationGrant",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("application", models.CharField(max_length=64, choices=[(name, name) for name in APPLICATIONS])),
|
||||
("role", models.CharField(max_length=16, choices=[(name, name) for name in ROLES])),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
"constraints": [
|
||||
models.UniqueConstraint(fields=("user", "application"), name="identity_user_application"),
|
||||
models.CheckConstraint(condition=models.Q(application__in=APPLICATIONS), name="identity_known_application"),
|
||||
models.CheckConstraint(condition=models.Q(role__in=ROLES), name="identity_known_role"),
|
||||
]
|
||||
},
|
||||
)
|
||||
]
|
||||
0
django_ui/identity/migrations/__init__.py
Normal file
0
django_ui/identity/migrations/__init__.py
Normal file
25
django_ui/identity/models.py
Normal file
25
django_ui/identity/models.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
APPLICATIONS = (
|
||||
"auto-care", "autonomous-dev-studio", "codex-commander", "knowledge-graph",
|
||||
"loutre-brain", "loutre-finance", "loutre-home", "loutre-os",
|
||||
"nasflix-ai-media-curator", "nasflix-media-health", "network-guardian",
|
||||
"pi-doctor", "print-pilot", "pwa-factory", "reverse-proxy-manager",
|
||||
"smart-inventory", "trip-organizer",
|
||||
)
|
||||
ROLES = ("reader", "operator", "admin")
|
||||
|
||||
|
||||
class ApplicationGrant(models.Model):
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
application = models.CharField(max_length=64, choices=[(name, name) for name in APPLICATIONS])
|
||||
role = models.CharField(max_length=16, choices=[(name, name) for name in ROLES])
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=("user", "application"), name="identity_user_application"),
|
||||
models.CheckConstraint(condition=models.Q(application__in=APPLICATIONS), name="identity_known_application"),
|
||||
models.CheckConstraint(condition=models.Q(role__in=ROLES), name="identity_known_role"),
|
||||
]
|
||||
104
django_ui/identity/views.py
Normal file
104
django_ui/identity/views.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""Same-origin identity contract in the existing Django service, deny by default."""
|
||||
from functools import wraps
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import DatabaseError
|
||||
from django.http import JsonResponse
|
||||
from django.views.decorators.http import require_GET, require_POST, require_http_methods
|
||||
|
||||
from django_ui.views import AUDIT, client_ip, json_body, security_headers
|
||||
from .models import APPLICATIONS, ROLES, ApplicationGrant
|
||||
|
||||
PERMISSIONS = {"reader": ("read",), "operator": ("read", "write"), "admin": ("read", "write", "admin")}
|
||||
|
||||
|
||||
def reply(body, status=200):
|
||||
response = security_headers(JsonResponse(body, status=status))
|
||||
response["Vary"] = "Cookie"
|
||||
return response
|
||||
|
||||
|
||||
def identity_request(view):
|
||||
@wraps(view)
|
||||
def guarded(request, application, **kwargs):
|
||||
try:
|
||||
if not request.user.is_authenticated:
|
||||
return reply({"detail": "Authentification requise"}, 401)
|
||||
if application not in APPLICATIONS:
|
||||
return reply({"detail": "Application inconnue"}, 404)
|
||||
if request.headers.get("Sec-Fetch-Site") == "cross-site":
|
||||
return reply({"detail": "Requête intersite refusée"}, 403)
|
||||
if request.method not in {"GET", "HEAD", "OPTIONS"} and (
|
||||
request.content_type != "application/json" or len(request.body) > 4096
|
||||
):
|
||||
return reply({"detail": "Corps JSON limité requis"}, 400)
|
||||
return view(request, application, **kwargs)
|
||||
except DatabaseError:
|
||||
return reply({"detail": "Identité indisponible ; accès refusé"}, 503)
|
||||
|
||||
return guarded
|
||||
|
||||
|
||||
def principal(request, application):
|
||||
grant = ApplicationGrant.objects.filter(user=request.user, application=application).first()
|
||||
if not grant or grant.role not in PERMISSIONS:
|
||||
return None
|
||||
return {
|
||||
"issuer": "django-codexpi",
|
||||
"subject": str(request.user.pk),
|
||||
"username": request.user.get_username(),
|
||||
"application": application,
|
||||
"role": grant.role,
|
||||
"permissions": list(PERMISSIONS[grant.role]),
|
||||
}
|
||||
|
||||
|
||||
@require_GET
|
||||
@identity_request
|
||||
def application_identity(request, application):
|
||||
identity = principal(request, application)
|
||||
return reply(identity) if identity else reply({"detail": "Accès à cette application refusé"}, 403)
|
||||
|
||||
|
||||
@require_POST
|
||||
@identity_request
|
||||
def authorize(request, application):
|
||||
body = json_body(request)
|
||||
if set(body) != {"permission"} or body["permission"] not in ("read", "write", "admin"):
|
||||
return reply({"detail": "Permission invalide"}, 400)
|
||||
identity = principal(request, application)
|
||||
if not identity or body["permission"] not in identity["permissions"]:
|
||||
return reply({"detail": "Permission refusée"}, 403)
|
||||
return reply({**identity, "authorized_permission": body["permission"]})
|
||||
|
||||
|
||||
@require_http_methods(["GET", "PUT", "DELETE"])
|
||||
@identity_request
|
||||
def application_grant(request, application, user_id):
|
||||
if not (request.user.is_staff or request.user.is_superuser):
|
||||
return reply({"detail": "Administration des identités requise"}, 403)
|
||||
target = get_user_model().objects.filter(pk=user_id).first()
|
||||
if not target:
|
||||
return reply({"detail": "Compte introuvable"}, 404)
|
||||
grants = ApplicationGrant.objects.filter(user=target, application=application)
|
||||
if request.method == "GET":
|
||||
grant = grants.first()
|
||||
return reply({"subject": str(target.pk), "application": application, "role": grant.role if grant else None})
|
||||
if request.method == "DELETE":
|
||||
grants.delete()
|
||||
role = None
|
||||
else:
|
||||
body = json_body(request)
|
||||
if set(body) != {"role"} or body["role"] not in ROLES:
|
||||
return reply({"detail": "Rôle invalide"}, 400)
|
||||
role = body["role"]
|
||||
ApplicationGrant.objects.update_or_create(user=target, application=application, defaults={"role": role})
|
||||
AUDIT.log(
|
||||
"identity.grant_revoked" if role is None else "identity.grant_updated",
|
||||
client_ip(request),
|
||||
username=request.user.get_username(),
|
||||
project_id=application,
|
||||
outcome="allowed",
|
||||
reason=f"target_user={target.pk};role={role or 'none'}",
|
||||
)
|
||||
return reply({"subject": str(target.pk), "application": application, "role": role})
|
||||
98
django_ui/settings.py
Normal file
98
django_ui/settings.py
Normal file
@@ -0,0 +1,98 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parents[1]
|
||||
DATA_DIR = Path(os.getenv("CODEXPI_DATA_DIR", BASE_DIR / "data"))
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def env_bool(name: str, default: bool) -> bool:
|
||||
value = os.getenv(name)
|
||||
return default if value is None else value.lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def secret_key() -> str:
|
||||
configured_file = os.getenv("CODEXPI_DJANGO_SECRET_KEY_FILE")
|
||||
if configured_file:
|
||||
value = Path(configured_file).read_text(encoding="utf-8").strip()
|
||||
if len(value) < 32:
|
||||
raise RuntimeError("CODEXPI_DJANGO_SECRET_KEY_FILE doit contenir un secret d’au moins 32 caractères")
|
||||
return value
|
||||
configured = os.getenv("CODEXPI_DJANGO_SECRET_KEY")
|
||||
if configured:
|
||||
if len(configured) < 32:
|
||||
raise RuntimeError("CODEXPI_DJANGO_SECRET_KEY doit contenir au moins 32 caractères")
|
||||
return configured
|
||||
path = DATA_DIR / "django_secret_key"
|
||||
if not path.exists():
|
||||
try:
|
||||
descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
||||
with os.fdopen(descriptor, "w", encoding="utf-8") as target:
|
||||
target.write(secrets.token_urlsafe(64))
|
||||
except FileExistsError:
|
||||
pass
|
||||
return path.read_text(encoding="utf-8").strip()
|
||||
|
||||
|
||||
SECRET_KEY = secret_key()
|
||||
DEBUG = False
|
||||
ALLOWED_HOSTS = [item.strip() for item in os.getenv("CODEXPI_DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1,.ts.net,nginx").split(",") if item.strip()]
|
||||
CSRF_TRUSTED_ORIGINS = [item.strip() for item in os.getenv("CODEXPI_TRUSTED_ORIGINS", "https://*.ts.net").split(",") if item.strip()]
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django_ui.identity",
|
||||
]
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
ROOT_URLCONF = "django_ui.urls"
|
||||
ASGI_APPLICATION = "django_ui.asgi.application"
|
||||
WSGI_APPLICATION = "django_ui.wsgi.application"
|
||||
|
||||
DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": DATA_DIR / "django.sqlite3", "OPTIONS": {"timeout": 30}}}
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
USE_TZ = True
|
||||
TIME_ZONE = "Europe/Paris"
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", "OPTIONS": {"min_length": 12}},
|
||||
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
||||
]
|
||||
PASSWORD_HASHERS = [
|
||||
"django.contrib.auth.hashers.Argon2PasswordHasher",
|
||||
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
|
||||
]
|
||||
|
||||
SESSION_ENGINE = "django.contrib.sessions.backends.db"
|
||||
SESSION_COOKIE_NAME = os.getenv("CODEXPI_COOKIE_NAME", "codexpi_session")
|
||||
SESSION_COOKIE_AGE = int(os.getenv("CODEXPI_SESSION_HOURS", "12")) * 3600
|
||||
SESSION_COOKIE_SECURE = env_bool("CODEXPI_COOKIE_SECURE", True)
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
SESSION_COOKIE_SAMESITE = "Strict"
|
||||
SESSION_SAVE_EVERY_REQUEST = False
|
||||
CSRF_COOKIE_SECURE = SESSION_COOKIE_SECURE
|
||||
CSRF_COOKIE_HTTPONLY = True
|
||||
CSRF_COOKIE_SAMESITE = "Strict"
|
||||
CSRF_HEADER_NAME = "HTTP_X_CSRF_TOKEN"
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
USE_X_FORWARDED_HOST = True
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||
SECURE_REFERRER_POLICY = "no-referrer"
|
||||
SECURE_HSTS_SECONDS = 31_536_000
|
||||
X_FRAME_OPTIONS = "DENY"
|
||||
|
||||
CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache", "LOCATION": "codexpi-login-limiter"}}
|
||||
21
django_ui/urls.py
Normal file
21
django_ui/urls.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from django.urls import path, re_path
|
||||
|
||||
from . import views
|
||||
from .identity import views as identity
|
||||
|
||||
urlpatterns = [
|
||||
path("api/bootstrap", views.bootstrap, name="bootstrap"),
|
||||
path("api/auth/login", views.login_api, name="login"),
|
||||
path("api/auth/setup", views.setup_api, name="setup"),
|
||||
path("api/auth/logout", views.logout_api, name="logout"),
|
||||
path("api/auth/me", views.me_api, name="me"),
|
||||
path("api/auth/applications/<slug:application>", identity.application_identity, name="application-identity"),
|
||||
path("api/auth/applications/<slug:application>/authorize", identity.authorize, name="application-authorize"),
|
||||
path("api/auth/application-grants/<slug:application>/<int:user_id>", identity.application_grant, name="application-grant"),
|
||||
path("manifest.webmanifest", views.manifest, name="manifest"),
|
||||
path("version.json", views.frontend_version, name="frontend-version"),
|
||||
path("sw.js", views.service_worker, name="service-worker"),
|
||||
re_path(r"^assets/(?P<path>.+)$", views.asset, name="asset"),
|
||||
re_path(r"^icons/(?P<path>.+)$", views.icon, name="icon"),
|
||||
re_path(r"^.*$", views.index, name="index"),
|
||||
]
|
||||
192
django_ui/views.py
Normal file
192
django_ui/views.py
Normal file
@@ -0,0 +1,192 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import hashlib
|
||||
import time
|
||||
from collections import deque
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate, get_user_model, login, logout
|
||||
from django.core.cache import cache
|
||||
from django.http import FileResponse, Http404, JsonResponse
|
||||
from django.middleware.csrf import get_token, rotate_token
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from django.views.decorators.http import require_GET, require_POST
|
||||
|
||||
from codexpi.security_audit import SecurityAuditLogger
|
||||
|
||||
|
||||
FRONTEND = settings.BASE_DIR / "frontend" / "dist"
|
||||
AUDIT = SecurityAuditLogger(settings.DATA_DIR / "security.log")
|
||||
WINDOW_SECONDS = 300
|
||||
MAX_ATTEMPTS = 8
|
||||
|
||||
|
||||
def client_ip(request) -> str:
|
||||
forwarded = request.META.get("HTTP_X_FORWARDED_FOR", "")
|
||||
return (forwarded.split(",", 1)[0].strip() if forwarded else "") or request.META.get("HTTP_X_REAL_IP") or request.META.get("REMOTE_ADDR") or "unknown"
|
||||
|
||||
|
||||
def audit_session_id(session_key: str | None) -> str | None:
|
||||
"""Return a non-reversible correlation id, never the session cookie value."""
|
||||
if not session_key:
|
||||
return None
|
||||
return f"sha256:{hashlib.sha256(session_key.encode()).hexdigest()[:20]}"
|
||||
|
||||
|
||||
def security_headers(response):
|
||||
response["Content-Security-Policy"] = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' ws: wss:; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"
|
||||
response["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()"
|
||||
response["Cache-Control"] = "no-store"
|
||||
return response
|
||||
|
||||
|
||||
def json_body(request) -> dict:
|
||||
try:
|
||||
value = json.loads(request.body or b"{}")
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
value = {}
|
||||
return value if isinstance(value, dict) else {}
|
||||
|
||||
|
||||
def rate_state(key: str) -> deque[float]:
|
||||
now = time.monotonic()
|
||||
values = deque(cache.get(key, []))
|
||||
while values and values[0] < now - WINDOW_SECONDS:
|
||||
values.popleft()
|
||||
return values
|
||||
|
||||
|
||||
@require_GET
|
||||
@ensure_csrf_cookie
|
||||
def bootstrap(request):
|
||||
token = get_token(request)
|
||||
return security_headers(JsonResponse({"setup_required": not get_user_model().objects.exists(), "cookie_secure": settings.SESSION_COOKIE_SECURE, "csrf_token": token}))
|
||||
|
||||
|
||||
@require_POST
|
||||
def login_api(request):
|
||||
source = client_ip(request)
|
||||
body = json_body(request)
|
||||
username = str(body.get("username", ""))[:150]
|
||||
password = str(body.get("password", ""))[:512]
|
||||
ip_key = f"login-ip:{source}"
|
||||
user_key = f"login-user:{username.lower()}"
|
||||
events = rate_state(ip_key)
|
||||
if len(events) >= MAX_ATTEMPTS or cache.get(f"locked:{username.lower()}"):
|
||||
AUDIT.log("auth.login_rate_limited", source, username=username, outcome="denied", reason="rate_limit_or_lock")
|
||||
return security_headers(JsonResponse({"detail": "Trop de tentatives. Réessayez plus tard."}, status=429))
|
||||
user = authenticate(request, username=username, password=password)
|
||||
if user is None or not user.is_active:
|
||||
now = time.monotonic()
|
||||
events.append(now)
|
||||
cache.set(ip_key, list(events), WINDOW_SECONDS)
|
||||
failures = int(cache.get(user_key, 0)) + 1
|
||||
cache.set(user_key, failures, 900)
|
||||
if failures >= 5:
|
||||
cache.set(f"locked:{username.lower()}", True, 900)
|
||||
time.sleep(min(0.25 * (2 ** max(0, failures - 1)), 4.0))
|
||||
AUDIT.log("auth.login_failed", source, username=username, outcome="denied", reason="invalid_or_locked")
|
||||
return security_headers(JsonResponse({"detail": "Identifiants invalides ou compte temporairement verrouillé"}, status=401))
|
||||
cache.delete(ip_key)
|
||||
cache.delete(user_key)
|
||||
cache.delete(f"locked:{username.lower()}")
|
||||
login(request, user)
|
||||
rotate_token(request)
|
||||
csrf_token = get_token(request)
|
||||
request.session["codexpi_username"] = user.get_username()
|
||||
request.session["codexpi_is_admin"] = bool(user.is_staff or user.is_superuser)
|
||||
request.session["codexpi_api_csrf"] = csrf_token
|
||||
request.session.set_expiry(settings.SESSION_COOKIE_AGE)
|
||||
request.session.save()
|
||||
correlation_id = audit_session_id(request.session.session_key)
|
||||
AUDIT.log("auth.login_succeeded", source, username=user.get_username(), session_id=correlation_id, outcome="allowed")
|
||||
AUDIT.log("session.created", source, username=user.get_username(), session_id=correlation_id, outcome="created")
|
||||
return security_headers(JsonResponse({"username": user.get_username(), "is_admin": bool(user.is_staff or user.is_superuser), "csrf_token": csrf_token, "expires_at": request.session.get_expiry_date().isoformat()}))
|
||||
|
||||
|
||||
@require_POST
|
||||
def setup_api(request):
|
||||
User = get_user_model()
|
||||
if User.objects.exists():
|
||||
return security_headers(JsonResponse({"detail": "La configuration initiale est déjà terminée"}, status=409))
|
||||
body = json_body(request)
|
||||
username = str(body.get("username", "")).strip()
|
||||
password = str(body.get("password", ""))
|
||||
if not username or len(username) < 3 or len(password) < 12:
|
||||
return security_headers(JsonResponse({"detail": "Utilisez un nom valide et un mot de passe d’au moins 12 caractères"}, status=400))
|
||||
user = User.objects.create_superuser(username=username, password=password)
|
||||
login(request, user)
|
||||
rotate_token(request)
|
||||
csrf_token = get_token(request)
|
||||
request.session["codexpi_username"] = user.get_username()
|
||||
request.session["codexpi_is_admin"] = True
|
||||
request.session["codexpi_api_csrf"] = csrf_token
|
||||
request.session.set_expiry(settings.SESSION_COOKIE_AGE)
|
||||
request.session.save()
|
||||
AUDIT.log("auth.initial_admin_created", client_ip(request), username=user.get_username(), outcome="created")
|
||||
return security_headers(JsonResponse({"username": user.get_username(), "is_admin": True, "csrf_token": csrf_token}, status=201))
|
||||
|
||||
|
||||
@require_POST
|
||||
def logout_api(request):
|
||||
if not request.user.is_authenticated:
|
||||
return security_headers(JsonResponse({"detail": "Authentification requise"}, status=401))
|
||||
source = client_ip(request)
|
||||
username = request.user.get_username()
|
||||
session_id = audit_session_id(request.session.session_key)
|
||||
logout(request)
|
||||
AUDIT.log("session.deleted", source, username=username, session_id=session_id, outcome="revoked")
|
||||
return security_headers(JsonResponse({"ok": True}))
|
||||
|
||||
|
||||
@require_GET
|
||||
def me_api(request):
|
||||
if not request.user.is_authenticated:
|
||||
return security_headers(JsonResponse({"detail": "Authentification requise"}, status=401))
|
||||
token = request.session.get("codexpi_api_csrf")
|
||||
if not token:
|
||||
token = get_token(request)
|
||||
request.session["codexpi_api_csrf"] = token
|
||||
return security_headers(JsonResponse({"username": request.user.get_username(), "is_admin": bool(request.user.is_staff or request.user.is_superuser), "csrf_token": token, "expires_at": request.session.get_expiry_date().isoformat()}))
|
||||
|
||||
|
||||
def safe_file(root: Path, relative: str) -> Path:
|
||||
candidate = (root / relative).resolve()
|
||||
resolved = root.resolve()
|
||||
if candidate != resolved and resolved not in candidate.parents:
|
||||
raise Http404
|
||||
if not candidate.is_file():
|
||||
raise Http404
|
||||
return candidate
|
||||
|
||||
|
||||
def index(_request):
|
||||
response = FileResponse(safe_file(FRONTEND, "index.html").open("rb"), content_type="text/html")
|
||||
return security_headers(response)
|
||||
|
||||
|
||||
def asset(_request, path: str):
|
||||
return FileResponse(safe_file(FRONTEND / "assets", path).open("rb"))
|
||||
|
||||
|
||||
def icon(_request, path: str):
|
||||
return FileResponse(safe_file(FRONTEND / "icons", path).open("rb"))
|
||||
|
||||
|
||||
def manifest(_request):
|
||||
return FileResponse(safe_file(FRONTEND, "manifest.webmanifest").open("rb"), content_type="application/manifest+json")
|
||||
|
||||
|
||||
def frontend_version(_request):
|
||||
response = FileResponse(safe_file(FRONTEND, "version.json").open("rb"), content_type="application/json")
|
||||
response["Cache-Control"] = "no-store"
|
||||
return response
|
||||
|
||||
|
||||
def service_worker(_request):
|
||||
response = FileResponse(safe_file(FRONTEND, "sw.js").open("rb"), content_type="application/javascript")
|
||||
response["Service-Worker-Allowed"] = "/"
|
||||
response["Cache-Control"] = "no-cache"
|
||||
return response
|
||||
6
django_ui/wsgi.py
Normal file
6
django_ui/wsgi.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_ui.settings")
|
||||
application = get_wsgi_application()
|
||||
Reference in New Issue
Block a user