You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

108 lines
3.7 KiB

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, []