Create Basic Django Framework with API, SQLite, PostgreSQL, MySQL and SSO with OIDC

This commit is contained in:
= 2025-09-05 15:32:33 +02:00
parent 38bb91285d
commit 37168500d1
15 changed files with 196 additions and 74 deletions

23
.env-example Normal file
View file

@ -0,0 +1,23 @@
# Django core
DEBUG=True
SECRET_KEY=dev-secret-key
DJANGO_SETTINGS_MODULE=config.settings
# Superuser (optional)
DJANGO_SUPERUSER_USERNAME=admin
DJANGO_SUPERUSER_EMAIL=admin@example.com
DJANGO_SUPERUSER_PASSWORD=changeme
# OIDC / SSO
SSO_ENABLED=False
OIDC_RP_CLIENT_ID=django-app
OIDC_RP_CLIENT_SECRET=changeme
OIDC_OP_DISCOVERY_ENDPOINT=https://auth.example.com/.well-known/openid-configuration
# Database settings (Default: SQLite)
DB_ENGINE=sqlite
DB_NAME=db.sqlite3
DB_USER=
DB_PASSWORD=
DB_HOST=
DB_PORT=

3
.gitignore vendored
View file

@ -1 +1,4 @@
.venv .venv
.startserver.sh
.env
sb.sqlite3

View file

@ -1,8 +1,20 @@
from rest_framework.decorators import api_view, permission_classes from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
@api_view(["GET"]) @api_view(["GET"])
@permission_classes([AllowAny]) # erstmal offen, später absichern @permission_classes([AllowAny]) # This endpoint is deliberately open to everyone
def ping(request): def ping(request):
return Response({"status": "ok"}) return Response({"status": "ok"})
@api_view(["GET"])
@permission_classes([IsAuthenticated]) # Requires either session (OIDC) or basic authentication
def secure_ping(request):
return Response({
"status": "ok",
"user": request.user.username, # The authenticated username
# Indicates whether the request was authenticated via session (OIDC) or via basic auth
"auth_via": request.auth.__class__.__name__ if request.auth else "session/basic"
})

View file

@ -1,42 +1,47 @@
import environ
import os import os
from pathlib import Path from pathlib import Path
import environ
# Build paths inside the project like this: BASE_DIR / 'subdir'. # ---------------------------------------------------------------------------
# Base project paths
# ---------------------------------------------------------------------------
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
# Load Env-Vars # ---------------------------------------------------------------------------
# Environment configuration
# ---------------------------------------------------------------------------
env = environ.Env() env = environ.Env()
environ.Env.read_env(os.path.join(BASE_DIR, ".env")) environ.Env.read_env(os.path.join(BASE_DIR, ".env"))
# Quick-start development settings - unsuitable for production # ---------------------------------------------------------------------------
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ # Security settings
# ---------------------------------------------------------------------------
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env( SECRET_KEY = env(
"SECRET_KEY", "SECRET_KEY",
default="django-insecure-lbfv*h@=mjj#xq^!k@-5f2oiq@u6ms9t6=3&nr+!#itih%jh^l" default="django-insecure-lbfv*h@=mjj#xq^!k@-5f2oiq@u6ms9t6=3&nr+!#itih%jh^l" # fallback only for development
) )
DEBUG = env.bool("DEBUG", default=False)
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env.bool("DEBUG", default=True)
ALLOWED_HOSTS = ["localhost", "127.0.0.1"] ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
# ---------------------------------------------------------------------------
# Application definition # Installed apps
# ---------------------------------------------------------------------------
INSTALLED_APPS = [ INSTALLED_APPS = [
# Django built-in apps
"django.contrib.admin", "django.contrib.admin",
"django.contrib.auth", "django.contrib.auth",
"django.contrib.contenttypes", "django.contrib.contenttypes",
"django.contrib.sessions", "django.contrib.sessions",
"django.contrib.messages", "django.contrib.messages",
"django.contrib.staticfiles", "django.contrib.staticfiles",
# Third-party apps
"rest_framework", "rest_framework",
] ]
# ---------------------------------------------------------------------------
# Middleware
# ---------------------------------------------------------------------------
MIDDLEWARE = [ MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
@ -47,12 +52,19 @@ MIDDLEWARE = [
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
] ]
# ---------------------------------------------------------------------------
# URL & WSGI configuration
# ---------------------------------------------------------------------------
ROOT_URLCONF = "config.urls" ROOT_URLCONF = "config.urls"
WSGI_APPLICATION = "config.wsgi.application"
# ---------------------------------------------------------------------------
# Templates
# ---------------------------------------------------------------------------
TEMPLATES = [ TEMPLATES = [
{ {
"BACKEND": "django.template.backends.django.DjangoTemplates", "BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [], "DIRS": [], # can be extended if custom templates are needed
"APP_DIRS": True, "APP_DIRS": True,
"OPTIONS": { "OPTIONS": {
"context_processors": [ "context_processors": [
@ -65,89 +77,100 @@ TEMPLATES = [
}, },
] ]
WSGI_APPLICATION = "config.wsgi.application" # ---------------------------------------------------------------------------
# Database configuration
# ---------------------------------------------------------------------------
DB_ENGINE = env("DB_ENGINE", default="sqlite").lower()
if DB_ENGINE == "postgres":
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": env("DB_NAME", default="postgres"),
"USER": env("DB_USER", default="postgres"),
"PASSWORD": env("DB_PASSWORD", default=""),
"HOST": env("DB_HOST", default="localhost"),
"PORT": env("DB_PORT", default="5432"),
}
}
# Database elif DB_ENGINE == "mysql":
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"NAME": env("DB_NAME", default="mysql"),
"USER": env("DB_USER", default="root"),
"PASSWORD": env("DB_PASSWORD", default=""),
"HOST": env("DB_HOST", default="localhost"),
"PORT": env("DB_PORT", default="3306"),
"OPTIONS": {
"charset": "utf8mb4", # recommended for full Unicode support
},
}
}
else: # default: SQLite
DATABASES = { DATABASES = {
"default": { "default": {
"ENGINE": "django.db.backends.sqlite3", "ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3", "NAME": BASE_DIR / "db.sqlite3", # fixed filename for simplicity
} }
} }
# ---------------------------------------------------------------------------
# Password validation # Authentication & password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators # ---------------------------------------------------------------------------
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
"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"},
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
] ]
# ---------------------------------------------------------------------------
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/ # ---------------------------------------------------------------------------
LANGUAGE_CODE = "en-us" LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC" TIME_ZONE = "UTC"
USE_I18N = True USE_I18N = True
USE_TZ = True USE_TZ = True
# ---------------------------------------------------------------------------
# Static files (CSS, JavaScript, Images) # Static files
# https://docs.djangoproject.com/en/4.2/howto/static-files/ # ---------------------------------------------------------------------------
STATIC_URL = "static/" STATIC_URL = "static/"
# ---------------------------------------------------------------------------
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field # ---------------------------------------------------------------------------
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# ---------------------------------------------------------------------------
# Django REST framework configuration
# ---------------------------------------------------------------------------
REST_FRAMEWORK = { REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [ "DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated", "rest_framework.permissions.IsAuthenticated", # all endpoints protected by default
], ],
"DEFAULT_AUTHENTICATION_CLASSES": [ "DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication", "rest_framework.authentication.SessionAuthentication", # required for OIDC/session login
"rest_framework.authentication.BasicAuthentication", "rest_framework.authentication.BasicAuthentication", # allows Basic Auth for API clients
], ],
} }
# OIDC Vars # ---------------------------------------------------------------------------
# OpenID Connect (SSO) configuration
# ---------------------------------------------------------------------------
SSO_ENABLED = env.bool("SSO_ENABLED", default=False) SSO_ENABLED = env.bool("SSO_ENABLED", default=False)
if SSO_ENABLED: if SSO_ENABLED:
INSTALLED_APPS += [ INSTALLED_APPS += ["mozilla_django_oidc"]
"mozilla_django_oidc", MIDDLEWARE += ["mozilla_django_oidc.middleware.SessionRefresh"]
]
MIDDLEWARE += [
"mozilla_django_oidc.middleware.SessionRefresh",
]
LOGIN_URL = "/oidc/authenticate/" LOGIN_URL = "/oidc/authenticate/"
LOGIN_REDIRECT_URL = "/" LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/" LOGOUT_REDIRECT_URL = "/"
OIDC_RP_CLIENT_ID = env("OIDC_RP_CLIENT_ID", default="django-app") OIDC_RP_CLIENT_ID = env("OIDC_RP_CLIENT_ID", default="django-app")
OIDC_RP_CLIENT_SECRET = env("OIDC_RP_CLIENT_SECRET", default="changeme") OIDC_RP_CLIENT_SECRET = env("OIDC_RP_CLIENT_SECRET", default="changeme")
OIDC_OP_DISCOVERY_ENDPOINT = env( OIDC_OP_DISCOVERY_ENDPOINT = env(

View file

@ -1,13 +1,16 @@
from django.conf import settings from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from api.views import ping from api.views import ping, secure_ping
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("api/ping/", ping), path("api/ping/", ping), # Public healthcheck endpoint
path("api/secure-ping/", secure_ping) # Protected API endpoint
] ]
# Add OIDC routes only if Single Sign-On is enabled
if settings.SSO_ENABLED: if settings.SSO_ENABLED:
urlpatterns += [ urlpatterns += [
path("oidc/", include("mozilla_django_oidc.urls")), path("oidc/", include("mozilla_django_oidc.urls")),

BIN
db.sqlite3 Normal file

Binary file not shown.

View file

@ -0,0 +1,9 @@
---
services:
risk-management:
build: .
container_name: django_web
ports:
- "8000:8000"
volumes:
- ./data/app/:/app

View file

@ -1,6 +1,8 @@
#!/bin/sh #!/bin/sh
set -e set -e
export DJANGO_SETTINGS_MODULE=config.settings
echo "Running migrations..." echo "Running migrations..."
python manage.py migrate --noinput python manage.py migrate --noinput

47
startserver.sh Executable file
View file

@ -0,0 +1,47 @@
#!/usr/bin/env bash
# -----------------------------------------------------------------------------
# Startup script for the Django project
# -----------------------------------------------------------------------------
# This script ensures that the development environment is ready to run.
#
# Features:
# - Runs database migrations
# - Creates a superuser if it does not already exist
# - Starts the Django development server
#
# Note:
# This script is for local development only.
# In production you should use a proper WSGI/ASGI server (e.g., Gunicorn, Daphne).
# -----------------------------------------------------------------------------
set -e # Exit immediately if a command exits with a non-zero status
# Load environment variables from .env if available
if [ -f ".env" ]; then
export $(grep -v '^#' .env | xargs)
fi
# Ensure we are in the project root directory
cd "$(dirname "$0")"
echo ">>> Running migrations..."
python manage.py migrate --noinput
echo ">>> Checking if superuser exists..."
python manage.py shell <<EOF
from django.contrib.auth import get_user_model
User = get_user_model()
username = "${DJANGO_SUPERUSER_USERNAME:-admin}"
if not User.objects.filter(username=username).exists():
print(f"Creating superuser '{username}'...")
User.objects.create_superuser(
username=username,
email="${DJANGO_SUPERUSER_EMAIL:-admin@example.com}",
password="${DJANGO_SUPERUSER_PASSWORD:-changeme}"
)
else:
print(f"Superuser '{username}' already exists.")
EOF
echo ">>> Starting Django development server at http://127.0.0.1:8000 ..."
python manage.py runserver 0.0.0.0:8000