logo-upload: media-rechten zelfherstellend via gosu-entrypoint + module hernoemd naar 'Logo'

This commit is contained in:
bes-r 2026-06-21 14:42:12 +02:00
parent 7a5d3b75eb
commit d8c4523dc0
5 changed files with 27 additions and 15 deletions

View file

@ -8,8 +8,9 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
WORKDIR /app WORKDIR /app
# Systeempakketten die psycopg/gunicorn soms nodig hebben. # Systeempakketten die psycopg/gunicorn soms nodig hebben.
# gosu = privileges veilig droppen in de entrypoint (zie entrypoint.sh).
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y --no-install-recommends libpq5 \ && apt-get install -y --no-install-recommends libpq5 gosu \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Eerst requirements (betere build-cache), dan de rest. # Eerst requirements (betere build-cache), dan de rest.
@ -21,12 +22,12 @@ COPY . .
# entrypoint wacht op de database, migreert en verzamelt statics. # entrypoint wacht op de database, migreert en verzamelt statics.
RUN chmod +x /app/entrypoint.sh RUN chmod +x /app/entrypoint.sh
# Draai niet als root: een eventueel lek in de app geeft dan geen # Maak een niet-root gebruiker aan. De entrypoint start als root (om de
# root-rechten in de container. # rechten op de gedeelde volumes te kunnen zetten) en zakt daarna met gosu
# terug naar appuser, zodat gunicorn zelf NIET als root draait.
RUN useradd --create-home --shell /usr/sbin/nologin appuser \ RUN useradd --create-home --shell /usr/sbin/nologin appuser \
&& mkdir -p /app/staticfiles /app/media \ && mkdir -p /app/staticfiles /app/media/pictogrammen /app/media/huisstijl \
&& chown -R appuser:appuser /app && chown -R appuser:appuser /app
USER appuser
EXPOSE 8000 EXPOSE 8000
ENTRYPOINT ["/app/entrypoint.sh"] ENTRYPOINT ["/app/entrypoint.sh"]

View file

@ -2,6 +2,17 @@
# Wordt bij elke containerstart uitgevoerd, vóór gunicorn. # Wordt bij elke containerstart uitgevoerd, vóór gunicorn.
set -e set -e
# De container start als root, zodat we de rechten op de gedeelde volumes
# (static + media) goed kunnen zetten — ook als die ooit als root zijn
# aangemaakt (anders: 'Permission denied' bij het uploaden van bv. een logo).
# Daarna draaien migraties, collectstatic én gunicorn als 'appuser'.
RUN_AS=""
if [ "$(id -u)" = "0" ]; then
mkdir -p /app/media/pictogrammen /app/media/huisstijl /app/staticfiles
chown -R appuser:appuser /app/media /app/staticfiles || true
RUN_AS="gosu appuser"
fi
# Wacht tot PostgreSQL bereikbaar is (alleen als we Postgres gebruiken). # Wacht tot PostgreSQL bereikbaar is (alleen als we Postgres gebruiken).
if [ -n "$POSTGRES_DB" ]; then if [ -n "$POSTGRES_DB" ]; then
echo "Wachten op PostgreSQL op ${POSTGRES_HOST:-db}:${POSTGRES_PORT:-5432}..." echo "Wachten op PostgreSQL op ${POSTGRES_HOST:-db}:${POSTGRES_PORT:-5432}..."
@ -21,12 +32,12 @@ else:
PY PY
fi fi
# Migraties uitvoeren en statische bestanden verzamelen. # Migraties uitvoeren en statische bestanden verzamelen (als appuser).
echo "Migraties uitvoeren..." echo "Migraties uitvoeren..."
python manage.py migrate --noinput $RUN_AS python manage.py migrate --noinput
echo "Statische bestanden verzamelen..." echo "Statische bestanden verzamelen..."
python manage.py collectstatic --noinput $RUN_AS python manage.py collectstatic --noinput
# Geef door aan het CMD (gunicorn). # Geef door aan het CMD (gunicorn), als appuser.
exec "$@" exec $RUN_AS "$@"

View file

@ -5,7 +5,7 @@ class HuisstijlConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField" default_auto_field = "django.db.models.BigAutoField"
name = "modules.huisstijl" name = "modules.huisstijl"
label = "module_huisstijl" label = "module_huisstijl"
verbose_name = "Module: Huisstijl (logo)" verbose_name = "Module: Logo"
def ready(self): def ready(self):
from plugins.registry import MenuItem, ModuleSpec, registry from plugins.registry import MenuItem, ModuleSpec, registry
@ -13,7 +13,7 @@ class HuisstijlConfig(AppConfig):
registry.register( registry.register(
ModuleSpec( ModuleSpec(
key="huisstijl", key="huisstijl",
name="Huisstijl", name="Logo",
description=( description=(
"Toont het schoollogo in elke roosterweergave en op afdrukken. " "Toont het schoollogo in elke roosterweergave en op afdrukken. "
"Logo te uploaden via Instellingen, met een kwaliteitscheck " "Logo te uploaden via Instellingen, met een kwaliteitscheck "
@ -24,7 +24,7 @@ class HuisstijlConfig(AppConfig):
core=False, core=False,
default_enabled=True, default_enabled=True,
menu_items=( menu_items=(
MenuItem("Huisstijl", "/huisstijl", icon="badge", order=74, group="Instellingen"), MenuItem("Logo", "/logo", icon="badge", order=74, group="Instellingen"),
), ),
) )
) )

View file

@ -47,7 +47,7 @@ const PAGES = {
"/vak-wizard": VakWizardPage, "/vak-wizard": VakWizardPage,
"/personen-wizard": PersonenWizardPage, "/personen-wizard": PersonenWizardPage,
"/auto-rooster": AutoRoosterPage, "/auto-rooster": AutoRoosterPage,
"/huisstijl": HuisstijlPage, "/logo": HuisstijlPage,
}; };
const RESET_PARAMS = (() => { const RESET_PARAMS = (() => {

View file

@ -2830,7 +2830,7 @@ export function RoosterScherm({ mode }) {
<div className="inst-body"><p className="muted small">Vrije locatie-aanduidingen (bv. gymzaal, zwembad).</p><LocatiesPage /></div> <div className="inst-body"><p className="muted small">Vrije locatie-aanduidingen (bv. gymzaal, zwembad).</p><LocatiesPage /></div>
</details> </details>
<details className="inst-sectie"> <details className="inst-sectie">
<summary><span className="ico">🖼</span> Huisstijl / logo</summary> <summary><span className="ico">🖼</span> Logo</summary>
<div className="inst-body"><HuisstijlPage /></div> <div className="inst-body"><HuisstijlPage /></div>
</details> </details>
</div> </div>