@seasonkoh/webaz 0.1.26 → 0.1.28
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/LICENSE +2 -2
- package/NOTICE +24 -3
- package/README.md +74 -330
- package/README.zh-CN.md +419 -0
- package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +21 -0
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +8 -3
- package/dist/layer1-agent/L1-1-mcp-server/auth.js +13 -1
- package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +69 -0
- package/dist/layer1-agent/L1-1-mcp-server/server.js +270 -82
- package/dist/layer2-business/L2-9-contribution/admin-coordination-ingestion-engine.js +181 -0
- package/dist/layer2-business/L2-9-contribution/admin-coordination-resolver.js +114 -0
- package/dist/layer2-business/L2-9-contribution/admin-coordination-store.js +251 -0
- package/dist/layer2-business/L2-9-contribution/admin-operator-claim-workflow.js +390 -0
- package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +24 -0
- package/dist/layer2-business/L2-9-contribution/build-task-participation.js +6 -2
- package/dist/layer2-business/L2-9-contribution/build-task-quota.js +337 -0
- package/dist/layer2-business/L2-9-contribution/build-task-read.js +25 -2
- package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +57 -7
- package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +1 -1
- package/dist/layer2-business/L2-9-contribution/contribution-facts-read.js +66 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +187 -18
- package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +29 -4
- package/dist/ledger.js +1 -1
- package/dist/pwa/admin-audit.js +38 -0
- package/dist/pwa/anti-abuse-thresholds.js +135 -0
- package/dist/pwa/cf-origin-guard.js +33 -0
- package/dist/pwa/contract-fingerprint.js +1 -0
- package/dist/pwa/data/onboarding-cases.js +2 -2
- package/dist/pwa/data/onboarding-quiz.js +1 -1
- package/dist/pwa/economic-participation.js +2 -2
- package/dist/pwa/integration-contract.js +46 -4
- package/dist/pwa/internal/pv-settlement.js +12 -0
- package/dist/pwa/internal/wallet-signer.js +26 -0
- package/dist/pwa/public/app-account.js +977 -0
- package/dist/pwa/public/app-admin.js +608 -0
- package/dist/pwa/public/app-agents.js +63 -0
- package/dist/pwa/public/app-ai.js +2162 -0
- package/dist/pwa/public/app-contribution.js +836 -0
- package/dist/pwa/public/app-discover.js +1296 -0
- package/dist/pwa/public/app-listings.js +226 -0
- package/dist/pwa/public/app-profile.js +1692 -0
- package/dist/pwa/public/app-seller.js +199 -0
- package/dist/pwa/public/app-shop.js +1145 -0
- package/dist/pwa/public/app.js +15075 -23960
- package/dist/pwa/public/i18n.js +31 -28
- package/dist/pwa/public/index.html +11 -1
- package/dist/pwa/public/openapi.json +4851 -2776
- package/dist/pwa/pv-kill-switch.js +31 -0
- package/dist/pwa/routes/admin-admins.js +48 -1
- package/dist/pwa/routes/admin-analytics.js +1 -10
- package/dist/pwa/routes/admin-atomic.js +4 -17
- package/dist/pwa/routes/admin-operator-claims.js +280 -0
- package/dist/pwa/routes/admin-reports.js +4 -26
- package/dist/pwa/routes/admin-tokenomics.js +2 -76
- package/dist/pwa/routes/admin-users-lifecycle.js +1 -14
- package/dist/pwa/routes/admin-users-query.js +23 -1
- package/dist/pwa/routes/admin-wallet-ops.js +1 -1
- package/dist/pwa/routes/agent-grants.js +255 -0
- package/dist/pwa/routes/auth-read.js +1 -5
- package/dist/pwa/routes/auth-register.js +3 -13
- package/dist/pwa/routes/build-task-quota.js +113 -0
- package/dist/pwa/routes/claim-verify.js +15 -11
- package/dist/pwa/routes/contribution-facts.js +18 -0
- package/dist/pwa/routes/dispute-cases.js +5 -4
- package/dist/pwa/routes/growth.js +3 -3
- package/dist/pwa/routes/orders-action.js +27 -10
- package/dist/pwa/routes/orders-create.js +1 -1
- package/dist/pwa/routes/products-meta.js +19 -6
- package/dist/pwa/routes/profile-placement.js +1 -1
- package/dist/pwa/routes/promoter.js +10 -29
- package/dist/pwa/routes/public-build-tasks.js +5 -1
- package/dist/pwa/routes/public-utils.js +9 -12
- package/dist/pwa/routes/referral.js +5 -26
- package/dist/pwa/routes/rewards-apply.js +3 -2
- package/dist/pwa/routes/share-redirects.js +1 -1
- package/dist/pwa/routes/shareables-interactions.js +2 -1
- package/dist/pwa/routes/task-proposals.js +85 -9
- package/dist/pwa/routes/users-public.js +1 -4
- package/dist/pwa/routes/wallet-read.js +2 -14
- package/dist/pwa/routes/webauthn.js +7 -2
- package/dist/pwa/server-schema.js +9 -0
- package/dist/pwa/server.js +319 -2034
- package/dist/runtime/agent-grant-scopes.js +128 -0
- package/dist/runtime/agent-grant-verifier.js +67 -0
- package/dist/runtime/agent-pairing.js +60 -0
- package/dist/runtime/apply-webaz-runtime-schema.js +15 -0
- package/dist/runtime/webaz-schema-helpers.js +1848 -0
- package/dist/settlement-math.js +3 -3
- package/dist/version.js +6 -4
- package/package.json +43 -8
- package/dist/index.js +0 -182
- package/dist/pwa/public/docs/ECONOMIC-MODEL.md +0 -287
- package/dist/pwa/public/docs/INTEGRATOR.md +0 -67
- package/dist/pwa/public/docs/META-RULES-FULL.md +0 -543
- package/dist/test-dispute.js +0 -153
- package/dist/test-manifest.js +0 -61
- package/dist/test-mcp-tools.js +0 -135
- package/dist/test-reputation.js +0 -116
- package/dist/test-skill-market.js +0 -101
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Participation recording switch (default ON — only an explicit '0' disables). */
|
|
2
|
+
export const PARTICIPATION_RECORDING_KEY = 'participation_recording_active';
|
|
3
|
+
/** Matching-rewards operational on-switch + legal/governance clearance marker (both required, default OFF). */
|
|
4
|
+
export const MATCHING_REWARDS_ACTIVE_KEY = 'matching_rewards_active';
|
|
5
|
+
export const MATCHING_REWARDS_CLEARED_KEY = 'matching_rewards_activation_cleared';
|
|
6
|
+
const readParam = (db, k) => db.prepare('SELECT value FROM protocol_params WHERE key = ?').get(k)?.value;
|
|
7
|
+
/**
|
|
8
|
+
* DEFAULT ON. Neutral participation/contribution recording (PV ledger + PV aggregation) is allowed unless
|
|
9
|
+
* explicitly turned off. Absent param / query error → ON (recording is safe + neutral; not a payout).
|
|
10
|
+
*/
|
|
11
|
+
export function participationRecordingActive(db) {
|
|
12
|
+
try {
|
|
13
|
+
return readParam(db, PARTICIPATION_RECORDING_KEY) !== '0'; // default ON; only an explicit '0' disables
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return true; // recording is neutral/no-payout — failing to ON preserves the visible-participation principle
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* DEFAULT OFF. Matching-rewards settlement + payout + any reward distribution. True ONLY when the
|
|
21
|
+
* operational on-switch AND the legal/governance-clearance marker are BOTH '1'. FAIL-CLOSED on any
|
|
22
|
+
* missing param / query error → false (never pay on uncertainty; never break the order-settlement hot path).
|
|
23
|
+
*/
|
|
24
|
+
export function matchingRewardsActive(db) {
|
|
25
|
+
try {
|
|
26
|
+
return readParam(db, MATCHING_REWARDS_ACTIVE_KEY) === '1' && readParam(db, MATCHING_REWARDS_CLEARED_KEY) === '1';
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -140,11 +140,58 @@ export function registerAdminAdminsRoutes(app, deps) {
|
|
|
140
140
|
catch { }
|
|
141
141
|
rolesArr = rolesArr.filter(r => r !== 'admin');
|
|
142
142
|
const newRole = rolesArr[0] || 'buyer';
|
|
143
|
-
db.prepare(`UPDATE users SET role = ?, roles = ?, admin_type = NULL, admin_scope = NULL, updated_at = datetime('now') WHERE id = ?`)
|
|
143
|
+
db.prepare(`UPDATE users SET role = ?, roles = ?, admin_type = NULL, admin_scope = NULL, admin_permissions = NULL, updated_at = datetime('now') WHERE id = ?`)
|
|
144
144
|
.run(newRole, JSON.stringify(rolesArr), targetId);
|
|
145
145
|
db.prepare(`INSERT INTO admin_audit_log (id, admin_id, action, target_type, target_id, detail) VALUES (?,?,?,?,?,?)`)
|
|
146
146
|
.run(generateId('audit'), root.id, 'admin_revoke', 'user', targetId, JSON.stringify({ revoked_type: target.admin_type, new_role: newRole }));
|
|
147
147
|
})();
|
|
148
148
|
res.json({ ok: true });
|
|
149
149
|
});
|
|
150
|
+
// ── ROOT-only emergency freeze of an admin: suspend + strip ALL admin capability + revoke sessions ──
|
|
151
|
+
// For incident response (e.g. a compromised / rogue admin). Atomic. Cannot freeze self or the last root.
|
|
152
|
+
app.post('/api/admin/admins/:id/emergency-freeze', async (req, res) => {
|
|
153
|
+
const root = requireRootAdmin(req, res);
|
|
154
|
+
if (!root)
|
|
155
|
+
return;
|
|
156
|
+
const targetId = req.params.id;
|
|
157
|
+
if (targetId === root.id)
|
|
158
|
+
return void res.json({ error: '不能冻结自己' });
|
|
159
|
+
const reason = (req.body?.reason ? String(req.body.reason).slice(0, 200) : '') || 'emergency admin freeze';
|
|
160
|
+
const target = await dbOne(`SELECT id, role, roles, admin_type FROM users WHERE id = ?`, [targetId]);
|
|
161
|
+
if (!target)
|
|
162
|
+
return void res.json({ error: '用户不存在' });
|
|
163
|
+
if (!target.admin_type)
|
|
164
|
+
return void res.json({ error: '目标必须是 admin 账号' });
|
|
165
|
+
if (target.admin_type === 'root') {
|
|
166
|
+
const rootCount = (await dbOne(`SELECT COUNT(1) as n FROM users WHERE admin_type = 'root'`, [])).n;
|
|
167
|
+
if (rootCount <= 1)
|
|
168
|
+
return void res.json({ error: '不能冻结最后一个 root admin' });
|
|
169
|
+
}
|
|
170
|
+
let sessionsRevoked = 0;
|
|
171
|
+
db.transaction(() => {
|
|
172
|
+
let rolesArr = [];
|
|
173
|
+
try {
|
|
174
|
+
rolesArr = JSON.parse(target.roles || '[]');
|
|
175
|
+
}
|
|
176
|
+
catch { }
|
|
177
|
+
const oldRoles = [...rolesArr];
|
|
178
|
+
rolesArr = rolesArr.filter(r => r !== 'admin');
|
|
179
|
+
const newRole = rolesArr[0] || 'buyer';
|
|
180
|
+
// 1) suspend
|
|
181
|
+
db.prepare(`INSERT INTO user_moderation (user_id, suspended, reason, suspended_by, suspended_at)
|
|
182
|
+
VALUES (?, 1, ?, ?, datetime('now'))
|
|
183
|
+
ON CONFLICT(user_id) DO UPDATE SET suspended = 1, reason = excluded.reason, suspended_by = excluded.suspended_by, suspended_at = excluded.suspended_at`)
|
|
184
|
+
.run(targetId, reason, root.id);
|
|
185
|
+
// 2) strip ALL admin capability
|
|
186
|
+
db.prepare(`UPDATE users SET role = ?, roles = ?, admin_type = NULL, admin_scope = NULL, admin_permissions = NULL, updated_at = datetime('now') WHERE id = ?`)
|
|
187
|
+
.run(newRole, JSON.stringify(rolesArr), targetId);
|
|
188
|
+
// 3) revoke all active sessions
|
|
189
|
+
const r = db.prepare(`UPDATE user_sessions SET revoked_at = datetime('now') WHERE user_id = ? AND revoked_at IS NULL`).run(targetId);
|
|
190
|
+
sessionsRevoked = r.changes;
|
|
191
|
+
// 4) audit
|
|
192
|
+
db.prepare(`INSERT INTO admin_audit_log (id, admin_id, action, target_type, target_id, detail) VALUES (?,?,?,?,?,?)`)
|
|
193
|
+
.run(generateId('audit'), root.id, 'admin_emergency_freeze', 'user', targetId, JSON.stringify({ old_admin_type: target.admin_type, old_roles: oldRoles, sessions_revoked: sessionsRevoked, reason }));
|
|
194
|
+
})();
|
|
195
|
+
res.json({ ok: true, frozen: targetId, sessions_revoked: sessionsRevoked });
|
|
196
|
+
});
|
|
150
197
|
}
|
|
@@ -215,24 +215,15 @@ export function registerAdminAnalyticsRoutes(app, deps) {
|
|
|
215
215
|
AND (vs.suspended_until IS NULL OR vs.suspended_until < datetime('now'))
|
|
216
216
|
`));
|
|
217
217
|
const tokenomics = await (async () => {
|
|
218
|
-
|
|
219
|
-
const mb = await dbOne("SELECT balance FROM management_bonus_pool WHERE id=1");
|
|
218
|
+
// matching-rewards admin metrics (pool / scores / mgmt-bonus / binary payout) removed — engine excised (#401).
|
|
220
219
|
const pendingLedger = (await dbOne("SELECT COUNT(*) as n FROM pv_ledger WHERE processed = 0")).n;
|
|
221
220
|
const commCount = (await dbOne("SELECT COUNT(*) as n, COALESCE(SUM(amount),0) as t FROM commission_records"));
|
|
222
221
|
const dirtyUsers = (await dbOne("SELECT COUNT(*) as n FROM users WHERE pv_dirty_at IS NOT NULL")).n;
|
|
223
|
-
const matchedTotal = (await dbOne("SELECT COUNT(*) as n, COALESCE(SUM(waz_amount),0) as w FROM binary_score_records WHERE settled_at IS NOT NULL"));
|
|
224
222
|
return {
|
|
225
|
-
pool_balance: Number(gf?.pool_balance ?? 0),
|
|
226
|
-
scores_pending: Number(gf?.total_scores_pending ?? 0),
|
|
227
|
-
current_n: Number(gf?.current_n ?? 0),
|
|
228
|
-
last_settled_at: gf?.last_settled_at,
|
|
229
|
-
management_bonus: Number(mb?.balance ?? 0),
|
|
230
223
|
ledger_pending: pendingLedger,
|
|
231
224
|
dirty_users: dirtyUsers,
|
|
232
225
|
commission_records: commCount.n,
|
|
233
226
|
commission_total: commCount.t,
|
|
234
|
-
binary_settled: matchedTotal.n,
|
|
235
|
-
binary_waz_total: matchedTotal.w,
|
|
236
227
|
};
|
|
237
228
|
})();
|
|
238
229
|
res.json({
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export function registerAdminAtomicRoutes(app, deps) {
|
|
2
|
-
|
|
2
|
+
// matching settlement / payout endpoints removed — engine excised (#401). Only neutral
|
|
3
|
+
// participation-recording (process-ledger) remains. (runBinarySettlement / executeSafeSettlementCron
|
|
4
|
+
// are still injected by the server but no longer wired to any endpoint here.)
|
|
5
|
+
const { requireProtocolAdmin, processPvLedger, logAdminAction } = deps;
|
|
3
6
|
app.post('/api/admin/atomic/process-ledger', (req, res) => {
|
|
4
7
|
const admin = requireProtocolAdmin(req, res);
|
|
5
8
|
if (!admin)
|
|
@@ -8,20 +11,4 @@ export function registerAdminAtomicRoutes(app, deps) {
|
|
|
8
11
|
logAdminAction(admin.id, 'atomic_process_ledger', 'protocol', null, { processed });
|
|
9
12
|
res.json({ processed });
|
|
10
13
|
});
|
|
11
|
-
app.post('/api/admin/atomic/run-settlement', (req, res) => {
|
|
12
|
-
const admin = requireProtocolAdmin(req, res);
|
|
13
|
-
if (!admin)
|
|
14
|
-
return;
|
|
15
|
-
const settled = runBinarySettlement();
|
|
16
|
-
logAdminAction(admin.id, 'atomic_run_settlement', 'protocol', null, { settled });
|
|
17
|
-
res.json({ settled });
|
|
18
|
-
});
|
|
19
|
-
app.post('/api/admin/atomic/distribute', (req, res) => {
|
|
20
|
-
const admin = requireProtocolAdmin(req, res);
|
|
21
|
-
if (!admin)
|
|
22
|
-
return;
|
|
23
|
-
const result = executeSafeSettlementCron();
|
|
24
|
-
logAdminAction(admin.id, 'atomic_distribute', 'protocol', null, { result });
|
|
25
|
-
res.json(result);
|
|
26
|
-
});
|
|
27
14
|
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { logAdminAction } from '../admin-audit.js';
|
|
2
|
+
import { proposeClaim, confirmClaim, approveClaim, rejectClaim, revokeApprovedClaim, deriveClaimState, listClaimsForSeat, listPendingConfirmationsForContributor, listAllClaims, emitClaimNotifications, claimedEventIdOfApproved, requestUnlink, approveUnlink, rejectUnlink, pendingUnlinkForApproved, listContributorRelationships, listPendingUnlinkRequests, } from '../../layer2-business/L2-9-contribution/admin-operator-claim-workflow.js';
|
|
3
|
+
function httpFor(code) {
|
|
4
|
+
switch (code) {
|
|
5
|
+
case 'claim_not_found':
|
|
6
|
+
case 'approved_not_found':
|
|
7
|
+
case 'request_not_found': return 404;
|
|
8
|
+
case 'not_admin':
|
|
9
|
+
case 'not_root':
|
|
10
|
+
case 'not_contributor':
|
|
11
|
+
case 'not_party':
|
|
12
|
+
case 'gate_failed': return 403;
|
|
13
|
+
case 'bad_state':
|
|
14
|
+
case 'not_confirmed':
|
|
15
|
+
case 'contributor_rejected':
|
|
16
|
+
case 'already_pending': return 409;
|
|
17
|
+
default: return 400; // invalid_input / contributor_not_found / self_link_* / self_related_* / dishonest_marking
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
21
|
+
function shape(v) {
|
|
22
|
+
if (!v)
|
|
23
|
+
return null;
|
|
24
|
+
return {
|
|
25
|
+
claimed_event_id: v.claimed.event_id,
|
|
26
|
+
admin_account_id: v.claimed.admin_account_id,
|
|
27
|
+
contributor_account_id: v.claimed.contributor_account_id,
|
|
28
|
+
status: v.status,
|
|
29
|
+
proposed_at: v.claimed.created_at,
|
|
30
|
+
confirmation: v.confirmation ? { decision: v.confirmation.decision, decided_by: v.confirmation.decided_by, at: v.confirmation.created_at } : null,
|
|
31
|
+
approved: v.approved ? { event_id: v.approved.event_id, at: v.approved.created_at } : null,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function registerAdminOperatorClaimRoutes(app, deps) {
|
|
35
|
+
const { db, errorRes, auth, requireAdmin, requireRootAdmin, consumeGateToken } = deps;
|
|
36
|
+
// Best-effort: a notify failure must never roll back / fail the claim mutation.
|
|
37
|
+
const notify = (kind, claimedEventId) => { try {
|
|
38
|
+
emitClaimNotifications(db, kind, claimedEventId);
|
|
39
|
+
}
|
|
40
|
+
catch { /* notifications are degradable */ } };
|
|
41
|
+
// shape + surface whether an ACTIVE approved claim has a pending unlink request (drives the UI).
|
|
42
|
+
const shapeClaim = (v) => {
|
|
43
|
+
const base = shape(v);
|
|
44
|
+
if (base && v?.status === 'approved' && v.approved) {
|
|
45
|
+
const pu = pendingUnlinkForApproved(db, v.approved.event_id);
|
|
46
|
+
base.unlink_pending = pu ? { request_event_id: pu.request_event_id, requested_by: pu.requested_by, requester_role: pu.requester_role, at: pu.created_at } : null;
|
|
47
|
+
}
|
|
48
|
+
return base;
|
|
49
|
+
};
|
|
50
|
+
// ── admin proposes linking THEIR OWN seat to a contributor account ──
|
|
51
|
+
app.post('/api/admin/operator-claims', (req, res) => {
|
|
52
|
+
const admin = requireAdmin(req, res);
|
|
53
|
+
if (!admin)
|
|
54
|
+
return;
|
|
55
|
+
const contributorAccountId = String((req.body?.contributor_account_id ?? '')).trim();
|
|
56
|
+
const rationale = req.body?.rationale ? String(req.body.rationale) : undefined;
|
|
57
|
+
if (!contributorAccountId)
|
|
58
|
+
return errorRes(res, 400, 'invalid_input', 'contributor_account_id required');
|
|
59
|
+
try {
|
|
60
|
+
const out = db.transaction(() => {
|
|
61
|
+
const r = proposeClaim(db, { actorAdminId: admin.id, contributorAccountId, rationale });
|
|
62
|
+
if (!r.ok)
|
|
63
|
+
return r;
|
|
64
|
+
logAdminAction(db, { adminId: admin.id, action: 'operator_claim.propose', targetType: 'user', targetId: contributorAccountId, detail: { claimed_event_id: r.claimedEventId }, context: { actorType: 'admin_account', agentMode: 'human_direct' } });
|
|
65
|
+
return r;
|
|
66
|
+
})();
|
|
67
|
+
if (!out.ok)
|
|
68
|
+
return errorRes(res, httpFor(out.code), out.code, out.message);
|
|
69
|
+
notify('proposed', out.claimedEventId); // → tell the contributor to confirm
|
|
70
|
+
res.json({ ok: true, claim: shape(deriveClaimState(db, out.claimedEventId)) });
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
errorRes(res, 500, 'internal', e.message);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
// ── the calling admin's own seat: its claims + states (shapeClaim → carries unlink_pending so the
|
|
77
|
+
// admin-seat owner can request/track unlink on their own active claims) ──
|
|
78
|
+
app.get('/api/admin/operator-claims/me', (req, res) => {
|
|
79
|
+
const admin = requireAdmin(req, res);
|
|
80
|
+
if (!admin)
|
|
81
|
+
return;
|
|
82
|
+
res.json({ seat: admin.id, claims: listClaimsForSeat(db, admin.id).map(shapeClaim) });
|
|
83
|
+
});
|
|
84
|
+
// ── contributor: claims pointing at ME awaiting my confirmation ──
|
|
85
|
+
app.get('/api/me/operator-claim-confirmations', (req, res) => {
|
|
86
|
+
const user = auth(req, res);
|
|
87
|
+
if (!user)
|
|
88
|
+
return;
|
|
89
|
+
res.json({ pending: listPendingConfirmationsForContributor(db, user.id).map(shape) });
|
|
90
|
+
});
|
|
91
|
+
// ── contributor accepts/rejects a claim pointing at them ──
|
|
92
|
+
app.post('/api/me/operator-claim-confirmations/:claimedEventId', (req, res) => {
|
|
93
|
+
const user = auth(req, res);
|
|
94
|
+
if (!user)
|
|
95
|
+
return;
|
|
96
|
+
const decision = String(req.body?.decision ?? '');
|
|
97
|
+
const rationale = req.body?.rationale ? String(req.body.rationale) : undefined;
|
|
98
|
+
try {
|
|
99
|
+
const out = db.transaction(() => {
|
|
100
|
+
const r = confirmClaim(db, { claimedEventId: String(req.params.claimedEventId), deciderId: user.id, decision: decision, rationale });
|
|
101
|
+
if (!r.ok)
|
|
102
|
+
return r;
|
|
103
|
+
logAdminAction(db, { adminId: user.id, action: 'operator_claim.confirm', targetType: 'operator_claim', targetId: String(req.params.claimedEventId), detail: { decision }, context: { actorType: 'human', agentMode: 'human_direct' } });
|
|
104
|
+
return r;
|
|
105
|
+
})();
|
|
106
|
+
if (!out.ok)
|
|
107
|
+
return errorRes(res, httpFor(out.code), out.code, out.message);
|
|
108
|
+
notify(decision === 'accepted' ? 'accepted' : 'rejected_by_contributor', String(req.params.claimedEventId)); // → root to approve, or admin that it was declined
|
|
109
|
+
res.json({ ok: true, claim: shape(deriveClaimState(db, String(req.params.claimedEventId))) });
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
errorRes(res, 500, 'internal', e.message);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
// ── ROOT: review queue (all claims, optional ?status=) ──
|
|
116
|
+
app.get('/api/admin/operator-claims', (req, res) => {
|
|
117
|
+
const root = requireRootAdmin(req, res);
|
|
118
|
+
if (!root)
|
|
119
|
+
return;
|
|
120
|
+
const status = req.query.status ? String(req.query.status) : undefined;
|
|
121
|
+
res.json({ claims: listAllClaims(db, status).map(shape) });
|
|
122
|
+
});
|
|
123
|
+
// ── ROOT: claim detail ──
|
|
124
|
+
app.get('/api/admin/operator-claims/:claimedEventId', (req, res) => {
|
|
125
|
+
const root = requireRootAdmin(req, res);
|
|
126
|
+
if (!root)
|
|
127
|
+
return;
|
|
128
|
+
const v = deriveClaimState(db, String(req.params.claimedEventId));
|
|
129
|
+
if (!v)
|
|
130
|
+
return errorRes(res, 404, 'claim_not_found', 'no such claim');
|
|
131
|
+
res.json({ claim: shape(v) });
|
|
132
|
+
});
|
|
133
|
+
// Shared tx + audit + error boilerplate. Each ROOT route below inlines requireRootAdmin(req, res)
|
|
134
|
+
// FIRST (so the api-docs/OpenAPI generator detects the auth gate) then delegates here.
|
|
135
|
+
const runRootMutation = (res, root, req, action, targetId, fn, notifyKind, notifyClaimedEventId) => {
|
|
136
|
+
try {
|
|
137
|
+
const out = db.transaction(() => {
|
|
138
|
+
const r = fn();
|
|
139
|
+
if (!r.ok)
|
|
140
|
+
return r;
|
|
141
|
+
logAdminAction(db, { adminId: root.id, action, targetType: 'operator_claim', targetId, detail: { result: r }, context: { actorType: 'admin_account', agentMode: 'human_direct', approvalKind: req.body?.approval_kind, conflictDisclosure: req.body?.conflict_disclosure } });
|
|
142
|
+
return r;
|
|
143
|
+
})();
|
|
144
|
+
if (!out.ok)
|
|
145
|
+
return errorRes(res, httpFor(out.code), out.code, out.message);
|
|
146
|
+
if (notifyKind && notifyClaimedEventId)
|
|
147
|
+
notify(notifyKind, notifyClaimedEventId); // → tell contributor + proposing admin
|
|
148
|
+
res.json({ ok: true, result: out });
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
errorRes(res, 500, 'internal', e.message);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
// ── ROOT: approve a proposed-or-confirmed claim ──
|
|
155
|
+
app.post('/api/admin/operator-claims/:claimedEventId/approve', (req, res) => {
|
|
156
|
+
const root = requireRootAdmin(req, res);
|
|
157
|
+
if (!root)
|
|
158
|
+
return;
|
|
159
|
+
const id = String(req.params.claimedEventId);
|
|
160
|
+
runRootMutation(res, root, req, 'operator_claim.approve', id, () => approveClaim(db, { claimedEventId: id, approverId: root.id, approvalKind: String(req.body?.approval_kind ?? ''), conflictDisclosure: String(req.body?.conflict_disclosure ?? ''), rationale: req.body?.rationale ? String(req.body.rationale) : undefined }), 'approved', id);
|
|
161
|
+
});
|
|
162
|
+
// ── ROOT: reject a still-proposed/confirmed claim ──
|
|
163
|
+
app.post('/api/admin/operator-claims/:claimedEventId/reject', (req, res) => {
|
|
164
|
+
const root = requireRootAdmin(req, res);
|
|
165
|
+
if (!root)
|
|
166
|
+
return;
|
|
167
|
+
const id = String(req.params.claimedEventId);
|
|
168
|
+
runRootMutation(res, root, req, 'operator_claim.reject', id, () => rejectClaim(db, { claimedEventId: id, approverId: root.id, rationale: req.body?.rationale ? String(req.body.rationale) : undefined }), 'rejected_by_root', id);
|
|
169
|
+
});
|
|
170
|
+
// ── ROOT: revoke an APPROVED (active) claim ──
|
|
171
|
+
app.post('/api/admin/operator-claims/:approvedEventId/revoke', (req, res) => {
|
|
172
|
+
const root = requireRootAdmin(req, res);
|
|
173
|
+
if (!root)
|
|
174
|
+
return;
|
|
175
|
+
const id = String(req.params.approvedEventId);
|
|
176
|
+
const claimedId = claimedEventIdOfApproved(db, id) ?? undefined; // resolve the claim behind the approval for notifications
|
|
177
|
+
runRootMutation(res, root, req, 'operator_claim.revoke', id, () => revokeApprovedClaim(db, { approvedEventId: id, revokerId: root.id, rationale: req.body?.rationale ? String(req.body.rationale) : undefined }), 'revoked', claimedId);
|
|
178
|
+
});
|
|
179
|
+
// ── contributor self-view: ALL relationships pointing at me (pending/confirmed/approved/history) ──
|
|
180
|
+
app.get('/api/me/operator-claims', (req, res) => {
|
|
181
|
+
const user = auth(req, res);
|
|
182
|
+
if (!user)
|
|
183
|
+
return;
|
|
184
|
+
res.json({ relationships: listContributorRelationships(db, user.id).map(shapeClaim) });
|
|
185
|
+
});
|
|
186
|
+
// ── EITHER PARTY requests UNLINK of an active approved claim — passkey-gated (not just a UI confirm) ──
|
|
187
|
+
app.post('/api/me/operator-claims/:approvedEventId/request-unlink', (req, res) => {
|
|
188
|
+
const user = auth(req, res);
|
|
189
|
+
if (!user)
|
|
190
|
+
return;
|
|
191
|
+
const approvedEventId = String(req.params.approvedEventId);
|
|
192
|
+
const webauthnToken = req.body?.webauthn_token ? String(req.body.webauthn_token) : undefined;
|
|
193
|
+
const reason = req.body?.reason ? String(req.body.reason) : undefined;
|
|
194
|
+
// fresh passkey gate bound to THIS approved claim (purpose 'operator_claim_unlink'). TODO: could be
|
|
195
|
+
// promoted to the typed requireHumanPresence iron-rule purpose; consumeGateToken is the same token store.
|
|
196
|
+
const gate = consumeGateToken(user.id, webauthnToken, 'operator_claim_unlink', (data) => !!data && typeof data === 'object' && data.approved_event_id === approvedEventId);
|
|
197
|
+
if (!gate.ok)
|
|
198
|
+
return errorRes(res, 403, 'gate_failed', gate.reason || '需要 Passkey 验证(重新发起解除申请)');
|
|
199
|
+
try {
|
|
200
|
+
const out = db.transaction(() => {
|
|
201
|
+
const r = requestUnlink(db, { approvedEventId, requesterId: user.id, reason, humanAuthRef: webauthnToken });
|
|
202
|
+
if (!r.ok)
|
|
203
|
+
return r;
|
|
204
|
+
logAdminAction(db, { adminId: user.id, action: 'operator_claim.unlink_request', targetType: 'operator_claim', targetId: approvedEventId, detail: { request_event_id: r.requestEventId, requester_role: r.requesterRole, reason: reason ?? null, human_auth_ref: webauthnToken ?? null }, context: { actorType: 'human', agentMode: 'human_direct', humanAuthorizationId: webauthnToken } });
|
|
205
|
+
return r;
|
|
206
|
+
})();
|
|
207
|
+
if (!out.ok)
|
|
208
|
+
return errorRes(res, httpFor(out.code), out.code, out.message);
|
|
209
|
+
notify('unlink_requested', claimedEventIdOfApproved(db, approvedEventId) ?? ''); // → root review queue
|
|
210
|
+
res.json({ ok: true, request_event_id: out.requestEventId, requester_role: out.requesterRole });
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
errorRes(res, 500, 'internal', e.message);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
// ── ROOT: pending unlink requests (review queue). Path under /unlink/ to avoid the /:claimedEventId match.
|
|
217
|
+
// self_or_related flags each request the viewing root is a party to → the UI then REQUIRES honest marking. ──
|
|
218
|
+
app.get('/api/admin/operator-claims/unlink/requests', (req, res) => {
|
|
219
|
+
const root = requireRootAdmin(req, res);
|
|
220
|
+
if (!root)
|
|
221
|
+
return;
|
|
222
|
+
const rid = root.id;
|
|
223
|
+
const requests = listPendingUnlinkRequests(db).map((r) => ({
|
|
224
|
+
...r,
|
|
225
|
+
self_or_related: rid === r.admin_account_id || rid === r.contributor_account_id || rid === r.requested_by,
|
|
226
|
+
}));
|
|
227
|
+
res.json({ requests });
|
|
228
|
+
});
|
|
229
|
+
// ── ROOT: approve an unlink request → revokes the claim. When root is self-or-related to the
|
|
230
|
+
// relationship/request, approval_kind + conflict_disclosure are required (governance honesty). ──
|
|
231
|
+
app.post('/api/admin/operator-claims/unlink/:requestEventId/approve', (req, res) => {
|
|
232
|
+
const root = requireRootAdmin(req, res);
|
|
233
|
+
if (!root)
|
|
234
|
+
return;
|
|
235
|
+
const id = String(req.params.requestEventId);
|
|
236
|
+
const approvalKind = req.body?.approval_kind ? String(req.body.approval_kind) : undefined;
|
|
237
|
+
const conflictDisclosure = req.body?.conflict_disclosure ? String(req.body.conflict_disclosure) : undefined;
|
|
238
|
+
try {
|
|
239
|
+
const out = db.transaction(() => {
|
|
240
|
+
const r = approveUnlink(db, { requestEventId: id, approverId: root.id, approvalKind, conflictDisclosure });
|
|
241
|
+
if (!r.ok)
|
|
242
|
+
return r;
|
|
243
|
+
logAdminAction(db, { adminId: root.id, action: 'operator_claim.unlink_approve', targetType: 'operator_claim', targetId: id, detail: { result: r }, context: { actorType: 'admin_account', agentMode: 'human_direct', approvalKind: r.approvalKind, conflictDisclosure: r.conflictDisclosure } });
|
|
244
|
+
return r;
|
|
245
|
+
})();
|
|
246
|
+
if (!out.ok)
|
|
247
|
+
return errorRes(res, httpFor(out.code), out.code, out.message);
|
|
248
|
+
notify('unlink_approved', out.claimedEventId);
|
|
249
|
+
res.json({ ok: true, result: out });
|
|
250
|
+
}
|
|
251
|
+
catch (e) {
|
|
252
|
+
errorRes(res, 500, 'internal', e.message);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
// ── ROOT: reject an unlink request → claim stays active. Same self-or-related marking discipline. ──
|
|
256
|
+
app.post('/api/admin/operator-claims/unlink/:requestEventId/reject', (req, res) => {
|
|
257
|
+
const root = requireRootAdmin(req, res);
|
|
258
|
+
if (!root)
|
|
259
|
+
return;
|
|
260
|
+
const id = String(req.params.requestEventId);
|
|
261
|
+
const approvalKind = req.body?.approval_kind ? String(req.body.approval_kind) : undefined;
|
|
262
|
+
const conflictDisclosure = req.body?.conflict_disclosure ? String(req.body.conflict_disclosure) : undefined;
|
|
263
|
+
try {
|
|
264
|
+
const out = db.transaction(() => {
|
|
265
|
+
const r = rejectUnlink(db, { requestEventId: id, approverId: root.id, approvalKind, conflictDisclosure });
|
|
266
|
+
if (!r.ok)
|
|
267
|
+
return r;
|
|
268
|
+
logAdminAction(db, { adminId: root.id, action: 'operator_claim.unlink_reject', targetType: 'operator_claim', targetId: id, detail: { result: r }, context: { actorType: 'admin_account', agentMode: 'human_direct', approvalKind: r.approvalKind, conflictDisclosure: r.conflictDisclosure } });
|
|
269
|
+
return r;
|
|
270
|
+
})();
|
|
271
|
+
if (!out.ok)
|
|
272
|
+
return errorRes(res, httpFor(out.code), out.code, out.message);
|
|
273
|
+
notify('unlink_rejected', out.claimedEventId);
|
|
274
|
+
res.json({ ok: true, result: out });
|
|
275
|
+
}
|
|
276
|
+
catch (e) {
|
|
277
|
+
errorRes(res, 500, 'internal', e.message);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
@@ -61,9 +61,8 @@ export function registerAdminReportsRoutes(app, deps) {
|
|
|
61
61
|
sql += ` ORDER BY vt.created_at DESC LIMIT 100`;
|
|
62
62
|
res.json({ tasks: await dbAll(sql, params) });
|
|
63
63
|
});
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// 隐私第一:运营财务,仅 protocol admin 可见(详见 docs/REWARD-ENGINES-DECOUPLING.md)
|
|
64
|
+
// 收入治理视图 — 联盟佣金(按真实成交)协议级聚合。匹配奖励引擎已切除(#401),不再聚合其指标。
|
|
65
|
+
// 隐私第一:运营财务,仅 protocol admin 可见。
|
|
67
66
|
app.get('/api/admin/economic-summary', async (req, res) => {
|
|
68
67
|
const admin = requireProtocolAdmin(req, res);
|
|
69
68
|
if (!admin)
|
|
@@ -87,12 +86,7 @@ export function registerAdminReportsRoutes(app, deps) {
|
|
|
87
86
|
// charity_fund 自此纯净(仅捐款/还愿/拨款),不再承接佣金兜底。
|
|
88
87
|
const charity = await dbOne(`SELECT balance, total_donated, total_disbursed, total_redirected FROM charity_fund WHERE id='main'`);
|
|
89
88
|
const cpool = await dbOne(`SELECT balance, total_chain_gap, total_orphan_sponsor, total_region_cap FROM commission_reserve WHERE id='main'`);
|
|
90
|
-
//
|
|
91
|
-
const binarySettled = (await dbOne(`SELECT COUNT(*) AS cnt, COALESCE(SUM(waz_amount),0) AS waz FROM binary_score_records WHERE settled_at IS NOT NULL`));
|
|
92
|
-
const binaryPending = (await dbOne(`SELECT COUNT(*) AS cnt, COALESCE(SUM(score),0) AS score FROM binary_score_records WHERE settled_at IS NULL`));
|
|
93
|
-
const periods = (await dbOne(`SELECT COUNT(*) AS cnt, COALESCE(SUM(cash_distributed),0) AS dist, COALESCE(SUM(cash_retained),0) AS retained FROM settlement_periods WHERE status='completed'`));
|
|
94
|
-
const gfund = await dbOne(`SELECT pool_balance, total_scores_pending, current_n, pv_escrow_reserve FROM global_fund WHERE id=1`);
|
|
95
|
-
// 资金管道:global_fund 蓄水来源(fund_base 1% + commission region_cap redirect)
|
|
89
|
+
// 资金管道:fund_base 1% 累计 + commission region_cap redirect(历史,新订单恒 0)
|
|
96
90
|
const pipe = (await dbOne(`SELECT COALESCE(SUM(amount_base),0) AS base, COALESCE(SUM(amount_l3),0) AS redirect FROM fund_deposits`));
|
|
97
91
|
res.json({
|
|
98
92
|
engine_a_commission: {
|
|
@@ -105,22 +99,6 @@ export function registerAdminReportsRoutes(app, deps) {
|
|
|
105
99
|
legacy_global_fund_redirect: r2(pipe.redirect), // 历史:解耦前曾入 global_fund(fund_deposits.amount_l3),新订单恒 0
|
|
106
100
|
note: '即时分账;兜底全部入 commission_reserve(独立科目,只进不出,治理决定用途),不再污染 charity / global_fund。',
|
|
107
101
|
},
|
|
108
|
-
engine_b_pv_matching: {
|
|
109
|
-
cash_distributed_total: r2(binarySettled.waz),
|
|
110
|
-
settled_match_count: binarySettled.cnt,
|
|
111
|
-
pending_score: r2(binaryPending.score),
|
|
112
|
-
pending_match_count: binaryPending.cnt,
|
|
113
|
-
settlement_periods_completed: periods.cnt,
|
|
114
|
-
periods_cash_distributed: r2(periods.dist),
|
|
115
|
-
periods_cash_retained: r2(periods.retained),
|
|
116
|
-
note: 'Score → 现金经安全阀拨付,源自 global_fund,永不透支。',
|
|
117
|
-
},
|
|
118
|
-
global_fund: {
|
|
119
|
-
pool_balance: r2(Number(gfund?.pool_balance || 0)),
|
|
120
|
-
pv_escrow_reserve: r2(Number(gfund?.pv_escrow_reserve || 0)), // #1106 已承诺未兑付的 PV 负债(隔离于可分配池)
|
|
121
|
-
total_scores_pending: r2(Number(gfund?.total_scores_pending || 0)),
|
|
122
|
-
current_n: r2(Number(gfund?.current_n || 0)),
|
|
123
|
-
},
|
|
124
102
|
funding_pipe: {
|
|
125
103
|
fund_base_1pct_accumulated: r2(pipe.base),
|
|
126
104
|
commission_redirect_accumulated: r2(pipe.redirect),
|
|
@@ -131,7 +109,7 @@ export function registerAdminReportsRoutes(app, deps) {
|
|
|
131
109
|
total_donated: r2(Number(charity?.total_donated || 0)),
|
|
132
110
|
total_disbursed: r2(Number(charity?.total_disbursed || 0)),
|
|
133
111
|
},
|
|
134
|
-
governance_hint: '
|
|
112
|
+
governance_hint: '联盟佣金=消费即时分账→钱包,兜底→commission_reserve(独立科目,只进不出)。匹配奖励引擎已切除(#401)。',
|
|
135
113
|
generated_at: new Date().toISOString(),
|
|
136
114
|
});
|
|
137
115
|
});
|
|
@@ -7,23 +7,14 @@ export function registerAdminTokenomicsRoutes(app, deps) {
|
|
|
7
7
|
const admin = requireProtocolAdmin(req, res);
|
|
8
8
|
if (!admin)
|
|
9
9
|
return;
|
|
10
|
-
|
|
10
|
+
// matching-rewards admin views (tier config / binary leaderboard / mgmt-bonus / pool injection) removed —
|
|
11
|
+
// engine excised (#401). Only neutral participation-record + affiliate-commission stats remain.
|
|
11
12
|
const topComm = await dbAll(`
|
|
12
13
|
SELECT cr.beneficiary_id, u.name, COUNT(*) as records, COALESCE(SUM(cr.amount),0) as earned
|
|
13
14
|
FROM commission_records cr LEFT JOIN users u ON u.id = cr.beneficiary_id
|
|
14
15
|
WHERE cr.beneficiary_id != 'sys_protocol'
|
|
15
16
|
GROUP BY cr.beneficiary_id ORDER BY earned DESC LIMIT 10
|
|
16
17
|
`);
|
|
17
|
-
const topBinary = await dbAll(`
|
|
18
|
-
SELECT bsr.user_id, u.name,
|
|
19
|
-
COUNT(*) as hits,
|
|
20
|
-
COALESCE(SUM(CASE WHEN settled_at IS NOT NULL THEN waz_amount ELSE 0 END),0) as waz_total,
|
|
21
|
-
COALESCE(SUM(score),0) as score_total
|
|
22
|
-
FROM binary_score_records bsr LEFT JOIN users u ON u.id = bsr.user_id
|
|
23
|
-
GROUP BY bsr.user_id ORDER BY waz_total DESC LIMIT 10
|
|
24
|
-
`);
|
|
25
|
-
const gf = await dbOne("SELECT * FROM global_fund WHERE id=1");
|
|
26
|
-
const mb = await dbOne("SELECT balance FROM management_bonus_pool WHERE id=1");
|
|
27
18
|
const pvLedger = await dbOne(`
|
|
28
19
|
SELECT COUNT(*) as total,
|
|
29
20
|
SUM(CASE WHEN processed=0 THEN 1 ELSE 0 END) as pending,
|
|
@@ -31,43 +22,10 @@ export function registerAdminTokenomicsRoutes(app, deps) {
|
|
|
31
22
|
FROM pv_ledger
|
|
32
23
|
`);
|
|
33
24
|
res.json({
|
|
34
|
-
global_fund: gf,
|
|
35
|
-
management_bonus_pool: mb,
|
|
36
|
-
tier_config: tiers,
|
|
37
25
|
pv_ledger: pvLedger,
|
|
38
26
|
top_commission: topComm,
|
|
39
|
-
top_binary: topBinary,
|
|
40
27
|
});
|
|
41
28
|
});
|
|
42
|
-
// 调整 Tier 配置
|
|
43
|
-
app.post('/api/admin/tokenomics/tier', async (req, res) => {
|
|
44
|
-
const admin = requireProtocolAdmin(req, res);
|
|
45
|
-
if (!admin)
|
|
46
|
-
return;
|
|
47
|
-
const { tier, pv_threshold, score_per_hit, active } = req.body;
|
|
48
|
-
if (!Number.isInteger(tier) || tier < 1 || tier > 10)
|
|
49
|
-
return void res.json({ error: 'tier 无效' });
|
|
50
|
-
if (Number(pv_threshold) <= 0 || Number(score_per_hit) <= 0)
|
|
51
|
-
return void res.json({ error: '阈值/分数必须 > 0' });
|
|
52
|
-
await dbRun(`INSERT OR REPLACE INTO binary_tier_config (tier, pv_threshold, score_per_hit, active) VALUES (?,?,?,?)`, [tier, Number(pv_threshold), Number(score_per_hit), active ? 1 : 0]);
|
|
53
|
-
logAdminAction(admin.id, 'tokenomics_tier_update', 'tier', String(tier), { pv_threshold, score_per_hit, active });
|
|
54
|
-
res.json({ success: true });
|
|
55
|
-
});
|
|
56
|
-
// 管理津贴资格列表 + 开关
|
|
57
|
-
app.get('/api/admin/tokenomics/mgmt-bonus', async (req, res) => {
|
|
58
|
-
const admin = requireProtocolAdmin(req, res);
|
|
59
|
-
if (!admin)
|
|
60
|
-
return;
|
|
61
|
-
const enabled = (await dbOne("SELECT value FROM system_state WHERE key='mgmt_bonus_enabled'"))?.value === '1';
|
|
62
|
-
const eligible = await dbAll(`
|
|
63
|
-
SELECT u.id, u.name, u.created_at,
|
|
64
|
-
(SELECT COUNT(*) FROM users WHERE sponsor_id = u.id) as l1_count,
|
|
65
|
-
COALESCE((SELECT SUM(amount) FROM commission_records WHERE beneficiary_id = u.id),0) as total_commission
|
|
66
|
-
FROM users u WHERE u.mgmt_bonus_eligible = 1
|
|
67
|
-
ORDER BY u.created_at DESC LIMIT 100
|
|
68
|
-
`);
|
|
69
|
-
res.json({ enabled, eligible_users: eligible, eligible_count: eligible.length });
|
|
70
|
-
});
|
|
71
29
|
// 注册必须 ref 开关
|
|
72
30
|
app.post('/api/admin/tokenomics/require-ref/toggle', async (req, res) => {
|
|
73
31
|
const admin = requireProtocolAdmin(req, res);
|
|
@@ -79,36 +37,4 @@ export function registerAdminTokenomicsRoutes(app, deps) {
|
|
|
79
37
|
logAdminAction(admin.id, 'require_ref_toggle', 'system', 'require_ref_to_register', { value: v });
|
|
80
38
|
res.json({ success: true, enabled: !!enabled });
|
|
81
39
|
});
|
|
82
|
-
// 管理津贴池开关
|
|
83
|
-
app.post('/api/admin/tokenomics/mgmt-bonus/toggle', async (req, res) => {
|
|
84
|
-
const admin = requireProtocolAdmin(req, res);
|
|
85
|
-
if (!admin)
|
|
86
|
-
return;
|
|
87
|
-
const { enabled } = req.body;
|
|
88
|
-
const v = enabled ? '1' : '0';
|
|
89
|
-
await dbRun("INSERT OR REPLACE INTO system_state (key, value) VALUES ('mgmt_bonus_enabled', ?)", [v]);
|
|
90
|
-
logAdminAction(admin.id, 'mgmt_bonus_toggle', 'system', 'mgmt_bonus_enabled', { value: v });
|
|
91
|
-
res.json({ success: true, enabled: !!enabled });
|
|
92
|
-
});
|
|
93
|
-
// 池注资
|
|
94
|
-
app.post('/api/admin/tokenomics/inject', async (req, res) => {
|
|
95
|
-
const admin = requireProtocolAdmin(req, res);
|
|
96
|
-
if (!admin)
|
|
97
|
-
return;
|
|
98
|
-
const { pool, amount } = req.body;
|
|
99
|
-
const n = Number(amount);
|
|
100
|
-
if (!(n > 0))
|
|
101
|
-
return void res.json({ error: '金额必须 > 0' });
|
|
102
|
-
if (pool === 'global_fund') {
|
|
103
|
-
await dbRun("UPDATE global_fund SET pool_balance = pool_balance + ? WHERE id=1", [n]);
|
|
104
|
-
}
|
|
105
|
-
else if (pool === 'management_bonus') {
|
|
106
|
-
await dbRun("UPDATE management_bonus_pool SET balance = balance + ? WHERE id=1", [n]);
|
|
107
|
-
}
|
|
108
|
-
else {
|
|
109
|
-
return void res.json({ error: 'pool 名称无效(global_fund / management_bonus)' });
|
|
110
|
-
}
|
|
111
|
-
logAdminAction(admin.id, 'tokenomics_pool_inject', 'pool', pool, { amount: n });
|
|
112
|
-
res.json({ success: true });
|
|
113
|
-
});
|
|
114
40
|
}
|
|
@@ -20,20 +20,7 @@ export function registerAdminUsersLifecycleRoutes(app, deps) {
|
|
|
20
20
|
logAdminAction(admin.id, 'l1_share_override', 'user', req.params.id, { value: v, note: note || null });
|
|
21
21
|
res.json({ success: true, value: v });
|
|
22
22
|
});
|
|
23
|
-
|
|
24
|
-
const admin = requireProtocolAdmin(req, res);
|
|
25
|
-
if (!admin)
|
|
26
|
-
return;
|
|
27
|
-
if (!adminCanOperateOn(admin, req.params.id, res))
|
|
28
|
-
return;
|
|
29
|
-
const { eligible, note } = req.body;
|
|
30
|
-
const target = await dbOne("SELECT id, name FROM users WHERE id = ?", [req.params.id]);
|
|
31
|
-
if (!target)
|
|
32
|
-
return void res.json({ error: '用户不存在' });
|
|
33
|
-
await dbRun("UPDATE users SET mgmt_bonus_eligible = ?, updated_at = datetime('now') WHERE id = ?", [eligible ? 1 : 0, req.params.id]);
|
|
34
|
-
logAdminAction(admin.id, eligible ? 'mgmt_bonus_grant' : 'mgmt_bonus_revoke', 'user', req.params.id, { note: note || null });
|
|
35
|
-
res.json({ success: true });
|
|
36
|
-
});
|
|
23
|
+
// 解除账号登录锁定:清零失败次数 + 解锁
|
|
37
24
|
app.post('/api/admin/users/:id/reset-failed-attempts', async (req, res) => {
|
|
38
25
|
const admin = requireUsersAdmin(req, res);
|
|
39
26
|
if (!admin)
|