diff --git a/backend/Dockerfile b/backend/Dockerfile index c4e6519..23f7c10 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -2,16 +2,12 @@ FROM python:3.12-slim WORKDIR /app -# uv installieren RUN pip install uv -# Nur dependency files zuerst kopieren (Docker Cache!) COPY pyproject.toml uv.lock ./ - RUN uv sync --frozen --no-dev -# Rest kopieren -COPY app.py . +COPY . . -EXPOSE 5000 -CMD ["uv", "run", "app.py"] +EXPOSE 8000 +CMD ["uv", "run", "gunicorn", "-b", "0.0.0.0:8000", "app:app"] diff --git a/backend/__pycache__/app.cpython-312.pyc b/backend/__pycache__/app.cpython-312.pyc new file mode 100644 index 0000000..934e241 Binary files /dev/null and b/backend/__pycache__/app.cpython-312.pyc differ diff --git a/backend/app.py b/backend/app.py index 6e8530c..25819d1 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,67 +1,346 @@ import os import sqlite3 +import uuid from datetime import datetime -from flask import Flask, request, jsonify +from functools import wraps +from pathlib import Path + +from flask import ( + Flask, + flash, + g, + redirect, + render_template, + request, + send_from_directory, + session, + url_for, +) +from werkzeug.utils import secure_filename app = Flask(__name__) +base_dir = Path(__file__).resolve().parent +app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "change-me-in-production") +app.config["DB_PATH"] = os.environ.get("DB_PATH", str(base_dir / "app.sqlite3")) +app.config["UPLOAD_FOLDER"] = os.environ.get("UPLOAD_FOLDER", str(base_dir / "uploads")) +app.config["EVENT_PASSWORD"] = os.environ.get("EVENT_PASSWORD", "wedding2026") +app.config["MAX_CONTENT_LENGTH"] = int(os.environ.get("MAX_UPLOAD_BYTES", str(8 * 1024 * 1024))) +app.config["LOCATION_NAME"] = os.environ.get("LOCATION_NAME", "Schlossgarten Venue") +app.config["LOCATION_ADDRESS"] = os.environ.get( + "LOCATION_ADDRESS", "Musterstraße 12, 12345 Musterstadt" +) +app.config["LOCATION_WEBSITE_URL"] = os.environ.get( + "LOCATION_WEBSITE_URL", "https://example.com/location" +) +app.config["GOOGLE_MAPS_EMBED_URL"] = os.environ.get( + "GOOGLE_MAPS_EMBED_URL", + "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d0", +) -DB_PATH = os.environ.get("DB_PATH", "/app/db/app.sqlite3") +ALLOWED_EXTENSIONS = {"jpg", "jpeg", "png"} -def get_db(): - os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) - conn = sqlite3.connect(DB_PATH) - conn.row_factory = sqlite3.Row - return conn +TEXTS = { + "de": { + "brand": "Svenja & Dominic", + "subtitle": "Willkommen zu unserer Hochzeits-App", + "login": "Login", + "name": "Dein Name", + "event_password": "Event-Passwort", + "login_submit": "Weiter zum Dashboard", + "dashboard": "Dashboard", + "logout": "Abmelden", + "rsvp": "RSVP", + "upload": "Upload", + "gallery": "Galerie", + "info": "Infos", + "save": "Speichern", + "attending": "Ich komme", + "not_attending": "Ich komme nicht", + "plus_one": "Ich bringe eine Begleitperson mit", + "file": "Bild auswählen", + "upload_submit": "Foto hochladen", + "schedule": "Ablauf", + "hotels": "Hotels", + "taxi": "Taxi", + "location": "Location", + "visit_location": "Zur Location-Webseite", + }, + "en": { + "brand": "Svenja & Dominic", + "subtitle": "Welcome to our wedding app", + "login": "Login", + "name": "Your name", + "event_password": "Event password", + "login_submit": "Open dashboard", + "dashboard": "Dashboard", + "logout": "Logout", + "rsvp": "RSVP", + "upload": "Upload", + "gallery": "Gallery", + "info": "Info", + "save": "Save", + "attending": "I will attend", + "not_attending": "I cannot attend", + "plus_one": "I will bring a plus-one", + "file": "Select image", + "upload_submit": "Upload photo", + "schedule": "Schedule", + "hotels": "Hotels", + "taxi": "Taxi", + "location": "Location", + "visit_location": "Visit location website", + }, +} -def init_db(): - with get_db() as conn: - conn.execute(""" - CREATE TABLE IF NOT EXISTS rsvps ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - attending INTEGER NOT NULL, -- 1 = ja, 0 = nein - plus_one INTEGER NOT NULL DEFAULT 0, -- 1 = ja, 0 = nein - created_at TEXT NOT NULL - ); - """) + +def get_lang() -> str: + lang = session.get("lang", "de") + return lang if lang in TEXTS else "de" + + +def t(key: str) -> str: + return TEXTS[get_lang()].get(key, key) + + +@app.context_processor +def inject_common() -> dict: + return { + "t": t, + "lang": get_lang(), + "guest_name": session.get("guest_name"), + "location_name": app.config["LOCATION_NAME"], + "location_address": app.config["LOCATION_ADDRESS"], + "location_website_url": app.config["LOCATION_WEBSITE_URL"], + "google_maps_embed_url": app.config["GOOGLE_MAPS_EMBED_URL"], + } + + +def get_db() -> sqlite3.Connection: + db_path = app.config["DB_PATH"] + os.makedirs(os.path.dirname(db_path), exist_ok=True) + + if "db" not in g: + g.db = sqlite3.connect(db_path) + g.db.row_factory = sqlite3.Row + return g.db + + +@app.teardown_appcontext +def close_db(_error) -> None: + db = g.pop("db", None) + if db is not None: + db.close() + + +def init_db() -> None: + db_path = app.config["DB_PATH"] + db_dir = os.path.dirname(db_path) + if db_dir: + os.makedirs(db_dir, exist_ok=True) + + with sqlite3.connect(db_path) as conn: + conn.execute( + """ + CREATE TABLE IF NOT EXISTS guests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + attending INTEGER, + plus_one INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL + ) + """ + ) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS uploads ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + filename TEXT NOT NULL, + uploaded_by INTEGER NOT NULL, + uploaded_at TEXT NOT NULL, + FOREIGN KEY(uploaded_by) REFERENCES guests(id) + ) + """ + ) conn.commit() + +def login_required(view): + @wraps(view) + def wrapped(*args, **kwargs): + if "guest_id" not in session: + return redirect(url_for("landing")) + return view(*args, **kwargs) + + return wrapped + + +def is_allowed_file(filename: str) -> bool: + return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS + + +def upsert_guest(name: str) -> int: + now = datetime.utcnow().isoformat() + db = get_db() + row = db.execute("SELECT id FROM guests WHERE name = ?", (name,)).fetchone() + if row: + return int(row["id"]) + + cursor = db.execute( + "INSERT INTO guests (name, created_at) VALUES (?, ?)", + (name, now), + ) + db.commit() + return int(cursor.lastrowid) + + @app.get("/health") def health(): return {"status": "ok"} -@app.post("/api/rsvp") -def create_rsvp(): - data = request.get_json(force=True, silent=True) or {} - name = (data.get("name") or "").strip() - attending = data.get("attending") - plus_one = data.get("plus_one", 0) + +@app.get("/") +def landing(): + if "guest_id" in session: + return redirect(url_for("dashboard")) + return render_template("login.html") + + +@app.post("/login") +def login(): + name = (request.form.get("name") or "").strip() + event_password = request.form.get("event_password") or "" if not name: - return jsonify({"error": "name is required"}), 400 - if attending not in (0, 1, True, False): - return jsonify({"error": "attending must be 0/1"}), 400 + flash("Bitte Namen eingeben.") + return redirect(url_for("landing")) - attending_int = 1 if attending in (1, True) else 0 - plus_one_int = 1 if plus_one in (1, True) else 0 + if event_password != app.config["EVENT_PASSWORD"]: + flash("Ungültiges Event-Passwort.") + return redirect(url_for("landing")) - with get_db() as conn: - conn.execute( - "INSERT INTO rsvps (name, attending, plus_one, created_at) VALUES (?, ?, ?, ?)", - (name, attending_int, plus_one_int, datetime.utcnow().isoformat()) + guest_id = upsert_guest(name) + session["guest_id"] = guest_id + session["guest_name"] = name + return redirect(url_for("dashboard")) + + +@app.post("/logout") +def logout(): + session.clear() + return redirect(url_for("landing")) + + +@app.post("/lang/") +def set_lang(code: str): + if code in TEXTS: + session["lang"] = code + return redirect(request.referrer or url_for("landing")) + + +@app.get("/dashboard") +@login_required +def dashboard(): + return render_template("dashboard.html") + + +@app.route("/rsvp", methods=["GET", "POST"]) +@login_required +def rsvp(): + db = get_db() + + if request.method == "POST": + attending_raw = request.form.get("attending") + plus_one = 1 if request.form.get("plus_one") == "on" else 0 + + if attending_raw not in {"yes", "no"}: + flash("Bitte eine RSVP-Auswahl treffen.") + return redirect(url_for("rsvp")) + + attending = 1 if attending_raw == "yes" else 0 + if not attending: + plus_one = 0 + + db.execute( + "UPDATE guests SET attending = ?, plus_one = ? WHERE id = ?", + (attending, plus_one, session["guest_id"]), ) - conn.commit() + db.commit() + flash("RSVP gespeichert.") + return redirect(url_for("rsvp")) - return jsonify({"ok": True}) + guest = db.execute( + "SELECT attending, plus_one FROM guests WHERE id = ?", + (session["guest_id"],), + ).fetchone() -@app.get("/api/rsvps") -def list_rsvps(): - with get_db() as conn: - rows = conn.execute( - "SELECT id, name, attending, plus_one, created_at FROM rsvps ORDER BY id DESC" - ).fetchall() - return jsonify([dict(r) for r in rows]) + return render_template("rsvp.html", guest=guest) + + +@app.route("/upload", methods=["GET", "POST"]) +@login_required +def upload(): + if request.method == "POST": + file = request.files.get("photo") + if file is None or file.filename == "": + flash("Bitte eine Bilddatei auswählen.") + return redirect(url_for("upload")) + + if not is_allowed_file(file.filename): + flash("Nur JPG/JPEG/PNG sind erlaubt.") + return redirect(url_for("upload")) + + safe_name = secure_filename(file.filename) + ext = safe_name.rsplit(".", 1)[1].lower() + stored_name = f"{uuid.uuid4().hex}.{ext}" + + upload_dir = app.config["UPLOAD_FOLDER"] + os.makedirs(upload_dir, exist_ok=True) + file.save(os.path.join(upload_dir, stored_name)) + + db = get_db() + db.execute( + "INSERT INTO uploads (filename, uploaded_by, uploaded_at) VALUES (?, ?, ?)", + (stored_name, session["guest_id"], datetime.utcnow().isoformat()), + ) + db.commit() + + flash("Upload erfolgreich.") + return redirect(url_for("gallery")) + + return render_template("upload.html") + + +@app.get("/gallery") +@login_required +def gallery(): + db = get_db() + images = db.execute( + """ + SELECT uploads.filename, uploads.uploaded_at, guests.name AS uploaded_by + FROM uploads + JOIN guests ON guests.id = uploads.uploaded_by + ORDER BY uploads.id DESC + """ + ).fetchall() + return render_template("gallery.html", images=images) + + +@app.get("/uploads/") +@login_required +def serve_upload(filename: str): + return send_from_directory(app.config["UPLOAD_FOLDER"], filename) + + +@app.get("/info/") +@login_required +def info(page: str): + allowed = {"schedule", "hotels", "taxi", "location"} + if page not in allowed: + return redirect(url_for("dashboard")) + return render_template("info.html", page=page) + + +init_db() if __name__ == "__main__": - init_db() - app.run(host="0.0.0.0", port=5000) + app.run(host="0.0.0.0", port=8000) diff --git a/backend/app.sqlite3 b/backend/app.sqlite3 new file mode 100644 index 0000000..711cf41 Binary files /dev/null and b/backend/app.sqlite3 differ diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 2657804..0be2c9c 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -6,4 +6,5 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [ "flask>=3.1.2", + "gunicorn>=23.0.0", ] diff --git a/backend/static/styles.css b/backend/static/styles.css new file mode 100644 index 0000000..7fc2036 --- /dev/null +++ b/backend/static/styles.css @@ -0,0 +1,172 @@ +:root { + --cream: #f8f2e8; + --beige: #efe3d3; + --forest: #274235; + --gold: #b8914c; + --ink: #1f1f1f; + --card: #fffdf9; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + color: var(--ink); + font-family: "Source Sans 3", sans-serif; + background: radial-gradient(circle at top, #fff, var(--cream)); +} + +h1, +h2, +h3 { + font-family: "Playfair Display", serif; + margin-top: 0; +} + +.topbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + border-bottom: 1px solid rgba(39, 66, 53, 0.12); + background: rgba(255, 255, 255, 0.82); + backdrop-filter: blur(4px); +} + +.brand { + color: var(--forest); + text-decoration: none; + font-size: 1.2rem; + font-weight: 700; +} + +.host { + color: rgba(31, 31, 31, 0.7); + font-size: 0.82rem; +} + +.toolbar { + display: flex; + gap: 0.4rem; +} + +.container { + width: min(960px, 92vw); + margin: 1.5rem auto 3rem; +} + +.card { + background: var(--card); + border: 1px solid rgba(39, 66, 53, 0.1); + border-radius: 18px; + box-shadow: 0 10px 32px rgba(39, 66, 53, 0.08); + padding: 1.25rem; + margin-bottom: 1rem; +} + +.hero { + background: linear-gradient(145deg, #fff, var(--beige)); +} + +.card-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 0.8rem; +} + +.link-card { + color: var(--forest); + text-decoration: none; + display: flex; + align-items: center; + justify-content: center; + min-height: 90px; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.link-card:hover { + transform: translateY(-3px); + box-shadow: 0 14px 30px rgba(39, 66, 53, 0.16); +} + +.form-card { + max-width: 560px; +} + +.form-grid { + display: grid; + gap: 0.8rem; +} + +input[type="text"], +input[type="password"], +input[type="file"] { + width: 100%; + margin-top: 0.25rem; + border: 1px solid #d9ceb9; + border-radius: 12px; + padding: 0.65rem 0.75rem; + background: #fff; +} + +.radio-row { + display: flex; + align-items: center; + gap: 0.45rem; +} + +.btn { + display: inline-block; + border: 0; + border-radius: 12px; + padding: 0.6rem 0.9rem; + cursor: pointer; + color: #fff; + background: var(--forest); + text-decoration: none; + transition: filter 0.2s ease; +} + +.btn:hover { + filter: brightness(1.08); +} + +.btn-ghost { + color: var(--forest); + background: transparent; + border: 1px solid rgba(39, 66, 53, 0.2); +} + +.flash { + padding: 0.7rem 0.9rem; + border-radius: 10px; + background: #f2f7f3; + border: 1px solid rgba(39, 66, 53, 0.2); +} + +.gallery-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 0.8rem; +} + +.gallery-item { + margin: 0; +} + +.gallery-item img { + width: 100%; + aspect-ratio: 4 / 3; + object-fit: cover; + border-radius: 12px; +} + +.map-wrap iframe { + width: 100%; + min-height: 320px; + border: 0; + border-radius: 12px; + margin: 0.8rem 0; +} diff --git a/backend/templates/base.html b/backend/templates/base.html new file mode 100644 index 0000000..cadf4c0 --- /dev/null +++ b/backend/templates/base.html @@ -0,0 +1,45 @@ + + + + + + {{ t('brand') }} + + + + + + +
+
+ {{ t('brand') }} +
{{ request.host }}
+
+
+
+ +
+
+ +
+ {% if guest_name %} +
+ +
+ {% endif %} +
+
+ +
+ {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} +

{{ message }}

+ {% endfor %} + {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+ + diff --git a/backend/templates/dashboard.html b/backend/templates/dashboard.html new file mode 100644 index 0000000..df95904 --- /dev/null +++ b/backend/templates/dashboard.html @@ -0,0 +1,17 @@ +{% extends 'base.html' %} +{% block content %} +
+

{{ t('dashboard') }}

+

Hallo {{ guest_name }}.

+
+ +
+ {{ t('rsvp') }} + {{ t('upload') }} + {{ t('gallery') }} + {{ t('schedule') }} + {{ t('hotels') }} + {{ t('taxi') }} + {{ t('location') }} +
+{% endblock %} diff --git a/backend/templates/gallery.html b/backend/templates/gallery.html new file mode 100644 index 0000000..8ea1148 --- /dev/null +++ b/backend/templates/gallery.html @@ -0,0 +1,20 @@ +{% extends 'base.html' %} +{% block content %} +
+

{{ t('gallery') }}

+ {% if images %} + + {% else %} +

Noch keine Bilder vorhanden.

+ {% endif %} +
+{% endblock %} diff --git a/backend/templates/info.html b/backend/templates/info.html new file mode 100644 index 0000000..52afbbd --- /dev/null +++ b/backend/templates/info.html @@ -0,0 +1,31 @@ +{% extends 'base.html' %} +{% block content %} +
+

+ {% if page == 'schedule' %}{{ t('schedule') }}{% endif %} + {% if page == 'hotels' %}{{ t('hotels') }}{% endif %} + {% if page == 'taxi' %}{{ t('taxi') }}{% endif %} + {% if page == 'location' %}{{ t('location') }}{% endif %} +

+ + {% if page == 'schedule' %} +

15:00 Trauung, 17:00 Empfang, 19:00 Dinner.

+ {% elif page == 'hotels' %} +

Empfehlungen folgen. Bitte frühzeitig buchen.

+ {% elif page == 'taxi' %} +

Taxi-Service: 01234 / 567890 (24/7).

+ {% elif page == 'location' %} +

{{ location_name }}

+

{{ location_address }}

+
+ +
+ {{ t('visit_location') }} + {% endif %} +
+{% endblock %} diff --git a/backend/templates/login.html b/backend/templates/login.html new file mode 100644 index 0000000..b666b32 --- /dev/null +++ b/backend/templates/login.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} +{% block content %} +
+

{{ t('subtitle') }}

+

Passwortgeschützter Zugriff für unsere Gäste.

+
+ +
+

{{ t('login') }}

+
+ + + + + +
+
+{% endblock %} diff --git a/backend/templates/rsvp.html b/backend/templates/rsvp.html new file mode 100644 index 0000000..efa8acd --- /dev/null +++ b/backend/templates/rsvp.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} +{% block content %} +
+

{{ t('rsvp') }}

+
+ + + + + + + +
+
+{% endblock %} diff --git a/backend/templates/upload.html b/backend/templates/upload.html new file mode 100644 index 0000000..38376aa --- /dev/null +++ b/backend/templates/upload.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} +{% block content %} +
+

{{ t('upload') }}

+
+ + +
+
+{% endblock %} diff --git a/backend/uv.lock b/backend/uv.lock index 08b774e..6294d82 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -8,10 +8,14 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "flask" }, + { name = "gunicorn" }, ] [package.metadata] -requires-dist = [{ name = "flask", specifier = ">=3.1.2" }] +requires-dist = [ + { name = "flask", specifier = ">=3.1.2" }, + { name = "gunicorn", specifier = ">=23.0.0" }, +] [[package]] name = "blinker" @@ -60,6 +64,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, ] +[[package]] +name = "gunicorn" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/13/ef67f59f6a7896fdc2c1d62b5665c5219d6b0a9a1784938eb9a28e55e128/gunicorn-25.1.0.tar.gz", hash = "sha256:1426611d959fa77e7de89f8c0f32eed6aa03ee735f98c01efba3e281b1c47616", size = 594377, upload-time = "2026-02-13T11:09:58.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/73/4ad5b1f6a2e21cf1e85afdaad2b7b1a933985e2f5d679147a1953aaa192c/gunicorn-25.1.0-py3-none-any.whl", hash = "sha256:d0b1236ccf27f72cfe14bce7caadf467186f19e865094ca84221424e839b8b8b", size = 197067, upload-time = "2026-02-13T11:09:57.146Z" }, +] + [[package]] name = "itsdangerous" version = "2.2.0" @@ -144,6 +160,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + [[package]] name = "werkzeug" version = "3.1.5" diff --git a/data/db/app.sqlite3 b/data/db/app.sqlite3 index 13a7023..746c425 100644 Binary files a/data/db/app.sqlite3 and b/data/db/app.sqlite3 differ diff --git a/docker-compose.yml b/docker-compose.yml index c620681..4cafd89 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,16 +6,21 @@ services: ports: - "127.0.0.1:8080:80" volumes: - - ./frontend/public:/usr/share/nginx/html:ro + - ./frontend/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + - backend backend: build: ./backend container_name: wedding-backend restart: unless-stopped ports: - - "127.0.0.1:5000:5000" + - "127.0.0.1:5000:8000" volumes: - ./data/uploads:/app/uploads - ./data/db:/app/db environment: - DB_PATH=/app/db/app.sqlite3 + - UPLOAD_FOLDER=/app/uploads + - EVENT_PASSWORD=${EVENT_PASSWORD:-wedding2026} + - SECRET_KEY=${SECRET_KEY:-change-me-in-production} diff --git a/frontend/nginx/default.conf b/frontend/nginx/default.conf new file mode 100644 index 0000000..605a77f --- /dev/null +++ b/frontend/nginx/default.conf @@ -0,0 +1,15 @@ +server { + listen 80; + server_name _; + + client_max_body_size 10M; + + location / { + proxy_pass http://backend: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; + } +} diff --git a/frontend/public/index.html b/frontend/public/index.html index 7de1aa7..82e28c6 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -2,10 +2,19 @@ - - Svenja & Dominic + + Wedding App + - -

Hallo Welt 👋

+ +
+

Svenja & Dominic

+

Nginx ist erreichbar. Die App läuft im Backend.

+ Zum Login +