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