加上登录和角色权限,并用 Vue 控制台区分用户与管理员。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user