diff --git a/.env.example b/.env.example index 261f3fd..a4d00a3 100644 --- a/.env.example +++ b/.env.example @@ -27,6 +27,16 @@ DJANGO_CSRF_TRUSTED_ORIGINS=https://rooster.example.nl # in dit project geeft die header door. DJANGO_SECURE=1 +# --- Opslag --------------------------------------------------------------- +# Waar de gegevens staan. Een NAAM = Docker beheert het volume; een PAD = +# gewone map naast de code (direct benaderbaar, makkelijk te back-uppen). +# scripts/install.sh vult dit in op basis van je keuze. +# Docker-volumes: DATA_DB=pgdata DATA_MEDIA=media_volume DATA_STATIC=static_volume +# Gewone mappen: DATA_DB=./data/postgres DATA_MEDIA=./data/media DATA_STATIC=./data/static +DATA_DB=pgdata +DATA_MEDIA=media_volume +DATA_STATIC=static_volume + # --- PostgreSQL ----------------------------------------------------------- POSTGRES_DB=rooster POSTGRES_USER=rooster diff --git a/.gitignore b/.gitignore index 184a620..ac38de3 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,9 @@ logs/ .DS_Store Thumbs.db desktop.ini + +# Gegevens bij de 'gewone mappen'-opslagkeuze (database, media, statics). +# Deze map hoort nooit in git en moet buiten 'git status' blijven, anders +# weigert scripts/deploy.sh te draaien wegens 'lokale wijzigingen'. +/data/ +.env.backup-* diff --git a/DEPLOY.md b/DEPLOY.md index bfacd37..9940e76 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -78,30 +78,67 @@ Je hoeft op de server **niets** te installeren behalve Docker zelf (met de ## Eenmalig: server klaarzetten -1. Zorg dat Docker + Compose op de test-server staan: - ```bash - docker --version - docker compose version - ``` -2. Haal de code op: - ```bash - git clone /roosterwijs.git - cd roosterwijs - ``` -3. Maak het `.env`-bestand aan vanuit het voorbeeld en vul echte waarden in: - ```bash - cp .env.example .env - nano .env - ``` - Belangrijk om aan te passen: - - `DJANGO_SECRET_KEY` — genereer met - `python3 -c "import secrets; print(secrets.token_urlsafe(50))"` - - `DJANGO_ALLOWED_HOSTS` — de hostnaam/IP van de test-server - - `DJANGO_CSRF_TRUSTED_ORIGINS` — exact de origin die de browser gebruikt, - dus `https://` achter TLS (of `http://:8080` zonder TLS) - - `POSTGRES_PASSWORD` — een sterk wachtwoord +Het installatiescript doet alles hieronder in zeven stappen en stelt drie +vragen. Standaard komt alles in **`/srv/server-up`**. - `.env` staat in `.gitignore` en komt dus **niet** in git — dat hoort zo. +```bash +git clone /roosterwijs.git /tmp/roosterwijs-install +/tmp/roosterwijs-install/scripts/install.sh +``` + +Heb je nog geen schrijfrechten op `/srv`, dan zegt het script welk commando je +eenmalig als beheerder moet draaien. Een andere map kan met: + +```bash +ROOSTERWIJS_DIR=/pad/naar/map ./scripts/install.sh +``` + +Het script controleert Docker, haalt de code op, vraagt waar de gegevens moeten +staan, genereert `.env` met een willekeurige `DJANGO_SECRET_KEY` en een sterk +databasewachtwoord, bouwt de containers en biedt aan een beheerder aan te maken. + +### De opslagkeuze + +Bij stap 4 kies je waar database, media en statics terechtkomen: + +| Keuze | Waar | Wanneer | +|-------|------|---------| +| **1. Docker-volumes** | door Docker beheerd (`docker volume ls`) | standaard; afgeschermd, maar alleen bereikbaar via docker-commando's | +| **2. Gewone mappen** | `/srv/server-up/data/` | alles zichtbaar naast de code, makkelijk te back-uppen en in te zien | + +De keuze staat als `DATA_DB`, `DATA_MEDIA` en `DATA_STATIC` in `.env` en is +later te wijzigen. **Let op:** omzetten verplaatst geen gegevens. Wissel je van +volumes naar mappen, maak dan eerst een dump en zet die daarna terug: + +```bash +docker compose exec db pg_dump -U rooster rooster > ~/rooster.sql # vóór het omzetten +# ... DATA_* aanpassen in .env, dan: +docker compose down && docker compose up -d db +cat ~/rooster.sql | docker compose exec -T db psql -U rooster rooster +docker compose up -d +``` + +Zonder die stap start de app met een **lege** database — de oude gegevens staan +dan nog wel in het oude volume, maar worden niet meer gebruikt. + +### Handmatig, zonder script + +```bash +git clone /roosterwijs.git /srv/server-up +cd /srv/server-up +cp .env.example .env +nano .env +``` + +Belangrijk om aan te passen: +- `DJANGO_SECRET_KEY` — genereer met `openssl rand -base64 48` +- `DJANGO_ALLOWED_HOSTS` — de hostnaam/IP van de server +- `DJANGO_CSRF_TRUSTED_ORIGINS` — exact de origin die de browser gebruikt, + dus `https://` achter TLS (of `http://:8080` zonder TLS) +- `POSTGRES_PASSWORD` — een sterk wachtwoord +- `DATA_DB` / `DATA_MEDIA` / `DATA_STATIC` — zie de tabel hierboven + +`.env` staat in `.gitignore` en komt dus **niet** in git — dat hoort zo. ## Starten diff --git a/README.md b/README.md index 38ffae1..84d3193 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,23 @@ geboortedatum vast. Een nieuwe module toevoegen = nieuwe app maken, één regel in `MODULE_APPS` (in `backend/config/settings.py`), klaar. +## Installeren op een server + +Eén commando, zeven stappen, drie vragen. Alles komt standaard in +`/srv/server-up`: + +```bash +git clone /roosterwijs.git /tmp/roosterwijs-install +/tmp/roosterwijs-install/scripts/install.sh +``` + +Het script controleert Docker, haalt de code op, laat je kiezen of de gegevens +in **Docker-volumes** of in **gewone mappen** (`/srv/server-up/data/`) komen, +genereert `.env` met willekeurige geheimen, bouwt de containers en maakt een +beheerder aan. Bijwerken doe je daarna met `./scripts/deploy.sh`. + +Details, de opslagkeuze en het draaien achter TLS staan in `DEPLOY.md`. + ## Starten (lokaal ontwikkelen) > Voor een test- of productieserver gelden **andere** stappen: zie `DEPLOY.md`. diff --git a/docker-compose.yml b/docker-compose.yml index 377614a..2e738e7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,12 @@ -# Stack voor de test-server (en later productie): +# Stack voor de test-server (en later productie). +# +# Waar de gegevens staan, bepaal je in .env (scripts/install.sh vraagt ernaar): +# DATA_DB / DATA_MEDIA / DATA_STATIC +# - een naam (bv. pgdata) -> Docker beheert het volume +# - een pad (bv. ./data/postgres) -> gewone map, direct benaderbaar +# Laat je ze leeg, dan gelden de named volumes hieronder — precies zoals +# bestaande installaties nu al draaien. +# # db - PostgreSQL # backend - Django via gunicorn # nginx - serveert de frontend en proxyt /api + /admin naar de backend @@ -13,7 +21,7 @@ services: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes: - - pgdata:/var/lib/postgresql/data + - ${DATA_DB:-pgdata}:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 5s @@ -47,9 +55,9 @@ services: FRONTEND_BASE_URL: ${FRONTEND_BASE_URL:-} volumes: # Verzamelde statics delen met nginx. - - static_volume:/app/staticfiles - # Geüploade media (pictogrammen) delen met nginx. - - media_volume:/app/media + - ${DATA_STATIC:-static_volume}:/app/staticfiles + # Geüploade media (pictogrammen, logo) delen met nginx. + - ${DATA_MEDIA:-media_volume}:/app/media nginx: build: ./frontend @@ -61,8 +69,8 @@ services: - "8080:80" volumes: # Lees de door Django verzamelde statics (alleen-lezen). - - static_volume:/static:ro - - media_volume:/media:ro + - ${DATA_STATIC:-static_volume}:/static:ro + - ${DATA_MEDIA:-media_volume}:/media:ro volumes: pgdata: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 663a237..b7976d3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "roosterwijs-frontend", - "version": "0.2.3-beta", + "version": "0.2.4-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roosterwijs-frontend", - "version": "0.2.3-beta", + "version": "0.2.4-beta", "dependencies": { "react": "^18.3.1", "react-dom": "^18.3.1" diff --git a/frontend/package.json b/frontend/package.json index 51abb99..11479d5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "roosterwijs-frontend", "private": true, - "version": "0.2.3-beta", + "version": "0.2.4-beta", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/modern.css b/frontend/src/modern.css index 48ff542..eedd161 100644 --- a/frontend/src/modern.css +++ b/frontend/src/modern.css @@ -35,7 +35,7 @@ button,a,input,select{transition:border-color .15s,background-color .15s,color . .cog-btn{width:100%;min-height:38px;justify-content:flex-start;font-size:15px;padding:0} .cog-btn::after{content:"Beheer en instellingen";margin-left:8px;font-size:13px} .cog-dropdown{position:static;min-width:0;margin:4px 0 0;box-shadow:none} -.topbar::after{content:"v0.2.03 beta";order:4;margin:10px 8px 0;color:#98a2b3;font-size:10px} +.topbar::after{content:"v0.2.04 beta";order:4;margin:10px 8px 0;color:#98a2b3;font-size:10px} .content{margin-left:var(--sidebar);width:calc(100% - var(--sidebar));max-width:1680px;padding:32px 38px 48px} .content-wide{padding:24px 28px 42px} h1{font-size:clamp(24px,2.4vw,30px);line-height:1.2;letter-spacing:-.025em;font-weight:750} diff --git a/scripts/deploy.sh b/scripts/deploy.sh index cbb3a95..f1be707 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -66,6 +66,16 @@ else fi # --- Bouwen en starten ---------------------------------------------------- +# Bij de 'gewone mappen'-opslagkeuze moeten die mappen bestaan; Docker zou ze +# anders als root aanmaken. +# shellcheck disable=SC1091 +set -a; . ./.env; set +a +for pad in "${DATA_DB:-}" "${DATA_MEDIA:-}" "${DATA_STATIC:-}"; do + case "$pad" in + ./*|/*) mkdir -p "$pad" ;; + esac +done + docker compose up -d --build # --- Controleren ---------------------------------------------------------- diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..d4e4176 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,252 @@ +#!/usr/bin/env bash +# +# Roosterwijs installeren op een server. +# +# Gebruik: ./scripts/install.sh +# ROOSTERWIJS_DIR=/pad/naar/map ./scripts/install.sh +# +# Het script doorloopt zeven stappen, stelt onderweg drie vragen en raakt +# nooit iets aan zonder dat te melden. Afbreken kan altijd met Ctrl-C. +set -euo pipefail + +DOELMAP="${ROOSTERWIJS_DIR:-/srv/server-up}" + +# ---------------------------------------------------------------- vormgeving +# Kleuren alleen als we naar een terminal schrijven (en NO_COLOR niet gezet is), +# zodat de uitvoer in een logbestand leesbaar blijft. +if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then + VET=$'\033[1m'; DOF=$'\033[2m'; STOP=$'\033[0m' + GROEN=$'\033[32m'; GEEL=$'\033[33m'; ROOD=$'\033[31m'; BLAUW=$'\033[36m' +else + VET=''; DOF=''; STOP=''; GROEN=''; GEEL=''; ROOD=''; BLAUW='' +fi + +LIJN=$(printf '─%.0s' $(seq 1 62)) +STAP_NR=0 +STAP_TOTAAL=7 + +kop() { + printf '\n%s%s%s\n' "$BLAUW" "$LIJN" "$STOP" + printf ' %sROOSTERWIJS%s %s· installatie%s\n' "$VET" "$STOP" "$DOF" "$STOP" + printf '%s%s%s\n' "$BLAUW" "$LIJN" "$STOP" +} +stap() { STAP_NR=$((STAP_NR + 1)); printf '\n %s[%d/%d]%s %s%s%s\n' "$BLAUW" "$STAP_NR" "$STAP_TOTAAL" "$STOP" "$VET" "$1" "$STOP"; } +ok() { printf ' %s✓%s %s\n' "$GROEN" "$STOP" "$1"; } +info() { printf ' %s·%s %s\n' "$DOF" "$STOP" "$1"; } +let_op(){ printf ' %s!%s %s\n' "$GEEL" "$STOP" "$1"; } +fout() { printf '\n %s✗ %s%s\n\n' "$ROOD" "$1" "$STOP" >&2; exit 1; } + +# vraag "Tekst" "standaardwaarde" -> antwoord in $ANTWOORD +vraag() { + local tekst="$1" standaard="${2:-}" invoer + if [ -n "$standaard" ]; then + printf ' %s%s%s %s[%s]%s: ' "$VET" "$tekst" "$STOP" "$DOF" "$standaard" "$STOP" + else + printf ' %s%s%s: ' "$VET" "$tekst" "$STOP" + fi + read -r invoer /dev/null || invoer="" + ANTWOORD="${invoer:-$standaard}" +} + +# jaNee "Vraag" "j|n" -> 0 = ja, 1 = nee +jaNee() { + local tekst="$1" standaard="${2:-j}" invoer + local hint="j/N"; [ "$standaard" = "j" ] && hint="J/n" + printf ' %s%s%s %s(%s)%s: ' "$VET" "$tekst" "$STOP" "$DOF" "$hint" "$STOP" + read -r invoer /dev/null || invoer="" + invoer="${invoer:-$standaard}" + case "$invoer" in [jJyY]*) return 0 ;; *) return 1 ;; esac +} + +# Wachtwoord/sleutel genereren zonder Python op de server te vereisen. +genereer_geheim() { + if command -v openssl >/dev/null 2>&1; then + openssl rand -base64 48 | tr -dc 'A-Za-z0-9' | cut -c1-50 + else + LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 50 + fi +} + +kop + +# ------------------------------------------------------------- 1. vereisten +stap "Vereisten controleren" +command -v git >/dev/null 2>&1 || fout "git ontbreekt. Installeer git en probeer opnieuw." +ok "git $(git --version | awk '{print $3}')" +command -v docker >/dev/null 2>&1 || fout "docker ontbreekt. Zie https://docs.docker.com/engine/install/" +ok "docker $(docker --version | awk '{print $3}' | tr -d ,)" +docker compose version >/dev/null 2>&1 || fout "'docker compose' werkt niet. Installeer de compose-plugin." +ok "docker compose $(docker compose version --short 2>/dev/null || echo aanwezig)" +docker info >/dev/null 2>&1 || fout "Kan niet met de Docker-daemon praten. Draait Docker, en zit je in de groep 'docker'?" +ok "Docker-daemon bereikbaar" + +# --------------------------------------------------------------- 2. doelmap +stap "Doelmap $DOELMAP" +if [ ! -d "$DOELMAP" ]; then + if mkdir -p "$DOELMAP" 2>/dev/null; then + ok "aangemaakt" + else + printf '\n' + let_op "Geen rechten om $DOELMAP aan te maken." + info "Draai eenmalig, als beheerder:" + printf '\n %ssudo mkdir -p %s && sudo chown "$USER":"$USER" %s%s\n\n' "$VET" "$DOELMAP" "$DOELMAP" "$STOP" + fout "Start daarna dit script opnieuw." + fi +else + [ -w "$DOELMAP" ] || fout "$DOELMAP bestaat, maar je hebt er geen schrijfrechten." + ok "bestaat en is beschrijfbaar" +fi + +# ------------------------------------------------------------------ 3. code +stap "Code ophalen" +if [ -d "$DOELMAP/.git" ]; then + ok "checkout gevonden" + if jaNee "Bijwerken naar de laatste versie?" j; then + git -C "$DOELMAP" fetch origin --quiet + tak="$(git -C "$DOELMAP" rev-parse --abbrev-ref HEAD)" + git -C "$DOELMAP" reset --hard "origin/$tak" --quiet + ok "bijgewerkt naar $(git -C "$DOELMAP" rev-parse --short HEAD) op $tak" + else + info "overgeslagen; huidige versie blijft staan" + fi +elif [ -n "$(ls -A "$DOELMAP" 2>/dev/null)" ]; then + fout "$DOELMAP is niet leeg en bevat geen git-checkout. Maak de map leeg of kies een andere met ROOSTERWIJS_DIR=..." +else + herkomst="" + if git -C "$(dirname "$0")/.." rev-parse --git-dir >/dev/null 2>&1; then + herkomst="$(git -C "$(dirname "$0")/.." remote get-url origin 2>/dev/null || true)" + fi + vraag "Git-URL van de repository" "$herkomst" + [ -n "$ANTWOORD" ] || fout "Zonder URL kan de code niet worden opgehaald." + git clone "$ANTWOORD" "$DOELMAP" --quiet || fout "Klonen mislukt. Klopt de URL en heb je toegang (SSH-sleutel)?" + ok "gekloond naar $DOELMAP" +fi +cd "$DOELMAP" + +# --------------------------------------------------------------- 4. opslag +stap "Waar komen de gegevens te staan?" +printf '\n' +printf ' %s1)%s %sDocker-volumes%s %s(standaard)%s\n' "$VET" "$STOP" "$VET" "$STOP" "$DOF" "$STOP" +printf ' Docker beheert de opslag. Netjes afgeschermd, maar je\n' +printf ' komt er alleen bij via docker-commandos.\n\n' +printf ' %s2)%s %sGewone mappen%s onder %s/data\n' "$VET" "$STOP" "$VET" "$STOP" "$DOELMAP" +printf ' Database, media en statics staan als zichtbare mappen\n' +printf ' naast de code. Makkelijk back-uppen en inzien.\n\n' +vraag "Keuze (1 of 2)" "1" +case "$ANTWOORD" in + 2) + OPSLAG="mappen" + DATA_DB="./data/postgres"; DATA_MEDIA="./data/media"; DATA_STATIC="./data/static" + mkdir -p data/postgres data/media data/static + ok "mappen aangemaakt onder $DOELMAP/data" + ;; + *) + OPSLAG="volumes" + DATA_DB="pgdata"; DATA_MEDIA="media_volume"; DATA_STATIC="static_volume" + ok "Docker beheert de opslag (volumes pgdata, media_volume, static_volume)" + ;; +esac + +# ------------------------------------------------------------------ 5. .env +stap "Instellingen (.env)" +if [ -f .env ]; then + ok ".env bestaat al" + if jaNee "Laten staan en ongewijzigd gebruiken?" j; then + info "bestaande instellingen blijven gelden" + if ! grep -q '^DATA_DB=' .env; then + { echo ""; echo "# Opslagkeuze (toegevoegd door install.sh)" + echo "DATA_DB=$DATA_DB"; echo "DATA_MEDIA=$DATA_MEDIA"; echo "DATA_STATIC=$DATA_STATIC"; } >> .env + ok "opslagkeuze toegevoegd aan .env" + fi + else + cp .env ".env.backup-$(date +%Y%m%d-%H%M%S)" + ok "oude .env bewaard als .env.backup-*" + rm -f .env + fi +fi + +if [ ! -f .env ]; then + vraag "Hostnaam waarop de app bereikbaar is" "rooster.example.nl" + HOSTNAAM="$ANTWOORD" + if jaNee "Draait er TLS (https) voor deze server?" j; then + SCHEMA="https"; SECURE=1 + else + SCHEMA="http"; SECURE=0 + fi + BASIS_URL="$SCHEMA://$HOSTNAAM" + [ "$SCHEMA" = "http" ] && BASIS_URL="$BASIS_URL:8080" + + cat > .env < +FRONTEND_BASE_URL=$BASIS_URL +ENVEOF + chmod 600 .env + ok "aangemaakt met willekeurige sleutel en databasewachtwoord" + info "hostnaam: $HOSTNAAM · $( [ "$SECURE" = 1 ] && echo 'TLS aan' || echo 'TLS uit' )" +fi + +# ------------------------------------------------------------ 6. bouwen +stap "Containers bouwen en starten" +info "dit duurt de eerste keer enkele minuten" +docker compose up -d --build || fout "Bouwen mislukt. Bekijk de uitvoer hierboven." +ok "db, backend en nginx draaien" + +# --------------------------------------------------------- 7. beheerder +stap "Beheerder" +aantal="$(docker compose exec -T backend python manage.py shell -c \ + 'from django.contrib.auth import get_user_model; print(get_user_model().objects.count())' 2>/dev/null | tr -dc '0-9')" +aantal="${aantal:-0}" +if [ "$aantal" -gt 0 ]; then + ok "er zijn al $aantal account(s); geen nieuwe nodig" +else + let_op "Er is nog geen account. Zonder account kun je nergens inloggen." + if jaNee "Nu een beheerder aanmaken?" j; then + docker compose exec backend python manage.py createsuperuser /dev/null || \ + let_op "Aanmaken afgebroken; doe het later met het commando hieronder." + fi +fi + +# ------------------------------------------------------------------ klaar +printf '\n%s%s%s\n' "$GROEN" "$LIJN" "$STOP" +printf ' %s✓ Installatie afgerond%s\n' "$GROEN$VET" "$STOP" +printf '%s%s%s\n\n' "$GROEN" "$LIJN" "$STOP" +printf ' %sMap%s %s\n' "$DOF" "$STOP" "$DOELMAP" +if [ "$OPSLAG" = "mappen" ]; then + printf ' %sGegevens%s %s/data (database, media, statics)\n' "$DOF" "$STOP" "$DOELMAP" +else + printf ' %sGegevens%s Docker-volumes (docker volume ls)\n' "$DOF" "$STOP" +fi +printf ' %sBijwerken%s ./scripts/deploy.sh\n' "$DOF" "$STOP" +printf ' %sLogs%s docker compose logs -f\n' "$DOF" "$STOP" +printf ' %sBeheerder%s docker compose exec backend python manage.py createsuperuser\n\n' "$DOF" "$STOP"