feat: Add base project structure with API and web components

This commit is contained in:
Ke Sun
2025-12-02 20:28:01 +08:00
parent f3de6d6cc9
commit c1adc62ec6
817 changed files with 111226 additions and 106 deletions

View File

@@ -0,0 +1,570 @@
import datetime
import secrets
import string
from sqlalchemy.orm import Session
import uuid
from app.models.user_model import User
from app.repositories import user_repository
from app.schemas.user_schema import UserCreate
from app.schemas.tenant_schema import TenantCreate
from app.services.tenant_service import TenantService
from app.services.session_service import SessionService
from app.core.security import get_password_hash, verify_password
from app.core.config import settings
from app.core.logging_config import get_business_logger
from app.core.exceptions import BusinessException, PermissionDeniedException
from app.core.error_codes import BizCode
# from app.services import workspace_service
# from app.schemas.workspace_schema import WorkspaceCreate
# 获取业务逻辑专用日志器
business_logger = get_business_logger()
def create_initial_superuser(db: Session):
business_logger.info("检查并创建初始超级用户")
superuser = user_repository.get_superuser(db)
if superuser:
business_logger.info("超级用户已存在,跳过创建")
return
user_in = UserCreate(
username=settings.FIRST_SUPERUSER_USERNAME,
email=settings.FIRST_SUPERUSER_EMAIL,
password=settings.FIRST_SUPERUSER_PASSWORD,
)
try:
business_logger.debug("开始创建初始租户")
# Create a default tenant for the superuser
default_tenant = TenantCreate(
name=f"{user_in.username}'s Tenant",
description=f"Default tenant for {user_in.username}",
)
# Create tenant service and create tenant with user assignment
tenant_service = TenantService(db)
tenant = tenant_service.create_tenant(default_tenant)
db.flush()
business_logger.debug("开始创建初始超级用户")
hashed_password = get_password_hash(user_in.password)
superuser = user_repository.create_user(
db=db, user=user_in, hashed_password=hashed_password, is_superuser=True,
tenant_id=tenant.id
)
db.commit()
db.refresh(superuser)
business_logger.info(f"初始超级用户创建成功: {superuser.username} (ID: {superuser.id})")
return superuser
except Exception as e:
business_logger.error(f"初始超级用户创建失败: {str(e)}")
db.rollback()
raise BusinessException(
f"初始超级用户创建失败: {str(e)}",
code=BizCode.DB_ERROR,
context={"username": username, "email": email},
cause=e
)
def create_user(db: Session, user: UserCreate) -> User:
business_logger.info(f"创建用户: {user.username}, email: {user.email}")
try:
# 检查用户名是否已存在
business_logger.debug(f"检查用户名是否已存在: {user.username}")
db_user_by_username = user_repository.get_user_by_username(db, username=user.username)
if db_user_by_username:
business_logger.warning(f"用户名已存在: {user.username}")
raise BusinessException(
"用户名已存在",
code=BizCode.DUPLICATE_NAME,
context={"username": user.username, "email": user.email}
)
# 检查邮箱是否已注册
business_logger.debug(f"检查邮箱是否已注册: {user.email}")
db_user_by_email = user_repository.get_user_by_email(db, email=user.email)
if db_user_by_email:
business_logger.warning(f"邮箱已注册: {user.email}")
raise BusinessException(
"邮箱已注册",
code=BizCode.DUPLICATE_NAME,
context={"email": user.email, "username": user.username}
)
# 创建普通用户,需要有默认租户
business_logger.debug(f"开始创建用户: {user.username}")
hashed_password = get_password_hash(user.password)
# 获取默认租户(第一个活跃租户)
from app.repositories.tenant_repository import TenantRepository
tenant_repo = TenantRepository(db)
tenants = tenant_repo.get_tenants(skip=0, limit=1, is_active=True)
if not tenants:
business_logger.error("系统中没有可用的租户")
raise BusinessException(
"系统配置错误:没有可用的租户",
code=BizCode.TENANT_NOT_FOUND,
context={"username": user.username, "email": user.email}
)
default_tenant = tenants[0]
new_user = user_repository.create_user(
db=db, user=user, hashed_password=hashed_password,
tenant_id=default_tenant.id, is_superuser=False
)
db.commit()
db.refresh(new_user)
business_logger.info(f"用户创建成功: {new_user.username} (ID: {new_user.id})")
return new_user
except Exception as e:
business_logger.error(f"用户创建失败: {user.username} - {str(e)}")
db.rollback()
raise BusinessException(
f"用户创建失败: {user.username} - {str(e)}",
code=BizCode.DB_ERROR,
context={"username": user.username, "email": user.email},
cause=e
)
def create_superuser(db: Session, user: UserCreate, current_user: User) -> User:
business_logger.info(f"创建超级管理员: {user.username}, email: {user.email}")
# 检查当前用户是否为超级管理员
from app.core.permissions import permission_service, Subject
subject = Subject.from_user(current_user)
try:
permission_service.check_superuser(
subject,
error_message="只有超级管理员才能创建超级管理员用户"
)
except PermissionDeniedException as e:
business_logger.warning(f"非超级管理员尝试创建超级管理员用户: {user.username}")
raise BusinessException(
str(e),
code=BizCode.FORBIDDEN,
context={
"current_user_id": str(current_user.id),
"current_user_username": current_user.username,
"target_username": user.username
}
)
try:
# 检查用户名是否已存在
business_logger.debug(f"检查用户名是否已存在: {user.username}")
db_user_by_username = user_repository.get_user_by_username(db, username=user.username)
if db_user_by_username:
business_logger.warning(f"用户名已存在: {user.username}")
raise BusinessException(
"用户名已存在",
code=BizCode.DUPLICATE_NAME,
context={
"username": user.username,
"email": user.email,
"created_by": str(current_user.id)
}
)
# 检查邮箱是否已注册
business_logger.debug(f"检查邮箱是否已注册: {user.email}")
db_user_by_email = user_repository.get_user_by_email(db, email=user.email)
if db_user_by_email:
business_logger.warning(f"邮箱已注册: {user.email}")
raise BusinessException(
"邮箱已注册",
code=BizCode.DUPLICATE_NAME,
context={
"email": user.email,
"username": user.username,
"created_by": str(current_user.id)
}
)
# 创建超级管理员用户并加入当前用户的租户
business_logger.debug(f"开始创建超级管理员: {user.username}")
hashed_password = get_password_hash(user.password)
new_user = user_repository.create_user(
db=db, user=user, hashed_password=hashed_password,
tenant_id=current_user.tenant_id, is_superuser=True
)
db.commit()
db.refresh(new_user)
business_logger.info(f"超级管理员创建成功: {new_user.username} (ID: {new_user.id}), 已加入租户: {current_user.tenant_id}")
return new_user
except Exception as e:
business_logger.error(f"超级管理员创建失败: {user.username} - {str(e)}")
db.rollback()
raise BusinessException(
f"超级管理员创建失败: {user.username} - {str(e)}",
code=BizCode.DB_ERROR,
context={
"username": user.username,
"email": user.email,
"created_by": str(current_user.id),
"tenant_id": str(current_user.tenant_id)
},
cause=e
)
def deactivate_user(db: Session, user_id_to_deactivate: uuid.UUID, current_user: User) -> User:
business_logger.info(f"停用用户: user_id={user_id_to_deactivate}, 操作者: {current_user.username}")
try:
# 查找用户
business_logger.debug(f"查找待停用用户: {user_id_to_deactivate}")
db_user = user_repository.get_user_by_id(db, user_id=user_id_to_deactivate)
if not db_user:
business_logger.warning(f"用户不存在: {user_id_to_deactivate}")
raise BusinessException(
"用户不存在",
code=BizCode.USER_NOT_FOUND,
context={"user_id": str(user_id_to_deactivate)}
)
# 权限检查 using permission service
from app.core.permissions import permission_service, Subject, Resource, Action
subject = Subject.from_user(current_user)
resource = Resource.from_user(db_user)
try:
permission_service.require_permission(
subject,
Action.DEACTIVATE,
resource,
error_message="没有权限停用该用户"
)
except PermissionDeniedException as e:
business_logger.warning(f"权限不足: 用户 {current_user.username} 尝试停用用户 {user_id_to_deactivate}")
raise BusinessException(
str(e),
code=BizCode.FORBIDDEN,
context={
"current_user_id": str(current_user.id),
"current_user_username": current_user.username,
"target_user_id": str(user_id_to_deactivate)
}
)
# 检查用户类型,如果是超级管理员,判断一下不是唯一的一个
if db_user.is_superuser:
is_only_superuser = user_repository.check_superuser_only(db)
if is_only_superuser:
business_logger.warning(f"停用超级管理员用户: {db_user.username} (ID: {user_id_to_deactivate})")
raise BusinessException(
"不能停用唯一的超级管理员用户",
code=BizCode.FORBIDDEN,
context={
"user_id": str(user_id_to_deactivate),
"username": db_user.username
}
)
# 停用用户
business_logger.debug(f"执行用户停用: {db_user.username} (ID: {user_id_to_deactivate})")
db_user.is_active = False
db.add(db_user)
db.commit()
db.refresh(db_user)
business_logger.info(f"用户停用成功: {db_user.username} (ID: {user_id_to_deactivate})")
return db_user
except Exception as e:
business_logger.error(f"用户停用失败: user_id={user_id_to_deactivate} - {str(e)}")
db.rollback()
if isinstance(e, BusinessException):
raise e
raise BusinessException(f"{str(e)}", code=BizCode.DB_ERROR)
def activate_user(db: Session, user_id_to_activate: uuid.UUID, current_user: User) -> User:
business_logger.info(f"激活用户: user_id={user_id_to_activate}, 操作者: {current_user.username}")
try:
# 查找用户
business_logger.debug(f"查找待激活用户: {user_id_to_activate}")
db_user = user_repository.get_user_by_id(db, user_id=user_id_to_activate)
if not db_user:
business_logger.warning(f"用户不存在: {user_id_to_activate}")
raise BusinessException("用户不存在", code=BizCode.USER_NOT_FOUND)
# 权限检查 using permission service
from app.core.permissions import permission_service, Subject, Resource, Action
subject = Subject.from_user(current_user)
resource = Resource.from_user(db_user)
try:
permission_service.require_permission(
subject,
Action.ACTIVATE,
resource,
error_message="没有权限激活该用户"
)
except PermissionDeniedException as e:
business_logger.warning(f"权限不足: 用户 {current_user.username} 尝试激活用户 {user_id_to_activate}")
raise BusinessException(str(e), code=BizCode.FORBIDDEN)
# 激活用户
business_logger.debug(f"执行用户激活: {db_user.username} (ID: {user_id_to_activate})")
db_user.is_active = True
db.add(db_user)
db.commit()
db.refresh(db_user)
business_logger.info(f"用户激活成功: {db_user.username} (ID: {user_id_to_activate})")
return db_user
except Exception as e:
business_logger.error(f"用户激活失败: user_id={user_id_to_activate} - {str(e)}")
db.rollback()
raise BusinessException(f"用户激活失败: user_id={user_id_to_activate} - {str(e)}", code=BizCode.DB_ERROR)
def get_user(db: Session, user_id: uuid.UUID, current_user: User) -> User:
business_logger.info(f"获取用户信息: user_id={user_id}, 操作者: {current_user.username}")
try:
# 查找用户
business_logger.debug(f"查找用户: {user_id}")
db_user = user_repository.get_user_by_id(db, user_id=user_id)
if not db_user:
business_logger.warning(f"用户不存在: {user_id}")
raise BusinessException("用户不存在", code=BizCode.USER_NOT_FOUND)
# 权限检查 using permission service
from app.core.permissions import permission_service, Subject, Resource, Action
subject = Subject.from_user(current_user)
resource = Resource.from_user(db_user)
try:
permission_service.require_permission(
subject,
Action.READ,
resource,
error_message="没有权限获取该用户信息"
)
except PermissionDeniedException as e:
business_logger.warning(f"权限不足: 用户 {current_user.username} 尝试获取用户 {user_id} 信息")
raise BusinessException(str(e), code=BizCode.FORBIDDEN)
# 返回用户信息
business_logger.debug(f"返回用户信息: {db_user.username} (ID: {user_id})")
return db_user
except Exception as e:
business_logger.error(f"获取用户信息失败: user_id={user_id} - {str(e)}")
raise BusinessException(f"获取用户信息失败: user_id={user_id} - {str(e)}", code=BizCode.DB_ERROR)
def get_tenant_superusers(db: Session, current_user: User, include_inactive: bool = True) -> list[User]:
"""获取当前租户下的超管账号列表"""
business_logger.info(f"获取租户超管列表: tenant_id={current_user.tenant_id}, 请求者: {current_user.username}, include_inactive={include_inactive}")
try:
# 检查当前用户是否有权限查看(只有超管才能查看超管列表)
from app.core.permissions import permission_service, Subject
subject = Subject.from_user(current_user)
try:
permission_service.check_superuser(
subject,
error_message="只有超级管理员才能查看超管列表"
)
except PermissionDeniedException as e:
business_logger.warning(f"非超级管理员尝试查看超管列表: {current_user.username}")
raise BusinessException(str(e), code=BizCode.FORBIDDEN)
# 检查用户是否有租户
if not current_user.tenant_id:
business_logger.warning(f"用户没有租户信息: {current_user.username}")
raise BusinessException("用户没有租户信息", code=BizCode.TENANT_NOT_FOUND)
# 获取租户下的超管列表
business_logger.debug(f"查询租户超管: tenant_id={current_user.tenant_id}, include_inactive={include_inactive}")
is_active_filter = None if include_inactive else True
superusers = user_repository.get_superusers_by_tenant(
db=db,
tenant_id=current_user.tenant_id,
is_active=is_active_filter
)
business_logger.info(f"租户超管查询成功: tenant_id={current_user.tenant_id}, count={len(superusers)}")
return superusers
except Exception as e:
business_logger.error(f"获取租户超管列表失败: tenant_id={current_user.tenant_id} - {str(e)}")
raise BusinessException(f"获取租户超管列表失败: tenant_id={current_user.tenant_id} - {str(e)}", code=BizCode.DB_ERROR)
def update_last_login_time(db: Session, user_id: uuid.UUID) -> User:
"""更新用户的最后登录时间"""
business_logger.info(f"更新用户最后登录时间: user_id={user_id}")
try:
# 获取用户
db_user = user_repository.get_user_by_id(db=db, user_id=user_id)
if not db_user:
business_logger.warning(f"用户不存在: {user_id}")
raise BusinessException("用户不存在", code=BizCode.USER_NOT_FOUND)
# 更新最后登录时间
db_user.last_login_at = datetime.datetime.now()
db.commit()
db.refresh(db_user)
business_logger.info(f"用户最后登录时间更新成功: {db_user.username} (ID: {user_id})")
return db_user
except HTTPException:
raise
except Exception as e:
business_logger.error(f"更新用户最后登录时间失败: user_id={user_id} - {str(e)}")
db.rollback()
raise
async def change_password(db: Session, user_id: uuid.UUID, old_password: str, new_password: str, current_user: User) -> User:
"""普通用户修改自己的密码"""
business_logger.info(f"用户修改密码请求: user_id={user_id}, current_user={current_user.id}")
# 检查权限:只能修改自己的密码
if current_user.id != user_id:
business_logger.warning(f"用户尝试修改他人密码: current_user={current_user.id}, target_user={user_id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You can only change your own password"
)
try:
# 获取用户
db_user = user_repository.get_user_by_id(db=db, user_id=user_id)
if not db_user:
business_logger.warning(f"用户不存在: {user_id}")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
)
# 验证旧密码
if not verify_password(old_password, db_user.hashed_password):
business_logger.warning(f"用户旧密码验证失败: {user_id}")
raise BusinessException("当前密码不正确", code=BizCode.VALIDATION_FAILED)
# 更新密码
db_user.hashed_password = get_password_hash(new_password)
db.commit()
db.refresh(db_user)
# 使所有旧 tokens 失效
await SessionService.invalidate_all_user_tokens(str(user_id))
business_logger.info(f"用户密码修改成功: {db_user.username} (ID: {user_id})")
return db_user
except Exception as e:
business_logger.error(f"修改用户密码失败: user_id={user_id} - {str(e)}")
db.rollback()
raise BusinessException(f"修改用户密码失败: user_id={user_id} - {str(e)}", code=BizCode.DB_ERROR)
async def admin_change_password(db: Session, target_user_id: uuid.UUID, new_password: str = None, current_user: User = None) -> tuple[User, str]:
"""
超级管理员修改指定用户的密码
Args:
db: 数据库会话
target_user_id: 目标用户ID
new_password: 新密码如果为None则自动生成随机密码
current_user: 当前用户(超级管理员)
Returns:
tuple[User, str]: (更新后的用户对象, 实际使用的密码)
"""
business_logger.info(f"管理员修改用户密码请求: admin={current_user.id}, target_user={target_user_id}")
# 检查权限:只有超级管理员可以修改他人密码
from app.core.permissions import permission_service, Subject
subject = Subject.from_user(current_user)
try:
permission_service.check_superuser(
subject,
error_message="只有超级管理员可以修改他人密码"
)
except PermissionDeniedException as e:
business_logger.warning(f"非超管用户尝试修改他人密码: current_user={current_user.id}")
raise BusinessException(str(e), code=BizCode.FORBIDDEN)
try:
# 获取目标用户
target_user = user_repository.get_user_by_id(db=db, user_id=target_user_id)
if not target_user:
business_logger.warning(f"目标用户不存在: {target_user_id}")
raise BusinessException("目标用户不存在", code=BizCode.USER_NOT_FOUND)
# 检查租户权限:超管只能修改同租户用户的密码
if current_user.tenant_id != target_user.tenant_id:
business_logger.warning(f"跨租户密码修改尝试: admin_tenant={current_user.tenant_id}, target_tenant={target_user.tenant_id}")
raise BusinessException("不可跨租户修改用户密码", code=BizCode.FORBIDDEN)
# 如果没有提供新密码,则生成随机密码
actual_password = new_password if new_password else generate_random_password()
# 更新密码
target_user.hashed_password = get_password_hash(actual_password)
db.commit()
db.refresh(target_user)
# 使所有旧 tokens 失效
await SessionService.invalidate_all_user_tokens(str(target_user_id))
password_type = "指定密码" if new_password else "随机生成密码"
business_logger.info(f"管理员修改用户密码成功: admin={current_user.username}, target={target_user.username} (ID: {target_user_id}), 类型={password_type}")
return target_user, actual_password
except Exception as e:
business_logger.error(f"管理员修改用户密码失败: admin={current_user.id}, target_user={target_user_id} - {str(e)}")
db.rollback()
raise BusinessException(f"管理员修改用户密码失败: admin={current_user.id}, target_user={target_user_id} - {str(e)}", code=BizCode.DB_ERROR)
def generate_random_password(length: int = 12) -> str:
"""
生成随机密码
Args:
length: 密码长度默认12位
Returns:
str: 生成的随机密码
"""
# 确保密码包含大小写字母、数字和特殊字符
lowercase = string.ascii_lowercase
uppercase = string.ascii_uppercase
digits = string.digits
special_chars = "!@#$%^&*"
# 确保至少包含每种字符类型
password = [
secrets.choice(lowercase),
secrets.choice(uppercase),
secrets.choice(digits),
secrets.choice(special_chars)
]
# 填充剩余长度
all_chars = lowercase + uppercase + digits + special_chars
for _ in range(length - 4):
password.append(secrets.choice(all_chars))
# 打乱顺序
secrets.SystemRandom().shuffle(password)
return ''.join(password)