c
This commit is contained in:
+7
-1
@@ -22,8 +22,14 @@ def read_env_value(key: str, default: str = "") -> str:
|
||||
line = line.strip()
|
||||
if line.startswith(f"{key}="):
|
||||
return line.split("=", 1)[1].strip().strip('"')
|
||||
except Exception:
|
||||
except FileNotFoundError:
|
||||
# appfactory.env nemusí existovat (např. lokální vývoj) – očekávaný stav, vrátíme default.
|
||||
pass
|
||||
except Exception as exc:
|
||||
# Neočekávaná chyba čtení configu – nesmí být tichá.
|
||||
from app.logging_config import get_logger
|
||||
|
||||
get_logger(__name__).warning("Čtení %s (klíč %s) selhalo: %s", APPFACTORY_ENV, key, exc)
|
||||
|
||||
return default
|
||||
|
||||
|
||||
+59
-12
@@ -8,6 +8,9 @@ from urllib.request import urlopen
|
||||
|
||||
from app.config import get_gitea_admin_token, get_gitea_server_url
|
||||
from app.db.users import update_gitea_sync_state
|
||||
from app.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
GITEA_ACCOUNT_ROLES = {"admin", "developer"}
|
||||
|
||||
@@ -22,41 +25,84 @@ def desired_gitea_username(user: dict) -> str:
|
||||
|
||||
def apply_gitea_credentials(user: dict, new_username: str = "", new_password: str = "") -> dict:
|
||||
"""Admin nastaví Gitea uživatelské jméno a/nebo heslo přes admin API.
|
||||
Vrací {"status": "synced"/"error", "applied": [...], "gitea_username", "gitea_user_id"}.
|
||||
|
||||
Heslo a přejmenování se aplikují NEZÁVISLE – když jedno selže, druhé se i tak provede a do
|
||||
výsledku se vrátí přesný důvod selhání (chybí endpoint na staré Gitea, jméno obsazené apod.).
|
||||
Vrací: {"status": synced|partial|error, "applied": [...], "errors": [...],
|
||||
"gitea_username", "gitea_user_id", "renamed": bool}.
|
||||
Heslo se nikam neukládá – jen se předá do Gitea."""
|
||||
new_username = (new_username or "").strip()
|
||||
new_password = new_password or ""
|
||||
|
||||
if not new_username and not new_password:
|
||||
return {"status": "error", "error": "Nebyla zadána žádná změna."}
|
||||
return {"status": "error", "error": "Nebyla zadána žádná změna.", "errors": [], "applied": [], "renamed": False}
|
||||
if not _requires_gitea_account(user):
|
||||
return {
|
||||
"status": "error",
|
||||
"error": "Uživatel nemá mít Gitea účet (potřebuje roli admin/developer a aktivní účet).",
|
||||
"errors": [],
|
||||
"applied": [],
|
||||
"renamed": False,
|
||||
}
|
||||
|
||||
user_id = int(user["id"])
|
||||
try:
|
||||
current_username = desired_gitea_username(user)
|
||||
gitea_user = _ensure_gitea_user(user, current_username)
|
||||
current_login = _gitea_login(gitea_user) or current_username
|
||||
except Exception as exc:
|
||||
message = f"Gitea účet se nepodařilo načíst: {str(exc)[:400]}"
|
||||
logger.error("apply_gitea_credentials: %s (user id=%s)", message, user_id)
|
||||
return {
|
||||
"status": "error",
|
||||
"error": message,
|
||||
"errors": [],
|
||||
"applied": [],
|
||||
"renamed": False,
|
||||
}
|
||||
|
||||
current_login = _gitea_login(gitea_user) or current_username
|
||||
final_username = current_login
|
||||
applied: list[str] = []
|
||||
if new_username and new_username != current_login:
|
||||
_rename_gitea_user(current_login, new_username)
|
||||
current_login = new_username
|
||||
applied.append("username")
|
||||
errors: list[str] = []
|
||||
|
||||
# 1) Heslo – nezávisle na přejmenování (vždy se zkusí, i kdyby rename neexistoval).
|
||||
if new_password:
|
||||
try:
|
||||
_set_gitea_password(current_login, user, new_password, gitea_user=gitea_user)
|
||||
applied.append("password")
|
||||
applied.append("heslo")
|
||||
logger.info("Gitea heslo nastaveno pro %s (user id=%s)", current_login, user_id)
|
||||
except Exception as exc:
|
||||
detail = str(exc)[:300]
|
||||
errors.append(f"heslo: {detail}")
|
||||
logger.error("Gitea nastavení hesla selhalo pro %s (user id=%s): %s", current_login, user_id, detail)
|
||||
|
||||
# 2) Přejmenování (Gitea >= 1.21). Na starší Gitea vrátí HTTP 404 → zobrazí se srozumitelně.
|
||||
if new_username and new_username != current_login:
|
||||
try:
|
||||
_rename_gitea_user(current_login, new_username)
|
||||
final_username = new_username
|
||||
applied.append("jméno")
|
||||
logger.info("Gitea účet přejmenován %s -> %s (user id=%s)", current_login, new_username, user_id)
|
||||
except Exception as exc:
|
||||
detail = str(exc)[:300]
|
||||
errors.append(f"jméno: {detail}")
|
||||
logger.error("Gitea přejmenování %s -> %s selhalo (user id=%s): %s", current_login, new_username, user_id, detail)
|
||||
|
||||
if applied and not errors:
|
||||
status = "synced"
|
||||
elif applied:
|
||||
status = "partial"
|
||||
else:
|
||||
status = "error"
|
||||
|
||||
return {
|
||||
"status": "synced",
|
||||
"status": status,
|
||||
"applied": applied,
|
||||
"gitea_username": current_login,
|
||||
"errors": errors,
|
||||
"gitea_username": final_username,
|
||||
"gitea_user_id": int(gitea_user["id"]),
|
||||
"renamed": "jméno" in applied,
|
||||
}
|
||||
except Exception as exc:
|
||||
return {"status": "error", "error": str(exc)[:500]}
|
||||
|
||||
|
||||
def _rename_gitea_user(current_username: str, new_username: str) -> dict:
|
||||
@@ -121,6 +167,7 @@ def sync_gitea_user(user: dict) -> dict:
|
||||
return {"status": "not_required", "action": "none"}
|
||||
except Exception as exc:
|
||||
error = str(exc)[:500]
|
||||
logger.error("Gitea sync selhal pro uživatele id=%s (%s): %s", user_id, username, error)
|
||||
update_gitea_sync_state(user_id, "error", error=error)
|
||||
return {"status": "error", "error": error}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
_CONFIGURED = False
|
||||
|
||||
|
||||
def configure_logging() -> None:
|
||||
"""Jednotné logování portálu do stdout (zachytí uvicorn / docker logs).
|
||||
|
||||
Pravidlo projektu: žádné tiché selhání – chyby a neočekávané stavy patří do logu.
|
||||
Úroveň lze řídit přes PORTAL_LOG_LEVEL (výchozí INFO)."""
|
||||
global _CONFIGURED
|
||||
if _CONFIGURED:
|
||||
return
|
||||
|
||||
level_name = (os.environ.get("PORTAL_LOG_LEVEL") or "INFO").upper()
|
||||
level = getattr(logging, level_name, logging.INFO)
|
||||
|
||||
root = logging.getLogger()
|
||||
if not root.handlers:
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s"))
|
||||
root.addHandler(handler)
|
||||
root.setLevel(level)
|
||||
|
||||
_CONFIGURED = True
|
||||
|
||||
|
||||
def get_logger(name: str) -> logging.Logger:
|
||||
configure_logging()
|
||||
return logging.getLogger(name)
|
||||
@@ -5,10 +5,12 @@ from fastapi.staticfiles import StaticFiles
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
|
||||
from .config import read_env_bool, read_env_value
|
||||
from .logging_config import configure_logging
|
||||
from .routes import alerting, apps, audit, auth, backups, catalog, deployments, developers, environment, health, incidents, jobs, logs, operations, runtime, scheduled_scripts, users, workers
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
configure_logging()
|
||||
app = FastAPI(title="CSBot Services Portal")
|
||||
session_secret = read_env_value("PORTAL_SESSION_SECRET", "") or "dev-only-appfactory-session-secret"
|
||||
|
||||
|
||||
@@ -104,6 +104,14 @@ def developers_page(user=Depends(require_user)):
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2><i class="fa-solid fa-server" aria-hidden="true"></i> Replikace serveru</h2>
|
||||
<p class="muted">
|
||||
Checklist a kopírovatelné příkazy pro čistou instalaci nebo obnovu AppFactory na nový Linux server.
|
||||
</p>
|
||||
<p><a class="btn" href="/portal/developers/server-replication"><i class="fa-solid fa-list-check" aria-hidden="true"></i> Replikace serveru / požadavky na nový server</a></p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2><i class="fa-solid fa-cloud-arrow-up" aria-hidden="true"></i> Jak publikovat</h2>
|
||||
<p class="muted">Postup od založení nové služby po nasazení změn.</p>
|
||||
@@ -181,3 +189,370 @@ def developers_page(user=Depends(require_user)):
|
||||
""",
|
||||
user=user,
|
||||
)
|
||||
|
||||
|
||||
# --- Replikace serveru / požadavky na nový server -------------------------------------------------
|
||||
# Čistě dokumentační stránka (checklist + kopírovatelné příkazy). Žádná Docker logika v portálu.
|
||||
# Bash/text bloky jsou běžné (ne f-string) řetězce, aby složené závorky ({}, {{.Names}}) zůstaly
|
||||
# literálně a copy tlačítka kopírovala přesný obsah.
|
||||
|
||||
def _code_block(code: str) -> str:
|
||||
"""Kopírovatelný blok příkazu (tmavý <pre> + tlačítko Kopírovat)."""
|
||||
escaped = html.escape(code.strip("\n"))
|
||||
return (
|
||||
'<div class="code-block">'
|
||||
'<button type="button" class="copy-button code-copy" onclick="return copyText(this)">'
|
||||
'<i class="fa-solid fa-copy" aria-hidden="true"></i> Kopírovat</button>'
|
||||
f'<pre>{escaped}</pre>'
|
||||
'</div>'
|
||||
)
|
||||
|
||||
|
||||
def _text_block(text: str) -> str:
|
||||
"""Blok prostého výpisu (např. chybová hláška) bez kopírování."""
|
||||
return f'<pre class="text-block">{html.escape(text.strip(chr(10)))}</pre>'
|
||||
|
||||
|
||||
APT_INSTALL = r"""apt update
|
||||
|
||||
apt install -y \
|
||||
git \
|
||||
curl \
|
||||
wget \
|
||||
jq \
|
||||
sqlite3 \
|
||||
unzip \
|
||||
zip \
|
||||
tar \
|
||||
gzip \
|
||||
ca-certificates \
|
||||
gnupg \
|
||||
lsb-release \
|
||||
bash \
|
||||
sudo \
|
||||
openssh-client \
|
||||
openssh-server \
|
||||
nano \
|
||||
vim \
|
||||
rsync \
|
||||
cron \
|
||||
procps \
|
||||
net-tools \
|
||||
dnsutils \
|
||||
iputils-ping \
|
||||
software-properties-common"""
|
||||
|
||||
DOCKER_INSTALL = r"""curl -fsSL https://get.docker.com | sh
|
||||
|
||||
apt install -y docker-compose-plugin"""
|
||||
|
||||
DOCKER_VERIFY = r"""docker --version
|
||||
docker compose version"""
|
||||
|
||||
DOCKER_GROUP = r"""groupadd docker || true
|
||||
usermod -aG docker root"""
|
||||
|
||||
DOCKER_GROUP_ADMIN = r"""usermod -aG docker jiri"""
|
||||
|
||||
UTILITIES = """git
|
||||
curl
|
||||
wget
|
||||
jq
|
||||
sqlite3
|
||||
tar
|
||||
zip
|
||||
unzip
|
||||
bash
|
||||
cron
|
||||
rsync
|
||||
ssh
|
||||
docker
|
||||
docker compose"""
|
||||
|
||||
DNS_LIST = """services.csbot.cz
|
||||
git.csbot.cz
|
||||
registry.csbot.cz"""
|
||||
|
||||
DNS_CHECK = r"""nslookup services.csbot.cz
|
||||
nslookup git.csbot.cz
|
||||
nslookup registry.csbot.cz"""
|
||||
|
||||
FIREWALL = """22 SSH
|
||||
80 HTTP
|
||||
443 HTTPS"""
|
||||
|
||||
STRUCTURE = """/opt/appfactory
|
||||
/opt/appfactory/config
|
||||
/opt/appfactory/data
|
||||
/opt/appfactory/workspace
|
||||
/opt/appfactory/backups
|
||||
/opt/appfactory/logs
|
||||
/opt/appfactory/tools"""
|
||||
|
||||
GIT_CONFIG_HOST = r"""git config --system --add safe.directory "*" """
|
||||
|
||||
SERVER_VERIFY = r"""git --version
|
||||
docker --version
|
||||
docker compose version
|
||||
jq --version
|
||||
sqlite3 --version
|
||||
curl --version"""
|
||||
|
||||
FIX_PERMISSIONS = r"""cat > /opt/appfactory/tools/fix-appfactory-server-permissions.sh <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
APPFACTORY_ROOT="/opt/appfactory"
|
||||
|
||||
echo "[1/9] Create appfactory group..."
|
||||
getent group appfactory >/dev/null || groupadd appfactory
|
||||
|
||||
echo "[2/9] Add known users to appfactory group..."
|
||||
for u in root jiri git caddy www-data; do
|
||||
if id "$u" >/dev/null 2>&1; then
|
||||
usermod -aG appfactory "$u" || true
|
||||
fi
|
||||
done
|
||||
|
||||
echo "[3/9] Ensure base directories exist..."
|
||||
mkdir -p "$APPFACTORY_ROOT/config"
|
||||
mkdir -p "$APPFACTORY_ROOT/data"
|
||||
mkdir -p "$APPFACTORY_ROOT/workspace"
|
||||
mkdir -p "$APPFACTORY_ROOT/backups"
|
||||
mkdir -p "$APPFACTORY_ROOT/logs"
|
||||
mkdir -p "$APPFACTORY_ROOT/tools"
|
||||
|
||||
echo "[4/9] Set ownership under /opt/appfactory..."
|
||||
chown -R root:appfactory "$APPFACTORY_ROOT"
|
||||
|
||||
echo "[5/9] Set directory permissions..."
|
||||
find "$APPFACTORY_ROOT" -type d -exec chmod 2775 {} \;
|
||||
|
||||
echo "[6/9] Set file permissions..."
|
||||
find "$APPFACTORY_ROOT" -type f -exec chmod 664 {} \;
|
||||
|
||||
echo "[7/9] Make shell scripts executable..."
|
||||
find "$APPFACTORY_ROOT" -type f -name "*.sh" -exec chmod 775 {} \;
|
||||
|
||||
echo "[8/9] Fix Git safe.directory on host..."
|
||||
git config --system --add safe.directory "*" || true
|
||||
|
||||
echo "[9/9] Fix Gitea sessions directory if present..."
|
||||
if [ -d "$APPFACTORY_ROOT/data/gitea/gitea" ]; then
|
||||
rm -rf "$APPFACTORY_ROOT/data/gitea/gitea/sessions"
|
||||
mkdir -p "$APPFACTORY_ROOT/data/gitea/gitea/sessions"
|
||||
chmod 777 "$APPFACTORY_ROOT/data/gitea/gitea/sessions"
|
||||
fi
|
||||
|
||||
echo "Done."
|
||||
EOF
|
||||
|
||||
bash /opt/appfactory/tools/fix-appfactory-server-permissions.sh"""
|
||||
|
||||
CONTAINER_GIT = r"""for c in appfactory-portal appfactory-worker appfactory-gitea; do
|
||||
if docker ps -a --format '{{.Names}}' | grep -qx "$c"; then
|
||||
docker exec "$c" sh -lc 'git config --system --add safe.directory "*" || git config --global --add safe.directory "*" || true' || true
|
||||
fi
|
||||
done"""
|
||||
|
||||
GITEA_PANIC = """PANIC: session(start): chtimes /data/gitea/sessions/... operation not permitted"""
|
||||
|
||||
GITEA_SESSIONS_FIX = r"""docker stop appfactory-gitea
|
||||
|
||||
rm -rf /opt/appfactory/data/gitea/gitea/sessions
|
||||
mkdir -p /opt/appfactory/data/gitea/gitea/sessions
|
||||
chmod 777 /opt/appfactory/data/gitea/gitea/sessions
|
||||
|
||||
docker start appfactory-gitea"""
|
||||
|
||||
GITEA_HOOKS_BROKEN = """Git hooks of this repository seem to be broken"""
|
||||
|
||||
GITEA_HOOKS_FIX = r"""docker exec -u git -it appfactory-gitea sh -lc '
|
||||
gitea admin regenerate hooks --config /data/gitea/conf/app.ini
|
||||
'
|
||||
docker restart appfactory-gitea"""
|
||||
|
||||
COMPOSE_ENV_ERRORS = """APPFACTORY_UID variable is not set
|
||||
APPFACTORY_GID variable is not set
|
||||
APPFACTORY_DOCKER_GID variable is not set
|
||||
Unable to find group"""
|
||||
|
||||
COMPOSE_ENV_EXPORT = r"""export APPFACTORY_UID="$(id -u)"
|
||||
export APPFACTORY_GID="$(id -g)"
|
||||
export APPFACTORY_DOCKER_GID="$(getent group docker | cut -d: -f3)" """
|
||||
|
||||
MOUNT_YAML = """volumes:
|
||||
- /opt/appfactory/config:/opt/appfactory/config:rw"""
|
||||
|
||||
READONLY_PATH = """/opt/appfactory/config/appfactory.env"""
|
||||
|
||||
READONLY_ERROR = """Read-only file system"""
|
||||
|
||||
|
||||
@router.get("/developers/server-replication", response_class=HTMLResponse)
|
||||
def server_replication_page(user=Depends(require_user)):
|
||||
body = "".join([
|
||||
"""
|
||||
<div class="card">
|
||||
<h2><i class="fa-solid fa-server" aria-hidden="true"></i> Replikace serveru / požadavky na nový server</h2>
|
||||
<p class="muted">
|
||||
Checklist pro čistou obnovu nebo instalaci AppFactory na nový Linux server. Jde o dokumentaci pro
|
||||
administrátora – příkazy se spouštějí na serveru, ne z portálu.
|
||||
</p>
|
||||
<p><a href="/portal/developers">← Zpět na Pro vývojáře</a></p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2><i class="fa-solid fa-clipboard-check" aria-hidden="true"></i> 1. Požadavky na nový server</h2>
|
||||
<p class="muted">Nový server musí mít:</p>
|
||||
<ul class="req-list">
|
||||
<li>Linux server, ideálně Ubuntu/Debian</li>
|
||||
<li>Docker</li>
|
||||
<li>Docker Compose plugin</li>
|
||||
<li>Git</li>
|
||||
<li>curl</li>
|
||||
<li>bash</li>
|
||||
<li>sqlite3</li>
|
||||
<li>openssh-client</li>
|
||||
<li>přístup k repozitářům v Gitea</li>
|
||||
<li>funkční DNS pro:
|
||||
<ul>
|
||||
<li>services.csbot.cz</li>
|
||||
<li>git.csbot.cz</li>
|
||||
<li>registry.csbot.cz</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>adresář <code>/opt/appfactory</code></li>
|
||||
<li>Docker síť používanou AppFactory</li>
|
||||
<li>volumes/adresáře pro:
|
||||
<ul>
|
||||
<li>/opt/appfactory/config</li>
|
||||
<li>/opt/appfactory/data</li>
|
||||
<li>/opt/appfactory/workspace</li>
|
||||
<li>/opt/appfactory/backups</li>
|
||||
<li>/opt/appfactory/logs</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2><i class="fa-solid fa-download" aria-hidden="true"></i> 2. Základní instalace nového serveru</h2>
|
||||
<p class="muted">Před spuštěním bootstrapu musí být na serveru nainstalováno:</p>
|
||||
""",
|
||||
_code_block(APT_INSTALL),
|
||||
"""
|
||||
<h3>Docker a Docker Compose plugin</h3>
|
||||
""",
|
||||
_code_block(DOCKER_INSTALL),
|
||||
"<p class=\"muted\">Ověření:</p>",
|
||||
_code_block(DOCKER_VERIFY),
|
||||
"""
|
||||
<h3>Docker skupina</h3>
|
||||
""",
|
||||
_code_block(DOCKER_GROUP),
|
||||
"<p class=\"muted\">Případně i pro administrátora:</p>",
|
||||
_code_block(DOCKER_GROUP_ADMIN),
|
||||
"""
|
||||
<h3>Povinné utility používané AppFactory</h3>
|
||||
<p class="muted">Používají je skripty AppFactory:</p>
|
||||
""",
|
||||
_text_block(UTILITIES),
|
||||
"""
|
||||
<h3>DNS požadavky</h3>
|
||||
<p class="muted">Musí fungovat:</p>
|
||||
""",
|
||||
_text_block(DNS_LIST),
|
||||
"<p class=\"muted\">Kontrola:</p>",
|
||||
_code_block(DNS_CHECK),
|
||||
"""
|
||||
<h3>Firewall</h3>
|
||||
<p class="muted">Musí být otevřené:</p>
|
||||
""",
|
||||
_text_block(FIREWALL),
|
||||
"""
|
||||
<h3>Struktura AppFactory</h3>
|
||||
<p class="muted">Musí existovat:</p>
|
||||
""",
|
||||
_text_block(STRUCTURE),
|
||||
"""
|
||||
<h3>Git konfigurace (host)</h3>
|
||||
""",
|
||||
_code_block(GIT_CONFIG_HOST),
|
||||
"""
|
||||
<h3>Ověření serveru</h3>
|
||||
<p class="muted">Před bootstrapem musí projít:</p>
|
||||
""",
|
||||
_code_block(SERVER_VERIFY),
|
||||
"<p class=\"muted\">Pokud některý příkaz selže, server není připraven pro AppFactory.</p>",
|
||||
"</div>",
|
||||
"""
|
||||
<div class="card">
|
||||
<h2><i class="fa-solid fa-lock" aria-hidden="true"></i> 3. Povinné nastavení práv</h2>
|
||||
<p class="muted">
|
||||
V AppFactory nechceme řešit opakované chyby typu:
|
||||
</p>
|
||||
<ul class="req-list">
|
||||
<li>Permission denied</li>
|
||||
<li>Read-only file system</li>
|
||||
<li>detected dubious ownership</li>
|
||||
<li>operation not permitted</li>
|
||||
<li>Git safe.directory problém</li>
|
||||
</ul>
|
||||
<p class="muted">
|
||||
Proto má být pro AppFactory část serveru sjednocené právo zápisu pro relevantní procesy.
|
||||
</p>
|
||||
<p class="alert alert-danger">
|
||||
<strong>Důležité:</strong> Neaplikovat <code>chmod 777</code> na celý Linux server.
|
||||
Aplikovat pouze na <code>/opt/appfactory</code> a na konkrétní problematické runtime adresáře.
|
||||
</p>
|
||||
|
||||
<h3>Serverový příkaz (oprava práv)</h3>
|
||||
""",
|
||||
_code_block(FIX_PERMISSIONS),
|
||||
"""
|
||||
<h3>Oprava Git safe.directory v kontejnerech</h3>
|
||||
""",
|
||||
_code_block(CONTAINER_GIT),
|
||||
"</div>",
|
||||
"""
|
||||
<div class="card">
|
||||
<h2><i class="fa-solid fa-code-branch" aria-hidden="true"></i> 4. Gitea – sessions</h2>
|
||||
<p class="muted">Pokud Gitea spadne na chybu:</p>
|
||||
""",
|
||||
_text_block(GITEA_PANIC),
|
||||
"<p class=\"muted\">Použij:</p>",
|
||||
_code_block(GITEA_SESSIONS_FIX),
|
||||
"</div>",
|
||||
"""
|
||||
<div class="card">
|
||||
<h2><i class="fa-solid fa-link-slash" aria-hidden="true"></i> 5. Gitea – hooks</h2>
|
||||
<p class="muted">Pokud Gitea ukazuje:</p>
|
||||
""",
|
||||
_text_block(GITEA_HOOKS_BROKEN),
|
||||
"<p class=\"muted\">Oprava:</p>",
|
||||
_code_block(GITEA_HOOKS_FIX),
|
||||
"</div>",
|
||||
"""
|
||||
<div class="card">
|
||||
<h2><i class="fa-solid fa-gears" aria-hidden="true"></i> 6. docker compose – env proměnné</h2>
|
||||
<p class="muted">Pokud docker compose hlásí:</p>
|
||||
""",
|
||||
_text_block(COMPOSE_ENV_ERRORS),
|
||||
"<p class=\"muted\">Použij:</p>",
|
||||
_code_block(COMPOSE_ENV_EXPORT),
|
||||
"</div>",
|
||||
"""
|
||||
<div class="card">
|
||||
<h2><i class="fa-solid fa-hard-drive" aria-hidden="true"></i> 7. Mounty</h2>
|
||||
<p class="muted">Portál musí mít RW přístup ke konfiguraci:</p>
|
||||
""",
|
||||
_code_block(MOUNT_YAML),
|
||||
"<p class=\"muted\">Nepoužívat read-only mount pro:</p>",
|
||||
_text_block(READONLY_PATH),
|
||||
"<p class=\"muted\">protože editor appfactory.env pak končí chybou:</p>",
|
||||
_text_block(READONLY_ERROR),
|
||||
"</div>",
|
||||
])
|
||||
|
||||
return page("Replikace serveru", body, user=user)
|
||||
|
||||
@@ -8,6 +8,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from app.auth import require_user
|
||||
from app.db.audit import log_audit_event
|
||||
from app.db.jobs import create_job
|
||||
from app.logging_config import get_logger
|
||||
from app.db.scheduled_scripts import (
|
||||
create_scheduled_script,
|
||||
delete_scheduled_script,
|
||||
@@ -143,11 +144,11 @@ def save_script_file(script_name: str, content: str) -> None:
|
||||
reason = exc.strerror or str(exc)
|
||||
raise HTTPException(status_code=500, detail=f"Soubor se nepodařilo uložit: {reason}")
|
||||
# Spustitelná práva nastavujeme best-effort – soubor už je uložený, takže selhání chmod
|
||||
# (jiný vlastník, FS bez podpory práv) nesmí hlásit chybu uložení.
|
||||
# (jiný vlastník, FS bez podpory práv) nesmí hlásit chybu uložení, ale nesmí být ani tiché.
|
||||
try:
|
||||
os.chmod(path, 0o755)
|
||||
except OSError:
|
||||
pass
|
||||
except OSError as exc:
|
||||
get_logger(__name__).warning("Nepodařilo se nastavit spustitelná práva na %s: %s", path, exc)
|
||||
|
||||
|
||||
def validate_schedule_type(value: str) -> str:
|
||||
|
||||
+21
-10
@@ -18,9 +18,11 @@ from app.db.users import (
|
||||
update_user_role,
|
||||
)
|
||||
from app.gitea_provisioning import apply_gitea_credentials, sync_gitea_user
|
||||
from app.logging_config import get_logger
|
||||
from app.templates.layout import page
|
||||
|
||||
router = APIRouter()
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# Gitea handle: začíná alfanumerickým znakem, dál povolen . _ -; max 40 znaků.
|
||||
GITEA_USERNAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$")
|
||||
@@ -383,14 +385,10 @@ def update_gitea_credentials_action(
|
||||
)
|
||||
|
||||
result = apply_gitea_credentials(target, new_username, new_password)
|
||||
if result.get("status") == "error":
|
||||
return RedirectResponse(
|
||||
url="/portal/admin/users?error=" + quote(f"Gitea: {result.get('error')}"),
|
||||
status_code=303,
|
||||
)
|
||||
|
||||
applied = result.get("applied") or []
|
||||
if "username" in applied:
|
||||
errors = result.get("errors") or []
|
||||
|
||||
if result.get("renamed"):
|
||||
set_gitea_username(user_id, result.get("gitea_username") or new_username)
|
||||
|
||||
log_audit_event(
|
||||
@@ -401,12 +399,25 @@ def update_gitea_credentials_action(
|
||||
metadata={
|
||||
"username": target.get("username"),
|
||||
"gitea_username": result.get("gitea_username"),
|
||||
"applied": applied, # heslo se nikdy nezaznamenává
|
||||
"applied": applied,
|
||||
"errors": errors, # heslo se nikdy nezaznamenává
|
||||
},
|
||||
)
|
||||
changed = ", ".join(applied) if applied else "beze změny"
|
||||
|
||||
status = result.get("status")
|
||||
if status == "error":
|
||||
detail = result.get("error") or "; ".join(errors) or "neznámá chyba"
|
||||
logger.error("Změna Gitea údajů selhala pro uživatele id=%s: %s", user_id, detail)
|
||||
return RedirectResponse(url="/portal/admin/users?error=" + quote(f"Gitea: {detail}"), status_code=303)
|
||||
|
||||
if errors:
|
||||
msg = f"Gitea částečně aktualizováno ({', '.join(applied)}). Nepovedlo se: {'; '.join(errors)}"
|
||||
logger.warning("Gitea údaje částečně pro uživatele id=%s: applied=%s errors=%s", user_id, applied, errors)
|
||||
return RedirectResponse(url="/portal/admin/users?error=" + quote(msg), status_code=303)
|
||||
|
||||
logger.info("Gitea údaje aktualizovány pro uživatele id=%s: %s", user_id, applied)
|
||||
return RedirectResponse(
|
||||
url="/portal/admin/users?message=" + quote(f"Gitea přihlašovací údaje aktualizovány ({changed})."),
|
||||
url="/portal/admin/users?message=" + quote(f"Gitea aktualizováno: {', '.join(applied) or 'beze změny'}."),
|
||||
status_code=303,
|
||||
)
|
||||
|
||||
|
||||
@@ -1021,6 +1021,49 @@ pre {
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
/* Dokumentační stránka: kopírovatelné bloky příkazů + výpisy. */
|
||||
.code-block {
|
||||
position: relative;
|
||||
margin: 10px 0 18px;
|
||||
}
|
||||
|
||||
.code-block .code-copy {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 1;
|
||||
width: auto;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.code-block pre {
|
||||
margin: 0;
|
||||
padding-top: 44px;
|
||||
}
|
||||
|
||||
.text-block {
|
||||
margin: 8px 0 16px;
|
||||
padding: 12px 14px;
|
||||
background: #f1f5f9;
|
||||
color: #334155;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.req-list {
|
||||
margin: 8px 0 0;
|
||||
padding-left: 22px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.req-list ul {
|
||||
margin: 4px 0 4px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
/* Kompaktní log v řádku tabulky (globální prohlížeč logů). */
|
||||
.log-inline {
|
||||
margin: 0;
|
||||
|
||||
Reference in New Issue
Block a user