Skip to content

Commit 647445f

Browse files
authored
Merge pull request #362 from commandlayer/codex/restore-admin-claims-api-routes
Restore admin claims API routes for claims dashboard
2 parents e2d0316 + 3173944 commit 647445f

4 files changed

Lines changed: 215 additions & 0 deletions

File tree

api/admin/_auth.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use strict';
2+
3+
function extractBearerToken(req) {
4+
const authHeader = req.headers.authorization || req.headers.Authorization || '';
5+
if (!authHeader.startsWith('Bearer ')) return null;
6+
return authHeader.slice('Bearer '.length).trim();
7+
}
8+
9+
function isAdminAuthorized(req) {
10+
const expected = process.env.ADMIN_API_KEY;
11+
if (!expected) return false;
12+
13+
const bearer = extractBearerToken(req);
14+
if (bearer && bearer === expected) return true;
15+
16+
const headerKey = req.headers['x-admin-api-key'];
17+
return Boolean(headerKey && headerKey === expected);
18+
}
19+
20+
function requireAdminAuth(req, res) {
21+
if (isAdminAuthorized(req)) return true;
22+
res.status(401).json({ ok: false, status: 'UNAUTHORIZED' });
23+
return false;
24+
}
25+
26+
module.exports = {
27+
extractBearerToken,
28+
isAdminAuthorized,
29+
requireAdminAuth,
30+
};

api/admin/claim.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
'use strict';
2+
3+
const db = require('../../lib/db');
4+
const { requireAdminAuth } = require('./_auth');
5+
6+
async function hasTable(tableName) {
7+
const result = await db.query('select to_regclass($1) as table_name', [tableName]);
8+
return Boolean(result.rows[0] && result.rows[0].table_name);
9+
}
10+
11+
module.exports = async function handler(req, res) {
12+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
13+
res.setHeader('Cache-Control', 'no-store');
14+
15+
if (req.method !== 'GET') {
16+
res.setHeader('Allow', 'GET');
17+
return res.status(405).json({ ok: false, status: 'METHOD_NOT_ALLOWED' });
18+
}
19+
20+
if (!requireAdminAuth(req, res)) return;
21+
22+
const claimId = req.query && typeof req.query.claimId === 'string' ? req.query.claimId.trim() : '';
23+
if (!claimId) return res.status(400).json({ ok: false, status: 'CLAIM_ID_REQUIRED' });
24+
25+
const claimResult = await db.query('select * from claim_requests where claim_id = $1 limit 1', [claimId]);
26+
const claim = claimResult.rows[0];
27+
if (!claim) return res.status(404).json({ ok: false, status: 'CLAIM_NOT_FOUND' });
28+
29+
const agentsResult = await db.query('select * from claim_agents where claim_id = $1 order by created_at asc', [claimId]);
30+
const eventsResult = await db.query('select * from claim_events where claim_id = $1 order by created_at desc', [claimId]);
31+
32+
let transitions = [];
33+
if (await hasTable('claim_status_transitions')) {
34+
const transitionsResult = await db.query('select * from claim_status_transitions where claim_id = $1 order by created_at desc', [claimId]);
35+
transitions = transitionsResult.rows;
36+
}
37+
38+
let cards = [];
39+
if (await hasTable('agent_cards')) {
40+
const cardsResult = await db.query('select * from agent_cards where claim_id = $1 order by created_at asc', [claimId]);
41+
cards = cardsResult.rows;
42+
}
43+
44+
let latestPayment = null;
45+
if (await hasTable('claim_payments')) {
46+
const paymentResult = await db.query('select * from claim_payments where claim_id = $1 order by created_at desc limit 1', [claimId]);
47+
latestPayment = paymentResult.rows[0] || null;
48+
}
49+
50+
return res.status(200).json({ ok: true, claim, agents: agentsResult.rows, events: eventsResult.rows, transitions, cards, latestPayment });
51+
};

api/admin/claims.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
'use strict';
2+
3+
const db = require('../../lib/db');
4+
const { requireAdminAuth } = require('./_auth');
5+
6+
module.exports = async function handler(req, res) {
7+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
8+
res.setHeader('Cache-Control', 'no-store');
9+
10+
if (req.method !== 'GET') {
11+
res.setHeader('Allow', 'GET');
12+
return res.status(405).json({ ok: false, status: 'METHOD_NOT_ALLOWED' });
13+
}
14+
15+
if (!requireAdminAuth(req, res)) return;
16+
17+
const result = await db.query(
18+
`select
19+
cr.claim_id,
20+
cr.tenant,
21+
cr.authenticated_address,
22+
cr.pack_id,
23+
cr.status,
24+
cr.payment_status,
25+
cr.created_at,
26+
cr.paid_at,
27+
cr.stripe_checkout_session_id,
28+
coalesce(ca.agent_count, 0)::int as agent_count
29+
from claim_requests cr
30+
left join (
31+
select claim_id, count(*)::int as agent_count
32+
from claim_agents
33+
group by claim_id
34+
) ca on ca.claim_id = cr.claim_id
35+
order by cr.created_at desc`
36+
);
37+
38+
const claims = result.rows.map((row) => ({
39+
claimId: row.claim_id,
40+
tenant: row.tenant,
41+
wallet: row.authenticated_address,
42+
packId: row.pack_id,
43+
status: row.status,
44+
paymentStatus: row.payment_status,
45+
agentCount: row.agent_count,
46+
createdAt: row.created_at,
47+
paidAt: row.paid_at,
48+
stripeCheckoutSessionId: row.stripe_checkout_session_id,
49+
}));
50+
51+
return res.status(200).json({ ok: true, claims });
52+
};

tests/api-admin-claims.test.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
'use strict';
2+
3+
const test = require('node:test');
4+
const assert = require('node:assert/strict');
5+
6+
const db = require('../lib/db');
7+
const claimsHandler = require('../api/admin/claims');
8+
const claimHandler = require('../api/admin/claim');
9+
10+
function makeRes() {
11+
return {
12+
statusCode: 200,
13+
headers: {},
14+
body: null,
15+
setHeader(name, value) { this.headers[name.toLowerCase()] = value; },
16+
status(code) { this.statusCode = code; return this; },
17+
json(payload) { this.body = payload; return this; },
18+
};
19+
}
20+
21+
test('GET /api/admin/claims missing ADMIN_API_KEY returns auth error, not 404', async () => {
22+
process.env.ADMIN_API_KEY = 'admin-secret';
23+
const res = makeRes();
24+
await claimsHandler({ method: 'GET', headers: {} }, res);
25+
assert.equal(res.statusCode, 401);
26+
assert.equal(res.body.status, 'UNAUTHORIZED');
27+
});
28+
29+
test('GET /api/admin/claims valid auth returns claims array', async () => {
30+
process.env.ADMIN_API_KEY = 'admin-secret';
31+
db.query = async () => ({ rows: [{ claim_id: 'c1', tenant: 't1', authenticated_address: '0xabc', pack_id: 'p1', status: 'created', payment_status: 'unpaid', created_at: '2026-05-27T00:00:00.000Z', paid_at: null, stripe_checkout_session_id: null, agent_count: 2 }] });
32+
const res = makeRes();
33+
await claimsHandler({ method: 'GET', headers: { authorization: 'Bearer admin-secret' } }, res);
34+
assert.equal(res.statusCode, 200);
35+
assert.equal(res.body.ok, true);
36+
assert.equal(Array.isArray(res.body.claims), true);
37+
assert.deepEqual(res.body.claims[0], {
38+
claimId: 'c1', tenant: 't1', wallet: '0xabc', packId: 'p1', status: 'created', paymentStatus: 'unpaid', agentCount: 2, createdAt: '2026-05-27T00:00:00.000Z', paidAt: null, stripeCheckoutSessionId: null,
39+
});
40+
});
41+
42+
test('GET /api/admin/claim missing claimId returns validation error', async () => {
43+
process.env.ADMIN_API_KEY = 'admin-secret';
44+
const res = makeRes();
45+
await claimHandler({ method: 'GET', headers: { authorization: 'Bearer admin-secret' }, query: {} }, res);
46+
assert.equal(res.statusCode, 400);
47+
assert.equal(res.body.status, 'CLAIM_ID_REQUIRED');
48+
});
49+
50+
test('GET /api/admin/claim unknown claim returns CLAIM_NOT_FOUND', async () => {
51+
process.env.ADMIN_API_KEY = 'admin-secret';
52+
db.query = async (q) => (q.includes('from claim_requests') ? { rows: [] } : { rows: [] });
53+
const res = makeRes();
54+
await claimHandler({ method: 'GET', headers: { authorization: 'Bearer admin-secret' }, query: { claimId: 'missing' } }, res);
55+
assert.equal(res.statusCode, 404);
56+
assert.equal(res.body.status, 'CLAIM_NOT_FOUND');
57+
});
58+
59+
test('GET /api/admin/claim returns detail shape compatible with admin UI', async () => {
60+
process.env.ADMIN_API_KEY = 'admin-secret';
61+
db.query = async (q) => {
62+
if (q.includes('from claim_requests')) return { rows: [{ claim_id: 'c2', status: 'approved', tenant: 'tenant-a', authenticated_address: '0x123', pack_id: 'pack' }] };
63+
if (q.includes('from claim_agents')) return { rows: [{ claim_id: 'c2', ens: 'alpha.eth', card_url: 'https://example/card.json' }] };
64+
if (q.includes('from claim_events')) return { rows: [{ claim_id: 'c2', event_type: 'approved', created_at: '2026-05-27T01:00:00.000Z' }] };
65+
if (q.includes('to_regclass')) return { rows: [{ table_name: 'exists' }] };
66+
if (q.includes('from claim_status_transitions')) return { rows: [{ claim_id: 'c2', from_status: 'created', to_status: 'approved' }] };
67+
if (q.includes('from agent_cards')) return { rows: [{ claim_id: 'c2', card_url: 'https://example/card.json' }] };
68+
if (q.includes('from claim_payments')) return { rows: [{ claim_id: 'c2', stripe_checkout_session_id: 'cs_123' }] };
69+
return { rows: [] };
70+
};
71+
72+
const res = makeRes();
73+
await claimHandler({ method: 'GET', headers: { authorization: 'Bearer admin-secret' }, query: { claimId: 'c2' } }, res);
74+
assert.equal(res.statusCode, 200);
75+
assert.equal(res.body.ok, true);
76+
assert.equal(res.body.claim.claim_id, 'c2');
77+
assert.equal(Array.isArray(res.body.agents), true);
78+
assert.equal(Array.isArray(res.body.events), true);
79+
assert.equal(Array.isArray(res.body.transitions), true);
80+
assert.equal(Array.isArray(res.body.cards), true);
81+
assert.equal(res.body.latestPayment.stripe_checkout_session_id, 'cs_123');
82+
});

0 commit comments

Comments
 (0)