"""Local account authentication and RBAC mapping.""" from datetime import UTC, datetime, timedelta from app.core.config import get_settings from app.core.exceptions import AppException from app.core.security import create_access_token, verify_password from app.models.entities import OperationAudit, UserAccount from app.repositories.users import UserRepository from app.schemas.auth import TokenData, UserData MAX_LOGIN_FAILURES = 5 LOCK_MINUTES = 30 class AuthService: def __init__(self, session) -> None: # noqa: ANN001 self.repo = UserRepository(session) self.session = session async def login(self, username: str, password: str) -> TokenData: user = await self.repo.get_by_username(username) now = datetime.now(UTC) if user is None or user.status != "启用": self.session.add( OperationAudit( actor_emp_no=username[:20], action="auth.login", result="failure", audit_metadata={"reason": "invalid_credentials"}, ) ) await self.session.commit() raise AppException("工号/企业邮箱或密码错误", code=401, status_code=401) if user.locked_until and user.locked_until > now: self.session.add( OperationAudit( actor_user_id=user.id, actor_emp_no=user.emp_no, action="auth.login", result="failure", audit_metadata={"reason": "account_locked"}, ) ) await self.session.commit() raise AppException("账号已锁定,请稍后重试", code=423, status_code=423) if not verify_password(password, user.password_hash): user.login_failed_count += 1 if user.login_failed_count >= MAX_LOGIN_FAILURES: user.locked_until = now + timedelta(minutes=LOCK_MINUTES) user.login_failed_count = 0 self.session.add( OperationAudit( actor_user_id=user.id, actor_emp_no=user.emp_no, action="auth.login", result="failure", audit_metadata={"reason": "invalid_credentials"}, ) ) await self.session.flush() await self.session.commit() raise AppException("工号/企业邮箱或密码错误", code=401, status_code=401) user.login_failed_count = 0 user.locked_until = None user.last_login_at = now self.session.add(OperationAudit(actor_user_id=user.id, actor_emp_no=user.emp_no, action="auth.login")) await self.session.flush() return await self._token_data(user) async def _token_data(self, user: UserAccount) -> TokenData: permissions = await self.repo.permissions_for_role(user.role) permission_keys = sorted({permission.func_key for permission in permissions}) settings = get_settings() return TokenData( access_token=create_access_token(user.id, role=user.role), expires_in=settings.jwt_expire_minutes * 60, user=self.to_user_data(user, permission_keys), ) @staticmethod def to_user_data(user: UserAccount, permissions: list[str] | None = None) -> UserData: return UserData( id=user.id, emp_no=user.emp_no, email=user.email, name=user.name, dept=user.dept, position=user.position, role=user.role, status=user.status, dingtalk_id=user.dingtalk_id, dingtalk_job_number=user.dingtalk_job_number, dingtalk_title=user.dingtalk_title, dingtalk_dept_id=user.dingtalk_dept_id, dingtalk_is_dept_manager=user.dingtalk_is_dept_manager, permissions=permissions or [], ) async def me(self, user: UserAccount) -> UserData: permissions = sorted({permission.func_key for permission in await self.repo.permissions_for_role(user.role)}) return self.to_user_data(user, permissions)