users.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. from __future__ import annotations
  2. from flask import g, jsonify, request
  3. from sqlalchemy import text
  4. from sqlalchemy.exc import IntegrityError
  5. from app import db
  6. from app.api.system import bp
  7. from app.commands.bootstrap_admin import validate_password
  8. from app.core.common.identifiers import new_governance_uid
  9. from app.core.system.auth import hash_password
  10. from app.core.system.permissions import MANAGE_USERS, require_permissions
  11. from app.models.result import failed, success
  12. VALID_ROLES = {"admin", "editor", "viewer"}
  13. def _active_admin_count(session) -> int:
  14. return int(
  15. session.execute(
  16. text(
  17. "SELECT COUNT(DISTINCT u.id) FROM public.users u "
  18. "JOIN public.user_roles ur ON ur.user_id = u.id "
  19. "JOIN public.roles r ON r.id = ur.role_id "
  20. "WHERE u.status = 'active' AND r.name = 'admin'"
  21. )
  22. ).scalar_one()
  23. )
  24. def _is_active_admin(session, user_id: str) -> bool:
  25. return bool(
  26. session.execute(
  27. text(
  28. "SELECT 1 FROM public.users u "
  29. "JOIN public.user_roles ur ON ur.user_id = u.id "
  30. "JOIN public.roles r ON r.id = ur.role_id "
  31. "WHERE u.id = CAST(:id AS uuid) AND u.status = 'active' "
  32. "AND r.name = 'admin'"
  33. ),
  34. {"id": user_id},
  35. ).scalar()
  36. )
  37. def _serialize_users(session) -> list[dict]:
  38. rows = session.execute(
  39. text(
  40. "SELECT u.id::text, u.username, u.display_name, u.status, "
  41. "u.created_at, u.last_login_at, "
  42. "COALESCE(array_agg(r.name ORDER BY r.name) FILTER "
  43. "(WHERE r.name IS NOT NULL), ARRAY[]::varchar[]) "
  44. "FROM public.users u "
  45. "LEFT JOIN public.user_roles ur ON ur.user_id = u.id "
  46. "LEFT JOIN public.roles r ON r.id = ur.role_id "
  47. "GROUP BY u.id ORDER BY u.created_at, u.username"
  48. )
  49. )
  50. return [
  51. {
  52. "id": row[0],
  53. "username": row[1],
  54. "display_name": row[2],
  55. "status": row[3],
  56. "created_at": row[4].isoformat(),
  57. "last_login_at": row[5].isoformat() if row[5] else None,
  58. "roles": list(row[6]),
  59. }
  60. for row in rows
  61. ]
  62. @bp.route("/users", methods=["GET"])
  63. @require_permissions(MANAGE_USERS)
  64. def list_users():
  65. return jsonify(success(_serialize_users(db.session)))
  66. @bp.route("/users", methods=["POST"])
  67. @require_permissions(MANAGE_USERS)
  68. def create_user():
  69. body = request.get_json(silent=True) or {}
  70. username = str(body.get("username") or "").strip()
  71. password = str(body.get("password") or "")
  72. roles = set(body.get("roles") or ["viewer"])
  73. if not username or not roles or not roles <= VALID_ROLES:
  74. return jsonify(failed("用户名或角色无效", code=400)), 400
  75. try:
  76. validate_password(password)
  77. user_id = new_governance_uid()
  78. db.session.execute(
  79. text(
  80. "INSERT INTO public.users (id, username, display_name, password_hash) "
  81. "VALUES (CAST(:id AS uuid), :username, :display_name, :password_hash)"
  82. ),
  83. {
  84. "id": user_id,
  85. "username": username,
  86. "display_name": body.get("display_name") or username,
  87. "password_hash": hash_password(password),
  88. },
  89. )
  90. db.session.execute(
  91. text(
  92. "INSERT INTO public.user_roles (user_id, role_id, assigned_by) "
  93. "SELECT CAST(:user_id AS uuid), id, CAST(:assigned_by AS uuid) "
  94. "FROM public.roles WHERE name = ANY(:roles)"
  95. ),
  96. {"user_id": user_id, "assigned_by": g.current_user["id"], "roles": sorted(roles)},
  97. )
  98. db.session.commit()
  99. return jsonify(success({"id": user_id}, "用户创建成功", code=201)), 201
  100. except (ValueError, IntegrityError) as exc:
  101. db.session.rollback()
  102. return jsonify(failed(str(exc), code=400)), 400
  103. @bp.route("/users/<user_id>", methods=["PUT"])
  104. @require_permissions(MANAGE_USERS)
  105. def update_user(user_id: str):
  106. body = request.get_json(silent=True) or {}
  107. status = body.get("status")
  108. if status not in (None, "active", "disabled"):
  109. return jsonify(failed("用户状态无效", code=400)), 400
  110. if status == "disabled" and _is_active_admin(db.session, user_id):
  111. if _active_admin_count(db.session) <= 1:
  112. return jsonify(failed("不能停用最后一个有效管理员", code=409)), 409
  113. values = {"id": user_id}
  114. assignments = []
  115. if status is not None:
  116. assignments.append("status = :status")
  117. values["status"] = status
  118. if "display_name" in body:
  119. assignments.append("display_name = :display_name")
  120. values["display_name"] = str(body.get("display_name") or "")[:100] or None
  121. if "password" in body:
  122. validate_password(str(body["password"]))
  123. assignments.append("password_hash = :password_hash")
  124. values["password_hash"] = hash_password(str(body["password"]))
  125. if not assignments:
  126. return jsonify(failed("没有可更新字段", code=400)), 400
  127. result = db.session.execute(
  128. text(
  129. "UPDATE public.users SET " + ", ".join(assignments) +
  130. ", updated_at = CURRENT_TIMESTAMP WHERE id = CAST(:id AS uuid)"
  131. ),
  132. values,
  133. )
  134. if not result.rowcount:
  135. db.session.rollback()
  136. return jsonify(failed("用户不存在", code=404)), 404
  137. db.session.commit()
  138. return jsonify(success(message="用户更新成功"))
  139. @bp.route("/users/<user_id>/roles", methods=["PUT"])
  140. @require_permissions(MANAGE_USERS)
  141. def update_user_roles(user_id: str):
  142. body = request.get_json(silent=True) or {}
  143. roles = set(body.get("roles") or [])
  144. if not roles or not roles <= VALID_ROLES:
  145. return jsonify(failed("角色无效", code=400)), 400
  146. if _is_active_admin(db.session, user_id) and "admin" not in roles:
  147. if _active_admin_count(db.session) <= 1:
  148. return jsonify(failed("不能移除最后一个有效管理员角色", code=409)), 409
  149. db.session.execute(
  150. text("DELETE FROM public.user_roles WHERE user_id = CAST(:id AS uuid)"),
  151. {"id": user_id},
  152. )
  153. result = db.session.execute(
  154. text(
  155. "INSERT INTO public.user_roles (user_id, role_id, assigned_by) "
  156. "SELECT CAST(:user_id AS uuid), id, CAST(:assigned_by AS uuid) "
  157. "FROM public.roles WHERE name = ANY(:roles)"
  158. ),
  159. {"user_id": user_id, "assigned_by": g.current_user["id"], "roles": sorted(roles)},
  160. )
  161. if result.rowcount != len(roles):
  162. db.session.rollback()
  163. return jsonify(failed("用户或角色不存在", code=404)), 404
  164. db.session.commit()
  165. return jsonify(success(message="角色更新成功"))