From b62bba01737e7da7ec61852a53af42254924d008 Mon Sep 17 00:00:00 2001 From: schn33fuchs Date: Tue, 14 Jul 2026 19:23:14 +0200 Subject: [PATCH] Docker shit --- .dockerignore | 13 ++ backend/Cargo.toml | 2 +- backend/src/main.rs | 8 ++ docker/Dockerfile | 127 ++++++++++++++++++ docker/Dockerfile.backend | 66 +++++++++ docker/compose.yaml | 48 ++++++- frontend/src/darkmode.rs | 43 +++++- frontend/src/pages/setup.rs | 2 + frontend/src/styles/_variables.scss | 5 + frontend/src/styles/components/_darkmode.scss | 8 +- frontend/src/styles/components/_sidebar.scss | 6 + frontend/src/styles/main.scss | 13 ++ 12 files changed, 334 insertions(+), 7 deletions(-) create mode 100644 .dockerignore create mode 100644 docker/Dockerfile create mode 100644 docker/Dockerfile.backend diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c4f90c8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +target/ +backend/target/ +frontend/target/ +frontend/dist/ +frontend/node_modules/ +dist/ +.git/ +.idea/ +.env +.antigravitycli/ +*.pdb +**/*.rs.bk +info ding video.mp4 diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 5e8dd6b..f43c09e 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -8,7 +8,7 @@ axum = "0.8.9" axum-extra = { version = "0.12.6", features = ["cookie", "typed-header", "form"] } serde = { workspace = true } serde_json = { workspace = true } -sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio", "tls-native-tls", "chrono"] } +sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio", "tls-native-tls", "chrono", "migrate"] } tokio = { version = "1.52.1", features = ["rt-multi-thread", "macros"] } dotenv = "0.15.0" chrono = { workspace = true } diff --git a/backend/src/main.rs b/backend/src/main.rs index be3dba6..8997909 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -77,6 +77,14 @@ async fn main() { } }; + // Run database migrations on container/application startup + println!("Running database migrations..."); + if let Err(err) = sqlx::migrate!("./migrations").run(&pool).await { + println!("Failed to run database migrations: {:?}", err); + std::process::exit(1); + } + println!("Database migrations completed successfully"); + // Configure CORS to allow requests from frontend let cors = CorsLayer::new() .allow_origin(env.origin.parse::().unwrap()) diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..c19906e --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,127 @@ +# ============================================================ +# Dockerfile — Full application (frontend + backend) +# ============================================================ +# Multi-stage build: +# 1. chef – install cargo-chef for dependency caching +# 2. planner – generate recipe.json from the workspace +# 3. backend – build the backend binary (release) +# 4. frontend – build the frontend WASM bundle with Trunk +# 5. runtime – minimal image: backend binary + static frontend + nginx +# ============================================================ + +# --------------- Stage 1: Chef base --------------- +FROM rust:1-bookworm AS chef +RUN cargo install cargo-chef +WORKDIR /app + +# --------------- Stage 2: Planner --------------- +FROM chef AS planner +COPY Cargo.toml Cargo.lock ./ +COPY src/ src/ +COPY backend/ backend/ +COPY frontend/ frontend/ +RUN cargo chef prepare --recipe-path recipe.json + +# --------------- Stage 3: Backend builder --------------- +FROM chef AS backend-builder + +COPY --from=planner /app/recipe.json recipe.json +RUN cargo chef cook --release --recipe-path recipe.json -p backend + +COPY Cargo.toml Cargo.lock ./ +COPY src/ src/ +COPY backend/ backend/ +COPY frontend/ frontend/ +RUN cargo build --release -p backend + +# --------------- Stage 4: Frontend builder --------------- +FROM chef AS frontend-builder + +# Add the WASM target +RUN rustup target add wasm32-unknown-unknown + +# Install Trunk (the WASM bundler used by the frontend) and sass (for SCSS) +RUN cargo install trunk + +# Cache frontend dependencies +COPY --from=planner /app/recipe.json recipe.json +RUN cargo chef cook --release --target wasm32-unknown-unknown --recipe-path recipe.json -p frontend + +# Copy full workspace source for the build +COPY Cargo.toml Cargo.lock ./ +COPY src/ src/ +COPY backend/ backend/ +COPY frontend/ frontend/ + +# Build the frontend WASM bundle +# Trunk outputs to frontend/dist by default +WORKDIR /app/frontend +RUN trunk build --release + +# --------------- Stage 5: Runtime --------------- +FROM debian:bookworm-slim AS runtime + +# Install runtime dependencies: +# - ca-certificates & libssl3: TLS for PostgreSQL connections +# - nginx: serves the frontend static files and reverse-proxies /api to the backend +RUN apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates libssl3 nginx && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy the compiled backend binary +COPY --from=backend-builder /app/target/release/backend /app/backend + +# Copy migrations +COPY backend/migrations/ /app/migrations/ + +# Copy the frontend static build output +COPY --from=frontend-builder /app/frontend/dist /var/www/html + +# Nginx configuration: serve frontend + reverse-proxy /api to the backend +RUN cat > /etc/nginx/sites-available/default <<'EOF' +server { + listen 80; + server_name _; + + root /var/www/html; + index index.html; + + # Serve static frontend files, fall back to index.html for client-side routing + location / { + try_files $uri $uri/ /index.html; + } + + # Reverse-proxy API requests to the Axum backend + location /api/ { + proxy_pass http://127.0.0.1:8001; + 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; + } +} +EOF + +# Startup script: launch backend in the background, then nginx in the foreground +RUN cat > /app/start.sh <<'SCRIPT' +#!/bin/bash +set -e + +# Start the backend API server in the background +/app/backend & + +# Start nginx in the foreground (keeps the container alive) +nginx -g 'daemon off;' +SCRIPT +RUN chmod +x /app/start.sh + +EXPOSE 80 + +# Environment variables should be provided at runtime: +# DATABASE_URL – PostgreSQL connection string +# TOKEN_SECRET – JWT signing key +# ORIGIN – Allowed CORS origin (should be the public URL of this container) + +CMD ["/app/start.sh"] diff --git a/docker/Dockerfile.backend b/docker/Dockerfile.backend new file mode 100644 index 0000000..421da8a --- /dev/null +++ b/docker/Dockerfile.backend @@ -0,0 +1,66 @@ +# ============================================================ +# Dockerfile.backend — Backend only +# ============================================================ +# Multi-stage build: +# 1. chef – install cargo-chef for dependency caching +# 2. planner – generate a recipe.json from the workspace +# 3. builder – build dependencies first (cached), then the backend binary +# 4. runtime – minimal image with just the compiled binary +# ============================================================ + +# --------------- Stage 1: Chef base --------------- +FROM rust:1-bookworm AS chef +RUN cargo install cargo-chef +WORKDIR /app + +# --------------- Stage 2: Planner --------------- +FROM chef AS planner +# Copy the full workspace so cargo-chef can resolve all dependencies +COPY Cargo.toml Cargo.lock ./ +COPY src/ src/ +COPY backend/ backend/ +COPY frontend/ frontend/ +RUN cargo chef prepare --recipe-path recipe.json + +# --------------- Stage 3: Builder --------------- +FROM chef AS builder + +# Cache dependency compilation in its own layer +COPY --from=planner /app/recipe.json recipe.json +RUN cargo chef cook --release --recipe-path recipe.json -p backend + +# Now copy the real source and build +COPY Cargo.toml Cargo.lock ./ +COPY src/ src/ +COPY backend/ backend/ +COPY frontend/ frontend/ +RUN cargo build --release -p backend + +# --------------- Stage 4: Runtime --------------- +FROM debian:bookworm-slim AS runtime + +# Install TLS root certificates and PostgreSQL client libs (needed by sqlx +# at runtime for TLS connections via native-tls) +RUN apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates libssl3 && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy the compiled binary +COPY --from=builder /app/target/release/backend /app/backend + +# Copy migrations so they can be run at startup if needed +COPY backend/migrations/ /app/migrations/ + +EXPOSE 8001 + +# Environment variables should be provided at runtime via docker run --env +# or docker compose environment/env_file. +# +# Required: +# DATABASE_URL – PostgreSQL connection string +# TOKEN_SECRET – JWT signing key +# ORIGIN – Allowed CORS origin (frontend URL) + +CMD ["/app/backend"] diff --git a/docker/compose.yaml b/docker/compose.yaml index 41b46e2..b92cd90 100644 --- a/docker/compose.yaml +++ b/docker/compose.yaml @@ -6,9 +6,53 @@ services: ports: - 5432:5432 environment: - - POSTGRES_PASSWORD=tickets + POSTGRES_PASSWORD: tickets + POSTGRES_DB: system_data volumes: - - pg_data:/var/lib/postregsql/pg_data + - pg_data:/var/lib/postgresql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 3s + retries: 5 + + # Full application (frontend + backend + nginx) + # Usage: docker compose --profile full up --build + app: + profiles: ["full"] + build: + context: .. + dockerfile: docker/Dockerfile + container_name: ticketsystem + restart: unless-stopped + ports: + - 8080:80 + environment: + DATABASE_URL: postgres://postgres:tickets@postgres:5432/system_data + TOKEN_SECRET: 160257c8c5ff2298363529e963a5901d2efa6d6ae67d634487c4d2bbc41c533f + ORIGIN: http://localhost:8080 + depends_on: + postgres: + condition: service_healthy + + # Backend only (API server) + # Usage: docker compose --profile backend up --build + backend: + profiles: ["backend"] + build: + context: .. + dockerfile: docker/Dockerfile.backend + container_name: ticketsystem-backend + restart: unless-stopped + ports: + - 8001:8001 + environment: + DATABASE_URL: postgres://postgres:tickets@postgres:5432/system_data + TOKEN_SECRET: 160257c8c5ff2298363529e963a5901d2efa6d6ae67d634487c4d2bbc41c533f + ORIGIN: http://localhost:8000 + depends_on: + postgres: + condition: service_healthy volumes: pg_data: diff --git a/frontend/src/darkmode.rs b/frontend/src/darkmode.rs index 3c2ed48..4d7fc5e 100644 --- a/frontend/src/darkmode.rs +++ b/frontend/src/darkmode.rs @@ -19,23 +19,60 @@ use yew::prelude::*; /// ``` #[function_component] pub fn ThemeToggle() -> Html { + let is_dark = use_state(|| { + if let Some(window) = web_sys::window() { + if let Some(document) = window.document() { + if let Some(html) = document.document_element() { + return html.get_attribute("data-theme").unwrap_or_default() == "dark"; + } + } + } + false + }); + let onclick = { - Callback::from(|_| { + let is_dark = is_dark.clone(); + Callback::from(move |_| { if let Some(window) = web_sys::window() { if let Some(document) = window.document() { if let Some(html) = document.document_element() { let current = html.get_attribute("data-theme").unwrap_or_default(); let new_theme = if current == "dark" { "" } else { "dark" }; let _ = html.set_attribute("data-theme", new_theme); + is_dark.set(new_theme == "dark"); } } } }) }; + let icon_html = if *is_dark { + // Sun SVG for dark mode + html! { + + + + + + + + + + + + } + } else { + // Moon SVG for light mode + html! { + + + + } + }; + html! { - } } diff --git a/frontend/src/pages/setup.rs b/frontend/src/pages/setup.rs index 9aa86ab..339f1b0 100644 --- a/frontend/src/pages/setup.rs +++ b/frontend/src/pages/setup.rs @@ -20,6 +20,7 @@ pub struct AdminSetupScheme { pub first_name: String, pub last_name: String, pub username: String, + pub is_admin: bool, pub pwd: String, } @@ -154,6 +155,7 @@ pub fn initial_admin_setup() -> Html { first_name: first_name_val, last_name: last_name_val, username: username_val, + is_admin: true, pwd: pwd_val, }; diff --git a/frontend/src/styles/_variables.scss b/frontend/src/styles/_variables.scss index 0983579..c64bf39 100644 --- a/frontend/src/styles/_variables.scss +++ b/frontend/src/styles/_variables.scss @@ -11,6 +11,8 @@ --color: #000000; --color-container: #f0f0f0; --color-text: #000000; + --color-placeholder: #757575; + --color-input: #1a1a1a; } [data-theme='dark'] { @@ -26,4 +28,7 @@ --color: #000000; --color-container: #1a1a1a; --color-text: #000000; + --color-placeholder: #a0a0a0; + --color-input: #f0f0f0; } + diff --git a/frontend/src/styles/components/_darkmode.scss b/frontend/src/styles/components/_darkmode.scss index e31aa1b..53cb26c 100644 --- a/frontend/src/styles/components/_darkmode.scss +++ b/frontend/src/styles/components/_darkmode.scss @@ -6,11 +6,17 @@ padding: 0.75rem; font-size: 1rem; border-radius: 0.5rem; - color: white; + color: var(--color-input); border: none; cursor: pointer; margin-bottom: 15px; } + + input::placeholder, + textarea::placeholder { + color: var(--color-placeholder); + opacity: 1; + } button:hover { cursor: pointer; diff --git a/frontend/src/styles/components/_sidebar.scss b/frontend/src/styles/components/_sidebar.scss index ff0e52b..f218d8b 100644 --- a/frontend/src/styles/components/_sidebar.scss +++ b/frontend/src/styles/components/_sidebar.scss @@ -55,6 +55,12 @@ cursor: pointer; text-align: left; margin-top: var(--spacing-sm); + + &.theme-toggle-btn { + display: flex; + justify-content: center; + align-items: center; + } } .sidebar-header { diff --git a/frontend/src/styles/main.scss b/frontend/src/styles/main.scss index 319fa2a..c346755 100644 --- a/frontend/src/styles/main.scss +++ b/frontend/src/styles/main.scss @@ -90,10 +90,12 @@ body { padding: 16px; font-size: 1.8rem; background-color: #f0f0f0; + color: var(--color-input); box-shadow: #6d6d6d; box-shadow: 6px 6px 8px; } + input[type="checkbox"] { width: auto; } @@ -123,4 +125,15 @@ body { box-sizing: border-box; } +input, select, textarea { + color: var(--color-input); +} + +input::placeholder, +textarea::placeholder { + color: var(--color-placeholder); + opacity: 1; +} + +