@seasonkoh/webaz 0.1.24 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/layer0-foundation/L0-1-database/db-backends/pg-backend.js +51 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/sql-dialect-datetime.js +437 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/sql-placeholders.js +98 -0
- package/dist/layer0-foundation/L0-1-database/db.js +65 -0
- package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +13 -11
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +1 -1
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +13 -11
- package/dist/layer1-agent/L1-1-mcp-server/server.js +288 -208
- package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +14 -12
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +8 -5
- package/dist/layer2-business/L2-7-snf/snf-engine.js +16 -14
- package/dist/layer2-business/L2-8-feedback/build-feedback-engine.js +18 -10
- package/dist/layer2-business/L2-9-contribution/build-reputation-engine.js +37 -23
- package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +182 -0
- package/dist/layer2-business/L2-9-contribution/build-task-participation.js +47 -0
- package/dist/layer2-business/L2-9-contribution/build-task-read.js +222 -0
- package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +11 -3
- package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +16 -0
- package/dist/layer2-business/L2-9-contribution/contribution-display-envelope.js +40 -0
- package/dist/layer2-business/L2-9-contribution/contribution-score-contract.js +36 -0
- package/dist/layer2-business/L2-9-contribution/contribution-score-evidence.js +61 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/canonical.js +60 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/github-credential.schema.js +140 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/github-fetch-adapter.js +437 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/self-consistency.js +38 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/verifier.js +231 -0
- package/dist/layer2-business/L2-9-contribution/github-credential-ingestion-engine.js +145 -0
- package/dist/layer2-business/L2-9-contribution/github-credential-store.js +115 -0
- package/dist/layer2-business/L2-9-contribution/identity-binding-engine.js +134 -0
- package/dist/layer2-business/L2-9-contribution/identity-binding-store.js +101 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-engine.js +126 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-store.js +30 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-discovery.js +55 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-engine.js +109 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-fact-precondition.js +22 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-proof-verifier.js +97 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-read.js +59 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-ai-store.js +99 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +191 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +129 -0
- package/dist/layer2-business/L2-notes/note-photo-storage.js +4 -2
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +17 -15
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +11 -8
- package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +9 -8
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +11 -8
- package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +22 -16
- package/dist/pwa/acp-feed.js +13 -1
- package/dist/pwa/admin-bearer-auth.js +21 -0
- package/dist/pwa/contract-fingerprint.js +2 -0
- package/dist/pwa/email-delivery.js +127 -0
- package/dist/pwa/endpoint-actions.js +5 -1
- package/dist/pwa/goal-index.js +8 -8
- package/dist/pwa/human-presence.js +62 -0
- package/dist/pwa/public/app.js +1485 -283
- package/dist/pwa/public/i18n.js +297 -59
- package/dist/pwa/public/index.html +1 -0
- package/dist/pwa/public/openapi.json +5 -5
- package/dist/pwa/public/whitepaper/en/index.html +153 -0
- package/dist/pwa/public/whitepaper/zh-CN/index.html +153 -0
- package/dist/pwa/rate-limit.js +22 -0
- package/dist/pwa/routes/account-deletion.js +15 -13
- package/dist/pwa/routes/addresses.js +10 -9
- package/dist/pwa/routes/admin-admins.js +13 -14
- package/dist/pwa/routes/admin-analytics.js +109 -69
- package/dist/pwa/routes/admin-atomic.js +10 -4
- package/dist/pwa/routes/admin-catalog.js +13 -11
- package/dist/pwa/routes/admin-editor-picks.js +15 -10
- package/dist/pwa/routes/admin-events.js +5 -3
- package/dist/pwa/routes/admin-health.js +2 -1
- package/dist/pwa/routes/admin-moderation.js +50 -29
- package/dist/pwa/routes/admin-ops.js +35 -23
- package/dist/pwa/routes/admin-protocol-params.js +16 -19
- package/dist/pwa/routes/admin-reports.js +23 -21
- package/dist/pwa/routes/admin-tokenomics.js +26 -25
- package/dist/pwa/routes/admin-users-lifecycle.js +37 -40
- package/dist/pwa/routes/admin-users-query.js +65 -53
- package/dist/pwa/routes/admin-verifier-flow.js +82 -41
- package/dist/pwa/routes/admin-verifier-whitelist.js +55 -27
- package/dist/pwa/routes/admin-wallet-ops.js +32 -7
- package/dist/pwa/routes/agent-buy.js +46 -22
- package/dist/pwa/routes/agent-governance.js +52 -56
- package/dist/pwa/routes/ai.js +7 -5
- package/dist/pwa/routes/analytics.js +43 -41
- package/dist/pwa/routes/anchors.js +19 -20
- package/dist/pwa/routes/announcements.js +13 -13
- package/dist/pwa/routes/arbitrator.js +97 -31
- package/dist/pwa/routes/auction.js +157 -116
- package/dist/pwa/routes/auth-login.js +6 -4
- package/dist/pwa/routes/auth-read.js +21 -10
- package/dist/pwa/routes/auth-register.js +111 -26
- package/dist/pwa/routes/auth-sessions.js +12 -11
- package/dist/pwa/routes/blocklist.js +16 -15
- package/dist/pwa/routes/build-feedback.js +10 -9
- package/dist/pwa/routes/build-reputation.js +6 -2
- package/dist/pwa/routes/build-tasks.js +45 -13
- package/dist/pwa/routes/buyer-feeds.js +27 -25
- package/dist/pwa/routes/cart.js +16 -15
- package/dist/pwa/routes/charity.js +212 -150
- package/dist/pwa/routes/chat.js +42 -43
- package/dist/pwa/routes/checkin-tasks.js +10 -9
- package/dist/pwa/routes/checkout-helpers.js +12 -10
- package/dist/pwa/routes/claim-initiators.js +34 -14
- package/dist/pwa/routes/claim-verify.js +86 -53
- package/dist/pwa/routes/claim-voting.js +43 -18
- package/dist/pwa/routes/contribution-identity.js +164 -0
- package/dist/pwa/routes/contribution-score.js +19 -0
- package/dist/pwa/routes/coupons.js +19 -16
- package/dist/pwa/routes/dashboards.js +18 -16
- package/dist/pwa/routes/dispute-cases.js +25 -24
- package/dist/pwa/routes/disputes-read.js +45 -51
- package/dist/pwa/routes/disputes-write.js +124 -61
- package/dist/pwa/routes/evidence.js +9 -9
- package/dist/pwa/routes/external-anchors.js +13 -12
- package/dist/pwa/routes/feedback.js +29 -33
- package/dist/pwa/routes/flash-sales.js +18 -16
- package/dist/pwa/routes/follows.js +25 -24
- package/dist/pwa/routes/governance-auto-deactivate.js +21 -9
- package/dist/pwa/routes/governance-onboarding.js +70 -59
- package/dist/pwa/routes/group-buys.js +22 -22
- package/dist/pwa/routes/growth.js +34 -31
- package/dist/pwa/routes/import-product.js +12 -10
- package/dist/pwa/routes/kyc.js +9 -8
- package/dist/pwa/routes/leaderboard.js +20 -18
- package/dist/pwa/routes/listings.js +23 -22
- package/dist/pwa/routes/logistics.js +10 -8
- package/dist/pwa/routes/manifests.js +27 -27
- package/dist/pwa/routes/me-data.js +23 -21
- package/dist/pwa/routes/notifications.js +7 -6
- package/dist/pwa/routes/offers.js +30 -12
- package/dist/pwa/routes/orders-action.js +51 -29
- package/dist/pwa/routes/orders-create.js +75 -20
- package/dist/pwa/routes/orders-read.js +21 -20
- package/dist/pwa/routes/p2p-products.js +30 -18
- package/dist/pwa/routes/payments-governance.js +61 -56
- package/dist/pwa/routes/peers.js +9 -8
- package/dist/pwa/routes/pin-receipts.js +13 -13
- package/dist/pwa/routes/products-aliases.js +12 -10
- package/dist/pwa/routes/products-claims.js +36 -17
- package/dist/pwa/routes/products-create.js +53 -38
- package/dist/pwa/routes/products-crud.js +17 -16
- package/dist/pwa/routes/products-links.js +49 -26
- package/dist/pwa/routes/products-list.js +6 -4
- package/dist/pwa/routes/products-meta.js +40 -39
- package/dist/pwa/routes/products-update.js +19 -5
- package/dist/pwa/routes/profile-credentials.js +20 -19
- package/dist/pwa/routes/profile-identity.js +14 -13
- package/dist/pwa/routes/profile-location.js +7 -6
- package/dist/pwa/routes/profile-placement.js +20 -19
- package/dist/pwa/routes/profile-prefs.js +11 -11
- package/dist/pwa/routes/promoter.js +58 -66
- package/dist/pwa/routes/public-build-tasks.js +19 -0
- package/dist/pwa/routes/public-utils.js +108 -46
- package/dist/pwa/routes/push.js +16 -15
- package/dist/pwa/routes/ratings.js +92 -32
- package/dist/pwa/routes/recover-key.js +66 -26
- package/dist/pwa/routes/referral.js +37 -52
- package/dist/pwa/routes/reputation.js +3 -2
- package/dist/pwa/routes/returns.js +76 -73
- package/dist/pwa/routes/reviews.js +41 -18
- package/dist/pwa/routes/rewards-apply.js +16 -15
- package/dist/pwa/routes/rewards-auto-downgrade.js +9 -7
- package/dist/pwa/routes/rewards-escrow-expire.js +7 -5
- package/dist/pwa/routes/rfqs.js +163 -85
- package/dist/pwa/routes/search.js +16 -14
- package/dist/pwa/routes/secondhand.js +25 -22
- package/dist/pwa/routes/seller-quota.js +24 -26
- package/dist/pwa/routes/share-redirects.js +60 -55
- package/dist/pwa/routes/shareables-interactions.js +34 -35
- package/dist/pwa/routes/shareables.js +55 -51
- package/dist/pwa/routes/shop-referral.js +58 -0
- package/dist/pwa/routes/shops.js +25 -20
- package/dist/pwa/routes/signaling.js +10 -9
- package/dist/pwa/routes/skill-market.js +16 -16
- package/dist/pwa/routes/skills.js +15 -14
- package/dist/pwa/routes/snf.js +14 -13
- package/dist/pwa/routes/tags.js +10 -9
- package/dist/pwa/routes/task-proposals.js +121 -0
- package/dist/pwa/routes/trial.js +72 -52
- package/dist/pwa/routes/trusted-kpi.js +20 -18
- package/dist/pwa/routes/url-claim.js +67 -28
- package/dist/pwa/routes/users-public.js +62 -70
- package/dist/pwa/routes/variants.js +12 -13
- package/dist/pwa/routes/verifier-user.js +61 -21
- package/dist/pwa/routes/verify-tasks.js +49 -25
- package/dist/pwa/routes/waitlist.js +16 -15
- package/dist/pwa/routes/wallet-read.js +75 -37
- package/dist/pwa/routes/wallet-write.js +12 -9
- package/dist/pwa/routes/webauthn.js +25 -26
- package/dist/pwa/routes/webhooks.js +26 -26
- package/dist/pwa/routes/welcome.js +45 -50
- package/dist/pwa/routes/wishlist-qa.js +29 -32
- package/dist/pwa/server.js +304 -90
- package/dist/version.js +1 -1
- package/package.json +76 -3
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// RFC-014 PR6 — 拍卖 stake 锁定/释放走整数 base-units + 绝对值落库。
|
|
2
2
|
import { toUnits } from '../../money.js';
|
|
3
3
|
import { applyWalletDelta } from '../../ledger.js';
|
|
4
|
+
// RFC-016 Phase 1 — 纯校验读/公开读/读回 → async seam;db.transaction 内 stake 写序列 + cron 保持同步。
|
|
5
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js';
|
|
4
6
|
// ─── 拍卖常量(域内)──────────────────────────────────────────
|
|
5
7
|
const AUC_MAX_WINDOW_MIN = 14 * 24 * 60; // 14 天上限
|
|
6
8
|
const AUC_MIN_WINDOW_MIN = 5;
|
|
@@ -54,9 +56,9 @@ export function fireDueAuctionReminders(db, generateId) {
|
|
|
54
56
|
return { fired };
|
|
55
57
|
}
|
|
56
58
|
export function registerAuctionRoutes(app, deps) {
|
|
57
|
-
const { db, auth, generateId, RFQ_MAX_QTY, RFQ_MAX_PRICE, LISTING_CATEGORIES, isListingCategoryKey, requireProtocolAdmin } = deps;
|
|
59
|
+
const { db, auth, generateId, RFQ_MAX_QTY, RFQ_MAX_PRICE, LISTING_CATEGORIES, isListingCategoryKey, requireProtocolAdmin, logAdminAction } = deps;
|
|
58
60
|
// 卖家发起拍卖
|
|
59
|
-
app.post('/api/auctions', (req, res) => {
|
|
61
|
+
app.post('/api/auctions', async (req, res) => {
|
|
60
62
|
const user = auth(req, res);
|
|
61
63
|
if (!user)
|
|
62
64
|
return;
|
|
@@ -90,12 +92,12 @@ export function registerAuctionRoutes(app, deps) {
|
|
|
90
92
|
const windowMin = Math.max(AUC_MIN_WINDOW_MIN, Math.min(AUC_MAX_WINDOW_MIN, Math.floor(Number(body.window_min || AUC_DEFAULT_WINDOW_MIN))));
|
|
91
93
|
const sniperExtend = Math.max(0, Math.min(60, Math.floor(Number(body.sniper_extend_min ?? AUC_DEFAULT_SNIPER_MIN))));
|
|
92
94
|
// 频率限制
|
|
93
|
-
const today =
|
|
95
|
+
const today = (await dbOne("SELECT COUNT(1) as n FROM auctions WHERE seller_id = ? AND created_at > datetime('now','-1 day')", [user.id])).n;
|
|
94
96
|
if (today >= AUC_DAILY_CAP_PER_SELLER)
|
|
95
97
|
return void res.json({ error: `今日已达上限 ${AUC_DAILY_CAP_PER_SELLER} 场拍卖` });
|
|
96
98
|
// 卖家担保金
|
|
97
99
|
const sellerStake = aucSellerStake(startingPrice);
|
|
98
|
-
const wallet =
|
|
100
|
+
const wallet = await dbOne('SELECT balance FROM wallets WHERE user_id = ?', [user.id]);
|
|
99
101
|
if (!wallet || Number(wallet.balance) < sellerStake) {
|
|
100
102
|
return void res.json({ error: `余额不足,卖家担保金 ${sellerStake} WAZ(5% × 起拍价)` });
|
|
101
103
|
}
|
|
@@ -103,7 +105,7 @@ export function registerAuctionRoutes(app, deps) {
|
|
|
103
105
|
let productId = null;
|
|
104
106
|
if (body.product_id) {
|
|
105
107
|
productId = String(body.product_id);
|
|
106
|
-
const p =
|
|
108
|
+
const p = await dbOne("SELECT seller_id, stock, status FROM products WHERE id = ?", [productId]);
|
|
107
109
|
if (!p)
|
|
108
110
|
return void res.json({ error: '关联商品不存在' });
|
|
109
111
|
if (p.seller_id !== user.id)
|
|
@@ -114,19 +116,43 @@ export function registerAuctionRoutes(app, deps) {
|
|
|
114
116
|
return void res.json({ error: `库存不足(${p.stock} < ${qty})` });
|
|
115
117
|
}
|
|
116
118
|
const id = generateId('auc');
|
|
117
|
-
|
|
118
|
-
db.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
119
|
+
try {
|
|
120
|
+
db.transaction(() => {
|
|
121
|
+
// 余额守恒 guard(Codex PR#228 P1):tx 内重读余额并在任何写之前判定。
|
|
122
|
+
// 上面 `await dbOne` 预检与同步 stake tx 之间有 yield,并发请求可都通过陈旧余额预检后
|
|
123
|
+
// 双双锁押 → 超额。同步 tx 体内无 yield,这次重读反映已提交的扣减,失败即抛回滚。
|
|
124
|
+
const wTx = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(user.id);
|
|
125
|
+
if (!wTx || Number(wTx.balance) < sellerStake)
|
|
126
|
+
throw new Error('AUC_INSUFFICIENT');
|
|
127
|
+
db.prepare(`
|
|
128
|
+
INSERT INTO auctions (id, seller_id, listing_id, product_id, title, spec_json, qty, category,
|
|
129
|
+
starting_price, current_price, min_increment, reserve_price, deadline_at, sniper_extend_min,
|
|
130
|
+
seller_stake_locked, notes)
|
|
131
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,datetime('now', '+' || ? || ' minutes'),?,?,?)
|
|
132
|
+
`).run(id, user.id, body.listing_id ? String(body.listing_id) : null, productId, title, body.spec_json ? JSON.stringify(body.spec_json) : null, qty, cat, startingPrice, startingPrice, minIncrement, reservePrice, windowMin, sniperExtend, sellerStake, body.notes ? String(body.notes).slice(0, 500) : null);
|
|
133
|
+
applyWalletDelta(db, user.id, { balance: -toUnits(sellerStake), staked: toUnits(sellerStake) });
|
|
134
|
+
// 商品状态机 CAS(Codex follow-up #239):active→auction_pending 必须带 status 守卫。
|
|
135
|
+
// 上面 `await dbOne` 校验 status='active' 与同步 tx 间有 yield,并发 create 可都通过陈旧
|
|
136
|
+
// 'active' 预检 → 同一商品被双双挂进两个拍卖。CAS changes=0 即已被他人移走,抛回滚。
|
|
137
|
+
if (productId) {
|
|
138
|
+
const flipped = db.prepare("UPDATE products SET status = 'auction_pending', updated_at = datetime('now') WHERE id = ? AND status = 'active'").run(productId);
|
|
139
|
+
if (flipped.changes === 0)
|
|
140
|
+
throw new Error('AUC_PRODUCT_CONFLICT');
|
|
141
|
+
}
|
|
142
|
+
})();
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
const msg = e.message;
|
|
146
|
+
if (msg === 'AUC_INSUFFICIENT') {
|
|
147
|
+
return void res.json({ error: `余额不足,卖家担保金 ${sellerStake} WAZ(5% × 起拍价)` });
|
|
148
|
+
}
|
|
149
|
+
if (msg === 'AUC_PRODUCT_CONFLICT') {
|
|
150
|
+
return void res.json({ error: '关联商品状态已变更(可能已被上架到其它拍卖或下架),请刷新后重试' });
|
|
151
|
+
}
|
|
152
|
+
throw e;
|
|
153
|
+
}
|
|
154
|
+
// QA 轮 12 P1:返回完整 echo 字段 + ISO deadline_at 与 detail 一致(tx 后纯读回)
|
|
155
|
+
const created = (await dbOne('SELECT deadline_at, status FROM auctions WHERE id = ?', [id]));
|
|
130
156
|
res.json({
|
|
131
157
|
id,
|
|
132
158
|
seller_stake: sellerStake,
|
|
@@ -145,7 +171,7 @@ export function registerAuctionRoutes(app, deps) {
|
|
|
145
171
|
});
|
|
146
172
|
});
|
|
147
173
|
// 看板:浏览公开拍卖(匿名可访问)
|
|
148
|
-
app.get('/api/auctions', (req, res) => {
|
|
174
|
+
app.get('/api/auctions', async (req, res) => {
|
|
149
175
|
const where = ["a.status = 'open'", "a.deadline_at > datetime('now')"];
|
|
150
176
|
const args = [];
|
|
151
177
|
if (req.query.category) {
|
|
@@ -159,7 +185,7 @@ export function registerAuctionRoutes(app, deps) {
|
|
|
159
185
|
args.push(like, like);
|
|
160
186
|
}
|
|
161
187
|
const limit = Math.min(100, Math.max(1, Number(req.query.limit) || 30));
|
|
162
|
-
const rows =
|
|
188
|
+
const rows = await dbAll(`
|
|
163
189
|
SELECT a.id, a.seller_id, a.title, a.qty, a.category, a.starting_price, a.current_price,
|
|
164
190
|
a.min_increment, a.reserve_price, a.deadline_at, a.bid_count, a.sniper_extend_min, a.created_at,
|
|
165
191
|
u.handle as seller_handle, u.region as seller_region
|
|
@@ -168,42 +194,42 @@ export function registerAuctionRoutes(app, deps) {
|
|
|
168
194
|
WHERE ${where.join(' AND ')}
|
|
169
195
|
ORDER BY a.deadline_at ASC
|
|
170
196
|
LIMIT ?
|
|
171
|
-
|
|
197
|
+
`, [...args, limit]);
|
|
172
198
|
res.json({ items: rows, categories: LISTING_CATEGORIES });
|
|
173
199
|
});
|
|
174
200
|
// 我的:买家=我出过价的,卖家=我发起的
|
|
175
|
-
app.get('/api/auctions/mine', (req, res) => {
|
|
201
|
+
app.get('/api/auctions/mine', async (req, res) => {
|
|
176
202
|
const user = auth(req, res);
|
|
177
203
|
if (!user)
|
|
178
204
|
return;
|
|
179
|
-
const seller =
|
|
180
|
-
const buyer =
|
|
205
|
+
const seller = await dbAll(`SELECT * FROM auctions WHERE seller_id = ? ORDER BY created_at DESC LIMIT 50`, [user.id]);
|
|
206
|
+
const buyer = await dbAll(`
|
|
181
207
|
SELECT DISTINCT a.*, (SELECT b.price FROM auction_bids b WHERE b.auction_id = a.id AND b.buyer_id = ? ORDER BY b.submitted_at DESC LIMIT 1) as my_last_bid,
|
|
182
208
|
(SELECT b.status FROM auction_bids b WHERE b.auction_id = a.id AND b.buyer_id = ? ORDER BY b.submitted_at DESC LIMIT 1) as my_last_status
|
|
183
209
|
FROM auctions a
|
|
184
210
|
JOIN auction_bids b ON b.auction_id = a.id
|
|
185
211
|
WHERE b.buyer_id = ? ORDER BY a.created_at DESC LIMIT 50
|
|
186
|
-
|
|
212
|
+
`, [user.id, user.id, user.id]);
|
|
187
213
|
res.json({ as_seller: seller, as_buyer: buyer });
|
|
188
214
|
});
|
|
189
215
|
// 详情:含 bid 历史(buyer 身份脱敏;卖家+出价人本人 可见全名)
|
|
190
|
-
app.get('/api/auctions/:id', (req, res) => {
|
|
216
|
+
app.get('/api/auctions/:id', async (req, res) => {
|
|
191
217
|
const user = auth(req, res);
|
|
192
218
|
if (!user)
|
|
193
219
|
return;
|
|
194
|
-
const auc =
|
|
220
|
+
const auc = await dbOne('SELECT * FROM auctions WHERE id = ?', [req.params.id]);
|
|
195
221
|
if (!auc)
|
|
196
222
|
return void res.status(404).json({ error: '拍卖不存在' });
|
|
197
223
|
const isSellerSelf = auc.seller_id === user.id;
|
|
198
224
|
const isSettled = auc.status !== 'open';
|
|
199
|
-
const bids =
|
|
225
|
+
const bids = await dbAll(`
|
|
200
226
|
SELECT b.id, b.buyer_id, b.price, b.stake_locked, b.status, b.submitted_at, b.resolved_at,
|
|
201
227
|
u.handle as buyer_handle
|
|
202
228
|
FROM auction_bids b
|
|
203
229
|
LEFT JOIN users u ON u.id = b.buyer_id
|
|
204
230
|
WHERE b.auction_id = ?
|
|
205
231
|
ORDER BY b.price DESC, b.submitted_at ASC
|
|
206
|
-
|
|
232
|
+
`, [req.params.id]);
|
|
207
233
|
// 脱敏:非 (卖家/拍卖结束/出价人本人) 时,buyer_id 用后 6 位 + handle 隐藏
|
|
208
234
|
const safeBids = bids.map(b => {
|
|
209
235
|
const isMine = b.buyer_id === user.id;
|
|
@@ -218,11 +244,11 @@ export function registerAuctionRoutes(app, deps) {
|
|
|
218
244
|
res.json({ auction: auc, bids: safeBids, is_seller: isSellerSelf });
|
|
219
245
|
});
|
|
220
246
|
// 拍卖「⏰ 提醒我」(#959)
|
|
221
|
-
app.post('/api/auctions/:id/remind', (req, res) => {
|
|
247
|
+
app.post('/api/auctions/:id/remind', async (req, res) => {
|
|
222
248
|
const user = auth(req, res);
|
|
223
249
|
if (!user)
|
|
224
250
|
return;
|
|
225
|
-
const aucRow =
|
|
251
|
+
const aucRow = await dbOne("SELECT id, deadline_at, status, seller_id FROM auctions WHERE id = ?", [req.params.id]);
|
|
226
252
|
if (!aucRow)
|
|
227
253
|
return void res.status(404).json({ error: '拍卖不存在' });
|
|
228
254
|
if (aucRow.seller_id === user.id)
|
|
@@ -246,28 +272,28 @@ export function registerAuctionRoutes(app, deps) {
|
|
|
246
272
|
tx();
|
|
247
273
|
res.json({ ok: true, subscribed: true, leads_minutes: AUCTION_REMINDER_LEADS });
|
|
248
274
|
});
|
|
249
|
-
app.delete('/api/auctions/:id/remind', (req, res) => {
|
|
275
|
+
app.delete('/api/auctions/:id/remind', async (req, res) => {
|
|
250
276
|
const user = auth(req, res);
|
|
251
277
|
if (!user)
|
|
252
278
|
return;
|
|
253
|
-
const r =
|
|
279
|
+
const r = await dbRun("DELETE FROM auction_reminders WHERE auction_id = ? AND user_id = ?", [req.params.id, user.id]);
|
|
254
280
|
res.json({ ok: true, deleted: r.changes });
|
|
255
281
|
});
|
|
256
|
-
app.get('/api/auctions/:id/remind', (req, res) => {
|
|
282
|
+
app.get('/api/auctions/:id/remind', async (req, res) => {
|
|
257
283
|
const user = auth(req, res);
|
|
258
284
|
if (!user)
|
|
259
285
|
return;
|
|
260
|
-
const rows =
|
|
286
|
+
const rows = await dbAll("SELECT lead_minutes, fire_at, sent_at FROM auction_reminders WHERE auction_id = ? AND user_id = ? ORDER BY lead_minutes DESC", [req.params.id, user.id]);
|
|
261
287
|
res.json({ subscribed: rows.length > 0, reminders: rows });
|
|
262
288
|
});
|
|
263
289
|
// 买家:出价
|
|
264
|
-
app.post('/api/auctions/:id/bids', (req, res) => {
|
|
290
|
+
app.post('/api/auctions/:id/bids', async (req, res) => {
|
|
265
291
|
const user = auth(req, res);
|
|
266
292
|
if (!user)
|
|
267
293
|
return;
|
|
268
294
|
if (user.role !== 'buyer')
|
|
269
295
|
return void res.json({ error: '仅买家可出价' });
|
|
270
|
-
const auc =
|
|
296
|
+
const auc = await dbOne('SELECT * FROM auctions WHERE id = ?', [req.params.id]);
|
|
271
297
|
if (!auc)
|
|
272
298
|
return void res.status(404).json({ error: '拍卖不存在' });
|
|
273
299
|
if (auc.status !== 'open')
|
|
@@ -298,10 +324,10 @@ export function registerAuctionRoutes(app, deps) {
|
|
|
298
324
|
}
|
|
299
325
|
const qty = Math.max(1, Math.floor(Number(auc.qty || 1)));
|
|
300
326
|
const stake = aucBuyerStake(price, qty);
|
|
301
|
-
const wallet =
|
|
327
|
+
const wallet = await dbOne('SELECT balance FROM wallets WHERE user_id = ?', [user.id]);
|
|
302
328
|
// QA 轮 12 P1:自我加价 affordability check 应含已锁旧 stake(会在 tx 内释放)
|
|
303
329
|
// 否则用户必须 ≥ 2× stake 余额才能加价,UX 卡。
|
|
304
|
-
const myExisting =
|
|
330
|
+
const myExisting = await dbOne("SELECT stake_locked FROM auction_bids WHERE auction_id = ? AND buyer_id = ? AND status = 'active'", [req.params.id, user.id]);
|
|
305
331
|
const myExistingStake = Number(myExisting?.stake_locked || 0);
|
|
306
332
|
const availableForBid = Number(wallet?.balance || 0) + myExistingStake;
|
|
307
333
|
if (!wallet || availableForBid < stake) {
|
|
@@ -313,104 +339,117 @@ export function registerAuctionRoutes(app, deps) {
|
|
|
313
339
|
let newDeadlineExt = null;
|
|
314
340
|
let sellerTopup = 0;
|
|
315
341
|
let closedErr = '';
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
if (fresh.status !== 'open') {
|
|
324
|
-
closedErr = `closed_${fresh.status}`;
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
if (fresh.deadline_at <= new Date().toISOString().replace('T', ' ').slice(0, 19)) {
|
|
328
|
-
closedErr = 'expired';
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
// 价格重判(中间可能有别人插队)
|
|
332
|
-
const curFresh = Number(fresh.current_price);
|
|
333
|
-
const isFirstFresh = Number(fresh.bid_count) === 0;
|
|
334
|
-
if (isFirstFresh) {
|
|
335
|
-
if (price < startingPrice) {
|
|
336
|
-
closedErr = `below_starting_${startingPrice}`;
|
|
342
|
+
try {
|
|
343
|
+
db.transaction(() => {
|
|
344
|
+
// P1 #4:TX 内重读 auction 状态 + deadline 防 TOCTOU
|
|
345
|
+
const fresh = db.prepare('SELECT status, deadline_at, current_price, bid_count, seller_stake_locked, max_extends, extends_used, sniper_extend_min FROM auctions WHERE id = ?').get(req.params.id);
|
|
346
|
+
if (!fresh) {
|
|
347
|
+
closedErr = 'not_found';
|
|
337
348
|
return;
|
|
338
349
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const minNeed = Math.round((curFresh + Number(auc.min_increment)) * 100) / 100;
|
|
342
|
-
if (price < minNeed) {
|
|
343
|
-
closedErr = `below_min_${minNeed}`;
|
|
350
|
+
if (fresh.status !== 'open') {
|
|
351
|
+
closedErr = `closed_${fresh.status}`;
|
|
344
352
|
return;
|
|
345
353
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if (myPrev) {
|
|
350
|
-
db.prepare("UPDATE auction_bids SET status = 'outbid', resolved_at = datetime('now') WHERE id = ?").run(myPrev.id);
|
|
351
|
-
if (myPrev.stake_locked > 0)
|
|
352
|
-
applyWalletDelta(db, user.id, { balance: toUnits(myPrev.stake_locked), staked: -toUnits(myPrev.stake_locked) });
|
|
353
|
-
}
|
|
354
|
-
// 释放别人的最高 active bid
|
|
355
|
-
const others = db.prepare("SELECT id, buyer_id, stake_locked FROM auction_bids WHERE auction_id = ? AND status = 'active' AND buyer_id != ?").all(req.params.id, user.id);
|
|
356
|
-
for (const o of others) {
|
|
357
|
-
db.prepare("UPDATE auction_bids SET status = 'outbid', resolved_at = datetime('now') WHERE id = ?").run(o.id);
|
|
358
|
-
if (o.stake_locked > 0)
|
|
359
|
-
applyWalletDelta(db, o.buyer_id, { balance: toUnits(o.stake_locked), staked: -toUnits(o.stake_locked) });
|
|
360
|
-
}
|
|
361
|
-
// 插入新 bid
|
|
362
|
-
db.prepare(`INSERT INTO auction_bids (id, auction_id, buyer_id, price, stake_locked) VALUES (?,?,?,?,?)`)
|
|
363
|
-
.run(id, req.params.id, user.id, price, stake);
|
|
364
|
-
applyWalletDelta(db, user.id, { balance: -toUnits(stake), staked: toUnits(stake) });
|
|
365
|
-
// P1 #9:卖家 stake 动态补足(5% × current_price,余额不足则尽量补)
|
|
366
|
-
const targetSellerStake = Math.max(1, Math.round(price * AUC_SELLER_STAKE_PCT * 100) / 100);
|
|
367
|
-
const curSellerStake = Number(fresh.seller_stake_locked) || 0;
|
|
368
|
-
if (targetSellerStake > curSellerStake) {
|
|
369
|
-
const delta = Math.round((targetSellerStake - curSellerStake) * 100) / 100;
|
|
370
|
-
const sWal = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(auc.seller_id);
|
|
371
|
-
const canTopup = sWal ? Math.min(delta, Number(sWal.balance)) : 0;
|
|
372
|
-
if (canTopup > 0) {
|
|
373
|
-
applyWalletDelta(db, auc.seller_id, { balance: -toUnits(canTopup), staked: toUnits(canTopup) });
|
|
374
|
-
db.prepare('UPDATE auctions SET seller_stake_locked = seller_stake_locked + ? WHERE id = ?').run(canTopup, req.params.id);
|
|
375
|
-
sellerTopup = canTopup;
|
|
354
|
+
if (fresh.deadline_at <= new Date().toISOString().replace('T', ' ').slice(0, 19)) {
|
|
355
|
+
closedErr = 'expired';
|
|
356
|
+
return;
|
|
376
357
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
358
|
+
// 价格重判(中间可能有别人插队)
|
|
359
|
+
const curFresh = Number(fresh.current_price);
|
|
360
|
+
const isFirstFresh = Number(fresh.bid_count) === 0;
|
|
361
|
+
if (isFirstFresh) {
|
|
362
|
+
if (price < startingPrice) {
|
|
363
|
+
closedErr = `below_starting_${startingPrice}`;
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
const minNeed = Math.round((curFresh + Number(auc.min_increment)) * 100) / 100;
|
|
369
|
+
if (price < minNeed) {
|
|
370
|
+
closedErr = `below_min_${minNeed}`;
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// 释放本人之前的 active bid
|
|
375
|
+
const myPrev = db.prepare("SELECT id, stake_locked FROM auction_bids WHERE auction_id = ? AND buyer_id = ? AND status = 'active'").get(req.params.id, user.id);
|
|
376
|
+
if (myPrev) {
|
|
377
|
+
db.prepare("UPDATE auction_bids SET status = 'outbid', resolved_at = datetime('now') WHERE id = ?").run(myPrev.id);
|
|
378
|
+
if (myPrev.stake_locked > 0)
|
|
379
|
+
applyWalletDelta(db, user.id, { balance: toUnits(myPrev.stake_locked), staked: -toUnits(myPrev.stake_locked) });
|
|
380
|
+
}
|
|
381
|
+
// 释放别人的最高 active bid
|
|
382
|
+
const others = db.prepare("SELECT id, buyer_id, stake_locked FROM auction_bids WHERE auction_id = ? AND status = 'active' AND buyer_id != ?").all(req.params.id, user.id);
|
|
383
|
+
for (const o of others) {
|
|
384
|
+
db.prepare("UPDATE auction_bids SET status = 'outbid', resolved_at = datetime('now') WHERE id = ?").run(o.id);
|
|
385
|
+
if (o.stake_locked > 0)
|
|
386
|
+
applyWalletDelta(db, o.buyer_id, { balance: toUnits(o.stake_locked), staked: -toUnits(o.stake_locked) });
|
|
387
|
+
}
|
|
388
|
+
// 余额守恒 guard(Codex PR#228 P1):tx 内、释放本人旧 stake 之后、锁新 stake 之前重读余额。
|
|
389
|
+
// 上面 `await dbOne` 预检与同步 tx 间的 yield 让并发请求都通过陈旧余额 → 双双锁押超额。
|
|
390
|
+
// 此时 balance 已含本人旧 stake 的释放(若有),等价于预检的 availableForBid;不足即抛回滚。
|
|
391
|
+
const wTx = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(user.id);
|
|
392
|
+
if (!wTx || Number(wTx.balance) < stake)
|
|
393
|
+
throw new Error('AUC_INSUFFICIENT');
|
|
394
|
+
// 插入新 bid
|
|
395
|
+
db.prepare(`INSERT INTO auction_bids (id, auction_id, buyer_id, price, stake_locked) VALUES (?,?,?,?,?)`)
|
|
396
|
+
.run(id, req.params.id, user.id, price, stake);
|
|
397
|
+
applyWalletDelta(db, user.id, { balance: -toUnits(stake), staked: toUnits(stake) });
|
|
398
|
+
// P1 #9:卖家 stake 动态补足(5% × current_price,余额不足则尽量补)
|
|
399
|
+
const targetSellerStake = Math.max(1, Math.round(price * AUC_SELLER_STAKE_PCT * 100) / 100);
|
|
400
|
+
const curSellerStake = Number(fresh.seller_stake_locked) || 0;
|
|
401
|
+
if (targetSellerStake > curSellerStake) {
|
|
402
|
+
const delta = Math.round((targetSellerStake - curSellerStake) * 100) / 100;
|
|
403
|
+
const sWal = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(auc.seller_id);
|
|
404
|
+
const canTopup = sWal ? Math.min(delta, Number(sWal.balance)) : 0;
|
|
405
|
+
if (canTopup > 0) {
|
|
406
|
+
applyWalletDelta(db, auc.seller_id, { balance: -toUnits(canTopup), staked: toUnits(canTopup) });
|
|
407
|
+
db.prepare('UPDATE auctions SET seller_stake_locked = seller_stake_locked + ? WHERE id = ?').run(canTopup, req.params.id);
|
|
408
|
+
sellerTopup = canTopup;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// P1 #5:反狙击延长(max_extends 上限保护)
|
|
412
|
+
const sniperMin = Number(fresh.sniper_extend_min || 0);
|
|
413
|
+
const deadlineMs = Date.parse(String(fresh.deadline_at).replace(' ', 'T') + 'Z');
|
|
414
|
+
const nowMs = Date.now();
|
|
415
|
+
const inSnipeWindow = sniperMin > 0 && deadlineMs - nowMs < sniperMin * 60_000;
|
|
416
|
+
const canExtend = Number(fresh.extends_used) < Number(fresh.max_extends || 10);
|
|
417
|
+
if (inSnipeWindow && canExtend) {
|
|
418
|
+
newDeadlineExt = sniperMin;
|
|
419
|
+
db.prepare(`UPDATE auctions SET current_price = ?, bid_count = bid_count + 1,
|
|
387
420
|
deadline_at = datetime(deadline_at, '+' || ? || ' minutes'),
|
|
388
421
|
extends_used = extends_used + 1,
|
|
389
422
|
updated_at = datetime('now') WHERE id = ?`).run(price, sniperMin, req.params.id);
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
db.prepare(`UPDATE auctions SET current_price = ?, bid_count = bid_count + 1, updated_at = datetime('now') WHERE id = ?`).run(price, req.params.id);
|
|
426
|
+
}
|
|
427
|
+
})();
|
|
428
|
+
}
|
|
429
|
+
catch (e) {
|
|
430
|
+
if (e.message === 'AUC_INSUFFICIENT') {
|
|
431
|
+
return void res.json({ error: `余额不足,出价押金 ${stake} WAZ(被超越后立即释放)` });
|
|
390
432
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}
|
|
394
|
-
})();
|
|
433
|
+
throw e;
|
|
434
|
+
}
|
|
395
435
|
if (closedErr) {
|
|
396
436
|
const ce = closedErr;
|
|
397
437
|
return void res.json({ error: ce === 'expired' ? '拍卖已到期' : ce === 'not_found' ? '拍卖不存在' : ce.startsWith('below_') ? '出价不足,请刷新页面查看当前最高价' : `拍卖已结束(${ce})` });
|
|
398
438
|
}
|
|
399
|
-
// 通知卖家 + 被超越的买家
|
|
439
|
+
// 通知卖家 + 被超越的买家(tx 后 fire-and-forget 单写 → seam)
|
|
400
440
|
try {
|
|
401
|
-
|
|
402
|
-
VALUES (?,?,'auction_new_bid',?,?,datetime('now'))`)
|
|
403
|
-
.run(generateId('ntf'), auc.seller_id, `🔨 新出价 ${price} WAZ`, `拍卖:${String(auc.title).slice(0, 30)}`);
|
|
441
|
+
await dbRun(`INSERT INTO notifications (id, user_id, type, title, body, created_at)
|
|
442
|
+
VALUES (?,?,'auction_new_bid',?,?,datetime('now'))`, [generateId('ntf'), auc.seller_id, `🔨 新出价 ${price} WAZ`, `拍卖:${String(auc.title).slice(0, 30)}`]);
|
|
404
443
|
}
|
|
405
444
|
catch { }
|
|
406
445
|
res.json({ id, stake_locked: stake, current_price: price, sniper_extended_min: newDeadlineExt, seller_topup: sellerTopup || undefined });
|
|
407
446
|
});
|
|
408
447
|
// 卖家:取消(仅未出价时)
|
|
409
|
-
app.delete('/api/auctions/:id', (req, res) => {
|
|
448
|
+
app.delete('/api/auctions/:id', async (req, res) => {
|
|
410
449
|
const user = auth(req, res);
|
|
411
450
|
if (!user)
|
|
412
451
|
return;
|
|
413
|
-
const auc =
|
|
452
|
+
const auc = await dbOne('SELECT * FROM auctions WHERE id = ?', [req.params.id]);
|
|
414
453
|
if (!auc)
|
|
415
454
|
return void res.status(404).json({ error: '拍卖不存在' });
|
|
416
455
|
if (auc.seller_id !== user.id)
|
|
@@ -434,6 +473,8 @@ export function registerAuctionRoutes(app, deps) {
|
|
|
434
473
|
const admin = requireProtocolAdmin(req, res);
|
|
435
474
|
if (!admin)
|
|
436
475
|
return;
|
|
437
|
-
|
|
476
|
+
const result = fireDueAuctionReminders(db, generateId);
|
|
477
|
+
logAdminAction(admin.id, 'auction_reminders_run', 'protocol', null, { result });
|
|
478
|
+
res.json(result);
|
|
438
479
|
});
|
|
439
480
|
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerAuthLoginRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
// db 已全量走 RFC-016 异步 seam(dbAll),不再直接用 deps.db
|
|
4
|
+
const { INTERNAL_AUDITOR_ID, isLocked, verifyPassword, recordFailure, resetFailures, recordSession } = deps;
|
|
5
|
+
app.post('/api/login', async (req, res) => {
|
|
4
6
|
const { name, password } = req.body;
|
|
5
7
|
if (!name?.trim() || !password)
|
|
6
8
|
return void res.json({ error: '请填写用户名 / 昵称和密码' });
|
|
7
9
|
const ref = name.trim().replace(/^@/, '').toLowerCase();
|
|
8
|
-
let matches =
|
|
10
|
+
let matches = await dbAll("SELECT * FROM users WHERE handle = ? AND id NOT IN ('sys_protocol', ?)", [ref, INTERNAL_AUDITOR_ID]);
|
|
9
11
|
if (matches.length === 0) {
|
|
10
|
-
matches =
|
|
12
|
+
matches = await dbAll("SELECT * FROM users WHERE name = ? AND id NOT IN ('sys_protocol', ?)", [name.trim(), INTERNAL_AUDITOR_ID]);
|
|
11
13
|
}
|
|
12
14
|
if (matches.length === 0)
|
|
13
15
|
return void res.json({ error: '账号或密码错误' });
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerAuthReadRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
// db 已全量走 RFC-016 异步 seam(dbOne),不再直接用 deps.db
|
|
4
|
+
const { auth, safeRoles, getRegionMaxLevels, userMlmGate, getUserLevel } = deps;
|
|
5
|
+
app.get('/api/me', async (req, res) => {
|
|
4
6
|
const user = auth(req, res);
|
|
5
7
|
if (!user)
|
|
6
8
|
return;
|
|
7
|
-
const wallet =
|
|
9
|
+
const wallet = await dbOne('SELECT * FROM wallets WHERE user_id = ?', [user.id]);
|
|
8
10
|
let roles = [];
|
|
9
11
|
try {
|
|
10
12
|
roles = JSON.parse(user.roles || JSON.stringify([user.role]));
|
|
@@ -14,17 +16,25 @@ export function registerAuthReadRoutes(app, deps) {
|
|
|
14
16
|
}
|
|
15
17
|
const region = user.region || 'global';
|
|
16
18
|
const maxLevels = getRegionMaxLevels(region);
|
|
17
|
-
const pvEnabled =
|
|
18
|
-
|
|
19
|
+
const pvEnabled = (await dbOne("SELECT pv_enabled FROM region_config WHERE region = ?", [region]))?.pv_enabled ?? 0;
|
|
20
|
+
// 恢复能力标志(供首页"无恢复方式"横幅 + 凭证清单徽章用)。password_hash 不外泄,只回布尔。
|
|
21
|
+
const passkeyCount = (await dbOne('SELECT COUNT(*) AS n FROM webauthn_credentials WHERE user_id = ?', [user.id]))?.n ?? 0;
|
|
22
|
+
res.json({
|
|
23
|
+
...user, api_key: undefined, password_hash: undefined,
|
|
24
|
+
roles, wallet: wallet || null, region_max_levels: maxLevels, region_pv_enabled: Number(pvEnabled) === 1 ? 1 : 0,
|
|
25
|
+
email_verified: !!user.email_verified,
|
|
26
|
+
has_password: !!user.password_hash,
|
|
27
|
+
has_passkey: Number(passkeyCount) > 0,
|
|
28
|
+
});
|
|
19
29
|
});
|
|
20
|
-
app.get('/api/profile', (req, res) => {
|
|
30
|
+
app.get('/api/profile', async (req, res) => {
|
|
21
31
|
const user = auth(req, res);
|
|
22
32
|
if (!user)
|
|
23
33
|
return;
|
|
24
|
-
const wallet =
|
|
34
|
+
const wallet = await dbOne('SELECT balance, staked, escrowed, earned FROM wallets WHERE user_id = ?', [user.id]);
|
|
25
35
|
const roles = safeRoles(user);
|
|
26
|
-
const pv =
|
|
27
|
-
const pendingScore =
|
|
36
|
+
const pv = await dbOne("SELECT total_left_pv, total_right_pv FROM users WHERE id = ?", [user.id]);
|
|
37
|
+
const pendingScore = (await dbOne("SELECT COALESCE(SUM(score),0) as s FROM binary_score_records WHERE user_id = ? AND settled_at IS NULL", [user.id])).s;
|
|
28
38
|
res.json({
|
|
29
39
|
id: user.id, name: user.name, role: user.role, roles, api_key: user.api_key, wallet: wallet || null,
|
|
30
40
|
permanent_code: user.permanent_code ?? null,
|
|
@@ -41,9 +51,10 @@ export function registerAuthReadRoutes(app, deps) {
|
|
|
41
51
|
phone: user.phone ?? null,
|
|
42
52
|
phone_verified: !!user.phone_verified,
|
|
43
53
|
has_password: !!user.password_hash,
|
|
54
|
+
has_passkey: ((await dbOne('SELECT COUNT(*) AS n FROM webauthn_credentials WHERE user_id = ?', [user.id]))?.n ?? 0) > 0,
|
|
44
55
|
region: user.region ?? 'global',
|
|
45
56
|
region_max_levels: getRegionMaxLevels(user.region || 'global'),
|
|
46
|
-
region_pv_enabled: (
|
|
57
|
+
region_pv_enabled: (((await dbOne("SELECT pv_enabled FROM region_config WHERE region = ?", [user.region || 'global']))?.pv_enabled ?? 0) === 1 ? 1 : 0),
|
|
47
58
|
...(() => { const g = userMlmGate(user.region || 'global'); return { mlm_ui_visible: g.mlmUiVisible, mlm_payout_levels: g.payoutLevels }; })(),
|
|
48
59
|
bio: user.bio ?? null,
|
|
49
60
|
search_anchor: user.search_anchor ?? null,
|