加上登录和角色权限,并用 Vue 控制台区分用户与管理员。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+172
-61
@@ -5,25 +5,34 @@ from decimal import Decimal
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import case, func, select
|
||||
from sqlalchemy.orm import Session, selectinload
|
||||
|
||||
from app.clock import utcnow
|
||||
from app.database import get_db
|
||||
from app.models import ComputeNode, ComputeTask, NodeStatus, NodeYield, User, WithdrawalLog, WithdrawalStatus, YieldStatus
|
||||
from app.money import money, to_decimal
|
||||
from app.deps import AdminUser, CurrentUser
|
||||
from app.errors import AppError
|
||||
from app.models import ComputeNode, ComputeTask, User, UserRole, WithdrawalLog, WithdrawalStatus
|
||||
from app.money import money
|
||||
from app.schemas import (
|
||||
AdminUserOut,
|
||||
BalanceOut,
|
||||
ContributionOut,
|
||||
DashboardOut,
|
||||
HeartbeatIn,
|
||||
HeartbeatOut,
|
||||
LoginIn,
|
||||
MeNodeCreate,
|
||||
MeWithdrawalCreate,
|
||||
NodeCreate,
|
||||
NodeOut,
|
||||
NodeView,
|
||||
RegisterIn,
|
||||
SettleOut,
|
||||
SummaryOut,
|
||||
TaskOut,
|
||||
TaskReportIn,
|
||||
TokenOut,
|
||||
UserCreate,
|
||||
UserOut,
|
||||
WithdrawalConfirm,
|
||||
@@ -33,8 +42,18 @@ from app.schemas import (
|
||||
WithdrawalView,
|
||||
YieldOut,
|
||||
)
|
||||
from app.security import create_access_token
|
||||
from app.services.auth import authenticate, register_account
|
||||
from app.services.common import get_node_by_sn, get_user_by_wallet
|
||||
from app.services.heartbeat import record_heartbeat
|
||||
from app.services.portal import (
|
||||
authorized_amount,
|
||||
live_nodes,
|
||||
node_counts,
|
||||
pending_withdrawal_amount,
|
||||
settled_points,
|
||||
user_rows,
|
||||
)
|
||||
from app.services.registry import register_node, register_user
|
||||
from app.services.tasks import list_pending_tasks, report_task
|
||||
from app.services.withdrawals import available_balance, confirm_withdrawal, reject_withdrawal, request_withdrawal
|
||||
@@ -44,35 +63,90 @@ router = APIRouter(prefix="/api/v1")
|
||||
Db = Annotated[Session, Depends(get_db)]
|
||||
|
||||
|
||||
@router.post("/auth/register", response_model=TokenOut)
|
||||
def register(body: RegisterIn, db: Db) -> TokenOut:
|
||||
user = register_account(db, body.wallet_address, body.username, body.password)
|
||||
return _token(user)
|
||||
|
||||
|
||||
@router.post("/auth/login", response_model=TokenOut)
|
||||
def login(body: LoginIn, db: Db) -> TokenOut:
|
||||
user = authenticate(db, body.wallet_address, body.username, body.password)
|
||||
return _token(user)
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserOut)
|
||||
def me(user: CurrentUser) -> User:
|
||||
return user
|
||||
|
||||
|
||||
@router.get("/me/dashboard", response_model=DashboardOut)
|
||||
def my_dashboard(user: CurrentUser, db: Db) -> DashboardOut:
|
||||
nodes = live_nodes(db, user.id)
|
||||
withdrawals = db.scalars(
|
||||
select(WithdrawalLog).where(WithdrawalLog.user_id == user.id).order_by(WithdrawalLog.id.desc()).limit(20)
|
||||
).all()
|
||||
today = money(sum((item["today_points"] for item in nodes), Decimal("0")))
|
||||
return DashboardOut(
|
||||
wallet_address=user.wallet_address,
|
||||
username=user.username,
|
||||
role=user.role,
|
||||
available_points=available_balance(db, user.id),
|
||||
settled_points=settled_points(db, user.id),
|
||||
today_points=today,
|
||||
nodes=[NodeView.model_validate(item) for item in nodes],
|
||||
withdrawals=[WithdrawalOut.model_validate(item) for item in withdrawals],
|
||||
)
|
||||
|
||||
|
||||
@router.post("/me/nodes", response_model=NodeOut)
|
||||
def create_my_node(body: MeNodeCreate, user: CurrentUser, db: Db) -> NodeOut:
|
||||
return register_node(db, user.wallet_address, body.sn, body.purchase_amount, body.callback_url)
|
||||
|
||||
|
||||
@router.post("/me/withdrawals", response_model=WithdrawalOut)
|
||||
def create_my_withdrawal(body: MeWithdrawalCreate, user: CurrentUser, db: Db) -> WithdrawalOut:
|
||||
row, remaining = request_withdrawal(db, user.wallet_address, body.amount, body.asset_symbol)
|
||||
return WithdrawalOut.model_validate(row).model_copy(update={"available_after": remaining})
|
||||
|
||||
|
||||
@router.get("/admin/summary", response_model=SummaryOut)
|
||||
def admin_summary(_: AdminUser, db: Db) -> SummaryOut:
|
||||
return _summary(db)
|
||||
|
||||
|
||||
@router.get("/admin/users", response_model=list[AdminUserOut])
|
||||
def admin_users(_: AdminUser, db: Db) -> list[dict]:
|
||||
return user_rows(db)
|
||||
|
||||
|
||||
@router.get("/admin/nodes", response_model=list[NodeView])
|
||||
def admin_nodes(_: AdminUser, db: Db) -> list[NodeView]:
|
||||
return [NodeView.model_validate(item) for item in live_nodes(db)]
|
||||
|
||||
|
||||
@router.get("/admin/withdrawals", response_model=list[WithdrawalView])
|
||||
def admin_withdrawals(_: AdminUser, db: Db) -> list[WithdrawalView]:
|
||||
return [_withdrawal_view(row) for row in _withdrawal_rows(db)]
|
||||
|
||||
|
||||
@router.post("/admin/withdrawals/{withdrawal_id}/confirm", response_model=WithdrawalOut)
|
||||
def admin_confirm(withdrawal_id: int, body: WithdrawalConfirm, _: AdminUser, db: Db) -> WithdrawalOut:
|
||||
return confirm_withdrawal(db, withdrawal_id, body.tx_hash)
|
||||
|
||||
|
||||
@router.post("/admin/withdrawals/{withdrawal_id}/reject", response_model=WithdrawalOut)
|
||||
def admin_reject(withdrawal_id: int, body: WithdrawalReject, _: AdminUser, db: Db) -> WithdrawalOut:
|
||||
return reject_withdrawal(db, withdrawal_id, body.note)
|
||||
|
||||
|
||||
@router.get("/summary", response_model=SummaryOut)
|
||||
def summary(db: Db) -> SummaryOut:
|
||||
users = db.scalar(select(func.count()).select_from(User)) or 0
|
||||
nodes = db.scalar(select(func.count()).select_from(ComputeNode)) or 0
|
||||
online = db.scalar(
|
||||
select(func.count()).select_from(ComputeNode).where(ComputeNode.status == NodeStatus.ONLINE)
|
||||
) or 0
|
||||
authorized = _sum(db.scalars(select(ComputeNode.purchase_amount)).all())
|
||||
settled = _sum(
|
||||
db.scalars(select(NodeYield.points_delta).where(NodeYield.status == YieldStatus.SETTLED)).all()
|
||||
)
|
||||
pending = _sum(
|
||||
db.scalars(
|
||||
select(WithdrawalLog.amount).where(WithdrawalLog.status == WithdrawalStatus.PENDING)
|
||||
).all()
|
||||
)
|
||||
return SummaryOut(
|
||||
users=users,
|
||||
nodes=nodes,
|
||||
online=online,
|
||||
offline=nodes - online,
|
||||
authorized_amount=authorized,
|
||||
settled_points=settled,
|
||||
pending_withdrawals=pending,
|
||||
)
|
||||
def summary(_: AdminUser, db: Db) -> SummaryOut:
|
||||
return _summary(db)
|
||||
|
||||
|
||||
@router.get("/users", response_model=list[UserOut])
|
||||
def list_users(db: Db) -> list[User]:
|
||||
def list_users(_: AdminUser, db: Db) -> list[User]:
|
||||
return list(db.scalars(select(User).order_by(User.id.desc())).all())
|
||||
|
||||
|
||||
@@ -82,21 +156,19 @@ def create_user(body: UserCreate, db: Db) -> UserOut:
|
||||
|
||||
|
||||
@router.get("/nodes", response_model=list[NodeView])
|
||||
def list_nodes(db: Db) -> list[NodeView]:
|
||||
rows = db.scalars(
|
||||
select(ComputeNode).options(selectinload(ComputeNode.user)).order_by(ComputeNode.id.desc())
|
||||
).all()
|
||||
return [_node_view(row) for row in rows]
|
||||
def list_nodes(_: AdminUser, db: Db) -> list[NodeView]:
|
||||
return [NodeView.model_validate(item) for item in live_nodes(db)]
|
||||
|
||||
|
||||
@router.post("/nodes", response_model=NodeOut)
|
||||
def create_node(body: NodeCreate, db: Db) -> NodeOut:
|
||||
def create_node(body: NodeCreate, user: CurrentUser, db: Db) -> NodeOut:
|
||||
_must_own_wallet(user, body.wallet_address)
|
||||
return register_node(db, body.wallet_address, body.sn, body.purchase_amount, body.callback_url)
|
||||
|
||||
|
||||
@router.get("/nodes/{sn}", response_model=NodeOut)
|
||||
def get_node(sn: str, db: Db) -> NodeOut:
|
||||
return get_node_by_sn(db, sn)
|
||||
def get_node(sn: str, user: CurrentUser, db: Db) -> NodeOut:
|
||||
return _owned_node(db, user, sn)
|
||||
|
||||
|
||||
@router.post("/node/heartbeat", response_model=HeartbeatOut)
|
||||
@@ -121,14 +193,14 @@ def report(body: TaskReportIn, db: Db) -> TaskOut:
|
||||
|
||||
|
||||
@router.get("/nodes/{sn}/contribution", response_model=ContributionOut)
|
||||
def contribution(sn: str, yield_date: date, db: Db) -> ContributionOut:
|
||||
node = get_node_by_sn(db, sn)
|
||||
def contribution(sn: str, yield_date: date, user: CurrentUser, db: Db) -> ContributionOut:
|
||||
node = _owned_node(db, user, sn)
|
||||
computed = compute_node_yield(db, node, yield_date)
|
||||
return ContributionOut(sn=node.sn, **computed.__dict__)
|
||||
|
||||
|
||||
@router.post("/yield/settle", response_model=SettleOut)
|
||||
def settle(db: Db, yield_date: date | None = None) -> SettleOut:
|
||||
def settle(_: AdminUser, db: Db, yield_date: date | None = None) -> SettleOut:
|
||||
target = yield_date or (utcnow().date() - timedelta(days=1))
|
||||
summary = settle_yield_date(db, target)
|
||||
return SettleOut(
|
||||
@@ -141,52 +213,91 @@ def settle(db: Db, yield_date: date | None = None) -> SettleOut:
|
||||
|
||||
|
||||
@router.get("/nodes/{sn}/yields", response_model=list[YieldOut])
|
||||
def yields(sn: str, db: Db) -> list[YieldOut]:
|
||||
def yields(sn: str, user: CurrentUser, db: Db) -> list[YieldOut]:
|
||||
_owned_node(db, user, sn)
|
||||
return list_node_yields(db, sn)
|
||||
|
||||
|
||||
@router.get("/withdrawals", response_model=list[WithdrawalView])
|
||||
def list_withdrawals(db: Db) -> list[WithdrawalView]:
|
||||
rows = db.scalars(
|
||||
select(WithdrawalLog).options(selectinload(WithdrawalLog.user)).order_by(WithdrawalLog.id.desc())
|
||||
).all()
|
||||
return [
|
||||
WithdrawalView(
|
||||
**WithdrawalOut.model_validate(row).model_dump(),
|
||||
wallet_address=row.user.wallet_address,
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
def list_withdrawals(_: AdminUser, db: Db) -> list[WithdrawalView]:
|
||||
return [_withdrawal_view(row) for row in _withdrawal_rows(db)]
|
||||
|
||||
|
||||
@router.get("/users/{wallet_address}/balance", response_model=BalanceOut)
|
||||
def balance(wallet_address: str, db: Db) -> BalanceOut:
|
||||
user = get_user_by_wallet(db, wallet_address)
|
||||
return BalanceOut(wallet_address=user.wallet_address, available_points=available_balance(db, user.id))
|
||||
def balance(wallet_address: str, user: CurrentUser, db: Db) -> BalanceOut:
|
||||
_must_own_wallet(user, wallet_address)
|
||||
owner = get_user_by_wallet(db, wallet_address)
|
||||
return BalanceOut(wallet_address=owner.wallet_address, available_points=available_balance(db, owner.id))
|
||||
|
||||
|
||||
@router.post("/withdrawals", response_model=WithdrawalOut)
|
||||
def create_withdrawal(body: WithdrawalCreate, db: Db) -> WithdrawalOut:
|
||||
def create_withdrawal(body: WithdrawalCreate, user: CurrentUser, db: Db) -> WithdrawalOut:
|
||||
_must_own_wallet(user, body.wallet_address)
|
||||
row, remaining = request_withdrawal(db, body.wallet_address, body.amount, body.asset_symbol)
|
||||
return WithdrawalOut.model_validate(row).model_copy(update={"available_after": remaining})
|
||||
|
||||
|
||||
@router.post("/withdrawals/{withdrawal_id}/confirm", response_model=WithdrawalOut)
|
||||
def confirm(withdrawal_id: int, body: WithdrawalConfirm, db: Db) -> WithdrawalOut:
|
||||
def confirm(withdrawal_id: int, body: WithdrawalConfirm, _: AdminUser, db: Db) -> WithdrawalOut:
|
||||
return confirm_withdrawal(db, withdrawal_id, body.tx_hash)
|
||||
|
||||
|
||||
@router.post("/withdrawals/{withdrawal_id}/reject", response_model=WithdrawalOut)
|
||||
def reject(withdrawal_id: int, body: WithdrawalReject, db: Db) -> WithdrawalOut:
|
||||
def reject(withdrawal_id: int, body: WithdrawalReject, _: AdminUser, db: Db) -> WithdrawalOut:
|
||||
return reject_withdrawal(db, withdrawal_id, body.note)
|
||||
|
||||
|
||||
def _node_view(node: ComputeNode) -> NodeView:
|
||||
return NodeView(**NodeOut.model_validate(node).model_dump(), wallet_address=node.user.wallet_address)
|
||||
def _summary(db: Session) -> SummaryOut:
|
||||
users = db.scalar(select(func.count()).select_from(User)) or 0
|
||||
nodes, online = node_counts(db)
|
||||
return SummaryOut(
|
||||
users=users,
|
||||
nodes=nodes,
|
||||
online=online,
|
||||
offline=nodes - online,
|
||||
authorized_amount=authorized_amount(db),
|
||||
settled_points=settled_points(db),
|
||||
pending_withdrawals=pending_withdrawal_amount(db),
|
||||
)
|
||||
|
||||
|
||||
def _sum(values: list[object]) -> Decimal:
|
||||
return money(sum((to_decimal(item) for item in values), Decimal("0")))
|
||||
def _token(user: User) -> TokenOut:
|
||||
return TokenOut(
|
||||
access_token=create_access_token(user),
|
||||
role=user.role,
|
||||
wallet_address=user.wallet_address,
|
||||
username=user.username,
|
||||
)
|
||||
|
||||
|
||||
def _must_own_wallet(user: User, wallet_address: str) -> None:
|
||||
if user.role != UserRole.ADMIN and user.wallet_address != wallet_address.strip():
|
||||
raise AppError("只能操作自己的钱包", 403)
|
||||
|
||||
|
||||
def _owned_node(db: Session, user: User, sn: str) -> ComputeNode:
|
||||
node = get_node_by_sn(db, sn)
|
||||
if user.role != UserRole.ADMIN and node.user_id != user.id:
|
||||
raise AppError("无权查看该节点", 403)
|
||||
return node
|
||||
|
||||
|
||||
def _withdrawal_rows(db: Session) -> list[WithdrawalLog]:
|
||||
pending_first = case((WithdrawalLog.status == WithdrawalStatus.PENDING, 0), else_=1)
|
||||
return list(
|
||||
db.scalars(
|
||||
select(WithdrawalLog)
|
||||
.options(selectinload(WithdrawalLog.user))
|
||||
.order_by(pending_first, WithdrawalLog.id.desc())
|
||||
).all()
|
||||
)
|
||||
|
||||
|
||||
def _withdrawal_view(row: WithdrawalLog) -> WithdrawalView:
|
||||
return WithdrawalView(
|
||||
**WithdrawalOut.model_validate(row).model_dump(),
|
||||
wallet_address=row.user.wallet_address,
|
||||
)
|
||||
|
||||
|
||||
def _task_out(task: ComputeTask) -> TaskOut:
|
||||
|
||||
Reference in New Issue
Block a user