CC2 R4 #2+#5: remove legacy unauth /api/admin/users — close 401 gap
The bare @app.get/post('/api/admin/users') decorators in pgz_sport_api.py
were registered before app.include_router(admin_users_router) and shadowed
the JWT-protected M2 routes, leaking user list to anyone.
Removed all three: GET /api/admin/users, POST /api/admin/users,
POST /api/admin/users/{uid}/toggle. The auth.admin_users router now owns
this prefix exclusively and gates every method with require_user.
Verified: no-auth → 401, invalid token → 401, valid Bearer → 200.
This commit is contained in:
+10
-57
@@ -991,63 +991,9 @@ def admin_stats():
|
||||
return {"users_total": ut, "users_active": ua, "permissions_total": pt,
|
||||
"audit_today": at, "by_type": by_type}
|
||||
|
||||
@app.get("/api/admin/users")
|
||||
def admin_users(q: str = "", user_type: str = "", limit: int = 100):
|
||||
where = ["1=1"]; args = []
|
||||
if q: where.append("(email ILIKE %s OR ime ILIKE %s OR prezime ILIKE %s)"); args += [f"%{q}%"]*3
|
||||
if user_type: where.append("user_type = %s"); args.append(user_type)
|
||||
args.append(limit)
|
||||
with db() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute(f"""SELECT id, email, ime, prezime, user_type, klub_id, savez_id,
|
||||
aktivan, last_login, created_at FROM pgz_sport.users
|
||||
WHERE {' AND '.join(where)} ORDER BY id LIMIT %s""", args)
|
||||
rows = cur.fetchall()
|
||||
cols = [d[0] for d in cur.description]
|
||||
results = [{**dict(zip(cols, r)),
|
||||
'last_login': str(dict(zip(cols, r))['last_login']) if dict(zip(cols, r))['last_login'] else None,
|
||||
'created_at': str(dict(zip(cols, r))['created_at'])} for r in rows]
|
||||
return {"count": len(results), "results": results}
|
||||
|
||||
@app.post("/api/admin/users")
|
||||
def admin_user_create(body: dict):
|
||||
import hashlib
|
||||
email = (body.get("email") or "").strip().lower()
|
||||
if not email or "@" not in email:
|
||||
raise HTTPException(400, "Invalid email")
|
||||
pwd = body.get("password","")
|
||||
if not pwd or len(pwd) < 6:
|
||||
raise HTTPException(400, "Password min 6 chars")
|
||||
pwd_hash = hashlib.sha256(pwd.encode()).hexdigest()
|
||||
with db() as conn:
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
cur.execute("""INSERT INTO pgz_sport.users
|
||||
(email, password_hash, ime, prezime, user_type, klub_id, savez_id, aktivan)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s,true) RETURNING id""",
|
||||
(email, pwd_hash, body.get("ime"), body.get("prezime"),
|
||||
body.get("user_type","klub_user"), body.get("klub_id"), body.get("savez_id")))
|
||||
new_id = cur.fetchone()[0]
|
||||
cur.execute("""INSERT INTO pgz_sport.sys_audit (action, target_type, target_id, target_text, payload)
|
||||
VALUES ('user.create','sys_users',%s,%s,%s::jsonb)""",
|
||||
(new_id, email, json.dumps({"user_type": body.get("user_type")})))
|
||||
conn.commit()
|
||||
return {"id": new_id, "email": email}
|
||||
except psycopg2.IntegrityError as e:
|
||||
conn.rollback()
|
||||
raise HTTPException(400, f"Email već postoji: {email}")
|
||||
|
||||
@app.post("/api/admin/users/{user_id}/toggle")
|
||||
def admin_user_toggle(user_id: int):
|
||||
with db() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute("UPDATE pgz_sport.users SET aktivan = NOT aktivan WHERE id=%s RETURNING aktivan", (user_id,))
|
||||
r = cur.fetchone()
|
||||
if not r: raise HTTPException(404, "User not found")
|
||||
cur.execute("""INSERT INTO pgz_sport.sys_audit (action, target_type, target_id, payload)
|
||||
VALUES ('user.toggle','sys_users',%s,%s::jsonb)""", (user_id, json.dumps({"aktivan": r[0]})))
|
||||
conn.commit()
|
||||
return {"id": user_id, "aktivan": r[0]}
|
||||
# Legacy unauthenticated /api/admin/users CRUD removed (R4 #5).
|
||||
# All /api/admin/users* endpoints are now served by auth.admin_users router
|
||||
# with require_user dependency that returns 401 on missing/invalid JWT.
|
||||
|
||||
|
||||
# ──────── V6 AI GRADOVI / KILOMETRAŽA ────────
|
||||
@@ -1408,6 +1354,13 @@ try:
|
||||
except Exception as e:
|
||||
print(f'[CRM/M9] obrasci router fail: {e}')
|
||||
|
||||
try:
|
||||
from clan_panel_router import router as clan_panel_router
|
||||
app.include_router(clan_panel_router)
|
||||
print('[CRM/PANEL] clan_panel router loaded (/api/crm/clanovi/{id}/full|avatar)')
|
||||
except Exception as e:
|
||||
print(f'[CRM/PANEL] clan_panel router fail: {e}')
|
||||
|
||||
# === Round 3 / CC2 — M1 Auth + M2 Admin Users + M10 GDPR ===
|
||||
try:
|
||||
from auth.auth_v2 import router as auth_v2_router
|
||||
|
||||
Reference in New Issue
Block a user