Upravil jsem Google OAuth tak, že Google uživatel se hledá/vytváří přes email, ukládá auth_provider='google', provider_subject, display_name, avatar_url a po úspěšném loginu last_login_at. První Google uživatel v prázdné users tabulce dostane admin, další noví Google uživatelé viewer.

Přidal jsem AUTH_MODE=google|mixed|local:

google: jen Google login, lokální/Gitea nejdou ani přes přímý endpoint
mixed: Google + lokální/Gitea fallback
local: jen lokální login
Správa uživatelů zůstává admin-only, guard posledního admina platí pro odebrání role i deaktivaci. Přidal jsem do ní i přehled oprávnění rolí a zobrazení provider/avatar metadat.

DB migrace teď přes PRAGMA table_info(users) doplňuje chybějící sloupce bez mazání/recreate existující tabulky; pokud users vůbec neexistuje, bezpečně ji vytvoří. Podporované sloupce zahrnují email, display_name, role, is_enabled, created_at, updated_at, last_login_at, auth_provider, provider_subject, plus kompatibilní sloupce pro lokální login.
This commit is contained in:
JiriUhlir
2026-06-10 08:37:42 +02:00
parent 9cec3d001f
commit 272b1eb883
7 changed files with 295 additions and 16 deletions
+37 -1
View File
@@ -31,6 +31,34 @@ def render_enabled_pill(enabled) -> str:
return '<span class="pill pill-muted">disabled</span>'
def render_role_overview() -> str:
role_rows = (
("admin", "Full administration, user management, role changes, disabling/enabling users, and admin-only operations."),
("developer", "Operational portal access for application and service workflows. No access to Administration / Users."),
("viewer", "Default Google role. Read-oriented portal access where routes allow it. No access to Administration / Users."),
)
rows = ""
for role, description in role_rows:
rows += f"""
<tr>
<td><span class="pill pill-muted">{html.escape(role)}</span></td>
<td>{html.escape(description)}</td>
</tr>
"""
return f"""
<div class="card">
<h2>Role Permissions</h2>
<table>
<tr>
<th>Role</th>
<th>Access</th>
</tr>
{rows}
</table>
</div>
"""
@router.get("/admin/users", response_class=HTMLResponse)
def users_page(request: Request, message: str = "", error: str = "", user=Depends(require_user)):
require_admin(user)
@@ -49,6 +77,9 @@ def users_page(request: Request, message: str = "", error: str = "", user=Depend
display_name = html.escape(managed_user.get("display_name", "") or "")
email = html.escape(managed_user.get("email", "") or "")
role = html.escape(managed_user.get("role", "") or "")
auth_provider = html.escape(managed_user.get("auth_provider", "") or "")
avatar_url = html.escape(managed_user.get("avatar_url", "") or "", quote=True)
avatar = f'<img class="user-avatar" src="{avatar_url}" alt="">' if avatar_url else ""
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))
@@ -60,7 +91,9 @@ def users_page(request: Request, message: str = "", error: str = "", user=Depend
<td>{user_id_html}</td>
<td><strong>{username}</strong></td>
<td>{display_name}</td>
<td>{avatar}</td>
<td>{email}</td>
<td>{auth_provider}</td>
<td>
<form method="post" action="/portal/admin/users/{user_id_html}/role" class="inline-form">
<select name="role">{render_role_options(role)}</select>
@@ -79,7 +112,7 @@ def users_page(request: Request, message: str = "", error: str = "", user=Depend
"""
if not rows:
rows = '<tr><td colspan="9">No users found.</td></tr>'
rows = '<tr><td colspan="11">No users found.</td></tr>'
return page(
"Users",
@@ -97,7 +130,9 @@ def users_page(request: Request, message: str = "", error: str = "", user=Depend
<th>ID</th>
<th>Username</th>
<th>Display name</th>
<th>Avatar</th>
<th>Email</th>
<th>Provider</th>
<th>Role</th>
<th>Status</th>
<th>Created</th>
@@ -107,6 +142,7 @@ def users_page(request: Request, message: str = "", error: str = "", user=Depend
{rows}
</table>
</div>
{render_role_overview()}
""",
user=user,
)