mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
refactor: 将访问令牌功能迁移至模块系统
- 新增 management_tokens 模块定义,支持通过环境变量控制可用性 - 从静态路由中移除 management-tokens,改由模块动态注册 - 前端路由添加模块激活检查,未激活时重定向到仪表盘 - 认证服务添加模块激活检查,未激活时禁止令牌认证 - 导航菜单中移除访问令牌入口(保留直接 URL 访问) - 修复 alembic 迁移脚本格式和错误处理
This commit is contained in:
@@ -21,11 +21,13 @@ This migration consolidates all schema changes from 2026-01-08 to 2026-01-10:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
from sqlalchemy import inspect
|
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
from sqlalchemy.exc import ProgrammingError
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
# 配置日志
|
# 配置日志
|
||||||
alembic_logger = logging.getLogger("alembic.runtime.migration")
|
alembic_logger = logging.getLogger("alembic.runtime.migration")
|
||||||
@@ -66,9 +68,18 @@ def upgrade() -> None:
|
|||||||
|
|
||||||
# ========== 1. provider_api_keys: 添加 provider_id 和 api_formats ==========
|
# ========== 1. provider_api_keys: 添加 provider_id 和 api_formats ==========
|
||||||
if not _column_exists("provider_api_keys", "provider_id"):
|
if not _column_exists("provider_api_keys", "provider_id"):
|
||||||
op.add_column("provider_api_keys", sa.Column("provider_id", sa.String(36), nullable=True))
|
try:
|
||||||
|
op.add_column(
|
||||||
|
"provider_api_keys", sa.Column("provider_id", sa.String(36), nullable=True)
|
||||||
|
)
|
||||||
|
except ProgrammingError as exc:
|
||||||
|
if getattr(getattr(exc, "orig", None), "pgcode", None) == "42701":
|
||||||
|
alembic_logger.warning("provider_api_keys.provider_id already exists; skipping add")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
# 数据迁移:从 endpoint 获取 provider_id
|
# 数据迁移:从 endpoint 获取 provider_id(如果 endpoint_id 仍存在)
|
||||||
|
if _column_exists("provider_api_keys", "endpoint_id"):
|
||||||
op.execute("""
|
op.execute("""
|
||||||
UPDATE provider_api_keys k
|
UPDATE provider_api_keys k
|
||||||
SET provider_id = e.provider_id
|
SET provider_id = e.provider_id
|
||||||
@@ -76,63 +87,65 @@ def upgrade() -> None:
|
|||||||
WHERE k.endpoint_id = e.id AND k.provider_id IS NULL
|
WHERE k.endpoint_id = e.id AND k.provider_id IS NULL
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# 检查无法关联的孤儿 Key
|
# 检查无法关联的孤儿 Key
|
||||||
result = bind.execute(sa.text(
|
result = bind.execute(
|
||||||
"SELECT COUNT(*) FROM provider_api_keys WHERE provider_id IS NULL"
|
sa.text("SELECT COUNT(*) FROM provider_api_keys WHERE provider_id IS NULL")
|
||||||
))
|
)
|
||||||
orphan_count = result.scalar() or 0
|
orphan_count = result.scalar() or 0
|
||||||
if orphan_count > 0:
|
if orphan_count > 0:
|
||||||
# 使用 logger 记录更明显的告警
|
# 使用 logger 记录更明显的告警
|
||||||
alembic_logger.warning("=" * 60)
|
alembic_logger.warning("=" * 60)
|
||||||
alembic_logger.warning(f"[MIGRATION WARNING] 发现 {orphan_count} 个无法关联 Provider 的孤儿 Key")
|
alembic_logger.warning(
|
||||||
alembic_logger.warning("=" * 60)
|
f"[MIGRATION WARNING] 发现 {orphan_count} 个无法关联 Provider 的孤儿 Key"
|
||||||
alembic_logger.info("正在备份孤儿 Key 到 _orphan_api_keys_backup 表...")
|
)
|
||||||
|
alembic_logger.warning("=" * 60)
|
||||||
|
alembic_logger.info("正在备份孤儿 Key 到 _orphan_api_keys_backup 表...")
|
||||||
|
|
||||||
# 先备份孤儿数据到临时表,避免数据丢失
|
# 先备份孤儿数据到临时表,避免数据丢失
|
||||||
op.execute("""
|
op.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS _orphan_api_keys_backup AS
|
CREATE TABLE IF NOT EXISTS _orphan_api_keys_backup AS
|
||||||
SELECT *, NOW() as backup_at
|
SELECT *, NOW() as backup_at
|
||||||
FROM provider_api_keys
|
FROM provider_api_keys
|
||||||
WHERE provider_id IS NULL
|
WHERE provider_id IS NULL
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# 记录备份的 Key ID
|
# 记录备份的 Key ID
|
||||||
orphan_ids = bind.execute(sa.text(
|
orphan_ids = bind.execute(
|
||||||
"SELECT id, name FROM provider_api_keys WHERE provider_id IS NULL"
|
sa.text("SELECT id, name FROM provider_api_keys WHERE provider_id IS NULL")
|
||||||
)).fetchall()
|
).fetchall()
|
||||||
alembic_logger.info("备份的孤儿 Key 列表:")
|
alembic_logger.info("备份的孤儿 Key 列表:")
|
||||||
for key_id, key_name in orphan_ids:
|
for key_id, key_name in orphan_ids:
|
||||||
alembic_logger.info(f" - Key: {key_name} (ID: {key_id})")
|
alembic_logger.info(f" - Key: {key_name} (ID: {key_id})")
|
||||||
|
|
||||||
# 删除孤儿数据
|
# 删除孤儿数据
|
||||||
op.execute("DELETE FROM provider_api_keys WHERE provider_id IS NULL")
|
op.execute("DELETE FROM provider_api_keys WHERE provider_id IS NULL")
|
||||||
alembic_logger.info(f"已备份并删除 {orphan_count} 个孤儿 Key")
|
alembic_logger.info(f"已备份并删除 {orphan_count} 个孤儿 Key")
|
||||||
|
|
||||||
# 提供恢复指南
|
# 提供恢复指南
|
||||||
alembic_logger.warning("-" * 60)
|
alembic_logger.warning("-" * 60)
|
||||||
alembic_logger.warning("[恢复指南] 如需恢复孤儿 Key:")
|
alembic_logger.warning("[恢复指南] 如需恢复孤儿 Key:")
|
||||||
alembic_logger.warning(" 1. 查询备份表: SELECT * FROM _orphan_api_keys_backup;")
|
alembic_logger.warning(" 1. 查询备份表: SELECT * FROM _orphan_api_keys_backup;")
|
||||||
alembic_logger.warning(" 2. 确定正确的 provider_id")
|
alembic_logger.warning(" 2. 确定正确的 provider_id")
|
||||||
alembic_logger.warning(" 3. 执行恢复:")
|
alembic_logger.warning(" 3. 执行恢复:")
|
||||||
alembic_logger.warning(" INSERT INTO provider_api_keys (...)")
|
alembic_logger.warning(" INSERT INTO provider_api_keys (...)")
|
||||||
alembic_logger.warning(" SELECT ... FROM _orphan_api_keys_backup WHERE ...;")
|
alembic_logger.warning(" SELECT ... FROM _orphan_api_keys_backup WHERE ...;")
|
||||||
alembic_logger.warning("-" * 60)
|
alembic_logger.warning("-" * 60)
|
||||||
|
|
||||||
# 设置 NOT NULL 并创建外键
|
# 设置 NOT NULL 并创建外键
|
||||||
op.alter_column("provider_api_keys", "provider_id", nullable=False)
|
op.alter_column("provider_api_keys", "provider_id", nullable=False)
|
||||||
|
|
||||||
if not _constraint_exists("provider_api_keys", "fk_provider_api_keys_provider"):
|
if not _constraint_exists("provider_api_keys", "fk_provider_api_keys_provider"):
|
||||||
op.create_foreign_key(
|
op.create_foreign_key(
|
||||||
"fk_provider_api_keys_provider",
|
"fk_provider_api_keys_provider",
|
||||||
"provider_api_keys",
|
"provider_api_keys",
|
||||||
"providers",
|
"providers",
|
||||||
["provider_id"],
|
["provider_id"],
|
||||||
["id"],
|
["id"],
|
||||||
ondelete="CASCADE",
|
ondelete="CASCADE",
|
||||||
)
|
)
|
||||||
|
|
||||||
if not _index_exists("provider_api_keys", "idx_provider_api_keys_provider_id"):
|
if not _index_exists("provider_api_keys", "idx_provider_api_keys_provider_id"):
|
||||||
op.create_index("idx_provider_api_keys_provider_id", "provider_api_keys", ["provider_id"])
|
op.create_index("idx_provider_api_keys_provider_id", "provider_api_keys", ["provider_id"])
|
||||||
|
|
||||||
if not _column_exists("provider_api_keys", "api_formats"):
|
if not _column_exists("provider_api_keys", "api_formats"):
|
||||||
op.add_column("provider_api_keys", sa.Column("api_formats", sa.JSON(), nullable=True))
|
op.add_column("provider_api_keys", sa.Column("api_formats", sa.JSON(), nullable=True))
|
||||||
@@ -149,7 +162,9 @@ def upgrade() -> None:
|
|||||||
|
|
||||||
# 修改 endpoint_id 为可空,外键改为 SET NULL
|
# 修改 endpoint_id 为可空,外键改为 SET NULL
|
||||||
if _constraint_exists("provider_api_keys", "provider_api_keys_endpoint_id_fkey"):
|
if _constraint_exists("provider_api_keys", "provider_api_keys_endpoint_id_fkey"):
|
||||||
op.drop_constraint("provider_api_keys_endpoint_id_fkey", "provider_api_keys", type_="foreignkey")
|
op.drop_constraint(
|
||||||
|
"provider_api_keys_endpoint_id_fkey", "provider_api_keys", type_="foreignkey"
|
||||||
|
)
|
||||||
op.alter_column("provider_api_keys", "endpoint_id", nullable=True)
|
op.alter_column("provider_api_keys", "endpoint_id", nullable=True)
|
||||||
# 不再重建外键,因为后面会删除这个字段
|
# 不再重建外键,因为后面会删除这个字段
|
||||||
|
|
||||||
@@ -239,10 +254,14 @@ def upgrade() -> None:
|
|||||||
op.alter_column("provider_api_keys", "max_concurrent", new_column_name="rpm_limit")
|
op.alter_column("provider_api_keys", "max_concurrent", new_column_name="rpm_limit")
|
||||||
|
|
||||||
if _column_exists("provider_api_keys", "learned_max_concurrent"):
|
if _column_exists("provider_api_keys", "learned_max_concurrent"):
|
||||||
op.alter_column("provider_api_keys", "learned_max_concurrent", new_column_name="learned_rpm_limit")
|
op.alter_column(
|
||||||
|
"provider_api_keys", "learned_max_concurrent", new_column_name="learned_rpm_limit"
|
||||||
|
)
|
||||||
|
|
||||||
if _column_exists("provider_api_keys", "last_concurrent_peak"):
|
if _column_exists("provider_api_keys", "last_concurrent_peak"):
|
||||||
op.alter_column("provider_api_keys", "last_concurrent_peak", new_column_name="last_rpm_peak")
|
op.alter_column(
|
||||||
|
"provider_api_keys", "last_concurrent_peak", new_column_name="last_rpm_peak"
|
||||||
|
)
|
||||||
|
|
||||||
# 删除废弃字段
|
# 删除废弃字段
|
||||||
for col in ["rate_limit", "daily_limit", "monthly_limit"]:
|
for col in ["rate_limit", "daily_limit", "monthly_limit"]:
|
||||||
@@ -431,7 +450,9 @@ def downgrade() -> None:
|
|||||||
|
|
||||||
# 11. 恢复 provider_endpoints.max_concurrent
|
# 11. 恢复 provider_endpoints.max_concurrent
|
||||||
if not _column_exists("provider_endpoints", "max_concurrent"):
|
if not _column_exists("provider_endpoints", "max_concurrent"):
|
||||||
op.add_column("provider_endpoints", sa.Column("max_concurrent", sa.Integer(), nullable=True))
|
op.add_column(
|
||||||
|
"provider_endpoints", sa.Column("max_concurrent", sa.Integer(), nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
# 10. 恢复 endpoint_id
|
# 10. 恢复 endpoint_id
|
||||||
if not _column_exists("provider_api_keys", "endpoint_id"):
|
if not _column_exists("provider_api_keys", "endpoint_id"):
|
||||||
@@ -457,9 +478,13 @@ def downgrade() -> None:
|
|||||||
if _column_exists("provider_api_keys", "rpm_limit"):
|
if _column_exists("provider_api_keys", "rpm_limit"):
|
||||||
op.alter_column("provider_api_keys", "rpm_limit", new_column_name="max_concurrent")
|
op.alter_column("provider_api_keys", "rpm_limit", new_column_name="max_concurrent")
|
||||||
if _column_exists("provider_api_keys", "learned_rpm_limit"):
|
if _column_exists("provider_api_keys", "learned_rpm_limit"):
|
||||||
op.alter_column("provider_api_keys", "learned_rpm_limit", new_column_name="learned_max_concurrent")
|
op.alter_column(
|
||||||
|
"provider_api_keys", "learned_rpm_limit", new_column_name="learned_max_concurrent"
|
||||||
|
)
|
||||||
if _column_exists("provider_api_keys", "last_rpm_peak"):
|
if _column_exists("provider_api_keys", "last_rpm_peak"):
|
||||||
op.alter_column("provider_api_keys", "last_rpm_peak", new_column_name="last_concurrent_peak")
|
op.alter_column(
|
||||||
|
"provider_api_keys", "last_rpm_peak", new_column_name="last_concurrent_peak"
|
||||||
|
)
|
||||||
|
|
||||||
# 恢复已删除的字段
|
# 恢复已删除的字段
|
||||||
if not _column_exists("provider_api_keys", "rate_limit"):
|
if not _column_exists("provider_api_keys", "rate_limit"):
|
||||||
@@ -491,12 +516,12 @@ def downgrade() -> None:
|
|||||||
op.drop_column("providers", "timeout")
|
op.drop_column("providers", "timeout")
|
||||||
|
|
||||||
# 3. models: global_model_id 改回 NOT NULL
|
# 3. models: global_model_id 改回 NOT NULL
|
||||||
result = bind.execute(sa.text(
|
result = bind.execute(sa.text("SELECT COUNT(*) FROM models WHERE global_model_id IS NULL"))
|
||||||
"SELECT COUNT(*) FROM models WHERE global_model_id IS NULL"
|
|
||||||
))
|
|
||||||
orphan_model_count = result.scalar() or 0
|
orphan_model_count = result.scalar() or 0
|
||||||
if orphan_model_count > 0:
|
if orphan_model_count > 0:
|
||||||
alembic_logger.warning(f"[WARN] 发现 {orphan_model_count} 个无 global_model_id 的独立模型,将被删除")
|
alembic_logger.warning(
|
||||||
|
f"[WARN] 发现 {orphan_model_count} 个无 global_model_id 的独立模型,将被删除"
|
||||||
|
)
|
||||||
op.execute("DELETE FROM models WHERE global_model_id IS NULL")
|
op.execute("DELETE FROM models WHERE global_model_id IS NULL")
|
||||||
alembic_logger.info(f"已删除 {orphan_model_count} 个独立模型")
|
alembic_logger.info(f"已删除 {orphan_model_count} 个独立模型")
|
||||||
op.alter_column("models", "global_model_id", nullable=False)
|
op.alter_column("models", "global_model_id", nullable=False)
|
||||||
|
|||||||
@@ -493,7 +493,6 @@ const navigation = computed(() => {
|
|||||||
items: [
|
items: [
|
||||||
{ name: '模型目录', href: '/dashboard/models', icon: Box },
|
{ name: '模型目录', href: '/dashboard/models', icon: Box },
|
||||||
{ name: 'API 密钥', href: '/dashboard/api-keys', icon: Key },
|
{ name: 'API 密钥', href: '/dashboard/api-keys', icon: Key },
|
||||||
{ name: '访问令牌', href: '/dashboard/management-tokens', icon: KeyRound },
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -517,6 +516,7 @@ const navigation = computed(() => {
|
|||||||
// 图标映射
|
// 图标映射
|
||||||
const iconMap: Record<string, LucideIcon> = {
|
const iconMap: Record<string, LucideIcon> = {
|
||||||
Key,
|
Key,
|
||||||
|
KeyRound,
|
||||||
FileUp,
|
FileUp,
|
||||||
Shield,
|
Shield,
|
||||||
Puzzle,
|
Puzzle,
|
||||||
@@ -556,7 +556,6 @@ const navigation = computed(() => {
|
|||||||
{ name: '提供商', href: '/admin/providers', icon: FolderTree },
|
{ name: '提供商', href: '/admin/providers', icon: FolderTree },
|
||||||
{ name: '模型管理', href: '/admin/models', icon: Layers },
|
{ name: '模型管理', href: '/admin/models', icon: Layers },
|
||||||
{ name: '独立密钥', href: '/admin/keys', icon: Key },
|
{ name: '独立密钥', href: '/admin/keys', icon: Key },
|
||||||
{ name: '访问令牌', href: '/admin/management-tokens', icon: KeyRound },
|
|
||||||
{ name: '异步任务', href: '/admin/async-tasks', icon: Zap },
|
{ name: '异步任务', href: '/admin/async-tasks', icon: Zap },
|
||||||
{ name: '使用记录', href: '/admin/usage', icon: BarChart3 },
|
{ name: '使用记录', href: '/admin/usage', icon: BarChart3 },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -87,7 +87,8 @@ const routes: RouteRecordRaw[] = [
|
|||||||
{
|
{
|
||||||
path: 'management-tokens',
|
path: 'management-tokens',
|
||||||
name: 'ManagementTokens',
|
name: 'ManagementTokens',
|
||||||
component: () => importWithRetry(() => import('@/views/user/ManagementTokens.vue'))
|
component: () => importWithRetry(() => import('@/views/user/ManagementTokens.vue')),
|
||||||
|
meta: { module: 'management_tokens' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'announcements',
|
path: 'announcements',
|
||||||
@@ -139,7 +140,8 @@ const routes: RouteRecordRaw[] = [
|
|||||||
{
|
{
|
||||||
path: 'management-tokens',
|
path: 'management-tokens',
|
||||||
name: 'AdminManagementTokens',
|
name: 'AdminManagementTokens',
|
||||||
component: () => importWithRetry(() => import('@/views/user/ManagementTokens.vue'))
|
component: () => importWithRetry(() => import('@/views/user/ManagementTokens.vue')),
|
||||||
|
meta: { module: 'management_tokens' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'providers',
|
path: 'providers',
|
||||||
@@ -329,6 +331,26 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
}
|
}
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
} else if (moduleName) {
|
||||||
|
// 非管理员页面但需要模块的路由(如用户侧访问令牌)
|
||||||
|
// 确保模块状态已加载
|
||||||
|
if (!moduleStore.loaded) {
|
||||||
|
try {
|
||||||
|
await moduleStore.fetchModules()
|
||||||
|
} catch (error) {
|
||||||
|
// fail-close: 获取模块状态失败时拒绝访问
|
||||||
|
log.warn('Failed to fetch modules status, denying access', { error })
|
||||||
|
next('/dashboard')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 用户侧需要检查模块是否激活(active),而不仅仅是可用(available)
|
||||||
|
if (!moduleStore.isActive(moduleName)) {
|
||||||
|
log.warn(`Module ${moduleName} is not active, redirecting to user dashboard`)
|
||||||
|
next('/dashboard')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next()
|
||||||
} else {
|
} else {
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,13 @@ from .adaptive import router as adaptive_router
|
|||||||
from .api_keys import router as api_keys_router
|
from .api_keys import router as api_keys_router
|
||||||
from .billing import router as billing_router
|
from .billing import router as billing_router
|
||||||
from .endpoints import router as endpoints_router
|
from .endpoints import router as endpoints_router
|
||||||
from .management_tokens import router as management_tokens_router
|
|
||||||
from .models import router as models_router
|
from .models import router as models_router
|
||||||
from .modules import router as modules_router
|
from .modules import router as modules_router
|
||||||
from .monitoring import router as monitoring_router
|
from .monitoring import router as monitoring_router
|
||||||
|
from .provider_oauth import router as provider_oauth_router
|
||||||
from .provider_ops import router as provider_ops_router
|
from .provider_ops import router as provider_ops_router
|
||||||
from .provider_query import router as provider_query_router
|
from .provider_query import router as provider_query_router
|
||||||
from .provider_strategy import router as provider_strategy_router
|
from .provider_strategy import router as provider_strategy_router
|
||||||
from .provider_oauth import router as provider_oauth_router
|
|
||||||
from .providers import router as providers_router
|
from .providers import router as providers_router
|
||||||
from .security import router as security_router
|
from .security import router as security_router
|
||||||
from .stats import router as stats_router
|
from .stats import router as stats_router
|
||||||
@@ -38,12 +37,12 @@ router.include_router(models_router)
|
|||||||
router.include_router(security_router)
|
router.include_router(security_router)
|
||||||
router.include_router(stats_router)
|
router.include_router(stats_router)
|
||||||
router.include_router(provider_query_router)
|
router.include_router(provider_query_router)
|
||||||
router.include_router(management_tokens_router)
|
|
||||||
router.include_router(modules_router)
|
router.include_router(modules_router)
|
||||||
router.include_router(provider_ops_router)
|
router.include_router(provider_ops_router)
|
||||||
router.include_router(video_tasks_router)
|
router.include_router(video_tasks_router)
|
||||||
|
|
||||||
# 注意:ldap_router 已迁移到模块系统,由 ModuleRegistry 动态注册
|
# 注意:以下路由已迁移到模块系统,由 ModuleRegistry 动态注册
|
||||||
# 当 LDAP_AVAILABLE=true 时才会注册路由
|
# - ldap_router: 当 LDAP_AVAILABLE=true 时注册
|
||||||
|
# - management_tokens_router: 当 MANAGEMENT_TOKENS_AVAILABLE=true 时注册
|
||||||
|
|
||||||
__all__ = ["router"]
|
__all__ = ["router"]
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from .management_tokens import router as management_tokens_router
|
|
||||||
from .routes import router as me_router
|
from .routes import router as me_router
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
router.include_router(me_router)
|
router.include_router(me_router)
|
||||||
router.include_router(management_tokens_router)
|
|
||||||
|
# 注意:management_tokens_router 已迁移到模块系统,由 ModuleRegistry 动态注册
|
||||||
|
# 当 MANAGEMENT_TOKENS_AVAILABLE=true 时注册
|
||||||
|
|
||||||
__all__ = ["router"]
|
__all__ = ["router"]
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from src.core.modules.base import ModuleDefinition
|
|||||||
# 导入所有模块定义
|
# 导入所有模块定义
|
||||||
from src.modules.gemini_files import gemini_files_module
|
from src.modules.gemini_files import gemini_files_module
|
||||||
from src.modules.ldap import ldap_module
|
from src.modules.ldap import ldap_module
|
||||||
|
from src.modules.management_tokens import management_tokens_module
|
||||||
from src.modules.oauth import oauth_module
|
from src.modules.oauth import oauth_module
|
||||||
|
|
||||||
# 所有模块列表
|
# 所有模块列表
|
||||||
@@ -16,6 +17,7 @@ ALL_MODULES: list[ModuleDefinition] = [
|
|||||||
ldap_module,
|
ldap_module,
|
||||||
oauth_module,
|
oauth_module,
|
||||||
gemini_files_module,
|
gemini_files_module,
|
||||||
|
management_tokens_module,
|
||||||
]
|
]
|
||||||
|
|
||||||
__all__ = ["ALL_MODULES"]
|
__all__ = ["ALL_MODULES"]
|
||||||
|
|||||||
78
src/modules/management_tokens/__init__.py
Normal file
78
src/modules/management_tokens/__init__.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
"""
|
||||||
|
访问令牌模块
|
||||||
|
|
||||||
|
提供管理 API 访问令牌的功能
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from src.core.modules.base import (
|
||||||
|
ModuleCategory,
|
||||||
|
ModuleDefinition,
|
||||||
|
ModuleHealth,
|
||||||
|
ModuleMetadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
|
||||||
|
def _get_router() -> Any:
|
||||||
|
"""延迟导入路由(避免启动时加载重依赖)
|
||||||
|
|
||||||
|
返回一个合并了管理员和用户两个路由的 APIRouter
|
||||||
|
- /api/admin/management-tokens: 管理员管理所有用户的令牌
|
||||||
|
- /api/me/management-tokens: 用户管理自己的令牌
|
||||||
|
"""
|
||||||
|
from src.api.admin.management_tokens import router as admin_router
|
||||||
|
from src.api.user_me.management_tokens import router as user_router
|
||||||
|
|
||||||
|
# 创建一个组合路由器
|
||||||
|
combined_router = APIRouter()
|
||||||
|
combined_router.include_router(admin_router)
|
||||||
|
combined_router.include_router(user_router)
|
||||||
|
|
||||||
|
return combined_router
|
||||||
|
|
||||||
|
|
||||||
|
async def _health_check() -> ModuleHealth:
|
||||||
|
"""健康检查 - 简化版,不依赖数据库连接"""
|
||||||
|
return ModuleHealth.HEALTHY
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_config(db: Session) -> tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
验证配置是否可以启用模块
|
||||||
|
|
||||||
|
访问令牌模块没有特殊配置要求,始终可用
|
||||||
|
"""
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
|
||||||
|
# 访问令牌模块定义
|
||||||
|
management_tokens_module = ModuleDefinition(
|
||||||
|
metadata=ModuleMetadata(
|
||||||
|
name="management_tokens",
|
||||||
|
display_name="访问令牌",
|
||||||
|
description="管理 API 访问令牌,支持细粒度权限控制和 IP 白名单",
|
||||||
|
category=ModuleCategory.SECURITY,
|
||||||
|
# 可用性控制
|
||||||
|
env_key="MANAGEMENT_TOKENS_AVAILABLE",
|
||||||
|
default_available=True,
|
||||||
|
required_packages=[],
|
||||||
|
# 路由配置
|
||||||
|
api_prefix="/api/admin/management-tokens, /api/me/management-tokens",
|
||||||
|
# 前端配置 - 不在导航栏显示,但保留路由用于直接访问
|
||||||
|
admin_route="/admin/management-tokens",
|
||||||
|
admin_menu_icon=None,
|
||||||
|
admin_menu_group=None, # 不显示在导航菜单中
|
||||||
|
admin_menu_order=0,
|
||||||
|
),
|
||||||
|
router_factory=_get_router,
|
||||||
|
health_check=_health_check,
|
||||||
|
validate_config=_validate_config,
|
||||||
|
)
|
||||||
@@ -614,10 +614,17 @@ class AuthService:
|
|||||||
RateLimitException: 超过速率限制时抛出(用于返回 429)
|
RateLimitException: 超过速率限制时抛出(用于返回 429)
|
||||||
"""
|
"""
|
||||||
from src.core.exceptions import RateLimitException
|
from src.core.exceptions import RateLimitException
|
||||||
|
from src.core.modules import get_module_registry
|
||||||
from src.models.database import AuditEventType, ManagementToken
|
from src.models.database import AuditEventType, ManagementToken
|
||||||
from src.services.rate_limit.ip_limiter import IPRateLimiter
|
from src.services.rate_limit.ip_limiter import IPRateLimiter
|
||||||
from src.services.system.audit import AuditService
|
from src.services.system.audit import AuditService
|
||||||
|
|
||||||
|
# 检查访问令牌模块是否激活
|
||||||
|
module_registry = get_module_registry()
|
||||||
|
if not module_registry.is_active("management_tokens", db):
|
||||||
|
logger.warning("Management Token 认证失败 - 访问令牌模块未激活")
|
||||||
|
return None
|
||||||
|
|
||||||
# 速率限制检查(防止暴力破解)
|
# 速率限制检查(防止暴力破解)
|
||||||
allowed, remaining, ttl = await IPRateLimiter.check_limit(
|
allowed, remaining, ttl = await IPRateLimiter.check_limit(
|
||||||
client_ip,
|
client_ip,
|
||||||
|
|||||||
Reference in New Issue
Block a user