diff --git a/.gitignore b/.gitignore index 65b5ab2..016b59e 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,3 @@ pnpm-debug.log* # jetbrains setting folder .idea/ -/ai/ diff --git a/.woodpecker.yml b/.woodpecker.yml index 1316125..d6afc02 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -1,11 +1,16 @@ steps: - deploy_frontend: + deploy: image: node:20 - secrets: [FLY_API_TOKEN] + environment: + FLY_API_TOKEN: + from_secret: FLY_API_TOKEN commands: - curl -L https://fly.io/install.sh | sh - export PATH="$HOME/.fly/bin:$PATH" - - flyctl deploy --config fly.toml --app gallus-pub --remote-only - when: - branch: main - event: push \ No newline at end of file + - flyctl deploy --config fly.toml --app gallus-pub + +when: + branch: + - main + event: + - push diff --git a/Dockerfile b/Dockerfile index a9c26c5..5e532af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,8 +2,7 @@ FROM node:20-alpine AS build WORKDIR /app COPY package*.json ./ -# Fallback to npm install if no lockfile is present -RUN npm ci || npm install +RUN npm ci COPY . . # Ensure CSS variables are present RUN mkdir -p public/styles @@ -17,8 +16,7 @@ RUN npm install -g serve COPY --from=build /app/dist ./dist EXPOSE 3000 -# Serve static files (no SPA fallback), so /admin serves dist/admin/index.html -CMD ["serve", "-l", "3000", "dist"] +CMD ["serve", "-s", "dist", "-l", "3000"] HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD wget -qO- http://localhost:3000/ || exit 1 \ No newline at end of file diff --git a/Dockerfile.caddy b/Dockerfile.caddy deleted file mode 100644 index e63695c..0000000 --- a/Dockerfile.caddy +++ /dev/null @@ -1,9 +0,0 @@ -FROM caddy:2-alpine - -# Embed Caddyfile directly to avoid host path issues on Windows -RUN mkdir -p /etc/caddy \ - && printf ":80\nlog\nroute {\n handle /api/* {\n reverse_proxy backend:8080\n }\n handle {\n reverse_proxy frontend:3000\n }\n}\n" > /etc/caddy/Caddyfile - -EXPOSE 80 - -CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] diff --git a/MIGRATION_README.md b/MIGRATION_README.md deleted file mode 100644 index 194b1c6..0000000 --- a/MIGRATION_README.md +++ /dev/null @@ -1,142 +0,0 @@ -# Migration der alten Events und Gallery-Bilder - -## ✅ Was wurde migriert? - -### Events (7 Stück): -- Karaoke (wiederkehrend) -- Pub Quiz (wiederkehrend) -- Schlager Hüttenzauber Karaoke -- Adventskalender -- Santa Karaoke-Party -- Weihnachtsferien -- Neujahrs-Apero - -### Gallery-Bilder (9 Stück): -- Gallery1.webp bis Gallery9.webp - -## 📁 Wo liegen die Bilder? - -Alle Bilder wurden konvertiert und liegen jetzt in: -- **Events:** `backend/data/images/events/` -- **Gallery:** `backend/data/images/gallery/` - -Die Bilder wurden automatisch: -- Von PNG/JPG/JPEG zu WebP konvertiert -- Auf max. 1600px Breite skaliert -- Mit 85% Qualität optimiert - -## 🚀 Deployment-Schritte - -### 1. Lokale Vorbereitung (bereits erledigt ✓) -- ✓ Migrations-Script erstellt -- ✓ Bilder konvertiert und in `backend/data/images/` kopiert -- ✓ Public API-Endpunkte erstellt (`/api/events/public`, `/api/gallery/public`) -- ✓ Frontend aktualisiert, um Events und Gallery dynamisch zu laden - -### 2. Auf Fly.io deployen - -Alle Änderungen committen und pushen: -```bash -git add . -git commit -m "feat: Migrate old events and gallery images to CMS" -git push origin main -``` - -Woodpecker CI wird automatisch beide Services deployen. - -### 3. Nach dem ersten Deploy - Datenbank initialisieren - -**Wichtig:** Die Bilder sind bereits im Repository in `backend/data/images/`, aber die Datenbank muss noch mit den Event- und Gallery-Einträgen befüllt werden. - -#### Via fly ssh (Empfohlen): - -```bash -# In das Backend einloggen -fly ssh console -a gallus-cms-backend - -# Prüfen ob Bilder da sind -ls -la /app/data/images/events/ -ls -la /app/data/images/gallery/ - -# Migrations-Script ausführen -cd /app -npm run migrate:old-data -``` - -#### Alternative: Manuell via Admin-Panel - -1. Gehe zu https://gallus-pub.ch/admin -2. Melde dich an -3. Für jedes Event: - - Klicke auf "Neues Event" - - Gib Titel, Datum und Beschreibung ein - - Statt Bild hochzuladen, trage manuell die imageUrl ein: - - z.B. `/images/events/event_karaoke.webp` - - Speichere das Event - -## 🔍 Verifikation - -Nach dem Deployment prüfen: - -1. **Frontend:** https://gallus-pub.ch/ - - Events sollten angezeigt werden - - Gallery sollte Bilder zeigen - -2. **Admin:** https://gallus-pub.ch/admin - - Events können bearbeitet werden - - Neue Events können hinzugefügt werden - -3. **Backend Health:** https://cms.gallus-pub.ch/health - - Status sollte "ok" sein - -## 📝 Event-Daten für manuelles Einfügen - -Falls du die Events manuell via Admin-Panel einfügen möchtest: - -### Karaoke -- **Titel:** Karaoke -- **Datum:** 2025-12-31 -- **Beschreibung:** Bei uns gibt es Karaoke Mi-Sa!!
Seid ihr eine Gruppe und lieber unter euch? ..unseren 2.Stock kannst du auch mieten ;)
Reserviere am besten gleich per Whatsapp 077 232 27 70 -- **Bild-URL:** `/images/events/event_karaoke.webp` - -### Pub Quiz -- **Titel:** Pub Quiz -- **Datum:** 2025-12-31 -- **Beschreibung:** Jeden Freitag findet unser Pub Quiz statt. Gespielt wird tischweise in 3-4 Runden.
Jede Woche gibt es ein anderes Thema. Es geht um Ruhm und Ehre und zusätzlich werden die Sieger der Herzen durch das Publikum gekürt! <3
Auch Einzelpersonen sind herzlich willkommen!
*zum mitmachen minimum 1 Getränk konsumieren oder 5CHF -- **Bild-URL:** `/images/events/event_pub-quiz.webp` - -### Schlager Hüttenzauber Karaoke -- **Titel:** Schlager Hüttenzauber Karaoke -- **Datum:** 2025-11-27 -- **Beschreibung:** Ab 19:00 Uhr Eintritt ist Frei! Reservieren unter 077 232 27 70 -- **Bild-URL:** `/images/events/event_schlager-karaoke.webp` - -### Adventskalender -- **Titel:** Adventskalender -- **Datum:** 2025-12-20 -- **Beschreibung:** Jeden Tag neue Überraschungen! Check unsere Social Media Stories! -- **Bild-URL:** `/images/events/event_advents-kalender.webp` - -### Santa Karaoke-Party -- **Titel:** Santa Karaoke-Party -- **Datum:** 2025-12-06 -- **Beschreibung:** 🤶🏻🎅🏻Komme als Weihnachts-Mann/-Frau und bekomme einen Shot auf's Haus!🤶🏻🎅🏻 -- **Bild-URL:** `/images/events/event_santa_karaoke.webp` - -### Weihnachtsferien -- **Titel:** Weihnachtsferien -- **Datum:** 2025-12-21 -- **Beschreibung:** Wir sind ab 02.01.2026 wieder wie gewohnt für euch da! 🍀.
Für Anfragen WA 077 232 27 70 Antwort innerhalb 48h -- **Bild-URL:** `/images/events/event_ferien.webp` - -### Neujahrs-Apero -- **Titel:** Neujahrs-Apero -- **Datum:** 2026-01-02 -- **Beschreibung:** 18:00-20:00 Uhr -- **Bild-URL:** `/images/events/event_neujahrs-apero.webp` - -## ⚠️ Wichtige Hinweise - -1. **Bilder sind im Volume persistent:** Alle Bilder in `/app/data/` bleiben bei Restarts erhalten -2. **Datenbank ist persistent:** Die SQLite-DB in `/app/data/gallus_cms.db` bleibt erhalten -3. **Alte Bilder in `public/images/`:** Die alten Original-Bilder bleiben im Frontend-Repository, werden aber nicht mehr verwendet diff --git a/README.md b/README.md index edd28d9..e34a99b 100644 --- a/README.md +++ b/README.md @@ -45,4 +45,3 @@ All commands are run from the root of the project, from a terminal: ## 👀 Want to learn more? Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). -# Test commit to trigger Woodpecker diff --git a/backend/.env.local b/backend/.env.local deleted file mode 100644 index ce01029..0000000 --- a/backend/.env.local +++ /dev/null @@ -1,34 +0,0 @@ -# Local development environment for Gallus CMS Backend - -# Database -DB_CLIENT=sqlite -DATABASE_URL= -DATABASE_PATH=./data/gallus_cms.db - -# Gitea OAuth -GITEA_URL=https://git.bookageek.ch -GITEA_CLIENT_ID=bcddfe24-3099-41bc-bb7a-6c6d80bd9048 -GITEA_CLIENT_SECRET=gto_me7hsswrsdq2ygey65edlc6qa2xxr3i5nrq7q4jvjwx654ytrh7q -# Frontend proxy callback in local dev -GITEA_REDIRECT_URI=http://localhost:4321/api/auth/callback -GITEA_ALLOWED_USERS=Gallus-maintanance - -# Git repository for content versioning -GIT_REPO_URL=https://git.bookageek.ch/Kenzo/Gallus_Pub -GIT_TOKEN=1482ae7bcdbd7610bf0cfd468b6757722d16a2a2 -GIT_USER_NAME=Gallus-maintanance -GIT_USER_EMAIL=Admin@gallus-pub.ch -GIT_WORKSPACE_DIR=./data/workspace - -# JWT & Session secrets (use strong random strings in real deployments) -JWT_SECRET=local-dev-jwt-secret-please-change-1234567890abcdef -SESSION_SECRET=local-dev-session-secret-please-change-abcdef1234567890 - -# Server & CORS -PORT=3000 -NODE_ENV=development -FRONTEND_URL=http://localhost:4321 -CORS_ORIGIN=http://localhost:4321 - -# Upload limits -MAX_FILE_SIZE=5242880 diff --git a/backend/.gitignore b/backend/.gitignore index 7814f45..e2f0e3e 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -4,9 +4,7 @@ dist *.log .DS_Store /tmp -/data/*.db -/data/*.db-wal -/data/*.db-shm -/data/workspace -# Allow images to be committed -!/data/images +/data +*.db +*.db-wal +*.db-shm diff --git a/backend/DEPLOYMENT.md b/backend/DEPLOYMENT.md index 72720bd..ba60365 100644 --- a/backend/DEPLOYMENT.md +++ b/backend/DEPLOYMENT.md @@ -1,6 +1,6 @@ # Deployment Guide -## Prerequisite +## Prerequisites 1. Fly.io CLI installed: `curl -L https://fly.io/install.sh | sh` 2. Fly.io account: `flyctl auth login` diff --git a/backend/Dockerfile b/backend/Dockerfile index aafe7b1..a3c3db2 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -10,8 +10,7 @@ RUN apk add --no-cache python3 make g++ # Install dependencies COPY package*.json ./ -# Use npm ci when lockfile exists, fallback to npm install for local/dev -RUN npm ci || npm install +RUN npm ci # Copy source COPY . . @@ -27,8 +26,17 @@ WORKDIR /app # Install runtime dependencies (git for simple-git, sqlite3 CLI tool) RUN apk add --no-cache git sqlite -# Copy production dependencies from builder (already compiled native modules) -COPY --from=builder /app/node_modules ./node_modules +# Install build dependencies for better-sqlite3 (needed for npm ci) +RUN apk add --no-cache python3 make g++ + +# Copy package files +COPY package*.json ./ + +# Install production dependencies only +RUN npm ci --production + +# Remove build dependencies after install +RUN apk del python3 make g++ # Copy built files from builder COPY --from=builder /app/dist ./dist @@ -55,5 +63,5 @@ ENV DATABASE_PATH=/app/data/gallus_cms.db HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD node -e "require('http').get('http://localhost:8080/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" -# Run DB migrations if present, then start application -CMD ["/bin/sh", "-lc", "[ -f dist/migrate.js ] && node dist/migrate.js || true; node dist/index.js"] +# Start application +CMD ["node", "dist/index.js"] diff --git a/backend/README.md b/backend/README.md index 29dcb78..8aef3e7 100644 --- a/backend/README.md +++ b/backend/README.md @@ -52,4 +52,4 @@ See parent directory for complete documentation: - `CMS_CONCEPT.md` - System architecture - `CMS_GITEA_AUTH.md` - Authentication details - `CMS_IMPLEMENTATION_EXAMPLE.md` - Code examples -- `CMS_SETUP_GUIDE.md` - Deployment guide +- `CMS_SETUP_GUIDE.md` - Deployment guide diff --git a/backend/data/images/events/event_advents-kalender.webp b/backend/data/images/events/event_advents-kalender.webp deleted file mode 100644 index 35f826a..0000000 Binary files a/backend/data/images/events/event_advents-kalender.webp and /dev/null differ diff --git a/backend/data/images/events/event_ferien.webp b/backend/data/images/events/event_ferien.webp deleted file mode 100644 index c282cd7..0000000 Binary files a/backend/data/images/events/event_ferien.webp and /dev/null differ diff --git a/backend/data/images/events/event_karaoke.webp b/backend/data/images/events/event_karaoke.webp deleted file mode 100644 index 4912672..0000000 Binary files a/backend/data/images/events/event_karaoke.webp and /dev/null differ diff --git a/backend/data/images/events/event_neujahrs-apero.webp b/backend/data/images/events/event_neujahrs-apero.webp deleted file mode 100644 index 4e1c1f2..0000000 Binary files a/backend/data/images/events/event_neujahrs-apero.webp and /dev/null differ diff --git a/backend/data/images/events/event_pub-quiz.webp b/backend/data/images/events/event_pub-quiz.webp deleted file mode 100644 index 4936ecb..0000000 Binary files a/backend/data/images/events/event_pub-quiz.webp and /dev/null differ diff --git a/backend/data/images/events/event_santa_karaoke.webp b/backend/data/images/events/event_santa_karaoke.webp deleted file mode 100644 index 129cf5a..0000000 Binary files a/backend/data/images/events/event_santa_karaoke.webp and /dev/null differ diff --git a/backend/data/images/events/event_schlager-karaoke.webp b/backend/data/images/events/event_schlager-karaoke.webp deleted file mode 100644 index 2ca22d9..0000000 Binary files a/backend/data/images/events/event_schlager-karaoke.webp and /dev/null differ diff --git a/backend/data/images/gallery/Gallery1.webp b/backend/data/images/gallery/Gallery1.webp deleted file mode 100644 index cc23860..0000000 Binary files a/backend/data/images/gallery/Gallery1.webp and /dev/null differ diff --git a/backend/data/images/gallery/Gallery2.webp b/backend/data/images/gallery/Gallery2.webp deleted file mode 100644 index 75e5de4..0000000 Binary files a/backend/data/images/gallery/Gallery2.webp and /dev/null differ diff --git a/backend/data/images/gallery/Gallery3.webp b/backend/data/images/gallery/Gallery3.webp deleted file mode 100644 index 8641d8d..0000000 Binary files a/backend/data/images/gallery/Gallery3.webp and /dev/null differ diff --git a/backend/data/images/gallery/Gallery4.webp b/backend/data/images/gallery/Gallery4.webp deleted file mode 100644 index bfb6c4e..0000000 Binary files a/backend/data/images/gallery/Gallery4.webp and /dev/null differ diff --git a/backend/data/images/gallery/Gallery5.webp b/backend/data/images/gallery/Gallery5.webp deleted file mode 100644 index ba13919..0000000 Binary files a/backend/data/images/gallery/Gallery5.webp and /dev/null differ diff --git a/backend/data/images/gallery/Gallery6.webp b/backend/data/images/gallery/Gallery6.webp deleted file mode 100644 index 6cdb18f..0000000 Binary files a/backend/data/images/gallery/Gallery6.webp and /dev/null differ diff --git a/backend/data/images/gallery/Gallery7.webp b/backend/data/images/gallery/Gallery7.webp deleted file mode 100644 index 77f19f7..0000000 Binary files a/backend/data/images/gallery/Gallery7.webp and /dev/null differ diff --git a/backend/data/images/gallery/Gallery8.webp b/backend/data/images/gallery/Gallery8.webp deleted file mode 100644 index 7926875..0000000 Binary files a/backend/data/images/gallery/Gallery8.webp and /dev/null differ diff --git a/backend/data/images/gallery/Gallery9.webp b/backend/data/images/gallery/Gallery9.webp deleted file mode 100644 index 3d2860c..0000000 Binary files a/backend/data/images/gallery/Gallery9.webp and /dev/null differ diff --git a/backend/fly.toml b/backend/fly.toml index 9d57e39..07438fd 100644 --- a/backend/fly.toml +++ b/backend/fly.toml @@ -3,8 +3,6 @@ app = "gallus-cms-backend" primary_region = "ams" [build] - # Ensure Fly uses the Dockerfile in this backend directory - dockerfile = "Dockerfile" [env] PORT = "8080" @@ -12,10 +10,6 @@ primary_region = "ams" GITEA_URL = "https://git.bookageek.ch" DATABASE_PATH = "/app/data/gallus_cms.db" GIT_WORKSPACE_DIR = "/app/data/workspace" - # Cross-site frontend and OAuth - FRONTEND_URL = "https://gallus-pub.ch" - CORS_ORIGIN = "https://gallus-pub.ch" - GITEA_REDIRECT_URI = "https://cms.gallus-pub.ch/api/auth/callback" [http_service] internal_port = 8080 diff --git a/backend/package.json b/backend/package.json index 7f5e169..1e837c8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -9,15 +9,14 @@ "start": "node dist/index.js", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", - "db:studio": "drizzle-kit studio", - "migrate:old-data": "tsx src/scripts/migrate-old-data.ts" + "db:studio": "drizzle-kit studio" }, "dependencies": { "@fastify/cookie": "^9.3.1", "@fastify/cors": "^9.0.1", "@fastify/jwt": "^8.0.0", - "@fastify/static": "^6.12.0", "@fastify/multipart": "^8.1.0", + "@fastify/session": "^10.8.0", "bcrypt": "^5.1.1", "better-sqlite3": "^11.10.0", "drizzle-orm": "^0.33.0", diff --git a/backend/src/config/database.ts b/backend/src/config/database.ts index ec263d4..48b064f 100644 --- a/backend/src/config/database.ts +++ b/backend/src/config/database.ts @@ -2,103 +2,14 @@ import { drizzle } from 'drizzle-orm/better-sqlite3'; import Database from 'better-sqlite3'; import * as schema from '../db/schema.js'; import { env } from './env.js'; -import fs from 'fs'; -import path from 'path'; if (!env.DATABASE_PATH) { throw new Error('DATABASE_PATH environment variable is not set'); } -// Ensure directory exists BEFORE opening the database file -const dbDir = path.dirname(env.DATABASE_PATH); -if (!fs.existsSync(dbDir)) { - fs.mkdirSync(dbDir, { recursive: true }); -} - const sqlite = new Database(env.DATABASE_PATH); // Enable WAL mode for better concurrent access sqlite.pragma('journal_mode = WAL'); export const db = drizzle(sqlite, { schema }); - -// Auto-create tables if they don't exist -export function initDatabase() { - console.log('🔧 Initializing database...'); - - try { - // Check if users table exists (acts as a sentinel for initial setup) - const tableCheck = sqlite - .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='users'") - .get(); - - if (!tableCheck) { - console.log('📝 Creating database schema...'); - - sqlite.exec(` - PRAGMA foreign_keys = ON; - - CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, - gitea_id TEXT UNIQUE NOT NULL, - gitea_username TEXT NOT NULL, - gitea_email TEXT, - display_name TEXT, - avatar_url TEXT, - role TEXT DEFAULT 'admin', - created_at INTEGER DEFAULT (unixepoch()), - last_login INTEGER - ); - - CREATE TABLE IF NOT EXISTS events ( - id TEXT PRIMARY KEY, - title TEXT NOT NULL, - date TEXT NOT NULL, - description TEXT NOT NULL, - image_url TEXT NOT NULL, - display_order INTEGER NOT NULL, - is_published INTEGER DEFAULT 1, - created_at INTEGER DEFAULT (unixepoch()), - updated_at INTEGER DEFAULT (unixepoch()) - ); - - CREATE TABLE IF NOT EXISTS gallery_images ( - id TEXT PRIMARY KEY, - image_url TEXT NOT NULL, - alt_text TEXT NOT NULL, - display_order INTEGER NOT NULL, - is_published INTEGER DEFAULT 1, - created_at INTEGER DEFAULT (unixepoch()) - ); - - CREATE TABLE IF NOT EXISTS content_sections ( - id TEXT PRIMARY KEY, - section_name TEXT UNIQUE NOT NULL, - content_json TEXT NOT NULL, - updated_at INTEGER DEFAULT (unixepoch()) - ); - - CREATE TABLE IF NOT EXISTS site_settings ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL, - updated_at INTEGER DEFAULT (unixepoch()) - ); - - CREATE TABLE IF NOT EXISTS publish_history ( - id TEXT PRIMARY KEY, - user_id TEXT REFERENCES users(id), - commit_hash TEXT, - commit_message TEXT, - published_at INTEGER DEFAULT (unixepoch()) - ); - `); - - console.log('✅ Database schema created successfully!'); - } else { - console.log('✅ Database already initialized.'); - } - } catch (error) { - console.error('❌ Error initializing database:', error); - throw error; - } -} diff --git a/backend/src/index.ts b/backend/src/index.ts index 403cf73..884d4c1 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -3,11 +3,9 @@ import cors from '@fastify/cors'; import jwt from '@fastify/jwt'; import multipart from '@fastify/multipart'; import cookie from '@fastify/cookie'; +import session from '@fastify/session'; import { authenticate } from './middleware/auth.middleware.js'; import { env, validateEnv } from './config/env.js'; -import { db, initDatabase } from './config/database.js'; -import fastifyStatic from '@fastify/static'; -import path from 'path'; // Import routes import authRoute from './routes/auth.js'; @@ -46,12 +44,17 @@ fastify.register(cors, { fastify.register(cookie); +fastify.register(session, { + secret: env.SESSION_SECRET, + cookie: { + secure: env.NODE_ENV === 'production', + httpOnly: true, + maxAge: 600000, // 10 minutes (only needed for OAuth flow) + }, +}); + fastify.register(jwt, { secret: env.JWT_SECRET, - cookie: { - cookieName: 'token', - signed: false, - }, }); fastify.register(multipart, { @@ -60,14 +63,6 @@ fastify.register(multipart, { }, }); -// Serve static files (uploaded images, etc.) from persistent volume -const dataDir = env.GIT_WORKSPACE_DIR || path.join(process.cwd(), 'data'); -fastify.register(fastifyStatic, { - root: dataDir, - prefix: '/static/', - decorateReply: false -}); - // Decorate fastify with authenticate method fastify.decorate('authenticate', authenticate); @@ -110,8 +105,6 @@ fastify.setErrorHandler((error, request, reply) => { // Start server const start = async () => { try { - // Initialize database before starting server - initDatabase(); await fastify.listen({ port: env.PORT, host: '0.0.0.0' }); console.log(`🚀 Server listening on port ${env.PORT}`); console.log(`📝 Environment: ${env.NODE_ENV}`); diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 8b737bd..f5625f6 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -6,15 +6,10 @@ import { eq } from 'drizzle-orm'; import { GiteaService } from '../services/gitea.service.js'; import { env } from '../config/env.js'; -// Use explicit JSON schema for Fastify route validation to avoid provider issues -const callbackQueryJsonSchema = { - type: 'object', - required: ['code', 'state'], - properties: { - code: { type: 'string' }, - state: { type: 'string' }, - }, -} as const; +const callbackSchema = z.object({ + code: z.string(), + state: z.string(), +}); const authRoute: FastifyPluginAsync = async (fastify) => { const giteaService = new GiteaService(); @@ -27,14 +22,8 @@ const authRoute: FastifyPluginAsync = async (fastify) => { // Generate CSRF state token const state = giteaService.generateState(); - // Store state in a short-lived cookie - reply.setCookie('oauth_state', state, { - path: '/', - httpOnly: true, - sameSite: 'none', - secure: true, - maxAge: 10 * 60, // 10 minutes - }); + // Store state in session + request.session.set('oauth_state', state); // Generate authorization URL const authUrl = giteaService.getAuthorizationUrl(state); @@ -49,20 +38,20 @@ const authRoute: FastifyPluginAsync = async (fastify) => { */ fastify.get('/auth/callback', { schema: { - querystring: callbackQueryJsonSchema, + querystring: callbackSchema, }, }, async (request, reply) => { try { - const { code, state } = request.query as { code: string; state: string }; + const { code, state } = request.query as z.infer; - // Verify CSRF state from cookie - const expectedState = request.cookies?.oauth_state as string | undefined; + // Verify CSRF state + const expectedState = request.session.get('oauth_state'); if (!expectedState || state !== expectedState) { - return reply.code(400).send({ error: 'Invalid state parameter' }); + return reply.code(400).send({ error: 'Invalid state parameter' }); } - // Clear state cookie - reply.clearCookie('oauth_state', { path: '/' }); + // Clear state from session + request.session.delete('oauth_state'); // Exchange code for access token const tokenResponse = await giteaService.exchangeCodeForToken(code); @@ -114,28 +103,18 @@ const authRoute: FastifyPluginAsync = async (fastify) => { { id: user.id, giteaId: user.giteaId, - username: user.giteaUsername || '', - role: user.role ?? 'admin', + username: user.giteaUsername, + role: user.role, }, { expiresIn: '24h' } ); - // Also set token as HttpOnly cookie so subsequent API calls authenticate reliably - // Cross-site admin (gallus-pub.ch) -> backend (cms.gallus-pub.ch) requires SameSite=None & Secure in production - reply.setCookie('token', token, { - path: '/', - httpOnly: true, - sameSite: (env.NODE_ENV === 'production' ? 'none' : 'lax'), - secure: (env.NODE_ENV === 'production') || (!!env.FRONTEND_URL && env.FRONTEND_URL.startsWith('https')), - maxAge: 60 * 60 * 24, // 24h - }); - - // Redirect to admin dashboard + // Redirect to frontend with token const frontendUrl = env.FRONTEND_URL; - return reply.redirect(`${frontendUrl}/admin`); + return reply.redirect(`${frontendUrl}/auth/callback?token=${token}`); } catch (error) { - fastify.log.error({ err: error }, 'OAuth callback error'); + fastify.log.error('OAuth callback error:', error); return reply.code(500).send({ error: 'Authentication failed' }); } }); @@ -160,14 +139,12 @@ const authRoute: FastifyPluginAsync = async (fastify) => { } return { - user: { - id: user.id, - giteaUsername: user.giteaUsername, - giteaEmail: user.giteaEmail, - displayName: user.displayName, - avatarUrl: user.avatarUrl, - role: user.role, - }, + id: user.id, + username: user.giteaUsername, + email: user.giteaEmail, + displayName: user.displayName, + avatarUrl: user.avatarUrl, + role: user.role, }; }); @@ -180,7 +157,6 @@ const authRoute: FastifyPluginAsync = async (fastify) => { }, async (request, reply) => { // For JWT, logout is primarily client-side (delete token) // You could maintain a token blacklist in Redis for production - reply.clearCookie('token', { path: '/' }); return { message: 'Logged out successfully' }; }); }; diff --git a/backend/src/routes/content.ts b/backend/src/routes/content.ts index 47c2081..6ce19c4 100644 --- a/backend/src/routes/content.ts +++ b/backend/src/routes/content.ts @@ -4,14 +4,9 @@ import { db } from '../config/database.js'; import { contentSections } from '../db/schema.js'; import { eq } from 'drizzle-orm'; -// Fastify JSON schema for content section body -const contentBodyJsonSchema = { - type: 'object', - required: ['contentJson'], - properties: { - contentJson: {}, // allow any JSON - }, -} as const; +const contentSectionSchema = z.object({ + contentJson: z.record(z.any()), +}); const contentRoute: FastifyPluginAsync = async (fastify) => { @@ -41,12 +36,12 @@ const contentRoute: FastifyPluginAsync = async (fastify) => { // Update content section fastify.put('/content/:section', { schema: { - body: contentBodyJsonSchema, + body: contentSectionSchema, }, preHandler: [fastify.authenticate], }, async (request, reply) => { const { section } = request.params as { section: string }; - const { contentJson } = request.body as any; + const { contentJson } = request.body as z.infer; // Check if section exists const [existing] = await db @@ -92,7 +87,7 @@ const contentRoute: FastifyPluginAsync = async (fastify) => { const sections = await db.select().from(contentSections); return { - sections: (sections as any[]).map((s: any) => ({ + sections: sections.map(s => ({ section: s.sectionName, content: s.contentJson, updatedAt: s.updatedAt, diff --git a/backend/src/routes/events.ts b/backend/src/routes/events.ts index 8e1553e..7e19920 100644 --- a/backend/src/routes/events.ts +++ b/backend/src/routes/events.ts @@ -1,95 +1,121 @@ import { FastifyPluginAsync } from 'fastify'; +import { z } from 'zod'; import { db } from '../config/database.js'; import { events } from '../db/schema.js'; import { eq } from 'drizzle-orm'; -// Fastify JSON schema for event body -const eventBodyJsonSchema = { - type: 'object', - required: ['title', 'date', 'description', 'imageUrl', 'displayOrder'], - properties: { - title: { type: 'string', minLength: 1, maxLength: 200 }, - date: { type: 'string', minLength: 1, maxLength: 100 }, - description: { type: 'string', minLength: 1 }, - imageUrl: { type: 'string', minLength: 1 }, - displayOrder: { type: 'integer', minimum: 0 }, - isPublished: { type: 'boolean' }, - }, -} as const; - -const reorderBodyJsonSchema = { - type: 'object', - required: ['orders'], - properties: { - orders: { - type: 'array', - items: { - type: 'object', - required: ['id', 'displayOrder'], - properties: { - id: { type: 'string' }, - displayOrder: { type: 'integer', minimum: 0 }, - }, - }, - }, - }, -} as const; +const eventSchema = z.object({ + title: z.string().min(1).max(200), + date: z.string().min(1).max(100), + description: z.string().min(1), + imageUrl: z.string().url(), + displayOrder: z.number().int().min(0), + isPublished: z.boolean().optional().default(true), +}); const eventsRoute: FastifyPluginAsync = async (fastify) => { - // PUBLIC: List published events (no auth required) - fastify.get('/events/public', async () => { - const all = await db.select().from(events) - .where(eq(events.isPublished, true)) - .orderBy(events.displayOrder); - return { events: all }; - }); - // List all events (by displayOrder) - admin only - fastify.get('/events', { preHandler: [fastify.authenticate] }, async () => { - const all = await db.select().from(events).orderBy(events.displayOrder); - return { events: all }; + // List all events + fastify.get('/events', { + preHandler: [fastify.authenticate], + }, async (request, reply) => { + const allEvents = await db.select().from(events).orderBy(events.displayOrder); + return { events: allEvents }; }); // Get single event - fastify.get('/events/:id', { preHandler: [fastify.authenticate] }, async (request, reply) => { + fastify.get('/events/:id', { + preHandler: [fastify.authenticate], + }, async (request, reply) => { const { id } = request.params as { id: string }; - const rows = await db.select().from(events).where(eq(events.id, id)).limit(1); - if (rows.length === 0) return reply.code(404).send({ error: 'Event not found' }); - return { event: rows[0] }; + const event = await db.select().from(events).where(eq(events.id, id)).limit(1); + + if (event.length === 0) { + return reply.code(404).send({ error: 'Event not found' }); + } + + return { event: event[0] }; }); // Create event - fastify.post('/events', { schema: { body: eventBodyJsonSchema }, preHandler: [fastify.authenticate] }, async (request, reply) => { - const data = request.body as any; - const [row] = await db.insert(events).values(data).returning(); - return reply.code(201).send({ event: row }); + fastify.post('/events', { + schema: { + body: eventSchema, + }, + preHandler: [fastify.authenticate], + }, async (request, reply) => { + const data = request.body as z.infer; + + const [newEvent] = await db.insert(events).values(data).returning(); + + return reply.code(201).send({ event: newEvent }); }); // Update event - fastify.put('/events/:id', { schema: { body: eventBodyJsonSchema }, preHandler: [fastify.authenticate] }, async (request, reply) => { + fastify.put('/events/:id', { + schema: { + body: eventSchema, + }, + preHandler: [fastify.authenticate], + }, async (request, reply) => { const { id } = request.params as { id: string }; - const data = request.body as any; - const [row] = await db.update(events).set({ ...data, updatedAt: new Date() }).where(eq(events.id, id)).returning(); - if (!row) return reply.code(404).send({ error: 'Event not found' }); - return { event: row }; + const data = request.body as z.infer; + + const [updated] = await db + .update(events) + .set({ ...data, updatedAt: new Date() }) + .where(eq(events.id, id)) + .returning(); + + if (!updated) { + return reply.code(404).send({ error: 'Event not found' }); + } + + return { event: updated }; }); // Delete event - fastify.delete('/events/:id', { preHandler: [fastify.authenticate] }, async (request, reply) => { + fastify.delete('/events/:id', { + preHandler: [fastify.authenticate], + }, async (request, reply) => { const { id } = request.params as { id: string }; - const [row] = await db.delete(events).where(eq(events.id, id)).returning(); - if (!row) return reply.code(404).send({ error: 'Event not found' }); + + const [deleted] = await db + .delete(events) + .where(eq(events.id, id)) + .returning(); + + if (!deleted) { + return reply.code(404).send({ error: 'Event not found' }); + } + return { message: 'Event deleted successfully' }; }); - // Reorder events (synchronous transaction for better-sqlite3) - fastify.put('/events/reorder', { schema: { body: reorderBodyJsonSchema }, preHandler: [fastify.authenticate] }, async (request) => { + // Reorder events + fastify.put('/events/reorder', { + schema: { + body: z.object({ + orders: z.array(z.object({ + id: z.string().uuid(), + displayOrder: z.number().int().min(0), + })), + }), + }, + preHandler: [fastify.authenticate], + }, async (request, reply) => { const { orders } = request.body as { orders: Array<{ id: string; displayOrder: number }> }; - db.transaction((tx: any) => { + + // Update all in transaction + await db.transaction(async (tx) => { for (const { id, displayOrder } of orders) { - tx.update(events).set({ displayOrder }).where(eq(events.id, id)).run?.(); + await tx + .update(events) + .set({ displayOrder }) + .where(eq(events.id, id)); } }); + return { message: 'Events reordered successfully' }; }); }; diff --git a/backend/src/routes/gallery.ts b/backend/src/routes/gallery.ts index 966ea3d..a3cda3b 100644 --- a/backend/src/routes/gallery.ts +++ b/backend/src/routes/gallery.ts @@ -3,33 +3,17 @@ import { z } from 'zod'; import { db } from '../config/database.js'; import { galleryImages } from '../db/schema.js'; import { eq } from 'drizzle-orm'; -import fs from 'fs'; -import path from 'path'; -import sharp from 'sharp'; -// Fastify JSON schema for gallery image body -const galleryBodyJsonSchema = { - type: 'object', - required: ['imageUrl', 'altText', 'displayOrder'], - properties: { - imageUrl: { type: 'string', minLength: 1 }, - altText: { type: 'string', minLength: 1, maxLength: 200 }, - displayOrder: { type: 'integer', minimum: 0 }, - isPublished: { type: 'boolean' }, - }, -} as const; +const galleryImageSchema = z.object({ + imageUrl: z.string().url(), + altText: z.string().min(1).max(200), + displayOrder: z.number().int().min(0), + isPublished: z.boolean().optional().default(true), +}); const galleryRoute: FastifyPluginAsync = async (fastify) => { - // PUBLIC: List published gallery images (no auth required) - fastify.get('/gallery/public', async () => { - const images = await db.select().from(galleryImages) - .where(eq(galleryImages.isPublished, true)) - .orderBy(galleryImages.displayOrder); - return { images }; - }); - - // List all gallery images - admin only + // List all gallery images fastify.get('/gallery', { preHandler: [fastify.authenticate], }, async (request, reply) => { @@ -54,102 +38,26 @@ const galleryRoute: FastifyPluginAsync = async (fastify) => { // Create gallery image fastify.post('/gallery', { schema: { - body: galleryBodyJsonSchema, + body: galleryImageSchema, }, preHandler: [fastify.authenticate], }, async (request, reply) => { - const data = request.body as any; + const data = request.body as z.infer; const [newImage] = await db.insert(galleryImages).values(data).returning(); return reply.code(201).send({ image: newImage }); }); - // Upload image file (multipart) - fastify.post('/gallery/upload', { - preHandler: [fastify.authenticate], - }, async (request, reply) => { - try { - // Expect a single file field named "file" - const file = await (request as any).file(); - if (!file) { - return reply.code(400).send({ error: 'No file uploaded' }); - } - - const altText = (file.fields?.altText?.value as string | undefined) || ''; - const displayOrderRaw = (file.fields?.displayOrder?.value as string | undefined) || '0'; - const displayOrder = Number.parseInt(displayOrderRaw) || 0; - - const mime = file.mimetype as string | undefined; - if (!mime || !mime.startsWith('image/')) { - return reply.code(400).send({ error: 'Only image uploads are allowed' }); - } - - // Prepare directories - use persistent volume for Fly.io - const dataDir = process.env.GIT_WORKSPACE_DIR || path.join(process.cwd(), 'data'); - const uploadDir = path.join(dataDir, 'images', 'gallery'); - if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true }); - - // Read uploaded stream into buffer - const chunks: Buffer[] = []; - for await (const chunk of file.file) { - chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); - } - const inputBuffer = Buffer.concat(chunks); - - // Generate filename - const stamp = Date.now().toString(36); - const rand = Math.random().toString(36).slice(2, 8); - const baseName = `${stamp}-${rand}`; - - // Try to convert to webp and limit size; fallback to original - let outBuffer: Buffer | null = null; - let outExt = '.webp'; - try { - outBuffer = await sharp(inputBuffer) - .rotate() - .resize({ width: 1600, withoutEnlargement: true }) - .webp({ quality: 82 }) - .toBuffer(); - } catch { - outBuffer = inputBuffer; - // naive extension from mimetype - const extFromMime = mime.split('/')[1] || 'bin'; - outExt = '.' + extFromMime.replace(/[^a-z0-9]/gi, '').toLowerCase(); - } - - const filename = baseName + outExt; - const destPath = path.join(uploadDir, filename); - fs.writeFileSync(destPath, outBuffer); - - // Public URL (served via /static) - const publicUrl = `/static/images/gallery/${filename}`; - - // Store in DB (optional but useful) - const [row] = await db.insert(galleryImages).values({ - imageUrl: publicUrl, - altText: altText || filename, - displayOrder, - isPublished: true, - }).returning(); - - return reply.code(201).send({ image: row }); - - } catch (err) { - fastify.log.error({ err }, 'Upload failed'); - return reply.code(500).send({ error: 'Failed to upload image' }); - } - }); - // Update gallery image fastify.put('/gallery/:id', { schema: { - body: galleryBodyJsonSchema, + body: galleryImageSchema, }, preHandler: [fastify.authenticate], }, async (request, reply) => { const { id } = request.params as { id: string }; - const data = request.body as any; + const data = request.body as z.infer; const [updated] = await db .update(galleryImages) @@ -185,32 +93,24 @@ const galleryRoute: FastifyPluginAsync = async (fastify) => { // Reorder gallery images fastify.put('/gallery/reorder', { schema: { - body: { - type: 'object', - required: ['orders'], - properties: { - orders: { - type: 'array', - items: { - type: 'object', - required: ['id', 'displayOrder'], - properties: { - id: { type: 'string' }, - displayOrder: { type: 'integer', minimum: 0 }, - }, - }, - }, - }, - }, + body: z.object({ + orders: z.array(z.object({ + id: z.string().uuid(), + displayOrder: z.number().int().min(0), + })), + }), }, preHandler: [fastify.authenticate], }, async (request, reply) => { const { orders } = request.body as { orders: Array<{ id: string; displayOrder: number }> }; - // Update all in synchronous transaction (better-sqlite3 requirement) - db.transaction((tx: any) => { + // Update all in transaction + await db.transaction(async (tx) => { for (const { id, displayOrder } of orders) { - tx.update(galleryImages).set({ displayOrder }).where(eq(galleryImages.id, id)).run?.(); + await tx + .update(galleryImages) + .set({ displayOrder }) + .where(eq(galleryImages.id, id)); } }); diff --git a/backend/src/routes/publish.ts b/backend/src/routes/publish.ts index 81dc49b..5da211d 100644 --- a/backend/src/routes/publish.ts +++ b/backend/src/routes/publish.ts @@ -6,24 +6,19 @@ import { db } from '../config/database.js'; import { events, galleryImages, contentSections, publishHistory } from '../db/schema.js'; import { eq } from 'drizzle-orm'; -// Fastify JSON schema for publish body -const publishBodyJsonSchema = { - type: 'object', - required: ['commitMessage'], - properties: { - commitMessage: { type: 'string', minLength: 1, maxLength: 200 }, - }, -} as const; +const publishSchema = z.object({ + commitMessage: z.string().min(1).max(200), +}); const publishRoute: FastifyPluginAsync = async (fastify) => { fastify.post('/publish', { schema: { - body: publishBodyJsonSchema, + body: publishSchema, }, preHandler: [fastify.authenticate], }, async (request, reply) => { try { - const { commitMessage } = request.body as any; + const { commitMessage } = request.body as z.infer; const userId = request.user.id; fastify.log.info('Starting publish process...'); @@ -48,8 +43,8 @@ const publishRoute: FastifyPluginAsync = async (fastify) => { .orderBy(galleryImages.displayOrder); const sectionsData = await db.select().from(contentSections); - const sectionsMap = new Map( - (sectionsData as any[]).map((s: any) => [s.sectionName as string, s.contentJson as any]) + const sectionsMap = new Map( + sectionsData.map(s => [s.sectionName, s.contentJson as any]) ); fastify.log.info(`Fetched ${eventsData.length} events, ${galleryData.length} images, ${sectionsData.length} sections`); @@ -58,13 +53,13 @@ const publishRoute: FastifyPluginAsync = async (fastify) => { const fileGenerator = new FileGeneratorService(); await fileGenerator.writeFiles( gitService.getWorkspacePath(''), - (eventsData as any[]).map((e: any) => ({ + eventsData.map(e => ({ title: e.title, date: e.date, description: e.description, imageUrl: e.imageUrl, })), - (galleryData as any[]).map((g: any) => ({ + galleryData.map(g => ({ imageUrl: g.imageUrl, altText: g.altText, })), @@ -92,14 +87,14 @@ const publishRoute: FastifyPluginAsync = async (fastify) => { }; } catch (error) { - fastify.log.error({ err: error }, 'Publish error'); + fastify.log.error('Publish error:', error); // Attempt to reset git state on error try { const gitService = new GitService(); await gitService.reset(); } catch (resetError) { - fastify.log.error({ err: resetError }, 'Failed to reset git state'); + fastify.log.error('Failed to reset git state:', resetError); } return reply.code(500).send({ diff --git a/backend/src/routes/settings.ts b/backend/src/routes/settings.ts index 8fc84d1..6a5a751 100644 --- a/backend/src/routes/settings.ts +++ b/backend/src/routes/settings.ts @@ -4,14 +4,9 @@ import { db } from '../config/database.js'; import { siteSettings } from '../db/schema.js'; import { eq } from 'drizzle-orm'; -// Fastify JSON schema for settings body -const settingBodyJsonSchema = { - type: 'object', - required: ['value'], - properties: { - value: { type: 'string' }, - }, -} as const; +const settingSchema = z.object({ + value: z.string(), +}); const settingsRoute: FastifyPluginAsync = async (fastify) => { @@ -55,12 +50,12 @@ const settingsRoute: FastifyPluginAsync = async (fastify) => { // Update setting fastify.put('/settings/:key', { schema: { - body: settingBodyJsonSchema, + body: settingSchema, }, preHandler: [fastify.authenticate], }, async (request, reply) => { const { key } = request.params as { key: string }; - const { value } = request.body as any; + const { value } = request.body as z.infer; // Check if setting exists const [existing] = await db diff --git a/backend/src/scripts/migrate-old-data.ts b/backend/src/scripts/migrate-old-data.ts deleted file mode 100644 index 029a0ab..0000000 --- a/backend/src/scripts/migrate-old-data.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { db } from '../config/database.js'; -import { events, galleryImages } from '../db/schema.js'; -import fs from 'fs'; -import path from 'path'; -import sharp from 'sharp'; - -// Old events data -const oldEvents = [ - { - image: "/images/events/event_karaoke.jpg", - title: "Karaoke", - date: "2025-12-31", // Set as ongoing event - description: `Bei uns gibt es Karaoke Mi-Sa!!
-Seid ihr eine Gruppe und lieber unter euch? ..unseren 2.Stock kannst du auch mieten ;)
-Reserviere am besten gleich per Whatsapp 077 232 27 70`, - displayOrder: 0, - }, - { - image: "/images/events/event_pub-quiz.jpg", - title: "Pub Quiz", - date: "2025-12-31", // Set as ongoing event - description: `Jeden Freitag findet unser Pub Quiz statt. Gespielt wird tischweise in 3-4 Runden.
-Jede Woche gibt es ein anderes Thema. Es geht um Ruhm und Ehre und zusätzlich werden die Sieger der Herzen durch das Publikum gekürt! <3
-Auch Einzelpersonen sind herzlich willkommen!
-*zum mitmachen minimum 1 Getränk konsumieren oder 5CHF`, - displayOrder: 1, - }, - { - image: "/images/events/event_schlager-karaoke.jpeg", - title: "Schlager Hüttenzauber Karaoke", - date: "2025-11-27", - description: `Ab 19:00 Uhr Eintritt ist Frei! Reservieren unter 077 232 27 70`, - displayOrder: 2, - }, - { - image: "/images/events/event_advents-kalender.jpeg", - title: "Adventskalender", - date: "2025-12-20", - description: `Jeden Tag neue Überraschungen! Check unsere Social Media Stories!`, - displayOrder: 3, - }, - { - image: "/images/events/event_santa_karaoke.jpeg", - title: "Santa Karaoke-Party", - date: "2025-12-06", - description: `🤶🏻🎅🏻Komme als Weihnachts-Mann/-Frau und bekomme einen Shot auf's Haus!🤶🏻🎅🏻`, - displayOrder: 4, - }, - { - image: "/images/events/event_ferien.jpeg", - title: "Weihnachtsferien", - date: "2025-12-21", - description: `Wir sind ab 02.01.2026 wieder wie gewohnt für euch da! 🍀.
Für Anfragen WA 077 232 27 70 Antwort innerhalb 48h`, - displayOrder: 5, - }, - { - image: "/images/events/event_neujahrs-apero.jpeg", - title: "Neujahrs-Apero", - date: "2026-01-02", - description: `18:00-20:00 Uhr`, - displayOrder: 6, - }, -]; - -// Old gallery images -const oldGalleryImages = [ - { src: "/images/gallery/Gallery7.png", alt: "Gallery 7" }, - { src: "/images/gallery/Gallery8.png", alt: "Gallery 8" }, - { src: "/images/gallery/Gallery9.png", alt: "Gallery 9" }, - { src: "/images/gallery/Gallery6.png", alt: "Gallery 6" }, - { src: "/images/gallery/Gallery1.png", alt: "Gallery 1" }, - { src: "/images/gallery/Gallery2.png", alt: "Gallery 2" }, - { src: "/images/gallery/Gallery3.png", alt: "Gallery 3" }, - { src: "/images/gallery/Gallery4.png", alt: "Gallery 4" }, - { src: "/images/gallery/Gallery5.png", alt: "Gallery 5" }, -]; - -async function copyAndConvertImage( - sourcePath: string, - destDir: string, - filename: string -): Promise { - const projectRoot = path.join(process.cwd(), '..'); - const fullSourcePath = path.join(projectRoot, 'public', sourcePath); - - // Ensure destination directory exists - if (!fs.existsSync(destDir)) { - fs.mkdirSync(destDir, { recursive: true }); - } - - const ext = path.extname(filename); - const baseName = path.basename(filename, ext); - const webpFilename = `${baseName}.webp`; - const destPath = path.join(destDir, webpFilename); - - console.log(`Processing: ${fullSourcePath} -> ${destPath}`); - - // Check if source exists - if (!fs.existsSync(fullSourcePath)) { - console.error(`Source file not found: ${fullSourcePath}`); - throw new Error(`Source file not found: ${fullSourcePath}`); - } - - // Convert to webp and copy - await sharp(fullSourcePath) - .rotate() // Auto-rotate based on EXIF - .resize({ width: 1600, withoutEnlargement: true }) - .webp({ quality: 85 }) - .toFile(destPath); - - return `/images/${path.relative(destDir, destPath).replace(/\\/g, '/')}`; -} - -async function migrateEvents() { - console.log('\n=== Migrating Events ===\n'); - - const dataDir = process.env.GIT_WORKSPACE_DIR || path.join(process.cwd(), 'data'); - const eventsImageDir = path.join(dataDir, 'images', 'events'); - - for (const event of oldEvents) { - try { - const filename = path.basename(event.image); - const newImageUrl = await copyAndConvertImage( - event.image, - eventsImageDir, - filename - ); - - const [newEvent] = await db.insert(events).values({ - title: event.title, - date: event.date, - description: event.description, - imageUrl: newImageUrl, - displayOrder: event.displayOrder, - isPublished: true, - }).returning(); - - console.log(`✓ Migrated event: ${newEvent.title}`); - } catch (error) { - console.error(`✗ Failed to migrate event "${event.title}":`, error); - } - } -} - -async function migrateGallery() { - console.log('\n=== Migrating Gallery Images ===\n'); - - const dataDir = process.env.GIT_WORKSPACE_DIR || path.join(process.cwd(), 'data'); - const galleryImageDir = path.join(dataDir, 'images', 'gallery'); - - for (let i = 0; i < oldGalleryImages.length; i++) { - const img = oldGalleryImages[i]; - try { - const filename = path.basename(img.src); - const newImageUrl = await copyAndConvertImage( - img.src, - galleryImageDir, - filename - ); - - const [newImage] = await db.insert(galleryImages).values({ - imageUrl: newImageUrl, - altText: img.alt, - displayOrder: i, - isPublished: true, - }).returning(); - - console.log(`✓ Migrated gallery image: ${newImage.altText}`); - } catch (error) { - console.error(`✗ Failed to migrate gallery image "${img.alt}":`, error); - } - } -} - -async function main() { - console.log('Starting migration of old data...\n'); - console.log('Working directory:', process.cwd()); - console.log('Data directory:', process.env.GIT_WORKSPACE_DIR || path.join(process.cwd(), 'data')); - - try { - await migrateEvents(); - await migrateGallery(); - console.log('\n✓ Migration completed successfully!'); - } catch (error) { - console.error('\n✗ Migration failed:', error); - process.exit(1); - } -} - -main(); diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index aeda1fd..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,38 +0,0 @@ -services: - frontend: - build: - context: . - dockerfile: Dockerfile - environment: - - BACKEND_URL=http://proxy:4321 - depends_on: - - backend - - backend: - build: - context: ./backend - dockerfile: Dockerfile - env_file: - - ./backend/.env.local - environment: - - NODE_ENV=production - - PORT=8080 - - DATABASE_PATH=/app/data/gallus_cms.db - - GIT_WORKSPACE_DIR=/app/workspace - volumes: - - backend_data:/app/data - - backend_workspace:/app/workspace - - proxy: - build: - context: . - dockerfile: Dockerfile.caddy - depends_on: - - frontend - - backend - ports: - - "4321:80" - -volumes: - backend_data: - backend_workspace: \ No newline at end of file diff --git a/fly.toml b/fly.toml index 8520c3e..6bb56b8 100644 --- a/fly.toml +++ b/fly.toml @@ -9,9 +9,6 @@ kill_timeout = 5 [env] PORT = "3000" NODE_ENV = "production" - BACKEND_PORT = "8080" - DATABASE_PATH = "/app/data/db/gallus_cms.db" - GIT_WORKSPACE_DIR = "/app/data/workspace" [http_service] internal_port = 3000 @@ -42,8 +39,4 @@ kill_timeout = 5 [[vm]] memory = "512MB" cpu_kind = "shared" - cpus = 1 - -[[mounts]] - source = "gallus_data" - destination = "/app/data" \ No newline at end of file + cpus = 1 \ No newline at end of file diff --git a/src/pages/admin.astro b/src/pages/admin.astro deleted file mode 100644 index 8570387..0000000 --- a/src/pages/admin.astro +++ /dev/null @@ -1,294 +0,0 @@ ---- -const title = 'Admin'; ---- - - - - - - {title} - - - -

Admin

-
-

Authentifizierung

-
Prüfe Anmeldestatus...
-
- Mit Gitea anmelden - - -
-
- - - - - - - - diff --git a/src/pages/auth/callback.astro b/src/pages/auth/callback.astro deleted file mode 100644 index 0cb55dd..0000000 --- a/src/pages/auth/callback.astro +++ /dev/null @@ -1,29 +0,0 @@ ---- -const title = 'Anmeldung wird abgeschlossen...'; ---- - - - - - - {title} - - -

{title}

- - - diff --git a/src/pages/index.astro b/src/pages/index.astro index abe7868..221a536 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -5,42 +5,82 @@ import Welcome from "../components/Welcome.astro"; import EventsGrid from "../components/EventsGrid.astro"; import Drinks from "../components/Drinks.astro"; import ImageCarousel from "../components/ImageCarousel.astro"; -import Contact from "../components/Contact.astro"; -import About from "../components/About.astro"; -const API_BASE = 'https://cms.gallus-pub.ch'; +const events = [ + { + image: "/images/events/event_karaoke.jpg", + title: "Karaoke", + date: "Mittwoch - Samstag", + description: ` + Bei uns gibt es Karaoke Mi-Sa!!
+ Seid ihr eine Gruppe und lieber unter euch? ..unseren 2.Stock kannst du auch mieten ;)
+ Reserviere am besten gleich per Whatsapp 077 232 27 70 + `, + }, + { + image: "/images/events/event_pub-quiz.jpg", + title: "Pub Quiz", + date: "Jeden Freitag", + description: ` + Jeden Freitag findet unser Pub Quiz statt. Gespielt wird tischweise in 3-4 Runden.
+ Jede Woche gibt es ein anderes Thema. Es geht um Ruhm und Ehre und zusätzlich werden die Sieger der Herzen durch das Publikum gekürt! <3
+ Auch Einzelpersonen sind herzlich willkommen!
+ *zum mitmachen minimum 1 Getränk konsumieren oder 5CHF + `, + }, + { + image: "/images/events/event_schlager-karaoke.jpeg", + title: "Schlager Hüttenzauber Karaoke", + date: "27. November - 19:00 Uhr", + description: ` + Ab 19:00 Uhr Eintritt ist Frei! Reservieren unter 077 232 27 70 + `, + }, + { + image: "/images/events/event_advents-kalender.jpeg", + title: "Adventskalender", + date: "03. Dezember - 20. Dezember 2025", + description: ` + Jeden Tag neue Überraschungen! Check unsere Social Media Stories! + `, + }, + { + image: "/images/events/event_santa_karaoke.jpeg", + title: "Santa Karaoke-Party", + date: "06. Dezember 2025", + description: ` +🤶🏻🎅🏻Komme als Weihnachts-Mann/-Frau und bekomme einen Shot auf's Haus!🤶🏻🎅🏻 `, + }, + { + image: "/images/events/event_ferien.jpeg", + title: "Weihnachtsferien", + date: "21. Dezember 2025 - 01. Januar 2026", + description: ` + Wir sind ab 02.01.2026 wieder wie gewohnt für euch da! 🍀.
Für Anfragen WA 077 232 27 70 Antwort innerhalb 48h + `, + }, + { + image: "/images/events/event_neujahrs-apero.jpeg", + title: "Neujahrs-Apero", + date: "02. Januar 2026 - 18:00-20:00 Uhr", + description: ` + + `, + }, -// Fetch events from backend API -let events = []; -try { - const eventsResponse = await fetch(`${API_BASE}/api/events/public`); - if (eventsResponse.ok) { - const eventsData = await eventsResponse.json(); - events = (eventsData.events || []).map((ev: any) => ({ - image: `${API_BASE}${ev.imageUrl}`, - title: ev.title, - date: ev.date, - description: ev.description - })); - } -} catch (error) { - console.error('Failed to fetch events:', error); -} +]; -// Fetch gallery images from backend API -let images = []; -try { - const galleryResponse = await fetch(`${API_BASE}/api/gallery/public`); - if (galleryResponse.ok) { - const galleryData = await galleryResponse.json(); - images = (galleryData.images || []).map((img: any) => ({ - src: `${API_BASE}${img.imageUrl}`, - alt: img.altText - })); - } -} catch (error) { - console.error('Failed to fetch gallery:', error); -} +const images = [ + { src: "/images/gallery/Gallery7.png", alt: "Siebtes Bild" }, + { src: "/images/gallery/Gallery8.png", alt: "Achtes Bild" }, + { src: "/images/gallery/Gallery9.png", alt: "Neuntes Bild" }, + { src: "/images/gallery/Gallery6.png", alt: "Sechstes Bild" }, + { src: "/images/gallery/Gallery1.png", alt: "Erstes Bild" }, + { src: "/images/gallery/Gallery2.png", alt: "Zweites Bild" }, + { src: "/images/gallery/Gallery3.png", alt: "Drittes Bild" }, + { src: "/images/gallery/Gallery4.png", alt: "Viertes Bild" }, + { src: "/images/gallery/Gallery5.png", alt: "Fünftes Bild" }, +]; ---