Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions stellar-payment-platform/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,18 @@ app.get('/metrics', async (req, res) => {
}
});


/**
* @openapi
* /federation:
* get:
* tags:
* - v1
* description: GET /federation
* responses:
* 200:
* description: Success
*/
app.get('/federation', ipLimiter, etagCache, validateSchema({ query: federationQuerySchema }), async (req, res, next) => {
const { q: queryValue, type } = req.query;

Expand Down Expand Up @@ -623,6 +635,18 @@ const verifyFreighterRegistrationSignature = ({
* - Validates that provided signature(s) meet minimum threshold
* - Ensures authorization requirements are satisfied
*/

/**
* @openapi
* /register:
* post:
* tags:
* - v1
* description: POST /register
* responses:
* 200:
* description: Success
*/
app.post('/register', ipLimiter, idempotencyMiddleware(redisClient), requireJson, validateSchema({ body: registerBodySchema }), async (req, res, next) => {
// registerBodySchema has already guaranteed that username is a trimmed
// 3-20 character alphanumeric string and address is a non-empty trimmed
Expand Down
14 changes: 14 additions & 0 deletions stellar-payment-platform/src/routes/v1/paymentRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,26 @@ const { idempotencyMiddleware } = require('../../../middleware/idempotency');

module.exports = (redisClient) => {
const router = express.Router();

router.use(idempotencyMiddleware(redisClient));

// ── Idempotency protection for payment intent creation (POST /payments/bulk).
// Duplicate submissions within 24h return the originally created intents. ──
router.use(idempotencyMiddleware(redisClient));

// POST /payments/bulk

/**
* @openapi
* /payments/bulk:
* post:
* tags:
* - v1
* description: POST /payments/bulk
* responses:
* 200:
* description: Success
*/
router.post('/payments/bulk', requireJson, validateSchema({ body: bulkPaymentSchema }), asyncHandler(async (req, res, next) => {
const intents = req.body;

Expand Down
12 changes: 12 additions & 0 deletions stellar-payment-platform/src/routes/v1/statsRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ const { fetchAdminStats } = require('../../services/statsService');
module.exports = (redisClient) => {
const router = express.Router();


/**
* @openapi
* /stats:
* get:
* tags:
* - v1
* description: GET /stats
* responses:
* 200:
* description: Success
*/
router.get('/stats', etagCache, asyncHandler(async (req, res, next) => {
try {
const stats = await getCachedStats(redisClient, () => fetchAdminStats(prisma, poolGet));
Expand Down
14 changes: 13 additions & 1 deletion stellar-payment-platform/src/routes/v1/userRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ router.delete('/register/:username', asyncHandler(async (req, res, next) => {
where: { username },
data: { deletedAt: new Date() },
});

// Invalidate any stale federation cache entries
invalidateFederationCache(username, existing.address);
});
Expand Down Expand Up @@ -513,6 +513,18 @@ const totalPages = Math.ceil(totalCount / limit);
}
}));


/**
* @openapi
* /users:
* get:
* tags:
* - v1
* description: GET /users
* responses:
* 200:
* description: Success
*/
router.get('/users', etagCache, validateSchema({ query: usersQuerySchema }), asyncHandler(async (req, res, next) => {
const { limit: cursorLimit, cursor, invalid: invalidCursor } = parseCursorQuery(req.query);
const { page, limit, skip } = parsePagination(req.query);
Expand Down
30 changes: 29 additions & 1 deletion stellar-payment-platform/src/routes/v1/webhookRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ module.exports = (redisClient) => {
// 2xx response. Read-only GET /webhooks is ignored. ────────────────────────
router.use(idempotencyMiddleware(redisClient));



const DEFAULT_FEDERATION_DOMAIN = 'localhost';

const authenticateWebhookCall = (req) =>
Expand Down Expand Up @@ -169,7 +171,6 @@ router.post('/webhooks/verify-test', asyncHandler(async (req, res, next) => {
}
}));


/**
* @openapi
* /webhooks:
Expand Down Expand Up @@ -392,6 +393,33 @@ router.delete('/webhooks/:id', asyncHandler(async (req, res, next) => {
}
}));

router.post('/webhooks/verify-test', (req, res) => {
const { secret, payload } = req.body;
const signature = req.headers['x-webhook-signature'];

if (!secret || !payload) {
return res.status(400).json({ error: 'Missing secret or payload' });
}

const expectedSignature = crypto.createHmac('sha256', secret).update(payload).digest('hex');

if (signature === expectedSignature) {
return res.status(200).json({
ok: true,
valid: true,
message: 'Signature verification succeeded',
expectedSignature,
});
} else {
return res.status(401).json({
ok: false,
valid: false,
error: { code: 'INVALID_WEBHOOK_SIGNATURE' },
receivedSignature: signature,
});
}
});

router.all('/webhooks', (req, res) => {
if (req.method !== 'GET' && req.method !== 'POST') {
return res.status(405).json({ error: 'Method Not Allowed' });
Expand Down
Loading