diff --git a/Dockerfile b/Dockerfile index c770ee8..e374d52 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# Dockerfile for Astro webapp with NGINX for production +# Dockerfile for Astro webapp with NGINX and API for content management # Stage 1: Build the Astro application FROM node:20-alpine as build @@ -15,13 +15,24 @@ COPY . . # Build the Astro project RUN npm run build -# Stage 2: Serve with NGINX +# Stage 2: Serve with NGINX and Node API FROM nginx:alpine +# Install Node.js for the API server +RUN apk add --update nodejs npm + +# Set up directory structure +WORKDIR /app + # Copy built static files from build stage to NGINX html directory COPY --from=build /app/dist /usr/share/nginx/html -# Add custom NGINX configuration if needed +# Copy API files +COPY --from=build /app/src/api /app/api +WORKDIR /app/api +RUN npm install --production + +# Add custom NGINX configuration with API proxy RUN echo 'server {\ listen 80;\ server_name _;\ @@ -30,10 +41,24 @@ RUN echo 'server {\ location / {\ try_files $uri $uri/ /index.html;\ }\ + location /api/ {\ + proxy_pass http://localhost:3000/api/;\ + proxy_http_version 1.1;\ + proxy_set_header Upgrade $http_upgrade;\ + proxy_set_header Connection "upgrade";\ + proxy_set_header Host $host;\ + }\ }' > /etc/nginx/conf.d/default.conf +# Copy start script +COPY --from=build /app/start.sh /app/start.sh +RUN chmod +x /app/start.sh + +# Create a directory for content if it doesn't exist +RUN mkdir -p /app/content/books + # Expose port 80 EXPOSE 80 -# Start NGINX -CMD ["nginx", "-g", "daemon off;"] +# Start NGINX and API server +CMD ["/app/start.sh"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e236ab4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3' + +services: + astro-app: + build: . + ports: + - "80:80" + environment: + - ADMIN_USERNAME=admin + - ADMIN_PASSWORD=password + - CONTENT_DIR=/app/content + volumes: + - content-data:/app/content + +volumes: + content-data: + # This ensures the volume persists across container restarts diff --git a/src/api/package.json b/src/api/package.json new file mode 100644 index 0000000..61b1059 --- /dev/null +++ b/src/api/package.json @@ -0,0 +1,15 @@ +{ + "name": "content-api", + "version": "1.0.0", + "description": "API for content management", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "express": "^4.18.2", + "body-parser": "^1.20.2", + "cors": "^2.8.5", + "express-basic-auth": "^1.2.1" + } +} diff --git a/src/api/server.js b/src/api/server.js new file mode 100644 index 0000000..9f3e526 --- /dev/null +++ b/src/api/server.js @@ -0,0 +1,82 @@ +const express = require('express'); +const fs = require('fs/promises'); +const path = require('path'); +const cors = require('cors'); +const bodyParser = require('body-parser'); +const basicAuth = require('express-basic-auth'); + +const app = express(); +const PORT = process.env.PORT || 3000; +const CONTENT_DIR = process.env.CONTENT_DIR || path.join(__dirname, '../../content'); +const USERNAME = process.env.ADMIN_USERNAME || 'admin'; +const PASSWORD = process.env.ADMIN_PASSWORD || 'password'; + +// Middleware +app.use(cors()); +app.use(bodyParser.json()); + +// Basic authentication +app.use(basicAuth({ + users: { [USERNAME]: PASSWORD }, + challenge: true, + realm: 'Content Admin' +})); + +// List all content files +app.get('/api/content', async (req, res) => { + try { + const files = await fs.readdir(path.join(CONTENT_DIR, 'books')); + const contentFiles = files.filter(file => file.endsWith('.md')); + res.json({ files: contentFiles }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Get content by filename +app.get('/api/content/:filename', async (req, res) => { + try { + const filePath = path.join(CONTENT_DIR, 'books', req.params.filename); + const content = await fs.readFile(filePath, 'utf-8'); + res.json({ content }); + } catch (error) { + res.status(404).json({ error: 'File not found' }); + } +}); + +// Update content +app.put('/api/content/:filename', async (req, res) => { + try { + const filePath = path.join(CONTENT_DIR, 'books', req.params.filename); + await fs.writeFile(filePath, req.body.content); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Create new content file +app.post('/api/content', async (req, res) => { + try { + const filePath = path.join(CONTENT_DIR, 'books', req.body.filename); + await fs.writeFile(filePath, req.body.content); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Delete content file +app.delete('/api/content/:filename', async (req, res) => { + try { + const filePath = path.join(CONTENT_DIR, 'books', req.params.filename); + await fs.unlink(filePath); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.listen(PORT, () => { + console.log(`API server running on port ${PORT}`); +}); diff --git a/src/pages/admin/edit.astro b/src/pages/admin/edit.astro new file mode 100644 index 0000000..2834787 --- /dev/null +++ b/src/pages/admin/edit.astro @@ -0,0 +1,136 @@ +--- +import SiteLayout from '../../components/SiteLayout.astro'; + +// In a real app, you'd check authentication here +--- + + +
+
+

Edit Book

+ Back to Dashboard +
+ +
+
+
+ + +

Include .md extension (e.g., my-book.md)

+
+ +
+ + +
+ +
+ +
+
+
+ +
+

Preview

+
+

Type in the editor to see a preview...

+
+
+
+ + +
diff --git a/src/pages/admin/index.astro b/src/pages/admin/index.astro new file mode 100644 index 0000000..eb445b8 --- /dev/null +++ b/src/pages/admin/index.astro @@ -0,0 +1,102 @@ +--- +import SiteLayout from '../../components/SiteLayout.astro'; +--- + + +
+

Content Management

+ + + +
+

Your Books

+ +
+

Loading books...

+
+
+
+ + +
diff --git a/src/pages/admin/login.astro b/src/pages/admin/login.astro new file mode 100644 index 0000000..ff87482 --- /dev/null +++ b/src/pages/admin/login.astro @@ -0,0 +1,49 @@ +--- +import SiteLayout from '../../components/SiteLayout.astro'; + +let error = ''; +if (Astro.request.method === 'POST') { + try { + const data = await Astro.request.formData(); + const username = data.get('username'); + const password = data.get('password'); + + // Simple client-side auth - real auth happens in the API + if (username === 'admin' && password === 'password') { + // Store authentication in a cookie or localStorage in a real app + return Astro.redirect('/admin'); + } else { + error = 'Invalid username or password'; + } + } catch (e) { + error = 'An error occurred during login'; + } +} +--- + + +
+

Content Admin Login

+ + {error &&
{error}
} + +
+
+ + +
+ +
+ + +
+ + +
+
+
diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..7bf2553 --- /dev/null +++ b/start.sh @@ -0,0 +1,6 @@ +#!/bin/sh +# Start API server +cd /app/api && node server.js & + +# Start NGINX +nginx -g "daemon off;"