mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-21 12:52:26 +00:00
chore: 完成多批次代码优化与重构
- 重构工作流模块目录结构,迁移代码文件 - 修复类型断言空值安全问题,添加 ! 操作符 - 优化样式类名,替换 flex-cc 为标准 flex 工具类 - 更新路由标签简化文案,移除冗余注释 - 调整 ruff 配置,放宽行长度限制 - 更新 README 与多语言文案,优化项目描述 - 修复表单、图表组件的类型与样式问题 - 简化搜索表单、数据卡片的布局代码
This commit is contained in:
@@ -54,7 +54,7 @@ from .service import (
|
||||
TenantRegisterService,
|
||||
)
|
||||
|
||||
AuthRouter = APIRouter(route_class=OperationLogRoute, prefix="/auth", tags=["系统管理/认证授权"])
|
||||
AuthRouter = APIRouter(route_class=OperationLogRoute, prefix="/auth", tags=["认证授权"])
|
||||
|
||||
_AUTH_TENANTS_NS = "auth_tenants"
|
||||
|
||||
@@ -85,9 +85,7 @@ async def login_for_access_token_controller(
|
||||
异常:
|
||||
- CustomException: 认证失败时抛出异常。
|
||||
"""
|
||||
login_result = await LoginService.authenticate_user_service(
|
||||
request=request, redis=redis, login_form=login_form, db=db
|
||||
)
|
||||
login_result = await LoginService.authenticate_user_service(request=request, redis=redis, login_form=login_form, db=db)
|
||||
|
||||
logger.info(f"用户{login_form.username}登录成功")
|
||||
|
||||
@@ -121,9 +119,7 @@ async def get_new_token_controller(
|
||||
异常:
|
||||
- CustomException: 刷新令牌失败时抛出异常。
|
||||
"""
|
||||
new_token = await LoginService.refresh_token_service(
|
||||
db=db, redis=redis, refresh_token=payload
|
||||
)
|
||||
new_token = await LoginService.refresh_token_service(db=db, redis=redis, refresh_token=payload)
|
||||
return SuccessResponse(data=new_token, msg="刷新成功")
|
||||
|
||||
|
||||
@@ -229,9 +225,7 @@ async def get_auto_login_token_controller(
|
||||
- AutoLoginTokenSchema: 免登录Token和用户信息
|
||||
"""
|
||||
tenant_id = None if auth.user.is_superuser else auth.user.tenant_id
|
||||
result = await AutoLoginService.create_auto_login_token_service(
|
||||
redis=redis, db=db, user_id=user_id, tenant_id=tenant_id
|
||||
)
|
||||
result = await AutoLoginService.create_auto_login_token_service(redis=redis, db=db, user_id=user_id, tenant_id=tenant_id)
|
||||
return SuccessResponse(data=result, msg="获取成功")
|
||||
|
||||
|
||||
@@ -258,9 +252,7 @@ async def auto_login_controller(
|
||||
返回:
|
||||
- JWTOutSchema: JWT令牌信息
|
||||
"""
|
||||
login_token = await AutoLoginService.auto_login_service(
|
||||
request=request, redis=redis, db=db, token=token
|
||||
)
|
||||
login_token = await AutoLoginService.auto_login_service(request=request, redis=redis, db=db, token=token)
|
||||
logger.info("用户免登录成功")
|
||||
return SuccessResponse(data=login_token, msg="登录成功")
|
||||
|
||||
@@ -291,9 +283,7 @@ async def select_tenant_controller(
|
||||
返回:
|
||||
- SelectTenantOutSchema: 包含新令牌的响应
|
||||
"""
|
||||
result = await LoginService.select_tenant_service(
|
||||
request=request, redis=redis, auth=auth, tenant_id=data.tenant_id
|
||||
)
|
||||
result = await LoginService.select_tenant_service(request=request, redis=redis, auth=auth, tenant_id=data.tenant_id)
|
||||
await FastAPICache.clear(namespace=_AUTH_TENANTS_NS)
|
||||
return SuccessResponse(data=result, msg="租户切换成功")
|
||||
|
||||
@@ -448,6 +438,7 @@ async def tenant_register_controller(
|
||||
|
||||
# ─── 忘记密码(自助重置)────────────────────────────────────
|
||||
|
||||
|
||||
@AuthRouter.post(
|
||||
"/forgot-password",
|
||||
summary="忘记密码",
|
||||
@@ -467,9 +458,7 @@ async def forgot_password_controller(
|
||||
返回:
|
||||
- str: 提示信息(无论成功与否均返回相同文案)
|
||||
"""
|
||||
msg = await PasswordResetService.forgot_password_service(
|
||||
redis=redis, db=db, email=data.email
|
||||
)
|
||||
msg = await PasswordResetService.forgot_password_service(redis=redis, db=db, email=data.email)
|
||||
return SuccessResponse(data=msg, msg="邮件已发送")
|
||||
|
||||
|
||||
@@ -492,7 +481,5 @@ async def reset_password_controller(
|
||||
返回:
|
||||
- str: 重置结果
|
||||
"""
|
||||
msg = await PasswordResetService.reset_password_with_token_service(
|
||||
redis=redis, db=db, token=data.token, new_password=data.new_password
|
||||
)
|
||||
msg = await PasswordResetService.reset_password_with_token_service(redis=redis, db=db, token=data.token, new_password=data.new_password)
|
||||
return SuccessResponse(data=msg, msg="密码已重置")
|
||||
|
||||
@@ -48,14 +48,14 @@ def _frontend_error_redirect(frontend_base: str, message: str) -> str:
|
||||
return f"{frontend_base}{sep}oauth_error={quote(message, safe='')}"
|
||||
|
||||
|
||||
def _frontend_success_redirect(
|
||||
frontend_base: str, access_token: str, refresh_token: str, token_type: str
|
||||
) -> str:
|
||||
q = urlencode({
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
"token_type": token_type,
|
||||
})
|
||||
def _frontend_success_redirect(frontend_base: str, access_token: str, refresh_token: str, token_type: str) -> str:
|
||||
q = urlencode(
|
||||
{
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
"token_type": token_type,
|
||||
}
|
||||
)
|
||||
sep = "&" if "?" in frontend_base else "?"
|
||||
return f"{frontend_base}{sep}{q}"
|
||||
|
||||
@@ -111,9 +111,7 @@ def build_authorize_url(
|
||||
"scope": "snsapi_login",
|
||||
"state": state,
|
||||
}
|
||||
return (
|
||||
"https://open.weixin.qq.com/connect/qrconnect?" + urlencode(params) + "#wechat_redirect"
|
||||
)
|
||||
return "https://open.weixin.qq.com/connect/qrconnect?" + urlencode(params) + "#wechat_redirect"
|
||||
|
||||
if provider == "qq":
|
||||
params = {
|
||||
@@ -149,9 +147,7 @@ async def _http_text(method: str, url: str, **kwargs: Any) -> str:
|
||||
return r.text
|
||||
|
||||
|
||||
async def exchange_github_token(
|
||||
client_id: str, client_secret: str, code: str, redirect_uri: str
|
||||
) -> str:
|
||||
async def exchange_github_token(client_id: str, client_secret: str, code: str, redirect_uri: str) -> str:
|
||||
data = await _http_json(
|
||||
"POST",
|
||||
"https://github.com/login/oauth/access_token",
|
||||
@@ -171,16 +167,16 @@ async def exchange_github_token(
|
||||
return str(token)
|
||||
|
||||
|
||||
async def exchange_gitee_token(
|
||||
client_id: str, client_secret: str, code: str, redirect_uri: str
|
||||
) -> str:
|
||||
qs = urlencode({
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"redirect_uri": redirect_uri,
|
||||
})
|
||||
async def exchange_gitee_token(client_id: str, client_secret: str, code: str, redirect_uri: str) -> str:
|
||||
qs = urlencode(
|
||||
{
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"redirect_uri": redirect_uri,
|
||||
}
|
||||
)
|
||||
data = await _http_json("GET", f"https://gitee.com/oauth/token?{qs}")
|
||||
if not isinstance(data, dict):
|
||||
raise CustomException(msg="Gitee token 响应格式错误")
|
||||
@@ -191,12 +187,14 @@ async def exchange_gitee_token(
|
||||
|
||||
|
||||
async def exchange_wechat_token(app_id: str, secret: str, code: str) -> tuple[str, str]:
|
||||
qs = urlencode({
|
||||
"appid": app_id,
|
||||
"secret": secret,
|
||||
"code": code,
|
||||
"grant_type": "authorization_code",
|
||||
})
|
||||
qs = urlencode(
|
||||
{
|
||||
"appid": app_id,
|
||||
"secret": secret,
|
||||
"code": code,
|
||||
"grant_type": "authorization_code",
|
||||
}
|
||||
)
|
||||
data = await _http_json("GET", f"https://api.weixin.qq.com/sns/oauth2/access_token?{qs}")
|
||||
if not isinstance(data, dict):
|
||||
raise CustomException(msg="微信 token 响应格式错误")
|
||||
@@ -207,16 +205,16 @@ async def exchange_wechat_token(app_id: str, secret: str, code: str) -> tuple[st
|
||||
return str(token), str(openid)
|
||||
|
||||
|
||||
async def exchange_qq_token(
|
||||
client_id: str, client_secret: str, code: str, redirect_uri: str
|
||||
) -> tuple[str, str]:
|
||||
qs = urlencode({
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"code": code,
|
||||
"redirect_uri": redirect_uri,
|
||||
})
|
||||
async def exchange_qq_token(client_id: str, client_secret: str, code: str, redirect_uri: str) -> tuple[str, str]:
|
||||
qs = urlencode(
|
||||
{
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"code": code,
|
||||
"redirect_uri": redirect_uri,
|
||||
}
|
||||
)
|
||||
text = await _http_text("GET", f"https://graph.qq.com/oauth2.0/token?{qs}")
|
||||
parts = dict(p.split("=", 1) for p in text.split("&") if "=" in p)
|
||||
token = parts.get("access_token")
|
||||
@@ -278,11 +276,13 @@ async def fetch_wechat_profile(access_token: str, openid: str) -> tuple[str, str
|
||||
|
||||
|
||||
async def fetch_qq_profile(access_token: str, app_id: str, openid: str) -> tuple[str, str]:
|
||||
qs = urlencode({
|
||||
"access_token": access_token,
|
||||
"oauth_consumer_key": app_id,
|
||||
"openid": openid,
|
||||
})
|
||||
qs = urlencode(
|
||||
{
|
||||
"access_token": access_token,
|
||||
"oauth_consumer_key": app_id,
|
||||
"openid": openid,
|
||||
}
|
||||
)
|
||||
user = await _http_json("GET", f"https://graph.qq.com/user/get_user_info?{qs}")
|
||||
if not isinstance(user, dict):
|
||||
raise CustomException(msg="QQ 用户信息格式错误")
|
||||
@@ -384,16 +384,12 @@ async def complete_oauth_login(
|
||||
if user.status == 1:
|
||||
raise CustomException(msg="用户已被停用")
|
||||
|
||||
user = await UserCRUD(
|
||||
AuthSchema(db=db, user=None, tenant_id=1, check_data_scope=False)
|
||||
).update_last_login_crud(id=user.id)
|
||||
user = await UserCRUD(AuthSchema(db=db, user=None, tenant_id=1, check_data_scope=False)).update_last_login_crud(id=user.id)
|
||||
if not user:
|
||||
raise CustomException(msg="用户不存在")
|
||||
|
||||
login_type = f"oauth_{provider}"
|
||||
token = await LoginService.create_token_service(
|
||||
request=request, redis=redis, user=user, login_type=login_type
|
||||
)
|
||||
token = await LoginService.create_token_service(request=request, redis=redis, user=user, login_type=login_type)
|
||||
await rc.delete(f"{STATE_PREFIX}{state}")
|
||||
return token, frontend
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from app.core.base_schema import JWTOutSchema
|
||||
@@ -69,15 +68,14 @@ class LoginWithTenantsSchema(JWTOutSchema):
|
||||
|
||||
# ─── 租户自助注册 ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TenantRegisterSchema(BaseModel):
|
||||
"""租户自助注册请求"""
|
||||
|
||||
username: str = Field(..., min_length=3, max_length=32, description="登录账号")
|
||||
password: str = Field(..., min_length=6, max_length=128, description="登录密码")
|
||||
email: str = Field(..., max_length=128, description="邮箱(用于接收通知)")
|
||||
tenant_name: str | None = Field(
|
||||
default=None, max_length=100, description="企业/团队名称(可选,默认:{用户名}的租户)"
|
||||
)
|
||||
tenant_name: str | None = Field(default=None, max_length=100, description="企业/团队名称(可选,默认:{用户名}的租户)")
|
||||
|
||||
|
||||
class TenantRegisterOutSchema(BaseModel):
|
||||
@@ -95,6 +93,7 @@ class TenantRegisterOutSchema(BaseModel):
|
||||
|
||||
# ─── 忘记密码(自助重置)─────────────────────────────────────────
|
||||
|
||||
|
||||
class ForgotPasswordSchema(BaseModel):
|
||||
"""忘记密码:提交邮箱,接收重置邮件"""
|
||||
|
||||
|
||||
@@ -149,9 +149,7 @@ class LoginService:
|
||||
)
|
||||
raise CustomException(msg="用户不存在")
|
||||
|
||||
if not PwdUtil.verify_password(
|
||||
plain_password=login_form.password, password_hash=user.password
|
||||
):
|
||||
if not PwdUtil.verify_password(plain_password=login_form.password, password_hash=user.password):
|
||||
await _write_login_log(
|
||||
username=_login_username,
|
||||
status=2,
|
||||
@@ -178,11 +176,8 @@ class LoginService:
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.api.v1.module_platform.tenant.model import TenantModel
|
||||
tenant_stmt = (
|
||||
select(TenantModel)
|
||||
.where(TenantModel.id == user.tenant_id, TenantModel.status == 0, TenantModel.is_deleted.is_(False))
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
tenant_stmt = select(TenantModel).where(TenantModel.id == user.tenant_id, TenantModel.status == 0, TenantModel.is_deleted.is_(False)).limit(1)
|
||||
tenant_result = await auth.db.execute(tenant_stmt)
|
||||
if not tenant_result.scalar_one_or_none():
|
||||
await _write_login_log(
|
||||
@@ -248,9 +243,7 @@ class LoginService:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def create_token_service(
|
||||
cls, request: Request, redis: Redis, user: UserModel, login_type: str
|
||||
) -> JWTOutSchema:
|
||||
async def create_token_service(cls, request: Request, redis: Redis, user: UserModel, login_type: str) -> JWTOutSchema:
|
||||
"""
|
||||
创建访问令牌和刷新令牌
|
||||
|
||||
@@ -381,9 +374,7 @@ class LoginService:
|
||||
refresh_expires = timedelta(seconds=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
|
||||
now = datetime.now()
|
||||
|
||||
session_info_json = (
|
||||
session_info if isinstance(session_info, str) else json.dumps(session_info)
|
||||
)
|
||||
session_info_json = session_info if isinstance(session_info, str) else json.dumps(session_info)
|
||||
|
||||
access_token = create_access_token(
|
||||
payload=JWTPayloadSchema(
|
||||
@@ -479,11 +470,7 @@ class LoginService:
|
||||
|
||||
# 超管可以看到所有租户
|
||||
if auth.user and auth.user.is_superuser:
|
||||
stmt = (
|
||||
select(TenantModel)
|
||||
.where(TenantModel.status == 0, TenantModel.is_deleted.is_(False))
|
||||
.order_by(TenantModel.sort, TenantModel.id)
|
||||
)
|
||||
stmt = select(TenantModel).where(TenantModel.status == 0, TenantModel.is_deleted.is_(False)).order_by(TenantModel.sort, TenantModel.id)
|
||||
result = await db.execute(stmt)
|
||||
tenant_objs = result.scalars().all()
|
||||
return [TenantOptionSchema(id=t.id, name=t.name, code=t.code) for t in tenant_objs]
|
||||
@@ -549,11 +536,7 @@ class LoginService:
|
||||
raise CustomException(msg="您不属于该租户,无法切换")
|
||||
|
||||
# 验证租户是否存在且状态正常
|
||||
tenant_stmt = (
|
||||
select(TenantModel)
|
||||
.where(TenantModel.id == tenant_id, TenantModel.status == 0)
|
||||
.limit(1)
|
||||
)
|
||||
tenant_stmt = select(TenantModel).where(TenantModel.id == tenant_id, TenantModel.status == 0).limit(1)
|
||||
result = await auth.db.execute(tenant_stmt)
|
||||
tenant = result.scalar_one_or_none()
|
||||
if not tenant:
|
||||
@@ -612,9 +595,7 @@ class LoginService:
|
||||
|
||||
set_current_tenant(tenant_id, auth.user.is_superuser)
|
||||
|
||||
logger.info(
|
||||
f"用户 {auth.user.username}(id={auth.user.id}) 切换到租户 {tenant.name}(id={tenant_id})"
|
||||
)
|
||||
logger.info(f"用户 {auth.user.username}(id={auth.user.id}) 切换到租户 {tenant.name}(id={tenant_id})")
|
||||
|
||||
return SelectTenantOutSchema(
|
||||
access_token=new_access_token,
|
||||
@@ -706,9 +687,7 @@ class AutoLoginService:
|
||||
TOKEN_EXPIRE = 300
|
||||
|
||||
@classmethod
|
||||
async def get_auto_login_users_service(
|
||||
cls, db: AsyncSession, tenant_id: int | None = None
|
||||
) -> list[AutoLoginUserSchema]:
|
||||
async def get_auto_login_users_service(cls, db: AsyncSession, tenant_id: int | None = None) -> list[AutoLoginUserSchema]:
|
||||
"""
|
||||
获取免登录用户列表
|
||||
|
||||
@@ -870,9 +849,7 @@ class AutoLoginService:
|
||||
await RedisCURD(redis).delete(token_key)
|
||||
|
||||
# 使用LoginService创建token
|
||||
jwt_token = await LoginService.create_token_service(
|
||||
request=request, redis=redis, user=user, login_type="PC端"
|
||||
)
|
||||
jwt_token = await LoginService.create_token_service(request=request, redis=redis, user=user, login_type="PC端")
|
||||
|
||||
logger.info(f"用户{user.username}免登录成功")
|
||||
|
||||
@@ -905,9 +882,13 @@ class TenantRegisterService:
|
||||
from app.api.v1.module_system.user.model import UserModel, UserRolesModel
|
||||
|
||||
# ── 1. 唯一性校验 ──
|
||||
exists_stmt = select(func.count()).select_from(UserModel).where(
|
||||
UserModel.is_deleted.is_(False),
|
||||
(UserModel.username == username) | (UserModel.email == email),
|
||||
exists_stmt = (
|
||||
select(func.count())
|
||||
.select_from(UserModel)
|
||||
.where(
|
||||
UserModel.is_deleted.is_(False),
|
||||
(UserModel.username == username) | (UserModel.email == email),
|
||||
)
|
||||
)
|
||||
cnt = (await db.execute(exists_stmt)).scalar() or 0
|
||||
if cnt > 0:
|
||||
@@ -999,9 +980,7 @@ class TenantRegisterService:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def _send_welcome_email(
|
||||
cls, to_email: str, username: str, tenant_name: str, trial_end: datetime
|
||||
) -> None:
|
||||
async def _send_welcome_email(cls, to_email: str, username: str, tenant_name: str, trial_end: datetime) -> None:
|
||||
"""发送欢迎邮件(不阻塞注册流程)。"""
|
||||
from sqlalchemy import select
|
||||
|
||||
@@ -1061,9 +1040,7 @@ class PasswordResetService:
|
||||
TOKEN_EXPIRE_SECONDS = 1800 # 30 分钟
|
||||
|
||||
@classmethod
|
||||
async def forgot_password_service(
|
||||
cls, redis: Redis, db: AsyncSession, email: str
|
||||
) -> str:
|
||||
async def forgot_password_service(cls, redis: Redis, db: AsyncSession, email: str) -> str:
|
||||
"""
|
||||
忘记密码:根据邮箱查找用户,生成重置令牌并尝试发送邮件。
|
||||
无论邮箱是否存在均返回相同文案(防止邮箱探测攻击)。
|
||||
@@ -1086,9 +1063,7 @@ class PasswordResetService:
|
||||
# 生成一次性令牌
|
||||
token = secrets.token_urlsafe(32)
|
||||
key = f"{cls.RESET_TOKEN_PREFIX}{token}"
|
||||
await RedisCURD(redis).set(
|
||||
key=key, value=str(user.id), expire=cls.TOKEN_EXPIRE_SECONDS
|
||||
)
|
||||
await RedisCURD(redis).set(key=key, value=str(user.id), expire=cls.TOKEN_EXPIRE_SECONDS)
|
||||
|
||||
# 尝试发送邮件(不阻塞)
|
||||
try:
|
||||
@@ -1099,9 +1074,7 @@ class PasswordResetService:
|
||||
return "若邮箱已注册,重置邮件已发送"
|
||||
|
||||
@classmethod
|
||||
async def reset_password_with_token_service(
|
||||
cls, redis: Redis, db: AsyncSession, token: str, new_password: str
|
||||
) -> str:
|
||||
async def reset_password_with_token_service(cls, redis: Redis, db: AsyncSession, token: str, new_password: str) -> str:
|
||||
"""使用令牌重置密码。校验令牌 → 更新密码 → 删除令牌。"""
|
||||
|
||||
from app.api.v1.module_system.user.model import UserModel
|
||||
|
||||
Reference in New Issue
Block a user