From 37168500d1064c70415a736c47ce121ea56778b0 Mon Sep 17 00:00:00 2001 From: = <=> Date: Fri, 5 Sep 2025 15:32:33 +0200 Subject: [PATCH] Create Basic Django Framework with API, SQLite, PostgreSQL, MySQL and SSO with OIDC --- .env-example | 23 +++ .gitignore | 3 + api/__pycache__/__init__.cpython-311.pyc | Bin 169 -> 180 bytes api/__pycache__/views.cpython-311.pyc | Bin 726 -> 1262 bytes api/views.py | 18 ++- config/__pycache__/__init__.cpython-311.pyc | Bin 172 -> 183 bytes config/__pycache__/settings.cpython-311.pyc | Bin 3758 -> 4666 bytes config/__pycache__/urls.cpython-311.pyc | Bin 751 -> 846 bytes config/__pycache__/wsgi.cpython-311.pyc | Bin 692 -> 703 bytes config/settings.py | 159 +++++++++++--------- config/urls.py | 9 +- db.sqlite3 | Bin 0 -> 131072 bytes docker-compose-sqlite.yml | 9 ++ entrypoint.sh | 2 + startserver.sh | 47 ++++++ 15 files changed, 196 insertions(+), 74 deletions(-) create mode 100644 .env-example create mode 100644 db.sqlite3 create mode 100644 docker-compose-sqlite.yml create mode 100755 startserver.sh diff --git a/.env-example b/.env-example new file mode 100644 index 0000000..71328b3 --- /dev/null +++ b/.env-example @@ -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= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1d17dae..f395f46 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ .venv +.startserver.sh +.env +sb.sqlite3 \ No newline at end of file diff --git a/api/__pycache__/__init__.cpython-311.pyc b/api/__pycache__/__init__.cpython-311.pyc index 9a9a706125e7693dc0b6fea7ee5e4b80c1360ba3..2fb33de5555a0f3d11b9035f34f604a1b22c5b5a 100644 GIT binary patch delta 72 zcmZ3pWE< z)U_G2ea|R5CS!yNeAaRN^;vI&Us+(Y)u2SYz%H5rvGy_7mk6u)9wS_yfAqX~+9;G3 zk}& zV4h}9qdK^0T41`sJkupy5jc+ak{ordtApu4LsD6Fdl6oP=_*-hQTBLX^3no0?{weH^NM|jz=31 VA!!&G1tk*4z;jlhgG6xD^B-5UD53xW delta 297 zcmaFId5x8CIWI340}u$T-IZa$IFV0+v1X#WJP#v73PTEW4r?x36x+lwe|Gj1wqOQL z_KCmLxEXJ8IOgQ!mpkTFPPSo;k6{Dq)?~cJUX)r`np#{^%n0Nw{Ib-~$j?pH&rU7N z%*#lvOfAwcN-fAQ)-TU2N{lbcEY6P4P0UNoOHa*B%`4GQEXdR^%S9sg(>x{2(?jkod)6lbfGXnv-f*Bm(4u z98lZ|Bt9@RGBVzntiU3ob%DX@27~?uRCI$u=K?Bvz{oi{nZ<^Wfss+|1Bd`y0sxq0 BQDXoA diff --git a/api/views.py b/api/views.py index 4665146..a5a49ba 100644 --- a/api/views.py +++ b/api/views.py @@ -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"}) \ No newline at end of file + 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" + }) \ No newline at end of file diff --git a/config/__pycache__/__init__.cpython-311.pyc b/config/__pycache__/__init__.cpython-311.pyc index 2579686a52b774f2e7ba0628ce4d574e844442eb..5106d16f76e9b38b4ac5dbc686ffff0206527733 100644 GIT binary patch delta 72 zcmZ3(xSf$}IWI340}ymvoycWo3+m96R6$ zj=%wrZ!a!k#P-+y2!0&Iz_#)r&atqs1fUbT5W2s?1PmVAB}*8093{|@g~uP7zr_Lg z9*u)*&$Io(P?PiWUg%jjSTO~?&}YQ^q37jzgAe=<-w%UF#)lw%B!2>)H2CHVg5bce zS-?17^?f91@KDkvBx&eS(%_M$0&w_9{pq}N+jopvs?h4=K7(N3!)K%QSJncaVKHCguywO zC^d07zl(DNJiX`WZOn^LxB#h_MR9}tpqS)c@JtEYvID@mcsD$IXuIJ#ZV0}?4Wlt) zwDl%@>y-+lhUYwd`@$=(F~gOFuX;|w6gQ5#m_TC^VtI5(^I+m)`yqY{yYzUNn8khG zwOaLtP>S5HmxM@hbF(6>6&pgW+Fmi8#tQAvOn)_1`r?J_>Fni9Hm#cLH!DJeSECdX`EWS)`F-+PH(%ZTXJSVt7#(RLfeqDAxIgYWb*fV_{=8K4B4ks1?z#LqG@g$pbM6XoMl%w6|O4F!Mr` z|MAb}VZ8k(^-pk0#cOrdvRQ1DE5fRX{u5ABeD#(fqNOxh?~OaJ<}YWiCzqEOGpS@D zlgp}fA-%M`m@K68s;N@D&5NRbp4yMBpJHR}m~Fu}p-(!g!*u(WZ83Ufrd-?LXUhB? zUM#{5p*kb-Xpx0RO%(Y0OeUWTN9Si}Pls28`g(Y&SS_~yu#GGfPz4N)`hBEIBQOk8 z04*7}0(|m+e+qC&#;sY~X=VVkwr?9b$)OEU4n*`ovT6a~3`vY~Y+43R&Cw?8vE~@CJMX4iw!LdI6X`IK zE))5Mi9coH9VXFb5)!>nFfHp-A|MlieafYH<`mZ3O6|}O&dO|}!zQ|H;t0mqzeDUS zJ&MY{M8}ut`kuv{liB$WJKtsJjjSdrl#tnIhmCgGsAjjYRt@Mt&@Qu-kusVGnEP6% zo|^o!$!`dl?M*i?v#|~v>$0&P^SqMsngW`7C(qONo&KFO8iD&9TAx*yq65JkN*MsO zTk%8{Hn{U~hn><)gdU;;VbVi%t&D8HDwEd~lTEfS$z;|rvND-7jGRm^YqTkdqkY#{ z;PMSgc7<_Cv|{U%D2295X3Yv5UbI(M;F*(w8*S51_MvP5^ttbqKQFgFdBEQ-cO3Dq zBQDwY35Nz;*swynW$(1?33te_lvPNFeC&ekPIbtXbY7%Q6n?X7Z+2tvde?TmwqrMcPTaJ_4g%C35XC~KGB_qQk&vc`$nD!YOA|X? zSEtT^#=2+{A?kRShew0_A1cy>rmL^(EI_!7VmfPiEJ=+Q|i5nS4kHf1Ukkie1m z>q|0xuCm9(mNf_VA5IanAKc$<0jd}iX#PZ8g4RK(MQ9S^bPN-ibVhJ&Cjx+}1IZ;c zg*K+eEgLgK%notfC9M#(F-Oywrx{dfR_HjMp*ftOd7Nw!oWkjrgfj;cJ>zKijFTo{ z_kT+iDd6mH%Fm*mUxgkCop2_+ieuqii|ptC)N_2tE>?_sAc-FRWJ77f;J<`ANCXM zRwThXk!QL2{)p4wcD+u!FLk>8k*((K^$qu$CyN=dc62YNtyY(o>y_2&O5gl!<#KJU zvbM6tw7``mOrCeXsDf`a~^0Rf~RjVCC*!?V0nw&PUa_#Nh;dgC|wv&~Xg@ zC{Kc!_oG~8-^AY9orL5<4@_=A*a+gKz)atL_la5LVcAqT9D*FyC12xN&|*E4a#;#1 zTpg0-SIMWZk}F4KOwUQ8B|ZmM+SdnB%hv{xn6C~r-B)-N3_B|RGYYA9dJq>Mx@t|6 zH2c!ZR%|j8gyMmk7l~};If$ZuLzE520`-FMC60X`37<9R*Mdy07fJ=$d@p2k1u{AI Rm(?`NA`J%t-g{#9F98ZD`icMm diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index c5ad2ae8b2c74d8d348be8adc8703d7d5e85eedb..0dd6577dfee1da669ddae854c493bd702e263cbd 100644 GIT binary patch delta 314 zcmaFQdX9~EIWI340}!a)-IeikBCjOlor&t^Qn{Q_oQw=i45_Rstf|adU=<)$S=3vV12F`oGe_~HGG_6@_(jsGJYba^K$0zmbK(JIZk`mbU{g{e4CNE|*G;-6= z$j?pH&q%FIElNzu&CJs;N-fAQ&Me6<%1kZR_YC&eH8M9aFw_moEY8;TP0UN2{F_mM zO9bdDMj$S}F?D*Woyu`f8E=&qs!a&Ozfw=h401gZxBits+* diff --git a/config/__pycache__/wsgi.cpython-311.pyc b/config/__pycache__/wsgi.cpython-311.pyc index 01cbb7e0ddcf6af7531e25cb878cd524dd2823fe..979f37389260ab976379a3838e8ced2b161df7d5 100644 GIT binary patch delta 74 zcmdnOx}TM6IWI340}ymv-N^Nm(a23dBR@A)KO?m=wJ0$qH#1MaD77HJII|?bC^NNK c-!s@>*T~$!z)&|Rvp8GVH!&}9vKCVp0Do>6TL1t6 delta 63 zcmdnbx`mZ%IWI340}zO;-pKWnQQcZUBR@A)KRdN7GcO~xGPOv*D77HJSid~8C^5b$ Rvp73GH!&|UZ?Y*<7XWX96?y;w diff --git a/config/settings.py b/config/settings.py index 1c18edc..ffd02cd 100644 --- a/config/settings.py +++ b/config/settings.py @@ -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 ) +DEBUG = env.bool("DEBUG", default=False) +ALLOWED_HOSTS = ["localhost", "127.0.0.1"] -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = env.bool("DEBUG", default=True) - -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() - -# Database -# https://docs.djangoproject.com/en/4.2/ref/settings/#databases - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", +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"), + } } -} +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 + }, + } + } -# Password validation -# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators +else: # default: SQLite + DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", # fixed filename for simplicity + } + } +# --------------------------------------------------------------------------- +# 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( @@ -157,4 +180,4 @@ if SSO_ENABLED: OIDC_RP_SIGN_ALGO = "RS256" OIDC_STORE_ID_TOKEN = True - OIDC_STORE_ACCESS_TOKEN = True \ No newline at end of file + OIDC_STORE_ACCESS_TOKEN = True diff --git a/config/urls.py b/config/urls.py index 52f3b99..4afde4b 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,14 +1,17 @@ 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")), - ] \ No newline at end of file + ] diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..6c2605045ed7ed1032f1d5598221b3b14838a6db GIT binary patch literal 131072 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 Running migrations..." +python manage.py migrate --noinput + +echo ">>> Checking if superuser exists..." +python manage.py shell <>> Starting Django development server at http://127.0.0.1:8000 ..." +python manage.py runserver 0.0.0.0:8000 \ No newline at end of file