Files
hyprcrd/support-relay/inc/routes/setup.php
T
thandClaude Opus 4.7 7bce6a3a36 v2.0.12 — support channel: relay + hyprcrd plugin
Central support desk for the Oumta project. Tickets arrive through a
self-hostable support-relay (public form, SMTP out), hyprcrd pulls
them in as cards, replies go back out through the relay, and KB
fragments are plain hyprcrd cards so the board + search + tags
machinery handles them for free.

support-relay/ (standalone PHP/SQLite service)
- Public form + submit endpoint with IP rate-limit, honeypot, and an
  email-matched reply path so users can follow up by typing their
  OUMTA-n issue id — mismatched email falls back to a fresh ticket
  so a hostile submitter can't hijack someone else's thread.
- One-shot setup wizard configures branding, issue-id prefix, public
  URL, mail footer template, SMTP creds (stored AES-256-GCM at rest),
  and generates the X-Support-Key API token shown exactly once.
- Hyprcrd-facing API: GET /api/hyprcrd/tickets?since=<iso>, POST
  …/reply (sends via SMTP, flips status to answered), PATCH
  …/status, DELETE, GET /ping, plus GET/POST /api/hyprcrd/settings
  exposing brand/public_url/mail_footer for post-setup edits.
- Minimal dependency-free SMTP client covering STARTTLS/TLS/none,
  AUTH LOGIN, RFC 2047 subject encoding, dot-stuffing. Extended to
  emit multipart/alternative (plain+HTML) + multipart/related so
  outbound mails carry the branded logo via Content-ID embedding;
  plain-text fallback stays intact for older clients.
- Mail footer template supports {brand}, {issue_id}, {public_url}
  placeholders. Lines with an empty placeholder are dropped, so the
  "Reply via …" line vanishes cleanly when no public URL is set.
- Favicon on all public pages. Logo (SVG by default, PNG preferred
  when both exist) picked up from logo2026col.* at send time.
- Docker: Dockerfile + docker-compose.yml for a stand-alone test
  container, plus a support-relay service in the main compose
  (profile "support") so hyprcrd can reach it at http://support-relay
  without host-networking shenanigans.

public/plugins/support (hyprcrd client)
- Full-screen Support overlay: ticket list (status-filterable),
  active ticket thread, reply composer with KB-fragment search +
  click-to-insert, "Save as fragment" button, "Close ticket" shortcut.
- KB fragments are regular cards in a configurable bucket. The
  composer just adds a fast search-and-insert path on top.
- Sync-tick hook auto-polls the relay alongside the existing CalDAV
  + relay-mail cycle.
- Settings tab in the main overlay: relay URL, API key, inbox / KB
  bucket pickers, connection test, and a "Relay config" subsection
  that round-trips to /api/hyprcrd/settings so the mail footer and
  branding can be edited without touching the relay's DB.
- Local cache tables support_tickets + support_replies mirror the
  relay's authoritative data keyed by relay_id.

Tooling
- dev.sh now opts into both relay + support profiles.
- dev-down.sh wipes support-relay volumes and bind-mount data for a
  clean re-setup. Hyprcrd + relay data is preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:41:19 +02:00

54 lines
3.0 KiB
PHP

<?php
// /api/setup — one-shot configuration on first install. After the admin
// completes this form the API key becomes required for every /api/*
// route and the setup endpoint returns 403. Designed to be impossible
// to re-trigger without clearing `settings.setup_done` in the DB.
if (!defined('SUPPORT_RELAY_BOOTSTRAPPED')) { http_response_code(404); exit; }
if ($route === '/api/setup' && $method === 'POST') {
if (!$isSetup) supportRelayJson(['error' => 'already configured'], 403);
if (supportRelayRateLimited($db, 'setup', 10, 10)) supportRelayJson(['error' => 'too many attempts'], 429);
$input = json_decode(file_get_contents('php://input'), true) ?: [];
// Required fields
$brand = trim((string)($input['brand'] ?? 'Oumta Support'));
$prefix = strtoupper(preg_replace('/[^A-Z0-9]/i', '', (string)($input['issue_prefix'] ?? 'OUMTA')));
$publicUrl = rtrim((string)($input['public_url'] ?? ''), '/');
$footer = trim((string)($input['mail_footer'] ?? ''));
$smtpHost = trim((string)($input['smtp_host'] ?? ''));
$smtpPort = (int)($input['smtp_port'] ?? 587);
$smtpUser = trim((string)($input['smtp_user'] ?? ''));
$smtpPw = (string)($input['smtp_password'] ?? '');
$smtpFrom = trim((string)($input['smtp_from'] ?? ''));
$smtpFromN = trim((string)($input['smtp_from_name'] ?? $brand));
$smtpEnc = in_array($input['smtp_encryption'] ?? 'starttls', ['starttls', 'tls', 'none'], true)
? $input['smtp_encryption'] : 'starttls';
if ($smtpHost === '') supportRelayJson(['error' => 'SMTP host required'], 400);
if (!filter_var($smtpFrom, FILTER_VALIDATE_EMAIL)) supportRelayJson(['error' => 'SMTP from must be a valid email'], 400);
if ($prefix === '' || strlen($prefix) > 12) supportRelayJson(['error' => 'issue prefix 1-12 chars, letters/digits only'], 400);
// Generate the API key now, show it back ONCE — admin copies it to
// hyprcrd. A second request after setup can't re-read or rotate it
// except through a re-install (clear setup_done). That's fine for a
// single-user admin workflow.
$apiKey = bin2hex(random_bytes(24));
supportRelaySet($db, 'brand', $brand);
supportRelaySet($db, 'issue_prefix', $prefix);
if ($publicUrl !== '') supportRelaySet($db, 'public_url', $publicUrl);
if ($footer !== '') supportRelaySet($db, 'mail_footer', $footer);
supportRelaySet($db, 'smtp_host', $smtpHost);
supportRelaySet($db, 'smtp_port', (string)$smtpPort);
supportRelaySet($db, 'smtp_user', $smtpUser);
if ($smtpPw !== '') supportRelaySet($db, 'smtp_password', supportRelayEncrypt($smtpPw, $MASTER_KEY));
supportRelaySet($db, 'smtp_from', $smtpFrom);
supportRelaySet($db, 'smtp_from_name', $smtpFromN);
supportRelaySet($db, 'smtp_encryption', $smtpEnc);
supportRelaySet($db, 'api_key', $apiKey);
supportRelaySet($db, 'setup_done', '1');
supportRelayJson(['ok' => true, 'api_key' => $apiKey]);
}