woodpecker soll nun auch das backend deployen
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
This commit is contained in:
@ -1,5 +1,5 @@
|
|||||||
steps:
|
steps:
|
||||||
deploy:
|
deploy_frontend:
|
||||||
image: node:20
|
image: node:20
|
||||||
environment:
|
environment:
|
||||||
FLY_API_TOKEN:
|
FLY_API_TOKEN:
|
||||||
@ -7,7 +7,20 @@ 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
|
||||||
|
|
||||||
|
build_and_deploy_backend:
|
||||||
|
image: node:20
|
||||||
|
environment:
|
||||||
|
FLY_API_TOKEN:
|
||||||
|
from_secret: FLY_API_TOKEN
|
||||||
|
commands:
|
||||||
|
- cd backend
|
||||||
|
- npm ci
|
||||||
|
- npm run build
|
||||||
|
- 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
|
||||||
|
|
||||||
when:
|
when:
|
||||||
branch:
|
branch:
|
||||||
|
|||||||
@ -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,12 @@ fastify.register(multipart, {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Serve static files (uploaded images, etc.)
|
||||||
|
fastify.register(fastifyStatic, {
|
||||||
|
root: path.join(process.cwd(), 'public'),
|
||||||
|
prefix: '/static/',
|
||||||
|
});
|
||||||
|
|
||||||
// Decorate fastify with authenticate method
|
// Decorate fastify with authenticate method
|
||||||
fastify.decorate('authenticate', authenticate);
|
fastify.decorate('authenticate', authenticate);
|
||||||
|
|
||||||
@ -99,6 +108,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
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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 = {
|
||||||
@ -54,6 +57,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
|
||||||
|
const publicRoot = path.join(process.cwd(), 'public');
|
||||||
|
const uploadDir = path.join(publicRoot, '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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user