Compare commits
12 Commits
feat/cms
...
af4877300f
| Author | SHA1 | Date | |
|---|---|---|---|
| af4877300f | |||
| 4a103cf7d6 | |||
| 97e7f88906 | |||
| 807c56de5a | |||
| 8ca30ae5f3 | |||
| 8f1254840c | |||
| 8b2d00385a | |||
| c55e274718 | |||
| e9a95ccf8d | |||
| b16ac76620 | |||
| 0e03b9dea9 | |||
| da3a950a1a |
1
.gitignore
vendored
1
.gitignore
vendored
@ -22,3 +22,4 @@ pnpm-debug.log*
|
|||||||
|
|
||||||
# jetbrains setting folder
|
# jetbrains setting folder
|
||||||
.idea/
|
.idea/
|
||||||
|
/ai/
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
|
when:
|
||||||
|
branch:
|
||||||
|
- main
|
||||||
|
event:
|
||||||
|
- push
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
deploy:
|
deploy_frontend:
|
||||||
image: node:20
|
image: node:20
|
||||||
environment:
|
environment:
|
||||||
FLY_API_TOKEN:
|
FLY_API_TOKEN:
|
||||||
@ -7,10 +13,15 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
- curl -L https://fly.io/install.sh | sh
|
- curl -L https://fly.io/install.sh | sh
|
||||||
- export PATH="$HOME/.fly/bin:$PATH"
|
- export PATH="$HOME/.fly/bin:$PATH"
|
||||||
- flyctl deploy --config fly.toml --app gallus-pub
|
- flyctl deploy --config fly.toml --app gallus-pub --remote-only
|
||||||
|
|
||||||
when:
|
deploy_backend:
|
||||||
branch:
|
image: node:20
|
||||||
- main
|
environment:
|
||||||
event:
|
FLY_API_TOKEN:
|
||||||
- push
|
from_secret: FLY_API_TOKEN
|
||||||
|
commands:
|
||||||
|
- cd backend
|
||||||
|
- curl -L https://fly.io/install.sh | sh
|
||||||
|
- export PATH="$HOME/.fly/bin:$PATH"
|
||||||
|
- flyctl deploy --config fly.toml --app gallus-cms-backend --remote-only
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Deployment Guide
|
# Deployment Guide
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisite
|
||||||
|
|
||||||
1. Fly.io CLI installed: `curl -L https://fly.io/install.sh | sh`
|
1. Fly.io CLI installed: `curl -L https://fly.io/install.sh | sh`
|
||||||
2. Fly.io account: `flyctl auth login`
|
2. Fly.io account: `flyctl auth login`
|
||||||
|
|||||||
@ -3,6 +3,8 @@ app = "gallus-cms-backend"
|
|||||||
primary_region = "ams"
|
primary_region = "ams"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
|
# Ensure Fly uses the Dockerfile in this backend directory
|
||||||
|
dockerfile = "Dockerfile"
|
||||||
|
|
||||||
[env]
|
[env]
|
||||||
PORT = "8080"
|
PORT = "8080"
|
||||||
@ -10,6 +12,10 @@ primary_region = "ams"
|
|||||||
GITEA_URL = "https://git.bookageek.ch"
|
GITEA_URL = "https://git.bookageek.ch"
|
||||||
DATABASE_PATH = "/app/data/gallus_cms.db"
|
DATABASE_PATH = "/app/data/gallus_cms.db"
|
||||||
GIT_WORKSPACE_DIR = "/app/data/workspace"
|
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]
|
[http_service]
|
||||||
internal_port = 8080
|
internal_port = 8080
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
"@fastify/cookie": "^9.3.1",
|
"@fastify/cookie": "^9.3.1",
|
||||||
"@fastify/cors": "^9.0.1",
|
"@fastify/cors": "^9.0.1",
|
||||||
"@fastify/jwt": "^8.0.0",
|
"@fastify/jwt": "^8.0.0",
|
||||||
|
"@fastify/static": "^6.12.0",
|
||||||
"@fastify/multipart": "^8.1.0",
|
"@fastify/multipart": "^8.1.0",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"better-sqlite3": "^11.10.0",
|
"better-sqlite3": "^11.10.0",
|
||||||
|
|||||||
@ -2,14 +2,103 @@ import { drizzle } from 'drizzle-orm/better-sqlite3';
|
|||||||
import Database from 'better-sqlite3';
|
import Database from 'better-sqlite3';
|
||||||
import * as schema from '../db/schema.js';
|
import * as schema from '../db/schema.js';
|
||||||
import { env } from './env.js';
|
import { env } from './env.js';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
if (!env.DATABASE_PATH) {
|
if (!env.DATABASE_PATH) {
|
||||||
throw new Error('DATABASE_PATH environment variable is not set');
|
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);
|
const sqlite = new Database(env.DATABASE_PATH);
|
||||||
|
|
||||||
// Enable WAL mode for better concurrent access
|
// Enable WAL mode for better concurrent access
|
||||||
sqlite.pragma('journal_mode = WAL');
|
sqlite.pragma('journal_mode = WAL');
|
||||||
|
|
||||||
export const db = drizzle(sqlite, { schema });
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -5,6 +5,9 @@ import multipart from '@fastify/multipart';
|
|||||||
import cookie from '@fastify/cookie';
|
import cookie from '@fastify/cookie';
|
||||||
import { authenticate } from './middleware/auth.middleware.js';
|
import { authenticate } from './middleware/auth.middleware.js';
|
||||||
import { env, validateEnv } from './config/env.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 routes
|
||||||
import authRoute from './routes/auth.js';
|
import authRoute from './routes/auth.js';
|
||||||
@ -57,6 +60,14 @@ 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
|
// Decorate fastify with authenticate method
|
||||||
fastify.decorate('authenticate', authenticate);
|
fastify.decorate('authenticate', authenticate);
|
||||||
|
|
||||||
@ -99,6 +110,8 @@ fastify.setErrorHandler((error, request, reply) => {
|
|||||||
// Start server
|
// Start server
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
try {
|
try {
|
||||||
|
// Initialize database before starting server
|
||||||
|
initDatabase();
|
||||||
await fastify.listen({ port: env.PORT, host: '0.0.0.0' });
|
await fastify.listen({ port: env.PORT, host: '0.0.0.0' });
|
||||||
console.log(`🚀 Server listening on port ${env.PORT}`);
|
console.log(`🚀 Server listening on port ${env.PORT}`);
|
||||||
console.log(`📝 Environment: ${env.NODE_ENV}`);
|
console.log(`📝 Environment: ${env.NODE_ENV}`);
|
||||||
|
|||||||
@ -31,9 +31,8 @@ const authRoute: FastifyPluginAsync = async (fastify) => {
|
|||||||
reply.setCookie('oauth_state', state, {
|
reply.setCookie('oauth_state', state, {
|
||||||
path: '/',
|
path: '/',
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: 'lax',
|
sameSite: 'none',
|
||||||
// Use HTTPS-based detection to avoid setting Secure on localhost HTTP
|
secure: true,
|
||||||
secure: !!env.FRONTEND_URL && env.FRONTEND_URL.startsWith('https'),
|
|
||||||
maxAge: 10 * 60, // 10 minutes
|
maxAge: 10 * 60, // 10 minutes
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -59,7 +58,7 @@ const authRoute: FastifyPluginAsync = async (fastify) => {
|
|||||||
// Verify CSRF state from cookie
|
// Verify CSRF state from cookie
|
||||||
const expectedState = request.cookies?.oauth_state as string | undefined;
|
const expectedState = request.cookies?.oauth_state as string | undefined;
|
||||||
if (!expectedState || state !== expectedState) {
|
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
|
// Clear state cookie
|
||||||
@ -122,11 +121,12 @@ const authRoute: FastifyPluginAsync = async (fastify) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Also set token as HttpOnly cookie so subsequent API calls authenticate reliably
|
// 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, {
|
reply.setCookie('token', token, {
|
||||||
path: '/',
|
path: '/',
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: 'lax',
|
sameSite: (env.NODE_ENV === 'production' ? 'none' : 'lax'),
|
||||||
secure: !!env.FRONTEND_URL && env.FRONTEND_URL.startsWith('https'),
|
secure: (env.NODE_ENV === 'production') || (!!env.FRONTEND_URL && env.FRONTEND_URL.startsWith('https')),
|
||||||
maxAge: 60 * 60 * 24, // 24h
|
maxAge: 60 * 60 * 24, // 24h
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -36,7 +36,15 @@ const reorderBodyJsonSchema = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const eventsRoute: FastifyPluginAsync = async (fastify) => {
|
const eventsRoute: FastifyPluginAsync = async (fastify) => {
|
||||||
// List all events (by displayOrder)
|
// 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 () => {
|
fastify.get('/events', { preHandler: [fastify.authenticate] }, async () => {
|
||||||
const all = await db.select().from(events).orderBy(events.displayOrder);
|
const all = await db.select().from(events).orderBy(events.displayOrder);
|
||||||
return { events: all };
|
return { events: all };
|
||||||
|
|||||||
@ -3,6 +3,9 @@ import { z } from 'zod';
|
|||||||
import { db } from '../config/database.js';
|
import { db } from '../config/database.js';
|
||||||
import { galleryImages } from '../db/schema.js';
|
import { galleryImages } from '../db/schema.js';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
// Fastify JSON schema for gallery image body
|
// Fastify JSON schema for gallery image body
|
||||||
const galleryBodyJsonSchema = {
|
const galleryBodyJsonSchema = {
|
||||||
@ -18,7 +21,15 @@ const galleryBodyJsonSchema = {
|
|||||||
|
|
||||||
const galleryRoute: FastifyPluginAsync = async (fastify) => {
|
const galleryRoute: FastifyPluginAsync = async (fastify) => {
|
||||||
|
|
||||||
// List all gallery images
|
// 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
|
||||||
fastify.get('/gallery', {
|
fastify.get('/gallery', {
|
||||||
preHandler: [fastify.authenticate],
|
preHandler: [fastify.authenticate],
|
||||||
}, async (request, reply) => {
|
}, async (request, reply) => {
|
||||||
@ -54,6 +65,82 @@ const galleryRoute: FastifyPluginAsync = async (fastify) => {
|
|||||||
return reply.code(201).send({ image: newImage });
|
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
|
// Update gallery image
|
||||||
fastify.put('/gallery/:id', {
|
fastify.put('/gallery/:id', {
|
||||||
schema: {
|
schema: {
|
||||||
|
|||||||
16
fly.toml
16
fly.toml
@ -4,14 +4,14 @@ kill_signal = "SIGINT"
|
|||||||
kill_timeout = 5
|
kill_timeout = 5
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
dockerfile = "Dockerfile.fly"
|
dockerfile = "Dockerfile"
|
||||||
|
|
||||||
[env]
|
[env]
|
||||||
PORT = "3000" # Caddy (serves frontend + proxies /api/*)
|
PORT = "3000"
|
||||||
NODE_ENV = "production"
|
NODE_ENV = "production"
|
||||||
BACKEND_PORT = "8080" # Fastify backend will listen here
|
BACKEND_PORT = "8080"
|
||||||
DATABASE_PATH = "/app/data/gallus_cms.db"
|
DATABASE_PATH = "/app/data/db/gallus_cms.db"
|
||||||
GIT_WORKSPACE_DIR = "/app/workspace"
|
GIT_WORKSPACE_DIR = "/app/data/workspace"
|
||||||
|
|
||||||
[http_service]
|
[http_service]
|
||||||
internal_port = 3000
|
internal_port = 3000
|
||||||
@ -46,8 +46,4 @@ kill_timeout = 5
|
|||||||
|
|
||||||
[[mounts]]
|
[[mounts]]
|
||||||
source = "gallus_data"
|
source = "gallus_data"
|
||||||
destination = "/app/data"
|
destination = "/app/data"
|
||||||
|
|
||||||
[[mounts]]
|
|
||||||
source = "gallus_workspace"
|
|
||||||
destination = "/app/workspace"
|
|
||||||
@ -36,7 +36,7 @@ const title = 'Admin';
|
|||||||
<h2>Authentifizierung</h2>
|
<h2>Authentifizierung</h2>
|
||||||
<div id="auth-status" class="muted">Prüfe Anmeldestatus...</div>
|
<div id="auth-status" class="muted">Prüfe Anmeldestatus...</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<a id="login-link" class="btn" href="/api/auth/gitea">Mit Gitea anmelden</a>
|
<a id="login-link" class="btn" href="https://cms.gallus-pub.ch/api/auth/gitea">Mit Gitea anmelden</a>
|
||||||
<button id="btn-relogin">Neu anmelden</button>
|
<button id="btn-relogin">Neu anmelden</button>
|
||||||
<button id="btn-logout">Abmelden</button>
|
<button id="btn-logout">Abmelden</button>
|
||||||
</div>
|
</div>
|
||||||
@ -76,8 +76,11 @@ const title = 'Admin';
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Base-URL des Backends (separate Subdomain)
|
||||||
|
const API_BASE = 'https://cms.gallus-pub.ch';
|
||||||
|
|
||||||
const api = async (path, opts = {}) => {
|
const api = async (path, opts = {}) => {
|
||||||
const res = await fetch(path, { credentials: 'include', ...opts });
|
const res = await fetch(API_BASE + path, { credentials: 'include', ...opts });
|
||||||
if (!res.ok) throw new Error(await res.text());
|
if (!res.ok) throw new Error(await res.text());
|
||||||
const ct = res.headers.get('content-type') || '';
|
const ct = res.headers.get('content-type') || '';
|
||||||
return ct.includes('application/json') ? res.json() : res.text();
|
return ct.includes('application/json') ? res.json() : res.text();
|
||||||
@ -107,13 +110,13 @@ const title = 'Admin';
|
|||||||
loginLink.addEventListener('click', (e) => {
|
loginLink.addEventListener('click', (e) => {
|
||||||
try {
|
try {
|
||||||
// Stelle sicher, dass Navigieren erzwungen wird
|
// Stelle sicher, dass Navigieren erzwungen wird
|
||||||
window.location.assign('/api/auth/gitea');
|
window.location.assign(API_BASE + '/api/auth/gitea');
|
||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
document.getElementById('btn-relogin').addEventListener('click', async () => {
|
document.getElementById('btn-relogin').addEventListener('click', async () => {
|
||||||
try { await api('/api/auth/logout', { method: 'POST' }); } catch {}
|
try { await api('/api/auth/logout', { method: 'POST' }); } catch {}
|
||||||
document.cookie = 'token=; Path=/; Max-Age=0;';
|
document.cookie = 'token=; Path=/; Max-Age=0;';
|
||||||
window.location.assign('/api/auth/gitea');
|
window.location.assign(API_BASE + '/api/auth/gitea');
|
||||||
});
|
});
|
||||||
document.getElementById('btn-logout').addEventListener('click', async () => {
|
document.getElementById('btn-logout').addEventListener('click', async () => {
|
||||||
try { await api('/api/auth/logout', { method: 'POST' }); } catch {}
|
try { await api('/api/auth/logout', { method: 'POST' }); } catch {}
|
||||||
@ -127,7 +130,7 @@ const title = 'Admin';
|
|||||||
fd.append('file', file);
|
fd.append('file', file);
|
||||||
if (altText) fd.append('altText', altText);
|
if (altText) fd.append('altText', altText);
|
||||||
fd.append('displayOrder', '0');
|
fd.append('displayOrder', '0');
|
||||||
const res = await fetch('/api/gallery/upload', { method: 'POST', body: fd, credentials: 'include' });
|
const res = await fetch(API_BASE + '/api/gallery/upload', { method: 'POST', body: fd, credentials: 'include' });
|
||||||
if (!res.ok) throw new Error(await res.text());
|
if (!res.ok) throw new Error(await res.text());
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,82 +5,42 @@ import Welcome from "../components/Welcome.astro";
|
|||||||
import EventsGrid from "../components/EventsGrid.astro";
|
import EventsGrid from "../components/EventsGrid.astro";
|
||||||
import Drinks from "../components/Drinks.astro";
|
import Drinks from "../components/Drinks.astro";
|
||||||
import ImageCarousel from "../components/ImageCarousel.astro";
|
import ImageCarousel from "../components/ImageCarousel.astro";
|
||||||
|
import Contact from "../components/Contact.astro";
|
||||||
|
import About from "../components/About.astro";
|
||||||
|
|
||||||
const events = [
|
const API_BASE = 'https://cms.gallus-pub.ch';
|
||||||
{
|
|
||||||
image: "/images/events/event_karaoke.jpg",
|
|
||||||
title: "Karaoke",
|
|
||||||
date: "Mittwoch - Samstag",
|
|
||||||
description: `
|
|
||||||
Bei uns gibt es Karaoke Mi-Sa!! <br>
|
|
||||||
Seid ihr eine Gruppe und lieber unter euch? ..unseren 2.Stock kannst du auch mieten ;) <br>
|
|
||||||
Reserviere am besten gleich per Whatsapp <a href="tel:+41772322770">077 232 27 70</a>
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
image: "/images/events/event_pub-quiz.jpg",
|
|
||||||
title: "Pub Quiz",
|
|
||||||
date: "Jeden Freitag",
|
|
||||||
description: `
|
|
||||||
Jeden Freitag findet unser <b>Pub Quiz</b> statt. Gespielt wird tischweise in 3-4 Runden. <br>
|
|
||||||
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 <br>
|
|
||||||
Auch Einzelpersonen sind herzlich willkommen! <br>
|
|
||||||
*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 <a href="tel:+41772322770">077 232 27 70</a>
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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! 🍀. <br> Für Anfragen WA <a href="tel:+41772322770">077 232 27 70</a> 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);
|
||||||
|
}
|
||||||
|
|
||||||
const images = [
|
// Fetch gallery images from backend API
|
||||||
{ src: "/images/gallery/Gallery7.png", alt: "Siebtes Bild" },
|
let images = [];
|
||||||
{ src: "/images/gallery/Gallery8.png", alt: "Achtes Bild" },
|
try {
|
||||||
{ src: "/images/gallery/Gallery9.png", alt: "Neuntes Bild" },
|
const galleryResponse = await fetch(`${API_BASE}/api/gallery/public`);
|
||||||
{ src: "/images/gallery/Gallery6.png", alt: "Sechstes Bild" },
|
if (galleryResponse.ok) {
|
||||||
{ src: "/images/gallery/Gallery1.png", alt: "Erstes Bild" },
|
const galleryData = await galleryResponse.json();
|
||||||
{ src: "/images/gallery/Gallery2.png", alt: "Zweites Bild" },
|
images = (galleryData.images || []).map((img: any) => ({
|
||||||
{ src: "/images/gallery/Gallery3.png", alt: "Drittes Bild" },
|
src: `${API_BASE}${img.imageUrl}`,
|
||||||
{ src: "/images/gallery/Gallery4.png", alt: "Viertes Bild" },
|
alt: img.altText
|
||||||
{ src: "/images/gallery/Gallery5.png", alt: "Fünftes Bild" },
|
}));
|
||||||
];
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch gallery:', error);
|
||||||
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
|
|||||||
Reference in New Issue
Block a user