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
.startserver.sh
.env
sb.sqlite3

View file

@ -1,8 +1,20 @@
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
@api_view(["GET"])
@permission_classes([AllowAny]) # erstmal offen, später absichern
@permission_classes([AllowAny]) # This endpoint is deliberately open to everyone
def ping(request):
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
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
# Load Env-Vars
# ---------------------------------------------------------------------------
# Environment configuration
# ---------------------------------------------------------------------------
env = environ.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 WARNING: keep the secret key used in production secret!
# ---------------------------------------------------------------------------
# Security settings
# ---------------------------------------------------------------------------
SECRET_KEY = env(
"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
)
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env.bool("DEBUG", default=True)
DEBUG = env.bool("DEBUG", default=False)
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
# Application definition
# ---------------------------------------------------------------------------
# Installed apps
# ---------------------------------------------------------------------------
INSTALLED_APPS = [
# Django built-in apps
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# Third-party apps
"rest_framework",
]
# ---------------------------------------------------------------------------
# Middleware
# ---------------------------------------------------------------------------
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
@ -47,12 +52,19 @@ MIDDLEWARE = [
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
# ---------------------------------------------------------------------------
# URL & WSGI configuration
# ---------------------------------------------------------------------------
ROOT_URLCONF = "config.urls"
WSGI_APPLICATION = "config.wsgi.application"
# ---------------------------------------------------------------------------
# Templates
# ---------------------------------------------------------------------------
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"DIRS": [], # can be extended if custom templates are needed
"APP_DIRS": True,
"OPTIONS": {
"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
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
elif DB_ENGINE == "mysql":
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 = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
"NAME": BASE_DIR / "db.sqlite3", # fixed filename for simplicity
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
# ---------------------------------------------------------------------------
# Authentication & password validation
# ---------------------------------------------------------------------------
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",
},
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
# ---------------------------------------------------------------------------
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
# ---------------------------------------------------------------------------
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
# ---------------------------------------------------------------------------
# Static files
# ---------------------------------------------------------------------------
STATIC_URL = "static/"
# ---------------------------------------------------------------------------
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
# ---------------------------------------------------------------------------
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# ---------------------------------------------------------------------------
# Django REST framework configuration
# ---------------------------------------------------------------------------
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
"rest_framework.permissions.IsAuthenticated", # all endpoints protected by default
],
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.BasicAuthentication",
"rest_framework.authentication.SessionAuthentication", # required for OIDC/session login
"rest_framework.authentication.BasicAuthentication", # allows Basic Auth for API clients
],
}
# OIDC Vars
# ---------------------------------------------------------------------------
# OpenID Connect (SSO) configuration
# ---------------------------------------------------------------------------
SSO_ENABLED = env.bool("SSO_ENABLED", default=False)
if SSO_ENABLED:
INSTALLED_APPS += [
"mozilla_django_oidc",
]
MIDDLEWARE += [
"mozilla_django_oidc.middleware.SessionRefresh",
]
INSTALLED_APPS += ["mozilla_django_oidc"]
MIDDLEWARE += ["mozilla_django_oidc.middleware.SessionRefresh"]
LOGIN_URL = "/oidc/authenticate/"
LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/"
OIDC_RP_CLIENT_ID = env("OIDC_RP_CLIENT_ID", default="django-app")
OIDC_RP_CLIENT_SECRET = env("OIDC_RP_CLIENT_SECRET", default="changeme")
OIDC_OP_DISCOVERY_ENDPOINT = env(

View file

@ -1,13 +1,16 @@
from django.conf import settings
from django.contrib import admin
from django.urls import path, include
from api.views import ping
from api.views import ping, secure_ping
urlpatterns = [
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:
urlpatterns += [
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
set -e
export DJANGO_SETTINGS_MODULE=config.settings
echo "Running migrations..."
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