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_PORT=3030
GITEA_WEB_PORT=3030 GITEA_WEB_PORT=3030
GITEA_SSH_PORT=2222 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_TYPE=mysql
GITEA_DB_HOST=gitea-db:3306 GITEA_DB_HOST=gitea-db:3306
GITEA_DB_NAME=gitea 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 { useOutletContext } from 'react-router-dom';
import { Button, Space, Badge, Spin, Grid, Result, Alert } from 'antd'; import { Button, Space, Badge, Spin, Grid, Result, Alert } from 'antd';
import { ReloadOutlined, LinkOutlined, DatabaseOutlined } from '@ant-design/icons'; 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 type { ServicesStatus, ServicesConfig } from '@/types/api';
import { buildServiceUrl } from '@/lib/service-url'; import { buildServiceUrl } from '@/lib/service-url';
const BANNER_DISMISSED_KEY = 'nocodb-auth-banner-dismissed';
export default function NocoDBPage() { export default function NocoDBPage() {
const { setPageHeader } = useOutletContext<AppOutletContext>(); const { setPageHeader } = useOutletContext<AppOutletContext>();
const screens = Grid.useBreakpoint(); const screens = Grid.useBreakpoint();
@ -17,9 +15,9 @@ export default function NocoDBPage() {
const [online, setOnline] = useState<boolean | null>(null); const [online, setOnline] = useState<boolean | null>(null);
const [config, setConfig] = useState<ServicesConfig | null>(null); const [config, setConfig] = useState<ServicesConfig | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [bannerDismissed, setBannerDismissed] = useState( const [iframeSrc, setIframeSrc] = useState<string | null>(null);
() => localStorage.getItem(BANNER_DISMISSED_KEY) === 'true' const [authFailed, setAuthFailed] = useState(false);
); const authAttempted = useRef(false);
const fetchStatus = useCallback(async () => { const fetchStatus = useCallback(async () => {
try { try {
@ -44,7 +42,31 @@ export default function NocoDBPage() {
? buildServiceUrl(config.nocodbSubdomain, config.domain, config.nocodbPort) ? buildServiceUrl(config.nocodbSubdomain, config.domain, config.nocodbPort)
: null; : 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(() => { const handleRefresh = useCallback(() => {
authAttempted.current = false;
setIframeSrc(null);
setAuthFailed(false);
fetchStatus(); fetchStatus();
}, [fetchStatus]); }, [fetchStatus]);
@ -115,11 +137,11 @@ export default function NocoDBPage() {
return ( return (
<div style={{ height: 'calc(100vh - 64px)', display: 'flex', flexDirection: 'column' }}> <div style={{ height: 'calc(100vh - 64px)', display: 'flex', flexDirection: 'column' }}>
{!bannerDismissed && ( {authFailed && (
<Alert <Alert
message={ 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"> <a href={serviceUrl} target="_blank" rel="noopener noreferrer">
sign in to NocoDB in a new tab sign in to NocoDB in a new tab
</a>{' '} </a>{' '}
@ -129,23 +151,26 @@ export default function NocoDBPage() {
type="info" type="info"
showIcon showIcon
closable closable
onClose={() => { onClose={() => setAuthFailed(false)}
setBannerDismissed(true);
localStorage.setItem(BANNER_DISMISSED_KEY, 'true');
}}
style={{ borderRadius: 0, flexShrink: 0 }} style={{ borderRadius: 0, flexShrink: 0 }}
/> />
)} )}
<iframe {iframeSrc ? (
src={serviceUrl} <iframe
style={{ src={iframeSrc}
width: '100%', style={{
flex: 1, width: '100%',
border: 'none', flex: 1,
display: 'block', border: 'none',
}} display: 'block',
title="NocoDB" }}
/> title="NocoDB"
/>
) : (
<div style={{ textAlign: 'center', padding: 80 }}>
<Spin size="large" tip="Signing in to NocoDB..." />
</div>
)}
</div> </div>
); );
} }

View File

@ -96,6 +96,8 @@ const envSchema = z.object({
// Platform Services (NocoDB, n8n, Gitea) // Platform Services (NocoDB, n8n, Gitea)
NOCODB_URL: z.string().default('http://changemaker-v2-nocodb:8080'), 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_PORT: z.coerce.number().default(8091),
NOCODB_EMBED_PORT: z.coerce.number().default(8881), NOCODB_EMBED_PORT: z.coerce.number().default(8881),
N8N_URL: z.string().default('http://n8n-changemaker:5678'), 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 // 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; let giteaReady = false;
for (let i = 0; i < 3; i++) { for (let i = 0; i < 6; i++) {
try { try {
const controller = new AbortController(); const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000); const timeout = setTimeout(() => controller.abort(), 5000);
try { try {
// Check if Gitea is online
const res = await fetch(`${env.GITEA_URL}/api/v1/version`, { signal: controller.signal }); const res = await fetch(`${env.GITEA_URL}/api/v1/version`, { signal: controller.signal });
if (res.ok) { if (res.ok) {
giteaReady = true; // Also verify admin auth works (user may not exist yet if init script is still running)
break; 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 { } finally {
clearTimeout(timeout); clearTimeout(timeout);
@ -411,14 +420,14 @@ async function autoSetupIfNeeded(): Promise<{ alreadyComplete: boolean; success:
} catch { } catch {
// Not ready yet // Not ready yet
} }
if (i < 2) { if (i < 5) {
logger.info(`Gitea auto-setup: waiting for Gitea to be ready (attempt ${i + 1}/3)...`); logger.info(`Gitea auto-setup: waiting for Gitea + admin user (attempt ${i + 1}/6)...`);
await new Promise(r => setTimeout(r, 15000)); await new Promise(r => setTimeout(r, 10000));
} }
} }
if (!giteaReady) { 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 // 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 // GET /api/services/config — return public-facing port numbers + subdomain info for iframe URLs
router.get( router.get(
'/config', '/config',

View File

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

View File

@ -128,6 +128,16 @@ services:
- GITEA_REGISTRY=${GITEA_REGISTRY:-gitea.bnkops.com/admin} - GITEA_REGISTRY=${GITEA_REGISTRY:-gitea.bnkops.com/admin}
- GITEA_REGISTRY_USER=${GITEA_REGISTRY_USER:-} - GITEA_REGISTRY_USER=${GITEA_REGISTRY_USER:-}
- GITEA_REGISTRY_PASS=${GITEA_REGISTRY_PASS:-} - 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) # GeoIP (MaxMind GeoLite2)
- MAXMIND_ACCOUNT_ID=${MAXMIND_ACCOUNT_ID:-} - MAXMIND_ACCOUNT_ID=${MAXMIND_ACCOUNT_ID:-}
- MAXMIND_LICENSE_KEY=${MAXMIND_LICENSE_KEY:-} - MAXMIND_LICENSE_KEY=${MAXMIND_LICENSE_KEY:-}
@ -688,11 +698,19 @@ services:
- GITEA__service__ENABLE_REVERSE_PROXY_EMAIL=false - GITEA__service__ENABLE_REVERSE_PROXY_EMAIL=false
- GITEA__service__REVERSE_PROXY_AUTHENTICATION_HEADER=X-WEBAUTH-USER - GITEA__service__REVERSE_PROXY_AUTHENTICATION_HEADER=X-WEBAUTH-USER
- GITEA__service__REQUIRE_SIGNIN_VIEW=true - 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 restart: unless-stopped
volumes: volumes:
- gitea-data:/data - gitea-data:/data
- ./scripts/gitea-init.sh:/custom/gitea-init.sh:ro
- /etc/timezone:/etc/timezone:ro - /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
command: ["/bin/sh", "/custom/gitea-init.sh"]
ports: ports:
- "127.0.0.1:${GITEA_WEB_PORT:-3030}:3000" - "127.0.0.1:${GITEA_WEB_PORT:-3030}:3000"
- "127.0.0.1:${GITEA_SSH_PORT:-2222}:22" - "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_REPO=${GITEA_DOCS_REPO:-admin/changemaker.lite}
- GITEA_DOCS_PREFIX=${GITEA_DOCS_PREFIX:-mkdocs/docs} - GITEA_DOCS_PREFIX=${GITEA_DOCS_PREFIX:-mkdocs/docs}
- GITEA_DOCS_BRANCH=${GITEA_DOCS_BRANCH:-v2} - 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) # GeoIP (MaxMind GeoLite2)
- MAXMIND_ACCOUNT_ID=${MAXMIND_ACCOUNT_ID:-} - MAXMIND_ACCOUNT_ID=${MAXMIND_ACCOUNT_ID:-}
- MAXMIND_LICENSE_KEY=${MAXMIND_LICENSE_KEY:-} - MAXMIND_LICENSE_KEY=${MAXMIND_LICENSE_KEY:-}
@ -714,11 +717,19 @@ services:
- GITEA__service__ENABLE_REVERSE_PROXY_EMAIL=false - GITEA__service__ENABLE_REVERSE_PROXY_EMAIL=false
- GITEA__service__REVERSE_PROXY_AUTHENTICATION_HEADER=X-WEBAUTH-USER - GITEA__service__REVERSE_PROXY_AUTHENTICATION_HEADER=X-WEBAUTH-USER
- GITEA__service__REQUIRE_SIGNIN_VIEW=true - 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 restart: unless-stopped
volumes: volumes:
- gitea-data:/data - gitea-data:/data
- ./scripts/gitea-init.sh:/custom/gitea-init.sh:ro
- /etc/timezone:/etc/timezone:ro - /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
command: ["/bin/sh", "/custom/gitea-init.sh"]
ports: ports:
- "127.0.0.1:${GITEA_WEB_PORT:-3030}:3000" - "127.0.0.1:${GITEA_WEB_PORT:-3030}:3000"
- "127.0.0.1:${GITEA_SSH_PORT:-2222}:22" - "127.0.0.1:${GITEA_SSH_PORT:-2222}:22"

View File

@ -78,6 +78,14 @@ server {
server_name db.${DOMAIN}; server_name db.${DOMAIN};
add_header Content-Security-Policy "frame-ancestors 'self' app.${DOMAIN}" always; 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 / { location / {
set $upstream_nocodb http://changemaker-v2-nocodb:8080; set $upstream_nocodb http://changemaker-v2-nocodb:8080;
proxy_pass $upstream_nocodb; proxy_pass $upstream_nocodb;
@ -287,6 +295,14 @@ server {
server { server {
listen ${NOCODB_EMBED_PORT}; 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 / { location / {
set $upstream_nocodb http://changemaker-v2-nocodb:8080; set $upstream_nocodb http://changemaker-v2-nocodb:8080;
proxy_pass $upstream_nocodb; 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/" cp "$PROJECT_DIR/api/prisma/init-gancio-db.sh" "$STAGE_DIR/scripts/"
# Runtime 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 \ upgrade.sh upgrade-check.sh upgrade-watcher.sh \
uninstall.sh test-deployment.sh; do uninstall.sh test-deployment.sh; do
if [[ -f "$PROJECT_DIR/scripts/$script" ]]; then 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