Files
timemaster/setup_server.sh
T
patrickandClaude Sonnet 5.5 7e165d37b6
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
feat(deploy): deploy.sh + lokale Host-Konfig, server-update/setup_server übernehmen Unit und sudoers aus dem Repo
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 09:40:35 +01:00

222 lines
10 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# ============================================================
# TimeMaster – Nativer Server-Setup (Debian 13)
# 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
echo "==> [2/7] Abhängigkeiten installieren"
apt-get install -y \
python3 python3-venv python3-dev python3-pip \
postgresql postgresql-contrib \
redis-server \
nginx \
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)
# 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 '${DB_PASS}';
END IF;
END
\$\$;
SELECT 'CREATE DATABASE timemaster_db OWNER timemaster'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'timemaster_db')\gexec
SELECT 'CREATE DATABASE timemaster_test OWNER timemaster'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'timemaster_test')\gexec
GRANT ALL PRIVILEGES ON DATABASE timemaster_db TO timemaster;
GRANT ALL PRIVILEGES ON DATABASE timemaster_test TO timemaster;
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
source venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
echo "==> [6/7] Alembic Migrations ausführen"
alembic upgrade head
echo "==> [7/7] nginx für Frontend konfigurieren"
mkdir -p /opt/timemaster/frontend/dist /opt/timemaster/backend/static
cat > /etc/nginx/sites-available/timemaster << 'NGINX'
# HTTP-only Konfiguration (SSL/HTTPS noch nicht eingerichtet)
# Sobald ein TLS-Zertifikat vorhanden ist:
# 1. Listen-Block auf 443 ssl http2 erweitern
# 2. ssl_certificate / ssl_certificate_key einkommentieren
# 3. HSTS-Header hinzufügen
# 4. HTTP->HTTPS-Redirect aktivieren
server {
listen 80;
server_name _;
client_max_body_size 20M;
# API Backend
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none';" always;
}
# FastAPI Docs (nur in dev aktiv – docs_url ist None in Production)
location /docs {
proxy_pass http://127.0.0.1:8000/docs;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none';" always;
}
location /redoc {
proxy_pass http://127.0.0.1:8000/redoc;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none';" always;
}
location /openapi.json {
proxy_pass http://127.0.0.1:8000/openapi.json;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none';" always;
}
# React Frontend (statische Dateien)
# HINWEIS: nginx-Regel: add_header in einem location-Block ueberschreibt
# alle add_header-Direktiven des parent server-Blocks. Daher Security-Header
# in jede location wiederholen.
location / {
root /opt/timemaster/frontend/dist;
index index.html;
try_files $uri $uri/ /index.html;
expires 1d;
add_header Cache-Control "public, must-revalidate";
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none';" always;
}
# Swagger/ReDoc-Assets (app/static/swagger-ui im Repo) + Uploads
location /static/ {
alias /opt/timemaster/backend/static/;
expires 7d;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none';" always;
}
}
NGINX
ln -sf /etc/nginx/sites-available/timemaster /etc/nginx/sites-enabled/timemaster
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl enable nginx && systemctl reload nginx
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: 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 (lokal, vom Dev-Rechner): ./deploy.sh <dev|prod> --frontend"
echo ""
echo "Hinweise:"
echo " - passlib wurde durch direktes bcrypt ersetzt (kompatibel mit bcrypt >= 4.0)"
echo " - pytest nutzt PostgreSQL test-DB (timemaster_test) – kein SQLite"
echo " - pytest.ini: asyncio_mode=auto, loop_scope=session für alle Fixtures"
echo " - openpyxl >= 3.1 für XLSX-Export (Dashboard/Reports)"
echo ""
echo "Verfügbare API-Endpunkte:"
echo " GET /api/v1/dashboard/me – Mitarbeiter-Dashboard"
echo " GET /api/v1/dashboard/team – Team-Dashboard (Manager+)"
echo " GET /api/v1/dashboard/company – Unternehmens-Dashboard (Admin)"
echo " GET /api/v1/reports/time – Zeiterfassungsbericht"
echo " GET /api/v1/reports/absences – Abwesenheitsbericht"
echo " GET /api/v1/reports/overtime – Überstundenbericht"
echo " GET /api/v1/reports/time/export?format=csv|xlsx"
echo " GET /api/v1/reports/absences/export?format=csv|xlsx"
echo " GET /api/v1/reports/overtime/export?format=csv|xlsx"