( ′∀`)σ≡σ☆))Д′)レ(゚∀゚;)ヘ=З=З=Зε≡(ノ´_ゝ`)ノ
<?php
// Final working project: single-file PHP UI + proxy for n8n
// Compatible with PHP 5.6+ (no return type hints, no arrow functions)
// Config (env-backed, with safe defaults)
$N8N_BASE_URL = getenv('N8N_BASE_URL') ?: 'https://thebrandai.app.n8n.cloud';
$WEBHOOK_PATH = getenv('N8N_WEBHOOK_PATH') ?: 'test';
$N8N_AUTH_TOKEN = getenv('N8N_AUTH_TOKEN') ?: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZDcwMDk1Mi1hNjhiLTRhMDMtODFkZC02NTY3OWRmOGE4YWEiLCJpc3MiOiJuOG4iLCJhdWQiOiJwdWJsaWMtYXBpIiwiaWF0IjoxNzYxNTA3MzY2LCJleHAiOjE3NjQwMTgwMDB9.w-J43tTtQz_qeTf7CCLIeH9cqsbLKjPRnLyMp0Opn-M';
function json_response($data, $status = 200, $extraHeaders = array()) {
http_response_code($status);
header('Content-Type: application/json');
foreach ($extraHeaders as $h) header($h);
echo is_string($data) ? $data : json_encode($data);
exit;
}
function get_request_headers() {
if (function_exists('getallheaders')) {
return getallheaders();
}
$headers = array();
foreach ($_SERVER as $k => $v) {
if (strpos($k, 'HTTP_') === 0) {
$name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($k, 5)))));
$headers[$name] = $v;
}
}
if (isset($_SERVER['CONTENT_TYPE'])) $headers['Content-Type'] = $_SERVER['CONTENT_TYPE'];
if (isset($_SERVER['CONTENT_LENGTH'])) $headers['Content-Length'] = $_SERVER['CONTENT_LENGTH'];
return $headers;
}
function proxy_to_n8n($targetUrl, $authToken = null) {
$method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'POST';
$inHeaders = get_request_headers();
// If PHP parsed files, rebuild multipart using CURLFile so cURL sets correct boundaries
$hasFiles = !empty($_FILES);
if ($hasFiles) {
$postFields = array();
// Include any form fields
foreach ($_POST as $k => $v) { $postFields[$k] = $v; }
// Attach uploaded files
foreach ($_FILES as $field => $info) {
if (is_array($info['name'])) {
$count = count($info['name']);
for ($i = 0; $i < $count; $i++) {
if ($info['error'][$i] === UPLOAD_ERR_OK) {
$postFields[$field.'['.$i.']'] = new CURLFile($info['tmp_name'][$i], $info['type'][$i], $info['name'][$i]);
}
}
} else {
if ($info['error'] === UPLOAD_ERR_OK) {
$postFields[$field] = new CURLFile($info['tmp_name'], $info['type'], $info['name']);
}
}
}
// Build headers, but let cURL set Content-Type and Content-Length
$headers = array();
foreach ($inHeaders as $name => $value) {
$n = strtolower($name);
if ($n === 'host' || $n === 'content-type' || $n === 'content-length' || $n === 'accept-encoding') continue;
$headers[] = $name . ': ' . $value;
}
if ($authToken) {
// Prefer server-side secret over any incoming Authorization
$tmp = array();
foreach ($headers as $h) { if (stripos($h, 'authorization:') !== 0) $tmp[] = $h; }
$headers = $tmp;
$headers[] = 'Authorization: Bearer ' . $authToken;
}
$ch = curl_init($targetUrl);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_HEADER, false);
// Auto-decompress gzip/deflate responses so we return readable JSON
curl_setopt($ch, CURLOPT_ENCODING, '');
$responseBody = curl_exec($ch);
if ($responseBody === false) {
$err = curl_error($ch);
curl_close($ch);
json_response(array('error' => 'Proxy error', 'detail' => $err), 502);
}
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (!$status) $status = 200;
$respType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
if (!$respType) $respType = 'application/json';
curl_close($ch);
http_response_code($status);
header('Content-Type: ' . $respType);
echo $responseBody;
exit;
}
// Fallback: raw body forwarding (no files present)
$body = file_get_contents('php://input');
$headers = array();
foreach ($inHeaders as $name => $value) {
$n = strtolower($name);
if ($n === 'host' || $n === 'accept-encoding') continue; // don't forward Host or Accept-Encoding
$headers[] = $name . ': ' . $value;
}
if ($authToken) {
$tmp = array();
foreach ($headers as $h) { if (stripos($h, 'authorization:') !== 0) $tmp[] = $h; }
$headers = $tmp;
$headers[] = 'Authorization: Bearer ' . $authToken;
}
$ch = curl_init($targetUrl);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_HEADER, false);
// Auto-decompress gzip/deflate responses so we return readable JSON
curl_setopt($ch, CURLOPT_ENCODING, '');
$responseBody = curl_exec($ch);
if ($responseBody === false) {
$err = curl_error($ch);
curl_close($ch);
json_response(array('error' => 'Proxy error', 'detail' => $err), 502);
}
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (!$status) $status = 200;
$respType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
if (!$respType) $respType = 'application/json';
curl_close($ch);
http_response_code($status);
header('Content-Type: ' . $respType);
echo $responseBody;
exit;
}
$action = isset($_GET['action']) ? $_GET['action'] : 'ui';
if ($action === 'trigger') {
// Forward initial trigger to n8n webhook
$url = rtrim($N8N_BASE_URL, '/') . '/webhook/' . urlencode($WEBHOOK_PATH);
proxy_to_n8n($url, $N8N_AUTH_TOKEN);
}
if ($action === 'resume') {
// Forward raw request to resumeUrl
$resumeUrl = isset($_GET['resumeUrl']) ? $_GET['resumeUrl'] : '';
if (!$resumeUrl) {
json_response(array('error' => 'Missing resumeUrl'), 400);
}
proxy_to_n8n($resumeUrl, $N8N_AUTH_TOKEN);
}
// UI (default)
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Custom UI Wizard ↔ n8n (main.php)</title>
<style>
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;padding:24px;max-width:880px;margin:0 auto;color:#111;background:#fafafa}
section{background:#fff;border:1px solid #e6e6e6;border-radius:12px;padding:16px;margin-bottom:16px}
h1{font-size:22px;margin:0 0 12px} h2{font-size:18px;margin:0 0 10px}
pre{background:#0b1021;color:#e6edf3;padding:12px;border-radius:8px;overflow:auto}
button{padding:10px 14px;border-radius:8px;border:1px solid #ddd;background:#1f7aec;color:#fff;font-weight:600;cursor:pointer}
input,textarea{width:100%;padding:10px;border:1px solid #ddd;border-radius:8px}
.small{color:#666;font-size:12px}
.code{font-family:ui-monospace,Consolas,Monaco,monospace}
.flex{display:flex;gap:12px;align-items:center}
.hidden{display:none}
/* Wizard additions */
.wizard{margin-top:8px}
.step{display:none}
.step.active{display:block}
.step-header{display:flex;align-items:center;gap:8px;margin-bottom:10px}
.step-index{background:#1f7aec;color:#fff;border-radius:999px;width:26px;height:26px;display:inline-flex;align-items:center;justify-content:center;font-weight:700}
.nav{display:flex;gap:8px;margin-top:12px}
.nav .secondary{background:#fff;color:#1f7aec;border:1px solid #1f7aec}
.progress{display:flex;gap:6px;margin-bottom:14px}
.progress .dot{width:8px;height:8px;border-radius:999px;background:#d0d7ff}
.progress .dot.active{background:#1f7aec}
/* List view for results */
.list{margin-top:8px;display:flex;flex-direction:column;gap:8px}
.item{border:1px solid #e6e6e6;border-radius:8px;padding:10px;background:#fff}
.item .label{font-weight:600;color:#333;margin-bottom:4px}
.item .value{color:#222;white-space:pre-wrap}
</style>
</head>
<body>
<h1>Custom UI Wizard for n8n Workflows</h1>
<section>
<div class="progress">
<span class="dot" id="p1"></span>
<span class="dot" id="p2"></span>
<span class="dot" id="p3"></span>
</div>
<div class="wizard">
<!-- Step 1: Trigger -->
<div class="step active" id="step1">
<div class="step-header"><span class="step-index">1</span><h2>Trigger Workflow</h2></div>
<p class="small">POSTs to <span class="code">main.php?action=trigger</span> — expects a <span class="code">resumeUrl</span>.</p>
<div>
<label>Optional JSON payload</label>
<textarea id="triggerPayload" rows="4">{"message":"Start"}</textarea>
</div>
<div class="nav">
<button id="btnTrigger">Trigger</button>
<button id="btnStep1Next" class="secondary" disabled>Next</button>
</div>
<div class="small" id="resumeHolder" class="hidden"></div>
<pre id="triggerOut">(awaiting trigger)</pre>
</div>
<!-- Step 2: Resume -->
<div class="step" id="step2">
<div class="step-header"><span class="step-index">2</span><h2>Resume with File Upload</h2></div>
<p class="small">Sends multipart data to <span class="code">main.php?action=resume&resumeUrl=...</span>.</p>
<form id="resumeForm">
<div>
<label>File</label>
<input id="fileInput" name="file" type="file" />
</div>
<div style="margin-top:8px">
<label>Notes (optional)</label>
<input id="notesInput" name="notes" type="text" placeholder="Add a note" />
</div>
<div class="nav">
<button type="submit">Resume</button>
<button id="btnStep2Next" class="secondary" disabled>Next</button>
</div>
<div class="small" id="resumeInfo">No resumeUrl yet.</div>
</form>
<pre id="resumeOut">(awaiting resume)</pre>
</div>
<!-- Step 3: Response -->
<div class="step" id="step3">
<div class="step-header"><span class="step-index">3</span><h2>Response</h2></div>
<div class="small">Captured <span class="code">resumeUrl</span>: <span class="code" id="capturedUrl">(none)</span></div>
<div class="list" id="resultList"></div>
<div style="margin-top:8px"><strong>Latest Response (raw JSON)</strong></div>
<pre id="finalOut">(no response yet)</pre>
<div class="nav">
<button id="btnRestart" class="secondary">Start Over</button>
</div>
</div>
</section>
<section>
<h2>Status</h2>
<div class="small">n8n base: <span class="code" id="baseUrl"></span> • webhook path: <span class="code" id="whPath"></span></div>
<div class="small">Files: <span class="code">project.md</span> • <span class="code">custom-ui.json</span></div>
</section>
<script>
var state = { step: 1, resumeUrl: null, triggerJson: null, resumeJson: null };
function $(id){ return document.getElementById(id); }
function clearList(){ var rl = $('resultList'); if (rl) rl.innerHTML=''; }
function renderListFromLatest(){
var rl = $('resultList');
if (!rl) return;
rl.innerHTML = '';
var latest = state.resumeJson || state.triggerJson;
if (!latest) return;
var obj = latest;
try {
if (Array.isArray(latest)) { obj = latest[0] || {}; }
if (obj.json) obj = obj.json;
} catch (e) {}
var summary = obj.summary || '';
var products = obj.products || '';
var resumeUrl = obj.resumeUrl || state.resumeUrl || '';
// Normalize products into array
var productsList = Array.isArray(products) ? products : (typeof products === 'string' ? products.split(',').map(function(s){return s.trim();}).filter(function(s){return s.length>0;}) : []);
// Build list items
var items = [
{ label: 'Summary', value: summary || '(empty)' },
{ label: 'Products', value: (productsList.length ? productsList.join('\n') : '(none)') },
{ label: 'Resume URL', value: resumeUrl || '(none)' }
];
items.forEach(function(it){
var d = document.createElement('div'); d.className = 'item';
var l = document.createElement('div'); l.className = 'label'; l.textContent = it.label;
var v = document.createElement('div'); v.className = 'value'; v.textContent = it.value;
d.appendChild(l); d.appendChild(v);
rl.appendChild(d);
});
}
function showStep(n){
state.step = n;
['step1','step2','step3'].forEach(function(id){
var el = $(id);
if (el) el.classList.toggle('active', id === 'step'+n);
});
['p1','p2','p3'].forEach(function(id, i){
var el = $(id);
if (el) el.classList.toggle('active', i < n);
});
if (n === 3) {
$('capturedUrl').textContent = state.resumeUrl || '(none)';
var latest = state.resumeJson || state.triggerJson || { info: 'No responses captured' };
$('finalOut').textContent = JSON.stringify(latest, null, 2);
renderListFromLatest();
} else {
clearList();
}
}
$('baseUrl').textContent = <?php echo json_encode(rtrim($N8N_BASE_URL, '/')); ?>;
$('whPath').textContent = <?php echo json_encode($WEBHOOK_PATH); ?>;
// Step 1: Trigger
$('btnTrigger').addEventListener('click', function(){
(async function(){
try {
var payloadText = $('triggerPayload').value.trim();
var body = payloadText;
var headers = { 'Content-Type': 'application/json' };
try { JSON.parse(payloadText); } catch (e) { body = JSON.stringify({ content: payloadText }); }
var res = await fetch('main.php?action=trigger', { method: 'POST', headers: headers, body: body });
var text = await res.text();
var json; try { json = JSON.parse(text); } catch (e) { json = { raw: text }; }
state.triggerJson = json;
$('triggerOut').textContent = JSON.stringify(json, null, 2);
var resumeUrl = json && (json.resumeUrl || (json.json && json.json.resumeUrl));
if (resumeUrl) {
state.resumeUrl = resumeUrl;
$('resumeInfo').textContent = 'resumeUrl captured';
$('resumeHolder').textContent = 'resumeUrl: ' + resumeUrl;
$('btnStep1Next').disabled = false;
showStep(2);
} else {
$('resumeInfo').textContent = 'No resumeUrl found in response.';
$('btnStep1Next').disabled = true;
}
} catch (e) {
$('triggerOut').textContent = 'Error: ' + e.message;
}
})();
});
$('btnStep1Next').addEventListener('click', function(){ if (!this.disabled) showStep(2); });
// Step 2: Resume
$('resumeForm').addEventListener('submit', function(e){
e.preventDefault();
if (!state.resumeUrl) {
$('resumeOut').textContent = 'Missing resumeUrl — trigger first.';
return;
}
(async function(){
try {
var fd = new FormData(document.getElementById('resumeForm'));
var url = 'main.php?action=resume&resumeUrl=' + encodeURIComponent(state.resumeUrl);
var res = await fetch(url, { method: 'POST', body: fd });
var text = await res.text();
var json; try { json = JSON.parse(text); } catch (e) { json = { raw: text }; }
state.resumeJson = json;
$('resumeOut').textContent = JSON.stringify(json, null, 2);
$('btnStep2Next').disabled = false;
showStep(3);
} catch (e) {
$('resumeOut').textContent = 'Error: ' + e.message;
}
})();
});
$('btnStep2Next').addEventListener('click', function(){ if (!this.disabled) showStep(3); });
// Step 3: Restart
$('btnRestart').addEventListener('click', function(){
state = { step: 1, resumeUrl: null, triggerJson: null, resumeJson: null };
$('triggerOut').textContent = '(awaiting trigger)';
$('resumeOut').textContent = '(awaiting resume)';
$('finalOut').textContent = '(no response yet)';
$('resumeInfo').textContent = 'No resumeUrl yet.';
$('btnStep1Next').disabled = true;
$('btnStep2Next').disabled = true;
showStep(1);
});
// init
showStep(1);
</script>
</body>
</html>