alpinux.site.2026/admin/app.py
Cédrix 60eb8bc952 feat: interface admin portail.alpinux.org/admin/ — déclencher mkdocs build
Mini-app Flask (admin/) accessible à https://portail.alpinux.org/admin/ :
- Authentification AlpID OIDC, accès restreint au groupe « admins » Keycloak
- Bouton « Lancer mkdocs build » avec confirmation
- Exécution de deploy-wiki.sh en arrière-plan (thread), log capturé en direct
- Polling JS toutes les 2s pendant le build (status + log + historique)
- Affichage du journal en terminal sombre avec suivi automatique
- Historique des 20 derniers builds (date, déclencheur, résultat, durée)
- ProxyFix pour X-Forwarded-Proto / X-Script-Name (Apache reverse proxy)

Infrastructure :
- scripts/portail.alpinux.org.admin.conf : bloc ProxyPass pour ISPConfig
- scripts/alpinux-admin.service : systemd Gunicorn port 5002
- admin/.env.example, admin/requirements.txt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 12:34:18 +02:00

120 lines
3.9 KiB
Python

import os
from flask import Flask, redirect, url_for, session, request, jsonify, render_template, abort
from authlib.integrations.flask_client import OAuth
from werkzeug.middleware.proxy_fix import ProxyFix
import builds
app = Flask(__name__)
app.secret_key = os.environ["SECRET_KEY"]
# Gère X-Forwarded-Proto et X-Script-Name envoyés par Apache
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_prefix=1)
# ── OIDC AlpID ────────────────────────────────────────────────────
oauth = OAuth(app)
oauth.register(
name="alpid",
server_metadata_url=os.environ["ALPID_DISCOVERY_URL"],
client_id=os.environ["ALPID_CLIENT_ID"],
client_secret=os.environ["ALPID_CLIENT_SECRET"],
client_kwargs={"scope": "openid profile email groups"},
)
ADMIN_GROUPS = set(os.environ.get("ADMIN_GROUPS", "admins").split(","))
def _user():
return session.get("user")
def _require_admin():
user = _user()
if not user:
session["next_url"] = request.url
return redirect(url_for("login"))
if not user.get("is_admin"):
abort(403)
return None
# ── Auth ──────────────────────────────────────────────────────────
@app.route("/auth/login")
def login():
return oauth.alpid.authorize_redirect(url_for("callback", _external=True))
@app.route("/auth/callback")
def callback():
token = oauth.alpid.authorize_access_token()
info = token.get("userinfo") or oauth.alpid.userinfo(token=token)
# Keycloak expose les groupes dans le claim "groups" (à activer dans le mapper)
groups = set(info.get("groups", []))
is_admin = bool(groups & ADMIN_GROUPS)
session["user"] = {
"sub": info["sub"],
"name": info.get("name") or info.get("preferred_username", "Admin"),
"email": info.get("email", ""),
"is_admin": is_admin,
}
return redirect(session.pop("next_url", url_for("index")))
@app.route("/auth/logout")
def logout():
session.clear()
return redirect(url_for("index"))
# ── Pages ─────────────────────────────────────────────────────────
@app.route("/")
def index():
redir = _require_admin()
if redir:
return redir
state = builds.get_state()
log = builds.get_log()
return render_template("index.html", user=_user(), state=state, log=log)
# ── API JSON ──────────────────────────────────────────────────────
@app.route("/api/build", methods=["POST"])
def api_build():
redir = _require_admin()
if redir:
return jsonify({"error": "non authentifié"}), 401
user = _user()
ok = builds.start_build(triggered_by=user["name"])
if ok:
return jsonify({"started": True})
return jsonify({"started": False, "reason": "Un build est déjà en cours"}), 409
@app.route("/api/status")
def api_status():
redir = _require_admin()
if redir:
return jsonify({"error": "non authentifié"}), 401
state = builds.get_state()
return jsonify({
"running": state.get("running", False),
"last_success": state.get("last_success"),
"last_duration": state.get("last_duration_s"),
"started_at": state.get("started_at"),
"triggered_by": state.get("triggered_by"),
"history": state.get("history", [])[:5],
})
@app.route("/api/log")
def api_log():
redir = _require_admin()
if redir:
return jsonify({"error": "non authentifié"}), 401
return jsonify({"log": builds.get_log()})
if __name__ == "__main__":
app.run(debug=False, port=5002)