exec(" CREATE TABLE IF NOT EXISTS gitea_accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, base_url TEXT NOT NULL, token TEXT NOT NULL, position INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) "); })(); // ── Encryption helpers (mirrors email plugin / CalDavSync) ──────── function giteaEncKey(): string { $db = Database::connect(); $vk = $db->query("SELECT value FROM settings WHERE key = 'vault_key'")->fetchColumn(); if ($vk) return substr(hash('sha256', $vk), 0, 32); $hash = $db->query("SELECT value FROM settings WHERE key = 'password_hash'")->fetchColumn(); return substr(hash('sha256', $hash ?: 'hyprcrd-default-key'), 0, 32); } function giteaEncrypt(string $plain): string { $key = giteaEncKey(); $iv = random_bytes(16); $cipher = openssl_encrypt($plain, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv); return base64_encode($iv . $cipher); } function giteaDecrypt(string $encoded): string { $key = giteaEncKey(); $raw = base64_decode($encoded); if (!$raw || strlen($raw) < 17) return ''; $iv = substr($raw, 0, 16); $cipher = substr($raw, 16); return openssl_decrypt($cipher, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv) ?: ''; } function giteaJson($data, int $code = 200): void { http_response_code($code); header('Content-Type: application/json'); echo json_encode($data); exit; } function giteaGetAccount(int $id): ?array { $db = Database::connect(); $stmt = $db->prepare('SELECT * FROM gitea_accounts WHERE id = :id'); $stmt->execute(['id' => $id]); return $stmt->fetch() ?: null; } function giteaOpenClient(array $row): GiteaClient { return new GiteaClient($row['base_url'], giteaDecrypt($row['token'])); } // ── Route registration ──────────────────────────────────────────── PluginManager::instance()->onApiRoute(function (string $route, string $method) { // ── Accounts CRUD ── if ($route === '/api/gitea/accounts' && $method === 'GET') { $db = Database::connect(); $rows = $db->query('SELECT id, name, base_url, position, created_at FROM gitea_accounts ORDER BY position ASC, id ASC')->fetchAll(); giteaJson($rows); } if ($route === '/api/gitea/accounts' && $method === 'POST') { $input = json_decode(file_get_contents('php://input'), true) ?: []; $name = trim((string)($input['name'] ?? '')); $baseUrl = trim((string)($input['base_url'] ?? '')); $token = (string)($input['token'] ?? ''); if ($name === '' || $baseUrl === '' || $token === '') { giteaJson(['error' => 'name, base_url, and token are required'], 400); } // Normalize base URL: strip trailing slash, accept with/without scheme if (!preg_match('#^https?://#i', $baseUrl)) { $baseUrl = 'https://' . $baseUrl; } $baseUrl = rtrim($baseUrl, '/'); $db = Database::connect(); $stmt = $db->prepare(' INSERT INTO gitea_accounts (name, base_url, token) VALUES (:n, :u, :t) '); $stmt->execute([ 'n' => $name, 'u' => $baseUrl, 't' => giteaEncrypt($token), ]); giteaJson(['ok' => true, 'id' => (int)$db->lastInsertId()]); } if (preg_match('#^/api/gitea/accounts/(\d+)$#', $route, $m) && $method === 'PATCH') { $id = (int)$m[1]; $row = giteaGetAccount($id); if (!$row) giteaJson(['error' => 'account not found'], 404); $input = json_decode(file_get_contents('php://input'), true) ?: []; $fields = []; $params = ['id' => $id]; if (array_key_exists('name', $input)) { $fields[] = 'name = :name'; $params['name'] = trim((string)$input['name']); } if (array_key_exists('base_url', $input)) { $u = trim((string)$input['base_url']); if (!preg_match('#^https?://#i', $u)) $u = 'https://' . $u; $fields[] = 'base_url = :base_url'; $params['base_url'] = rtrim($u, '/'); } if (!empty($input['token'])) { $fields[] = 'token = :token'; $params['token'] = giteaEncrypt($input['token']); } if (!$fields) giteaJson(['ok' => true]); $db = Database::connect(); $db->prepare('UPDATE gitea_accounts SET ' . implode(', ', $fields) . ' WHERE id = :id')->execute($params); giteaJson(['ok' => true]); } if (preg_match('#^/api/gitea/accounts/(\d+)$#', $route, $m) && $method === 'DELETE') { $id = (int)$m[1]; $db = Database::connect(); $db->prepare('DELETE FROM gitea_accounts WHERE id = :id')->execute(['id' => $id]); giteaJson(['ok' => true]); } if (preg_match('#^/api/gitea/accounts/(\d+)/test$#', $route, $m) && $method === 'POST') { $id = (int)$m[1]; $row = giteaGetAccount($id); if (!$row) giteaJson(['error' => 'account not found'], 404); try { $client = giteaOpenClient($row); $user = $client->testConnection(); giteaJson([ 'ok' => true, 'user' => [ 'login' => $user['login'] ?? null, 'full_name' => $user['full_name'] ?? null, 'email' => $user['email'] ?? null, 'avatar_url' => $user['avatar_url'] ?? null, ], 'base_url' => $row['base_url'], ]); } catch (GiteaApiException $e) { giteaJson(['ok' => false, 'error' => "HTTP {$e->statusCode}: " . substr($e->body, 0, 300)], 200); } catch (\Throwable $e) { giteaJson(['ok' => false, 'error' => $e->getMessage()], 200); } } });