2026-09-04 10:20:40 +08:00

103 lines
4.1 KiB
Python

"""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)