full_server.py 128 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605
  1. #!/usr/bin/env python3
  2. """
  3. 完整的SSO服务器 - 包含认证API
  4. """
  5. import sys
  6. import os
  7. import socket
  8. import json
  9. import uuid
  10. # 添加src目录到Python路径
  11. sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
  12. # 加载环境变量
  13. from dotenv import load_dotenv
  14. load_dotenv()
  15. from fastapi import FastAPI, HTTPException, Depends, Request
  16. from fastapi.middleware.cors import CORSMiddleware
  17. from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
  18. from pydantic import BaseModel
  19. from typing import Optional, Any
  20. import hashlib
  21. import secrets
  22. # 修复JWT导入 - 确保使用正确的JWT库
  23. try:
  24. # 首先尝试使用PyJWT
  25. import jwt as pyjwt
  26. # 测试是否有encode方法
  27. test_token = pyjwt.encode({"test": "data"}, "secret", algorithm="HS256")
  28. jwt = pyjwt
  29. print("✅ 使用PyJWT库")
  30. except (ImportError, AttributeError, TypeError) as e:
  31. print(f"PyJWT导入失败: {e}")
  32. try:
  33. # 尝试使用python-jose
  34. from jose import jwt
  35. print("✅ 使用python-jose库")
  36. except ImportError as e:
  37. print(f"python-jose导入失败: {e}")
  38. # 最后尝试安装PyJWT
  39. print("尝试安装PyJWT...")
  40. import subprocess
  41. import sys
  42. try:
  43. subprocess.check_call([sys.executable, "-m", "pip", "install", "PyJWT"])
  44. import jwt
  45. print("✅ PyJWT安装成功")
  46. except Exception as install_error:
  47. print(f"❌ PyJWT安装失败: {install_error}")
  48. raise ImportError("无法导入JWT库,请手动安装: pip install PyJWT")
  49. from datetime import datetime, timedelta, timezone
  50. import pymysql
  51. from urllib.parse import urlparse
  52. # 导入RBAC API - 移除循环导入
  53. # from rbac_api import get_user_menus, get_all_menus, get_all_roles, get_user_permissions
  54. # 数据模型
  55. class LoginRequest(BaseModel):
  56. username: str
  57. password: str
  58. remember_me: bool = False
  59. class TokenResponse(BaseModel):
  60. access_token: str
  61. refresh_token: Optional[str] = None
  62. token_type: str = "Bearer"
  63. expires_in: int
  64. scope: Optional[str] = None
  65. class UserInfo(BaseModel):
  66. id: str
  67. username: str
  68. email: str
  69. phone: Optional[str] = None
  70. avatar_url: Optional[str] = None
  71. is_active: bool
  72. is_superuser: bool = False
  73. roles: list = []
  74. permissions: list = []
  75. class ApiResponse(BaseModel):
  76. code: int
  77. message: str
  78. data: Optional[Any] = None
  79. timestamp: str
  80. # 配置
  81. JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "dev-jwt-secret-key-12345")
  82. ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30"))
  83. def check_port(port):
  84. """检查端口是否可用"""
  85. with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
  86. try:
  87. s.bind(('localhost', port))
  88. return True
  89. except OSError:
  90. return False
  91. def find_available_port(start_port=8000, max_port=8010):
  92. """查找可用端口"""
  93. for port in range(start_port, max_port + 1):
  94. if check_port(port):
  95. return port
  96. return None
  97. def get_db_connection():
  98. """获取数据库连接"""
  99. try:
  100. database_url = os.getenv('DATABASE_URL', '')
  101. if not database_url:
  102. return None
  103. parsed = urlparse(database_url)
  104. config = {
  105. 'host': parsed.hostname or 'localhost',
  106. 'port': parsed.port or 3306,
  107. 'user': parsed.username or 'root',
  108. 'password': parsed.password or '',
  109. 'database': parsed.path[1:] if parsed.path else 'sso_db',
  110. 'charset': 'utf8mb4'
  111. }
  112. return pymysql.connect(**config)
  113. except Exception as e:
  114. print(f"数据库连接失败: {e}")
  115. return None
  116. def verify_password_simple(password: str, stored_hash: str) -> bool:
  117. """验证密码(简化版)"""
  118. if stored_hash.startswith("sha256$"):
  119. parts = stored_hash.split("$")
  120. if len(parts) == 3:
  121. salt = parts[1]
  122. expected_hash = parts[2]
  123. actual_hash = hashlib.sha256((password + salt).encode()).hexdigest()
  124. return actual_hash == expected_hash
  125. return False
  126. def create_access_token(data: dict) -> str:
  127. """创建访问令牌"""
  128. to_encode = data.copy()
  129. expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
  130. to_encode.update({"exp": expire, "iat": datetime.now(timezone.utc)})
  131. encoded_jwt = jwt.encode(to_encode, JWT_SECRET_KEY, algorithm="HS256")
  132. return encoded_jwt
  133. def verify_token(token: str) -> Optional[dict]:
  134. """验证令牌"""
  135. try:
  136. payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=["HS256"])
  137. return payload
  138. except jwt.PyJWTError:
  139. return None
  140. # 创建FastAPI应用
  141. app = FastAPI(
  142. title="SSO认证中心",
  143. version="1.0.0",
  144. description="OAuth2单点登录认证中心",
  145. docs_url="/docs",
  146. redoc_url="/redoc"
  147. )
  148. # 配置CORS
  149. app.add_middleware(
  150. CORSMiddleware,
  151. allow_origins=["*"],
  152. allow_credentials=True,
  153. allow_methods=["*"],
  154. allow_headers=["*"],
  155. )
  156. security = HTTPBearer()
  157. @app.get("/")
  158. async def root():
  159. """根路径"""
  160. return ApiResponse(
  161. code=0,
  162. message="欢迎使用SSO认证中心",
  163. data={
  164. "name": "SSO认证中心",
  165. "version": "1.0.0",
  166. "docs": "/docs"
  167. },
  168. timestamp=datetime.now(timezone.utc).isoformat()
  169. ).model_dump()
  170. @app.get("/health")
  171. async def health_check():
  172. """健康检查"""
  173. return ApiResponse(
  174. code=0,
  175. message="服务正常运行",
  176. data={
  177. "status": "healthy",
  178. "version": "1.0.0",
  179. "timestamp": datetime.now(timezone.utc).isoformat()
  180. },
  181. timestamp=datetime.now(timezone.utc).isoformat()
  182. ).model_dump()
  183. @app.post("/api/v1/auth/login")
  184. async def login(request: Request, login_data: LoginRequest):
  185. """用户登录"""
  186. print(f"🔐 收到登录请求: username={login_data.username}")
  187. try:
  188. # 获取数据库连接
  189. print("📊 尝试连接数据库...")
  190. conn = get_db_connection()
  191. if not conn:
  192. print("❌ 数据库连接失败")
  193. return ApiResponse(
  194. code=500001,
  195. message="数据库连接失败",
  196. timestamp=datetime.now(timezone.utc).isoformat()
  197. ).model_dump()
  198. print("✅ 数据库连接成功")
  199. cursor = conn.cursor()
  200. # 查找用户
  201. print(f"🔍 查找用户: {login_data.username}")
  202. cursor.execute(
  203. "SELECT id, username, email, password_hash, is_active, is_superuser FROM users WHERE username = %s OR email = %s",
  204. (login_data.username, login_data.username)
  205. )
  206. user_data = cursor.fetchone()
  207. print(f"👤 用户查询结果: {user_data is not None}")
  208. cursor.close()
  209. conn.close()
  210. if not user_data:
  211. print("❌ 用户不存在")
  212. return ApiResponse(
  213. code=200001,
  214. message="用户名或密码错误",
  215. timestamp=datetime.now(timezone.utc).isoformat()
  216. ).model_dump()
  217. user_id, username, email, password_hash, is_active, is_superuser = user_data
  218. print(f"✅ 找到用户: {username}, 激活状态: {is_active}")
  219. # 检查用户状态
  220. if not is_active:
  221. print("❌ 用户已被禁用")
  222. return ApiResponse(
  223. code=200002,
  224. message="用户已被禁用",
  225. timestamp=datetime.now(timezone.utc).isoformat()
  226. ).model_dump()
  227. # 验证密码
  228. print(f"🔑 验证密码,哈希格式: {password_hash[:20]}...")
  229. password_valid = verify_password_simple(login_data.password, password_hash)
  230. print(f"🔑 密码验证结果: {password_valid}")
  231. if not password_valid:
  232. print("❌ 密码验证失败")
  233. return ApiResponse(
  234. code=200001,
  235. message="用户名或密码错误",
  236. timestamp=datetime.now(timezone.utc).isoformat()
  237. ).model_dump()
  238. # 生成令牌
  239. print("🎫 生成访问令牌...")
  240. token_data = {
  241. "sub": user_id,
  242. "username": username,
  243. "email": email,
  244. "is_superuser": is_superuser
  245. }
  246. access_token = create_access_token(token_data)
  247. print(f"✅ 令牌生成成功: {access_token[:50]}...")
  248. token_response = TokenResponse(
  249. access_token=access_token,
  250. token_type="Bearer",
  251. expires_in=ACCESS_TOKEN_EXPIRE_MINUTES * 60,
  252. scope="profile email"
  253. )
  254. print("🎉 登录成功")
  255. return ApiResponse(
  256. code=0,
  257. message="登录成功",
  258. data=token_response.model_dump(),
  259. timestamp=datetime.now(timezone.utc).isoformat()
  260. ).model_dump()
  261. except Exception as e:
  262. print(f"❌ 登录错误详情: {type(e).__name__}: {str(e)}")
  263. import traceback
  264. print(f"❌ 错误堆栈: {traceback.format_exc()}")
  265. return ApiResponse(
  266. code=500001,
  267. message="服务器内部错误",
  268. timestamp=datetime.now(timezone.utc).isoformat()
  269. ).model_dump()
  270. @app.get("/api/v1/users/profile")
  271. async def get_user_profile(credentials: HTTPAuthorizationCredentials = Depends(security)):
  272. """获取用户资料"""
  273. try:
  274. # 验证令牌
  275. payload = verify_token(credentials.credentials)
  276. if not payload:
  277. return ApiResponse(
  278. code=200002,
  279. message="无效的访问令牌",
  280. timestamp=datetime.now(timezone.utc).isoformat()
  281. ).model_dump()
  282. user_id = payload.get("sub")
  283. if not user_id:
  284. return ApiResponse(
  285. code=200002,
  286. message="无效的访问令牌",
  287. timestamp=datetime.now(timezone.utc).isoformat()
  288. ).model_dump()
  289. # 获取数据库连接
  290. conn = get_db_connection()
  291. if not conn:
  292. return ApiResponse(
  293. code=500001,
  294. message="数据库连接失败",
  295. timestamp=datetime.now(timezone.utc).isoformat()
  296. ).model_dump()
  297. cursor = conn.cursor()
  298. # 查找用户详细信息
  299. cursor.execute("""
  300. SELECT u.id, u.username, u.email, u.phone, u.avatar_url, u.is_active, u.is_superuser,
  301. u.last_login_at, u.created_at, u.updated_at,
  302. p.real_name, p.company, p.department, p.position
  303. FROM users u
  304. LEFT JOIN user_profiles p ON u.id = p.user_id
  305. WHERE u.id = %s
  306. """, (user_id,))
  307. user_data = cursor.fetchone()
  308. # 获取用户角色
  309. cursor.execute("""
  310. SELECT r.name
  311. FROM user_roles ur
  312. JOIN roles r ON ur.role_id = r.id
  313. WHERE ur.user_id = %s AND ur.is_active = 1
  314. """, (user_id,))
  315. roles = [row[0] for row in cursor.fetchall()]
  316. cursor.close()
  317. conn.close()
  318. if not user_data:
  319. return ApiResponse(
  320. code=200001,
  321. message="用户不存在",
  322. timestamp=datetime.now(timezone.utc).isoformat()
  323. ).model_dump()
  324. # 构建用户信息
  325. user_info = {
  326. "id": user_data[0],
  327. "username": user_data[1],
  328. "email": user_data[2],
  329. "phone": user_data[3],
  330. "avatar_url": user_data[4],
  331. "is_active": user_data[5],
  332. "is_superuser": user_data[6],
  333. "last_login_at": user_data[7].isoformat() if user_data[7] else None,
  334. "created_at": user_data[8].isoformat() if user_data[8] else None,
  335. "updated_at": user_data[9].isoformat() if user_data[9] else None,
  336. "real_name": user_data[10],
  337. "company": user_data[11],
  338. "department": user_data[12],
  339. "position": user_data[13],
  340. "roles": roles
  341. }
  342. return ApiResponse(
  343. code=0,
  344. message="获取用户资料成功",
  345. data=user_info,
  346. timestamp=datetime.now(timezone.utc).isoformat()
  347. ).model_dump()
  348. except Exception as e:
  349. print(f"获取用户资料错误: {e}")
  350. return ApiResponse(
  351. code=500001,
  352. message="服务器内部错误",
  353. timestamp=datetime.now(timezone.utc).isoformat()
  354. ).model_dump()
  355. @app.put("/api/v1/users/profile")
  356. async def update_user_profile(
  357. request: Request,
  358. profile_data: dict,
  359. credentials: HTTPAuthorizationCredentials = Depends(security)
  360. ):
  361. """更新用户资料"""
  362. try:
  363. # 验证令牌
  364. payload = verify_token(credentials.credentials)
  365. if not payload:
  366. return ApiResponse(
  367. code=200002,
  368. message="无效的访问令牌",
  369. timestamp=datetime.now(timezone.utc).isoformat()
  370. ).model_dump()
  371. user_id = payload.get("sub")
  372. # 获取数据库连接
  373. conn = get_db_connection()
  374. if not conn:
  375. return ApiResponse(
  376. code=500001,
  377. message="数据库连接失败",
  378. timestamp=datetime.now(timezone.utc).isoformat()
  379. ).model_dump()
  380. cursor = conn.cursor()
  381. # 更新用户基本信息
  382. update_fields = []
  383. update_values = []
  384. if 'email' in profile_data:
  385. update_fields.append('email = %s')
  386. update_values.append(profile_data['email'])
  387. if 'phone' in profile_data:
  388. update_fields.append('phone = %s')
  389. update_values.append(profile_data['phone'])
  390. if update_fields:
  391. update_values.append(user_id)
  392. cursor.execute(f"""
  393. UPDATE users
  394. SET {', '.join(update_fields)}, updated_at = NOW()
  395. WHERE id = %s
  396. """, update_values)
  397. # 更新或插入用户详情
  398. profile_fields = ['real_name', 'company', 'department', 'position']
  399. profile_updates = {k: v for k, v in profile_data.items() if k in profile_fields}
  400. if profile_updates:
  401. # 检查是否已有记录
  402. cursor.execute("SELECT id FROM user_profiles WHERE user_id = %s", (user_id,))
  403. profile_exists = cursor.fetchone()
  404. if profile_exists:
  405. # 更新现有记录
  406. update_fields = []
  407. update_values = []
  408. for field, value in profile_updates.items():
  409. update_fields.append(f'{field} = %s')
  410. update_values.append(value)
  411. update_values.append(user_id)
  412. cursor.execute(f"""
  413. UPDATE user_profiles
  414. SET {', '.join(update_fields)}, updated_at = NOW()
  415. WHERE user_id = %s
  416. """, update_values)
  417. else:
  418. # 插入新记录
  419. fields = ['user_id'] + list(profile_updates.keys())
  420. values = [user_id] + list(profile_updates.values())
  421. placeholders = ', '.join(['%s'] * len(values))
  422. cursor.execute(f"""
  423. INSERT INTO user_profiles ({', '.join(fields)}, created_at, updated_at)
  424. VALUES ({placeholders}, NOW(), NOW())
  425. """, values)
  426. conn.commit()
  427. cursor.close()
  428. conn.close()
  429. return ApiResponse(
  430. code=0,
  431. message="用户资料更新成功",
  432. timestamp=datetime.now(timezone.utc).isoformat()
  433. ).model_dump()
  434. except Exception as e:
  435. print(f"更新用户资料错误: {e}")
  436. return ApiResponse(
  437. code=500001,
  438. message="服务器内部错误",
  439. timestamp=datetime.now(timezone.utc).isoformat()
  440. ).model_dump()
  441. @app.put("/api/v1/users/password")
  442. async def change_user_password(
  443. request: Request,
  444. password_data: dict,
  445. credentials: HTTPAuthorizationCredentials = Depends(security)
  446. ):
  447. """修改用户密码"""
  448. try:
  449. # 验证令牌
  450. payload = verify_token(credentials.credentials)
  451. if not payload:
  452. return ApiResponse(
  453. code=200002,
  454. message="无效的访问令牌",
  455. timestamp=datetime.now(timezone.utc).isoformat()
  456. ).model_dump()
  457. user_id = payload.get("sub")
  458. old_password = password_data.get('old_password')
  459. new_password = password_data.get('new_password')
  460. if not old_password or not new_password:
  461. return ApiResponse(
  462. code=100001,
  463. message="缺少必要参数",
  464. timestamp=datetime.now(timezone.utc).isoformat()
  465. ).model_dump()
  466. # 获取数据库连接
  467. conn = get_db_connection()
  468. if not conn:
  469. return ApiResponse(
  470. code=500001,
  471. message="数据库连接失败",
  472. timestamp=datetime.now(timezone.utc).isoformat()
  473. ).model_dump()
  474. cursor = conn.cursor()
  475. # 验证当前密码
  476. cursor.execute("SELECT password_hash FROM users WHERE id = %s", (user_id,))
  477. result = cursor.fetchone()
  478. if not result or not verify_password_simple(old_password, result[0]):
  479. cursor.close()
  480. conn.close()
  481. return ApiResponse(
  482. code=200001,
  483. message="当前密码错误",
  484. timestamp=datetime.now(timezone.utc).isoformat()
  485. ).model_dump()
  486. # 生成新密码哈希
  487. new_password_hash = hash_password_simple(new_password)
  488. # 更新密码
  489. cursor.execute("""
  490. UPDATE users
  491. SET password_hash = %s, updated_at = NOW()
  492. WHERE id = %s
  493. """, (new_password_hash, user_id))
  494. conn.commit()
  495. cursor.close()
  496. conn.close()
  497. return ApiResponse(
  498. code=0,
  499. message="密码修改成功",
  500. timestamp=datetime.now(timezone.utc).isoformat()
  501. ).model_dump()
  502. except Exception as e:
  503. print(f"修改密码错误: {e}")
  504. return ApiResponse(
  505. code=500001,
  506. message="服务器内部错误",
  507. timestamp=datetime.now(timezone.utc).isoformat()
  508. ).model_dump()
  509. def hash_password_simple(password):
  510. """简单的密码哈希"""
  511. import hashlib
  512. import secrets
  513. # 生成盐值
  514. salt = secrets.token_hex(16)
  515. # 使用SHA256哈希
  516. password_hash = hashlib.sha256((password + salt).encode()).hexdigest()
  517. return f"sha256${salt}${password_hash}"
  518. @app.post("/api/v1/auth/logout")
  519. async def logout():
  520. """用户登出"""
  521. return ApiResponse(
  522. code=0,
  523. message="登出成功",
  524. timestamp=datetime.now(timezone.utc).isoformat()
  525. ).model_dump()
  526. # OAuth2 授权端点
  527. @app.get("/oauth/authorize")
  528. async def oauth_authorize(
  529. response_type: str,
  530. client_id: str,
  531. redirect_uri: str,
  532. scope: str = "profile",
  533. state: str = None
  534. ):
  535. """OAuth2授权端点"""
  536. try:
  537. print(f"🔐 OAuth授权请求: client_id={client_id}, redirect_uri={redirect_uri}, scope={scope}")
  538. # 验证必要参数
  539. if not response_type or not client_id or not redirect_uri:
  540. error_url = f"{redirect_uri}?error=invalid_request&error_description=Missing required parameters"
  541. if state:
  542. error_url += f"&state={state}"
  543. return {"error": "invalid_request", "redirect_url": error_url}
  544. # 验证response_type
  545. if response_type != "code":
  546. error_url = f"{redirect_uri}?error=unsupported_response_type&error_description=Only authorization code flow is supported"
  547. if state:
  548. error_url += f"&state={state}"
  549. return {"error": "unsupported_response_type", "redirect_url": error_url}
  550. # 获取数据库连接
  551. conn = get_db_connection()
  552. if not conn:
  553. error_url = f"{redirect_uri}?error=server_error&error_description=Database connection failed"
  554. if state:
  555. error_url += f"&state={state}"
  556. return {"error": "server_error", "redirect_url": error_url}
  557. cursor = conn.cursor()
  558. # 验证client_id和redirect_uri
  559. cursor.execute("""
  560. SELECT id, name, redirect_uris, scope, is_active, is_trusted
  561. FROM apps
  562. WHERE app_key = %s AND is_active = 1
  563. """, (client_id,))
  564. app_data = cursor.fetchone()
  565. cursor.close()
  566. conn.close()
  567. if not app_data:
  568. error_url = f"{redirect_uri}?error=invalid_client&error_description=Invalid client_id"
  569. if state:
  570. error_url += f"&state={state}"
  571. return {"error": "invalid_client", "redirect_url": error_url}
  572. app_id, app_name, redirect_uris_json, app_scope_json, is_active, is_trusted = app_data
  573. # 验证redirect_uri
  574. redirect_uris = json.loads(redirect_uris_json) if redirect_uris_json else []
  575. if redirect_uri not in redirect_uris:
  576. error_url = f"{redirect_uri}?error=invalid_request&error_description=Invalid redirect_uri"
  577. if state:
  578. error_url += f"&state={state}"
  579. return {"error": "invalid_request", "redirect_url": error_url}
  580. # 验证scope
  581. app_scopes = json.loads(app_scope_json) if app_scope_json else []
  582. requested_scopes = scope.split() if scope else []
  583. invalid_scopes = [s for s in requested_scopes if s not in app_scopes]
  584. if invalid_scopes:
  585. error_url = f"{redirect_uri}?error=invalid_scope&error_description=Invalid scope: {' '.join(invalid_scopes)}"
  586. if state:
  587. error_url += f"&state={state}"
  588. return {"error": "invalid_scope", "redirect_url": error_url}
  589. # TODO: 检查用户登录状态
  590. # 这里应该检查用户是否已登录(通过session或cookie)
  591. # 如果未登录,应该重定向到登录页面
  592. # 临时方案:返回登录页面,让用户先登录
  593. # 生产环境应该使用session管理
  594. # 构建登录页面URL,登录后返回授权页面
  595. login_page_url = f"/oauth/login?response_type={response_type}&client_id={client_id}&redirect_uri={redirect_uri}&scope={scope}"
  596. if state:
  597. login_page_url += f"&state={state}"
  598. print(f"🔐 需要用户登录,重定向到登录页面: {login_page_url}")
  599. from fastapi.responses import RedirectResponse
  600. return RedirectResponse(url=login_page_url, status_code=302)
  601. # 非受信任应用需要用户授权确认
  602. # 这里返回授权页面HTML
  603. authorization_html = f"""
  604. <!DOCTYPE html>
  605. <html>
  606. <head>
  607. <title>授权确认 - SSO认证中心</title>
  608. <meta charset="utf-8">
  609. <meta name="viewport" content="width=device-width, initial-scale=1">
  610. <style>
  611. body {{
  612. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  613. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  614. margin: 0;
  615. padding: 20px;
  616. min-height: 100vh;
  617. display: flex;
  618. align-items: center;
  619. justify-content: center;
  620. }}
  621. .auth-container {{
  622. background: white;
  623. border-radius: 10px;
  624. padding: 40px;
  625. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
  626. max-width: 400px;
  627. width: 100%;
  628. }}
  629. .auth-header {{
  630. text-align: center;
  631. margin-bottom: 30px;
  632. }}
  633. .auth-header h1 {{
  634. color: #333;
  635. margin-bottom: 10px;
  636. }}
  637. .app-info {{
  638. background: #f8f9fa;
  639. padding: 20px;
  640. border-radius: 8px;
  641. margin-bottom: 20px;
  642. }}
  643. .scope-list {{
  644. list-style: none;
  645. padding: 0;
  646. margin: 10px 0;
  647. }}
  648. .scope-list li {{
  649. padding: 5px 0;
  650. color: #666;
  651. }}
  652. .scope-list li:before {{
  653. content: "✓ ";
  654. color: #28a745;
  655. font-weight: bold;
  656. }}
  657. .auth-buttons {{
  658. display: flex;
  659. gap: 10px;
  660. margin-top: 20px;
  661. }}
  662. .btn {{
  663. flex: 1;
  664. padding: 12px 20px;
  665. border: none;
  666. border-radius: 6px;
  667. font-size: 16px;
  668. cursor: pointer;
  669. text-decoration: none;
  670. text-align: center;
  671. display: inline-block;
  672. }}
  673. .btn-primary {{
  674. background: #007bff;
  675. color: white;
  676. }}
  677. .btn-secondary {{
  678. background: #6c757d;
  679. color: white;
  680. }}
  681. .btn:hover {{
  682. opacity: 0.9;
  683. }}
  684. </style>
  685. </head>
  686. <body>
  687. <div class="auth-container">
  688. <div class="auth-header">
  689. <h1>授权确认</h1>
  690. <p>应用请求访问您的账户</p>
  691. </div>
  692. <div class="app-info">
  693. <h3>{app_name}</h3>
  694. <p>该应用请求以下权限:</p>
  695. <ul class="scope-list">
  696. """
  697. # 添加权限列表
  698. scope_descriptions = {
  699. "profile": "访问您的基本信息(用户名、头像等)",
  700. "email": "访问您的邮箱地址",
  701. "phone": "访问您的手机号码",
  702. "roles": "访问您的角色和权限信息"
  703. }
  704. for scope_item in requested_scopes:
  705. description = scope_descriptions.get(scope_item, f"访问 {scope_item} 信息")
  706. authorization_html += f"<li>{description}</li>"
  707. authorization_html += f"""
  708. </ul>
  709. </div>
  710. <div class="auth-buttons">
  711. <a href="/oauth/authorize/deny?client_id={client_id}&redirect_uri={redirect_uri}&state={state or ''}" class="btn btn-secondary">拒绝</a>
  712. <a href="/oauth/authorize/approve?client_id={client_id}&redirect_uri={redirect_uri}&scope={scope}&state={state or ''}" class="btn btn-primary">授权</a>
  713. </div>
  714. </div>
  715. </body>
  716. </html>
  717. """
  718. from fastapi.responses import HTMLResponse
  719. return HTMLResponse(content=authorization_html)
  720. except Exception as e:
  721. print(f"❌ OAuth授权错误: {e}")
  722. error_url = f"{redirect_uri}?error=server_error&error_description=Internal server error"
  723. if state:
  724. error_url += f"&state={state}"
  725. return {"error": "server_error", "redirect_url": error_url}
  726. @app.get("/oauth/login")
  727. async def oauth_login_page(
  728. response_type: str,
  729. client_id: str,
  730. redirect_uri: str,
  731. scope: str = "profile",
  732. state: str = None
  733. ):
  734. """OAuth2登录页面"""
  735. try:
  736. print(f"🔐 显示OAuth登录页面: client_id={client_id}")
  737. # 获取应用信息
  738. conn = get_db_connection()
  739. if not conn:
  740. return {"error": "server_error", "message": "数据库连接失败"}
  741. cursor = conn.cursor()
  742. cursor.execute("SELECT name FROM apps WHERE app_key = %s", (client_id,))
  743. app_data = cursor.fetchone()
  744. cursor.close()
  745. conn.close()
  746. app_name = app_data[0] if app_data else "未知应用"
  747. # 构建登录页面HTML
  748. login_html = f"""
  749. <!DOCTYPE html>
  750. <html lang="zh-CN">
  751. <head>
  752. <meta charset="UTF-8">
  753. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  754. <title>SSO登录 - {app_name}</title>
  755. <style>
  756. body {{
  757. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  758. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  759. margin: 0;
  760. padding: 20px;
  761. min-height: 100vh;
  762. display: flex;
  763. align-items: center;
  764. justify-content: center;
  765. }}
  766. .login-container {{
  767. background: white;
  768. border-radius: 15px;
  769. padding: 40px;
  770. box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
  771. max-width: 400px;
  772. width: 100%;
  773. }}
  774. .login-header {{
  775. text-align: center;
  776. margin-bottom: 30px;
  777. }}
  778. .login-header h1 {{
  779. color: #333;
  780. margin-bottom: 10px;
  781. }}
  782. .app-info {{
  783. background: #f8f9fa;
  784. padding: 15px;
  785. border-radius: 8px;
  786. margin-bottom: 20px;
  787. text-align: center;
  788. }}
  789. .form-group {{
  790. margin-bottom: 20px;
  791. }}
  792. .form-group label {{
  793. display: block;
  794. margin-bottom: 5px;
  795. font-weight: 500;
  796. color: #333;
  797. }}
  798. .form-group input {{
  799. width: 100%;
  800. padding: 12px;
  801. border: 1px solid #ddd;
  802. border-radius: 6px;
  803. font-size: 16px;
  804. box-sizing: border-box;
  805. }}
  806. .form-group input:focus {{
  807. outline: none;
  808. border-color: #007bff;
  809. box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
  810. }}
  811. .btn {{
  812. width: 100%;
  813. padding: 12px;
  814. background: #007bff;
  815. color: white;
  816. border: none;
  817. border-radius: 6px;
  818. font-size: 16px;
  819. font-weight: 500;
  820. cursor: pointer;
  821. transition: background 0.3s;
  822. }}
  823. .btn:hover {{
  824. background: #0056b3;
  825. }}
  826. .btn:disabled {{
  827. background: #6c757d;
  828. cursor: not-allowed;
  829. }}
  830. .error-message {{
  831. color: #dc3545;
  832. font-size: 14px;
  833. margin-top: 10px;
  834. text-align: center;
  835. }}
  836. .success-message {{
  837. color: #28a745;
  838. font-size: 14px;
  839. margin-top: 10px;
  840. text-align: center;
  841. }}
  842. </style>
  843. </head>
  844. <body>
  845. <div class="login-container">
  846. <div class="login-header">
  847. <h1>🔐 SSO登录</h1>
  848. <p>请登录以继续访问应用</p>
  849. </div>
  850. <div class="app-info">
  851. <strong>{app_name}</strong> 请求访问您的账户
  852. </div>
  853. <form id="loginForm" onsubmit="handleLogin(event)">
  854. <div class="form-group">
  855. <label for="username">用户名或邮箱</label>
  856. <input type="text" id="username" name="username" required>
  857. </div>
  858. <div class="form-group">
  859. <label for="password">密码</label>
  860. <input type="password" id="password" name="password" required>
  861. </div>
  862. <button type="submit" class="btn" id="loginBtn">登录</button>
  863. <div id="message"></div>
  864. </form>
  865. <div style="margin-top: 20px; text-align: center; font-size: 14px; color: #666;">
  866. <p>测试账号: admin / Admin123456</p>
  867. </div>
  868. </div>
  869. <script>
  870. async function handleLogin(event) {{
  871. event.preventDefault();
  872. const loginBtn = document.getElementById('loginBtn');
  873. const messageDiv = document.getElementById('message');
  874. loginBtn.disabled = true;
  875. loginBtn.textContent = '登录中...';
  876. messageDiv.innerHTML = '';
  877. const formData = new FormData(event.target);
  878. const loginData = {{
  879. username: formData.get('username'),
  880. password: formData.get('password'),
  881. remember_me: false
  882. }};
  883. try {{
  884. // 调用登录API
  885. const response = await fetch('/api/v1/auth/login', {{
  886. method: 'POST',
  887. headers: {{
  888. 'Content-Type': 'application/json'
  889. }},
  890. body: JSON.stringify(loginData)
  891. }});
  892. const result = await response.json();
  893. if (result.code === 0) {{
  894. messageDiv.innerHTML = '<div class="success-message">登录成功,正在跳转...</div>';
  895. // 登录成功后,重定向到授权页面
  896. const authUrl = `/oauth/authorize/authenticated?response_type={response_type}&client_id={client_id}&redirect_uri={redirect_uri}&scope={scope}&state={state or ''}&access_token=${{result.data.access_token}}`;
  897. setTimeout(() => {{
  898. window.location.href = authUrl;
  899. }}, 1000);
  900. }} else {{
  901. messageDiv.innerHTML = `<div class="error-message">${{result.message}}</div>`;
  902. }}
  903. }} catch (error) {{
  904. messageDiv.innerHTML = '<div class="error-message">登录失败,请重试</div>';
  905. }} finally {{
  906. loginBtn.disabled = false;
  907. loginBtn.textContent = '登录';
  908. }}
  909. }}
  910. </script>
  911. </body>
  912. </html>
  913. """
  914. from fastapi.responses import HTMLResponse
  915. return HTMLResponse(content=login_html)
  916. except Exception as e:
  917. print(f"❌ OAuth登录页面错误: {e}")
  918. return {"error": "server_error", "message": "服务器内部错误"}
  919. @app.get("/oauth/authorize/authenticated")
  920. async def oauth_authorize_authenticated(
  921. response_type: str,
  922. client_id: str,
  923. redirect_uri: str,
  924. access_token: str,
  925. scope: str = "profile",
  926. state: str = None
  927. ):
  928. """用户已登录后的授权处理"""
  929. try:
  930. print(f"🔐 用户已登录,处理授权: client_id={client_id}")
  931. # 验证访问令牌
  932. payload = verify_token(access_token)
  933. if not payload:
  934. error_url = f"{redirect_uri}?error=invalid_token&error_description=Invalid access token"
  935. if state:
  936. error_url += f"&state={state}"
  937. from fastapi.responses import RedirectResponse
  938. return RedirectResponse(url=error_url, status_code=302)
  939. user_id = payload.get("sub")
  940. username = payload.get("username", "")
  941. print(f"✅ 用户已验证: {username} ({user_id})")
  942. # 获取应用信息
  943. conn = get_db_connection()
  944. if not conn:
  945. error_url = f"{redirect_uri}?error=server_error&error_description=Database connection failed"
  946. if state:
  947. error_url += f"&state={state}"
  948. from fastapi.responses import RedirectResponse
  949. return RedirectResponse(url=error_url, status_code=302)
  950. cursor = conn.cursor()
  951. cursor.execute("SELECT name, is_trusted FROM apps WHERE app_key = %s", (client_id,))
  952. app_data = cursor.fetchone()
  953. cursor.close()
  954. conn.close()
  955. if not app_data:
  956. error_url = f"{redirect_uri}?error=invalid_client&error_description=Invalid client"
  957. if state:
  958. error_url += f"&state={state}"
  959. from fastapi.responses import RedirectResponse
  960. return RedirectResponse(url=error_url, status_code=302)
  961. app_name, is_trusted = app_data
  962. # 如果是受信任应用,直接授权
  963. if is_trusted:
  964. # 生成授权码
  965. auth_code = secrets.token_urlsafe(32)
  966. # TODO: 将授权码存储到数据库,关联用户和应用
  967. # 这里简化处理,实际应该存储到数据库
  968. # 重定向回应用
  969. callback_url = f"{redirect_uri}?code={auth_code}"
  970. if state:
  971. callback_url += f"&state={state}"
  972. print(f"✅ 受信任应用自动授权: {callback_url}")
  973. from fastapi.responses import RedirectResponse
  974. return RedirectResponse(url=callback_url, status_code=302)
  975. # 非受信任应用,显示授权确认页面
  976. # 这里可以返回授权确认页面的HTML
  977. # 为简化,暂时也直接授权
  978. auth_code = secrets.token_urlsafe(32)
  979. callback_url = f"{redirect_uri}?code={auth_code}"
  980. if state:
  981. callback_url += f"&state={state}"
  982. print(f"✅ 用户授权完成: {callback_url}")
  983. from fastapi.responses import RedirectResponse
  984. return RedirectResponse(url=callback_url, status_code=302)
  985. except Exception as e:
  986. print(f"❌ 授权处理错误: {e}")
  987. error_url = f"{redirect_uri}?error=server_error&error_description=Authorization failed"
  988. if state:
  989. error_url += f"&state={state}"
  990. from fastapi.responses import RedirectResponse
  991. return RedirectResponse(url=error_url, status_code=302)
  992. async def oauth_approve(
  993. client_id: str,
  994. redirect_uri: str,
  995. scope: str = "profile",
  996. state: str = None
  997. ):
  998. """用户同意授权"""
  999. try:
  1000. print(f"✅ 用户同意授权: client_id={client_id}")
  1001. # 生成授权码
  1002. auth_code = secrets.token_urlsafe(32)
  1003. # TODO: 将授权码存储到数据库,关联用户和应用
  1004. # 这里简化处理,实际应该:
  1005. # 1. 验证用户登录状态
  1006. # 2. 将授权码存储到数据库
  1007. # 3. 设置过期时间(通常10分钟)
  1008. # 构建回调URL
  1009. callback_url = f"{redirect_uri}?code={auth_code}"
  1010. if state:
  1011. callback_url += f"&state={state}"
  1012. print(f"🔄 重定向到: {callback_url}")
  1013. from fastapi.responses import RedirectResponse
  1014. return RedirectResponse(url=callback_url, status_code=302)
  1015. except Exception as e:
  1016. print(f"❌ 授权确认错误: {e}")
  1017. error_url = f"{redirect_uri}?error=server_error&error_description=Authorization failed"
  1018. if state:
  1019. error_url += f"&state={state}"
  1020. from fastapi.responses import RedirectResponse
  1021. return RedirectResponse(url=error_url, status_code=302)
  1022. @app.get("/oauth/authorize/deny")
  1023. async def oauth_deny(
  1024. client_id: str,
  1025. redirect_uri: str,
  1026. state: str = None
  1027. ):
  1028. """用户拒绝授权"""
  1029. try:
  1030. print(f"❌ 用户拒绝授权: client_id={client_id}")
  1031. # 构建错误回调URL
  1032. error_url = f"{redirect_uri}?error=access_denied&error_description=User denied authorization"
  1033. if state:
  1034. error_url += f"&state={state}"
  1035. from fastapi.responses import RedirectResponse
  1036. return RedirectResponse(url=error_url, status_code=302)
  1037. except Exception as e:
  1038. print(f"❌ 拒绝授权错误: {e}")
  1039. error_url = f"{redirect_uri}?error=server_error&error_description=Authorization failed"
  1040. if state:
  1041. error_url += f"&state={state}"
  1042. from fastapi.responses import RedirectResponse
  1043. return RedirectResponse(url=error_url, status_code=302)
  1044. @app.post("/oauth/token")
  1045. async def oauth_token(request: Request):
  1046. """OAuth2令牌端点"""
  1047. try:
  1048. # 获取请求数据
  1049. form_data = await request.form()
  1050. grant_type = form_data.get("grant_type")
  1051. code = form_data.get("code")
  1052. redirect_uri = form_data.get("redirect_uri")
  1053. client_id = form_data.get("client_id")
  1054. client_secret = form_data.get("client_secret")
  1055. print(f"🎫 令牌请求: grant_type={grant_type}, client_id={client_id}")
  1056. # 验证grant_type
  1057. if grant_type != "authorization_code":
  1058. return {
  1059. "error": "unsupported_grant_type",
  1060. "error_description": "Only authorization_code grant type is supported"
  1061. }
  1062. # 验证必要参数
  1063. if not code or not redirect_uri or not client_id:
  1064. return {
  1065. "error": "invalid_request",
  1066. "error_description": "Missing required parameters"
  1067. }
  1068. # 获取数据库连接
  1069. conn = get_db_connection()
  1070. if not conn:
  1071. return {
  1072. "error": "server_error",
  1073. "error_description": "Database connection failed"
  1074. }
  1075. cursor = conn.cursor()
  1076. # 验证客户端
  1077. cursor.execute("""
  1078. SELECT id, name, app_secret, redirect_uris, scope, is_active
  1079. FROM apps
  1080. WHERE app_key = %s AND is_active = 1
  1081. """, (client_id,))
  1082. app_data = cursor.fetchone()
  1083. if not app_data:
  1084. cursor.close()
  1085. conn.close()
  1086. return {
  1087. "error": "invalid_client",
  1088. "error_description": "Invalid client credentials"
  1089. }
  1090. app_id, app_name, stored_secret, redirect_uris_json, scope_json, is_active = app_data
  1091. # 验证客户端密钥(如果提供了)
  1092. if client_secret and client_secret != stored_secret:
  1093. cursor.close()
  1094. conn.close()
  1095. return {
  1096. "error": "invalid_client",
  1097. "error_description": "Invalid client credentials"
  1098. }
  1099. # 验证redirect_uri
  1100. redirect_uris = json.loads(redirect_uris_json) if redirect_uris_json else []
  1101. if redirect_uri not in redirect_uris:
  1102. cursor.close()
  1103. conn.close()
  1104. return {
  1105. "error": "invalid_grant",
  1106. "error_description": "Invalid redirect_uri"
  1107. }
  1108. # TODO: 验证授权码
  1109. # 这里简化处理,实际应该:
  1110. # 1. 从数据库查找授权码
  1111. # 2. 验证授权码是否有效且未过期
  1112. # 3. 验证授权码是否已被使用
  1113. # 4. 获取关联的用户ID
  1114. # 模拟用户ID(实际应该从授权码记录中获取)
  1115. user_id = "ed6a79d3-0083-4d81-8b48-fc522f686f74" # admin用户ID
  1116. # 生成访问令牌
  1117. token_data = {
  1118. "sub": user_id,
  1119. "client_id": client_id,
  1120. "scope": "profile email"
  1121. }
  1122. access_token = create_access_token(token_data)
  1123. refresh_token = secrets.token_urlsafe(32)
  1124. # TODO: 将令牌存储到数据库
  1125. cursor.close()
  1126. conn.close()
  1127. # 返回令牌响应
  1128. token_response = {
  1129. "access_token": access_token,
  1130. "token_type": "Bearer",
  1131. "expires_in": ACCESS_TOKEN_EXPIRE_MINUTES * 60,
  1132. "refresh_token": refresh_token,
  1133. "scope": "profile email"
  1134. }
  1135. print(f"✅ 令牌生成成功: {access_token[:50]}...")
  1136. return token_response
  1137. except Exception as e:
  1138. print(f"❌ 令牌生成错误: {e}")
  1139. return {
  1140. "error": "server_error",
  1141. "error_description": "Internal server error"
  1142. }
  1143. @app.get("/oauth/userinfo")
  1144. async def oauth_userinfo(credentials: HTTPAuthorizationCredentials = Depends(security)):
  1145. """OAuth2用户信息端点"""
  1146. try:
  1147. # 验证令牌
  1148. payload = verify_token(credentials.credentials)
  1149. if not payload:
  1150. return {
  1151. "error": "invalid_token",
  1152. "error_description": "Invalid or expired access token"
  1153. }
  1154. user_id = payload.get("sub")
  1155. client_id = payload.get("client_id")
  1156. scope = payload.get("scope", "").split()
  1157. print(f"👤 用户信息请求: user_id={user_id}, client_id={client_id}, scope={scope}")
  1158. # 获取数据库连接
  1159. conn = get_db_connection()
  1160. if not conn:
  1161. return {
  1162. "error": "server_error",
  1163. "error_description": "Database connection failed"
  1164. }
  1165. cursor = conn.cursor()
  1166. # 查找用户信息
  1167. cursor.execute("""
  1168. SELECT u.id, u.username, u.email, u.phone, u.avatar_url, u.is_active,
  1169. p.real_name, p.company, p.department, p.position
  1170. FROM users u
  1171. LEFT JOIN user_profiles p ON u.id = p.user_id
  1172. WHERE u.id = %s AND u.is_active = 1
  1173. """, (user_id,))
  1174. user_data = cursor.fetchone()
  1175. cursor.close()
  1176. conn.close()
  1177. if not user_data:
  1178. return {
  1179. "error": "invalid_token",
  1180. "error_description": "User not found or inactive"
  1181. }
  1182. # 构建用户信息响应(根据scope过滤)
  1183. user_info = {"sub": user_data[0]}
  1184. if "profile" in scope:
  1185. user_info.update({
  1186. "username": user_data[1],
  1187. "avatar_url": user_data[4],
  1188. "real_name": user_data[6],
  1189. "company": user_data[7],
  1190. "department": user_data[8],
  1191. "position": user_data[9]
  1192. })
  1193. if "email" in scope:
  1194. user_info["email"] = user_data[2]
  1195. if "phone" in scope:
  1196. user_info["phone"] = user_data[3]
  1197. print(f"✅ 返回用户信息: {user_info}")
  1198. return user_info
  1199. except Exception as e:
  1200. print(f"❌ 获取用户信息错误: {e}")
  1201. return {
  1202. "error": "server_error",
  1203. "error_description": "Internal server error"
  1204. }
  1205. @app.get("/api/v1/apps")
  1206. async def get_apps(
  1207. page: int = 1,
  1208. page_size: int = 20,
  1209. keyword: str = "",
  1210. status: str = "",
  1211. credentials: HTTPAuthorizationCredentials = Depends(security)
  1212. ):
  1213. """获取应用列表"""
  1214. try:
  1215. # 验证令牌
  1216. payload = verify_token(credentials.credentials)
  1217. if not payload:
  1218. return ApiResponse(
  1219. code=200002,
  1220. message="无效的访问令牌",
  1221. timestamp=datetime.now(timezone.utc).isoformat()
  1222. ).model_dump()
  1223. user_id = payload.get("sub")
  1224. # 获取数据库连接
  1225. conn = get_db_connection()
  1226. if not conn:
  1227. return ApiResponse(
  1228. code=500001,
  1229. message="数据库连接失败",
  1230. timestamp=datetime.now(timezone.utc).isoformat()
  1231. ).model_dump()
  1232. cursor = conn.cursor()
  1233. # 检查用户角色,决定是否显示所有应用
  1234. cursor.execute("""
  1235. SELECT COUNT(*) FROM user_roles ur
  1236. JOIN roles r ON ur.role_id = r.id
  1237. WHERE ur.user_id = %s AND r.name IN ('super_admin', 'admin', 'app_manager') AND ur.is_active = 1
  1238. """, (user_id,))
  1239. is_app_manager = cursor.fetchone()[0] > 0
  1240. # 构建查询条件
  1241. where_conditions = []
  1242. params = []
  1243. # 如果不是应用管理员,只显示自己创建的应用
  1244. if not is_app_manager:
  1245. where_conditions.append("created_by = %s")
  1246. params.append(user_id)
  1247. if keyword:
  1248. where_conditions.append("(name LIKE %s OR description LIKE %s)")
  1249. params.extend([f"%{keyword}%", f"%{keyword}%"])
  1250. if status == "active":
  1251. where_conditions.append("is_active = 1")
  1252. elif status == "inactive":
  1253. where_conditions.append("is_active = 0")
  1254. where_clause = " AND ".join(where_conditions) if where_conditions else "1=1"
  1255. # 查询总数
  1256. cursor.execute(f"SELECT COUNT(*) FROM apps WHERE {where_clause}", params)
  1257. total = cursor.fetchone()[0]
  1258. # 查询应用列表
  1259. offset = (page - 1) * page_size
  1260. cursor.execute(f"""
  1261. SELECT id, name, app_key, description, icon_url, redirect_uris, scope,
  1262. is_active, is_trusted, access_token_expires, refresh_token_expires,
  1263. created_at, updated_at
  1264. FROM apps
  1265. WHERE {where_clause}
  1266. ORDER BY created_at DESC
  1267. LIMIT %s OFFSET %s
  1268. """, params + [page_size, offset])
  1269. apps = []
  1270. for row in cursor.fetchall():
  1271. app = {
  1272. "id": row[0],
  1273. "name": row[1],
  1274. "app_key": row[2],
  1275. "description": row[3],
  1276. "icon_url": row[4],
  1277. "redirect_uris": json.loads(row[5]) if row[5] else [],
  1278. "scope": json.loads(row[6]) if row[6] else [],
  1279. "is_active": bool(row[7]),
  1280. "is_trusted": bool(row[8]),
  1281. "access_token_expires": row[9],
  1282. "refresh_token_expires": row[10],
  1283. "created_at": row[11].isoformat() if row[11] else None,
  1284. "updated_at": row[12].isoformat() if row[12] else None,
  1285. # 模拟统计数据
  1286. "today_requests": secrets.randbelow(1000),
  1287. "active_users": secrets.randbelow(100)
  1288. }
  1289. apps.append(app)
  1290. cursor.close()
  1291. conn.close()
  1292. return ApiResponse(
  1293. code=0,
  1294. message="获取应用列表成功",
  1295. data={
  1296. "items": apps,
  1297. "total": total,
  1298. "page": page,
  1299. "page_size": page_size
  1300. },
  1301. timestamp=datetime.now(timezone.utc).isoformat()
  1302. ).model_dump()
  1303. except Exception as e:
  1304. print(f"获取应用列表错误: {e}")
  1305. return ApiResponse(
  1306. code=500001,
  1307. message="服务器内部错误",
  1308. timestamp=datetime.now(timezone.utc).isoformat()
  1309. ).model_dump()
  1310. @app.get("/api/v1/apps/{app_id}")
  1311. async def get_app_detail(
  1312. app_id: str,
  1313. credentials: HTTPAuthorizationCredentials = Depends(security)
  1314. ):
  1315. """获取应用详情(包含密钥)"""
  1316. try:
  1317. # 验证令牌
  1318. payload = verify_token(credentials.credentials)
  1319. if not payload:
  1320. return ApiResponse(
  1321. code=200002,
  1322. message="无效的访问令牌",
  1323. timestamp=datetime.now(timezone.utc).isoformat()
  1324. ).model_dump()
  1325. user_id = payload.get("sub")
  1326. # 获取数据库连接
  1327. conn = get_db_connection()
  1328. if not conn:
  1329. return ApiResponse(
  1330. code=500001,
  1331. message="数据库连接失败",
  1332. timestamp=datetime.now(timezone.utc).isoformat()
  1333. ).model_dump()
  1334. cursor = conn.cursor()
  1335. # 查询应用详情(包含密钥)
  1336. cursor.execute("""
  1337. SELECT id, name, app_key, app_secret, description, icon_url,
  1338. redirect_uris, scope, is_active, is_trusted,
  1339. access_token_expires, refresh_token_expires,
  1340. created_at, updated_at
  1341. FROM apps
  1342. WHERE id = %s AND created_by = %s
  1343. """, (app_id, user_id))
  1344. app_data = cursor.fetchone()
  1345. cursor.close()
  1346. conn.close()
  1347. if not app_data:
  1348. return ApiResponse(
  1349. code=200001,
  1350. message="应用不存在或无权限",
  1351. timestamp=datetime.now(timezone.utc).isoformat()
  1352. ).model_dump()
  1353. app_detail = {
  1354. "id": app_data[0],
  1355. "name": app_data[1],
  1356. "app_key": app_data[2],
  1357. "app_secret": app_data[3],
  1358. "description": app_data[4],
  1359. "icon_url": app_data[5],
  1360. "redirect_uris": json.loads(app_data[6]) if app_data[6] else [],
  1361. "scope": json.loads(app_data[7]) if app_data[7] else [],
  1362. "is_active": bool(app_data[8]),
  1363. "is_trusted": bool(app_data[9]),
  1364. "access_token_expires": app_data[10],
  1365. "refresh_token_expires": app_data[11],
  1366. "created_at": app_data[12].isoformat() if app_data[12] else None,
  1367. "updated_at": app_data[13].isoformat() if app_data[13] else None
  1368. }
  1369. return ApiResponse(
  1370. code=0,
  1371. message="获取应用详情成功",
  1372. data=app_detail,
  1373. timestamp=datetime.now(timezone.utc).isoformat()
  1374. ).model_dump()
  1375. except Exception as e:
  1376. print(f"获取应用详情错误: {e}")
  1377. return ApiResponse(
  1378. code=500001,
  1379. message="服务器内部错误",
  1380. timestamp=datetime.now(timezone.utc).isoformat()
  1381. ).model_dump()
  1382. @app.post("/api/v1/apps")
  1383. async def create_app(
  1384. request: Request,
  1385. app_data: dict,
  1386. credentials: HTTPAuthorizationCredentials = Depends(security)
  1387. ):
  1388. """创建应用"""
  1389. try:
  1390. # 验证令牌
  1391. payload = verify_token(credentials.credentials)
  1392. if not payload:
  1393. return ApiResponse(
  1394. code=200002,
  1395. message="无效的访问令牌",
  1396. timestamp=datetime.now(timezone.utc).isoformat()
  1397. ).model_dump()
  1398. user_id = payload.get("sub")
  1399. # 验证必要字段
  1400. if not app_data.get('name') or not app_data.get('redirect_uris'):
  1401. return ApiResponse(
  1402. code=100001,
  1403. message="缺少必要参数",
  1404. timestamp=datetime.now(timezone.utc).isoformat()
  1405. ).model_dump()
  1406. # 获取数据库连接
  1407. conn = get_db_connection()
  1408. if not conn:
  1409. return ApiResponse(
  1410. code=500001,
  1411. message="数据库连接失败",
  1412. timestamp=datetime.now(timezone.utc).isoformat()
  1413. ).model_dump()
  1414. cursor = conn.cursor()
  1415. # 生成应用ID和密钥
  1416. app_id = str(uuid.uuid4())
  1417. app_key = generate_random_string(32)
  1418. app_secret = generate_random_string(64)
  1419. # 插入应用记录
  1420. cursor.execute("""
  1421. INSERT INTO apps (
  1422. id, name, app_key, app_secret, description, icon_url,
  1423. redirect_uris, scope, is_active, is_trusted,
  1424. access_token_expires, refresh_token_expires, created_by,
  1425. created_at, updated_at
  1426. ) VALUES (
  1427. %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()
  1428. )
  1429. """, (
  1430. app_id,
  1431. app_data['name'],
  1432. app_key,
  1433. app_secret,
  1434. app_data.get('description', ''),
  1435. app_data.get('icon_url', ''),
  1436. json.dumps(app_data['redirect_uris']),
  1437. json.dumps(app_data.get('scope', ['profile'])),
  1438. True,
  1439. app_data.get('is_trusted', False),
  1440. app_data.get('access_token_expires', 7200),
  1441. app_data.get('refresh_token_expires', 2592000),
  1442. user_id
  1443. ))
  1444. conn.commit()
  1445. cursor.close()
  1446. conn.close()
  1447. # 返回创建的应用信息
  1448. app_info = {
  1449. "id": app_id,
  1450. "name": app_data['name'],
  1451. "app_key": app_key,
  1452. "app_secret": app_secret,
  1453. "description": app_data.get('description', ''),
  1454. "redirect_uris": app_data['redirect_uris'],
  1455. "scope": app_data.get('scope', ['profile']),
  1456. "is_active": True,
  1457. "is_trusted": app_data.get('is_trusted', False)
  1458. }
  1459. return ApiResponse(
  1460. code=0,
  1461. message="应用创建成功",
  1462. data=app_info,
  1463. timestamp=datetime.now(timezone.utc).isoformat()
  1464. ).model_dump()
  1465. except Exception as e:
  1466. print(f"创建应用错误: {e}")
  1467. return ApiResponse(
  1468. code=500001,
  1469. message="服务器内部错误",
  1470. timestamp=datetime.now(timezone.utc).isoformat()
  1471. ).model_dump()
  1472. @app.put("/api/v1/apps/{app_id}/status")
  1473. async def toggle_app_status(
  1474. app_id: str,
  1475. status_data: dict,
  1476. credentials: HTTPAuthorizationCredentials = Depends(security)
  1477. ):
  1478. """切换应用状态"""
  1479. try:
  1480. # 验证令牌
  1481. payload = verify_token(credentials.credentials)
  1482. if not payload:
  1483. return ApiResponse(
  1484. code=200002,
  1485. message="无效的访问令牌",
  1486. timestamp=datetime.now(timezone.utc).isoformat()
  1487. ).model_dump()
  1488. user_id = payload.get("sub")
  1489. is_active = status_data.get('is_active')
  1490. if is_active is None:
  1491. return ApiResponse(
  1492. code=100001,
  1493. message="缺少必要参数",
  1494. timestamp=datetime.now(timezone.utc).isoformat()
  1495. ).model_dump()
  1496. # 获取数据库连接
  1497. conn = get_db_connection()
  1498. if not conn:
  1499. return ApiResponse(
  1500. code=500001,
  1501. message="数据库连接失败",
  1502. timestamp=datetime.now(timezone.utc).isoformat()
  1503. ).model_dump()
  1504. cursor = conn.cursor()
  1505. # 检查应用是否存在且属于当前用户
  1506. cursor.execute("""
  1507. SELECT id, name FROM apps
  1508. WHERE id = %s AND created_by = %s
  1509. """, (app_id, user_id))
  1510. app_data = cursor.fetchone()
  1511. if not app_data:
  1512. cursor.close()
  1513. conn.close()
  1514. return ApiResponse(
  1515. code=200001,
  1516. message="应用不存在或无权限",
  1517. timestamp=datetime.now(timezone.utc).isoformat()
  1518. ).model_dump()
  1519. # 更新应用状态
  1520. cursor.execute("""
  1521. UPDATE apps
  1522. SET is_active = %s, updated_at = NOW()
  1523. WHERE id = %s
  1524. """, (is_active, app_id))
  1525. conn.commit()
  1526. cursor.close()
  1527. conn.close()
  1528. action = "启用" if is_active else "禁用"
  1529. print(f"✅ 应用状态已更新: {app_data[1]} -> {action}")
  1530. return ApiResponse(
  1531. code=0,
  1532. message=f"应用已{action}",
  1533. timestamp=datetime.now(timezone.utc).isoformat()
  1534. ).model_dump()
  1535. except Exception as e:
  1536. print(f"切换应用状态错误: {e}")
  1537. return ApiResponse(
  1538. code=500001,
  1539. message="服务器内部错误",
  1540. timestamp=datetime.now(timezone.utc).isoformat()
  1541. ).model_dump()
  1542. @app.put("/api/v1/apps/{app_id}")
  1543. async def update_app(
  1544. app_id: str,
  1545. app_data: dict,
  1546. credentials: HTTPAuthorizationCredentials = Depends(security)
  1547. ):
  1548. """更新应用信息"""
  1549. try:
  1550. # 验证令牌
  1551. payload = verify_token(credentials.credentials)
  1552. if not payload:
  1553. return ApiResponse(
  1554. code=200002,
  1555. message="无效的访问令牌",
  1556. timestamp=datetime.now(timezone.utc).isoformat()
  1557. ).model_dump()
  1558. user_id = payload.get("sub")
  1559. # 验证必要参数
  1560. name = app_data.get('name', '').strip()
  1561. if not name:
  1562. return ApiResponse(
  1563. code=100001,
  1564. message="应用名称不能为空",
  1565. timestamp=datetime.now(timezone.utc).isoformat()
  1566. ).model_dump()
  1567. # 获取数据库连接
  1568. conn = get_db_connection()
  1569. if not conn:
  1570. return ApiResponse(
  1571. code=500001,
  1572. message="数据库连接失败",
  1573. timestamp=datetime.now(timezone.utc).isoformat()
  1574. ).model_dump()
  1575. cursor = conn.cursor()
  1576. # 检查应用是否存在且属于当前用户
  1577. cursor.execute("""
  1578. SELECT id, name FROM apps
  1579. WHERE id = %s AND created_by = %s
  1580. """, (app_id, user_id))
  1581. existing_app = cursor.fetchone()
  1582. if not existing_app:
  1583. cursor.close()
  1584. conn.close()
  1585. return ApiResponse(
  1586. code=200001,
  1587. message="应用不存在或无权限",
  1588. timestamp=datetime.now(timezone.utc).isoformat()
  1589. ).model_dump()
  1590. # 检查应用名称是否已被其他应用使用
  1591. cursor.execute("""
  1592. SELECT id FROM apps
  1593. WHERE name = %s AND created_by = %s AND id != %s
  1594. """, (name, user_id, app_id))
  1595. if cursor.fetchone():
  1596. cursor.close()
  1597. conn.close()
  1598. return ApiResponse(
  1599. code=200001,
  1600. message="应用名称已存在",
  1601. timestamp=datetime.now(timezone.utc).isoformat()
  1602. ).model_dump()
  1603. # 准备更新数据
  1604. description = (app_data.get('description') or '').strip()
  1605. icon_url = (app_data.get('icon_url') or '').strip()
  1606. redirect_uris = app_data.get('redirect_uris', [])
  1607. scope = app_data.get('scope', ['profile', 'email'])
  1608. is_trusted = app_data.get('is_trusted', False)
  1609. access_token_expires = app_data.get('access_token_expires', 7200)
  1610. refresh_token_expires = app_data.get('refresh_token_expires', 2592000)
  1611. # 验证回调URL
  1612. if not redirect_uris or not isinstance(redirect_uris, list):
  1613. cursor.close()
  1614. conn.close()
  1615. return ApiResponse(
  1616. code=100001,
  1617. message="至少需要一个回调URL",
  1618. timestamp=datetime.now(timezone.utc).isoformat()
  1619. ).model_dump()
  1620. # 验证权限范围
  1621. if not scope or not isinstance(scope, list):
  1622. scope = ['profile', 'email']
  1623. # 更新应用信息
  1624. cursor.execute("""
  1625. UPDATE apps
  1626. SET name = %s, description = %s, icon_url = %s,
  1627. redirect_uris = %s, scope = %s, is_trusted = %s,
  1628. access_token_expires = %s, refresh_token_expires = %s,
  1629. updated_at = NOW()
  1630. WHERE id = %s
  1631. """, (
  1632. name, description, icon_url,
  1633. json.dumps(redirect_uris), json.dumps(scope), is_trusted,
  1634. access_token_expires, refresh_token_expires, app_id
  1635. ))
  1636. conn.commit()
  1637. # 获取更新后的应用信息
  1638. cursor.execute("""
  1639. SELECT id, name, app_key, description, icon_url,
  1640. redirect_uris, scope, is_active, is_trusted,
  1641. access_token_expires, refresh_token_expires,
  1642. created_at, updated_at
  1643. FROM apps
  1644. WHERE id = %s
  1645. """, (app_id,))
  1646. app_info = cursor.fetchone()
  1647. cursor.close()
  1648. conn.close()
  1649. if app_info:
  1650. app_result = {
  1651. "id": app_info[0],
  1652. "name": app_info[1],
  1653. "app_key": app_info[2],
  1654. "description": app_info[3],
  1655. "icon_url": app_info[4],
  1656. "redirect_uris": json.loads(app_info[5]) if app_info[5] else [],
  1657. "scope": json.loads(app_info[6]) if app_info[6] else [],
  1658. "is_active": bool(app_info[7]),
  1659. "is_trusted": bool(app_info[8]),
  1660. "access_token_expires": app_info[9],
  1661. "refresh_token_expires": app_info[10],
  1662. "created_at": app_info[11].isoformat() if app_info[11] else None,
  1663. "updated_at": app_info[12].isoformat() if app_info[12] else None
  1664. }
  1665. print(f"✅ 应用已更新: {name}")
  1666. return ApiResponse(
  1667. code=0,
  1668. message="应用更新成功",
  1669. data=app_result,
  1670. timestamp=datetime.now(timezone.utc).isoformat()
  1671. ).model_dump()
  1672. else:
  1673. return ApiResponse(
  1674. code=500001,
  1675. message="获取更新后的应用信息失败",
  1676. timestamp=datetime.now(timezone.utc).isoformat()
  1677. ).model_dump()
  1678. except Exception as e:
  1679. print(f"更新应用错误: {e}")
  1680. return ApiResponse(
  1681. code=500001,
  1682. message="服务器内部错误",
  1683. timestamp=datetime.now(timezone.utc).isoformat()
  1684. ).model_dump()
  1685. @app.delete("/api/v1/apps/{app_id}")
  1686. async def delete_app(
  1687. app_id: str,
  1688. credentials: HTTPAuthorizationCredentials = Depends(security)
  1689. ):
  1690. """删除应用"""
  1691. try:
  1692. # 验证令牌
  1693. payload = verify_token(credentials.credentials)
  1694. if not payload:
  1695. return ApiResponse(
  1696. code=200002,
  1697. message="无效的访问令牌",
  1698. timestamp=datetime.now(timezone.utc).isoformat()
  1699. ).model_dump()
  1700. user_id = payload.get("sub")
  1701. # 获取数据库连接
  1702. conn = get_db_connection()
  1703. if not conn:
  1704. return ApiResponse(
  1705. code=500001,
  1706. message="数据库连接失败",
  1707. timestamp=datetime.now(timezone.utc).isoformat()
  1708. ).model_dump()
  1709. cursor = conn.cursor()
  1710. # 检查应用是否存在且属于当前用户
  1711. cursor.execute("""
  1712. SELECT id, name FROM apps
  1713. WHERE id = %s AND created_by = %s
  1714. """, (app_id, user_id))
  1715. app_data = cursor.fetchone()
  1716. if not app_data:
  1717. cursor.close()
  1718. conn.close()
  1719. return ApiResponse(
  1720. code=200001,
  1721. message="应用不存在或无权限",
  1722. timestamp=datetime.now(timezone.utc).isoformat()
  1723. ).model_dump()
  1724. # 删除应用(级联删除相关数据)
  1725. cursor.execute("DELETE FROM apps WHERE id = %s", (app_id,))
  1726. conn.commit()
  1727. cursor.close()
  1728. conn.close()
  1729. print(f"✅ 应用已删除: {app_data[1]}")
  1730. return ApiResponse(
  1731. code=0,
  1732. message="应用已删除",
  1733. timestamp=datetime.now(timezone.utc).isoformat()
  1734. ).model_dump()
  1735. except Exception as e:
  1736. print(f"删除应用错误: {e}")
  1737. return ApiResponse(
  1738. code=500001,
  1739. message="服务器内部错误",
  1740. timestamp=datetime.now(timezone.utc).isoformat()
  1741. ).model_dump()
  1742. @app.post("/api/v1/apps/{app_id}/reset-secret")
  1743. async def reset_app_secret(
  1744. app_id: str,
  1745. credentials: HTTPAuthorizationCredentials = Depends(security)
  1746. ):
  1747. """重置应用密钥"""
  1748. try:
  1749. # 验证令牌
  1750. payload = verify_token(credentials.credentials)
  1751. if not payload:
  1752. return ApiResponse(
  1753. code=200002,
  1754. message="无效的访问令牌",
  1755. timestamp=datetime.now(timezone.utc).isoformat()
  1756. ).model_dump()
  1757. user_id = payload.get("sub")
  1758. # 获取数据库连接
  1759. conn = get_db_connection()
  1760. if not conn:
  1761. return ApiResponse(
  1762. code=500001,
  1763. message="数据库连接失败",
  1764. timestamp=datetime.now(timezone.utc).isoformat()
  1765. ).model_dump()
  1766. cursor = conn.cursor()
  1767. # 检查应用是否存在且属于当前用户
  1768. cursor.execute("""
  1769. SELECT id, name FROM apps
  1770. WHERE id = %s AND created_by = %s
  1771. """, (app_id, user_id))
  1772. app_data = cursor.fetchone()
  1773. if not app_data:
  1774. cursor.close()
  1775. conn.close()
  1776. return ApiResponse(
  1777. code=200001,
  1778. message="应用不存在或无权限",
  1779. timestamp=datetime.now(timezone.utc).isoformat()
  1780. ).model_dump()
  1781. # 生成新的应用密钥
  1782. new_secret = generate_random_string(64)
  1783. # 更新应用密钥
  1784. cursor.execute("""
  1785. UPDATE apps
  1786. SET app_secret = %s, updated_at = NOW()
  1787. WHERE id = %s
  1788. """, (new_secret, app_id))
  1789. conn.commit()
  1790. cursor.close()
  1791. conn.close()
  1792. print(f"✅ 应用密钥已重置: {app_data[1]}")
  1793. return ApiResponse(
  1794. code=0,
  1795. message="应用密钥已重置",
  1796. data={"app_secret": new_secret},
  1797. timestamp=datetime.now(timezone.utc).isoformat()
  1798. ).model_dump()
  1799. except Exception as e:
  1800. print(f"重置应用密钥错误: {e}")
  1801. return ApiResponse(
  1802. code=500001,
  1803. message="服务器内部错误",
  1804. timestamp=datetime.now(timezone.utc).isoformat()
  1805. ).model_dump()
  1806. def generate_random_string(length=32):
  1807. """生成随机字符串"""
  1808. import secrets
  1809. import string
  1810. alphabet = string.ascii_letters + string.digits
  1811. return ''.join(secrets.choice(alphabet) for _ in range(length))
  1812. """获取验证码"""
  1813. try:
  1814. # 生成验证码
  1815. captcha_text, captcha_image = generate_captcha()
  1816. # 这里应该将验证码文本存储到缓存中(Redis或内存)
  1817. # 为了简化,我们暂时返回固定的验证码
  1818. captcha_id = secrets.token_hex(16)
  1819. return ApiResponse(
  1820. code=0,
  1821. message="获取验证码成功",
  1822. data={
  1823. "captcha_id": captcha_id,
  1824. "captcha_image": captcha_image,
  1825. "captcha_text": captcha_text # 生产环境中不应该返回这个
  1826. },
  1827. timestamp=datetime.now(timezone.utc).isoformat()
  1828. ).model_dump()
  1829. except Exception as e:
  1830. print(f"生成验证码错误: {e}")
  1831. return ApiResponse(
  1832. code=500001,
  1833. message="生成验证码失败",
  1834. timestamp=datetime.now(timezone.utc).isoformat()
  1835. ).model_dump()
  1836. def generate_captcha():
  1837. """生成验证码"""
  1838. try:
  1839. from PIL import Image, ImageDraw, ImageFont
  1840. import io
  1841. import base64
  1842. import random
  1843. import string
  1844. # 生成随机验证码文本
  1845. captcha_text = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
  1846. # 创建图片
  1847. width, height = 120, 40
  1848. image = Image.new('RGB', (width, height), color='white')
  1849. draw = ImageDraw.Draw(image)
  1850. # 尝试使用系统字体,如果失败则使用默认字体
  1851. try:
  1852. # Windows系统字体
  1853. font = ImageFont.truetype("arial.ttf", 20)
  1854. except:
  1855. try:
  1856. # 备用字体
  1857. font = ImageFont.truetype("C:/Windows/Fonts/arial.ttf", 20)
  1858. except:
  1859. # 使用默认字体
  1860. font = ImageFont.load_default()
  1861. # 绘制验证码文本
  1862. text_width = draw.textlength(captcha_text, font=font)
  1863. text_height = 20
  1864. x = (width - text_width) // 2
  1865. y = (height - text_height) // 2
  1866. # 添加一些随机颜色
  1867. colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD']
  1868. text_color = random.choice(colors)
  1869. draw.text((x, y), captcha_text, fill=text_color, font=font)
  1870. # 添加一些干扰线
  1871. for _ in range(3):
  1872. x1 = random.randint(0, width)
  1873. y1 = random.randint(0, height)
  1874. x2 = random.randint(0, width)
  1875. y2 = random.randint(0, height)
  1876. draw.line([(x1, y1), (x2, y2)], fill=random.choice(colors), width=1)
  1877. # 添加一些干扰点
  1878. for _ in range(20):
  1879. x = random.randint(0, width)
  1880. y = random.randint(0, height)
  1881. draw.point((x, y), fill=random.choice(colors))
  1882. # 转换为base64
  1883. buffer = io.BytesIO()
  1884. image.save(buffer, format='PNG')
  1885. image_data = buffer.getvalue()
  1886. image_base64 = base64.b64encode(image_data).decode('utf-8')
  1887. return captcha_text, f"data:image/png;base64,{image_base64}"
  1888. except ImportError:
  1889. # 如果PIL不可用,返回简单的文本验证码
  1890. captcha_text = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
  1891. # 创建一个简单的SVG验证码
  1892. svg_captcha = f"""
  1893. <svg width="120" height="40" xmlns="http://www.w3.org/2000/svg">
  1894. <rect width="120" height="40" fill="#f0f0f0" stroke="#ccc"/>
  1895. <text x="60" y="25" font-family="Arial" font-size="18" text-anchor="middle" fill="#333">{captcha_text}</text>
  1896. </svg>
  1897. """
  1898. svg_base64 = base64.b64encode(svg_captcha.encode('utf-8')).decode('utf-8')
  1899. return captcha_text, f"data:image/svg+xml;base64,{svg_base64}"
  1900. except Exception as e:
  1901. print(f"生成验证码图片失败: {e}")
  1902. # 返回默认验证码
  1903. return "1234", "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIwIiBoZWlnaHQ9IjQwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMjAiIGhlaWdodD0iNDAiIGZpbGw9IiNmMGYwZjAiIHN0cm9rZT0iI2NjYyIvPjx0ZXh0IHg9IjYwIiB5PSIyNSIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjE4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMzMzIj4xMjM0PC90ZXh0Pjwvc3ZnPg=="
  1904. # RBAC权限管理API
  1905. @app.get("/api/v1/user/menus")
  1906. async def api_get_user_menus(credentials: HTTPAuthorizationCredentials = Depends(security)):
  1907. """获取用户菜单"""
  1908. try:
  1909. payload = verify_token(credentials.credentials)
  1910. if not payload:
  1911. return ApiResponse(
  1912. code=401,
  1913. message="无效的访问令牌",
  1914. timestamp=datetime.now(timezone.utc).isoformat()
  1915. ).model_dump()
  1916. user_id = payload.get("sub")
  1917. conn = get_db_connection()
  1918. if not conn:
  1919. return ApiResponse(
  1920. code=500,
  1921. message="数据库连接失败",
  1922. timestamp=datetime.now(timezone.utc).isoformat()
  1923. ).model_dump()
  1924. cursor = conn.cursor()
  1925. # 检查用户是否是超级管理员
  1926. cursor.execute("""
  1927. SELECT COUNT(*) FROM user_roles ur
  1928. JOIN roles r ON ur.role_id = r.id
  1929. WHERE ur.user_id = %s AND r.name = 'super_admin' AND ur.is_active = 1
  1930. """, (user_id,))
  1931. is_super_admin = cursor.fetchone()[0] > 0
  1932. if is_super_admin:
  1933. # 超级管理员返回所有活跃菜单
  1934. cursor.execute("""
  1935. SELECT m.id, m.parent_id, m.name, m.title, m.path,
  1936. m.component, m.icon, m.sort_order, m.menu_type,
  1937. m.is_hidden, m.is_active
  1938. FROM menus m
  1939. WHERE m.is_active = 1
  1940. ORDER BY m.sort_order
  1941. """)
  1942. else:
  1943. # 普通用户根据角色权限获取菜单
  1944. cursor.execute("""
  1945. SELECT m.id, m.parent_id, m.name, m.title, m.path,
  1946. m.component, m.icon, m.sort_order, m.menu_type,
  1947. m.is_hidden, m.is_active
  1948. FROM menus m
  1949. JOIN role_menus rm ON m.id = rm.menu_id
  1950. JOIN user_roles ur ON rm.role_id = ur.role_id
  1951. WHERE ur.user_id = %s
  1952. AND ur.is_active = 1
  1953. AND m.is_active = 1
  1954. GROUP BY m.id, m.parent_id, m.name, m.title, m.path,
  1955. m.component, m.icon, m.sort_order, m.menu_type,
  1956. m.is_hidden, m.is_active
  1957. ORDER BY m.sort_order
  1958. """, (user_id,))
  1959. menus = []
  1960. for row in cursor.fetchall():
  1961. menu = {
  1962. "id": row[0],
  1963. "parent_id": row[1],
  1964. "name": row[2],
  1965. "title": row[3],
  1966. "path": row[4],
  1967. "component": row[5],
  1968. "icon": row[6],
  1969. "sort_order": row[7],
  1970. "menu_type": row[8],
  1971. "is_hidden": bool(row[9]),
  1972. "is_active": bool(row[10]),
  1973. "children": []
  1974. }
  1975. menus.append(menu)
  1976. # 构建菜单树
  1977. menu_tree = build_menu_tree(menus)
  1978. cursor.close()
  1979. conn.close()
  1980. return ApiResponse(
  1981. code=0,
  1982. message="获取用户菜单成功",
  1983. data=menu_tree,
  1984. timestamp=datetime.now(timezone.utc).isoformat()
  1985. ).model_dump()
  1986. except Exception as e:
  1987. print(f"获取用户菜单错误: {e}")
  1988. return ApiResponse(
  1989. code=500,
  1990. message="服务器内部错误",
  1991. timestamp=datetime.now(timezone.utc).isoformat()
  1992. ).model_dump()
  1993. def build_menu_tree(menus):
  1994. """构建菜单树结构"""
  1995. menu_map = {menu["id"]: menu for menu in menus}
  1996. tree = []
  1997. for menu in menus:
  1998. if menu["parent_id"] is None:
  1999. tree.append(menu)
  2000. else:
  2001. parent = menu_map.get(menu["parent_id"])
  2002. if parent:
  2003. parent["children"].append(menu)
  2004. return tree
  2005. @app.get("/api/v1/admin/menus")
  2006. async def api_get_all_menus(
  2007. page: int = 1,
  2008. page_size: int = 1000, # 增大默认页面大小,确保返回所有菜单
  2009. keyword: Optional[str] = None,
  2010. credentials: HTTPAuthorizationCredentials = Depends(security)
  2011. ):
  2012. """获取所有菜单(管理员)"""
  2013. try:
  2014. payload = verify_token(credentials.credentials)
  2015. if not payload:
  2016. return ApiResponse(
  2017. code=401,
  2018. message="无效的访问令牌",
  2019. timestamp=datetime.now(timezone.utc).isoformat()
  2020. ).model_dump()
  2021. # 简化权限检查 - 只检查是否为管理员
  2022. is_superuser = payload.get("is_superuser", False)
  2023. if not is_superuser:
  2024. return ApiResponse(
  2025. code=403,
  2026. message="权限不足",
  2027. timestamp=datetime.now(timezone.utc).isoformat()
  2028. ).model_dump()
  2029. conn = get_db_connection()
  2030. if not conn:
  2031. return ApiResponse(
  2032. code=500,
  2033. message="数据库连接失败",
  2034. timestamp=datetime.now(timezone.utc).isoformat()
  2035. ).model_dump()
  2036. cursor = conn.cursor()
  2037. # 构建查询条件
  2038. where_conditions = []
  2039. params = []
  2040. if keyword:
  2041. where_conditions.append("(m.title LIKE %s OR m.name LIKE %s)")
  2042. params.extend([f"%{keyword}%", f"%{keyword}%"])
  2043. where_clause = " AND ".join(where_conditions) if where_conditions else "1=1"
  2044. # 查询总数
  2045. cursor.execute(f"SELECT COUNT(*) FROM menus m WHERE {where_clause}", params)
  2046. total = cursor.fetchone()[0]
  2047. # 查询菜单列表 - 修改排序逻辑以支持树形结构
  2048. cursor.execute(f"""
  2049. SELECT m.id, m.parent_id, m.name, m.title, m.path, m.component,
  2050. m.icon, m.sort_order, m.menu_type, m.is_hidden, m.is_active,
  2051. m.description, m.created_at, m.updated_at,
  2052. pm.title as parent_title
  2053. FROM menus m
  2054. LEFT JOIN menus pm ON m.parent_id = pm.id
  2055. WHERE {where_clause}
  2056. ORDER BY
  2057. CASE WHEN m.parent_id IS NULL THEN 0 ELSE 1 END,
  2058. m.sort_order,
  2059. CASE WHEN m.menu_type = 'menu' THEN 0 ELSE 1 END,
  2060. m.created_at
  2061. LIMIT %s OFFSET %s
  2062. """, params + [page_size, (page - 1) * page_size])
  2063. menus = []
  2064. for row in cursor.fetchall():
  2065. menu = {
  2066. "id": row[0],
  2067. "parent_id": row[1],
  2068. "name": row[2],
  2069. "title": row[3],
  2070. "path": row[4],
  2071. "component": row[5],
  2072. "icon": row[6],
  2073. "sort_order": row[7],
  2074. "menu_type": row[8],
  2075. "is_hidden": bool(row[9]),
  2076. "is_active": bool(row[10]),
  2077. "description": row[11],
  2078. "created_at": row[12].isoformat() if row[12] else None,
  2079. "updated_at": row[13].isoformat() if row[13] else None,
  2080. "parent_title": row[14]
  2081. }
  2082. menus.append(menu)
  2083. cursor.close()
  2084. conn.close()
  2085. return ApiResponse(
  2086. code=0,
  2087. message="获取菜单列表成功",
  2088. data={
  2089. "items": menus,
  2090. "total": total,
  2091. "page": page,
  2092. "page_size": page_size
  2093. },
  2094. timestamp=datetime.now(timezone.utc).isoformat()
  2095. ).model_dump()
  2096. except Exception as e:
  2097. print(f"获取菜单列表错误: {e}")
  2098. return ApiResponse(
  2099. code=500,
  2100. message="服务器内部错误",
  2101. timestamp=datetime.now(timezone.utc).isoformat()
  2102. ).model_dump()
  2103. @app.get("/api/v1/admin/roles")
  2104. async def api_get_all_roles(
  2105. page: int = 1,
  2106. page_size: int = 20,
  2107. keyword: Optional[str] = None,
  2108. credentials: HTTPAuthorizationCredentials = Depends(security)
  2109. ):
  2110. """获取所有角色"""
  2111. try:
  2112. payload = verify_token(credentials.credentials)
  2113. if not payload:
  2114. return ApiResponse(
  2115. code=401,
  2116. message="无效的访问令牌",
  2117. timestamp=datetime.now(timezone.utc).isoformat()
  2118. ).model_dump()
  2119. # 简化权限检查 - 只检查是否为管理员
  2120. is_superuser = payload.get("is_superuser", False)
  2121. if not is_superuser:
  2122. return ApiResponse(
  2123. code=403,
  2124. message="权限不足",
  2125. timestamp=datetime.now(timezone.utc).isoformat()
  2126. ).model_dump()
  2127. conn = get_db_connection()
  2128. if not conn:
  2129. return ApiResponse(
  2130. code=500,
  2131. message="数据库连接失败",
  2132. timestamp=datetime.now(timezone.utc).isoformat()
  2133. ).model_dump()
  2134. cursor = conn.cursor()
  2135. # 构建查询条件
  2136. where_conditions = []
  2137. params = []
  2138. if keyword:
  2139. where_conditions.append("(r.display_name LIKE %s OR r.name LIKE %s)")
  2140. params.extend([f"%{keyword}%", f"%{keyword}%"])
  2141. where_clause = " AND ".join(where_conditions) if where_conditions else "1=1"
  2142. # 查询总数
  2143. cursor.execute(f"SELECT COUNT(*) FROM roles r WHERE {where_clause}", params)
  2144. total = cursor.fetchone()[0]
  2145. # 查询角色列表
  2146. offset = (page - 1) * page_size
  2147. cursor.execute(f"""
  2148. SELECT r.id, r.name, r.display_name, r.description, r.is_active,
  2149. r.is_system, r.created_at, r.updated_at,
  2150. COUNT(ur.user_id) as user_count
  2151. FROM roles r
  2152. LEFT JOIN user_roles ur ON r.id = ur.role_id AND ur.is_active = 1
  2153. WHERE {where_clause}
  2154. GROUP BY r.id
  2155. ORDER BY r.is_system DESC, r.created_at
  2156. LIMIT %s OFFSET %s
  2157. """, params + [page_size, offset])
  2158. roles = []
  2159. for row in cursor.fetchall():
  2160. role = {
  2161. "id": row[0],
  2162. "name": row[1],
  2163. "display_name": row[2],
  2164. "description": row[3],
  2165. "is_active": bool(row[4]),
  2166. "is_system": bool(row[5]),
  2167. "created_at": row[6].isoformat() if row[6] else None,
  2168. "updated_at": row[7].isoformat() if row[7] else None,
  2169. "user_count": row[8]
  2170. }
  2171. roles.append(role)
  2172. cursor.close()
  2173. conn.close()
  2174. return ApiResponse(
  2175. code=0,
  2176. message="获取角色列表成功",
  2177. data={
  2178. "items": roles,
  2179. "total": total,
  2180. "page": page,
  2181. "page_size": page_size
  2182. },
  2183. timestamp=datetime.now(timezone.utc).isoformat()
  2184. ).model_dump()
  2185. except Exception as e:
  2186. print(f"获取角色列表错误: {e}")
  2187. return ApiResponse(
  2188. code=500,
  2189. message="服务器内部错误",
  2190. timestamp=datetime.now(timezone.utc).isoformat()
  2191. ).model_dump()
  2192. @app.get("/api/v1/user/permissions")
  2193. async def api_get_user_permissions(credentials: HTTPAuthorizationCredentials = Depends(security)):
  2194. """获取用户权限"""
  2195. try:
  2196. payload = verify_token(credentials.credentials)
  2197. if not payload:
  2198. return ApiResponse(
  2199. code=401,
  2200. message="无效的访问令牌",
  2201. timestamp=datetime.now(timezone.utc).isoformat()
  2202. ).model_dump()
  2203. user_id = payload.get("sub")
  2204. conn = get_db_connection()
  2205. if not conn:
  2206. return ApiResponse(
  2207. code=500,
  2208. message="数据库连接失败",
  2209. timestamp=datetime.now(timezone.utc).isoformat()
  2210. ).model_dump()
  2211. cursor = conn.cursor()
  2212. # 获取用户权限
  2213. cursor.execute("""
  2214. SELECT DISTINCT p.name, p.resource, p.action
  2215. FROM permissions p
  2216. JOIN role_permissions rp ON p.id = rp.permission_id
  2217. JOIN user_roles ur ON rp.role_id = ur.role_id
  2218. WHERE ur.user_id = %s
  2219. AND ur.is_active = 1
  2220. AND p.is_active = 1
  2221. """, (user_id,))
  2222. permissions = []
  2223. for row in cursor.fetchall():
  2224. permissions.append({
  2225. "name": row[0],
  2226. "resource": row[1],
  2227. "action": row[2]
  2228. })
  2229. cursor.close()
  2230. conn.close()
  2231. return ApiResponse(
  2232. code=0,
  2233. message="获取用户权限成功",
  2234. data=permissions,
  2235. timestamp=datetime.now(timezone.utc).isoformat()
  2236. ).model_dump()
  2237. except Exception as e:
  2238. print(f"获取用户权限错误: {e}")
  2239. return ApiResponse(
  2240. code=500,
  2241. message="服务器内部错误",
  2242. timestamp=datetime.now(timezone.utc).isoformat()
  2243. ).model_dump()
  2244. # 用户管理API
  2245. @app.get("/api/v1/admin/users")
  2246. async def get_users(
  2247. page: int = 1,
  2248. page_size: int = 20,
  2249. keyword: Optional[str] = None,
  2250. credentials: HTTPAuthorizationCredentials = Depends(security)
  2251. ):
  2252. """获取用户列表"""
  2253. try:
  2254. payload = verify_token(credentials.credentials)
  2255. if not payload:
  2256. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2257. is_superuser = payload.get("is_superuser", False)
  2258. if not is_superuser:
  2259. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2260. conn = get_db_connection()
  2261. if not conn:
  2262. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2263. cursor = conn.cursor()
  2264. # 构建查询条件
  2265. where_conditions = []
  2266. params = []
  2267. if keyword:
  2268. where_conditions.append("(u.username LIKE %s OR u.email LIKE %s OR up.real_name LIKE %s)")
  2269. params.extend([f"%{keyword}%", f"%{keyword}%", f"%{keyword}%"])
  2270. where_clause = " AND ".join(where_conditions) if where_conditions else "1=1"
  2271. # 查询总数
  2272. cursor.execute(f"SELECT COUNT(*) FROM users u LEFT JOIN user_profiles up ON u.id = up.user_id WHERE {where_clause}", params)
  2273. total = cursor.fetchone()[0]
  2274. # 查询用户列表
  2275. offset = (page - 1) * page_size
  2276. cursor.execute(f"""
  2277. SELECT u.id, u.username, u.email, u.phone, u.is_active, u.is_superuser,
  2278. u.last_login_at, u.created_at, up.real_name, up.company, up.department,
  2279. GROUP_CONCAT(r.display_name) as roles
  2280. FROM users u
  2281. LEFT JOIN user_profiles up ON u.id = up.user_id
  2282. LEFT JOIN user_roles ur ON u.id = ur.user_id AND ur.is_active = 1
  2283. LEFT JOIN roles r ON ur.role_id = r.id
  2284. WHERE {where_clause}
  2285. GROUP BY u.id, u.username, u.email, u.phone, u.is_active, u.is_superuser,
  2286. u.last_login_at, u.created_at, up.real_name, up.company, up.department
  2287. ORDER BY u.created_at DESC
  2288. LIMIT %s OFFSET %s
  2289. """, params + [page_size, offset])
  2290. users = []
  2291. for row in cursor.fetchall():
  2292. users.append({
  2293. "id": row[0],
  2294. "username": row[1],
  2295. "email": row[2],
  2296. "phone": row[3],
  2297. "is_active": bool(row[4]),
  2298. "is_superuser": bool(row[5]),
  2299. "last_login_at": row[6].isoformat() if row[6] else None,
  2300. "created_at": row[7].isoformat() if row[7] else None,
  2301. "real_name": row[8],
  2302. "company": row[9],
  2303. "department": row[10],
  2304. "roles": row[11].split(',') if row[11] else []
  2305. })
  2306. cursor.close()
  2307. conn.close()
  2308. return ApiResponse(
  2309. code=0,
  2310. message="获取用户列表成功",
  2311. data={"items": users, "total": total, "page": page, "page_size": page_size},
  2312. timestamp=datetime.now(timezone.utc).isoformat()
  2313. ).model_dump()
  2314. except Exception as e:
  2315. print(f"获取用户列表错误: {e}")
  2316. return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2317. @app.post("/api/v1/admin/users")
  2318. async def create_user(
  2319. user_data: dict,
  2320. credentials: HTTPAuthorizationCredentials = Depends(security)
  2321. ):
  2322. """创建用户"""
  2323. try:
  2324. payload = verify_token(credentials.credentials)
  2325. if not payload:
  2326. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2327. is_superuser = payload.get("is_superuser", False)
  2328. if not is_superuser:
  2329. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2330. conn = get_db_connection()
  2331. if not conn:
  2332. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2333. cursor = conn.cursor()
  2334. # 检查用户名和邮箱是否已存在
  2335. cursor.execute("SELECT id FROM users WHERE username = %s OR email = %s",
  2336. (user_data['username'], user_data['email']))
  2337. if cursor.fetchone():
  2338. cursor.close()
  2339. conn.close()
  2340. return ApiResponse(code=400, message="用户名或邮箱已存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2341. # 生成用户ID
  2342. user_id = str(uuid.uuid4())
  2343. # 创建密码哈希
  2344. password_hash = hash_password_simple(user_data['password'])
  2345. # 插入用户
  2346. cursor.execute("""
  2347. INSERT INTO users (id, username, email, phone, password_hash, is_active, is_superuser, created_at, updated_at)
  2348. VALUES (%s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
  2349. """, (user_id, user_data['username'], user_data['email'], user_data.get('phone'),
  2350. password_hash, user_data.get('is_active', True), user_data.get('is_superuser', False)))
  2351. # 插入用户详情
  2352. if any(key in user_data for key in ['real_name', 'company', 'department']):
  2353. profile_id = str(uuid.uuid4())
  2354. cursor.execute("""
  2355. INSERT INTO user_profiles (id, user_id, real_name, company, department, created_at, updated_at)
  2356. VALUES (%s, %s, %s, %s, %s, NOW(), NOW())
  2357. """, (profile_id, user_id, user_data.get('real_name'), user_data.get('company'), user_data.get('department')))
  2358. # 分配角色
  2359. if 'role_ids' in user_data and user_data['role_ids']:
  2360. for role_id in user_data['role_ids']:
  2361. role_assignment_id = str(uuid.uuid4())
  2362. cursor.execute("""
  2363. INSERT INTO user_roles (id, user_id, role_id, assigned_by, created_at)
  2364. VALUES (%s, %s, %s, %s, NOW())
  2365. """, (role_assignment_id, user_id, role_id, payload.get("sub")))
  2366. conn.commit()
  2367. cursor.close()
  2368. conn.close()
  2369. return ApiResponse(code=0, message="用户创建成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2370. except Exception as e:
  2371. print(f"创建用户错误: {e}")
  2372. return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2373. @app.put("/api/v1/admin/users/{user_id}")
  2374. async def update_user(
  2375. user_id: str,
  2376. user_data: dict,
  2377. credentials: HTTPAuthorizationCredentials = Depends(security)
  2378. ):
  2379. """更新用户"""
  2380. try:
  2381. payload = verify_token(credentials.credentials)
  2382. if not payload:
  2383. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2384. is_superuser = payload.get("is_superuser", False)
  2385. if not is_superuser:
  2386. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2387. conn = get_db_connection()
  2388. if not conn:
  2389. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2390. cursor = conn.cursor()
  2391. # 更新用户基本信息
  2392. update_fields = []
  2393. update_values = []
  2394. for field in ['email', 'phone', 'is_active', 'is_superuser']:
  2395. if field in user_data:
  2396. update_fields.append(f'{field} = %s')
  2397. update_values.append(user_data[field])
  2398. if update_fields:
  2399. update_values.append(user_id)
  2400. cursor.execute(f"""
  2401. UPDATE users
  2402. SET {', '.join(update_fields)}, updated_at = NOW()
  2403. WHERE id = %s
  2404. """, update_values)
  2405. # 更新用户详情
  2406. profile_fields = ['real_name', 'company', 'department']
  2407. profile_updates = {k: v for k, v in user_data.items() if k in profile_fields}
  2408. if profile_updates:
  2409. # 检查是否已有记录
  2410. cursor.execute("SELECT id FROM user_profiles WHERE user_id = %s", (user_id,))
  2411. profile_exists = cursor.fetchone()
  2412. if profile_exists:
  2413. update_fields = []
  2414. update_values = []
  2415. for field, value in profile_updates.items():
  2416. update_fields.append(f'{field} = %s')
  2417. update_values.append(value)
  2418. update_values.append(user_id)
  2419. cursor.execute(f"""
  2420. UPDATE user_profiles
  2421. SET {', '.join(update_fields)}, updated_at = NOW()
  2422. WHERE user_id = %s
  2423. """, update_values)
  2424. else:
  2425. profile_id = str(uuid.uuid4())
  2426. fields = ['id', 'user_id'] + list(profile_updates.keys())
  2427. values = [profile_id, user_id] + list(profile_updates.values())
  2428. placeholders = ', '.join(['%s'] * len(values))
  2429. cursor.execute(f"""
  2430. INSERT INTO user_profiles ({', '.join(fields)}, created_at, updated_at)
  2431. VALUES ({placeholders}, NOW(), NOW())
  2432. """, values)
  2433. # 更新用户角色
  2434. if 'role_ids' in user_data:
  2435. # 删除现有角色
  2436. cursor.execute("DELETE FROM user_roles WHERE user_id = %s", (user_id,))
  2437. # 添加新角色
  2438. for role_id in user_data['role_ids']:
  2439. assignment_id = str(uuid.uuid4())
  2440. cursor.execute("""
  2441. INSERT INTO user_roles (id, user_id, role_id, assigned_by, created_at)
  2442. VALUES (%s, %s, %s, %s, NOW())
  2443. """, (assignment_id, user_id, role_id, payload.get("sub")))
  2444. conn.commit()
  2445. cursor.close()
  2446. conn.close()
  2447. return ApiResponse(code=0, message="用户更新成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2448. except Exception as e:
  2449. print(f"更新用户错误: {e}")
  2450. return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2451. @app.delete("/api/v1/admin/users/{user_id}")
  2452. async def delete_user(
  2453. user_id: str,
  2454. credentials: HTTPAuthorizationCredentials = Depends(security)
  2455. ):
  2456. """删除用户"""
  2457. try:
  2458. payload = verify_token(credentials.credentials)
  2459. if not payload:
  2460. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2461. is_superuser = payload.get("is_superuser", False)
  2462. if not is_superuser:
  2463. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2464. # 不能删除自己
  2465. if user_id == payload.get("sub"):
  2466. return ApiResponse(code=400, message="不能删除自己", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2467. conn = get_db_connection()
  2468. if not conn:
  2469. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2470. cursor = conn.cursor()
  2471. # 检查是否为超级管理员
  2472. cursor.execute("""
  2473. SELECT COUNT(*) FROM user_roles ur
  2474. JOIN roles r ON ur.role_id = r.id
  2475. WHERE ur.user_id = %s AND r.name = 'super_admin' AND ur.is_active = 1
  2476. """, (user_id,))
  2477. if cursor.fetchone()[0] > 0:
  2478. cursor.close()
  2479. conn.close()
  2480. return ApiResponse(code=400, message="不能删除超级管理员", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2481. # 删除相关数据
  2482. cursor.execute("DELETE FROM user_roles WHERE user_id = %s", (user_id,))
  2483. cursor.execute("DELETE FROM user_profiles WHERE user_id = %s", (user_id,))
  2484. cursor.execute("DELETE FROM users WHERE id = %s", (user_id,))
  2485. conn.commit()
  2486. cursor.close()
  2487. conn.close()
  2488. return ApiResponse(code=0, message="用户删除成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2489. except Exception as e:
  2490. print(f"删除用户错误: {e}")
  2491. return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2492. # 角色管理API
  2493. @app.post("/api/v1/admin/roles")
  2494. async def create_role(
  2495. role_data: dict,
  2496. credentials: HTTPAuthorizationCredentials = Depends(security)
  2497. ):
  2498. """创建角色"""
  2499. try:
  2500. payload = verify_token(credentials.credentials)
  2501. if not payload:
  2502. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2503. is_superuser = payload.get("is_superuser", False)
  2504. if not is_superuser:
  2505. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2506. conn = get_db_connection()
  2507. if not conn:
  2508. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2509. cursor = conn.cursor()
  2510. # 检查角色名是否已存在
  2511. cursor.execute("SELECT id FROM roles WHERE name = %s", (role_data['name'],))
  2512. if cursor.fetchone():
  2513. cursor.close()
  2514. conn.close()
  2515. return ApiResponse(code=400, message="角色名已存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2516. # 创建角色
  2517. role_id = str(uuid.uuid4())
  2518. cursor.execute("""
  2519. INSERT INTO roles (id, name, display_name, description, is_active, is_system, created_at, updated_at)
  2520. VALUES (%s, %s, %s, %s, %s, %s, NOW(), NOW())
  2521. """, (role_id, role_data['name'], role_data['display_name'], role_data.get('description'),
  2522. role_data.get('is_active', True), False))
  2523. conn.commit()
  2524. cursor.close()
  2525. conn.close()
  2526. return ApiResponse(code=0, message="角色创建成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2527. except Exception as e:
  2528. print(f"创建角色错误: {e}")
  2529. return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2530. @app.put("/api/v1/admin/roles/{role_id}")
  2531. async def update_role(
  2532. role_id: str,
  2533. role_data: dict,
  2534. credentials: HTTPAuthorizationCredentials = Depends(security)
  2535. ):
  2536. """更新角色"""
  2537. try:
  2538. payload = verify_token(credentials.credentials)
  2539. if not payload:
  2540. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2541. is_superuser = payload.get("is_superuser", False)
  2542. if not is_superuser:
  2543. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2544. conn = get_db_connection()
  2545. if not conn:
  2546. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2547. cursor = conn.cursor()
  2548. # 检查是否为系统角色
  2549. cursor.execute("SELECT is_system FROM roles WHERE id = %s", (role_id,))
  2550. role = cursor.fetchone()
  2551. if not role:
  2552. cursor.close()
  2553. conn.close()
  2554. return ApiResponse(code=404, message="角色不存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2555. if role[0]: # is_system
  2556. cursor.close()
  2557. conn.close()
  2558. return ApiResponse(code=400, message="不能修改系统角色", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2559. # 更新角色
  2560. update_fields = []
  2561. update_values = []
  2562. for field in ['display_name', 'description', 'is_active']:
  2563. if field in role_data:
  2564. update_fields.append(f'{field} = %s')
  2565. update_values.append(role_data[field])
  2566. if update_fields:
  2567. update_values.append(role_id)
  2568. cursor.execute(f"""
  2569. UPDATE roles
  2570. SET {', '.join(update_fields)}, updated_at = NOW()
  2571. WHERE id = %s
  2572. """, update_values)
  2573. conn.commit()
  2574. cursor.close()
  2575. conn.close()
  2576. return ApiResponse(code=0, message="角色更新成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2577. except Exception as e:
  2578. print(f"更新角色错误: {e}")
  2579. return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2580. @app.delete("/api/v1/admin/roles/{role_id}")
  2581. async def delete_role(
  2582. role_id: str,
  2583. credentials: HTTPAuthorizationCredentials = Depends(security)
  2584. ):
  2585. """删除角色"""
  2586. try:
  2587. payload = verify_token(credentials.credentials)
  2588. if not payload:
  2589. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2590. is_superuser = payload.get("is_superuser", False)
  2591. if not is_superuser:
  2592. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2593. conn = get_db_connection()
  2594. if not conn:
  2595. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2596. cursor = conn.cursor()
  2597. # 检查是否为系统角色
  2598. cursor.execute("SELECT is_system FROM roles WHERE id = %s", (role_id,))
  2599. role = cursor.fetchone()
  2600. if not role:
  2601. cursor.close()
  2602. conn.close()
  2603. return ApiResponse(code=404, message="角色不存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2604. if role[0]: # is_system
  2605. cursor.close()
  2606. conn.close()
  2607. return ApiResponse(code=400, message="不能删除系统角色", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2608. # 检查是否有用户使用此角色
  2609. cursor.execute("SELECT COUNT(*) FROM user_roles WHERE role_id = %s", (role_id,))
  2610. if cursor.fetchone()[0] > 0:
  2611. cursor.close()
  2612. conn.close()
  2613. return ApiResponse(code=400, message="该角色正在被使用,无法删除", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2614. # 删除角色相关数据
  2615. cursor.execute("DELETE FROM role_permissions WHERE role_id = %s", (role_id,))
  2616. cursor.execute("DELETE FROM role_menus WHERE role_id = %s", (role_id,))
  2617. cursor.execute("DELETE FROM roles WHERE id = %s", (role_id,))
  2618. conn.commit()
  2619. cursor.close()
  2620. conn.close()
  2621. return ApiResponse(code=0, message="角色删除成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2622. except Exception as e:
  2623. print(f"删除角色错误: {e}")
  2624. return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2625. # 角色菜单权限管理API
  2626. @app.get("/api/v1/admin/roles/{role_id}/menus")
  2627. async def get_role_menus(
  2628. role_id: str,
  2629. credentials: HTTPAuthorizationCredentials = Depends(security)
  2630. ):
  2631. """获取角色的菜单权限"""
  2632. try:
  2633. payload = verify_token(credentials.credentials)
  2634. if not payload:
  2635. return ApiResponse(
  2636. code=401,
  2637. message="无效的访问令牌",
  2638. timestamp=datetime.now(timezone.utc).isoformat()
  2639. ).model_dump()
  2640. # 检查管理员权限
  2641. is_superuser = payload.get("is_superuser", False)
  2642. if not is_superuser:
  2643. return ApiResponse(
  2644. code=403,
  2645. message="权限不足",
  2646. timestamp=datetime.now(timezone.utc).isoformat()
  2647. ).model_dump()
  2648. conn = get_db_connection()
  2649. if not conn:
  2650. return ApiResponse(
  2651. code=500,
  2652. message="数据库连接失败",
  2653. timestamp=datetime.now(timezone.utc).isoformat()
  2654. ).model_dump()
  2655. cursor = conn.cursor()
  2656. # 检查角色是否存在
  2657. cursor.execute("SELECT id, name FROM roles WHERE id = %s", (role_id,))
  2658. role = cursor.fetchone()
  2659. if not role:
  2660. cursor.close()
  2661. conn.close()
  2662. return ApiResponse(
  2663. code=404,
  2664. message="角色不存在",
  2665. timestamp=datetime.now(timezone.utc).isoformat()
  2666. ).model_dump()
  2667. # 检查是否为超级管理员角色
  2668. role_name = role[1]
  2669. is_super_admin_role = role_name == "super_admin"
  2670. if is_super_admin_role:
  2671. # 超级管理员默认拥有所有菜单权限
  2672. cursor.execute("""
  2673. SELECT id, name, title, parent_id, menu_type
  2674. FROM menus
  2675. WHERE is_active = 1
  2676. ORDER BY sort_order
  2677. """)
  2678. menu_permissions = cursor.fetchall()
  2679. else:
  2680. # 普通角色查询已分配的菜单权限
  2681. cursor.execute("""
  2682. SELECT m.id, m.name, m.title, m.parent_id, m.menu_type
  2683. FROM role_menus rm
  2684. JOIN menus m ON rm.menu_id = m.id
  2685. WHERE rm.role_id = %s AND m.is_active = 1
  2686. ORDER BY m.sort_order
  2687. """, (role_id,))
  2688. menu_permissions = cursor.fetchall()
  2689. cursor.close()
  2690. conn.close()
  2691. # 构建返回数据
  2692. menu_ids = [menu[0] for menu in menu_permissions]
  2693. menu_details = []
  2694. for menu in menu_permissions:
  2695. menu_details.append({
  2696. "id": menu[0],
  2697. "name": menu[1],
  2698. "title": menu[2],
  2699. "parent_id": menu[3],
  2700. "menu_type": menu[4]
  2701. })
  2702. return ApiResponse(
  2703. code=0,
  2704. message="获取角色菜单权限成功",
  2705. data={
  2706. "role_id": role_id,
  2707. "role_name": role[1],
  2708. "menu_ids": menu_ids,
  2709. "menu_details": menu_details,
  2710. "total": len(menu_ids)
  2711. },
  2712. timestamp=datetime.now(timezone.utc).isoformat()
  2713. ).model_dump()
  2714. except Exception as e:
  2715. print(f"获取角色菜单权限错误: {e}")
  2716. return ApiResponse(
  2717. code=500,
  2718. message="服务器内部错误",
  2719. timestamp=datetime.now(timezone.utc).isoformat()
  2720. ).model_dump()
  2721. @app.put("/api/v1/admin/roles/{role_id}/menus")
  2722. async def update_role_menus(
  2723. role_id: str,
  2724. request: Request,
  2725. credentials: HTTPAuthorizationCredentials = Depends(security)
  2726. ):
  2727. """更新角色的菜单权限"""
  2728. try:
  2729. payload = verify_token(credentials.credentials)
  2730. if not payload:
  2731. return ApiResponse(
  2732. code=401,
  2733. message="无效的访问令牌",
  2734. timestamp=datetime.now(timezone.utc).isoformat()
  2735. ).model_dump()
  2736. # 检查管理员权限
  2737. is_superuser = payload.get("is_superuser", False)
  2738. if not is_superuser:
  2739. return ApiResponse(
  2740. code=403,
  2741. message="权限不足",
  2742. timestamp=datetime.now(timezone.utc).isoformat()
  2743. ).model_dump()
  2744. # 获取请求数据
  2745. body = await request.json()
  2746. menu_ids = body.get("menu_ids", [])
  2747. if not isinstance(menu_ids, list):
  2748. return ApiResponse(
  2749. code=400,
  2750. message="菜单ID列表格式错误",
  2751. timestamp=datetime.now(timezone.utc).isoformat()
  2752. ).model_dump()
  2753. conn = get_db_connection()
  2754. if not conn:
  2755. return ApiResponse(
  2756. code=500,
  2757. message="数据库连接失败",
  2758. timestamp=datetime.now(timezone.utc).isoformat()
  2759. ).model_dump()
  2760. cursor = conn.cursor()
  2761. # 检查角色是否存在
  2762. cursor.execute("SELECT id, name FROM roles WHERE id = %s", (role_id,))
  2763. role = cursor.fetchone()
  2764. if not role:
  2765. cursor.close()
  2766. conn.close()
  2767. return ApiResponse(
  2768. code=404,
  2769. message="角色不存在",
  2770. timestamp=datetime.now(timezone.utc).isoformat()
  2771. ).model_dump()
  2772. # 检查是否为超级管理员角色
  2773. role_name = role[1]
  2774. is_super_admin_role = role_name == "super_admin"
  2775. if is_super_admin_role:
  2776. # 超级管理员角色不允许修改权限,始终拥有全部权限
  2777. cursor.close()
  2778. conn.close()
  2779. return ApiResponse(
  2780. code=400,
  2781. message="超级管理员角色拥有全部权限,无需修改",
  2782. timestamp=datetime.now(timezone.utc).isoformat()
  2783. ).model_dump()
  2784. # 验证菜单ID是否存在
  2785. if menu_ids:
  2786. placeholders = ','.join(['%s'] * len(menu_ids))
  2787. cursor.execute(f"""
  2788. SELECT id FROM menus
  2789. WHERE id IN ({placeholders}) AND is_active = 1
  2790. """, menu_ids)
  2791. valid_menu_ids = [row[0] for row in cursor.fetchall()]
  2792. invalid_menu_ids = set(menu_ids) - set(valid_menu_ids)
  2793. if invalid_menu_ids:
  2794. cursor.close()
  2795. conn.close()
  2796. return ApiResponse(
  2797. code=400,
  2798. message=f"无效的菜单ID: {', '.join(invalid_menu_ids)}",
  2799. timestamp=datetime.now(timezone.utc).isoformat()
  2800. ).model_dump()
  2801. # 开始事务
  2802. cursor.execute("START TRANSACTION")
  2803. try:
  2804. # 删除角色现有的菜单权限
  2805. cursor.execute("DELETE FROM role_menus WHERE role_id = %s", (role_id,))
  2806. # 添加新的菜单权限
  2807. if menu_ids:
  2808. values = [(role_id, menu_id) for menu_id in menu_ids]
  2809. cursor.executemany("""
  2810. INSERT INTO role_menus (role_id, menu_id, created_at)
  2811. VALUES (%s, %s, NOW())
  2812. """, values)
  2813. # 提交事务
  2814. conn.commit()
  2815. cursor.close()
  2816. conn.close()
  2817. return ApiResponse(
  2818. code=0,
  2819. message="角色菜单权限更新成功",
  2820. data={
  2821. "role_id": role_id,
  2822. "role_name": role[1],
  2823. "menu_ids": menu_ids,
  2824. "updated_count": len(menu_ids)
  2825. },
  2826. timestamp=datetime.now(timezone.utc).isoformat()
  2827. ).model_dump()
  2828. except Exception as e:
  2829. # 回滚事务
  2830. conn.rollback()
  2831. cursor.close()
  2832. conn.close()
  2833. raise e
  2834. except Exception as e:
  2835. print(f"更新角色菜单权限错误: {e}")
  2836. return ApiResponse(
  2837. code=500,
  2838. message="服务器内部错误",
  2839. timestamp=datetime.now(timezone.utc).isoformat()
  2840. ).model_dump()
  2841. # 菜单管理API
  2842. @app.post("/api/v1/admin/menus")
  2843. async def create_menu(
  2844. menu_data: dict,
  2845. credentials: HTTPAuthorizationCredentials = Depends(security)
  2846. ):
  2847. """创建菜单"""
  2848. try:
  2849. payload = verify_token(credentials.credentials)
  2850. if not payload:
  2851. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2852. is_superuser = payload.get("is_superuser", False)
  2853. if not is_superuser:
  2854. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2855. conn = get_db_connection()
  2856. if not conn:
  2857. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2858. cursor = conn.cursor()
  2859. # 检查菜单名是否已存在
  2860. cursor.execute("SELECT id FROM menus WHERE name = %s", (menu_data['name'],))
  2861. if cursor.fetchone():
  2862. cursor.close()
  2863. conn.close()
  2864. return ApiResponse(code=400, message="菜单标识已存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2865. # 创建菜单
  2866. menu_id = str(uuid.uuid4())
  2867. cursor.execute("""
  2868. INSERT INTO menus (id, parent_id, name, title, path, component, icon,
  2869. sort_order, menu_type, is_hidden, is_active, description, created_at, updated_at)
  2870. VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
  2871. """, (
  2872. menu_id, menu_data.get('parent_id'), menu_data['name'], menu_data['title'],
  2873. menu_data.get('path'), menu_data.get('component'), menu_data.get('icon'),
  2874. menu_data.get('sort_order', 0), menu_data.get('menu_type', 'menu'),
  2875. menu_data.get('is_hidden', False), menu_data.get('is_active', True),
  2876. menu_data.get('description')
  2877. ))
  2878. conn.commit()
  2879. cursor.close()
  2880. conn.close()
  2881. return ApiResponse(code=0, message="菜单创建成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2882. except Exception as e:
  2883. print(f"创建菜单错误: {e}")
  2884. return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2885. @app.put("/api/v1/admin/menus/{menu_id}")
  2886. async def update_menu(
  2887. menu_id: str,
  2888. menu_data: dict,
  2889. credentials: HTTPAuthorizationCredentials = Depends(security)
  2890. ):
  2891. """更新菜单"""
  2892. try:
  2893. payload = verify_token(credentials.credentials)
  2894. if not payload:
  2895. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2896. is_superuser = payload.get("is_superuser", False)
  2897. if not is_superuser:
  2898. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2899. conn = get_db_connection()
  2900. if not conn:
  2901. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2902. cursor = conn.cursor()
  2903. # 更新菜单
  2904. update_fields = []
  2905. update_values = []
  2906. for field in ['parent_id', 'title', 'path', 'component', 'icon', 'sort_order',
  2907. 'menu_type', 'is_hidden', 'is_active', 'description']:
  2908. if field in menu_data:
  2909. update_fields.append(f'{field} = %s')
  2910. update_values.append(menu_data[field])
  2911. if update_fields:
  2912. update_values.append(menu_id)
  2913. cursor.execute(f"""
  2914. UPDATE menus
  2915. SET {', '.join(update_fields)}, updated_at = NOW()
  2916. WHERE id = %s
  2917. """, update_values)
  2918. conn.commit()
  2919. cursor.close()
  2920. conn.close()
  2921. return ApiResponse(code=0, message="菜单更新成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2922. except Exception as e:
  2923. print(f"更新菜单错误: {e}")
  2924. return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2925. @app.delete("/api/v1/admin/menus/{menu_id}")
  2926. async def delete_menu(
  2927. menu_id: str,
  2928. credentials: HTTPAuthorizationCredentials = Depends(security)
  2929. ):
  2930. """删除菜单"""
  2931. try:
  2932. payload = verify_token(credentials.credentials)
  2933. if not payload:
  2934. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2935. is_superuser = payload.get("is_superuser", False)
  2936. if not is_superuser:
  2937. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2938. conn = get_db_connection()
  2939. if not conn:
  2940. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2941. cursor = conn.cursor()
  2942. # 检查是否有子菜单
  2943. cursor.execute("SELECT COUNT(*) FROM menus WHERE parent_id = %s", (menu_id,))
  2944. if cursor.fetchone()[0] > 0:
  2945. cursor.close()
  2946. conn.close()
  2947. return ApiResponse(code=400, message="该菜单下有子菜单,无法删除", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2948. # 删除菜单相关数据
  2949. cursor.execute("DELETE FROM role_menus WHERE menu_id = %s", (menu_id,))
  2950. cursor.execute("DELETE FROM menus WHERE id = %s", (menu_id,))
  2951. conn.commit()
  2952. cursor.close()
  2953. conn.close()
  2954. return ApiResponse(code=0, message="菜单删除成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2955. except Exception as e:
  2956. print(f"删除菜单错误: {e}")
  2957. return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2958. # 获取所有角色(用于下拉选择)
  2959. @app.get("/api/v1/roles/all")
  2960. async def get_all_roles_simple(credentials: HTTPAuthorizationCredentials = Depends(security)):
  2961. """获取所有角色(简化版,用于下拉选择)"""
  2962. try:
  2963. payload = verify_token(credentials.credentials)
  2964. if not payload:
  2965. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2966. conn = get_db_connection()
  2967. if not conn:
  2968. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2969. cursor = conn.cursor()
  2970. cursor.execute("""
  2971. SELECT id, name, display_name, is_system, is_active
  2972. FROM roles
  2973. WHERE is_active = 1
  2974. ORDER BY is_system DESC, display_name
  2975. """)
  2976. roles = []
  2977. for row in cursor.fetchall():
  2978. roles.append({
  2979. "id": row[0],
  2980. "name": row[1],
  2981. "display_name": row[2],
  2982. "is_system": bool(row[3]),
  2983. "is_active": bool(row[4])
  2984. })
  2985. cursor.close()
  2986. conn.close()
  2987. return ApiResponse(code=0, message="获取角色列表成功", data=roles, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2988. except Exception as e:
  2989. print(f"获取角色列表错误: {e}")
  2990. return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2991. if __name__ == "__main__":
  2992. import uvicorn
  2993. # 查找可用端口
  2994. port = find_available_port()
  2995. if port is None:
  2996. print("❌ 无法找到可用端口 (8000-8010)")
  2997. print("请手动停止占用这些端口的进程")
  2998. sys.exit(1)
  2999. print("=" * 60)
  3000. print("🚀 SSO认证中心完整服务器")
  3001. print("=" * 60)
  3002. print(f"✅ 找到可用端口: {port}")
  3003. print(f"🌐 访问地址: http://localhost:{port}")
  3004. print(f"📚 API文档: http://localhost:{port}/docs")
  3005. print(f"❤️ 健康检查: http://localhost:{port}/health")
  3006. print(f"🔐 登录API: http://localhost:{port}/api/v1/auth/login")
  3007. print("=" * 60)
  3008. print("📝 前端配置:")
  3009. print(f" VITE_API_BASE_URL=http://localhost:{port}")
  3010. print("=" * 60)
  3011. print("👤 测试账号:")
  3012. print(" 用户名: admin")
  3013. print(" 密码: Admin123456")
  3014. print("=" * 60)
  3015. print("按 Ctrl+C 停止服务器")
  3016. print()
  3017. try:
  3018. uvicorn.run(
  3019. app,
  3020. host="0.0.0.0",
  3021. port=port,
  3022. log_level="info"
  3023. )
  3024. except KeyboardInterrupt:
  3025. print("\n👋 服务器已停止")
  3026. except Exception as e:
  3027. print(f"❌ 启动失败: {e}")
  3028. sys.exit(1)
  3029. @app.get("/api/v1/auth/captcha")
  3030. async def get_captcha():
  3031. """获取验证码"""
  3032. try:
  3033. # 生成验证码
  3034. captcha_text, captcha_image = generate_captcha()
  3035. # 这里应该将验证码文本存储到缓存中(Redis或内存)
  3036. # 为了简化,我们暂时返回固定的验证码
  3037. captcha_id = secrets.token_hex(16)
  3038. return ApiResponse(
  3039. code=0,
  3040. message="获取验证码成功",
  3041. data={
  3042. "captcha_id": captcha_id,
  3043. "captcha_image": captcha_image,
  3044. "captcha_text": captcha_text # 生产环境中不应该返回这个
  3045. },
  3046. timestamp=datetime.now(timezone.utc).isoformat()
  3047. ).model_dump()
  3048. except Exception as e:
  3049. print(f"生成验证码错误: {e}")
  3050. return ApiResponse(
  3051. code=500001,
  3052. message="生成验证码失败",
  3053. timestamp=datetime.now(timezone.utc).isoformat()
  3054. ).model_dump()
  3055. def generate_captcha():
  3056. """生成验证码"""
  3057. try:
  3058. import random
  3059. import string
  3060. import base64
  3061. # 生成随机验证码文本
  3062. captcha_text = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
  3063. # 创建一个简单的SVG验证码
  3064. svg_captcha = f"""
  3065. <svg width="120" height="40" xmlns="http://www.w3.org/2000/svg">
  3066. <rect width="120" height="40" fill="#f0f0f0" stroke="#ccc"/>
  3067. <text x="60" y="25" font-family="Arial" font-size="18" text-anchor="middle" fill="#333">{captcha_text}</text>
  3068. </svg>
  3069. """
  3070. svg_base64 = base64.b64encode(svg_captcha.encode('utf-8')).decode('utf-8')
  3071. return captcha_text, f"data:image/svg+xml;base64,{svg_base64}"
  3072. except Exception as e:
  3073. print(f"生成验证码失败: {e}")
  3074. # 返回默认验证码
  3075. return "1234", "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIwIiBoZWlnaHQ9IjQwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMjAiIGhlaWdodD0iNDAiIGZpbGw9IiNmMGYwZjAiIHN0cm9rZT0iI2NjYyIvPjx0ZXh0IHg9IjYwIiB5PSIyNSIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjE4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMzMzIj4xMjM0PC90ZXh0Pjwvc3ZnPg=="