multi-tenant relay und card-push to user
This commit is contained in:
1 parent
7219bae655
commit
5b18c46ac2
22 files changed
+7453
-519
No files matched your search
+102
-14
@@ -821,33 +821,61 @@ if (str_starts_with($route, '/api/')) {
|
||||
|
||||
if ($route === '/api/relay/settings' && $method === 'GET') {
|
||||
$url = $db->query("SELECT value FROM settings WHERE key = 'relay_url'")->fetchColumn() ?: '';
|
||||
$hasKey = !empty($db->query("SELECT value FROM settings WHERE key = 'relay_key'")->fetchColumn());
|
||||
echo json_encode(['url' => $url, 'has_key' => $hasKey]); exit;
|
||||
$username = $db->query("SELECT value FROM settings WHERE key = 'relay_username'")->fetchColumn() ?: '';
|
||||
$key = $db->query("SELECT value FROM settings WHERE key = 'relay_key'")->fetchColumn() ?: '';
|
||||
// Single-user local app — echo the stored secret back so the user can
|
||||
// see and edit it directly without guessing if it's still saved.
|
||||
echo json_encode(['url' => $url, 'username' => $username, 'key' => $key]); exit;
|
||||
}
|
||||
|
||||
if ($route === '/api/relay/settings' && $method === 'POST') {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
if (isset($input['url'])) $db->prepare("INSERT OR REPLACE INTO settings (key, value) VALUES ('relay_url', :v)")->execute(['v' => $input['url']]);
|
||||
if (!empty($input['key'])) $db->prepare("INSERT OR REPLACE INTO settings (key, value) VALUES ('relay_key', :v)")->execute(['v' => $input['key']]);
|
||||
if (isset($input['username'])) $db->prepare("INSERT OR REPLACE INTO settings (key, value) VALUES ('relay_username', :v)")->execute(['v' => $input['username']]);
|
||||
// Always overwrite key when present (even empty) — single-user local
|
||||
// app, the input is now pre-populated so an empty submit is intentional
|
||||
if (array_key_exists('key', $input)) $db->prepare("INSERT OR REPLACE INTO settings (key, value) VALUES ('relay_key', :v)")->execute(['v' => $input['key']]);
|
||||
echo json_encode(['ok' => true]); exit;
|
||||
}
|
||||
|
||||
if ($route === '/api/relay/poll' && $method === 'POST') {
|
||||
$input = json_decode(file_get_contents('php://input'), true) ?: [];
|
||||
$strict = !empty($input['strict']); // test button sets this; background poll doesn't
|
||||
$relayUrl = $db->query("SELECT value FROM settings WHERE key = 'relay_url'")->fetchColumn();
|
||||
$relayKey = $db->query("SELECT value FROM settings WHERE key = 'relay_key'")->fetchColumn();
|
||||
if (!$relayUrl || !$relayKey) { echo json_encode(['ok' => true, 'pulled' => 0]); exit; }
|
||||
$relayUser = $db->query("SELECT value FROM settings WHERE key = 'relay_username'")->fetchColumn();
|
||||
if (!$relayUrl || !$relayKey || !$relayUser) {
|
||||
if ($strict) {
|
||||
$missing = [];
|
||||
if (!$relayUrl) $missing[] = 'url';
|
||||
if (!$relayUser) $missing[] = 'username';
|
||||
if (!$relayKey) $missing[] = 'secret';
|
||||
echo json_encode(['ok' => false, 'error' => 'relay not configured: missing ' . implode(', ', $missing)]); exit;
|
||||
}
|
||||
echo json_encode(['ok' => true, 'pulled' => 0]); exit;
|
||||
}
|
||||
|
||||
$ch = curl_init(rtrim($relayUrl, '/') . '/api/messages');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => ['X-Relay-Key: ' . $relayKey],
|
||||
CURLOPT_HTTPHEADER => ['X-Relay-User: ' . $relayUser, 'X-Relay-Key: ' . $relayKey],
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
]);
|
||||
$response = curl_exec($ch);
|
||||
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($status !== 200) { echo json_encode(['ok' => false, 'error' => "HTTP $status"]); exit; }
|
||||
if ($status !== 200) {
|
||||
$relayMsg = '';
|
||||
$decoded = json_decode($response, true);
|
||||
if (is_array($decoded) && !empty($decoded['error'])) {
|
||||
$relayMsg = ' — ' . $decoded['error'];
|
||||
} elseif (is_string($response) && $response !== '') {
|
||||
$relayMsg = ' — ' . substr($response, 0, 200);
|
||||
}
|
||||
echo json_encode(['ok' => false, 'error' => "HTTP $status$relayMsg", 'sent_username' => $relayUser]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$messages = json_decode($response, true);
|
||||
if (!is_array($messages)) { echo json_encode(['ok' => false, 'error' => 'invalid response']); exit; }
|
||||
@@ -909,7 +937,7 @@ if (str_starts_with($route, '/api/')) {
|
||||
$ach = curl_init(rtrim($relayUrl, '/') . '/api/attachments?relay_uid=' . urlencode($relayUid));
|
||||
curl_setopt_array($ach, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => ['X-Relay-Key: ' . $relayKey],
|
||||
CURLOPT_HTTPHEADER => ['X-Relay-User: ' . $relayUser, 'X-Relay-Key: ' . $relayKey],
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
]);
|
||||
$attResponse = curl_exec($ach);
|
||||
@@ -938,14 +966,45 @@ if (str_starts_with($route, '/api/')) {
|
||||
echo json_encode(['ok' => true, 'pulled' => $pulled]); exit;
|
||||
}
|
||||
|
||||
// List relay tenants (for the recipient picker)
|
||||
if ($route === '/api/relay/tenants' && $method === 'GET') {
|
||||
$relayUrl = $db->query("SELECT value FROM settings WHERE key = 'relay_url'")->fetchColumn();
|
||||
$relayKey = $db->query("SELECT value FROM settings WHERE key = 'relay_key'")->fetchColumn();
|
||||
$relayUser = $db->query("SELECT value FROM settings WHERE key = 'relay_username'")->fetchColumn();
|
||||
if (!$relayUrl || !$relayKey || !$relayUser) {
|
||||
echo json_encode(['ok' => false, 'error' => 'relay not configured']); exit;
|
||||
}
|
||||
$ch = curl_init(rtrim($relayUrl, '/') . '/api/tenants');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => ['X-Relay-User: ' . $relayUser, 'X-Relay-Key: ' . $relayKey],
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
]);
|
||||
$response = curl_exec($ch);
|
||||
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($status !== 200) {
|
||||
echo json_encode(['ok' => false, 'error' => "relay returned HTTP $status"]); exit;
|
||||
}
|
||||
$data = json_decode($response, true);
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'tenants' => $data['tenants'] ?? [],
|
||||
'self' => $data['self'] ?? $relayUser,
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Push a card to relay
|
||||
if ($route === '/api/relay/push' && $method === 'POST') {
|
||||
$relayUrl = $db->query("SELECT value FROM settings WHERE key = 'relay_url'")->fetchColumn();
|
||||
$relayKey = $db->query("SELECT value FROM settings WHERE key = 'relay_key'")->fetchColumn();
|
||||
if (!$relayUrl || !$relayKey) { echo json_encode(['ok' => false, 'error' => 'relay not configured']); exit; }
|
||||
$relayUser = $db->query("SELECT value FROM settings WHERE key = 'relay_username'")->fetchColumn();
|
||||
if (!$relayUrl || !$relayKey || !$relayUser) { echo json_encode(['ok' => false, 'error' => 'relay not configured (need url, username, and secret)']); exit; }
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$cardId = $input['card_id'] ?? null;
|
||||
$recipient = isset($input['recipient']) ? trim((string)$input['recipient']) : null;
|
||||
if (!$cardId) { http_response_code(400); echo json_encode(['error' => 'card_id required']); exit; }
|
||||
|
||||
$card = $db->prepare('SELECT * FROM cards WHERE id = ?');
|
||||
@@ -967,24 +1026,32 @@ if (str_starts_with($route, '/api/')) {
|
||||
return "HYPRCRD-RELAY-V1\n" . base64_encode($iv . $cipher);
|
||||
};
|
||||
|
||||
$payload = json_encode([
|
||||
$payloadData = [
|
||||
'title' => $encryptField($card['title'] ?? ''),
|
||||
'body' => $encryptField($card['body'] ?? ''),
|
||||
'color' => $card['color'] ?? '#38bdf8',
|
||||
'tags' => $card['tags'] ?? '',
|
||||
'relay_uid' => $relayUid,
|
||||
]);
|
||||
];
|
||||
if ($recipient) $payloadData['recipient'] = $recipient;
|
||||
$payload = json_encode($payloadData);
|
||||
|
||||
$ch = curl_init(rtrim($relayUrl, '/') . '/api/messages');
|
||||
$debugStream = fopen('php://temp', 'w+');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-Relay-Key: ' . $relayKey],
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-Relay-User: ' . $relayUser, 'X-Relay-Key: ' . $relayKey],
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_VERBOSE => true,
|
||||
CURLOPT_STDERR => $debugStream,
|
||||
]);
|
||||
$response = curl_exec($ch);
|
||||
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
rewind($debugStream);
|
||||
$curlDebug = stream_get_contents($debugStream);
|
||||
fclose($debugStream);
|
||||
curl_close($ch);
|
||||
|
||||
if ($status === 200 || $status === 201) {
|
||||
@@ -1013,7 +1080,7 @@ if (str_starts_with($route, '/api/')) {
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $attPayload,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-Relay-Key: ' . $relayKey],
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-Relay-User: ' . $relayUser, 'X-Relay-Key: ' . $relayKey],
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
]);
|
||||
curl_exec($ach);
|
||||
@@ -1022,7 +1089,25 @@ if (str_starts_with($route, '/api/')) {
|
||||
|
||||
echo json_encode(['ok' => true]); exit;
|
||||
}
|
||||
echo json_encode(['ok' => false, 'error' => "HTTP $status"]); exit;
|
||||
// Surface the relay's actual error message so the user can see why
|
||||
$relayMsg = '';
|
||||
$decoded = json_decode($response, true);
|
||||
if (is_array($decoded) && !empty($decoded['error'])) {
|
||||
$relayMsg = ' — ' . $decoded['error'];
|
||||
} elseif (is_string($response) && $response !== '') {
|
||||
$relayMsg = ' — ' . substr($response, 0, 200);
|
||||
}
|
||||
// Strip the X-Relay-Key value from the debug log so we don't leak the secret
|
||||
$safeDebug = preg_replace('/(X-Relay-Key:\s*)[^\r\n]+/i', '$1<redacted>', $curlDebug);
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'error' => "relay returned HTTP $status$relayMsg",
|
||||
'sent_username' => $relayUser,
|
||||
'relay_url' => rtrim($relayUrl, '/') . '/api/messages',
|
||||
'response_body' => is_string($response) ? substr($response, 0, 500) : null,
|
||||
'curl_debug' => $safeDebug,
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(404); echo json_encode(['error' => 'not found']); exit;
|
||||
@@ -1438,13 +1523,15 @@ if (str_starts_with($route, '/api/')) {
|
||||
</div>
|
||||
<div id="settings-panel-relay" class="hidden space-y-3">
|
||||
<div class="font-pixel text-[9px] uppercase tracking-wider text-t-dim mb-2"><i class="ph ph-broadcast text-xs"></i> relay</div>
|
||||
<p class="text-[10px] text-t-dim leading-relaxed">The relay is multi-tenant: each user has their own account on the relay server. Ask the relay admin to create a user for you, then enter the credentials below.</p>
|
||||
<div><label class="font-pixel text-[9px] text-t-dim uppercase block mb-1">relay url</label><input id="relay-url" type="text" placeholder="https://relay.example.com" autocomplete="off" class="w-full bg-t-bg border border-t-border text-t-bright px-3 py-2 text-xs focus:border-t-accent focus:outline-none placeholder:text-t-dim font-mono"></div>
|
||||
<div><label class="font-pixel text-[9px] text-t-dim uppercase block mb-1">username</label><input id="relay-username" type="text" placeholder="your relay username" autocomplete="off" class="w-full bg-t-bg border border-t-border text-t-bright px-3 py-2 text-xs focus:border-t-accent focus:outline-none placeholder:text-t-dim font-mono"></div>
|
||||
<div><label class="font-pixel text-[9px] text-t-dim uppercase block mb-1">shared secret</label><input id="relay-key" type="text" placeholder="shared secret" autocomplete="off" style="-webkit-text-security:disc;" class="w-full bg-t-bg border border-t-border text-t-bright px-3 py-2 text-xs focus:border-t-accent focus:outline-none font-mono"></div>
|
||||
<div class="flex gap-2">
|
||||
<button id="relay-save" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors"><i class="ph ph-floppy-disk text-sm"></i><span class="font-pixel text-[10px]">save</span></button>
|
||||
<button id="relay-test" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors"><i class="ph ph-plugs-connected text-sm"></i><span class="font-pixel text-[10px]">test</span></button>
|
||||
<span id="relay-status" class="font-pixel text-[9px] text-t-dim self-center"></span>
|
||||
</div>
|
||||
<div id="relay-status" class="hidden mt-1 px-3 py-2 text-[10px] font-mono break-words border border-t-border bg-t-bg"></div>
|
||||
</div>
|
||||
<div id="settings-panel-behavior" class="hidden space-y-5">
|
||||
<div>
|
||||
@@ -1819,6 +1906,7 @@ if (str_starts_with($route, '/api/')) {
|
||||
</div>
|
||||
<div id="ctx-buckets" class="hidden border-t border-t-border py-1"></div>
|
||||
<div id="ctx-members" class="hidden border-t border-t-border py-1 max-h-48 overflow-y-auto"></div>
|
||||
<div id="ctx-relay-tenants" class="hidden border-t border-t-border py-1 max-h-48 overflow-y-auto"></div>
|
||||
<div id="ctx-tags" class="hidden border-t border-t-border py-1">
|
||||
<div class="px-3 py-1.5"><input id="ctx-tag-input" type="text" placeholder="#tag..." class="w-full bg-t-bg border border-t-border text-t-text text-[10px] font-pixel px-2 py-1 rounded-sm focus:border-t-accent focus:outline-none"></div>
|
||||
<div id="ctx-tag-list" class="max-h-36 overflow-y-auto"></div>
|
||||
|
||||
Reference in new issue
Block a user