sprava uzivatelu
This commit is contained in:
+43
-10
@@ -4,10 +4,39 @@ from fastapi import HTTPException, Request, status
|
|||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
|
|
||||||
from app.db.database import get_connection
|
from app.db.database import get_connection
|
||||||
|
from app.db.migrations import run_migrations
|
||||||
|
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
|
||||||
|
|
||||||
|
def _table_columns(con, table_name: str) -> set[str]:
|
||||||
|
rows = con.execute(f"PRAGMA table_info({table_name})").fetchall()
|
||||||
|
return {row["name"] for row in rows}
|
||||||
|
|
||||||
|
|
||||||
|
def _user_select_columns(con) -> str:
|
||||||
|
columns = _table_columns(con, "users")
|
||||||
|
enabled_select = "COALESCE(is_enabled, 1) AS is_enabled" if "is_enabled" in columns else "1 AS is_enabled"
|
||||||
|
return f"id, username, display_name, email, password_hash, role, is_active, {enabled_select}, created_at, updated_at"
|
||||||
|
|
||||||
|
|
||||||
|
def mark_last_login(user_id: int) -> None:
|
||||||
|
con = get_connection()
|
||||||
|
columns = _table_columns(con, "users")
|
||||||
|
if "last_login_at" in columns:
|
||||||
|
con.execute(
|
||||||
|
"""
|
||||||
|
UPDATE users
|
||||||
|
SET last_login_at = CURRENT_TIMESTAMP,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(user_id,),
|
||||||
|
)
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
|
|
||||||
|
|
||||||
def hash_password(password: str) -> str:
|
def hash_password(password: str) -> str:
|
||||||
return pwd_context.hash(password)
|
return pwd_context.hash(password)
|
||||||
|
|
||||||
@@ -20,11 +49,12 @@ def verify_password(plain_password: str, password_hash: str) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def get_user_by_username(username: str) -> dict[str, Any] | None:
|
def get_user_by_username(username: str) -> dict[str, Any] | None:
|
||||||
|
run_migrations()
|
||||||
con = get_connection()
|
con = get_connection()
|
||||||
|
|
||||||
row = con.execute(
|
row = con.execute(
|
||||||
"""
|
f"""
|
||||||
SELECT id, username, display_name, email, password_hash, role, is_active, created_at, updated_at
|
SELECT {_user_select_columns(con)}
|
||||||
FROM users
|
FROM users
|
||||||
WHERE username = ?
|
WHERE username = ?
|
||||||
""",
|
""",
|
||||||
@@ -40,10 +70,11 @@ def find_or_create_oauth_user(username: str, display_name: str, email: str) -> d
|
|||||||
display_name = (display_name or username).strip() or username
|
display_name = (display_name or username).strip() or username
|
||||||
email = (email or "").strip()
|
email = (email or "").strip()
|
||||||
|
|
||||||
|
run_migrations()
|
||||||
con = get_connection()
|
con = get_connection()
|
||||||
row = con.execute(
|
row = con.execute(
|
||||||
"""
|
f"""
|
||||||
SELECT id, username, display_name, email, password_hash, role, is_active, created_at, updated_at
|
SELECT {_user_select_columns(con)}
|
||||||
FROM users
|
FROM users
|
||||||
WHERE username = ?
|
WHERE username = ?
|
||||||
""",
|
""",
|
||||||
@@ -82,8 +113,8 @@ def find_or_create_oauth_user(username: str, display_name: str, email: str) -> d
|
|||||||
con.commit()
|
con.commit()
|
||||||
user_id = row["id"] if row else con.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
|
user_id = row["id"] if row else con.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
|
||||||
user = con.execute(
|
user = con.execute(
|
||||||
"""
|
f"""
|
||||||
SELECT id, username, display_name, email, password_hash, role, is_active, created_at, updated_at
|
SELECT {_user_select_columns(con)}
|
||||||
FROM users
|
FROM users
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
""",
|
""",
|
||||||
@@ -95,11 +126,12 @@ def find_or_create_oauth_user(username: str, display_name: str, email: str) -> d
|
|||||||
|
|
||||||
|
|
||||||
def get_user_by_id(user_id: int) -> dict[str, Any] | None:
|
def get_user_by_id(user_id: int) -> dict[str, Any] | None:
|
||||||
|
run_migrations()
|
||||||
con = get_connection()
|
con = get_connection()
|
||||||
|
|
||||||
row = con.execute(
|
row = con.execute(
|
||||||
"""
|
f"""
|
||||||
SELECT id, username, display_name, email, password_hash, role, is_active, created_at, updated_at
|
SELECT {_user_select_columns(con)}
|
||||||
FROM users
|
FROM users
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
""",
|
""",
|
||||||
@@ -113,12 +145,13 @@ def get_user_by_id(user_id: int) -> dict[str, Any] | None:
|
|||||||
def authenticate_user(username: str, password: str) -> dict[str, Any] | None:
|
def authenticate_user(username: str, password: str) -> dict[str, Any] | None:
|
||||||
user = get_user_by_username(username.strip())
|
user = get_user_by_username(username.strip())
|
||||||
|
|
||||||
if not user or not user.get("is_active"):
|
if not user or not user.get("is_active") or not user.get("is_enabled", 1):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not verify_password(password, user.get("password_hash") or ""):
|
if not verify_password(password, user.get("password_hash") or ""):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
mark_last_login(int(user["id"]))
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
@@ -134,7 +167,7 @@ def current_user(request: Request) -> dict[str, Any] | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
user = get_user_by_id(user_id)
|
user = get_user_by_id(user_id)
|
||||||
if not user or not user.get("is_active"):
|
if not user or not user.get("is_active") or not user.get("is_enabled", 1):
|
||||||
request.session.pop("user_id", None)
|
request.session.pop("user_id", None)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,29 @@
|
|||||||
from app.db.database import get_connection
|
from app.db.database import get_connection
|
||||||
|
|
||||||
|
|
||||||
|
def _table_columns(con, table_name: str) -> set[str]:
|
||||||
|
rows = con.execute(f"PRAGMA table_info({table_name})").fetchall()
|
||||||
|
return {row["name"] for row in rows}
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_user_columns(con) -> None:
|
||||||
|
columns = _table_columns(con, "users")
|
||||||
|
if not columns:
|
||||||
|
return
|
||||||
|
|
||||||
|
if "is_enabled" not in columns:
|
||||||
|
default_expression = "COALESCE(is_active, 1)" if "is_active" in columns else "1"
|
||||||
|
con.execute("ALTER TABLE users ADD COLUMN is_enabled INTEGER NOT NULL DEFAULT 1")
|
||||||
|
con.execute(f"UPDATE users SET is_enabled = {default_expression} WHERE is_enabled IS NULL OR is_enabled = 1")
|
||||||
|
|
||||||
|
if "updated_at" not in columns:
|
||||||
|
con.execute("ALTER TABLE users ADD COLUMN updated_at TEXT")
|
||||||
|
con.execute("UPDATE users SET updated_at = COALESCE(created_at, CURRENT_TIMESTAMP) WHERE updated_at IS NULL")
|
||||||
|
|
||||||
|
|
||||||
def run_migrations():
|
def run_migrations():
|
||||||
con = get_connection()
|
con = get_connection()
|
||||||
|
ensure_user_columns(con)
|
||||||
|
|
||||||
con.execute(
|
con.execute(
|
||||||
"""
|
"""
|
||||||
|
|||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from app.db.database import get_connection
|
||||||
|
from app.db.migrations import run_migrations
|
||||||
|
|
||||||
|
ROLES = ("admin", "developer", "viewer")
|
||||||
|
|
||||||
|
|
||||||
|
def _table_columns(con, table_name: str) -> set[str]:
|
||||||
|
rows = con.execute(f"PRAGMA table_info({table_name})").fetchall()
|
||||||
|
return {row["name"] for row in rows}
|
||||||
|
|
||||||
|
|
||||||
|
def list_users() -> list[dict[str, Any]]:
|
||||||
|
run_migrations()
|
||||||
|
con = get_connection()
|
||||||
|
columns = _table_columns(con, "users")
|
||||||
|
last_login_select = "last_login_at" if "last_login_at" in columns else "NULL AS last_login_at"
|
||||||
|
|
||||||
|
rows = con.execute(
|
||||||
|
f"""
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
username,
|
||||||
|
display_name,
|
||||||
|
email,
|
||||||
|
role,
|
||||||
|
COALESCE(is_enabled, 1) AS is_enabled,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
{last_login_select}
|
||||||
|
FROM users
|
||||||
|
ORDER BY id
|
||||||
|
"""
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
con.close()
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def get_user(user_id: int) -> dict[str, Any] | None:
|
||||||
|
run_migrations()
|
||||||
|
con = get_connection()
|
||||||
|
columns = _table_columns(con, "users")
|
||||||
|
last_login_select = "last_login_at" if "last_login_at" in columns else "NULL AS last_login_at"
|
||||||
|
|
||||||
|
row = con.execute(
|
||||||
|
f"""
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
username,
|
||||||
|
display_name,
|
||||||
|
email,
|
||||||
|
role,
|
||||||
|
COALESCE(is_enabled, 1) AS is_enabled,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
{last_login_select}
|
||||||
|
FROM users
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(user_id,),
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
con.close()
|
||||||
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def count_enabled_admins(excluding_user_id: int | None = None) -> int:
|
||||||
|
run_migrations()
|
||||||
|
con = get_connection()
|
||||||
|
params: list[Any] = []
|
||||||
|
exclusion = ""
|
||||||
|
if excluding_user_id is not None:
|
||||||
|
exclusion = "AND id != ?"
|
||||||
|
params.append(excluding_user_id)
|
||||||
|
|
||||||
|
row = con.execute(
|
||||||
|
f"""
|
||||||
|
SELECT COUNT(*) AS count
|
||||||
|
FROM users
|
||||||
|
WHERE LOWER(COALESCE(role, '')) = 'admin'
|
||||||
|
AND COALESCE(is_enabled, 1) = 1
|
||||||
|
{exclusion}
|
||||||
|
""",
|
||||||
|
params,
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
con.close()
|
||||||
|
return int(row["count"] or 0)
|
||||||
|
|
||||||
|
|
||||||
|
def update_user_role(user_id: int, role: str) -> None:
|
||||||
|
if role not in ROLES:
|
||||||
|
raise ValueError("Invalid user role")
|
||||||
|
|
||||||
|
run_migrations()
|
||||||
|
con = get_connection()
|
||||||
|
con.execute(
|
||||||
|
"""
|
||||||
|
UPDATE users
|
||||||
|
SET role = ?,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(role, user_id),
|
||||||
|
)
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
|
|
||||||
|
|
||||||
|
def set_user_enabled(user_id: int, enabled: bool) -> None:
|
||||||
|
run_migrations()
|
||||||
|
con = get_connection()
|
||||||
|
con.execute(
|
||||||
|
"""
|
||||||
|
UPDATE users
|
||||||
|
SET is_enabled = ?,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(1 if enabled else 0, user_id),
|
||||||
|
)
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
+2
-1
@@ -5,7 +5,7 @@ from fastapi.staticfiles import StaticFiles
|
|||||||
from starlette.middleware.sessions import SessionMiddleware
|
from starlette.middleware.sessions import SessionMiddleware
|
||||||
|
|
||||||
from .config import read_env_bool, read_env_value
|
from .config import read_env_bool, read_env_value
|
||||||
from .routes import alerting, apps, audit, auth, backups, deployments, health, incidents, jobs, migration_readiness, operations, scheduled_scripts, workers
|
from .routes import alerting, apps, audit, auth, backups, deployments, health, incidents, jobs, migration_readiness, operations, scheduled_scripts, users, workers
|
||||||
|
|
||||||
|
|
||||||
def create_app() -> FastAPI:
|
def create_app() -> FastAPI:
|
||||||
@@ -34,6 +34,7 @@ def create_app() -> FastAPI:
|
|||||||
app.include_router(scheduled_scripts.router)
|
app.include_router(scheduled_scripts.router)
|
||||||
app.include_router(jobs.router)
|
app.include_router(jobs.router)
|
||||||
app.include_router(workers.router)
|
app.include_router(workers.router)
|
||||||
|
app.include_router(users.router)
|
||||||
app.include_router(audit.router)
|
app.include_router(audit.router)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
+9
-1
@@ -9,7 +9,7 @@ from urllib.request import urlopen
|
|||||||
from fastapi import APIRouter, Form, Request
|
from fastapi import APIRouter, Form, Request
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
|
|
||||||
from app.auth import authenticate_user, current_user, find_or_create_oauth_user
|
from app.auth import authenticate_user, current_user, find_or_create_oauth_user, mark_last_login
|
||||||
from app.config import (
|
from app.config import (
|
||||||
get_gitea_public_url,
|
get_gitea_public_url,
|
||||||
get_gitea_redirect_uri,
|
get_gitea_redirect_uri,
|
||||||
@@ -114,12 +114,16 @@ def gitea_callback(request: Request, code: str = "", state: str = "", error: str
|
|||||||
display_name = (gitea_user.get("full_name") or username).strip()
|
display_name = (gitea_user.get("full_name") or username).strip()
|
||||||
email = (gitea_user.get("email") or "").strip()
|
email = (gitea_user.get("email") or "").strip()
|
||||||
user = find_or_create_oauth_user(username, display_name, email)
|
user = find_or_create_oauth_user(username, display_name, email)
|
||||||
|
if not user.get("is_enabled", 1):
|
||||||
|
_log_gitea_failure("disabled_user", username=username)
|
||||||
|
return _render_login("User is disabled in the portal.")
|
||||||
if not user.get("is_active"):
|
if not user.get("is_active"):
|
||||||
_log_gitea_failure("disabled_user", username=username)
|
_log_gitea_failure("disabled_user", username=username)
|
||||||
return _render_login("Uživatel je v portálu vypnutý.")
|
return _render_login("Uživatel je v portálu vypnutý.")
|
||||||
|
|
||||||
request.session.clear()
|
request.session.clear()
|
||||||
request.session["user_id"] = user["id"]
|
request.session["user_id"] = user["id"]
|
||||||
|
mark_last_login(int(user["id"]))
|
||||||
log_audit_event(
|
log_audit_event(
|
||||||
user,
|
user,
|
||||||
action="auth.gitea.login.success",
|
action="auth.gitea.login.success",
|
||||||
@@ -201,12 +205,16 @@ def google_callback(request: Request, code: str = "", state: str = "", error: st
|
|||||||
username = email
|
username = email
|
||||||
display_name = (google_user.get("name") or email).strip()
|
display_name = (google_user.get("name") or email).strip()
|
||||||
user = find_or_create_oauth_user(username, display_name, email)
|
user = find_or_create_oauth_user(username, display_name, email)
|
||||||
|
if not user.get("is_enabled", 1):
|
||||||
|
_log_google_failure("disabled_user", username=username)
|
||||||
|
return _render_login("User is disabled in the portal.")
|
||||||
if not user.get("is_active"):
|
if not user.get("is_active"):
|
||||||
_log_google_failure("disabled_user", username=username)
|
_log_google_failure("disabled_user", username=username)
|
||||||
return _render_login("Uživatel je v portálu vypnutý.")
|
return _render_login("Uživatel je v portálu vypnutý.")
|
||||||
|
|
||||||
request.session.clear()
|
request.session.clear()
|
||||||
request.session["user_id"] = user["id"]
|
request.session["user_id"] = user["id"]
|
||||||
|
mark_last_login(int(user["id"]))
|
||||||
log_audit_event(
|
log_audit_event(
|
||||||
user,
|
user,
|
||||||
action="auth.google.login.success",
|
action="auth.google.login.success",
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
import html
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, Form, HTTPException, Request
|
||||||
|
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.templates.layout import page
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
def require_admin(user: dict) -> None:
|
||||||
|
if (user.get("role") or "").lower() != "admin":
|
||||||
|
raise HTTPException(status_code=403, detail="Only admins can manage users")
|
||||||
|
|
||||||
|
|
||||||
|
def render_role_options(selected: str) -> str:
|
||||||
|
options = []
|
||||||
|
for role in ROLES:
|
||||||
|
selected_attr = " selected" if selected == role else ""
|
||||||
|
options.append(f'<option value="{role}"{selected_attr}>{role}</option>')
|
||||||
|
return "".join(options)
|
||||||
|
|
||||||
|
|
||||||
|
def render_enabled_pill(enabled) -> str:
|
||||||
|
if enabled:
|
||||||
|
return '<span class="pill pill-success">enabled</span>'
|
||||||
|
return '<span class="pill pill-muted">disabled</span>'
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/admin/users", response_class=HTMLResponse)
|
||||||
|
def users_page(request: Request, message: str = "", error: str = "", user=Depends(require_user)):
|
||||||
|
require_admin(user)
|
||||||
|
|
||||||
|
notice = ""
|
||||||
|
if message:
|
||||||
|
notice = f'<p class="alert">{html.escape(message)}</p>'
|
||||||
|
if error:
|
||||||
|
notice = f'<p class="alert alert-danger">{html.escape(error)}</p>'
|
||||||
|
|
||||||
|
rows = ""
|
||||||
|
for managed_user in list_users():
|
||||||
|
user_id = int(managed_user.get("id"))
|
||||||
|
user_id_html = html.escape(str(user_id))
|
||||||
|
username = html.escape(managed_user.get("username", "") or "")
|
||||||
|
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 "")
|
||||||
|
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))
|
||||||
|
toggle_label = "Disable" if enabled else "Enable"
|
||||||
|
toggle_action = "disable" if enabled else "enable"
|
||||||
|
|
||||||
|
rows += f"""
|
||||||
|
<tr>
|
||||||
|
<td>{user_id_html}</td>
|
||||||
|
<td><strong>{username}</strong></td>
|
||||||
|
<td>{display_name}</td>
|
||||||
|
<td>{email}</td>
|
||||||
|
<td>
|
||||||
|
<form method="post" action="/portal/admin/users/{user_id_html}/role" class="inline-form">
|
||||||
|
<select name="role">{render_role_options(role)}</select>
|
||||||
|
<button type="submit" class="btn-secondary">Save</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<td>{render_enabled_pill(enabled)}</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>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
rows = '<tr><td colspan="9">No users found.</td></tr>'
|
||||||
|
|
||||||
|
return page(
|
||||||
|
"Users",
|
||||||
|
f"""
|
||||||
|
<div class="card">
|
||||||
|
<h2>Users</h2>
|
||||||
|
<p class="muted">Administration of portal users and roles.</p>
|
||||||
|
{notice}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>User List</h2>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Display name</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Last login</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
{rows}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
""",
|
||||||
|
user=user,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/admin/users/{user_id}/role")
|
||||||
|
def update_role_action(user_id: int, role: str = Form(...), user=Depends(require_user)):
|
||||||
|
require_admin(user)
|
||||||
|
target = get_user(user_id)
|
||||||
|
if not target:
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
if role not in ROLES:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid role")
|
||||||
|
|
||||||
|
old_role = (target.get("role") or "").lower()
|
||||||
|
if old_role == "admin" and role != "admin" and count_enabled_admins(excluding_user_id=user_id) == 0:
|
||||||
|
return RedirectResponse(
|
||||||
|
url="/portal/admin/users?error=" + quote("Cannot remove admin role from the last enabled admin."),
|
||||||
|
status_code=303,
|
||||||
|
)
|
||||||
|
|
||||||
|
update_user_role(user_id, role)
|
||||||
|
if old_role != role:
|
||||||
|
log_audit_event(
|
||||||
|
user,
|
||||||
|
action="user.role.updated",
|
||||||
|
target_type="user",
|
||||||
|
target_id=user_id,
|
||||||
|
metadata={
|
||||||
|
"username": target.get("username"),
|
||||||
|
"old_role": old_role,
|
||||||
|
"new_role": role,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return RedirectResponse(url="/portal/admin/users?message=" + quote("User role updated."), status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/admin/users/{user_id}/disable")
|
||||||
|
def disable_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")
|
||||||
|
if (target.get("role") or "").lower() == "admin" and count_enabled_admins(excluding_user_id=user_id) == 0:
|
||||||
|
return RedirectResponse(
|
||||||
|
url="/portal/admin/users?error=" + quote("Cannot disable the last enabled admin."),
|
||||||
|
status_code=303,
|
||||||
|
)
|
||||||
|
|
||||||
|
set_user_enabled(user_id, False)
|
||||||
|
log_audit_event(
|
||||||
|
user,
|
||||||
|
action="user.disabled",
|
||||||
|
target_type="user",
|
||||||
|
target_id=user_id,
|
||||||
|
metadata={"username": target.get("username")},
|
||||||
|
)
|
||||||
|
return RedirectResponse(url="/portal/admin/users?message=" + quote("User disabled."), status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/admin/users/{user_id}/enable")
|
||||||
|
def enable_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")
|
||||||
|
|
||||||
|
set_user_enabled(user_id, True)
|
||||||
|
log_audit_event(
|
||||||
|
user,
|
||||||
|
action="user.enabled",
|
||||||
|
target_type="user",
|
||||||
|
target_id=user_id,
|
||||||
|
metadata={"username": target.get("username")},
|
||||||
|
)
|
||||||
|
return RedirectResponse(url="/portal/admin/users?message=" + quote("User enabled."), status_code=303)
|
||||||
@@ -12,7 +12,10 @@ def page(title: str, body: str, user=None) -> str:
|
|||||||
if user:
|
if user:
|
||||||
username = html.escape(user.get("username", ""))
|
username = html.escape(user.get("username", ""))
|
||||||
display_name = html.escape(user.get("display_name") or user.get("username", ""))
|
display_name = html.escape(user.get("display_name") or user.get("username", ""))
|
||||||
nav = """
|
admin_links = ""
|
||||||
|
if (user.get("role") or "").lower() == "admin":
|
||||||
|
admin_links = '<a href="/portal/admin/users">Users</a>'
|
||||||
|
nav = f"""
|
||||||
<nav>
|
<nav>
|
||||||
<a class="nav-link" href="/portal/operations">Přehled</a>
|
<a class="nav-link" href="/portal/operations">Přehled</a>
|
||||||
<a class="nav-link" href="/portal/apps">Služby</a>
|
<a class="nav-link" href="/portal/apps">Služby</a>
|
||||||
@@ -31,6 +34,7 @@ def page(title: str, body: str, user=None) -> str:
|
|||||||
<div class="nav-menu">
|
<div class="nav-menu">
|
||||||
<button type="button" class="nav-menu-button">Správa</button>
|
<button type="button" class="nav-menu-button">Správa</button>
|
||||||
<div class="nav-menu-panel">
|
<div class="nav-menu-panel">
|
||||||
|
{admin_links}
|
||||||
<a href="/portal/audit">Audit</a>
|
<a href="/portal/audit">Audit</a>
|
||||||
<a href="/portal/backups">Zálohy</a>
|
<a href="/portal/backups">Zálohy</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user