@@ -20,16 +20,24 @@ class RegisterRequest(BaseModel):
2020 email : str = Field (..., description = "邮箱" )
2121 username : str = Field (..., min_length = 2 , max_length = 50 )
2222 password : str = Field (..., min_length = 6 , max_length = 100 )
23+ email_code : str | None = Field (default = None , description = "邮箱验证码(开启验证时必填)" )
2324
2425class LoginRequest (BaseModel ):
2526 email_or_username : str = Field (..., description = "邮箱或用户名" )
2627 password : str = Field (...)
2728
2829class TokenResponse (BaseModel ):
2930 access_token : str
31+ refresh_token : str | None = None
3032 token_type : str = "bearer"
3133 user : dict
3234
35+ class RefreshRequest (BaseModel ):
36+ refresh_token : str = Field (..., description = "Refresh Token" )
37+
38+ class SendCodeRequest (BaseModel ):
39+ email : str = Field (..., description = "接收验证码的邮箱" )
40+
3341class ProjectRequest (BaseModel ):
3442 name : str = Field (..., min_length = 1 , max_length = 200 )
3543 description : str = Field (default = "" , max_length = 1000 )
@@ -69,17 +77,23 @@ def _project_dict(p) -> dict:
6977
7078@router .post ("/auth/register" , tags = ["认证" ])
7179async def register (req : RegisterRequest , db : Session = Depends (get_db )) -> TokenResponse :
80+ # 开启邮箱验证时必须校验验证码
81+ if service .REQUIRE_EMAIL_VERIFICATION :
82+ if not req .email_code :
83+ raise HTTPException (status_code = status .HTTP_400_BAD_REQUEST , detail = "请先获取邮箱验证码" )
84+ if not service .verify_email_code (db , req .email , req .email_code ):
85+ raise HTTPException (status_code = status .HTTP_400_BAD_REQUEST , detail = "验证码错误或已过期" )
7286 try :
7387 user = service .register_user (db , req .email , req .username , req .password )
7488 except ValueError as e :
7589 raise HTTPException (status_code = status .HTTP_409_CONFLICT , detail = str (e ))
76- token = service .create_access_token ( user . id , user . username , user . role )
77- return TokenResponse (access_token = token , user = _user_dict (user ))
90+ access_token , refresh_token = service .create_token_pair ( db , user )
91+ return TokenResponse (access_token = access_token , refresh_token = refresh_token , user = _user_dict (user ))
7892
7993@router .post ("/auth/login" , tags = ["认证" ])
8094async def login (req : LoginRequest , db : Session = Depends (get_db )) -> TokenResponse :
81- # 锁定检查
82- locked , remaining = service .is_account_locked (req .email_or_username )
95+ # 锁定检查(DB持久化)
96+ locked , remaining = service .is_account_locked (db , req .email_or_username )
8397 if locked :
8498 raise HTTPException (
8599 status_code = status .HTTP_429_TOO_MANY_REQUESTS ,
@@ -88,7 +102,7 @@ async def login(req: LoginRequest, db: Session = Depends(get_db)) -> TokenRespon
88102
89103 user = service .authenticate_user (db , req .email_or_username , req .password )
90104 if not user :
91- failures = service .record_login_failure (req .email_or_username )
105+ failures = service .record_login_failure (db , req .email_or_username )
92106 remain_chances = max (0 , service ._MAX_FAILURES - failures )
93107 if remain_chances > 0 :
94108 raise HTTPException (
@@ -101,15 +115,41 @@ async def login(req: LoginRequest, db: Session = Depends(get_db)) -> TokenRespon
101115 detail = f"连续失败次数过多,账号已锁定 { service ._LOCK_SECONDS // 60 } 分钟"
102116 )
103117
104- service .clear_login_failures (req .email_or_username )
105- token = service .create_access_token ( user . id , user . username , user . role )
106- return TokenResponse (access_token = token , user = _user_dict (user ))
118+ service .clear_login_failures (db , req .email_or_username )
119+ access_token , refresh_token = service .create_token_pair ( db , user )
120+ return TokenResponse (access_token = access_token , refresh_token = refresh_token , user = _user_dict (user ))
107121
108122@router .get ("/auth/me" , tags = ["认证" ])
109123async def get_me (user : User = Depends (get_current_user )) -> dict :
110124 return _user_dict (user )
111125
112126
127+ @router .post ("/auth/refresh" , tags = ["认证" ])
128+ async def refresh_token_endpoint (req : RefreshRequest , db : Session = Depends (get_db )) -> TokenResponse :
129+ """用 Refresh Token 换取新的 Access Token + 新 Refresh Token(轮换机制)。"""
130+ result = service .verify_and_rotate_refresh_token (db , req .refresh_token )
131+ if not result :
132+ raise HTTPException (
133+ status_code = status .HTTP_401_UNAUTHORIZED ,
134+ detail = "refresh_token 无效或已过期,请重新登录" ,
135+ )
136+ access_token , new_refresh , user = result
137+ return TokenResponse (access_token = access_token , refresh_token = new_refresh , user = _user_dict (user ))
138+
139+
140+ @router .post ("/auth/send-code" , tags = ["认证" ])
141+ async def send_verification_code (req : SendCodeRequest , db : Session = Depends (get_db )) -> dict :
142+ """向指定邮箱发送6位注册验证码(10分钟有效)。"""
143+ code = service .create_verification_code (db , req .email )
144+ sent = service .send_verification_email (req .email , code )
145+ if not sent :
146+ # SMTP 未配置时,开发模式下直接返回验证码(生产环境应删除此行)
147+ if not service .REQUIRE_EMAIL_VERIFICATION :
148+ return {"ok" : True , "message" : "开发模式:验证码已生成" , "dev_code" : code }
149+ raise HTTPException (status_code = 503 , detail = "邮件服务未配置,请联系管理员" )
150+ return {"ok" : True , "message" : f"验证码已发送至 { req .email } " }
151+
152+
113153# ── 项目端点 ──
114154
115155@router .post ("/projects" , tags = ["项目" ])
0 commit comments