加上登录和角色权限,并用 Vue 控制台区分用户与管理员。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-10 22:43:31 +08:00
parent 78601aae5e
commit 8b55b9cf55
36 changed files with 2947 additions and 1007 deletions
+103
View File
@@ -0,0 +1,103 @@
"""注册、登录,以及首次启动时创建管理员。"""
from __future__ import annotations
import logging
import secrets
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.clock import utcnow
from app.config import settings
from app.errors import AppError
from app.models import User, UserRole
from app.security import hash_password, verify_password
from app.services.registry import register_user
logger = logging.getLogger(__name__)
def register_account(
session: Session,
wallet_address: str,
username: str | None,
password: str | None,
) -> User:
name = _clean_username(username)
secret = (password or "").strip()
if name and not secret:
raise AppError("设置用户名时必须填写密码")
if secret and not name:
raise AppError("设置密码时必须填写用户名")
if secret and len(secret) < 6:
raise AppError("密码至少 6 位")
return register_user(
session,
wallet_address,
username=name,
password_hash=hash_password(secret) if secret else None,
)
def authenticate(
session: Session,
wallet_address: str | None,
username: str | None,
password: str | None,
) -> User:
name = _clean_username(username)
wallet = (wallet_address or "").strip()
secret = (password or "").strip()
if name and secret:
user = session.scalar(select(User).where(User.username == name))
if user is None or not verify_password(secret, user.password_hash):
raise AppError("账号或密码错误", 401)
return user
if wallet and secret:
user = session.scalar(select(User).where(User.wallet_address == wallet))
if user is None or not verify_password(secret, user.password_hash):
raise AppError("账号或密码错误", 401)
return user
if wallet:
user = session.scalar(select(User).where(User.wallet_address == wallet))
if user is None:
raise AppError("钱包未注册", 401)
if user.role == UserRole.ADMIN:
raise AppError("管理员请使用账号密码登录", 403)
return user
raise AppError("请填写钱包地址,或账号和密码")
def ensure_admin(session: Session) -> None:
username = _clean_username(settings.admin_username) or "admin"
existing = session.scalar(select(User).where(User.username == username))
if existing is not None:
if existing.role != UserRole.ADMIN:
existing.role = UserRole.ADMIN
session.flush()
return
generated = not settings.admin_password
password = settings.admin_password or secrets.token_urlsafe(12)
wallet = settings.admin_wallet.strip() or "admin-root"
if session.scalar(select(User).where(User.wallet_address == wallet)) is not None:
wallet = f"{wallet}-{secrets.token_hex(3)}"
session.add(
User(
wallet_address=wallet,
username=username,
password_hash=hash_password(password),
role=UserRole.ADMIN,
created_at=utcnow(),
)
)
session.flush()
if generated:
logger.warning("已创建管理员 %s,初始密码:%s", username, password)
def _clean_username(username: str | None) -> str | None:
if username is None:
return None
cleaned = username.strip().lower()
return cleaned or None
+90
View File
@@ -0,0 +1,90 @@
"""控制台要看的实时快照。今日积分按产出公式预览,不写入流水。"""
from __future__ import annotations
from decimal import Decimal
from sqlalchemy import func, select
from sqlalchemy.orm import Session, selectinload
from app.clock import utcnow
from app.models import ComputeNode, NodeStatus, NodeYield, User, WithdrawalLog, WithdrawalStatus, YieldStatus
from app.money import money, to_decimal
from app.services.withdrawals import available_balance
from app.services.yield_engine import compute_node_yield
def settled_points(session: Session, user_id: int | None = None) -> Decimal:
query = select(NodeYield.points_delta).where(NodeYield.status == YieldStatus.SETTLED)
if user_id is not None:
query = query.where(NodeYield.user_id == user_id)
rows = session.scalars(query).all()
return money(sum((to_decimal(item) for item in rows), Decimal("0")))
def pending_withdrawal_amount(session: Session) -> Decimal:
rows = session.scalars(
select(WithdrawalLog.amount).where(WithdrawalLog.status == WithdrawalStatus.PENDING)
).all()
return money(sum((to_decimal(item) for item in rows), Decimal("0")))
def authorized_amount(session: Session) -> Decimal:
rows = session.scalars(select(ComputeNode.purchase_amount)).all()
return money(sum((to_decimal(item) for item in rows), Decimal("0")))
def node_counts(session: Session) -> tuple[int, int]:
nodes = session.scalar(select(func.count()).select_from(ComputeNode)) or 0
online = session.scalar(
select(func.count()).select_from(ComputeNode).where(ComputeNode.status == NodeStatus.ONLINE)
) or 0
return nodes, online
def live_nodes(session: Session, user_id: int | None = None) -> list[dict]:
query = select(ComputeNode).options(selectinload(ComputeNode.user)).order_by(ComputeNode.id.desc())
if user_id is not None:
query = query.where(ComputeNode.user_id == user_id)
today = utcnow().date()
snapshots = []
for node in session.scalars(query).all():
computed = compute_node_yield(session, node, today)
snapshots.append(
{
"id": node.id,
"sn": node.sn,
"user_id": node.user_id,
"wallet_address": node.user.wallet_address,
"purchase_amount": node.purchase_amount,
"license_months": node.license_months,
"licensed_at": node.licensed_at,
"license_expires_at": node.license_expires_at,
"status": node.status,
"last_heartbeat_at": node.last_heartbeat_at,
"callback_url": node.callback_url,
"online_rate": computed.online_rate,
"today_points": computed.points_delta if computed.eligible else Decimal("0"),
}
)
return snapshots
def user_rows(session: Session) -> list[dict]:
counts = dict(
session.execute(select(ComputeNode.user_id, func.count()).group_by(ComputeNode.user_id)).all()
)
rows = []
for user in session.scalars(select(User).order_by(User.id.desc())).all():
rows.append(
{
"id": user.id,
"wallet_address": user.wallet_address,
"username": user.username,
"role": user.role,
"created_at": user.created_at,
"node_count": int(counts.get(user.id, 0)),
"available_points": available_balance(session, user.id),
}
)
return rows
+18 -4
View File
@@ -12,22 +12,36 @@ from sqlalchemy.orm import Session
from app.clock import add_months, utcnow
from app.config import settings
from app.errors import AppError
from app.models import ComputeNode, NodeStatus, User
from app.models import ComputeNode, NodeStatus, User, UserRole
from app.money import money
from app.services.common import get_user_by_wallet
def register_user(session: Session, wallet_address: str) -> User:
def register_user(
session: Session,
wallet_address: str,
username: str | None = None,
password_hash: str | None = None,
role: UserRole = UserRole.USER,
) -> User:
wallet = wallet_address.strip()
if session.scalar(select(User).where(User.wallet_address == wallet)) is not None:
raise AppError("钱包地址已注册", 409)
user = User(wallet_address=wallet, created_at=utcnow())
if username and session.scalar(select(User).where(User.username == username)) is not None:
raise AppError("用户名已占用", 409)
user = User(
wallet_address=wallet,
username=username,
password_hash=password_hash,
role=role,
created_at=utcnow(),
)
session.add(user)
try:
session.flush()
except IntegrityError as exc:
session.rollback()
raise AppError("钱包地址已注册", 409) from exc
raise AppError("钱包地址或用户名已注册", 409) from exc
return user