Automate Gitea init, NocoDB auto sign-in, and fix prod compose

- Add scripts/gitea-init.sh: runs migrations + creates admin user on
  first boot, replacing the manual installation wizard
- Set GITEA__security__INSTALL_LOCK=true in both compose files
- Add NocoDB auth bridge (nginx) + /api/services/nocodb-auth proxy
  endpoint so the admin iframe auto-authenticates
- Update NocoDBPage.tsx to fetch token and use auth bridge flow
- Fix docker-compose.prod.yml missing Gitea env vars for API container
  (GITEA_URL, GITEA_API_TOKEN, GITEA_ADMIN_PASSWORD, etc.)
- Pass NC_ADMIN_EMAIL/PASSWORD to API for NocoDB auth proxy
- Increase Gitea auto-setup retries from 3 to 6 with admin auth check
- Update config.sh non-interactive mode to set GITEA_ADMIN_USER
- Include gitea-init.sh in release tarball (build-release.sh)

Bunker Admin
This commit is contained in:
bunker-admin 2026-04-09 12:49:33 -06:00
parent 0a8e1fe46b
commit 36b709b911
11 changed files with 223 additions and 34 deletions

View File

@ -222,6 +222,8 @@ GITEA_URL=http://gitea-changemaker:3000
GITEA_PORT=3030
GITEA_WEB_PORT=3030
GITEA_SSH_PORT=2222
# Admin user (auto-created on first boot by gitea-init.sh)
GITEA_ADMIN_USER=admin
GITEA_DB_TYPE=mysql
GITEA_DB_HOST=gitea-db:3306
GITEA_DB_NAME=gitea

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { useOutletContext } from 'react-router-dom';
import { Button, Space, Badge, Spin, Grid, Result, Alert } from 'antd';
import { ReloadOutlined, LinkOutlined, DatabaseOutlined } from '@ant-design/icons';
@ -7,8 +7,6 @@ import type { AppOutletContext } from '@/components/AppLayout';
import type { ServicesStatus, ServicesConfig } from '@/types/api';
import { buildServiceUrl } from '@/lib/service-url';
const BANNER_DISMISSED_KEY = 'nocodb-auth-banner-dismissed';
export default function NocoDBPage() {
const { setPageHeader } = useOutletContext<AppOutletContext>();
const screens = Grid.useBreakpoint();
@ -17,9 +15,9 @@ export default function NocoDBPage() {
const [online, setOnline] = useState<boolean | null>(null);
const [config, setConfig] = useState<ServicesConfig | null>(null);
const [loading, setLoading] = useState(true);
const [bannerDismissed, setBannerDismissed] = useState(
() => localStorage.getItem(BANNER_DISMISSED_KEY) === 'true'
);
const [iframeSrc, setIframeSrc] = useState<string | null>(null);
const [authFailed, setAuthFailed] = useState(false);
const authAttempted = useRef(false);
const fetchStatus = useCallback(async () => {
try {
@ -44,7 +42,31 @@ export default function NocoDBPage() {
? buildServiceUrl(config.nocodbSubdomain, config.domain, config.nocodbPort)
: null;
// Auto sign-in: fetch NocoDB auth token and navigate iframe to auth bridge
useEffect(() => {
if (!serviceUrl || !online || authAttempted.current) return;
authAttempted.current = true;
(async () => {
try {
const res = await api.get<{ token: string }>('/services/nocodb-auth');
if (res.data.token) {
// Navigate iframe to auth bridge (same origin as NocoDB) which sets the cookie
setIframeSrc(`${serviceUrl}/auth-bridge#${res.data.token}`);
return;
}
} catch {
// Auth endpoint unavailable — fall back to direct URL
}
setAuthFailed(true);
setIframeSrc(serviceUrl);
})();
}, [serviceUrl, online]);
const handleRefresh = useCallback(() => {
authAttempted.current = false;
setIframeSrc(null);
setAuthFailed(false);
fetchStatus();
}, [fetchStatus]);
@ -115,11 +137,11 @@ export default function NocoDBPage() {
return (
<div style={{ height: 'calc(100vh - 64px)', display: 'flex', flexDirection: 'column' }}>
{!bannerDismissed && (
{authFailed && (
<Alert
message={
<>
If the database browser appears blank, you may need to{' '}
Auto sign-in unavailable. You may need to{' '}
<a href={serviceUrl} target="_blank" rel="noopener noreferrer">
sign in to NocoDB in a new tab
</a>{' '}
@ -129,23 +151,26 @@ export default function NocoDBPage() {
type="info"
showIcon
closable
onClose={() => {
setBannerDismissed(true);
localStorage.setItem(BANNER_DISMISSED_KEY, 'true');
}}
onClose={() => setAuthFailed(false)}
style={{ borderRadius: 0, flexShrink: 0 }}
/>
)}
<iframe
src={serviceUrl}
style={{
width: '100%',
flex: 1,
border: 'none',
display: 'block',
}}
title="NocoDB"
/>
{iframeSrc ? (
<iframe
src={iframeSrc}
style={{
width: '100%',
flex: 1,
border: 'none',
display: 'block',
}}
title="NocoDB"
/>
) : (
<div style={{ textAlign: 'center', padding: 80 }}>
<Spin size="large" tip="Signing in to NocoDB..." />
</div>
)}
</div>
);
}

View File

@ -96,6 +96,8 @@ const envSchema = z.object({
// Platform Services (NocoDB, n8n, Gitea)
NOCODB_URL: z.string().default('http://changemaker-v2-nocodb:8080'),
NC_ADMIN_EMAIL: z.string().default(''),
NC_ADMIN_PASSWORD: z.string().default(''),
NOCODB_PORT: z.coerce.number().default(8091),
NOCODB_EMBED_PORT: z.coerce.number().default(8881),
N8N_URL: z.string().default('http://n8n-changemaker:5678'),

View File

@ -393,17 +393,26 @@ async function autoSetupIfNeeded(): Promise<{ alreadyComplete: boolean; success:
// DB might not be ready yet
}
// Wait for Gitea to be available (up to 3 retries, 15s apart)
// Wait for Gitea to be available and admin user to exist (up to 6 retries, 10s apart).
// The gitea-init.sh script creates the admin user after migrations,
// so we need to wait for both Gitea web AND admin auth to be ready.
let giteaReady = false;
for (let i = 0; i < 3; i++) {
for (let i = 0; i < 6; i++) {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
// Check if Gitea is online
const res = await fetch(`${env.GITEA_URL}/api/v1/version`, { signal: controller.signal });
if (res.ok) {
giteaReady = true;
break;
// Also verify admin auth works (user may not exist yet if init script is still running)
try {
await giteaBasicRequest<{ login: string }>('GET', '/user', 'admin', password);
giteaReady = true;
break;
} catch {
// Admin user not ready yet — gitea-init.sh may still be running
}
}
} finally {
clearTimeout(timeout);
@ -411,14 +420,14 @@ async function autoSetupIfNeeded(): Promise<{ alreadyComplete: boolean; success:
} catch {
// Not ready yet
}
if (i < 2) {
logger.info(`Gitea auto-setup: waiting for Gitea to be ready (attempt ${i + 1}/3)...`);
await new Promise(r => setTimeout(r, 15000));
if (i < 5) {
logger.info(`Gitea auto-setup: waiting for Gitea + admin user (attempt ${i + 1}/6)...`);
await new Promise(r => setTimeout(r, 10000));
}
}
if (!giteaReady) {
return { alreadyComplete: false, success: false, error: 'Gitea not reachable after 3 attempts' };
return { alreadyComplete: false, success: false, error: 'Gitea not reachable or admin user not ready after 6 attempts' };
}
// Run setup

View File

@ -64,6 +64,48 @@ router.get(
},
);
// GET /api/services/nocodb-auth — proxy NocoDB signin to get an auth token for iframe auto-login
router.get(
'/nocodb-auth',
async (_req: Request, res: Response, next: NextFunction) => {
try {
if (!env.NC_ADMIN_EMAIL || !env.NC_ADMIN_PASSWORD) {
res.status(503).json({ error: 'NocoDB admin credentials not configured' });
return;
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(`${env.NOCODB_URL}/api/v1/auth/user/signin`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: env.NC_ADMIN_EMAIL, password: env.NC_ADMIN_PASSWORD }),
signal: controller.signal,
});
if (!response.ok) {
res.status(502).json({ error: 'NocoDB authentication failed' });
return;
}
const data = (await response.json()) as { token?: string };
if (!data.token) {
res.status(502).json({ error: 'No token in NocoDB response' });
return;
}
res.json({ token: data.token });
} finally {
clearTimeout(timeout);
}
} catch (err) {
logger.error('NocoDB auth proxy failed', err);
next(err);
}
},
);
// GET /api/services/config — return public-facing port numbers + subdomain info for iframe URLs
router.get(
'/config',

View File

@ -852,19 +852,29 @@ configure_features() {
if [[ "$NON_INTERACTIVE" == "false" ]]; then
echo ""
info "Gitea auto-setup will create the API token, repos, and OAuth app automatically."
info "You need to provide the Gitea admin password (set during Gitea's first-run install)."
info "Gitea will be auto-initialized on first boot (no manual install wizard)."
info "The admin user and docs comment system will be configured automatically."
info "Provide a password for the Gitea admin account:"
echo ""
read -srp " Gitea admin password [leave blank to set up later via admin GUI]: " gitea_admin_pw
echo ""
if [[ -n "$gitea_admin_pw" ]]; then
update_env_var "GITEA_ADMIN_USER" "admin"
update_env_var "GITEA_ADMIN_PASSWORD" "$gitea_admin_pw"
update_env_var "GITEA_COMMENTS_REPO_OWNER" "admin"
success "Gitea admin password saved — auto-setup will run on next start"
success "Gitea admin user + docs comment auto-setup configured"
else
info "No password provided. Run Gitea Setup from the admin GUI after first start."
fi
else
# Non-interactive: reuse admin password for Gitea
local gitea_pw="${NI_ADMIN_PASSWORD:-}"
if [[ -n "$gitea_pw" ]]; then
update_env_var "GITEA_ADMIN_USER" "admin"
update_env_var "GITEA_ADMIN_PASSWORD" "$gitea_pw"
update_env_var "GITEA_COMMENTS_REPO_OWNER" "admin"
fi
fi
else
DOCS_COMMENTS_ENABLED="no"

View File

@ -128,6 +128,16 @@ services:
- GITEA_REGISTRY=${GITEA_REGISTRY:-gitea.bnkops.com/admin}
- GITEA_REGISTRY_USER=${GITEA_REGISTRY_USER:-}
- GITEA_REGISTRY_PASS=${GITEA_REGISTRY_PASS:-}
# Gitea (docs comments, version history, auto-setup)
- GITEA_URL=${GITEA_URL:-http://gitea-changemaker:3000}
- GITEA_API_TOKEN=${GITEA_API_TOKEN:-}
- GITEA_ADMIN_PASSWORD=${GITEA_ADMIN_PASSWORD:-}
- GITEA_DOCS_REPO=${GITEA_DOCS_REPO:-admin/changemaker.lite}
- GITEA_DOCS_PREFIX=${GITEA_DOCS_PREFIX:-mkdocs/docs}
- GITEA_DOCS_BRANCH=${GITEA_DOCS_BRANCH:-v2}
# NocoDB credentials (for auto sign-in proxy)
- NC_ADMIN_EMAIL=${NC_ADMIN_EMAIL:-admin@cmlite.org}
- NC_ADMIN_PASSWORD=${NC_ADMIN_PASSWORD:-}
# GeoIP (MaxMind GeoLite2)
- MAXMIND_ACCOUNT_ID=${MAXMIND_ACCOUNT_ID:-}
- MAXMIND_LICENSE_KEY=${MAXMIND_LICENSE_KEY:-}
@ -688,11 +698,19 @@ services:
- GITEA__service__ENABLE_REVERSE_PROXY_EMAIL=false
- GITEA__service__REVERSE_PROXY_AUTHENTICATION_HEADER=X-WEBAUTH-USER
- GITEA__service__REQUIRE_SIGNIN_VIEW=true
# Skip installation wizard — admin user created by gitea-init.sh
- GITEA__security__INSTALL_LOCK=true
# Admin user creation (used by gitea-init.sh on first boot)
- GITEA_ADMIN_USER=${GITEA_ADMIN_USER:-admin}
- GITEA_ADMIN_PASSWORD=${GITEA_ADMIN_PASSWORD:-}
- GITEA_ADMIN_EMAIL=${INITIAL_ADMIN_EMAIL:-admin@cmlite.org}
restart: unless-stopped
volumes:
- gitea-data:/data
- ./scripts/gitea-init.sh:/custom/gitea-init.sh:ro
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
command: ["/bin/sh", "/custom/gitea-init.sh"]
ports:
- "127.0.0.1:${GITEA_WEB_PORT:-3030}:3000"
- "127.0.0.1:${GITEA_SSH_PORT:-2222}:22"

View File

@ -136,6 +136,9 @@ services:
- GITEA_DOCS_REPO=${GITEA_DOCS_REPO:-admin/changemaker.lite}
- GITEA_DOCS_PREFIX=${GITEA_DOCS_PREFIX:-mkdocs/docs}
- GITEA_DOCS_BRANCH=${GITEA_DOCS_BRANCH:-v2}
# NocoDB credentials (for auto sign-in proxy)
- NC_ADMIN_EMAIL=${NC_ADMIN_EMAIL:-admin@cmlite.org}
- NC_ADMIN_PASSWORD=${NC_ADMIN_PASSWORD:-}
# GeoIP (MaxMind GeoLite2)
- MAXMIND_ACCOUNT_ID=${MAXMIND_ACCOUNT_ID:-}
- MAXMIND_LICENSE_KEY=${MAXMIND_LICENSE_KEY:-}
@ -714,11 +717,19 @@ services:
- GITEA__service__ENABLE_REVERSE_PROXY_EMAIL=false
- GITEA__service__REVERSE_PROXY_AUTHENTICATION_HEADER=X-WEBAUTH-USER
- GITEA__service__REQUIRE_SIGNIN_VIEW=true
# Skip installation wizard — admin user created by gitea-init.sh
- GITEA__security__INSTALL_LOCK=true
# Admin user creation (used by gitea-init.sh on first boot)
- GITEA_ADMIN_USER=${GITEA_ADMIN_USER:-admin}
- GITEA_ADMIN_PASSWORD=${GITEA_ADMIN_PASSWORD:-}
- GITEA_ADMIN_EMAIL=${INITIAL_ADMIN_EMAIL:-admin@cmlite.org}
restart: unless-stopped
volumes:
- gitea-data:/data
- ./scripts/gitea-init.sh:/custom/gitea-init.sh:ro
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
command: ["/bin/sh", "/custom/gitea-init.sh"]
ports:
- "127.0.0.1:${GITEA_WEB_PORT:-3030}:3000"
- "127.0.0.1:${GITEA_SSH_PORT:-2222}:22"

View File

@ -78,6 +78,14 @@ server {
server_name db.${DOMAIN};
add_header Content-Security-Policy "frame-ancestors 'self' app.${DOMAIN}" always;
# Auth bridge for iframe auto-sign-in (token passed via URL hash, never sent to server)
location = /auth-bridge {
default_type text/html;
add_header Cache-Control "no-store" always;
add_header Content-Security-Policy "frame-ancestors 'self' app.${DOMAIN}" always;
return 200 '<!DOCTYPE html><html><head><meta charset="utf-8"><script>(function(){var t=location.hash.substring(1);if(!t){document.body.innerText="No token";return;}document.cookie="nocodb-token="+encodeURIComponent(t)+";path=/;SameSite=Lax;max-age=86400";try{localStorage.setItem("nocodb-token",JSON.stringify(t));}catch(e){}window.location.replace("/dashboard/");})()</script></head><body>Signing in...</body></html>';
}
location / {
set $upstream_nocodb http://changemaker-v2-nocodb:8080;
proxy_pass $upstream_nocodb;
@ -287,6 +295,14 @@ server {
server {
listen ${NOCODB_EMBED_PORT};
# Auth bridge for iframe auto-sign-in (localhost/dev variant)
location = /auth-bridge {
default_type text/html;
add_header Cache-Control "no-store" always;
return 200 '<!DOCTYPE html><html><head><meta charset="utf-8"><script>(function(){var t=location.hash.substring(1);if(!t){document.body.innerText="No token";return;}document.cookie="nocodb-token="+encodeURIComponent(t)+";path=/;SameSite=Lax;max-age=86400";try{localStorage.setItem("nocodb-token",JSON.stringify(t));}catch(e){}window.location.replace("/dashboard/");})()</script></head><body>Signing in...</body></html>';
}
location / {
set $upstream_nocodb http://changemaker-v2-nocodb:8080;
proxy_pass $upstream_nocodb;

View File

@ -103,7 +103,7 @@ cp "$PROJECT_DIR/api/prisma/init-nocodb-db.sh" "$STAGE_DIR/scripts/"
cp "$PROJECT_DIR/api/prisma/init-gancio-db.sh" "$STAGE_DIR/scripts/"
# Runtime scripts
for script in nocodb-init.sh mkdocs-entrypoint.sh backup.sh \
for script in nocodb-init.sh gitea-init.sh mkdocs-entrypoint.sh backup.sh \
upgrade.sh upgrade-check.sh upgrade-watcher.sh \
uninstall.sh test-deployment.sh; do
if [[ -f "$PROJECT_DIR/scripts/$script" ]]; then

54
scripts/gitea-init.sh Executable file
View File

@ -0,0 +1,54 @@
#!/bin/sh
# =============================================================================
# Gitea Initialization Script
# =============================================================================
# Replaces the default CMD in the Gitea Docker container.
# Runs database migrations, creates the admin user (if credentials are provided
# and the user doesn't already exist), then starts the Gitea web server.
#
# This script is exec'd by /usr/bin/entrypoint, which has already:
# - Set up UID/GID
# - Created directories with correct permissions
# - Converted GITEA__* env vars into /data/gitea/conf/app.ini
# =============================================================================
set -e
PREFIX="[gitea-init]"
log() { echo "$PREFIX $1"; }
# --- Step 1: Run database migrations ---
log "Running database migrations..."
MIGRATE_OK=false
for i in $(seq 1 10); do
if gitea migrate 2>&1; then
MIGRATE_OK=true
log "Migrations complete"
break
fi
log "Waiting for database... (attempt $i/10)"
sleep 3
done
if [ "$MIGRATE_OK" = false ]; then
log "WARNING: Migrations may not have completed — starting anyway"
fi
# --- Step 2: Create admin user if credentials provided ---
if [ -n "$GITEA_ADMIN_USER" ] && [ -n "$GITEA_ADMIN_PASSWORD" ] && [ -n "$GITEA_ADMIN_EMAIL" ]; then
log "Creating admin user '${GITEA_ADMIN_USER}'..."
if gitea admin user create --admin \
--username "$GITEA_ADMIN_USER" \
--password "$GITEA_ADMIN_PASSWORD" \
--email "$GITEA_ADMIN_EMAIL" \
--must-change-password false 2>&1; then
log "Admin user created successfully"
else
log "Admin user already exists (or creation skipped)"
fi
else
log "No GITEA_ADMIN_USER/PASSWORD/EMAIL set — skipping admin creation"
fi
# --- Step 3: Start Gitea web server ---
log "Starting Gitea web server..."
exec gitea web