Revert all changes back to commit e406c7ffce49ca3e6ca064bc92fefa96bc0acf34
This commit is contained in:
parent
321cbe8789
commit
3bb52d0927
72
Dockerfile
72
Dockerfile
@ -1,68 +1,64 @@
|
|||||||
# Dockerfile for Astro webapp with NGINX and API for content management
|
# Dockerfile for Astro webapp with NGINX and API for content management
|
||||||
# Stage 1: Build the Astro application
|
# Stage 1: Build the Astro application
|
||||||
# Fixed 'as' to 'AS' in the following line
|
FROM node:20-alpine as build
|
||||||
FROM node:20-alpine AS build
|
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy root package files. Assumes API dependencies are in the root package.json.
|
# Copy package files and install dependencies
|
||||||
COPY package.json package-lock.json* ./
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
# Install ALL dependencies (including for API) from root package-lock.json
|
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
# Copy the rest of the application code
|
# Copy the rest of the application code
|
||||||
# Ensure .dockerignore is properly set up to exclude node_modules etc. from host
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Build the Astro project (generates into /app/dist)
|
# Build the Astro project
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Stage 2: Serve with NGINX and Node API
|
# Stage 2: Serve with NGINX and Node API
|
||||||
FROM nginx:alpine
|
FROM nginx:alpine
|
||||||
|
|
||||||
# Create a non-root user and group
|
# Install Node.js for the API server
|
||||||
RUN addgroup -S appgroup && adduser -S -G appgroup appuser
|
RUN apk add --update nodejs npm
|
||||||
|
|
||||||
# Install Node.js for the API server (su-exec is not strictly needed if USER appuser is used for CMD)
|
# Set up directory structure
|
||||||
RUN apk add --no-cache nodejs npm # su-exec can be removed if not used elsewhere
|
|
||||||
|
|
||||||
# Set base working directory for the final stage
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy built static files from build stage to NGINX html directory
|
# Copy built static files from build stage to NGINX html directory
|
||||||
COPY --from=build /app/dist /usr/share/nginx/html
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
RUN chown -R appuser:appgroup /usr/share/nginx/html && chmod -R 755 /usr/share/nginx/html
|
|
||||||
|
|
||||||
# Copy API source files from build stage
|
# Copy API files
|
||||||
COPY --from=build /app/src/api /app/api
|
COPY --from=build /app/src/api /app/api
|
||||||
RUN chown -R appuser:appgroup /app/api
|
WORKDIR /app/api
|
||||||
|
RUN npm install --production
|
||||||
|
|
||||||
# Copy node_modules from the build stage (contains all dependencies)
|
# Add custom NGINX configuration with API proxy
|
||||||
# The API running in /app/api will be able to resolve modules from /app/node_modules
|
RUN echo 'server {\
|
||||||
COPY --from=build --chown=appuser:appgroup /app/node_modules /app/node_modules
|
listen 80;\
|
||||||
|
server_name _;\
|
||||||
|
root /usr/share/nginx/html;\
|
||||||
|
index index.html;\
|
||||||
|
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 custom NGINX configuration
|
# Copy start script
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
||||||
RUN chown appuser:appgroup /etc/nginx/conf.d/default.conf && chmod 644 /etc/nginx/conf.d/default.conf
|
|
||||||
RUN mkdir -p /var/log/nginx && chown -R appuser:appgroup /var/log/nginx
|
|
||||||
RUN mkdir -p /run/nginx && chown -R appuser:appgroup /run/nginx # For PID file
|
|
||||||
|
|
||||||
# Copy start script from build stage
|
|
||||||
COPY --from=build /app/start.sh /app/start.sh
|
COPY --from=build /app/start.sh /app/start.sh
|
||||||
RUN chmod +x /app/start.sh && chown appuser:appgroup /app/start.sh
|
RUN chmod +x /app/start.sh
|
||||||
|
|
||||||
# Create and set permissions for content directory
|
# Create a directory for content if it doesn't exist
|
||||||
# This WORKDIR /app is important for relative paths in start.sh if any
|
RUN mkdir -p /app/content/books
|
||||||
WORKDIR /app
|
|
||||||
RUN mkdir -p ./content/books && chown -R appuser:appgroup ./content
|
|
||||||
|
|
||||||
# Expose port 80 (Nginx will listen on this port)
|
# Expose port 80
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
# Switch to non-root user for running the application
|
# Start NGINX and API server
|
||||||
USER appuser
|
|
||||||
|
|
||||||
# Start NGINX and API server using the start script
|
|
||||||
# Both processes will run as 'appuser'
|
|
||||||
CMD ["/app/start.sh"]
|
CMD ["/app/start.sh"]
|
||||||
|
|||||||
@ -4,6 +4,5 @@ import { defineConfig } from 'astro/config';
|
|||||||
import tailwind from '@astrojs/tailwind';
|
import tailwind from '@astrojs/tailwind';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
output: 'static',
|
|
||||||
integrations: [tailwind()]
|
integrations: [tailwind()]
|
||||||
});
|
});
|
||||||
32
nginx.conf
32
nginx.conf
@ -1,32 +0,0 @@
|
|||||||
http {
|
|
||||||
include /etc/nginx/mime.types;
|
|
||||||
default_type application/octet-stream;
|
|
||||||
|
|
||||||
sendfile on;
|
|
||||||
keepalive_timeout 65;
|
|
||||||
# gzip on; # Consider enabling gzip for better performance
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80 default_server;
|
|
||||||
server_name _;
|
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://localhost:3000/api/; # Node API listens on 3000
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -4,163 +4,79 @@ const path = require('path');
|
|||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const bodyParser = require('body-parser');
|
const bodyParser = require('body-parser');
|
||||||
const basicAuth = require('express-basic-auth');
|
const basicAuth = require('express-basic-auth');
|
||||||
const helmet = require('helmet'); // Added helmet
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
const CONTENT_DIR = process.env.CONTENT_DIR || path.join(__dirname, '../../content');
|
const CONTENT_DIR = process.env.CONTENT_DIR || path.join(__dirname, '../../content');
|
||||||
const BOOKS_DIR = path.join(CONTENT_DIR, 'books'); // Define books directory
|
const USERNAME = process.env.ADMIN_USERNAME || 'admin';
|
||||||
|
const PASSWORD = process.env.ADMIN_PASSWORD || 'password';
|
||||||
const USERNAME = process.env.ADMIN_USERNAME;
|
|
||||||
const PASSWORD = process.env.ADMIN_PASSWORD;
|
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
app.use(helmet({ contentSecurityPolicy: false })); // Added helmet, disable its CSP if Astro handles it
|
app.use(cors());
|
||||||
// For CORS, consider restricting the origin in production:
|
app.use(bodyParser.json());
|
||||||
// app.use(cors({ origin: 'https://your-admin-domain.com' }));
|
|
||||||
app.use(cors());
|
|
||||||
app.use(bodyParser.json({ limit: '1mb' })); // Added body parser size limit
|
|
||||||
|
|
||||||
// Basic authentication - only if credentials are set
|
// Basic authentication
|
||||||
// Commenting out API-level Basic Auth as Caddy reverse proxy will handle it.
|
app.use(basicAuth({
|
||||||
/*
|
users: { [USERNAME]: PASSWORD },
|
||||||
if (USERNAME && PASSWORD) {
|
challenge: true,
|
||||||
app.use(basicAuth({
|
realm: 'Content Admin'
|
||||||
users: { [USERNAME]: PASSWORD },
|
}));
|
||||||
challenge: true,
|
|
||||||
realm: 'Content Admin'
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
console.error('CRITICAL: ADMIN_USERNAME and/or ADMIN_PASSWORD environment variables are not set. API will not be secured.');
|
|
||||||
// Optionally, you could prevent the API routes from being registered or exit.
|
|
||||||
// For now, it will run unsecured if no credentials, which is a major risk.
|
|
||||||
// A better approach for production would be to throw an error and exit:
|
|
||||||
// throw new Error('Admin credentials not configured');
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Helper function for filename validation and path construction
|
|
||||||
function getSafeFilePath(filename) {
|
|
||||||
if (!filename || typeof filename !== 'string' || !/^[a-zA-Z0-9_-]+\.md$/.test(filename)) {
|
|
||||||
return { error: 'Invalid filename format. Must be alphanumeric, underscores, or hyphens, and end with .md.', path: null };
|
|
||||||
}
|
|
||||||
const cleanFilename = path.basename(filename); // Ensures no path characters are in the filename itself
|
|
||||||
if (cleanFilename !== filename) {
|
|
||||||
return { error: 'Invalid filename (contains path characters).', path: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolvedPath = path.join(BOOKS_DIR, cleanFilename);
|
|
||||||
|
|
||||||
// Final check to prevent escaping BOOKS_DIR
|
|
||||||
if (!resolvedPath.startsWith(BOOKS_DIR + path.sep)) {
|
|
||||||
return { error: 'Invalid path (attempt to escape content directory).', path: null };
|
|
||||||
}
|
|
||||||
return { error: null, path: resolvedPath };
|
|
||||||
}
|
|
||||||
|
|
||||||
// List all content files
|
// List all content files
|
||||||
app.get('/api/content', async (req, res) => {
|
app.get('/api/content', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await fs.mkdir(BOOKS_DIR, { recursive: true }); // Ensure directory exists
|
const files = await fs.readdir(path.join(CONTENT_DIR, 'books'));
|
||||||
const files = await fs.readdir(BOOKS_DIR);
|
|
||||||
const contentFiles = files.filter(file => file.endsWith('.md'));
|
const contentFiles = files.filter(file => file.endsWith('.md'));
|
||||||
res.json({ files: contentFiles });
|
res.json({ files: contentFiles });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error listing content:', error);
|
res.status(500).json({ error: error.message });
|
||||||
res.status(500).json({ error: 'Failed to list content.' });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get content by filename
|
// Get content by filename
|
||||||
app.get('/api/content/:filename', async (req, res) => {
|
app.get('/api/content/:filename', async (req, res) => {
|
||||||
const { error: filePathError, path: filePath } = getSafeFilePath(req.params.filename);
|
|
||||||
if (filePathError) {
|
|
||||||
return res.status(400).json({ error: filePathError });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const filePath = path.join(CONTENT_DIR, 'books', req.params.filename);
|
||||||
const content = await fs.readFile(filePath, 'utf-8');
|
const content = await fs.readFile(filePath, 'utf-8');
|
||||||
res.json({ content });
|
res.json({ content });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === 'ENOENT') {
|
res.status(404).json({ error: 'File not found' });
|
||||||
res.status(404).json({ error: 'File not found.' });
|
|
||||||
} else {
|
|
||||||
console.error(`Error reading file ${req.params.filename}:`, error);
|
|
||||||
res.status(500).json({ error: 'Failed to read file.' });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update content
|
// Update content
|
||||||
app.put('/api/content/:filename', async (req, res) => {
|
app.put('/api/content/:filename', async (req, res) => {
|
||||||
const { error: filePathError, path: filePath } = getSafeFilePath(req.params.filename);
|
|
||||||
if (filePathError) {
|
|
||||||
return res.status(400).json({ error: filePathError });
|
|
||||||
}
|
|
||||||
if (typeof req.body.content !== 'string') {
|
|
||||||
return res.status(400).json({ error: 'Invalid content.'});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.mkdir(BOOKS_DIR, { recursive: true }); // Ensure directory exists
|
const filePath = path.join(CONTENT_DIR, 'books', req.params.filename);
|
||||||
await fs.writeFile(filePath, req.body.content);
|
await fs.writeFile(filePath, req.body.content);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error writing file ${req.params.filename}:`, error);
|
res.status(500).json({ error: error.message });
|
||||||
res.status(500).json({ error: 'Failed to update file.' });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create new content file
|
// Create new content file
|
||||||
app.post('/api/content', async (req, res) => {
|
app.post('/api/content', async (req, res) => {
|
||||||
const { error: filePathError, path: filePath } = getSafeFilePath(req.body.filename);
|
|
||||||
if (filePathError) {
|
|
||||||
return res.status(400).json({ error: filePathError });
|
|
||||||
}
|
|
||||||
if (typeof req.body.content !== 'string') {
|
|
||||||
return res.status(400).json({ error: 'Invalid content.'});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.mkdir(BOOKS_DIR, { recursive: true }); // Ensure directory exists
|
const filePath = path.join(CONTENT_DIR, 'books', req.body.filename);
|
||||||
// Check if file already exists to prevent accidental overwrite via POST
|
|
||||||
try {
|
|
||||||
await fs.access(filePath);
|
|
||||||
return res.status(409).json({ error: 'File already exists. Use PUT to update.' });
|
|
||||||
} catch (accessError) {
|
|
||||||
// File does not exist, proceed to write
|
|
||||||
}
|
|
||||||
await fs.writeFile(filePath, req.body.content);
|
await fs.writeFile(filePath, req.body.content);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error creating file ${req.body.filename}:`, error);
|
res.status(500).json({ error: error.message });
|
||||||
res.status(500).json({ error: 'Failed to create file.' });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete content file
|
// Delete content file
|
||||||
app.delete('/api/content/:filename', async (req, res) => {
|
app.delete('/api/content/:filename', async (req, res) => {
|
||||||
const { error: filePathError, path: filePath } = getSafeFilePath(req.params.filename);
|
|
||||||
if (filePathError) {
|
|
||||||
return res.status(400).json({ error: filePathError });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const filePath = path.join(CONTENT_DIR, 'books', req.params.filename);
|
||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === 'ENOENT') {
|
res.status(500).json({ error: error.message });
|
||||||
res.status(404).json({ error: 'File not found.' });
|
|
||||||
} else {
|
|
||||||
console.error(`Error deleting file ${req.params.filename}:`, error);
|
|
||||||
res.status(500).json({ error: 'Failed to delete file.' });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`API server running on port ${PORT}`);
|
console.log(`API server running on port ${PORT}`);
|
||||||
if (!USERNAME || !PASSWORD) {
|
|
||||||
console.warn('WARNING: API is running WITHOUT authentication due to missing ADMIN_USERNAME or ADMIN_PASSWORD environment variables. This is a major security risk.');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
// src/middleware.js
|
|
||||||
export async function onRequest(context, next) {
|
|
||||||
const response = await next(); // Get the response from the next middleware or page
|
|
||||||
response.headers.set(
|
|
||||||
'Content-Security-Policy',
|
|
||||||
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; object-src 'none'; frame-ancestors 'none';"
|
|
||||||
);
|
|
||||||
return response; // Return the modified response
|
|
||||||
}
|
|
||||||
@ -55,7 +55,9 @@ import SiteLayout from '../../components/SiteLayout.astro';
|
|||||||
|
|
||||||
// Base API URL - adapt for production vs. development
|
// Base API URL - adapt for production vs. development
|
||||||
const getApiUrl = (path) => {
|
const getApiUrl = (path) => {
|
||||||
return `/api/${path}`; // Always use relative path for Nginx proxy
|
return window.location.hostname === 'localhost'
|
||||||
|
? `http://localhost:3000/api/${path}`
|
||||||
|
: `/api/${path}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load existing content if editing
|
// Load existing content if editing
|
||||||
@ -68,7 +70,7 @@ import SiteLayout from '../../components/SiteLayout.astro';
|
|||||||
|
|
||||||
const response = await fetch(getApiUrl(`content/${fileParam}`), {
|
const response = await fetch(getApiUrl(`content/${fileParam}`), {
|
||||||
headers: {
|
headers: {
|
||||||
// Authorization header removed; browser will handle Basic Auth prompt
|
'Authorization': 'Basic ' + btoa('admin:password') // In production, use proper auth
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -109,8 +111,8 @@ import SiteLayout from '../../components/SiteLayout.astro';
|
|||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: isNewFile ? 'POST' : 'PUT',
|
method: isNewFile ? 'POST' : 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
// Authorization header removed; browser will handle Basic Auth prompt
|
'Authorization': 'Basic ' + btoa('admin:password') // In production, use proper auth
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ filename, content })
|
body: JSON.stringify({ filename, content })
|
||||||
});
|
});
|
||||||
|
|||||||
@ -25,11 +25,13 @@ import SiteLayout from '../../components/SiteLayout.astro';
|
|||||||
// This would fetch from your API in production
|
// This would fetch from your API in production
|
||||||
async function fetchBooks() {
|
async function fetchBooks() {
|
||||||
try {
|
try {
|
||||||
const apiUrl = '/api/content'; // Always use relative path for Nginx proxy
|
const apiUrl = window.location.hostname === 'localhost'
|
||||||
|
? 'http://localhost:3000/api/content'
|
||||||
|
: '/api/content';
|
||||||
|
|
||||||
const response = await fetch(apiUrl, {
|
const response = await fetch(apiUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
// Authorization header removed; browser will handle Basic Auth prompt
|
'Authorization': 'Basic ' + btoa('admin:password') // In production, use proper auth
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -65,12 +67,14 @@ import SiteLayout from '../../components/SiteLayout.astro';
|
|||||||
const file = btn.getAttribute('data-file');
|
const file = btn.getAttribute('data-file');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const apiUrl = `/api/content/${file}`; // Always use relative path for Nginx proxy
|
const apiUrl = window.location.hostname === 'localhost'
|
||||||
|
? `http://localhost:3000/api/content/${file}`
|
||||||
|
: `/api/content/${file}`;
|
||||||
|
|
||||||
const response = await fetch(apiUrl, {
|
const response = await fetch(apiUrl, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Authorization': 'Basic ' + btoa('admin:password') // In production, use proper auth
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,49 @@
|
|||||||
---
|
---
|
||||||
// src/pages/admin/login.astro
|
import SiteLayout from '../../components/SiteLayout.astro';
|
||||||
// This page now just redirects to the main admin dashboard.
|
|
||||||
// The API's Basic Auth will protect the actual data.
|
let error = '';
|
||||||
return Astro.redirect('/admin');
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<SiteLayout title="Admin Login">
|
||||||
|
<div class="max-w-md mx-auto mt-10 p-6 bg-white rounded-lg shadow-md">
|
||||||
|
<h1 class="text-2xl font-bold mb-6 text-center">Content Admin Login</h1>
|
||||||
|
|
||||||
|
{error && <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">{error}</div>}
|
||||||
|
|
||||||
|
<form method="POST" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="username" class="block text-sm font-medium text-gray-700">Username</label>
|
||||||
|
<input type="text" id="username" name="username" required
|
||||||
|
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="password" class="block text-sm font-medium text-gray-700">Password</label>
|
||||||
|
<input type="password" id="password" name="password" required
|
||||||
|
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit"
|
||||||
|
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||||
|
Sign in
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</SiteLayout>
|
||||||
|
|||||||
29
start.sh
29
start.sh
@ -1,27 +1,6 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e # Exit immediately if a command exits with a non-zero status.
|
# Start API server
|
||||||
|
cd /app/api && node server.js &
|
||||||
|
|
||||||
# Navigate to API directory and start Node.js server in the background
|
# Start NGINX
|
||||||
# It will run as 'appuser' because of USER appuser in Dockerfile
|
nginx -g "daemon off;"
|
||||||
echo "Starting Node.js API server..."
|
|
||||||
cd /app/api
|
|
||||||
node server.js &
|
|
||||||
NODE_PID=$!
|
|
||||||
echo "Node.js API server started with PID $NODE_PID"
|
|
||||||
|
|
||||||
# Start NGINX in the foreground
|
|
||||||
# It will also run as 'appuser' (master and workers, due to USER appuser and nginx.conf user directive)
|
|
||||||
echo "Starting NGINX..."
|
|
||||||
nginx
|
|
||||||
NGINX_STATUS=$?
|
|
||||||
|
|
||||||
# If nginx exits, try to kill node process if it's still running
|
|
||||||
if ! kill -0 $NODE_PID > /dev/null 2>&1; then
|
|
||||||
echo "Node process already stopped."
|
|
||||||
else
|
|
||||||
echo "Nginx exited with status $NGINX_STATUS, stopping Node.js API server..."
|
|
||||||
kill $NODE_PID
|
|
||||||
wait $NODE_PID || true # Wait for node to stop, ignore error if already stopped
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit $NGINX_STATUS
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user