public.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. """公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/shanxia2.0.sql 基线)"""
  2. from typing import Optional
  3. from fastapi import APIRouter, Depends, HTTPException, Request
  4. from sqlalchemy.exc import OperationalError
  5. from sqlalchemy.orm import Session
  6. from app.core.database import get_db
  7. from app.core.security import verify_password
  8. from app.core.auth import (
  9. create_access_token,
  10. create_refresh_token,
  11. decode_token,
  12. get_current_user_by_token,
  13. extend_session,
  14. is_refresh_token_valid,
  15. is_session_valid,
  16. revoke_refresh_token,
  17. revoke_session,
  18. store_refresh_token,
  19. store_session,
  20. )
  21. from app.core.response import success_response, error_response
  22. from app.core.config import settings
  23. from app.models.models import SysUser
  24. from app.crud.user import user_crud
  25. from app.core.utils import generate_uuid
  26. from app.core import login_crypto
  27. from app.services.web_auth_roles import iter_user_menu_perm_codes, resolve_web_role_for_user
  28. from app.crud.crm.quote import view_by_share_code
  29. router = APIRouter(prefix="", tags=["公共接口"])
  30. def _avatar_url_field(user: SysUser) -> Optional[str]:
  31. raw = getattr(user, "avatar", None)
  32. if not raw:
  33. return None
  34. s = str(raw).strip()
  35. return s if s else None
  36. def _build_user_info_payload(user: SysUser, web_role: str) -> dict:
  37. """与 web-admin `userInfo` 字段约定一致。"""
  38. phone = (getattr(user, "phone_no", None) or "").strip() or None
  39. return {
  40. "id": user.id,
  41. "username": user.user_no,
  42. "realName": user.nick_name,
  43. "phone": phone,
  44. "email": (getattr(user, "email", None) or "").strip() or None,
  45. "role": web_role,
  46. "regionId": user.dept_id,
  47. "regionName": None,
  48. "avatarUrl": _avatar_url_field(user),
  49. }
  50. def _jwt_user_payload(user: SysUser, web_role: str) -> dict:
  51. return {
  52. "sub": user.id,
  53. "username": user.user_no,
  54. "role": web_role,
  55. "region_id": user.dept_id,
  56. "real_name": user.nick_name,
  57. }
  58. @router.get("/web/auth/login-rsa-public-key", response_model=dict)
  59. async def web_login_rsa_public_key():
  60. """Web 端登录用 RSA 公钥(PKCS#1 v1.5,与 JSEncrypt 一致)"""
  61. login_crypto.ensure_initialized()
  62. return success_response({"publicKeyPem": login_crypto.get_public_pem()})
  63. @router.post("/web/auth/login", response_model=dict)
  64. async def web_login(
  65. request: Request,
  66. body: dict,
  67. db: Session = Depends(get_db),
  68. ):
  69. """Web 端登录:账号为 sys_user.user_no,密码为 sys_user.password(bcrypt)。"""
  70. login_crypto.ensure_initialized()
  71. user_no, password, cred_err = login_crypto.extract_login_credentials(body)
  72. if cred_err:
  73. return error_response(400, cred_err)
  74. user = user_crud.get_by_user_no_for_web_login(db, user_no)
  75. if not user:
  76. return error_response(400, "用户名或密码错误")
  77. pwd_hash = getattr(user, "password", None) or ""
  78. if not verify_password(password, pwd_hash):
  79. return error_response(400, "用户名或密码错误")
  80. if user.status != "1":
  81. return error_response(403, "账号已被停用,请联系管理员")
  82. web_role = resolve_web_role_for_user(db, user.id, user.tenant_no)
  83. session_id = generate_uuid()
  84. access_jti = generate_uuid()
  85. refresh_jti = generate_uuid()
  86. access_token = create_access_token(
  87. data=_jwt_user_payload(user, web_role),
  88. session_id=session_id,
  89. token_jti=access_jti,
  90. )
  91. refresh_token = create_refresh_token(
  92. data={
  93. "sub": user.id,
  94. "username": user.user_no,
  95. "role": web_role,
  96. },
  97. session_id=session_id,
  98. refresh_jti=refresh_jti,
  99. )
  100. await store_session(session_id=session_id, user_id=user.id)
  101. await store_refresh_token(refresh_jti=refresh_jti, user_id=user.id)
  102. return success_response(
  103. {
  104. "token": access_token,
  105. "refreshToken": refresh_token,
  106. "expiresIn": settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
  107. "userInfo": _build_user_info_payload(user, web_role),
  108. }
  109. )
  110. @router.post("/web/auth/refresh-token", response_model=dict)
  111. async def refresh_token(body: dict, db: Session = Depends(get_db)):
  112. """刷新 Token"""
  113. refresh_token_val = body.get("refreshToken")
  114. if not refresh_token_val:
  115. return error_response(400, "refreshToken不能为空")
  116. try:
  117. payload = decode_token(refresh_token_val)
  118. if payload.get("type") != "refresh":
  119. return error_response(401, "无效的refreshToken")
  120. session_id = payload.get("sid")
  121. refresh_jti = payload.get("jti")
  122. if not await is_session_valid(session_id=session_id):
  123. return error_response(401, "refreshToken已失效,请重新登录")
  124. if not await is_refresh_token_valid(refresh_jti=refresh_jti):
  125. return error_response(401, "无效的refreshToken")
  126. user_id = payload.get("sub")
  127. user = user_crud.get(db, user_id)
  128. if not user or user.del_flag != 0 or user.status != "1":
  129. return error_response(401, "用户不存在或已禁用")
  130. web_role = resolve_web_role_for_user(db, user.id, user.tenant_no)
  131. await revoke_refresh_token(refresh_jti=refresh_jti)
  132. await extend_session(session_id=session_id)
  133. new_access_jti = generate_uuid()
  134. new_refresh_jti = generate_uuid()
  135. new_access_token = create_access_token(
  136. data=_jwt_user_payload(user, web_role),
  137. session_id=session_id,
  138. token_jti=new_access_jti,
  139. )
  140. new_refresh_token = create_refresh_token(
  141. data={
  142. "sub": user.id,
  143. "username": user.user_no,
  144. "role": web_role,
  145. },
  146. session_id=session_id,
  147. refresh_jti=new_refresh_jti,
  148. )
  149. await store_refresh_token(refresh_jti=new_refresh_jti, user_id=user.id)
  150. return success_response(
  151. {
  152. "token": new_access_token,
  153. "refreshToken": new_refresh_token,
  154. "expiresIn": settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
  155. }
  156. )
  157. except HTTPException:
  158. return error_response(401, "refreshToken已过期")
  159. @router.post("/web/auth/logout", response_model=dict)
  160. async def logout(request: Request):
  161. """登出:吊销 Redis 会话(若已配置 REDIS_HOST)。"""
  162. authorization = request.headers.get("Authorization", "")
  163. if not authorization.startswith("Bearer "):
  164. return error_response(401, "未授权")
  165. token = authorization.replace("Bearer ", "")
  166. try:
  167. payload = decode_token(token)
  168. session_id = payload.get("sid")
  169. if session_id:
  170. await revoke_session(session_id=session_id)
  171. except HTTPException:
  172. return error_response(401, "Token无效或已过期")
  173. return success_response({"message": "登出成功"})
  174. @router.get("/web/auth/current-user", response_model=dict)
  175. async def get_current_user_profile(request: Request, db: Session = Depends(get_db)):
  176. """获取当前用户信息(与 sys_user 字段对应)。"""
  177. authorization = request.headers.get("Authorization", "")
  178. if not authorization.startswith("Bearer "):
  179. return error_response(401, "未授权")
  180. token = authorization.replace("Bearer ", "")
  181. try:
  182. current_user = await get_current_user_by_token(token=token, db=db)
  183. au = current_user.get("avatar_url")
  184. avatar_url = str(au).strip() if au else None
  185. return success_response(
  186. {
  187. "id": current_user.get("user_id"),
  188. "username": current_user.get("username"),
  189. "realName": current_user.get("real_name"),
  190. "phone": current_user.get("phone"),
  191. "email": current_user.get("email"),
  192. "role": current_user.get("role"),
  193. "isSuperAdmin": current_user.get("role") == "super_admin",
  194. "regionId": current_user.get("region_id"),
  195. "regionName": None,
  196. "regionIds": [],
  197. "isActive": current_user.get("is_active"),
  198. "avatarUrl": avatar_url if avatar_url else None,
  199. }
  200. )
  201. except HTTPException:
  202. return error_response(401, "Token无效或已过期")
  203. @router.get("/web/auth/menu-codes", response_model=dict)
  204. async def get_web_auth_menu_codes(request: Request, db: Session = Depends(get_db)):
  205. """获取当前用户菜单权限标识(sys_menu.perms,经角色继承)。"""
  206. authorization = request.headers.get("Authorization", "")
  207. if not authorization.startswith("Bearer "):
  208. return error_response(401, "未授权")
  209. token = authorization.replace("Bearer ", "")
  210. try:
  211. current_user = await get_current_user_by_token(token=token, db=db)
  212. role = current_user.get("role")
  213. if role == "super_admin":
  214. return success_response(
  215. {
  216. "isSuperAdmin": True,
  217. "all": True,
  218. "menuCodes": [],
  219. }
  220. )
  221. try:
  222. codes = iter_user_menu_perm_codes(db, current_user.get("user_id") or "")
  223. menu_codes = sorted(codes)
  224. except OperationalError:
  225. menu_codes = []
  226. return success_response(
  227. {
  228. "isSuperAdmin": False,
  229. "all": False,
  230. "menuCodes": menu_codes,
  231. }
  232. )
  233. except HTTPException:
  234. return error_response(401, "Token无效或已过期")
  235. @router.get("/public/crm/quote/share/{share_code}")
  236. async def public_quote_share(
  237. share_code: str,
  238. request: Request,
  239. password: str = "",
  240. db: Session = Depends(get_db),
  241. ):
  242. """公开分享查看报价单 — Java: GET /share/{shareCode}(无 JWT)"""
  243. try:
  244. ip_address = request.client.host if request.client else ""
  245. user_agent = request.headers.get("user-agent", "")
  246. detail = view_by_share_code(
  247. db,
  248. share_code,
  249. password=password or "",
  250. ip_address=ip_address,
  251. user_agent=user_agent,
  252. )
  253. return success_response(detail)
  254. except ValueError as e:
  255. return error_response(400, str(e))
  256. except Exception as e:
  257. return error_response(500, str(e))