From 60049a777f180be7714f159efe29bcd946133ee5 Mon Sep 17 00:00:00 2001 From: MingxuanGame Date: Tue, 7 Oct 2025 13:07:14 +0000 Subject: [PATCH] feat(auth): support change password --- app/auth.py | 14 +++++++++ app/router/auth.py | 15 +--------- app/router/private/password.py | 53 ++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 14 deletions(-) create mode 100644 app/router/private/password.py diff --git a/app/auth.py b/app/auth.py index 7ebc3a2..2d19065 100644 --- a/app/auth.py +++ b/app/auth.py @@ -60,6 +60,20 @@ def validate_username(username: str) -> list[str]: return errors +def validate_password(password: str) -> list[str]: + """验证密码""" + errors = [] + + if not password: + errors.append("Password is required") + return errors + + if len(password) < 8: + errors.append("Password must be at least 8 characters long") + + return errors + + def verify_password_legacy(plain_password: str, bcrypt_hash: str) -> bool: """ 验证密码 - 使用 osu! 的验证方式 diff --git a/app/router/auth.py b/app/router/auth.py index 12f4b6c..28929d8 100644 --- a/app/router/auth.py +++ b/app/router/auth.py @@ -10,6 +10,7 @@ from app.auth import ( get_token_by_refresh_token, get_user_by_authorization_code, store_token, + validate_password, validate_username, ) from app.config import settings @@ -68,20 +69,6 @@ def validate_email(email: str) -> list[str]: return errors -def validate_password(password: str) -> list[str]: - """验证密码""" - errors = [] - - if not password: - errors.append("Password is required") - return errors - - if len(password) < 8: - errors.append("Password must be at least 8 characters long") - - return errors - - router = APIRouter(tags=["osu! OAuth 认证"]) diff --git a/app/router/private/password.py b/app/router/private/password.py new file mode 100644 index 0000000..376b8f7 --- /dev/null +++ b/app/router/private/password.py @@ -0,0 +1,53 @@ +from typing import Annotated + +from app.auth import ( + authenticate_user, + get_password_hash, + validate_password, +) +from app.database.auth import OAuthToken +from app.database.verification import LoginSession, TrustedDevice +from app.dependencies.database import Database +from app.dependencies.user import ClientUser +from app.log import log + +from .router import router + +from fastapi import Depends, Form, HTTPException +from fastapi_limiter.depends import RateLimiter +from sqlmodel import col, delete + +logger = log("Auth") + + +@router.post( + "/password/change", + name="更改密码", + tags=["验证", "g0v0 API"], + status_code=204, + dependencies=[Depends(RateLimiter(times=3, minutes=5))], +) +async def change_password( + current_user: ClientUser, + session: Database, + current_password: Annotated[str, Form(..., description="当前密码")], + new_password: Annotated[str, Form(..., description="新密码")], +): + """更改用户密码 + + 同时删除所有的已登录会话和信任设备 + + 速率限制: 5 分钟内最多 3 次 + """ + if not await authenticate_user(session, current_user.username, current_password): + raise HTTPException(status_code=403, detail="Password incorrect") + if errors := validate_password(new_password): + raise HTTPException(status_code=400, detail="; ".join(errors)) + + async with session.begin(): + current_user.pw_bcrypt = get_password_hash(new_password) + + await session.execute(delete(TrustedDevice).where(col(TrustedDevice.user_id) == current_user.id)) + await session.execute(delete(LoginSession).where(col(LoginSession.user_id) == current_user.id)) + await session.execute(delete(OAuthToken).where(col(OAuthToken.user_id) == current_user.id)) + logger.info(f"User {current_user.id} changed password and sessions revoked")