from __future__ import annotations from datetime import datetime from fastapi import Request, Response from sqlalchemy.orm import Session from app.common.errors.app_error import AuthenticationError, ConflictAppError from app.common.security.jwt import ( clear_auth_cookies, create_access_token, create_refresh_token, decode_refresh_token, set_auth_cookies, ) from app.common.security.password import hash_password, verify_password from app.common.utils.id_gen import new_public_id from app.models.entities import InviteCode, InviteRelation, User, Wallet from app.modules.auth.repository import AuthRepository from app.modules.wallets.service import WalletService class AuthService: def __init__(self, db: Session) -> None: self.db = db self.repository = AuthRepository(db) self.wallet_service = WalletService(db) def register(self, payload, request: Request, response: Response) -> dict: if self.repository.get_user_by_account(payload.account): raise ConflictAppError("account already exists", code=10010) user = User( public_id=new_public_id("usr"), email=payload.account, password_hash=hash_password(payload.password), nickname=payload.account.split("@")[0], status=1, register_ip=request.client.host if request.client else "", last_login_ip=request.client.host if request.client else "", last_login_at=datetime.utcnow(), ) self.db.add(user) self.db.flush() self.db.add(Wallet(user_id=user.id)) self.db.flush() self._bind_invite_relation(user.id, payload.invite_code, request) self.wallet_service.try_issue_signup_reward(user.id) self.db.commit() self.db.refresh(user) self._issue_tokens(user.public_id, response) return self.serialize_user(user) def login(self, payload, request: Request, response: Response) -> dict: user = self.repository.get_user_by_account(payload.account) if not user or not verify_password(payload.password, user.password_hash): raise AuthenticationError("invalid credentials") user.last_login_at = datetime.utcnow() user.last_login_ip = request.client.host if request.client else "" self.db.commit() self._issue_tokens(user.public_id, response) return self.serialize_user(user) def refresh(self, refresh_token: str | None, response: Response) -> dict: if not refresh_token: raise AuthenticationError() try: payload = decode_refresh_token(refresh_token) except Exception as exc: # noqa: BLE001 raise AuthenticationError() from exc if payload.get("scope") != "user": raise AuthenticationError() user = self.repository.get_user_by_public_id(payload["sub"]) if not user: raise AuthenticationError() self._issue_tokens(user.public_id, response) return self.serialize_user(user) def logout(self, response: Response) -> None: clear_auth_cookies(response, prefix="user") @staticmethod def serialize_user(user: User) -> dict: return { "publicId": user.public_id, "username": user.username or "", "nickname": user.nickname, "avatarUrl": user.avatar_url, "email": user.email or "", "mobile": user.mobile or "", "status": user.status, } def _issue_tokens(self, public_id: str, response: Response) -> None: access_token = create_access_token(public_id, scope="user") refresh_token = create_refresh_token(public_id, scope="user") set_auth_cookies(response, access_token, refresh_token, prefix="user") def _bind_invite_relation( self, invitee_user_id: int, invite_code_value: str | None, request: Request, ) -> None: if not invite_code_value: return invite_code = self.db.query(InviteCode).filter( InviteCode.invite_code == invite_code_value, InviteCode.status == 1, ).first() if not invite_code: return self.db.add( InviteRelation( inviter_user_id=invite_code.user_id, invitee_user_id=invitee_user_id, invite_code_id=invite_code.id, reward_status="pending", reward_points=0, register_ip=request.client.host if request.client else "", ) ) invite_code.used_count += 1