path correction
This commit is contained in:
@@ -6,8 +6,16 @@ CORS_ORIGIN=http://localhost:5173
|
|||||||
UPLOAD_DIR=./server/uploads
|
UPLOAD_DIR=./server/uploads
|
||||||
DATA_DIR=./server/data
|
DATA_DIR=./server/data
|
||||||
|
|
||||||
|
# Production (demo.cavinkare.in/citpl_website):
|
||||||
|
# 1. Copy dist/ + server/ + package.json + .env to the host
|
||||||
|
# 2. npm install --omit=dev && npm run server (or pm2 start server/index.js)
|
||||||
|
# 3. Ensure Apache serves dist/ at /citpl_website/ with public/.htaccess
|
||||||
|
# (PHP api/index.php proxies /api → 127.0.0.1:3001)
|
||||||
|
# CORS_ORIGIN=https://demo.cavinkare.in
|
||||||
|
|
||||||
# Leave unset to auto-use Vite base:
|
# Leave unset to auto-use Vite base:
|
||||||
# local: /api/...
|
# local: /api/...
|
||||||
# prod: /citpl_website/api/...
|
# prod: /citpl_website/api/...
|
||||||
# Only set if the API is on a different host than the site.
|
# Only set if the API is on a different host than the site.
|
||||||
# VITE_API_BASE=
|
# VITE_API_BASE=
|
||||||
|
# SERVE_STATIC=1
|
||||||
|
|||||||
Vendored
+22
-7
@@ -1,15 +1,30 @@
|
|||||||
# Deployed under /citpl_website/ on Apache.
|
# Deployed under /citpl_website/ on Apache.
|
||||||
# Requires: mod_rewrite, mod_headers, and (for API) mod_proxy + mod_proxy_http
|
# Requires: mod_rewrite, mod_headers, PHP curl
|
||||||
|
# API: PHP proxy to Node on 127.0.0.1:3001 (start with: npm run server / pm2)
|
||||||
|
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
RewriteEngine On
|
RewriteEngine On
|
||||||
|
RewriteBase /citpl_website/
|
||||||
|
|
||||||
# Proxy Node API + uploads (start server with: node server/index.js)
|
# API -> PHP proxy (works without mod_proxy)
|
||||||
RewriteRule ^api/(.*)$ http://127.0.0.1:3001/api/$1 [P,L]
|
RewriteRule ^api(?:/.*)?$ api/index.php [QSA,L]
|
||||||
RewriteRule ^uploads/(.*)$ http://127.0.0.1:3001/uploads/$1 [P,L]
|
|
||||||
|
|
||||||
# Allow admin login/section preview to iframe this page (same origin)
|
# Uploads -> PHP proxy
|
||||||
<Files "preview.html">
|
RewriteRule ^uploads/(.*)$ uploads-proxy.php?file=$1 [QSA,L]
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# Host security headers use frame-ancestors 'none' + X-Frame-Options DENY.
|
||||||
|
# Edit them so admin can embed preview.html (same origin).
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
Header always edit Content-Security-Policy "frame-ancestors 'none'" "frame-ancestors 'self'"
|
||||||
|
Header always edit X-Frame-Options "DENY" "SAMEORIGIN"
|
||||||
|
|
||||||
|
<FilesMatch "^(preview\.html|preview\.php|index\.html)$">
|
||||||
|
Header unset X-Frame-Options
|
||||||
Header always unset X-Frame-Options
|
Header always unset X-Frame-Options
|
||||||
|
Header unset Content-Security-Policy
|
||||||
Header always unset Content-Security-Policy
|
Header always unset Content-Security-Policy
|
||||||
|
Header always set X-Frame-Options "SAMEORIGIN"
|
||||||
Header always set Content-Security-Policy "frame-ancestors 'self'"
|
Header always set Content-Security-Policy "frame-ancestors 'self'"
|
||||||
</Files>
|
</FilesMatch>
|
||||||
|
</IfModule>
|
||||||
|
|||||||
Vendored
+2
-2
@@ -4,9 +4,9 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>CITPL Admin</title>
|
<title>CITPL Admin</title>
|
||||||
<script type="module" crossorigin src="/citpl_website/assets/admin-C5-foetU.js"></script>
|
<script type="module" crossorigin src="/citpl_website/assets/admin-Blm7TeBi.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/citpl_website/assets/x-DnG5sti3.js">
|
<link rel="modulepreload" crossorigin href="/citpl_website/assets/x-DnG5sti3.js">
|
||||||
<link rel="stylesheet" crossorigin href="/citpl_website/assets/admin-Chd5uVZT.css">
|
<link rel="stylesheet" crossorigin href="/citpl_website/assets/admin-yhMCgkf-.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
Vendored
+119
@@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Forward /citpl_website/api/* → Node (Express) without requiring Apache mod_proxy.
|
||||||
|
* Start the API on the server: cd /path/to/app && npm run server
|
||||||
|
* Or: pm2 start server/index.js --name citpl-api
|
||||||
|
*/
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$apiOrigin = getenv('CITPL_API_ORIGIN') ?: 'http://127.0.0.1:3001';
|
||||||
|
|
||||||
|
$requestUri = $_SERVER['REQUEST_URI'] ?? '/api';
|
||||||
|
$path = parse_url($requestUri, PHP_URL_PATH) ?: '/api';
|
||||||
|
|
||||||
|
// Keep everything from "/api" onward (works under /citpl_website/api/...)
|
||||||
|
if (preg_match('#(/api(?:/.*)?)$#', $path, $m)) {
|
||||||
|
$forwardPath = $m[1];
|
||||||
|
} else {
|
||||||
|
$forwardPath = '/api';
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $_SERVER['QUERY_STRING'] ?? '';
|
||||||
|
$url = rtrim($apiOrigin, '/') . $forwardPath . ($query !== '' ? '?' . $query : '');
|
||||||
|
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||||
|
$body = file_get_contents('php://input');
|
||||||
|
if ($body === false) {
|
||||||
|
$body = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$headers = [];
|
||||||
|
if (function_exists('getallheaders')) {
|
||||||
|
foreach (getallheaders() as $name => $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;
|
||||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+66
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Forward /citpl_website/uploads/* → Node static uploads (no mod_proxy required).
|
||||||
|
*/
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$apiOrigin = getenv('CITPL_API_ORIGIN') ?: 'http://127.0.0.1:3001';
|
||||||
|
$file = $_GET['file'] ?? '';
|
||||||
|
$file = str_replace(['..', '\\'], '', $file);
|
||||||
|
$file = ltrim($file, '/');
|
||||||
|
|
||||||
|
if ($file === '') {
|
||||||
|
http_response_code(400);
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['error' => '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;
|
||||||
+22
-7
@@ -1,15 +1,30 @@
|
|||||||
# Deployed under /citpl_website/ on Apache.
|
# Deployed under /citpl_website/ on Apache.
|
||||||
# Requires: mod_rewrite, mod_headers, and (for API) mod_proxy + mod_proxy_http
|
# Requires: mod_rewrite, mod_headers, PHP curl
|
||||||
|
# API: PHP proxy to Node on 127.0.0.1:3001 (start with: npm run server / pm2)
|
||||||
|
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
RewriteEngine On
|
RewriteEngine On
|
||||||
|
RewriteBase /citpl_website/
|
||||||
|
|
||||||
# Proxy Node API + uploads (start server with: node server/index.js)
|
# API -> PHP proxy (works without mod_proxy)
|
||||||
RewriteRule ^api/(.*)$ http://127.0.0.1:3001/api/$1 [P,L]
|
RewriteRule ^api(?:/.*)?$ api/index.php [QSA,L]
|
||||||
RewriteRule ^uploads/(.*)$ http://127.0.0.1:3001/uploads/$1 [P,L]
|
|
||||||
|
|
||||||
# Allow admin login/section preview to iframe this page (same origin)
|
# Uploads -> PHP proxy
|
||||||
<Files "preview.html">
|
RewriteRule ^uploads/(.*)$ uploads-proxy.php?file=$1 [QSA,L]
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# Host security headers use frame-ancestors 'none' + X-Frame-Options DENY.
|
||||||
|
# Edit them so admin can embed preview.html (same origin).
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
Header always edit Content-Security-Policy "frame-ancestors 'none'" "frame-ancestors 'self'"
|
||||||
|
Header always edit X-Frame-Options "DENY" "SAMEORIGIN"
|
||||||
|
|
||||||
|
<FilesMatch "^(preview\.html|preview\.php|index\.html)$">
|
||||||
|
Header unset X-Frame-Options
|
||||||
Header always unset X-Frame-Options
|
Header always unset X-Frame-Options
|
||||||
|
Header unset Content-Security-Policy
|
||||||
Header always unset Content-Security-Policy
|
Header always unset Content-Security-Policy
|
||||||
|
Header always set X-Frame-Options "SAMEORIGIN"
|
||||||
Header always set Content-Security-Policy "frame-ancestors 'self'"
|
Header always set Content-Security-Policy "frame-ancestors 'self'"
|
||||||
</Files>
|
</FilesMatch>
|
||||||
|
</IfModule>
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Forward /citpl_website/api/* → Node (Express) without requiring Apache mod_proxy.
|
||||||
|
* Start the API on the server: cd /path/to/app && npm run server
|
||||||
|
* Or: pm2 start server/index.js --name citpl-api
|
||||||
|
*/
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$apiOrigin = getenv('CITPL_API_ORIGIN') ?: 'http://127.0.0.1:3001';
|
||||||
|
|
||||||
|
$requestUri = $_SERVER['REQUEST_URI'] ?? '/api';
|
||||||
|
$path = parse_url($requestUri, PHP_URL_PATH) ?: '/api';
|
||||||
|
|
||||||
|
// Keep everything from "/api" onward (works under /citpl_website/api/...)
|
||||||
|
if (preg_match('#(/api(?:/.*)?)$#', $path, $m)) {
|
||||||
|
$forwardPath = $m[1];
|
||||||
|
} else {
|
||||||
|
$forwardPath = '/api';
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $_SERVER['QUERY_STRING'] ?? '';
|
||||||
|
$url = rtrim($apiOrigin, '/') . $forwardPath . ($query !== '' ? '?' . $query : '');
|
||||||
|
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||||
|
$body = file_get_contents('php://input');
|
||||||
|
if ($body === false) {
|
||||||
|
$body = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$headers = [];
|
||||||
|
if (function_exists('getallheaders')) {
|
||||||
|
foreach (getallheaders() as $name => $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;
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Forward /citpl_website/uploads/* → Node static uploads (no mod_proxy required).
|
||||||
|
*/
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$apiOrigin = getenv('CITPL_API_ORIGIN') ?: 'http://127.0.0.1:3001';
|
||||||
|
$file = $_GET['file'] ?? '';
|
||||||
|
$file = str_replace(['..', '\\'], '', $file);
|
||||||
|
$file = ltrim($file, '/');
|
||||||
|
|
||||||
|
if ($file === '') {
|
||||||
|
http_response_code(400);
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['error' => '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;
|
||||||
@@ -33,6 +33,14 @@ app.use('/api/auth', authRoutes);
|
|||||||
app.use('/api/content', contentRoutes);
|
app.use('/api/content', contentRoutes);
|
||||||
app.use('/api/upload', uploadRoutes);
|
app.use('/api/upload', uploadRoutes);
|
||||||
|
|
||||||
|
// 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.use((err, _req, res, _next) => {
|
app.use((err, _req, res, _next) => {
|
||||||
if (err instanceof multer.MulterError && err.code === 'LIMIT_FILE_SIZE') {
|
if (err instanceof multer.MulterError && err.code === 'LIMIT_FILE_SIZE') {
|
||||||
return res.status(413).json({ error: 'File too large (max 50MB)', code: 'FILE_TOO_LARGE' });
|
return res.status(413).json({ error: 'File too large (max 50MB)', code: 'FILE_TOO_LARGE' });
|
||||||
|
|||||||
+31
-1
@@ -73,9 +73,12 @@ function Toast({ message, type }) {
|
|||||||
|
|
||||||
function LoginPreview() {
|
function LoginPreview() {
|
||||||
const viewportRef = useRef(null);
|
const viewportRef = useRef(null);
|
||||||
|
const iframeRef = useRef(null);
|
||||||
const [scale, setScale] = useState(0.5);
|
const [scale, setScale] = useState(0.5);
|
||||||
|
const [frameBlocked, setFrameBlocked] = useState(false);
|
||||||
const previewWidth = 1280;
|
const previewWidth = 1280;
|
||||||
const previewHeight = 800;
|
const previewHeight = 800;
|
||||||
|
const previewSrc = `${BASE}preview.html`;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = viewportRef.current;
|
const el = viewportRef.current;
|
||||||
@@ -91,6 +94,22 @@ function LoginPreview() {
|
|||||||
observer.observe(el);
|
observer.observe(el);
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Host CSP (frame-ancestors 'none' / XFO DENY) can block the embed — show a fallback.
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
try {
|
||||||
|
const doc = iframeRef.current?.contentDocument;
|
||||||
|
if (!doc?.body || doc.body.childElementCount === 0) {
|
||||||
|
setFrameBlocked(true);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setFrameBlocked(true);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="login-preview">
|
<div className="login-preview">
|
||||||
<div className="login-preview-intro">
|
<div className="login-preview-intro">
|
||||||
@@ -116,12 +135,22 @@ function LoginPreview() {
|
|||||||
ref={viewportRef}
|
ref={viewportRef}
|
||||||
style={{ height: previewHeight * scale }}
|
style={{ height: previewHeight * scale }}
|
||||||
>
|
>
|
||||||
|
{frameBlocked ? (
|
||||||
|
<div className="login-preview-fallback">
|
||||||
|
<p>Inline preview is blocked by the server's frame policy.</p>
|
||||||
|
<a href={previewSrc} target="_blank" rel="noopener noreferrer" className="login-preview-link">
|
||||||
|
<ExternalLink size={14} />
|
||||||
|
Open preview
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div
|
<div
|
||||||
className="login-preview-stage"
|
className="login-preview-stage"
|
||||||
style={{ width: previewWidth * scale, height: previewHeight * scale }}
|
style={{ width: previewWidth * scale, height: previewHeight * scale }}
|
||||||
>
|
>
|
||||||
<iframe
|
<iframe
|
||||||
src={`${BASE}preview.html`}
|
ref={iframeRef}
|
||||||
|
src={previewSrc}
|
||||||
title="Website preview"
|
title="Website preview"
|
||||||
className="login-preview-iframe"
|
className="login-preview-iframe"
|
||||||
style={{
|
style={{
|
||||||
@@ -131,6 +160,7 @@ function LoginPreview() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -893,6 +893,28 @@ body {
|
|||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-preview-fallback {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 220px;
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
color: #64748b;
|
||||||
|
background: linear-gradient(160deg, #0f172a 0%, #1e293b 55%, #0f172a 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-preview-fallback p {
|
||||||
|
margin: 0;
|
||||||
|
max-width: 280px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
.login-preview-iframe {
|
.login-preview-iframe {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
+9
-1
@@ -22,7 +22,15 @@ async function apiFetch(path, options = {}) {
|
|||||||
|
|
||||||
const res = await fetch(`${API_BASE}${path}`, { ...options, headers });
|
const res = await fetch(`${API_BASE}${path}`, { ...options, headers });
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
if (!res.ok) throw new Error(data.error || 'Request failed');
|
if (!res.ok) {
|
||||||
|
const fallback =
|
||||||
|
res.status === 404
|
||||||
|
? 'API not found. Deploy server/ and run: npm run server (port 3001)'
|
||||||
|
: res.status === 502
|
||||||
|
? 'API server unreachable. Start Node on the host (port 3001).'
|
||||||
|
: 'Request failed';
|
||||||
|
throw new Error(data.error || fallback);
|
||||||
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user