From daccc4367770ef0e842cc3eaa4905ee6d580425c Mon Sep 17 00:00:00 2001 From: Kenzo Date: Mon, 8 Dec 2025 16:00:40 +0100 Subject: [PATCH] Add CMS features with admin interface and OAuth authentication integration - Introduced Caddy server for serving frontend and API backend. - Implemented admin dashboard for creating, editing, and managing events. - Replaced session-based authentication with token-based OAuth using Gitea. - Added support for drag-and-drop event reordering in the admin interface. - Standardized Fastify route validation with JSON schemas. - Enhanced authentication flow with cookie-based state and secure token storage. - Reworked backend routes to handle publishing, event management, and content updates. - Updated `Dockerfile.caddy` and `fly.toml` for deployment configuration. --- Dockerfile | 6 +- Dockerfile.caddy | 9 + backend/.env.local | 34 ++++ backend/Dockerfile | 20 +-- backend/package.json | 1 - backend/src/index.ts | 14 +- backend/src/routes/auth.ts | 70 +++++--- backend/src/routes/content.ts | 17 +- backend/src/routes/events.ts | 144 +++++++--------- backend/src/routes/gallery.ts | 57 ++++--- backend/src/routes/publish.ts | 27 +-- backend/src/routes/settings.ts | 15 +- docker-compose.yml | 38 +++++ fly.toml | 17 +- src/pages/admin.astro | 291 +++++++++++++++++++++++++++++++++ src/pages/auth/callback.astro | 29 ++++ 16 files changed, 603 insertions(+), 186 deletions(-) create mode 100644 Dockerfile.caddy create mode 100644 backend/.env.local create mode 100644 docker-compose.yml create mode 100644 src/pages/admin.astro create mode 100644 src/pages/auth/callback.astro diff --git a/Dockerfile b/Dockerfile index 5e532af..a9c26c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,8 @@ FROM node:20-alpine AS build WORKDIR /app COPY package*.json ./ -RUN npm ci +# Fallback to npm install if no lockfile is present +RUN npm ci || npm install COPY . . # Ensure CSS variables are present RUN mkdir -p public/styles @@ -16,7 +17,8 @@ RUN npm install -g serve COPY --from=build /app/dist ./dist EXPOSE 3000 -CMD ["serve", "-s", "dist", "-l", "3000"] +# Serve static files (no SPA fallback), so /admin serves dist/admin/index.html +CMD ["serve", "-l", "3000", "dist"] 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 new file mode 100644 index 0000000..e63695c --- /dev/null +++ b/Dockerfile.caddy @@ -0,0 +1,9 @@ +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/backend/.env.local b/backend/.env.local new file mode 100644 index 0000000..ce01029 --- /dev/null +++ b/backend/.env.local @@ -0,0 +1,34 @@ +# 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/Dockerfile b/backend/Dockerfile index a3c3db2..aafe7b1 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -10,7 +10,8 @@ RUN apk add --no-cache python3 make g++ # Install dependencies COPY package*.json ./ -RUN npm ci +# Use npm ci when lockfile exists, fallback to npm install for local/dev +RUN npm ci || npm install # Copy source COPY . . @@ -26,17 +27,8 @@ WORKDIR /app # Install runtime dependencies (git for simple-git, sqlite3 CLI tool) RUN apk add --no-cache git sqlite -# 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 production dependencies from builder (already compiled native modules) +COPY --from=builder /app/node_modules ./node_modules # Copy built files from builder COPY --from=builder /app/dist ./dist @@ -63,5 +55,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)})" -# Start application -CMD ["node", "dist/index.js"] +# 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"] diff --git a/backend/package.json b/backend/package.json index 1e837c8..62fd9dd 100644 --- a/backend/package.json +++ b/backend/package.json @@ -16,7 +16,6 @@ "@fastify/cors": "^9.0.1", "@fastify/jwt": "^8.0.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/index.ts b/backend/src/index.ts index 884d4c1..e83ae8c 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -3,7 +3,6 @@ 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'; @@ -44,17 +43,12 @@ 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, { diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index f5625f6..620681c 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -6,10 +6,15 @@ import { eq } from 'drizzle-orm'; import { GiteaService } from '../services/gitea.service.js'; import { env } from '../config/env.js'; -const callbackSchema = z.object({ - code: z.string(), - state: z.string(), -}); +// 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 authRoute: FastifyPluginAsync = async (fastify) => { const giteaService = new GiteaService(); @@ -22,8 +27,15 @@ const authRoute: FastifyPluginAsync = async (fastify) => { // Generate CSRF state token const state = giteaService.generateState(); - // Store state in session - request.session.set('oauth_state', state); + // Store state in a short-lived cookie + reply.setCookie('oauth_state', state, { + path: '/', + httpOnly: true, + sameSite: 'lax', + // Use HTTPS-based detection to avoid setting Secure on localhost HTTP + secure: !!env.FRONTEND_URL && env.FRONTEND_URL.startsWith('https'), + maxAge: 10 * 60, // 10 minutes + }); // Generate authorization URL const authUrl = giteaService.getAuthorizationUrl(state); @@ -38,20 +50,20 @@ const authRoute: FastifyPluginAsync = async (fastify) => { */ fastify.get('/auth/callback', { schema: { - querystring: callbackSchema, + querystring: callbackQueryJsonSchema, }, }, async (request, reply) => { try { - const { code, state } = request.query as z.infer; + const { code, state } = request.query as { code: string; state: string }; - // Verify CSRF state - const expectedState = request.session.get('oauth_state'); + // Verify CSRF state from cookie + const expectedState = request.cookies?.oauth_state as string | undefined; if (!expectedState || state !== expectedState) { return reply.code(400).send({ error: 'Invalid state parameter' }); } - // Clear state from session - request.session.delete('oauth_state'); + // Clear state cookie + reply.clearCookie('oauth_state', { path: '/' }); // Exchange code for access token const tokenResponse = await giteaService.exchangeCodeForToken(code); @@ -103,18 +115,27 @@ const authRoute: FastifyPluginAsync = async (fastify) => { { id: user.id, giteaId: user.giteaId, - username: user.giteaUsername, - role: user.role, + username: user.giteaUsername || '', + role: user.role ?? 'admin', }, { expiresIn: '24h' } ); - // Redirect to frontend with token + // Also set token as HttpOnly cookie so subsequent API calls authenticate reliably + reply.setCookie('token', token, { + path: '/', + httpOnly: true, + sameSite: 'lax', + secure: !!env.FRONTEND_URL && env.FRONTEND_URL.startsWith('https'), + maxAge: 60 * 60 * 24, // 24h + }); + + // Redirect to admin dashboard const frontendUrl = env.FRONTEND_URL; - return reply.redirect(`${frontendUrl}/auth/callback?token=${token}`); + return reply.redirect(`${frontendUrl}/admin`); } catch (error) { - fastify.log.error('OAuth callback error:', error); + fastify.log.error({ err: error }, 'OAuth callback error'); return reply.code(500).send({ error: 'Authentication failed' }); } }); @@ -139,12 +160,14 @@ const authRoute: FastifyPluginAsync = async (fastify) => { } return { - id: user.id, - username: user.giteaUsername, - email: user.giteaEmail, - displayName: user.displayName, - avatarUrl: user.avatarUrl, - role: user.role, + user: { + id: user.id, + giteaUsername: user.giteaUsername, + giteaEmail: user.giteaEmail, + displayName: user.displayName, + avatarUrl: user.avatarUrl, + role: user.role, + }, }; }); @@ -157,6 +180,7 @@ 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 6ce19c4..47c2081 100644 --- a/backend/src/routes/content.ts +++ b/backend/src/routes/content.ts @@ -4,9 +4,14 @@ import { db } from '../config/database.js'; import { contentSections } from '../db/schema.js'; import { eq } from 'drizzle-orm'; -const contentSectionSchema = z.object({ - contentJson: z.record(z.any()), -}); +// Fastify JSON schema for content section body +const contentBodyJsonSchema = { + type: 'object', + required: ['contentJson'], + properties: { + contentJson: {}, // allow any JSON + }, +} as const; const contentRoute: FastifyPluginAsync = async (fastify) => { @@ -36,12 +41,12 @@ const contentRoute: FastifyPluginAsync = async (fastify) => { // Update content section fastify.put('/content/:section', { schema: { - body: contentSectionSchema, + body: contentBodyJsonSchema, }, preHandler: [fastify.authenticate], }, async (request, reply) => { const { section } = request.params as { section: string }; - const { contentJson } = request.body as z.infer; + const { contentJson } = request.body as any; // Check if section exists const [existing] = await db @@ -87,7 +92,7 @@ const contentRoute: FastifyPluginAsync = async (fastify) => { const sections = await db.select().from(contentSections); return { - sections: sections.map(s => ({ + sections: (sections as any[]).map((s: any) => ({ section: s.sectionName, content: s.contentJson, updatedAt: s.updatedAt, diff --git a/backend/src/routes/events.ts b/backend/src/routes/events.ts index 7e19920..b38c370 100644 --- a/backend/src/routes/events.ts +++ b/backend/src/routes/events.ts @@ -1,121 +1,87 @@ 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'; -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), -}); +// 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 eventsRoute: FastifyPluginAsync = async (fastify) => { - - // 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 }; + // List all events (by displayOrder) + fastify.get('/events', { preHandler: [fastify.authenticate] }, async () => { + const all = await db.select().from(events).orderBy(events.displayOrder); + return { events: all }; }); // 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 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] }; + 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] }; }); // Create event - 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 }); + 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 }); }); // Update event - fastify.put('/events/:id', { - schema: { - body: eventSchema, - }, - preHandler: [fastify.authenticate], - }, async (request, reply) => { + fastify.put('/events/:id', { schema: { body: eventBodyJsonSchema }, preHandler: [fastify.authenticate] }, async (request, reply) => { const { id } = request.params as { id: string }; - 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 }; + 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 }; }); // 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 [deleted] = await db - .delete(events) - .where(eq(events.id, id)) - .returning(); - - if (!deleted) { - return reply.code(404).send({ error: 'Event not found' }); - } - + const [row] = await db.delete(events).where(eq(events.id, id)).returning(); + if (!row) return reply.code(404).send({ error: 'Event not found' }); return { message: 'Event deleted successfully' }; }); - // 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) => { + // Reorder events (synchronous transaction for better-sqlite3) + fastify.put('/events/reorder', { schema: { body: reorderBodyJsonSchema }, preHandler: [fastify.authenticate] }, async (request) => { const { orders } = request.body as { orders: Array<{ id: string; displayOrder: number }> }; - - // Update all in transaction - await db.transaction(async (tx) => { + db.transaction((tx: any) => { for (const { id, displayOrder } of orders) { - await tx - .update(events) - .set({ displayOrder }) - .where(eq(events.id, id)); + tx.update(events).set({ displayOrder }).where(eq(events.id, id)).run?.(); } }); - return { message: 'Events reordered successfully' }; }); }; diff --git a/backend/src/routes/gallery.ts b/backend/src/routes/gallery.ts index a3cda3b..8f61eaa 100644 --- a/backend/src/routes/gallery.ts +++ b/backend/src/routes/gallery.ts @@ -4,12 +4,17 @@ import { db } from '../config/database.js'; import { galleryImages } from '../db/schema.js'; import { eq } from 'drizzle-orm'; -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), -}); +// 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 galleryRoute: FastifyPluginAsync = async (fastify) => { @@ -38,11 +43,11 @@ const galleryRoute: FastifyPluginAsync = async (fastify) => { // Create gallery image fastify.post('/gallery', { schema: { - body: galleryImageSchema, + body: galleryBodyJsonSchema, }, preHandler: [fastify.authenticate], }, async (request, reply) => { - const data = request.body as z.infer; + const data = request.body as any; const [newImage] = await db.insert(galleryImages).values(data).returning(); @@ -52,12 +57,12 @@ const galleryRoute: FastifyPluginAsync = async (fastify) => { // Update gallery image fastify.put('/gallery/:id', { schema: { - body: galleryImageSchema, + body: galleryBodyJsonSchema, }, preHandler: [fastify.authenticate], }, async (request, reply) => { const { id } = request.params as { id: string }; - const data = request.body as z.infer; + const data = request.body as any; const [updated] = await db .update(galleryImages) @@ -93,24 +98,32 @@ const galleryRoute: FastifyPluginAsync = async (fastify) => { // Reorder gallery images fastify.put('/gallery/reorder', { schema: { - body: z.object({ - orders: z.array(z.object({ - id: z.string().uuid(), - displayOrder: z.number().int().min(0), - })), - }), + body: { + type: 'object', + required: ['orders'], + properties: { + orders: { + type: 'array', + items: { + type: 'object', + required: ['id', 'displayOrder'], + properties: { + id: { type: 'string' }, + displayOrder: { type: 'integer', minimum: 0 }, + }, + }, + }, + }, + }, }, preHandler: [fastify.authenticate], }, async (request, reply) => { const { orders } = request.body as { orders: Array<{ id: string; displayOrder: number }> }; - // Update all in transaction - await db.transaction(async (tx) => { + // Update all in synchronous transaction (better-sqlite3 requirement) + db.transaction((tx: any) => { for (const { id, displayOrder } of orders) { - await tx - .update(galleryImages) - .set({ displayOrder }) - .where(eq(galleryImages.id, id)); + tx.update(galleryImages).set({ displayOrder }).where(eq(galleryImages.id, id)).run?.(); } }); diff --git a/backend/src/routes/publish.ts b/backend/src/routes/publish.ts index 5da211d..81dc49b 100644 --- a/backend/src/routes/publish.ts +++ b/backend/src/routes/publish.ts @@ -6,19 +6,24 @@ import { db } from '../config/database.js'; import { events, galleryImages, contentSections, publishHistory } from '../db/schema.js'; import { eq } from 'drizzle-orm'; -const publishSchema = z.object({ - commitMessage: z.string().min(1).max(200), -}); +// Fastify JSON schema for publish body +const publishBodyJsonSchema = { + type: 'object', + required: ['commitMessage'], + properties: { + commitMessage: { type: 'string', minLength: 1, maxLength: 200 }, + }, +} as const; const publishRoute: FastifyPluginAsync = async (fastify) => { fastify.post('/publish', { schema: { - body: publishSchema, + body: publishBodyJsonSchema, }, preHandler: [fastify.authenticate], }, async (request, reply) => { try { - const { commitMessage } = request.body as z.infer; + const { commitMessage } = request.body as any; const userId = request.user.id; fastify.log.info('Starting publish process...'); @@ -43,8 +48,8 @@ const publishRoute: FastifyPluginAsync = async (fastify) => { .orderBy(galleryImages.displayOrder); const sectionsData = await db.select().from(contentSections); - const sectionsMap = new Map( - sectionsData.map(s => [s.sectionName, s.contentJson as any]) + const sectionsMap = new Map( + (sectionsData as any[]).map((s: any) => [s.sectionName as string, s.contentJson as any]) ); fastify.log.info(`Fetched ${eventsData.length} events, ${galleryData.length} images, ${sectionsData.length} sections`); @@ -53,13 +58,13 @@ const publishRoute: FastifyPluginAsync = async (fastify) => { const fileGenerator = new FileGeneratorService(); await fileGenerator.writeFiles( gitService.getWorkspacePath(''), - eventsData.map(e => ({ + (eventsData as any[]).map((e: any) => ({ title: e.title, date: e.date, description: e.description, imageUrl: e.imageUrl, })), - galleryData.map(g => ({ + (galleryData as any[]).map((g: any) => ({ imageUrl: g.imageUrl, altText: g.altText, })), @@ -87,14 +92,14 @@ const publishRoute: FastifyPluginAsync = async (fastify) => { }; } catch (error) { - fastify.log.error('Publish error:', error); + fastify.log.error({ err: error }, 'Publish error'); // Attempt to reset git state on error try { const gitService = new GitService(); await gitService.reset(); } catch (resetError) { - fastify.log.error('Failed to reset git state:', resetError); + fastify.log.error({ err: resetError }, 'Failed to reset git state'); } return reply.code(500).send({ diff --git a/backend/src/routes/settings.ts b/backend/src/routes/settings.ts index 6a5a751..8fc84d1 100644 --- a/backend/src/routes/settings.ts +++ b/backend/src/routes/settings.ts @@ -4,9 +4,14 @@ import { db } from '../config/database.js'; import { siteSettings } from '../db/schema.js'; import { eq } from 'drizzle-orm'; -const settingSchema = z.object({ - value: z.string(), -}); +// Fastify JSON schema for settings body +const settingBodyJsonSchema = { + type: 'object', + required: ['value'], + properties: { + value: { type: 'string' }, + }, +} as const; const settingsRoute: FastifyPluginAsync = async (fastify) => { @@ -50,12 +55,12 @@ const settingsRoute: FastifyPluginAsync = async (fastify) => { // Update setting fastify.put('/settings/:key', { schema: { - body: settingSchema, + body: settingBodyJsonSchema, }, preHandler: [fastify.authenticate], }, async (request, reply) => { const { key } = request.params as { key: string }; - const { value } = request.body as z.infer; + const { value } = request.body as any; // Check if setting exists const [existing] = await db diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..aeda1fd --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,38 @@ +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 6bb56b8..2dce8c5 100644 --- a/fly.toml +++ b/fly.toml @@ -4,11 +4,14 @@ kill_signal = "SIGINT" kill_timeout = 5 [build] - dockerfile = "Dockerfile" + dockerfile = "Dockerfile.fly" [env] - PORT = "3000" + PORT = "3000" # Caddy (serves frontend + proxies /api/*) NODE_ENV = "production" + BACKEND_PORT = "8080" # Fastify backend will listen here + DATABASE_PATH = "/app/data/gallus_cms.db" + GIT_WORKSPACE_DIR = "/app/workspace" [http_service] internal_port = 3000 @@ -39,4 +42,12 @@ kill_timeout = 5 [[vm]] memory = "512MB" cpu_kind = "shared" - cpus = 1 \ No newline at end of file + cpus = 1 + +[[mounts]] + source = "gallus_data" + destination = "/app/data" + +[[mounts]] + source = "gallus_workspace" + destination = "/app/workspace" \ No newline at end of file diff --git a/src/pages/admin.astro b/src/pages/admin.astro new file mode 100644 index 0000000..fbceae0 --- /dev/null +++ b/src/pages/admin.astro @@ -0,0 +1,291 @@ +--- +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 new file mode 100644 index 0000000..0cb55dd --- /dev/null +++ b/src/pages/auth/callback.astro @@ -0,0 +1,29 @@ +--- +const title = 'Anmeldung wird abgeschlossen...'; +--- + + + + + + {title} + + +

{title}

+ + +