Files
Foxel/setup/foxel_cli.py
shiyu e6ab01ef9d feat: add user and role management pages with API integration
- Implemented user management functionality in UsersPage including user creation, editing, deletion, and role assignment.
- Added role management functionality in RolesPage with role creation, editing, deletion, and path rule management.
- Created users API for handling user-related operations.
- Created roles API for handling role-related operations.
- Integrated permissions handling in both user and role management.
- Enhanced UI with Ant Design components for better user experience.
2026-01-30 15:59:22 +08:00

287 lines
9.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import os
import secrets
import sqlite3
import string
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from domain.config import VERSION
from domain.auth import get_password_hash
from domain.permission.types import PERMISSION_DEFINITIONS
from domain.role.types import SystemRoles
def _project_root() -> Path:
return PROJECT_ROOT
def _supports_color() -> bool:
return sys.stderr.isatty() and not os.getenv("NO_COLOR")
def _print_banner() -> None:
if not sys.stderr.isatty():
return
banner = "\n".join(
[
"███████╗ ██████╗ ██╗ ██╗███████╗██╗",
"██╔════╝██╔═══██╗╚██╗██╔╝██╔════╝██║",
"█████╗ ██║ ██║ ╚███╔╝ █████╗ ██║",
"██╔══╝ ██║ ██║ ██╔██╗ ██╔══╝ ██║",
"██║ ╚██████╔╝██╔╝ ██╗███████╗███████╗",
"╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝",
]
)
title = f"Foxel Admin CLI {VERSION}"
if _supports_color():
c_reset = "\033[0m"
c_bold = "\033[1m"
c_orange = "\033[38;5;208m"
c_orange_light = "\033[38;5;214m"
c_orange_lighter = "\033[38;5;220m"
banner_lines = banner.splitlines()
shades = [
c_orange,
c_orange_light,
c_orange_lighter,
c_orange_lighter,
c_orange_light,
c_orange,
]
for line, color in zip(banner_lines, shades, strict=False):
print(f"{c_bold}{color}{line}{c_reset}", file=sys.stderr)
print(f"{c_bold}{title}{c_reset}\n", file=sys.stderr)
else:
print(banner, file=sys.stderr)
print(f"{title}\n", file=sys.stderr)
def _default_db_path() -> Path:
return _project_root() / "data/db/db.sqlite3"
def _gen_password(length: int) -> str:
alphabet = string.ascii_letters + string.digits
return "".join(secrets.choice(alphabet) for _ in range(length))
def _find_user(conn: sqlite3.Connection, username_or_email: str) -> tuple[int, str] | None:
cursor = conn.cursor()
cursor.execute("SELECT id, username FROM user WHERE username = ?", (username_or_email,))
row = cursor.fetchone()
if row:
return int(row[0]), str(row[1])
cursor.execute("SELECT id, username FROM user WHERE email = ?", (username_or_email,))
row = cursor.fetchone()
if row:
return int(row[0]), str(row[1])
normalized = username_or_email.strip().lower()
if normalized and normalized != username_or_email:
cursor.execute("SELECT id, username FROM user WHERE email = ?", (normalized,))
row = cursor.fetchone()
if row:
return int(row[0]), str(row[1])
return None
def _cmd_reset_password(args: argparse.Namespace) -> int:
db_path = Path(args.db).expanduser() if args.db else _default_db_path()
if args.random:
password = _gen_password(args.length)
else:
password = args.password
hashed_password = get_password_hash(password)
conn = sqlite3.connect(str(db_path))
try:
user = _find_user(conn, args.username_or_email)
if not user:
print(f"用户不存在: {args.username_or_email}", file=sys.stderr)
return 1
user_id, username = user
conn.execute(
"UPDATE user SET hashed_password = ? WHERE id = ?",
(hashed_password, user_id),
)
conn.commit()
finally:
conn.close()
if args.random:
print(password)
print(f"已重置用户密码: {username} (id={user_id})", file=sys.stderr)
return 0
def _cmd_init_rbac(args: argparse.Namespace) -> int:
db_path = Path(args.db).expanduser() if args.db else _default_db_path()
role_definitions = [
{
"name": SystemRoles.ADMIN,
"description": "管理员角色,拥有所有系统和适配器权限",
},
{
"name": SystemRoles.USER,
"description": "普通用户角色,需要管理员配置路径权限",
},
{
"name": SystemRoles.VIEWER,
"description": "只读用户角色,仅可查看文件",
},
]
conn = sqlite3.connect(str(db_path))
try:
conn.execute("PRAGMA foreign_keys = ON")
cursor = conn.cursor()
try:
cursor.execute("SELECT 1 FROM permissions LIMIT 1")
cursor.execute("SELECT 1 FROM roles LIMIT 1")
cursor.execute("SELECT 1 FROM role_permissions LIMIT 1")
cursor.execute("SELECT 1 FROM path_rules LIMIT 1")
except sqlite3.OperationalError as exc:
print(f"数据库未初始化(缺少表)。请先启动一次服务生成表。{exc}", file=sys.stderr)
return 1
# upsert permissions
for perm in PERMISSION_DEFINITIONS:
cursor.execute(
"""
INSERT INTO permissions (code, name, category, description)
VALUES (?, ?, ?, ?)
ON CONFLICT(code) DO UPDATE SET
name = excluded.name,
category = excluded.category,
description = excluded.description
""",
(
perm["code"],
perm["name"],
perm["category"],
perm.get("description"),
),
)
# upsert roles
for role in role_definitions:
cursor.execute(
"""
INSERT INTO roles (name, description, is_system)
VALUES (?, ?, 1)
ON CONFLICT(name) DO UPDATE SET
description = excluded.description,
is_system = 1
""",
(role["name"], role["description"]),
)
# grant all permissions to Admin role
cursor.execute("SELECT id FROM roles WHERE name = ?", (SystemRoles.ADMIN,))
admin_row = cursor.fetchone()
if not admin_row:
print("初始化失败:未找到 Admin 角色", file=sys.stderr)
return 1
admin_role_id = int(admin_row[0])
cursor.execute("DELETE FROM role_permissions WHERE role_id = ?", (admin_role_id,))
cursor.execute("SELECT id FROM permissions")
permission_ids = [int(row[0]) for row in cursor.fetchall()]
cursor.executemany(
"INSERT INTO role_permissions (role_id, permission_id) VALUES (?, ?)",
[(admin_role_id, pid) for pid in permission_ids],
)
# ensure Admin has full access path rule
cursor.execute(
"SELECT id FROM path_rules WHERE role_id = ? AND path_pattern = ? LIMIT 1",
(admin_role_id, "/**"),
)
existing_rule = cursor.fetchone()
if existing_rule:
cursor.execute(
"""
UPDATE path_rules
SET is_regex = 0,
can_read = 1,
can_write = 1,
can_delete = 1,
can_share = 1,
priority = 100
WHERE id = ?
""",
(int(existing_rule[0]),),
)
else:
cursor.execute(
"""
INSERT INTO path_rules (
role_id, path_pattern, is_regex,
can_read, can_write, can_delete, can_share,
priority
)
VALUES (?, ?, 0, 1, 1, 1, 1, 100)
""",
(admin_role_id, "/**"),
)
conn.commit()
finally:
conn.close()
print(f"已初始化权限: {len(PERMISSION_DEFINITIONS)}", file=sys.stderr)
print("已补齐内置角色: Admin / User / Viewer", file=sys.stderr)
print("已为 Admin 角色授予全部权限并设置 /** 全路径规则", file=sys.stderr)
return 0
def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="foxel")
subparsers = parser.add_subparsers(dest="command", required=True)
reset_password = subparsers.add_parser("reset-password", help="重置用户密码")
reset_password.add_argument("username_or_email", help="用户名或邮箱")
reset_password.add_argument("password", nargs="?", help="新密码(或用 --random")
reset_password.add_argument("--random", action="store_true", help="生成随机密码并输出到 stdout")
reset_password.add_argument("--length", type=int, default=16, help="随机密码长度(默认 16")
reset_password.add_argument("--db", help="sqlite db 路径(默认 data/db/db.sqlite3")
reset_password.set_defaults(func=_cmd_reset_password)
init_rbac = subparsers.add_parser("init-rbac", help="初始化权限与内置角色")
init_rbac.add_argument("--db", help="sqlite db 路径(默认 data/db/db.sqlite3")
init_rbac.set_defaults(func=_cmd_init_rbac)
return parser
def main(argv: list[str] | None = None) -> int:
_print_banner()
parser = _build_parser()
args = parser.parse_args(argv)
if args.command == "reset-password" and not args.random and not args.password:
parser.error("reset-password 需要提供 password 或使用 --random")
return int(args.func(args))
if __name__ == "__main__":
raise SystemExit(main())