169 lines
6.8 KiB
Python
169 lines
6.8 KiB
Python
from typing import Optional
|
|
from uuid import uuid4
|
|
|
|
from fastapi import HTTPException, status
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.config import settings
|
|
from app.core.security import create_access_token, decode_access_token, get_password_hash, verify_password
|
|
from app.models.user import User
|
|
from app.repositories.user_repository import UserRepository
|
|
from app.services.audit_log_service import AuditLogService
|
|
from app.services.tenant_service import DEFAULT_TENANT_ID, TenantService
|
|
|
|
|
|
DEFAULT_ADMIN_USERNAME = "admin@example.com"
|
|
DEFAULT_ADMIN_PASSWORD = "123456"
|
|
DEFAULT_ADMIN_DISPLAY_NAME = "管理员"
|
|
DEFAULT_TENANT_ID = "tenant-demo-001"
|
|
|
|
|
|
class AuthService:
|
|
def __init__(self, db: Session):
|
|
self.db = db
|
|
self.repository = UserRepository(db)
|
|
self.audit_log_service = AuditLogService(db)
|
|
|
|
def register(self, username: str, password: str, display_name: str) -> dict:
|
|
normalized_username = username.strip().lower()
|
|
normalized_display_name = display_name.strip()
|
|
normalized_password = password.strip()
|
|
|
|
if not normalized_display_name:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="display name is required")
|
|
if len(normalized_password) < 6:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="password must be at least 6 characters")
|
|
if self.repository.get_by_username(normalized_username) is not None:
|
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="username already exists")
|
|
|
|
TenantService(self.db).ensure_default_tenant()
|
|
user = User(
|
|
id=f"user-{uuid4().hex[:12]}",
|
|
tenant_id=DEFAULT_TENANT_ID,
|
|
username=normalized_username,
|
|
display_name=normalized_display_name,
|
|
password_hash=get_password_hash(normalized_password),
|
|
role="member",
|
|
status="active",
|
|
)
|
|
created = self.repository.create(user)
|
|
self.audit_log_service.record_event(
|
|
tenant_id=created.tenant_id,
|
|
user_id=created.id,
|
|
action="register",
|
|
resource_type="user",
|
|
resource_id=created.id,
|
|
detail_json={"username": created.username, "display_name": created.display_name, "role": created.role},
|
|
)
|
|
return self._to_user_info(created)
|
|
|
|
def login(self, account: str, password: str) -> dict:
|
|
normalized_account = account.strip().lower()
|
|
user = self.repository.get_by_username(normalized_account)
|
|
|
|
if user is None or not verify_password(password, user.password_hash):
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="账号或密码错误")
|
|
|
|
token = create_access_token(user.id)
|
|
self.audit_log_service.record_event(
|
|
tenant_id=user.tenant_id,
|
|
user_id=user.id,
|
|
action="login",
|
|
resource_type="user",
|
|
resource_id=user.id,
|
|
detail_json={"username": user.username},
|
|
)
|
|
return {
|
|
"access_token": token,
|
|
"token_type": "Bearer",
|
|
"expires_in": settings.access_token_expire_minutes * 60,
|
|
"user": self._to_user_info(user),
|
|
}
|
|
|
|
def get_current_user(self, authorization: Optional[str]) -> dict:
|
|
user = self._require_user_from_authorization(authorization)
|
|
return self._to_user_info(user)
|
|
|
|
def update_current_user(self, authorization: Optional[str], display_name: str, password: Optional[str]) -> dict:
|
|
user = self._require_user_from_authorization(authorization)
|
|
normalized_display_name = display_name.strip()
|
|
if not normalized_display_name:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="display name is required")
|
|
user.display_name = normalized_display_name
|
|
password_updated = False
|
|
if password is not None and password.strip():
|
|
normalized_password = password.strip()
|
|
if len(normalized_password) < 6:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="password must be at least 6 characters")
|
|
user.password_hash = get_password_hash(normalized_password)
|
|
password_updated = True
|
|
updated = self.repository.update(user)
|
|
self.audit_log_service.record_event(
|
|
tenant_id=updated.tenant_id,
|
|
user_id=updated.id,
|
|
action="update_profile",
|
|
resource_type="user",
|
|
resource_id=updated.id,
|
|
detail_json={
|
|
"username": updated.username,
|
|
"display_name": updated.display_name,
|
|
"password_updated": password_updated,
|
|
},
|
|
)
|
|
return self._to_user_info(updated)
|
|
|
|
def require_current_user(self, authorization: Optional[str]) -> dict:
|
|
user = self._require_user_from_authorization(authorization)
|
|
return self._to_user_info(user)
|
|
|
|
def seed_default_admin(self) -> dict:
|
|
TenantService(self.db).ensure_default_tenant()
|
|
existing = self.repository.get_by_username(DEFAULT_ADMIN_USERNAME)
|
|
if existing is not None:
|
|
return {
|
|
"username": DEFAULT_ADMIN_USERNAME,
|
|
"password": None,
|
|
"created": False,
|
|
}
|
|
|
|
admin = User(
|
|
id="user-demo-001",
|
|
tenant_id=DEFAULT_TENANT_ID,
|
|
username=DEFAULT_ADMIN_USERNAME,
|
|
display_name=DEFAULT_ADMIN_DISPLAY_NAME,
|
|
password_hash=get_password_hash(DEFAULT_ADMIN_PASSWORD),
|
|
role="admin",
|
|
status="active",
|
|
)
|
|
self.repository.create(admin)
|
|
return {
|
|
"username": DEFAULT_ADMIN_USERNAME,
|
|
"password": DEFAULT_ADMIN_PASSWORD,
|
|
"created": True,
|
|
}
|
|
|
|
def _require_user_from_authorization(self, authorization: Optional[str]) -> User:
|
|
if not authorization or not authorization.startswith("Bearer "):
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="missing bearer token")
|
|
|
|
token = authorization.split(" ", 1)[1].strip()
|
|
subject = decode_access_token(token)
|
|
if subject is None:
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid token")
|
|
|
|
user = self.repository.get_by_id(subject)
|
|
if user is None or user.status != "active":
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="user not found")
|
|
|
|
return user
|
|
|
|
def _to_user_info(self, user: User) -> dict:
|
|
return {
|
|
"id": user.id,
|
|
"tenant_id": user.tenant_id,
|
|
"username": user.username,
|
|
"display_name": user.display_name,
|
|
"role": user.role,
|
|
"status": user.status,
|
|
}
|