diff --git a/dist/.htaccess b/dist/.htaccess
index 97dff0b..6262eb2 100644
--- a/dist/.htaccess
+++ b/dist/.htaccess
@@ -1,25 +1,22 @@
-# Deployed under /citpl_website/ on Apache.
-# Requires: mod_rewrite, mod_headers, PHP curl
-# API: PHP proxy to Node on 127.0.0.1:3001 (start with: npm run server / pm2)
+# Deployed under /citpl_website/ — Apache must proxy API to the Node server.
+# Requires: mod_rewrite, mod_proxy, mod_proxy_http, mod_headers
+# Start Node on the host: npm run server (or pm2 start server/index.js)
RewriteEngine On
RewriteBase /citpl_website/
- # API -> PHP proxy (works without mod_proxy)
- RewriteRule ^api(?:/.*)?$ api/index.php [QSA,L]
-
- # Uploads -> PHP proxy
- RewriteRule ^uploads/(.*)$ uploads-proxy.php?file=$1 [QSA,L]
+ # Proxy API + uploads to Express (Node) on port 3001
+ RewriteRule ^api/(.*)$ http://127.0.0.1:3001/api/$1 [P,L]
+ RewriteRule ^uploads/(.*)$ http://127.0.0.1:3001/uploads/$1 [P,L]
-# Host security headers use frame-ancestors 'none' + X-Frame-Options DENY.
-# Edit them so admin can embed preview.html (same origin).
+# Allow admin login preview iframe (same origin)
Header always edit Content-Security-Policy "frame-ancestors 'none'" "frame-ancestors 'self'"
Header always edit X-Frame-Options "DENY" "SAMEORIGIN"
-
+
Header unset X-Frame-Options
Header always unset X-Frame-Options
Header unset Content-Security-Policy
diff --git a/dist/api/index.php b/dist/api/index.php
deleted file mode 100644
index 035169d..0000000
--- a/dist/api/index.php
+++ /dev/null
@@ -1,119 +0,0 @@
- $value) {
- $lower = strtolower((string) $name);
- if ($lower === 'host' || $lower === 'content-length') {
- continue;
- }
- $headers[] = $name . ': ' . $value;
- }
-} else {
- foreach ($_SERVER as $key => $value) {
- if (strpos($key, 'HTTP_') !== 0) {
- continue;
- }
- $name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5)))));
- if (strtolower($name) === 'host') {
- continue;
- }
- $headers[] = $name . ': ' . $value;
- }
- if (!empty($_SERVER['CONTENT_TYPE'])) {
- $headers[] = 'Content-Type: ' . $_SERVER['CONTENT_TYPE'];
- }
-}
-
-if (!function_exists('curl_init')) {
- http_response_code(500);
- header('Content-Type: application/json');
- echo json_encode([
- 'error' => 'PHP curl extension is required for the API proxy',
- 'code' => 'PROXY_MISCONFIGURED',
- ]);
- exit;
-}
-
-$ch = curl_init($url);
-curl_setopt_array($ch, [
- CURLOPT_CUSTOMREQUEST => $method,
- CURLOPT_RETURNTRANSFER => true,
- CURLOPT_HEADER => true,
- CURLOPT_FOLLOWLOCATION => false,
- CURLOPT_CONNECTTIMEOUT => 5,
- CURLOPT_TIMEOUT => 60,
- CURLOPT_HTTPHEADER => $headers,
-]);
-
-if ($method !== 'GET' && $method !== 'HEAD') {
- curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
-}
-
-$response = curl_exec($ch);
-if ($response === false) {
- $err = curl_error($ch);
- curl_close($ch);
- http_response_code(502);
- header('Content-Type: application/json');
- echo json_encode([
- 'error' => 'API server unreachable. On the host run: npm run server (port 3001)',
- 'detail' => $err,
- 'code' => 'BAD_GATEWAY',
- ]);
- exit;
-}
-
-$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
-$headerSize = (int) curl_getinfo($ch, CURLINFO_HEADER_SIZE);
-curl_close($ch);
-
-$rawHeaders = substr($response, 0, $headerSize);
-$responseBody = substr($response, $headerSize);
-
-http_response_code($status > 0 ? $status : 502);
-
-$skip = ['transfer-encoding', 'connection', 'keep-alive', 'content-length'];
-foreach (explode("\r\n", $rawHeaders) as $line) {
- if ($line === '' || stripos($line, 'HTTP/') === 0) {
- continue;
- }
- $parts = explode(':', $line, 2);
- if (count($parts) < 2) {
- continue;
- }
- $name = trim($parts[0]);
- if (in_array(strtolower($name), $skip, true)) {
- continue;
- }
- header($name . ':' . $parts[1], false);
-}
-
-echo $responseBody;
diff --git a/dist/uploads-proxy.php b/dist/uploads-proxy.php
deleted file mode 100644
index c64afd0..0000000
--- a/dist/uploads-proxy.php
+++ /dev/null
@@ -1,66 +0,0 @@
- 'Missing file', 'code' => 'VALIDATION']);
- exit;
-}
-
-$url = rtrim($apiOrigin, '/') . '/uploads/' . str_replace(' ', '%20', $file);
-
-if (!function_exists('curl_init')) {
- http_response_code(500);
- exit('curl required');
-}
-
-$ch = curl_init($url);
-curl_setopt_array($ch, [
- CURLOPT_RETURNTRANSFER => true,
- CURLOPT_HEADER => true,
- CURLOPT_FOLLOWLOCATION => false,
- CURLOPT_CONNECTTIMEOUT => 5,
- CURLOPT_TIMEOUT => 60,
-]);
-
-$response = curl_exec($ch);
-if ($response === false) {
- curl_close($ch);
- http_response_code(502);
- exit('Upload server unreachable');
-}
-
-$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
-$headerSize = (int) curl_getinfo($ch, CURLINFO_HEADER_SIZE);
-curl_close($ch);
-
-http_response_code($status > 0 ? $status : 502);
-$rawHeaders = substr($response, 0, $headerSize);
-$body = substr($response, $headerSize);
-
-$skip = ['transfer-encoding', 'connection', 'keep-alive'];
-foreach (explode("\r\n", $rawHeaders) as $line) {
- if ($line === '' || stripos($line, 'HTTP/') === 0) {
- continue;
- }
- $parts = explode(':', $line, 2);
- if (count($parts) < 2) {
- continue;
- }
- $name = trim($parts[0]);
- if (in_array(strtolower($name), $skip, true)) {
- continue;
- }
- header($name . ':' . $parts[1], false);
-}
-
-echo $body;
diff --git a/package.json b/package.json
index b6bc917..158e040 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,7 @@
"dev": "concurrently \"npm run server\" \"vite\"",
"dev:client": "vite",
"server": "node server/index.js",
+ "start": "node server/index.js",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
diff --git a/public/.htaccess b/public/.htaccess
index 97dff0b..6262eb2 100644
--- a/public/.htaccess
+++ b/public/.htaccess
@@ -1,25 +1,22 @@
-# Deployed under /citpl_website/ on Apache.
-# Requires: mod_rewrite, mod_headers, PHP curl
-# API: PHP proxy to Node on 127.0.0.1:3001 (start with: npm run server / pm2)
+# Deployed under /citpl_website/ — Apache must proxy API to the Node server.
+# Requires: mod_rewrite, mod_proxy, mod_proxy_http, mod_headers
+# Start Node on the host: npm run server (or pm2 start server/index.js)
RewriteEngine On
RewriteBase /citpl_website/
- # API -> PHP proxy (works without mod_proxy)
- RewriteRule ^api(?:/.*)?$ api/index.php [QSA,L]
-
- # Uploads -> PHP proxy
- RewriteRule ^uploads/(.*)$ uploads-proxy.php?file=$1 [QSA,L]
+ # Proxy API + uploads to Express (Node) on port 3001
+ RewriteRule ^api/(.*)$ http://127.0.0.1:3001/api/$1 [P,L]
+ RewriteRule ^uploads/(.*)$ http://127.0.0.1:3001/uploads/$1 [P,L]
-# Host security headers use frame-ancestors 'none' + X-Frame-Options DENY.
-# Edit them so admin can embed preview.html (same origin).
+# Allow admin login preview iframe (same origin)
Header always edit Content-Security-Policy "frame-ancestors 'none'" "frame-ancestors 'self'"
Header always edit X-Frame-Options "DENY" "SAMEORIGIN"
-
+
Header unset X-Frame-Options
Header always unset X-Frame-Options
Header unset Content-Security-Policy
diff --git a/public/api/index.php b/public/api/index.php
deleted file mode 100644
index 035169d..0000000
--- a/public/api/index.php
+++ /dev/null
@@ -1,119 +0,0 @@
- $value) {
- $lower = strtolower((string) $name);
- if ($lower === 'host' || $lower === 'content-length') {
- continue;
- }
- $headers[] = $name . ': ' . $value;
- }
-} else {
- foreach ($_SERVER as $key => $value) {
- if (strpos($key, 'HTTP_') !== 0) {
- continue;
- }
- $name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5)))));
- if (strtolower($name) === 'host') {
- continue;
- }
- $headers[] = $name . ': ' . $value;
- }
- if (!empty($_SERVER['CONTENT_TYPE'])) {
- $headers[] = 'Content-Type: ' . $_SERVER['CONTENT_TYPE'];
- }
-}
-
-if (!function_exists('curl_init')) {
- http_response_code(500);
- header('Content-Type: application/json');
- echo json_encode([
- 'error' => 'PHP curl extension is required for the API proxy',
- 'code' => 'PROXY_MISCONFIGURED',
- ]);
- exit;
-}
-
-$ch = curl_init($url);
-curl_setopt_array($ch, [
- CURLOPT_CUSTOMREQUEST => $method,
- CURLOPT_RETURNTRANSFER => true,
- CURLOPT_HEADER => true,
- CURLOPT_FOLLOWLOCATION => false,
- CURLOPT_CONNECTTIMEOUT => 5,
- CURLOPT_TIMEOUT => 60,
- CURLOPT_HTTPHEADER => $headers,
-]);
-
-if ($method !== 'GET' && $method !== 'HEAD') {
- curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
-}
-
-$response = curl_exec($ch);
-if ($response === false) {
- $err = curl_error($ch);
- curl_close($ch);
- http_response_code(502);
- header('Content-Type: application/json');
- echo json_encode([
- 'error' => 'API server unreachable. On the host run: npm run server (port 3001)',
- 'detail' => $err,
- 'code' => 'BAD_GATEWAY',
- ]);
- exit;
-}
-
-$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
-$headerSize = (int) curl_getinfo($ch, CURLINFO_HEADER_SIZE);
-curl_close($ch);
-
-$rawHeaders = substr($response, 0, $headerSize);
-$responseBody = substr($response, $headerSize);
-
-http_response_code($status > 0 ? $status : 502);
-
-$skip = ['transfer-encoding', 'connection', 'keep-alive', 'content-length'];
-foreach (explode("\r\n", $rawHeaders) as $line) {
- if ($line === '' || stripos($line, 'HTTP/') === 0) {
- continue;
- }
- $parts = explode(':', $line, 2);
- if (count($parts) < 2) {
- continue;
- }
- $name = trim($parts[0]);
- if (in_array(strtolower($name), $skip, true)) {
- continue;
- }
- header($name . ':' . $parts[1], false);
-}
-
-echo $responseBody;
diff --git a/public/uploads-proxy.php b/public/uploads-proxy.php
deleted file mode 100644
index c64afd0..0000000
--- a/public/uploads-proxy.php
+++ /dev/null
@@ -1,66 +0,0 @@
- 'Missing file', 'code' => 'VALIDATION']);
- exit;
-}
-
-$url = rtrim($apiOrigin, '/') . '/uploads/' . str_replace(' ', '%20', $file);
-
-if (!function_exists('curl_init')) {
- http_response_code(500);
- exit('curl required');
-}
-
-$ch = curl_init($url);
-curl_setopt_array($ch, [
- CURLOPT_RETURNTRANSFER => true,
- CURLOPT_HEADER => true,
- CURLOPT_FOLLOWLOCATION => false,
- CURLOPT_CONNECTTIMEOUT => 5,
- CURLOPT_TIMEOUT => 60,
-]);
-
-$response = curl_exec($ch);
-if ($response === false) {
- curl_close($ch);
- http_response_code(502);
- exit('Upload server unreachable');
-}
-
-$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
-$headerSize = (int) curl_getinfo($ch, CURLINFO_HEADER_SIZE);
-curl_close($ch);
-
-http_response_code($status > 0 ? $status : 502);
-$rawHeaders = substr($response, 0, $headerSize);
-$body = substr($response, $headerSize);
-
-$skip = ['transfer-encoding', 'connection', 'keep-alive'];
-foreach (explode("\r\n", $rawHeaders) as $line) {
- if ($line === '' || stripos($line, 'HTTP/') === 0) {
- continue;
- }
- $parts = explode(':', $line, 2);
- if (count($parts) < 2) {
- continue;
- }
- $name = trim($parts[0]);
- if (in_array(strtolower($name), $skip, true)) {
- continue;
- }
- header($name . ':' . $parts[1], false);
-}
-
-echo $body;
diff --git a/server/db.js b/server/db.js
index bfada3d..c500285 100644
--- a/server/db.js
+++ b/server/db.js
@@ -34,14 +34,15 @@ export function initDb() {
console.log('Seeded content database from default-content.json');
}
- if (!fs.existsSync(ADMIN_FILE)) {
- const password = process.env.ADMIN_PASSWORD || 'admin123';
- const hash = bcrypt.hashSync(password, 12);
+ // Keep login credentials in sync with .env (ADMIN_USERNAME / ADMIN_PASSWORD)
+ const username = process.env.ADMIN_USERNAME || 'admin';
+ const password = process.env.ADMIN_PASSWORD || 'admin123';
+ if (process.env.ADMIN_USERNAME || process.env.ADMIN_PASSWORD || !fs.existsSync(ADMIN_FILE)) {
writeJson(ADMIN_FILE, {
- username: process.env.ADMIN_USERNAME || 'admin',
- passwordHash: hash,
+ username,
+ passwordHash: bcrypt.hashSync(password, 12),
});
- console.log(`Admin user created (username: ${process.env.ADMIN_USERNAME || 'admin'})`);
+ console.log(`Admin credentials loaded from .env (username: ${username})`);
}
}
diff --git a/server/index.js b/server/index.js
index 5ce2c95..e485ff4 100644
--- a/server/index.js
+++ b/server/index.js
@@ -2,6 +2,7 @@ import 'dotenv/config';
import express from 'express';
import cors from 'cors';
import path from 'path';
+import fs from 'fs';
import { fileURLToPath } from 'url';
import { initDb } from './db.js';
import authRoutes from './routes/auth.js';
@@ -12,6 +13,13 @@ import multer from 'multer';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PORT = process.env.PORT || 3001;
const UPLOAD_DIR = process.env.UPLOAD_DIR || path.join(__dirname, 'uploads');
+// Public URL prefix on Apache (e.g. /citpl_website). Leave empty if site is at domain root.
+const SITE_BASE = (process.env.SITE_BASE || '/citpl_website').replace(/\/$/, '');
+const distDir = path.join(__dirname, '..', 'dist');
+const serveStatic =
+ process.env.SERVE_STATIC === '1' ||
+ process.env.SERVE_STATIC === 'true' ||
+ (process.env.SERVE_STATIC !== '0' && fs.existsSync(distDir));
initDb();
@@ -23,22 +31,30 @@ app.use(cors({
}));
app.use(express.json({ limit: '10mb' }));
-app.use('/uploads', express.static(UPLOAD_DIR));
-app.get('/api/health', (_req, res) => {
- res.json({ status: 'ok', timestamp: new Date().toISOString() });
-});
+function mountApp(base) {
+ const prefix = base || '';
-app.use('/api/auth', authRoutes);
-app.use('/api/content', contentRoutes);
-app.use('/api/upload', uploadRoutes);
+ app.use(`${prefix}/uploads`, express.static(UPLOAD_DIR));
-// Optional: serve Vite dist from Node (set SERVE_STATIC=1). Useful when Apache
-// proxies the whole /citpl_website path to this process.
-if (process.env.SERVE_STATIC === '1') {
- const distDir = path.join(__dirname, '..', 'dist');
- app.use(express.static(distDir));
- app.use('/citpl_website', express.static(distDir));
+ app.get(`${prefix}/api/health`, (_req, res) => {
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
+ });
+
+ app.use(`${prefix}/api/auth`, authRoutes);
+ app.use(`${prefix}/api/content`, contentRoutes);
+ app.use(`${prefix}/api/upload`, uploadRoutes);
+
+ if (serveStatic) {
+ app.use(prefix || '/', express.static(distDir));
+ }
+}
+
+// Root paths — used when Apache proxies /citpl_website/api → http://127.0.0.1:3001/api
+mountApp('');
+// Prefixed paths — used when Apache proxies /citpl_website → http://127.0.0.1:3001/citpl_website
+if (SITE_BASE) {
+ mountApp(SITE_BASE);
}
app.use((err, _req, res, _next) => {
@@ -50,5 +66,10 @@ app.use((err, _req, res, _next) => {
});
app.listen(PORT, () => {
- console.log(`API server running on http://localhost:${PORT}`);
+ console.log(`CITPL Node server on http://localhost:${PORT}`);
+ console.log(` API: /api/* and ${SITE_BASE || ''}/api/*`);
+ console.log(` Auth: ADMIN_USERNAME from .env (${process.env.ADMIN_USERNAME || 'admin'})`);
+ if (serveStatic) {
+ console.log(` Static: ${distDir}`);
+ }
});