Files
hyprcrd/public/plugins/gitea/backend.php
T
2026-04-10 11:46:15 +02:00

172 lines
6.7 KiB
PHP

<?php
/**
* Gitea Plugin: backend.
*
* Replaces the standalone Python `gitea-issue-client` tool. MVP scope:
* accounts CRUD + connection test. Issue browsing/extraction comes once
* the connection layer is verified. See TODO.md for the deferred items.
*
* Routes:
* GET /api/gitea/accounts → list accounts (no tokens)
* POST /api/gitea/accounts → create account
* PATCH /api/gitea/accounts/{id} → update (omit token to keep)
* DELETE /api/gitea/accounts/{id} → remove
* POST /api/gitea/accounts/{id}/test → verify token via /api/v1/user
*/
require_once __DIR__ . '/GiteaClient.php';
// ── Schema migration ──────────────────────────────────────────────
(function () {
$db = Database::connect();
$db->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);
}
}
});