from __future__ import annotations from collections.abc import Sequence import re from app.extensions import db from app.models import Account ACCOUNT_ROLE_OPTIONS = ("admin", "editor", "entry_only", "quick_editor") ACCOUNT_STATUS_OPTIONS = ("active", "disabled") USERNAME_PATTERN = re.compile(r"^[A-Za-z0-9._-]{3,64}$") PASSWORD_UPPERCASE_PATTERN = re.compile(r"[A-Z]") PASSWORD_LOWERCASE_PATTERN = re.compile(r"[a-z]") PASSWORD_DIGIT_PATTERN = re.compile(r"[0-9]") MIN_PASSWORD_LENGTH = 8 def list_accounts() -> Sequence[Account]: result = db.session.execute( db.select(Account).order_by(Account.created_at.desc(), Account.id.desc()), ) return result.scalars().all() def get_account_or_none(account_id: int) -> Account | None: return db.session.get(Account, account_id) def is_username_taken(username: str, *, excluding_account_id: int | None = None) -> bool: query = db.select(Account.id).where(Account.username == username) if excluding_account_id is not None: query = query.where(Account.id != excluding_account_id) return db.session.execute(query.limit(1)).scalar_one_or_none() is not None def serialize_account_snapshot(account: Account) -> dict[str, object]: return { "username": account.username, "display_name": account.display_name, "role": account.role, "status": account.status, "last_login_at": account.last_login_at.isoformat() if account.last_login_at else None, } def parse_account_form(form: dict[str, str], *, is_create: bool) -> tuple[dict[str, object], list[str]]: errors: list[str] = [] username = form.get("username", "").strip() display_name = form.get("display_name", "").strip() or None role = form.get("role", "").strip() if is_create: if not username: errors.append("账号名不能为空。") elif not USERNAME_PATTERN.fullmatch(username): errors.append("账号名仅支持 3-64 位字母、数字、点、下划线或中横线。") if role not in ACCOUNT_ROLE_OPTIONS: errors.append("账号角色不合法。") payload: dict[str, object] = { "display_name": display_name, "role": role, } if is_create: password = form.get("password", "") confirm_password = form.get("confirm_password", "") if len(password) < MIN_PASSWORD_LENGTH: errors.append(f"初始密码长度不能少于 {MIN_PASSWORD_LENGTH} 位。") if not _is_password_complex_enough(password): errors.append("初始密码需同时包含大写字母、小写字母和数字。") if password != confirm_password: errors.append("两次输入的密码不一致。") payload["username"] = username payload["password"] = password if errors: return {}, errors return payload, [] def _is_password_complex_enough(password: str) -> bool: return ( PASSWORD_UPPERCASE_PATTERN.search(password) is not None and PASSWORD_LOWERCASE_PATTERN.search(password) is not None and PASSWORD_DIGIT_PATTERN.search(password) is not None ) def parse_password_reset_form(form: dict[str, str]) -> tuple[str | None, list[str]]: errors: list[str] = [] password = form.get("password", "") confirm_password = form.get("confirm_password", "") if len(password) < MIN_PASSWORD_LENGTH: errors.append(f"新密码长度不能少于 {MIN_PASSWORD_LENGTH} 位。") if not _is_password_complex_enough(password): errors.append("新密码需同时包含大写字母、小写字母和数字。") if password != confirm_password: errors.append("两次输入的新密码不一致。") if errors: return None, errors return password, []