mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-09-05 15:47:00 +08:00
feat(cli): add foxel_cli.py for user password management and setup symlink
This commit is contained in:
@@ -33,6 +33,8 @@ COPY . .
|
|||||||
|
|
||||||
RUN mkdir -p data/db data/mount && \
|
RUN mkdir -p data/db data/mount && \
|
||||||
chmod 777 data/db data/mount && \
|
chmod 777 data/db data/mount && \
|
||||||
|
chmod +x setup/foxel_cli.py && \
|
||||||
|
ln -sf /app/setup/foxel_cli.py /usr/local/bin/foxel && \
|
||||||
rm -rf /var/log/apt /var/cache/apt/archives
|
rm -rf /var/log/apt /var/cache/apt/archives
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
#!/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.auth.service import get_password_hash
|
||||||
|
from domain.config.service import VERSION
|
||||||
|
|
||||||
|
|
||||||
|
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 _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)
|
||||||
|
|
||||||
|
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())
|
||||||
Reference in New Issue
Block a user