fix(gitea): healthz probe + DB-first token + honest banner copy
Three related bugs surfaced by the Gitea Setup wizard: 1. checkStatus probed /api/v1/version unauthenticated, which returns 403 under REQUIRE_SIGNIN_VIEW=true (Gitea's default). Result: giteaOnline and installComplete always read false on any REQUIRE_SIGNIN_VIEW=true install, producing a green "Setup Complete" banner over two red-X foundational rows. Fix: use /api/healthz (public, exempt from sign-in requirement). Bonus: split installComplete from giteaOnline — online now honestly means "process responding", installComplete means "admin user exists" (verified either by DB flag or by Basic Auth probe on /user). Status code is now logged on healthz failure for debuggability. 2. docs-history.service.ts read the API token from env only, bypassing the DB. After the setup wizard wrote the token to siteSettings, docs-history still saw env.GITEA_API_TOKEN empty and silently did nothing. Same file also had a second /api/v1/version bug in isAvailable(). Fix: route token lookup through giteaClient.getConfig() (DB-first, env fallback — same pattern as the rest of the codebase). Switch isAvailable() to /api/healthz. 3. UI banner confidently claimed "Gitea is not running" on any non-200 response, including the 403 case above. Misleading, and dangerous to point users at `docker compose up -d gitea` when the container is already running. Fix: softer "not reachable" copy that points users at the API log for the actual status code. Bunker Admin
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { env } from '../../config/env';
|
||||
import { logger } from '../../utils/logger';
|
||||
import { giteaClient } from '../../services/gitea.client';
|
||||
|
||||
/**
|
||||
* Docs version history via Gitea API.
|
||||
@@ -39,8 +40,11 @@ function getBaseUrl(): string {
|
||||
return env.GITEA_URL;
|
||||
}
|
||||
|
||||
function getApiToken(): string {
|
||||
return env.GITEA_API_TOKEN || '';
|
||||
async function getApiToken(): Promise<string> {
|
||||
// DB-first (token is stored encrypted in siteSettings after the setup wizard runs),
|
||||
// env var is only a bootstrap fallback for environments that set it manually.
|
||||
const config = await giteaClient.getConfig();
|
||||
return config.apiToken || '';
|
||||
}
|
||||
|
||||
function getRepoPath(): string {
|
||||
@@ -65,7 +69,7 @@ async function giteaRequest<T>(
|
||||
path: string,
|
||||
body?: Record<string, unknown>,
|
||||
): Promise<T> {
|
||||
const token = getApiToken();
|
||||
const token = await getApiToken();
|
||||
if (!token) throw new Error('Gitea API token not configured');
|
||||
|
||||
const url = `${getBaseUrl()}/api/v1${path}`;
|
||||
@@ -123,7 +127,7 @@ async function commitFile(
|
||||
authorName: string,
|
||||
authorEmail: string,
|
||||
): Promise<void> {
|
||||
const token = getApiToken();
|
||||
const token = await getApiToken();
|
||||
if (!token) return; // Silently skip if not configured
|
||||
|
||||
const repoFilePath = `${getDocsPrefix()}/${docsRelativePath}`;
|
||||
@@ -239,14 +243,15 @@ async function restoreRevision(
|
||||
* Check if the Gitea history feature is available (token configured and Gitea reachable).
|
||||
*/
|
||||
async function isAvailable(): Promise<boolean> {
|
||||
const token = getApiToken();
|
||||
const token = await getApiToken();
|
||||
if (!token) return false;
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 5000);
|
||||
try {
|
||||
const res = await fetch(`${getBaseUrl()}/api/v1/version`, {
|
||||
// Use /api/healthz — public endpoint, exempt from REQUIRE_SIGNIN_VIEW.
|
||||
const res = await fetch(`${getBaseUrl()}/api/healthz`, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
return res.ok;
|
||||
|
||||
@@ -116,25 +116,30 @@ async function checkStatus(): Promise<{
|
||||
let reposCreated = false;
|
||||
let oauthConfigured = false;
|
||||
|
||||
// Check Gitea online
|
||||
// Check Gitea is reachable via the public healthz endpoint.
|
||||
// `/api/healthz` is exempt from REQUIRE_SIGNIN_VIEW, which gates every other /api/v1/* route.
|
||||
// Using an auth-gated endpoint here would produce a false-negative 403 on a perfectly healthy Gitea.
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 5000);
|
||||
try {
|
||||
const res = await fetch(`${env.GITEA_URL}/api/v1/version`, {
|
||||
const res = await fetch(`${env.GITEA_URL}/api/healthz`, {
|
||||
signal: controller.signal,
|
||||
redirect: 'manual',
|
||||
});
|
||||
giteaOnline = res.ok;
|
||||
installComplete = res.ok; // If API responds, install is complete
|
||||
if (!res.ok) {
|
||||
logger.debug(`Gitea /api/healthz returned ${res.status} ${res.statusText}`);
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
} catch {
|
||||
// Not reachable
|
||||
} catch (err) {
|
||||
logger.debug(`Gitea /api/healthz unreachable: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
|
||||
// Check DB settings
|
||||
let dbSetupComplete = false;
|
||||
try {
|
||||
const settings = await prisma.siteSettings.findFirst({
|
||||
select: {
|
||||
@@ -148,22 +153,39 @@ async function checkStatus(): Promise<{
|
||||
if (settings) {
|
||||
tokenConfigured = !!settings.giteaApiToken;
|
||||
oauthConfigured = !!settings.giteaOauthClientId && !!settings.giteaOauthClientSecret;
|
||||
|
||||
if (settings.giteaSetupComplete) {
|
||||
return {
|
||||
giteaOnline,
|
||||
installComplete,
|
||||
tokenConfigured,
|
||||
reposCreated: true, // Trust the flag
|
||||
oauthConfigured,
|
||||
setupComplete: true,
|
||||
};
|
||||
}
|
||||
dbSetupComplete = !!settings.giteaSetupComplete;
|
||||
}
|
||||
} catch {
|
||||
// DB not available
|
||||
}
|
||||
|
||||
// `installComplete` here means: Gitea's install wizard is done AND an admin user exists.
|
||||
// `giteaOnline` alone (healthz 200) proves the process is up but says nothing about admin bootstrap.
|
||||
// Preference order:
|
||||
// 1. If DB flag is set, trust it (fast path — no network call)
|
||||
// 2. Else if Gitea is online and we have an admin password, probe /user with Basic Auth
|
||||
if (dbSetupComplete) {
|
||||
installComplete = true;
|
||||
} else if (giteaOnline && env.GITEA_ADMIN_PASSWORD) {
|
||||
try {
|
||||
await giteaBasicRequest<{ login: string }>('GET', '/user', 'admin', env.GITEA_ADMIN_PASSWORD);
|
||||
installComplete = true;
|
||||
} catch {
|
||||
// Admin user doesn't exist or password mismatch — installComplete stays false
|
||||
}
|
||||
}
|
||||
|
||||
if (dbSetupComplete) {
|
||||
return {
|
||||
giteaOnline,
|
||||
installComplete,
|
||||
tokenConfigured,
|
||||
reposCreated: true, // Trust the flag
|
||||
oauthConfigured,
|
||||
setupComplete: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Check repos if we have a token
|
||||
if (tokenConfigured && giteaOnline) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user