feat(deploy): deploy.sh + lokale Host-Konfig, server-update/setup_server übernehmen Unit und sudoers aus dem Repo
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
patrickandClaude Sonnet 5.5 committed 2026-11-05 09:40:35 +01:00
1 parent 628955560a
commit 7e165d37b6
8 files changed
+128 -27

No files matched your search

+3
View File
@@ -49,3 +49,6 @@ resume~
Thumbs.db
CLAUDE.md
.claude/
# Lokale Deploy-Konfiguration (Hosts)
deploy.local.env
+5
View File
@@ -0,0 +1,5 @@
# Kopie nach deploy.local.env (gitignored) und anpassen – wird von deploy.sh,
# push-frontend.sh, remote.sh und scripts/check_migrations.sh gelesen.
TM_DEV_HOST=dev.example.com
TM_PROD_HOST=prod.example.com
TM_SSH_USER=root
+41 -9
View File
@@ -1,14 +1,46 @@
#!/usr/bin/env bash
# =============================================================================
# STILLGELEGT – nicht mehr verwenden.
# deploy.sh – LOKAL ausführen: Server aus dem Repo aktualisieren
#
# Dieses Skript rsyncte das lokale Arbeitsverzeichnis in den git-Baum des
# Servers und verschmutzte ihn dadurch (spätere 'git pull' schlugen fehl).
# Usage:
# ./deploy.sh dev|prod [--frontend] [--no-migrate]
#
# Neuer, entkoppelter Ablauf (jeder Server macht sich selbst):
# 1. Lokal: git add -A && git commit && git push origin main
# 2. Server: ssh root@<host> 'cd /opt/timemaster && ./server-update.sh'
# 3. Frontend (falls geändert, lokal): ./push-frontend.sh <137|164>
# Ablauf: prüft, dass alles gepusht ist → ssh <host> './server-update.sh'
# (Server zieht aus dem Git-Remote, migriert, übernimmt Unit/sudoers aus dem Repo,
# startet neu) → optional Frontend bauen + hochladen (push-frontend.sh).
# Hosts stehen in deploy.local.env (gitignored, Vorlage: deploy.local.env.example).
# =============================================================================
echo "deploy.sh ist stillgelegt. Nutze: git push → server-update.sh (auf dem Server) → push-frontend.sh (lokal)." >&2
exit 1
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
[[ -f "$SCRIPT_DIR/deploy.local.env" ]] && source "$SCRIPT_DIR/deploy.local.env"
die() { echo "[FEHLER] $*" >&2; exit 1; }
TARGET="${1:-}"; shift || true
case "$TARGET" in
dev) HOST="${TM_DEV_HOST:-}" ;;
prod) HOST="${TM_PROD_HOST:-}" ;;
*) die "Usage: ./deploy.sh dev|prod [--frontend] [--no-migrate]" ;;
esac
[[ -n "$HOST" ]] || die "Host für '$TARGET' nicht gesetzt (deploy.local.env)."
FRONTEND=false; UPDATE_ARGS=()
for a in "$@"; do
case "$a" in
--frontend) FRONTEND=true ;;
--no-migrate) UPDATE_ARGS+=("$a") ;;
*) die "Unbekannte Option: $a" ;;
esac
done
cd "$SCRIPT_DIR"
git fetch -q origin main
[[ -z "$(git status --porcelain --untracked-files=no)" ]] || echo "[!] Uncommittete Änderungen – der Server bekommt nur gepushte Commits."
[[ "$(git rev-parse HEAD)" == "$(git rev-parse origin/main)" ]] || die "Lokaler HEAD ist nicht gepusht (git push origin main)."
echo "==> Update $TARGET (${TM_SSH_USER:-root}@$HOST)"
ssh "${TM_SSH_USER:-root}@$HOST" "cd /opt/timemaster && ./server-update.sh ${UPDATE_ARGS[*]:-}"
if $FRONTEND; then
"$SCRIPT_DIR/push-frontend.sh" "$TARGET"
fi
+5 -4
View File
@@ -7,8 +7,8 @@
# (server-update.sh kann das Frontend also nicht selbst bauen).
#
# Usage:
# ./push-frontend.sh 137 # = dev → root@$TM_DEV_HOST (Default 192.168.1.137)
# ./push-frontend.sh 164 # = prod → root@$TM_PROD_HOST (Default 192.168.1.164)
# ./push-frontend.sh 137 # = dev → root@$TM_DEV_HOST (aus deploy.local.env)
# ./push-frontend.sh 164 # = prod → root@$TM_PROD_HOST (aus deploy.local.env)
# ./push-frontend.sh dev --no-build # bestehendes dist/ nur syncen
# Überschreiben: TM_DEV_HOST, TM_PROD_HOST, TM_SSH_USER (Default root)
# =============================================================================
@@ -22,8 +22,9 @@ GREEN='\033[0;32m'; RED='\033[0;31m'; NC='\033[0m'
log() { echo -e "${GREEN}==>${NC} $*"; }
die() { echo -e "${RED}[FEHLER]${NC} $*" >&2; exit 1; }
DEV_HOST="${TM_DEV_HOST:-192.168.1.137}"
PROD_HOST="${TM_PROD_HOST:-192.168.1.164}"
[[ -f "$SCRIPT_DIR/deploy.local.env" ]] && source "$SCRIPT_DIR/deploy.local.env"
DEV_HOST="${TM_DEV_HOST:-}"
PROD_HOST="${TM_PROD_HOST:-}"
SSH_USER="${TM_SSH_USER:-root}"
TARGET="${1:-}"
case "$TARGET" in
+4 -2
View File
@@ -12,8 +12,10 @@
# ./remote.sh env → .env auf Server bearbeiten
# ============================================================
# Ziel: TM_DEV_HOST (Default 192.168.1.137); TM_TARGET=prod nutzt TM_PROD_HOST (Default 192.168.1.164)
if [[ "${TM_TARGET:-dev}" == "prod" ]]; then HOST="${TM_PROD_HOST:-192.168.1.164}"; else HOST="${TM_DEV_HOST:-192.168.1.137}"; fi
# Hosts aus deploy.local.env; TM_TARGET=prod nutzt TM_PROD_HOST
[[ -f "$(dirname "$0")/deploy.local.env" ]] && source "$(dirname "$0")/deploy.local.env"
if [[ "${TM_TARGET:-dev}" == "prod" ]]; then HOST="${TM_PROD_HOST:-}"; else HOST="${TM_DEV_HOST:-}"; fi
[[ -n "$HOST" ]] || { echo "Host nicht gesetzt (deploy.local.env)." >&2; exit 1; }
SERVER="${TM_SSH_USER:-root}@$HOST"
REMOTE="/opt/timemaster"
+6 -4
View File
@@ -9,8 +9,8 @@
# ./scripts/check_migrations.sh [137|164|both]
#
# Argumente:
# 137 Nur Dev-Server prüfen (Standard; $TM_DEV_HOST, Default 192.168.1.137)
# 164 Nur Prod-Server prüfen ($TM_PROD_HOST, Default 192.168.1.164)
# 137 Nur Dev-Server prüfen (Standard; $TM_DEV_HOST)
# 164 Nur Prod-Server prüfen ($TM_PROD_HOST)
# both Beide Server prüfen
#
# Exit-Codes:
@@ -27,9 +27,11 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
BACKEND_DIR="$PROJECT_DIR/backend"
[[ -f "$(dirname "$0")/../deploy.local.env" ]] && source "$(dirname "$0")/../deploy.local.env"
SSH_USER="${TM_SSH_USER:-root}"
SERVER_137="$SSH_USER@${TM_DEV_HOST:-192.168.1.137}"
SERVER_164="$SSH_USER@${TM_PROD_HOST:-192.168.1.164}"
[[ -n "${TM_DEV_HOST:-}" && -n "${TM_PROD_HOST:-}" ]] || { echo "TM_DEV_HOST/TM_PROD_HOST nicht gesetzt (deploy.local.env)." >&2; exit 1; }
SERVER_137="$SSH_USER@$TM_DEV_HOST"
SERVER_164="$SSH_USER@$TM_PROD_HOST"
REMOTE_PATH="/opt/timemaster"
REMOTE_VENV="$REMOTE_PATH/backend/venv"
+21
View File
@@ -10,6 +10,7 @@
#
# Frontend: wird NICHT hier gebaut (kein Node auf den Servern, dist ist
# gitignored). Frontend liefert der Dev-Rechner separat per push-frontend.sh.
# Lokal aufrufbar über: ./deploy.sh <dev|prod> [--frontend]
#
# Usage (auf dem Server):
# cd /opt/timemaster && ./server-update.sh
@@ -56,6 +57,26 @@ else
warn "Migration übersprungen (--no-migrate)"
fi
# ── 2b. Systemkonfiguration aus dem Repo (idempotent) ─────────────────────────
id timemaster >/dev/null 2>&1 || { log "Systemuser timemaster anlegen"; useradd --system --no-create-home --shell /usr/sbin/nologin timemaster; }
[[ -f backend/.env ]] && { chown root:timemaster backend/.env; chmod 640 backend/.env; }
UNIT_CHANGED=false
if ! cmp -s "$REMOTE/timemaster.service" /etc/systemd/system/timemaster.service; then
log "systemd-Unit aus Repo übernehmen"
install -m 644 "$REMOTE/timemaster.service" /etc/systemd/system/timemaster.service
UNIT_CHANGED=true
fi
if command -v visudo >/dev/null && ! cmp -s "$REMOTE/timemaster.sudoers" /etc/sudoers.d/timemaster 2>/dev/null; then
log "sudo-Regel aus Repo übernehmen"
install -d -m 755 /etc/sudoers.d
install -m 440 "$REMOTE/timemaster.sudoers" /etc/sudoers.d/timemaster.new
visudo -cf /etc/sudoers.d/timemaster.new >/dev/null && mv /etc/sudoers.d/timemaster.new /etc/sudoers.d/timemaster \
|| { rm -f /etc/sudoers.d/timemaster.new; warn "sudoers ungültig – nicht übernommen"; }
elif ! command -v visudo >/dev/null; then
warn "sudo nicht installiert – TLS-Erneuerung aus der App nicht möglich (apt install sudo)"
fi
$UNIT_CHANGED && systemctl daemon-reload
# ── 3. Service neustarten ─────────────────────────────────────────────────────
log "systemctl restart $SERVICE"
systemctl restart "$SERVICE"
+43 -8
View File
@@ -1,10 +1,18 @@
#!/bin/bash
# ============================================================
# TimeMaster – Nativer Server-Setup (Debian 13)
# Führe dieses Script als root oder mit sudo aus
# Voraussetzung: Repo liegt unter /opt/timemaster
# git clone <repo-url> /opt/timemaster
# Führe dieses Script als root aus: bash /opt/timemaster/setup_server.sh
# Erzeugt DB-Passwort, SECRET_KEY(_DATA) und backend/.env automatisch (nur wenn
# .env noch fehlt), richtet Systemuser, systemd-Unit und sudo-Regel aus dem Repo ein.
# ============================================================
set -e
[[ $EUID -eq 0 ]] || { echo "Bitte als root ausführen." >&2; exit 1; }
cd /opt/timemaster 2>/dev/null || { echo "/opt/timemaster fehlt – Repo dorthin klonen." >&2; exit 1; }
ENV_FILE=/opt/timemaster/backend/.env
echo "==> [1/7] System-Pakete aktualisieren"
apt-get update && apt-get upgrade -y
@@ -14,17 +22,23 @@ apt-get install -y \
postgresql postgresql-contrib \
redis-server \
nginx \
git curl build-essential libpq-dev
git curl build-essential libpq-dev openssl sudo \
libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz0b
echo "==> [3/7] PostgreSQL einrichten"
systemctl enable postgresql && systemctl start postgresql
# Datenbank + User anlegen (Produktiv-DB + Test-DB)
sudo -u postgres psql <<SQL
# Passwort: aus vorhandener .env übernehmen, sonst neu erzeugen
if [[ -f "$ENV_FILE" ]]; then
DB_PASS="$(sed -nE 's#^DATABASE_URL=[^:]+://[^:]+:([^@]+)@.*#\1#p' "$ENV_FILE" | head -1)"
fi
DB_PASS="${DB_PASS:-$(openssl rand -hex 24)}"
runuser -u postgres -- psql <<SQL
DO \$\$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'timemaster') THEN
CREATE ROLE timemaster LOGIN PASSWORD 'timemaster_secret_change_me';
CREATE ROLE timemaster LOGIN PASSWORD '${DB_PASS}';
END IF;
END
\$\$;
@@ -39,6 +53,28 @@ SQL
echo "==> [4/7] Redis einrichten"
systemctl enable redis-server && systemctl start redis-server
echo "==> [4b] Systemuser, .env, systemd-Unit und sudo-Regel aus dem Repo"
id timemaster >/dev/null 2>&1 || useradd --system --no-create-home --shell /usr/sbin/nologin timemaster
if [[ ! -f "$ENV_FILE" ]]; then
IP="$(hostname -I | awk '{print $1}')"
umask 077
cat > "$ENV_FILE" <<ENV
APP_ENV=production
SECRET_KEY=$(openssl rand -hex 32)
SECRET_KEY_DATA=$(openssl rand -hex 32)
DATABASE_URL=postgresql+asyncpg://timemaster:${DB_PASS}@localhost:5432/timemaster_db
REDIS_URL=redis://localhost:6379/0
FRONTEND_URL=http://${IP}
ENV
echo " .env neu erzeugt (SECRET_KEY_DATA nach dem ersten Start NIE mehr ändern!)"
echo " Optional ergänzen: ALLOWED_HOSTS, RESEND_API_KEY, EMAIL_FROM, FRONTEND_URL (https)"
fi
chown root:timemaster "$ENV_FILE"; chmod 640 "$ENV_FILE"
install -d -m 755 /etc/sudoers.d
install -m 440 /opt/timemaster/timemaster.sudoers /etc/sudoers.d/timemaster && visudo -cf /etc/sudoers.d/timemaster
install -m 644 /opt/timemaster/timemaster.service /etc/systemd/system/timemaster.service
systemctl daemon-reload && systemctl enable timemaster
echo "==> [5/7] Python venv + Abhängigkeiten"
cd /opt/timemaster/backend
python3 -m venv venv
@@ -158,15 +194,14 @@ echo ""
echo "==> [Optional] Tests ausführen"
echo " python -m pytest tests/ -q"
echo ""
systemctl restart timemaster && sleep 3 && systemctl is-active timemaster
echo "✓ Setup abgeschlossen!"
echo " Backend: sudo systemctl start timemaster"
echo " Backend: läuft (systemctl status timemaster)"
echo " Frontend: http://$(hostname -I | awk '{print $1}')/"
echo " API-Docs: http://$(hostname -I | awk '{print $1}')/docs (und /redoc)"
echo ""
echo ""
echo "Frontend deployen (nach lokalem Build):"
echo " npm run build (im frontend/ Verzeichnis)"
echo " rsync -avz dist/ root@SERVER:/opt/timemaster/frontend/dist/"
echo "Frontend deployen (lokal, vom Dev-Rechner): ./deploy.sh <dev|prod> --frontend"
echo ""
echo "Hinweise:"
echo " - passlib wurde durch direktes bcrypt ersetzt (kompatibel mit bcrypt >= 4.0)"