Změny:
users migrace má nové sloupce gitea_user_id, gitea_sync_status, gitea_sync_error, gitea_synced_at.
Přidal jsem gitea_provisioning.py, který přes Gitea API vytváří, aktivuje, deaktivuje a synchronizuje účty.
Role viewer Gitea účet nemá; developer a admin účet mají.
Změny role a enable/disable v administraci spouští sync do Gitea.
Admin UI v routes/users.py zobrazuje stav Gitea synchronizace a má ruční Sync Gitea.
Google login po přihlášení dorovnává Gitea stav podle portálového uživatele.
Gitea OAuth login je v portálu vypnutý; endpointy zůstaly, ale vrací hlášku, že se má použít Google.
Lokální login se zobrazuje/povolí jen při explicitním AUTH_MODE=local.
Gitea API token se čte z GITEA_ADMIN_TOKEN, případně fallback GITEA_API_TOKEN nebo GITEA_TOKEN. Podřízené Gitea username je deterministicky portal-{user_id}, portálová tabulka zůstává zdroj pravdy.
Ověření: python -m py_compile prošel pro všechny upravené Python soubory.
This commit is contained in:
+10
-14
@@ -9,7 +9,7 @@ from urllib.request import urlopen
|
||||
from fastapi import APIRouter, Form, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
|
||||
from app.auth import authenticate_user, current_user, find_or_create_google_user, find_or_create_oauth_user, mark_last_login
|
||||
from app.auth import authenticate_user, current_user, find_or_create_google_user, mark_last_login
|
||||
from app.config import (
|
||||
get_auth_mode,
|
||||
get_gitea_public_url,
|
||||
@@ -21,6 +21,7 @@ from app.config import (
|
||||
read_env_value,
|
||||
)
|
||||
from app.db.audit import log_audit_event
|
||||
from app.gitea_provisioning import sync_gitea_user
|
||||
from app.templates.layout import page
|
||||
|
||||
router = APIRouter()
|
||||
@@ -39,7 +40,7 @@ def login_form(request: Request):
|
||||
|
||||
@router.post("/login", response_class=HTMLResponse)
|
||||
def login(request: Request, username: str = Form(...), password: str = Form(...)):
|
||||
if get_auth_mode() == "google":
|
||||
if get_auth_mode() != "local":
|
||||
return _render_login("Local login is disabled. Use Google sign in.")
|
||||
|
||||
user = authenticate_user(username, password)
|
||||
@@ -64,8 +65,7 @@ def login(request: Request, username: str = Form(...), password: str = Form(...)
|
||||
def gitea_login(request: Request):
|
||||
if current_user(request):
|
||||
return RedirectResponse(url="/portal/operations", status_code=303)
|
||||
if get_auth_mode() != "mixed":
|
||||
return _render_login("Gitea login is not enabled in this auth mode.")
|
||||
return _render_login("Gitea login is disabled. Use Google sign in.")
|
||||
|
||||
gitea_url = get_gitea_public_url()
|
||||
client_id = read_env_value("GITEA_OAUTH_CLIENT_ID", "")
|
||||
@@ -98,8 +98,7 @@ def gitea_login(request: Request):
|
||||
|
||||
@router.get("/auth/gitea/callback", response_class=HTMLResponse)
|
||||
def gitea_callback(request: Request, code: str = "", state: str = "", error: str = ""):
|
||||
if get_auth_mode() != "mixed":
|
||||
return _render_login("Gitea login is not enabled in this auth mode.")
|
||||
return _render_login("Gitea login is disabled. Use Google sign in.")
|
||||
|
||||
expected_state = request.session.pop("gitea_oauth_state", None)
|
||||
if error:
|
||||
@@ -228,6 +227,8 @@ def google_callback(request: Request, code: str = "", state: str = "", error: st
|
||||
_log_google_failure("disabled_user", username=username)
|
||||
return _render_login("Uživatel je v portálu vypnutý.")
|
||||
|
||||
sync_result = sync_gitea_user(user)
|
||||
|
||||
request.session.clear()
|
||||
request.session["user_id"] = user["id"]
|
||||
mark_last_login(int(user["id"]))
|
||||
@@ -242,6 +243,7 @@ def google_callback(request: Request, code: str = "", state: str = "", error: st
|
||||
"email": user.get("email"),
|
||||
"role": user.get("role"),
|
||||
"provider": "google",
|
||||
"gitea_sync_status": sync_result.get("status"),
|
||||
},
|
||||
)
|
||||
log_audit_event(
|
||||
@@ -249,7 +251,7 @@ def google_callback(request: Request, code: str = "", state: str = "", error: st
|
||||
action="auth.google.login.success",
|
||||
target_type="user",
|
||||
target_id=user.get("id"),
|
||||
metadata={"username": user.get("username"), "provider": "google"},
|
||||
metadata={"username": user.get("username"), "provider": "google", "gitea_sync_status": sync_result.get("status")},
|
||||
)
|
||||
return RedirectResponse(url="/portal", status_code=303)
|
||||
except Exception:
|
||||
@@ -397,12 +399,6 @@ def _render_login(error: str | None = None) -> str:
|
||||
error_html = f'<p class="alert alert-danger">{html.escape(error)}</p>'
|
||||
auth_mode = get_auth_mode()
|
||||
gitea_login_html = ""
|
||||
if auth_mode == "mixed" and is_gitea_oauth_button_enabled():
|
||||
gitea_login_html = """
|
||||
<p>
|
||||
<a class="btn btn-secondary" href="/portal/auth/gitea/login">Sign in with Gitea</a>
|
||||
</p>
|
||||
"""
|
||||
google_login_html = ""
|
||||
if auth_mode in {"google", "mixed"} and is_google_oauth_button_enabled():
|
||||
google_login_html = """
|
||||
@@ -414,7 +410,7 @@ def _render_login(error: str | None = None) -> str:
|
||||
google_login_html = '<p class="alert alert-danger">Google login is not fully configured.</p>'
|
||||
|
||||
local_login_html = ""
|
||||
if auth_mode in {"mixed", "local"}:
|
||||
if auth_mode == "local":
|
||||
local_login_html = """
|
||||
<form method="post" action="/portal/login">
|
||||
<h3>Local login</h3>
|
||||
|
||||
+77
-6
@@ -7,6 +7,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from app.auth import require_user
|
||||
from app.db.audit import log_audit_event
|
||||
from app.db.users import ROLES, count_enabled_admins, get_user, list_users, set_user_enabled, update_user_role
|
||||
from app.gitea_provisioning import sync_gitea_user
|
||||
from app.templates.layout import page
|
||||
|
||||
router = APIRouter()
|
||||
@@ -31,6 +32,17 @@ def render_enabled_pill(enabled) -> str:
|
||||
return '<span class="pill pill-muted">disabled</span>'
|
||||
|
||||
|
||||
def render_sync_pill(status: str) -> str:
|
||||
status = (status or "not_required").lower()
|
||||
if status == "synced":
|
||||
return '<span class="pill pill-success">synced</span>'
|
||||
if status == "error":
|
||||
return '<span class="pill pill-danger">error</span>'
|
||||
if status == "syncing":
|
||||
return '<span class="pill pill-muted">syncing</span>'
|
||||
return '<span class="pill pill-muted">not required</span>'
|
||||
|
||||
|
||||
def render_role_overview() -> str:
|
||||
role_rows = (
|
||||
("admin", "Full administration, user management, role changes, disabling/enabling users, and admin-only operations."),
|
||||
@@ -83,6 +95,15 @@ def users_page(request: Request, message: str = "", error: str = "", user=Depend
|
||||
created_at = html.escape(managed_user.get("created_at", "") or "")
|
||||
last_login_at = html.escape(managed_user.get("last_login_at", "") or "")
|
||||
enabled = bool(managed_user.get("is_enabled", True))
|
||||
gitea_user_id = managed_user.get("gitea_user_id")
|
||||
gitea_sync_status = managed_user.get("gitea_sync_status", "not_required") or "not_required"
|
||||
gitea_sync_error = html.escape(managed_user.get("gitea_sync_error", "") or "")
|
||||
gitea_synced_at = html.escape(managed_user.get("gitea_synced_at", "") or "")
|
||||
gitea_details = f"ID {html.escape(str(gitea_user_id))}" if gitea_user_id else "No account"
|
||||
if gitea_synced_at:
|
||||
gitea_details += f"<br><small>{gitea_synced_at}</small>"
|
||||
if gitea_sync_error:
|
||||
gitea_details += f'<br><small class="text-danger">{gitea_sync_error}</small>'
|
||||
toggle_label = "Disable" if enabled else "Enable"
|
||||
toggle_action = "disable" if enabled else "enable"
|
||||
|
||||
@@ -101,18 +122,22 @@ def users_page(request: Request, message: str = "", error: str = "", user=Depend
|
||||
</form>
|
||||
</td>
|
||||
<td>{render_enabled_pill(enabled)}</td>
|
||||
<td>{render_sync_pill(gitea_sync_status)}<br><small>{gitea_details}</small></td>
|
||||
<td>{created_at}</td>
|
||||
<td>{last_login_at}</td>
|
||||
<td class="actions-cell">
|
||||
<form method="post" action="/portal/admin/users/{user_id_html}/{toggle_action}" class="inline-form">
|
||||
<button type="submit" class="btn-secondary">{toggle_label}</button>
|
||||
</form>
|
||||
<form method="post" action="/portal/admin/users/{user_id_html}/sync-gitea" class="inline-form">
|
||||
<button type="submit" class="btn-secondary">Sync Gitea</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
if not rows:
|
||||
rows = '<tr><td colspan="11">No users found.</td></tr>'
|
||||
rows = '<tr><td colspan="12">No users found.</td></tr>'
|
||||
|
||||
return page(
|
||||
"Users",
|
||||
@@ -135,6 +160,7 @@ def users_page(request: Request, message: str = "", error: str = "", user=Depend
|
||||
<th>Provider</th>
|
||||
<th>Role</th>
|
||||
<th>Status</th>
|
||||
<th>Gitea sync</th>
|
||||
<th>Created</th>
|
||||
<th>Last login</th>
|
||||
<th>Actions</th>
|
||||
@@ -165,6 +191,8 @@ def update_role_action(user_id: int, role: str = Form(...), user=Depends(require
|
||||
)
|
||||
|
||||
update_user_role(user_id, role)
|
||||
target = get_user(user_id) or target
|
||||
sync_result = sync_gitea_user(target)
|
||||
if old_role != role:
|
||||
log_audit_event(
|
||||
user,
|
||||
@@ -175,10 +203,16 @@ def update_role_action(user_id: int, role: str = Form(...), user=Depends(require
|
||||
"username": target.get("username"),
|
||||
"old_role": old_role,
|
||||
"new_role": role,
|
||||
"gitea_sync_status": sync_result.get("status"),
|
||||
},
|
||||
)
|
||||
|
||||
return RedirectResponse(url="/portal/admin/users?message=" + quote("User role updated."), status_code=303)
|
||||
if sync_result.get("status") == "error":
|
||||
return RedirectResponse(
|
||||
url="/portal/admin/users?error=" + quote(f"User role updated, but Gitea sync failed: {sync_result.get('error')}"),
|
||||
status_code=303,
|
||||
)
|
||||
return RedirectResponse(url="/portal/admin/users?message=" + quote("User role updated and Gitea synchronized."), status_code=303)
|
||||
|
||||
|
||||
@router.post("/admin/users/{user_id}/disable")
|
||||
@@ -194,14 +228,21 @@ def disable_user_action(user_id: int, user=Depends(require_user)):
|
||||
)
|
||||
|
||||
set_user_enabled(user_id, False)
|
||||
target = get_user(user_id) or target
|
||||
sync_result = sync_gitea_user(target)
|
||||
log_audit_event(
|
||||
user,
|
||||
action="user.disabled",
|
||||
target_type="user",
|
||||
target_id=user_id,
|
||||
metadata={"username": target.get("username")},
|
||||
metadata={"username": target.get("username"), "gitea_sync_status": sync_result.get("status")},
|
||||
)
|
||||
return RedirectResponse(url="/portal/admin/users?message=" + quote("User disabled."), status_code=303)
|
||||
if sync_result.get("status") == "error":
|
||||
return RedirectResponse(
|
||||
url="/portal/admin/users?error=" + quote(f"User disabled, but Gitea sync failed: {sync_result.get('error')}"),
|
||||
status_code=303,
|
||||
)
|
||||
return RedirectResponse(url="/portal/admin/users?message=" + quote("User disabled and Gitea synchronized."), status_code=303)
|
||||
|
||||
|
||||
@router.post("/admin/users/{user_id}/enable")
|
||||
@@ -212,11 +253,41 @@ def enable_user_action(user_id: int, user=Depends(require_user)):
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
set_user_enabled(user_id, True)
|
||||
target = get_user(user_id) or target
|
||||
sync_result = sync_gitea_user(target)
|
||||
log_audit_event(
|
||||
user,
|
||||
action="user.enabled",
|
||||
target_type="user",
|
||||
target_id=user_id,
|
||||
metadata={"username": target.get("username")},
|
||||
metadata={"username": target.get("username"), "gitea_sync_status": sync_result.get("status")},
|
||||
)
|
||||
return RedirectResponse(url="/portal/admin/users?message=" + quote("User enabled."), status_code=303)
|
||||
if sync_result.get("status") == "error":
|
||||
return RedirectResponse(
|
||||
url="/portal/admin/users?error=" + quote(f"User enabled, but Gitea sync failed: {sync_result.get('error')}"),
|
||||
status_code=303,
|
||||
)
|
||||
return RedirectResponse(url="/portal/admin/users?message=" + quote("User enabled and Gitea synchronized."), status_code=303)
|
||||
|
||||
|
||||
@router.post("/admin/users/{user_id}/sync-gitea")
|
||||
def sync_gitea_user_action(user_id: int, user=Depends(require_user)):
|
||||
require_admin(user)
|
||||
target = get_user(user_id)
|
||||
if not target:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
sync_result = sync_gitea_user(target)
|
||||
log_audit_event(
|
||||
user,
|
||||
action="user.gitea.synced",
|
||||
target_type="user",
|
||||
target_id=user_id,
|
||||
metadata={"username": target.get("username"), **sync_result},
|
||||
)
|
||||
if sync_result.get("status") == "error":
|
||||
return RedirectResponse(
|
||||
url="/portal/admin/users?error=" + quote(f"Gitea sync failed: {sync_result.get('error')}"),
|
||||
status_code=303,
|
||||
)
|
||||
return RedirectResponse(url="/portal/admin/users?message=" + quote("Gitea synchronized."), status_code=303)
|
||||
|
||||
Reference in New Issue
Block a user