811d4cad42
Users can now report bugs, suggestions, or questions from a new /support page. Each submission sends a confirmation email and, when GITEA_URL/ GITEA_TOKEN/GITEA_REPO are configured in admin Settings, opens a labeled issue on that repo automatically (best-effort — failure doesn't block the ticket). Admins get a Support section to triage status and retry failed Gitea issue creation. New support_tickets table, gitea.ts client, site-settings entries for the three new secrets, and OpenAPI docs for the two user-facing endpoints. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
56 lines
1.7 KiB
TypeScript
56 lines
1.7 KiB
TypeScript
import { getSiteSetting } from "@/lib/site-settings";
|
|
|
|
const LABELS: Record<string, string[]> = {
|
|
bug: ["bug"],
|
|
suggestion: ["enhancement"],
|
|
question: ["question"],
|
|
};
|
|
|
|
/**
|
|
* Opens an issue on the configured Gitea repo for a support ticket. Returns
|
|
* the issue's HTML URL on success, or null if Gitea isn't configured or the
|
|
* request fails — callers must treat this as best-effort, not block on it,
|
|
* since the ticket itself is already saved regardless.
|
|
*/
|
|
export async function createGiteaIssue(opts: {
|
|
type: string;
|
|
title: string;
|
|
body: string;
|
|
}): Promise<{ url: string | null; error: string | null }> {
|
|
const [baseUrl, token, repo] = await Promise.all([
|
|
getSiteSetting("GITEA_URL"),
|
|
getSiteSetting("GITEA_TOKEN"),
|
|
getSiteSetting("GITEA_REPO"),
|
|
]);
|
|
|
|
if (!baseUrl || !token || !repo) {
|
|
return { url: null, error: null };
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/api/v1/repos/${repo}/issues`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `token ${token}`,
|
|
},
|
|
body: JSON.stringify({
|
|
title: opts.title,
|
|
body: opts.body,
|
|
labels: LABELS[opts.type] ?? [],
|
|
}),
|
|
signal: AbortSignal.timeout(10000),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const text = await res.text().catch(() => "");
|
|
return { url: null, error: `Gitea returned ${res.status}: ${text.slice(0, 300)}` };
|
|
}
|
|
|
|
const issue = (await res.json()) as { html_url?: string };
|
|
return { url: issue.html_url ?? null, error: issue.html_url ? null : "Gitea response had no html_url" };
|
|
} catch (err) {
|
|
return { url: null, error: err instanceof Error ? err.message : "Unknown error" };
|
|
}
|
|
}
|