172 lines
6.7 KiB
PHP
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);
|
|
}
|
|
}
|
|
});
|