新增
This commit is contained in:
@@ -7,9 +7,21 @@ from models.database import get_db
|
||||
from models.models import User
|
||||
from .jwt_utils import decode_access_token
|
||||
from .permissions import (
|
||||
ACCOUNTS_COOKIE,
|
||||
ACCOUNTS_CREATE,
|
||||
ACCOUNTS_DELETE,
|
||||
ACCOUNTS_START,
|
||||
ACCOUNTS_STOP,
|
||||
ACCOUNTS_UPDATE,
|
||||
ACCOUNTS_WRITE,
|
||||
ACCOUNTS_WRITE_GRANULAR,
|
||||
LINK_CARDS_WRITE,
|
||||
MESSAGES_WRITE,
|
||||
ORDERS_CREATE,
|
||||
ROLES_MANAGE,
|
||||
RULES_WRITE,
|
||||
SETTINGS_DATABASE,
|
||||
SYSTEM_LOGS_CLEAR,
|
||||
USERS_MANAGE,
|
||||
WRITE_PERMISSIONS,
|
||||
)
|
||||
@@ -71,34 +83,43 @@ async def require_write(user: User = Depends(get_current_user)) -> User:
|
||||
)
|
||||
|
||||
|
||||
async def require_accounts_write(user: User = Depends(get_current_user)) -> User:
|
||||
if has_permission(user.role, ACCOUNTS_WRITE):
|
||||
return user
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="缺少权限:accounts.write",
|
||||
)
|
||||
def _require_any(*codes: str, detail: str):
|
||||
async def _checker(user: User = Depends(get_current_user)) -> User:
|
||||
if any(has_permission(user.role, code) for code in codes):
|
||||
return user
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=detail)
|
||||
|
||||
return _checker
|
||||
|
||||
|
||||
async def require_messages_write(user: User = Depends(get_current_user)) -> User:
|
||||
if has_permission(user.role, MESSAGES_WRITE):
|
||||
return user
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="缺少权限:messages.write",
|
||||
)
|
||||
# Backward-compatible: any account write capability.
|
||||
require_accounts_write = _require_any(
|
||||
ACCOUNTS_WRITE,
|
||||
*ACCOUNTS_WRITE_GRANULAR,
|
||||
detail="缺少账号写权限",
|
||||
)
|
||||
require_accounts_create = require_permission(ACCOUNTS_CREATE)
|
||||
require_accounts_update = require_permission(ACCOUNTS_UPDATE)
|
||||
require_accounts_delete = require_permission(ACCOUNTS_DELETE)
|
||||
require_accounts_start = require_permission(ACCOUNTS_START)
|
||||
require_accounts_stop = require_permission(ACCOUNTS_STOP)
|
||||
require_accounts_cookie = require_permission(ACCOUNTS_COOKIE)
|
||||
|
||||
|
||||
async def require_rules_write(user: User = Depends(get_current_user)) -> User:
|
||||
if has_permission(user.role, RULES_WRITE):
|
||||
return user
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="缺少权限:rules.write",
|
||||
)
|
||||
require_messages_write = require_permission(MESSAGES_WRITE)
|
||||
require_rules_write = require_permission(RULES_WRITE)
|
||||
require_link_cards_write = require_permission(LINK_CARDS_WRITE)
|
||||
require_system_logs_clear = require_permission(SYSTEM_LOGS_CLEAR)
|
||||
require_settings_database = require_permission(SETTINGS_DATABASE)
|
||||
require_orders_create = require_permission(ORDERS_CREATE)
|
||||
|
||||
|
||||
async def require_user_manager(user: User = Depends(get_current_user)) -> User:
|
||||
if has_permission(user.role, USERS_MANAGE):
|
||||
return user
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="需要用户管理权限")
|
||||
|
||||
|
||||
async def require_role_manager(user: User = Depends(get_current_user)) -> User:
|
||||
if has_permission(user.role, ROLES_MANAGE):
|
||||
return user
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="需要角色管理权限")
|
||||
|
||||
+228
-12
@@ -1,6 +1,6 @@
|
||||
"""Fixed permission catalog for menus and actions.
|
||||
"""Fixed permission catalog: menus, button actions, and data scope.
|
||||
|
||||
UI and APIs only select from this list; new codes must be added in code.
|
||||
UI only selects from this list; new codes must be added in code.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -8,7 +8,9 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Menu visibility
|
||||
# ---------------------------------------------------------------------------
|
||||
MENU_DASHBOARD = "menu.dashboard"
|
||||
MENU_ACCOUNTS = "menu.accounts"
|
||||
MENU_MESSAGES = "menu.messages"
|
||||
@@ -19,23 +21,63 @@ MENU_SYSTEM_LOGS = "menu.system_logs"
|
||||
MENU_DOWNLOAD = "menu.download"
|
||||
MENU_HELP = "menu.help"
|
||||
MENU_USERS = "menu.users"
|
||||
MENU_ROLES = "menu.roles"
|
||||
MENU_SETTINGS = "menu.settings"
|
||||
MENU_DESKTOP_UPDATE = "menu.desktop_update"
|
||||
MENU_PAYMENT_SETTINGS = "menu.payment_settings"
|
||||
MENU_PAYMENT_ORDERS = "menu.payment_orders"
|
||||
|
||||
# Actions
|
||||
ACCOUNTS_WRITE = "accounts.write"
|
||||
# ---------------------------------------------------------------------------
|
||||
# Button / action permissions
|
||||
# ---------------------------------------------------------------------------
|
||||
# Accounts (granular). Legacy ``accounts.write`` expands to the full set.
|
||||
ACCOUNTS_CREATE = "accounts.create"
|
||||
ACCOUNTS_UPDATE = "accounts.update"
|
||||
ACCOUNTS_DELETE = "accounts.delete"
|
||||
ACCOUNTS_START = "accounts.start"
|
||||
ACCOUNTS_STOP = "accounts.stop"
|
||||
ACCOUNTS_COOKIE = "accounts.cookie"
|
||||
ACCOUNTS_WRITE = "accounts.write" # legacy bundle
|
||||
|
||||
MESSAGES_WRITE = "messages.write"
|
||||
RULES_WRITE = "rules.write"
|
||||
LINK_CARDS_WRITE = "link_cards.write"
|
||||
|
||||
LOGS_READ = "logs.read"
|
||||
RECEIVED_MESSAGES_READ = "received_messages.read"
|
||||
SYSTEM_LOGS_READ = "system_logs.read"
|
||||
SYSTEM_LOGS_CLEAR = "system_logs.clear"
|
||||
|
||||
USERS_MANAGE = "users.manage"
|
||||
ROLES_MANAGE = "roles.manage"
|
||||
SETTINGS_MANAGE = "settings.manage"
|
||||
SETTINGS_DATABASE = "settings.database"
|
||||
DESKTOP_MANAGE = "desktop.manage"
|
||||
PAYMENTS_MANAGE = "payments.manage"
|
||||
ORDERS_READ = "orders.read"
|
||||
ORDERS_CREATE = "orders.create"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Data scope
|
||||
# ---------------------------------------------------------------------------
|
||||
# Without this (and without is_admin), users only see their own data.
|
||||
DATA_SCOPE_ALL = "data.scope_all"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bundles / aliases expanded on save & when checking permissions
|
||||
# ---------------------------------------------------------------------------
|
||||
ACCOUNTS_WRITE_GRANULAR: tuple[str, ...] = (
|
||||
ACCOUNTS_CREATE,
|
||||
ACCOUNTS_UPDATE,
|
||||
ACCOUNTS_DELETE,
|
||||
ACCOUNTS_START,
|
||||
ACCOUNTS_STOP,
|
||||
ACCOUNTS_COOKIE,
|
||||
)
|
||||
|
||||
LEGACY_BUNDLES: dict[str, tuple[str, ...]] = {
|
||||
ACCOUNTS_WRITE: ACCOUNTS_WRITE_GRANULAR,
|
||||
}
|
||||
|
||||
ALL_PERMISSIONS: tuple[str, ...] = (
|
||||
MENU_DASHBOARD,
|
||||
@@ -48,21 +90,34 @@ ALL_PERMISSIONS: tuple[str, ...] = (
|
||||
MENU_DOWNLOAD,
|
||||
MENU_HELP,
|
||||
MENU_USERS,
|
||||
MENU_ROLES,
|
||||
MENU_SETTINGS,
|
||||
MENU_DESKTOP_UPDATE,
|
||||
MENU_PAYMENT_SETTINGS,
|
||||
MENU_PAYMENT_ORDERS,
|
||||
ACCOUNTS_CREATE,
|
||||
ACCOUNTS_UPDATE,
|
||||
ACCOUNTS_DELETE,
|
||||
ACCOUNTS_START,
|
||||
ACCOUNTS_STOP,
|
||||
ACCOUNTS_COOKIE,
|
||||
ACCOUNTS_WRITE,
|
||||
MESSAGES_WRITE,
|
||||
RULES_WRITE,
|
||||
LINK_CARDS_WRITE,
|
||||
LOGS_READ,
|
||||
RECEIVED_MESSAGES_READ,
|
||||
SYSTEM_LOGS_READ,
|
||||
SYSTEM_LOGS_CLEAR,
|
||||
USERS_MANAGE,
|
||||
ROLES_MANAGE,
|
||||
SETTINGS_MANAGE,
|
||||
SETTINGS_DATABASE,
|
||||
DESKTOP_MANAGE,
|
||||
PAYMENTS_MANAGE,
|
||||
ORDERS_READ,
|
||||
ORDERS_CREATE,
|
||||
DATA_SCOPE_ALL,
|
||||
)
|
||||
|
||||
PERMISSION_SET = frozenset(ALL_PERMISSIONS)
|
||||
@@ -70,8 +125,10 @@ PERMISSION_SET = frozenset(ALL_PERMISSIONS)
|
||||
WRITE_PERMISSIONS = frozenset(
|
||||
{
|
||||
ACCOUNTS_WRITE,
|
||||
*ACCOUNTS_WRITE_GRANULAR,
|
||||
MESSAGES_WRITE,
|
||||
RULES_WRITE,
|
||||
LINK_CARDS_WRITE,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -85,22 +142,35 @@ _PERMISSION_META: dict[str, dict[str, str]] = {
|
||||
MENU_SYSTEM_LOGS: {"group": "menu", "label": "系统诊断日志"},
|
||||
MENU_DOWNLOAD: {"group": "menu", "label": "软件下载"},
|
||||
MENU_HELP: {"group": "menu", "label": "帮助中心"},
|
||||
MENU_USERS: {"group": "menu", "label": "用户与角色"},
|
||||
MENU_USERS: {"group": "menu", "label": "用户管理"},
|
||||
MENU_ROLES: {"group": "menu", "label": "角色设定"},
|
||||
MENU_SETTINGS: {"group": "menu", "label": "系统设置"},
|
||||
MENU_DESKTOP_UPDATE: {"group": "menu", "label": "桌面端升级"},
|
||||
MENU_PAYMENT_SETTINGS: {"group": "menu", "label": "支付配置"},
|
||||
MENU_PAYMENT_ORDERS: {"group": "menu", "label": "我的订单"},
|
||||
ACCOUNTS_WRITE: {"group": "action", "label": "账号写操作(启动/停止/改凭证/删除)"},
|
||||
MESSAGES_WRITE: {"group": "action", "label": "发送私信"},
|
||||
ACCOUNTS_CREATE: {"group": "action", "label": "新增抖音账号"},
|
||||
ACCOUNTS_UPDATE: {"group": "action", "label": "编辑账号信息"},
|
||||
ACCOUNTS_DELETE: {"group": "action", "label": "删除账号"},
|
||||
ACCOUNTS_START: {"group": "action", "label": "启动托管 / 批量启动"},
|
||||
ACCOUNTS_STOP: {"group": "action", "label": "停止托管"},
|
||||
ACCOUNTS_COOKIE: {"group": "action", "label": "查看/修改 Cookie 与凭证"},
|
||||
ACCOUNTS_WRITE: {"group": "action", "label": "账号全部写操作(兼容旧版,等同下列细项)"},
|
||||
MESSAGES_WRITE: {"group": "action", "label": "发送私信 / 队列立即发送"},
|
||||
RULES_WRITE: {"group": "action", "label": "编辑自动回复规则"},
|
||||
LINK_CARDS_WRITE: {"group": "action", "label": "上传素材 / 生成链接卡片"},
|
||||
LOGS_READ: {"group": "action", "label": "查看回复日志"},
|
||||
RECEIVED_MESSAGES_READ: {"group": "action", "label": "查看接收消息日志"},
|
||||
SYSTEM_LOGS_READ: {"group": "action", "label": "查看系统诊断日志"},
|
||||
USERS_MANAGE: {"group": "action", "label": "管理用户与角色"},
|
||||
SYSTEM_LOGS_CLEAR: {"group": "action", "label": "清空系统诊断日志"},
|
||||
USERS_MANAGE: {"group": "action", "label": "管理用户"},
|
||||
ROLES_MANAGE: {"group": "action", "label": "管理角色"},
|
||||
SETTINGS_MANAGE: {"group": "action", "label": "管理系统设置"},
|
||||
SETTINGS_DATABASE: {"group": "action", "label": "管理数据库配置与迁移"},
|
||||
DESKTOP_MANAGE: {"group": "action", "label": "管理桌面端升级"},
|
||||
PAYMENTS_MANAGE: {"group": "action", "label": "管理支付配置"},
|
||||
PAYMENTS_MANAGE: {"group": "action", "label": "管理支付配置与全部订单"},
|
||||
ORDERS_READ: {"group": "action", "label": "查看我的订单"},
|
||||
ORDERS_CREATE: {"group": "action", "label": "购买额度 / 创建订单"},
|
||||
DATA_SCOPE_ALL: {"group": "data", "label": "查看全部用户数据(全局数据范围)"},
|
||||
}
|
||||
|
||||
OPERATOR_PERMISSIONS: tuple[str, ...] = (
|
||||
@@ -113,12 +183,14 @@ OPERATOR_PERMISSIONS: tuple[str, ...] = (
|
||||
MENU_DOWNLOAD,
|
||||
MENU_HELP,
|
||||
MENU_PAYMENT_ORDERS,
|
||||
ACCOUNTS_WRITE,
|
||||
*ACCOUNTS_WRITE_GRANULAR,
|
||||
MESSAGES_WRITE,
|
||||
RULES_WRITE,
|
||||
LINK_CARDS_WRITE,
|
||||
LOGS_READ,
|
||||
RECEIVED_MESSAGES_READ,
|
||||
ORDERS_READ,
|
||||
ORDERS_CREATE,
|
||||
)
|
||||
|
||||
VIEWER_PERMISSIONS: tuple[str, ...] = (
|
||||
@@ -149,14 +221,158 @@ def normalize_permissions(codes: list[str] | tuple[str, ...] | None) -> list[str
|
||||
return result
|
||||
|
||||
|
||||
def expand_legacy_bundles(codes: set[str]) -> set[str]:
|
||||
"""Expand legacy bundle codes into granular permissions."""
|
||||
expanded = set(codes)
|
||||
for bundle, parts in LEGACY_BUNDLES.items():
|
||||
if bundle in expanded:
|
||||
expanded.update(parts)
|
||||
return expanded
|
||||
|
||||
|
||||
# Menu → required action. Selecting a menu always grants the action.
|
||||
MENU_REQUIRED_ACTIONS: dict[str, str | tuple[str, ...]] = {
|
||||
MENU_USERS: USERS_MANAGE,
|
||||
MENU_ROLES: ROLES_MANAGE,
|
||||
MENU_SETTINGS: SETTINGS_MANAGE,
|
||||
MENU_DESKTOP_UPDATE: DESKTOP_MANAGE,
|
||||
MENU_PAYMENT_SETTINGS: PAYMENTS_MANAGE,
|
||||
MENU_PAYMENT_ORDERS: (ORDERS_READ, ORDERS_CREATE),
|
||||
MENU_LOGS: LOGS_READ,
|
||||
MENU_RECEIVED_MESSAGES: RECEIVED_MESSAGES_READ,
|
||||
MENU_SYSTEM_LOGS: SYSTEM_LOGS_READ,
|
||||
MENU_ACCOUNTS: (), # page access only; buttons are separate
|
||||
MENU_MESSAGES: (),
|
||||
MENU_RULES: (),
|
||||
}
|
||||
|
||||
# Action → primary menu only.
|
||||
ACTION_PRIMARY_MENU: dict[str, str] = {
|
||||
USERS_MANAGE: MENU_USERS,
|
||||
ROLES_MANAGE: MENU_ROLES,
|
||||
SETTINGS_MANAGE: MENU_SETTINGS,
|
||||
SETTINGS_DATABASE: MENU_SETTINGS,
|
||||
DESKTOP_MANAGE: MENU_DESKTOP_UPDATE,
|
||||
PAYMENTS_MANAGE: MENU_PAYMENT_SETTINGS,
|
||||
ORDERS_READ: MENU_PAYMENT_ORDERS,
|
||||
ORDERS_CREATE: MENU_PAYMENT_ORDERS,
|
||||
LOGS_READ: MENU_LOGS,
|
||||
RECEIVED_MESSAGES_READ: MENU_RECEIVED_MESSAGES,
|
||||
SYSTEM_LOGS_READ: MENU_SYSTEM_LOGS,
|
||||
SYSTEM_LOGS_CLEAR: MENU_SYSTEM_LOGS,
|
||||
ACCOUNTS_CREATE: MENU_ACCOUNTS,
|
||||
ACCOUNTS_UPDATE: MENU_ACCOUNTS,
|
||||
ACCOUNTS_DELETE: MENU_ACCOUNTS,
|
||||
ACCOUNTS_START: MENU_ACCOUNTS,
|
||||
ACCOUNTS_STOP: MENU_ACCOUNTS,
|
||||
ACCOUNTS_COOKIE: MENU_ACCOUNTS,
|
||||
ACCOUNTS_WRITE: MENU_ACCOUNTS,
|
||||
MESSAGES_WRITE: MENU_MESSAGES,
|
||||
RULES_WRITE: MENU_RULES,
|
||||
LINK_CARDS_WRITE: MENU_RULES,
|
||||
}
|
||||
|
||||
|
||||
def _iter_required_actions(menu: str) -> tuple[str, ...]:
|
||||
raw = MENU_REQUIRED_ACTIONS.get(menu)
|
||||
if raw is None:
|
||||
return ()
|
||||
if isinstance(raw, str):
|
||||
return (raw,)
|
||||
return tuple(raw)
|
||||
|
||||
|
||||
# Catalog / UI pairs (menu → first required action for checkbox hints).
|
||||
MENU_ACTION_PAIRS: tuple[tuple[str, str], ...] = tuple(
|
||||
(menu, actions[0])
|
||||
for menu, actions in (
|
||||
(m, _iter_required_actions(m)) for m in MENU_REQUIRED_ACTIONS
|
||||
)
|
||||
if actions
|
||||
)
|
||||
|
||||
|
||||
def expand_paired_permissions(codes: list[str] | tuple[str, ...] | None) -> list[str]:
|
||||
"""Normalize, expand legacy bundles, and auto-complete menu/action pairs."""
|
||||
selected = expand_legacy_bundles(set(normalize_permissions(codes)))
|
||||
for menu in list(selected):
|
||||
for action in _iter_required_actions(menu):
|
||||
selected.add(action)
|
||||
for action, menu in ACTION_PRIMARY_MENU.items():
|
||||
if action in selected:
|
||||
selected.add(menu)
|
||||
# If every granular account write is present, keep the legacy bundle flag.
|
||||
if all(code in selected for code in ACCOUNTS_WRITE_GRANULAR):
|
||||
selected.add(ACCOUNTS_WRITE)
|
||||
return [code for code in ALL_PERMISSIONS if code in selected]
|
||||
|
||||
|
||||
def permission_implies(held: set[str], needed: str) -> bool:
|
||||
"""Whether a held permission set satisfies ``needed`` (incl. legacy bundles)."""
|
||||
if needed in held:
|
||||
return True
|
||||
for bundle, parts in LEGACY_BUNDLES.items():
|
||||
if needed in parts and bundle in held:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def permission_catalog() -> dict[str, Any]:
|
||||
menus = []
|
||||
actions = []
|
||||
data = []
|
||||
meta_by_code: dict[str, dict[str, str]] = {}
|
||||
for code in ALL_PERMISSIONS:
|
||||
meta = _PERMISSION_META[code]
|
||||
item = {"code": code, "label": meta["label"]}
|
||||
if meta["group"] == "menu":
|
||||
meta_by_code[code] = meta
|
||||
group = meta["group"]
|
||||
if group == "menu":
|
||||
menus.append(item)
|
||||
elif group == "data":
|
||||
data.append(item)
|
||||
else:
|
||||
actions.append(item)
|
||||
return {"menus": menus, "actions": actions}
|
||||
|
||||
# Reverse index: menu → child action codes (preserve ALL_PERMISSIONS order).
|
||||
children_by_menu: dict[str, list[dict[str, str]]] = {m["code"]: [] for m in menus}
|
||||
for code in ALL_PERMISSIONS:
|
||||
if meta_by_code[code]["group"] != "action":
|
||||
continue
|
||||
parent = ACTION_PRIMARY_MENU.get(code)
|
||||
if parent and parent in children_by_menu:
|
||||
children_by_menu[parent].append(
|
||||
{"code": code, "label": meta_by_code[code]["label"]}
|
||||
)
|
||||
|
||||
tree: list[dict[str, Any]] = []
|
||||
for menu in menus:
|
||||
node: dict[str, Any] = {
|
||||
"code": menu["code"],
|
||||
"label": menu["label"],
|
||||
"kind": "menu",
|
||||
"children": children_by_menu.get(menu["code"], []),
|
||||
}
|
||||
tree.append(node)
|
||||
|
||||
if data:
|
||||
tree.append(
|
||||
{
|
||||
"code": "__group.data__",
|
||||
"label": "数据权限",
|
||||
"kind": "group",
|
||||
"children": [
|
||||
{"code": item["code"], "label": item["label"]} for item in data
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"menus": menus,
|
||||
"actions": actions,
|
||||
"data": data,
|
||||
"tree": tree,
|
||||
"pairs": [
|
||||
{"menu": menu, "action": action} for menu, action in MENU_ACTION_PAIRS
|
||||
],
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from models.models import Role, User
|
||||
from .permissions import ALL_PERMISSIONS, normalize_permissions
|
||||
from .permissions import ALL_PERMISSIONS, expand_paired_permissions, normalize_permissions
|
||||
from .roles import (
|
||||
ROLE_ADMIN,
|
||||
RoleRecord,
|
||||
@@ -48,7 +48,11 @@ def _decode_permissions(raw: str | None) -> list[str]:
|
||||
|
||||
|
||||
def role_to_record(row: Role) -> RoleRecord:
|
||||
perms = list(ALL_PERMISSIONS) if row.is_admin else _decode_permissions(row.permissions)
|
||||
perms = (
|
||||
list(ALL_PERMISSIONS)
|
||||
if row.is_admin
|
||||
else expand_paired_permissions(_decode_permissions(row.permissions))
|
||||
)
|
||||
return RoleRecord(
|
||||
code=row.code,
|
||||
label=row.label,
|
||||
@@ -70,7 +74,7 @@ async def refresh_role_cache(db: AsyncSession) -> list[RoleRecord]:
|
||||
|
||||
|
||||
async def seed_builtin_roles(db: AsyncSession) -> None:
|
||||
"""Insert missing built-in roles and keep admin permissions complete."""
|
||||
"""Insert missing built-in roles and keep system role permissions in sync."""
|
||||
seeds = {seed.code: seed for seed in default_role_seeds()}
|
||||
result = await db.execute(select(Role))
|
||||
existing = {row.code: row for row in result.scalars().all()}
|
||||
@@ -92,17 +96,35 @@ async def seed_builtin_roles(db: AsyncSession) -> None:
|
||||
)
|
||||
changed = True
|
||||
continue
|
||||
# Keep system flags and admin full permission set in sync.
|
||||
# Keep system flags and built-in permission sets in sync with code.
|
||||
if not row.is_system:
|
||||
row.is_system = True
|
||||
changed = True
|
||||
if seed.is_admin and (not row.is_admin or row.permissions != payload):
|
||||
row.is_admin = True
|
||||
row.permissions = payload
|
||||
row.label = seed.label
|
||||
changed = True
|
||||
elif not row.label:
|
||||
row.label = seed.label
|
||||
if seed.is_admin:
|
||||
if not row.is_admin or row.permissions != payload or row.label != seed.label:
|
||||
row.is_admin = True
|
||||
row.permissions = payload
|
||||
row.label = seed.label
|
||||
if seed.description and row.description != seed.description:
|
||||
row.description = seed.description
|
||||
changed = True
|
||||
else:
|
||||
# operator / viewer: resync catalog so new menu/action codes ship.
|
||||
desired = _encode_permissions(expand_paired_permissions(seed.permissions))
|
||||
if row.permissions != desired or row.label != seed.label:
|
||||
row.permissions = desired
|
||||
row.label = seed.label
|
||||
row.is_admin = False
|
||||
changed = True
|
||||
|
||||
# Repair custom roles that have unpaired menu/action selections.
|
||||
for row in existing.values():
|
||||
if row.code in seeds or row.is_admin:
|
||||
continue
|
||||
repaired = expand_paired_permissions(_decode_permissions(row.permissions))
|
||||
encoded = _encode_permissions(repaired)
|
||||
if row.permissions != encoded:
|
||||
row.permissions = encoded
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
|
||||
+30
-11
@@ -7,12 +7,15 @@ from typing import Iterable
|
||||
|
||||
from .permissions import (
|
||||
ALL_PERMISSIONS,
|
||||
DATA_SCOPE_ALL,
|
||||
OPERATOR_PERMISSIONS,
|
||||
VIEWER_PERMISSIONS,
|
||||
WRITE_PERMISSIONS,
|
||||
normalize_permissions,
|
||||
expand_legacy_bundles,
|
||||
permission_implies,
|
||||
)
|
||||
|
||||
|
||||
ROLE_ADMIN = "admin"
|
||||
ROLE_OPERATOR = "operator"
|
||||
ROLE_VIEWER = "viewer"
|
||||
@@ -44,7 +47,7 @@ def default_role_seeds() -> list[RoleRecord]:
|
||||
RoleRecord(
|
||||
code=ROLE_ADMIN,
|
||||
label=ROLE_LABELS[ROLE_ADMIN],
|
||||
description="拥有全部菜单与操作权限,可管理全局数据",
|
||||
description="拥有全部菜单、按钮与全局数据权限",
|
||||
is_system=True,
|
||||
is_admin=True,
|
||||
permissions=list(ALL_PERMISSIONS),
|
||||
@@ -52,7 +55,7 @@ def default_role_seeds() -> list[RoleRecord]:
|
||||
RoleRecord(
|
||||
code=ROLE_OPERATOR,
|
||||
label=ROLE_LABELS[ROLE_OPERATOR],
|
||||
description="管理自己的账号、规则与私信",
|
||||
description="管理自己的账号、规则与私信(仅本人数据)",
|
||||
is_system=True,
|
||||
is_admin=False,
|
||||
permissions=list(OPERATOR_PERMISSIONS),
|
||||
@@ -91,20 +94,27 @@ def role_label(code: str | None) -> str:
|
||||
|
||||
|
||||
def is_admin(role: str | None) -> bool:
|
||||
"""True when the role has global data scope (built-in admin)."""
|
||||
"""True for the built-in admin role (global admin flag)."""
|
||||
record = get_cached_role(role)
|
||||
if record is not None:
|
||||
return bool(record.is_admin)
|
||||
# Fallback before cache is warm / for unit tests.
|
||||
return str(role or "") == ROLE_ADMIN
|
||||
|
||||
|
||||
def has_global_scope(role: str | None) -> bool:
|
||||
"""True when the role may see all users' data (admin or data.scope_all)."""
|
||||
if is_admin(role):
|
||||
return True
|
||||
return has_permission(role, DATA_SCOPE_ALL)
|
||||
|
||||
|
||||
def can_write(role: str | None) -> bool:
|
||||
record = get_cached_role(role)
|
||||
if record is not None:
|
||||
if record.is_admin:
|
||||
return True
|
||||
return any(code in WRITE_PERMISSIONS for code in record.permissions)
|
||||
held = expand_legacy_bundles(set(record.permissions))
|
||||
return any(code in WRITE_PERMISSIONS for code in held)
|
||||
return str(role or "") in (ROLE_ADMIN, ROLE_OPERATOR)
|
||||
|
||||
|
||||
@@ -112,6 +122,10 @@ def can_manage_users(role: str | None) -> bool:
|
||||
return has_permission(role, "users.manage")
|
||||
|
||||
|
||||
def can_manage_roles(role: str | None) -> bool:
|
||||
return has_permission(role, "roles.manage")
|
||||
|
||||
|
||||
def has_permission(role: str | None, permission: str) -> bool:
|
||||
code = str(permission or "").strip()
|
||||
if not code:
|
||||
@@ -121,13 +135,14 @@ def has_permission(role: str | None, permission: str) -> bool:
|
||||
if str(role or "") == ROLE_ADMIN:
|
||||
return True
|
||||
if str(role or "") == ROLE_OPERATOR:
|
||||
return code in OPERATOR_PERMISSIONS
|
||||
return permission_implies(expand_legacy_bundles(set(OPERATOR_PERMISSIONS)), code)
|
||||
if str(role or "") == ROLE_VIEWER:
|
||||
return code in VIEWER_PERMISSIONS
|
||||
return permission_implies(set(VIEWER_PERMISSIONS), code)
|
||||
return False
|
||||
if record.is_admin:
|
||||
return True
|
||||
return code in record.permissions
|
||||
held = expand_legacy_bundles(set(record.permissions))
|
||||
return permission_implies(held, code)
|
||||
|
||||
|
||||
def permissions_for_role(role: str | None) -> list[str]:
|
||||
@@ -136,7 +151,9 @@ def permissions_for_role(role: str | None) -> list[str]:
|
||||
if str(role or "") == ROLE_ADMIN:
|
||||
return list(ALL_PERMISSIONS)
|
||||
if str(role or "") == ROLE_OPERATOR:
|
||||
return list(OPERATOR_PERMISSIONS)
|
||||
from .permissions import expand_paired_permissions
|
||||
|
||||
return expand_paired_permissions(OPERATOR_PERMISSIONS)
|
||||
if str(role or "") == ROLE_VIEWER:
|
||||
return list(VIEWER_PERMISSIONS)
|
||||
return []
|
||||
@@ -164,4 +181,6 @@ def sanitize_role_permissions(
|
||||
) -> list[str]:
|
||||
if force_all:
|
||||
return list(ALL_PERMISSIONS)
|
||||
return normalize_permissions(codes)
|
||||
from .permissions import expand_paired_permissions
|
||||
|
||||
return expand_paired_permissions(codes)
|
||||
|
||||
+23
-12
@@ -14,7 +14,7 @@ from .account_limits import (
|
||||
normalize_max_accounts,
|
||||
)
|
||||
from .account_quota import default_stop_worker, sync_user_account_quota
|
||||
from .dependencies import get_current_user, require_user_manager
|
||||
from .dependencies import get_current_user, require_role_manager, require_user_manager
|
||||
from .email_service import (
|
||||
build_password_reset_link,
|
||||
build_verification_link,
|
||||
@@ -37,6 +37,7 @@ from .role_service import (
|
||||
user_permission_payload,
|
||||
)
|
||||
from .roles import ROLE_OPERATOR, is_admin
|
||||
from .scopes import ensure_user_manageable, users_for_manager
|
||||
from .schemas import (
|
||||
LoginRequest,
|
||||
MessageResponse,
|
||||
@@ -393,17 +394,20 @@ async def list_auth_roles(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(get_current_user),
|
||||
):
|
||||
"""Lightweight role list for dropdowns (any logged-in user)."""
|
||||
"""Lightweight role list for dropdowns (any logged-in user).
|
||||
|
||||
Intentionally omits permission arrays to avoid leaking the full ACL map.
|
||||
"""
|
||||
records = await list_role_records(db)
|
||||
return RolesResponse(
|
||||
roles=[
|
||||
RoleInfo(
|
||||
value=item.code,
|
||||
label=item.label,
|
||||
description=item.description or None,
|
||||
description=None,
|
||||
is_system=item.is_system,
|
||||
is_admin=item.is_admin,
|
||||
permissions=list(item.permissions),
|
||||
permissions=[],
|
||||
)
|
||||
for item in records
|
||||
]
|
||||
@@ -417,9 +421,9 @@ roles_router = APIRouter(prefix="/api/roles", tags=["roles"])
|
||||
@users_router.get("", response_model=list[UserResponse])
|
||||
async def list_users(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(require_user_manager),
|
||||
current: User = Depends(require_user_manager),
|
||||
):
|
||||
result = await db.execute(select(User).order_by(User.id.asc()))
|
||||
result = await db.execute(users_for_manager(current).order_by(User.id.asc()))
|
||||
users = result.scalars().all()
|
||||
responses = []
|
||||
for user in users:
|
||||
@@ -431,13 +435,15 @@ async def list_users(
|
||||
async def create_user(
|
||||
body: UserCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(require_user_manager),
|
||||
current: User = Depends(require_user_manager),
|
||||
):
|
||||
settings = await load_settings(db)
|
||||
exists = await db.execute(select(User).where(User.username == body.username))
|
||||
if exists.scalar_one_or_none():
|
||||
raise HTTPException(status_code=400, detail="用户名已存在")
|
||||
role = await ensure_role_assignable(db, body.role)
|
||||
if is_admin(role) and not is_admin(current.role):
|
||||
raise HTTPException(status_code=403, detail="只有管理员可以分配管理员角色")
|
||||
email = await _ensure_email_available(db, str(body.email) if body.email else None)
|
||||
if settings.email_binding_required and not is_admin(role) and not email:
|
||||
raise HTTPException(status_code=400, detail="系统已开启「登录必须绑定邮箱」,请填写邮箱")
|
||||
@@ -455,6 +461,7 @@ async def create_user(
|
||||
email_verified=email_verified,
|
||||
email_verified_at=datetime.utcnow() if email and email_verified else None,
|
||||
max_accounts=normalize_max_accounts(body.max_accounts, role),
|
||||
created_by=current.id,
|
||||
)
|
||||
db.add(user)
|
||||
await db.commit()
|
||||
@@ -473,6 +480,7 @@ async def update_user(
|
||||
user = result.scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
ensure_user_manageable(current, user)
|
||||
settings = await load_settings(db)
|
||||
if user.id == current.id and body.is_active is False:
|
||||
raise HTTPException(status_code=400, detail="不能禁用当前登录账号")
|
||||
@@ -482,6 +490,8 @@ async def update_user(
|
||||
await guard_last_admin_change(db, user=user, deactivating=True)
|
||||
if body.role is not None:
|
||||
new_role = await ensure_role_assignable(db, body.role)
|
||||
if is_admin(new_role) and not is_admin(current.role):
|
||||
raise HTTPException(status_code=403, detail="只有管理员可以分配管理员角色")
|
||||
await guard_last_admin_change(db, user=user, new_role=new_role)
|
||||
|
||||
if body.display_name is not None:
|
||||
@@ -541,6 +551,7 @@ async def delete_user(
|
||||
user = result.scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
ensure_user_manageable(current, user)
|
||||
await guard_last_admin_change(db, user=user, deleting=True)
|
||||
await db.delete(user)
|
||||
await db.commit()
|
||||
@@ -550,7 +561,7 @@ async def delete_user(
|
||||
@roles_router.get("", response_model=RolesResponse)
|
||||
async def admin_list_roles(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(require_user_manager),
|
||||
_: User = Depends(require_role_manager),
|
||||
):
|
||||
records = await list_role_records(db)
|
||||
roles = []
|
||||
@@ -570,7 +581,7 @@ async def admin_list_roles(
|
||||
|
||||
|
||||
@roles_router.get("/catalog", response_model=PermissionCatalogResponse)
|
||||
async def get_permission_catalog(_: User = Depends(require_user_manager)):
|
||||
async def get_permission_catalog(_: User = Depends(require_role_manager)):
|
||||
return PermissionCatalogResponse(**permission_catalog())
|
||||
|
||||
|
||||
@@ -578,7 +589,7 @@ async def get_permission_catalog(_: User = Depends(require_user_manager)):
|
||||
async def create_custom_role(
|
||||
body: RoleCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(require_user_manager),
|
||||
_: User = Depends(require_role_manager),
|
||||
):
|
||||
record = await create_role(
|
||||
db,
|
||||
@@ -603,7 +614,7 @@ async def update_custom_role(
|
||||
code: str,
|
||||
body: RoleUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(require_user_manager),
|
||||
_: User = Depends(require_role_manager),
|
||||
):
|
||||
record = await update_role(
|
||||
db,
|
||||
@@ -627,7 +638,7 @@ async def update_custom_role(
|
||||
async def delete_custom_role(
|
||||
code: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(require_user_manager),
|
||||
_: User = Depends(require_role_manager),
|
||||
):
|
||||
await delete_role(db, code)
|
||||
return {"message": "角色已删除"}
|
||||
|
||||
@@ -132,3 +132,6 @@ class RoleUpdate(BaseModel):
|
||||
class PermissionCatalogResponse(BaseModel):
|
||||
menus: list[dict[str, Any]]
|
||||
actions: list[dict[str, Any]]
|
||||
data: list[dict[str, Any]] = Field(default_factory=list)
|
||||
tree: list[dict[str, Any]] = Field(default_factory=list)
|
||||
pairs: list[dict[str, Any]] = Field(default_factory=list)
|
||||
|
||||
+57
-14
@@ -5,8 +5,12 @@ from sqlalchemy import or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from models.models import Account, AutoReplyRule, MessageLog, ReceivedMessageLog, SystemLog, User
|
||||
from .permissions import ACCOUNTS_WRITE, RULES_WRITE
|
||||
from .roles import has_permission, is_admin
|
||||
from .permissions import (
|
||||
ACCOUNTS_UPDATE,
|
||||
ACCOUNTS_WRITE_GRANULAR,
|
||||
RULES_WRITE,
|
||||
)
|
||||
from .roles import has_global_scope, has_permission, is_admin
|
||||
|
||||
|
||||
async def get_owned_account(
|
||||
@@ -15,29 +19,42 @@ async def get_owned_account(
|
||||
account_id: int,
|
||||
*,
|
||||
write: bool = False,
|
||||
write_permission: str | None = None,
|
||||
) -> Account:
|
||||
result = await db.execute(select(Account).where(Account.id == account_id))
|
||||
account = result.scalar_one_or_none()
|
||||
if not account:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="账号不存在")
|
||||
if is_admin(user.role):
|
||||
if has_global_scope(user.role):
|
||||
if write:
|
||||
needed = write_permission or ACCOUNTS_UPDATE
|
||||
if not has_permission(user.role, needed):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"缺少权限:{needed}",
|
||||
)
|
||||
return account
|
||||
if account.owner_id != user.id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权访问该账号")
|
||||
if write and not has_permission(user.role, ACCOUNTS_WRITE):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="只读用户无法修改")
|
||||
if write:
|
||||
needed = write_permission or ACCOUNTS_UPDATE
|
||||
if not has_permission(user.role, needed):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"缺少权限:{needed}",
|
||||
)
|
||||
return account
|
||||
|
||||
|
||||
def accounts_for_user(user: User):
|
||||
stmt = select(Account)
|
||||
if not is_admin(user.role):
|
||||
if not has_global_scope(user.role):
|
||||
stmt = stmt.where(Account.owner_id == user.id)
|
||||
return stmt
|
||||
|
||||
|
||||
async def owned_account_ids(db: AsyncSession, user: User) -> Optional[set[int]]:
|
||||
if is_admin(user.role):
|
||||
if has_global_scope(user.role):
|
||||
return None
|
||||
result = await db.execute(select(Account.id).where(Account.owner_id == user.id))
|
||||
return {row[0] for row in result.all()}
|
||||
@@ -47,7 +64,7 @@ def logs_for_user(user: User, account_id: Optional[int] = None):
|
||||
stmt = select(MessageLog)
|
||||
if account_id is not None:
|
||||
stmt = stmt.where(MessageLog.account_id == account_id)
|
||||
if not is_admin(user.role):
|
||||
if not has_global_scope(user.role):
|
||||
owned = select(Account.id).where(Account.owner_id == user.id)
|
||||
stmt = stmt.where(MessageLog.account_id.in_(owned))
|
||||
return stmt
|
||||
@@ -57,7 +74,7 @@ def received_logs_for_user(user: User, account_id: Optional[int] = None):
|
||||
stmt = select(ReceivedMessageLog)
|
||||
if account_id is not None:
|
||||
stmt = stmt.where(ReceivedMessageLog.account_id == account_id)
|
||||
if not is_admin(user.role):
|
||||
if not has_global_scope(user.role):
|
||||
owned = select(Account.id).where(Account.owner_id == user.id)
|
||||
stmt = stmt.where(ReceivedMessageLog.account_id.in_(owned))
|
||||
return stmt
|
||||
@@ -67,19 +84,23 @@ def rules_for_user(user: User, account_id: Optional[int] = None):
|
||||
stmt = select(AutoReplyRule)
|
||||
if account_id is not None:
|
||||
stmt = stmt.where(AutoReplyRule.account_id == account_id)
|
||||
if is_admin(user.role):
|
||||
if has_global_scope(user.role):
|
||||
return stmt
|
||||
owned = select(Account.id).where(Account.owner_id == user.id)
|
||||
return stmt.where(AutoReplyRule.account_id.in_(owned))
|
||||
|
||||
|
||||
async def get_accessible_rule(db: AsyncSession, user: User, rule_id: int, *, write: bool = False) -> AutoReplyRule:
|
||||
async def get_accessible_rule(
|
||||
db: AsyncSession, user: User, rule_id: int, *, write: bool = False
|
||||
) -> AutoReplyRule:
|
||||
result = await db.execute(select(AutoReplyRule).where(AutoReplyRule.id == rule_id))
|
||||
rule = result.scalar_one_or_none()
|
||||
if not rule:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="规则不存在")
|
||||
|
||||
if is_admin(user.role):
|
||||
if has_global_scope(user.role):
|
||||
if write and not has_permission(user.role, RULES_WRITE):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="缺少权限:rules.write")
|
||||
return rule
|
||||
|
||||
if rule.account_id is None:
|
||||
@@ -89,7 +110,7 @@ async def get_accessible_rule(db: AsyncSession, user: User, rule_id: int, *, wri
|
||||
if rule.owner_id and rule.owner_id != user.id and account.owner_id != user.id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权访问该规则")
|
||||
if write and not has_permission(user.role, RULES_WRITE):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="只读用户无法修改")
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="缺少权限:rules.write")
|
||||
return rule
|
||||
|
||||
|
||||
@@ -97,7 +118,7 @@ def system_logs_for_user(user: User, account_id: Optional[int] = None):
|
||||
stmt = select(SystemLog)
|
||||
if account_id is not None:
|
||||
stmt = stmt.where(SystemLog.account_id == account_id)
|
||||
if not is_admin(user.role):
|
||||
if not has_global_scope(user.role):
|
||||
owned = select(Account.id).where(Account.owner_id == user.id)
|
||||
stmt = stmt.where(
|
||||
or_(
|
||||
@@ -106,3 +127,25 @@ def system_logs_for_user(user: User, account_id: Optional[int] = None):
|
||||
)
|
||||
)
|
||||
return stmt
|
||||
|
||||
|
||||
def users_for_manager(manager: User):
|
||||
"""Admins see all users; others only see themselves and users they created."""
|
||||
stmt = select(User)
|
||||
if is_admin(manager.role):
|
||||
return stmt
|
||||
return stmt.where(or_(User.created_by == manager.id, User.id == manager.id))
|
||||
|
||||
|
||||
def ensure_user_manageable(manager: User, target: User) -> None:
|
||||
"""Raise 403 unless manager may edit/delete target user."""
|
||||
if is_admin(manager.role):
|
||||
return
|
||||
if target.id == manager.id:
|
||||
return
|
||||
if target.created_by == manager.id:
|
||||
return
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="只能管理自己创建的用户",
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ from models.db_config import (
|
||||
from models.db_transfer import inspect_sqlite_source, migrate_sqlite_to_target
|
||||
from models.models import User
|
||||
from .dependencies import require_permission
|
||||
from .permissions import SETTINGS_MANAGE
|
||||
from .permissions import PAYMENTS_MANAGE, SETTINGS_DATABASE, SETTINGS_MANAGE
|
||||
from .email_service import send_test_email
|
||||
from .system_settings import (
|
||||
PASSWORD_PLACEHOLDER,
|
||||
@@ -231,7 +231,7 @@ async def update_system_settings(
|
||||
@router.get("/payment", response_model=PaymentSettingsResponse)
|
||||
async def get_payment_settings(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(require_permission(SETTINGS_MANAGE)),
|
||||
_: User = Depends(require_permission(PAYMENTS_MANAGE)),
|
||||
):
|
||||
data = await load_settings(db)
|
||||
return PaymentSettingsResponse(**settings_to_payment_response(data))
|
||||
@@ -241,7 +241,7 @@ async def get_payment_settings(
|
||||
async def update_payment_settings(
|
||||
body: PaymentSettingsUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(require_permission(SETTINGS_MANAGE)),
|
||||
_: User = Depends(require_permission(PAYMENTS_MANAGE)),
|
||||
):
|
||||
updates = body.model_dump(exclude_unset=True)
|
||||
data = await save_settings(db, updates)
|
||||
@@ -277,14 +277,14 @@ async def test_smtp_email(
|
||||
|
||||
|
||||
@router.get("/database", response_model=DatabaseSettingsResponse)
|
||||
async def get_database_settings(_: User = Depends(require_permission(SETTINGS_MANAGE))):
|
||||
async def get_database_settings(_: User = Depends(require_permission(SETTINGS_DATABASE))):
|
||||
return DatabaseSettingsResponse(**database_config_to_response())
|
||||
|
||||
|
||||
@router.put("/database", response_model=MessageResponse)
|
||||
async def update_database_settings(
|
||||
body: DatabaseSettingsUpdate,
|
||||
_: User = Depends(require_permission(SETTINGS_MANAGE)),
|
||||
_: User = Depends(require_permission(SETTINGS_DATABASE)),
|
||||
):
|
||||
payload = body.model_dump(exclude_unset=True)
|
||||
if payload.get("db_password") in (None, "", DB_PASSWORD_PLACEHOLDER):
|
||||
@@ -303,7 +303,7 @@ async def update_database_settings(
|
||||
@router.post("/database/test", response_model=MessageResponse)
|
||||
async def test_database_settings(
|
||||
body: DatabaseTestRequest,
|
||||
_: User = Depends(require_permission(SETTINGS_MANAGE)),
|
||||
_: User = Depends(require_permission(SETTINGS_DATABASE)),
|
||||
):
|
||||
payload = body.model_dump(exclude_unset=True)
|
||||
if payload.get("db_password") in (None, "", DB_PASSWORD_PLACEHOLDER):
|
||||
@@ -318,7 +318,7 @@ async def test_database_settings(
|
||||
@router.get("/database/migrate/preview", response_model=DatabaseMigratePreviewResponse)
|
||||
async def preview_database_migration(
|
||||
source_db_path: str | None = None,
|
||||
_: User = Depends(require_permission(SETTINGS_MANAGE)),
|
||||
_: User = Depends(require_permission(SETTINGS_DATABASE)),
|
||||
):
|
||||
return DatabaseMigratePreviewResponse(**await inspect_sqlite_source(source_db_path))
|
||||
|
||||
@@ -326,7 +326,7 @@ async def preview_database_migration(
|
||||
@router.post("/database/migrate", response_model=DatabaseMigrateResponse)
|
||||
async def migrate_database_data(
|
||||
body: DatabaseMigrateRequest,
|
||||
_: User = Depends(require_permission(SETTINGS_MANAGE)),
|
||||
_: User = Depends(require_permission(SETTINGS_DATABASE)),
|
||||
):
|
||||
payload = body.model_dump(exclude_unset=True)
|
||||
clear_target = bool(payload.pop("clear_target", False))
|
||||
|
||||
Reference in New Issue
Block a user