Skip to content
Open
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
6 changes: 4 additions & 2 deletions stellar-payment-platform/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,13 @@
"redis": "^4.7.0",
"uuid": "^9.0.1",
"xss": "^1.0.15",
"zod": "^4.4.3"
"zod": "^4.4.3",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.0"
},
"devDependencies": {
"jest": "^29.7.0",
"supertest": "^7.0.0",
"tsx": "4.23.1"
}
}
}
142 changes: 142 additions & 0 deletions stellar-payment-platform/server.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
require('./config/envCheck');
const express = require('express');
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const cors = require('cors');
const helmet = require('helmet');
const crypto = require('crypto');
Expand Down Expand Up @@ -73,6 +75,26 @@ if (process.env.SENTRY_DSN) {

const app = express();

const swaggerOptions = {
definition: {
openapi: '3.0.0',
info: {
title: 'Stellar Tags API',
version: '1.0.0',
description: 'API for Stellar Tags',
},
servers: [
{
url: 'http://localhost:5000',
},
],
},
apis: ['./server.js', './src/routes/v1/*.js'],
};
const swaggerSpec = swaggerJsdoc(swaggerOptions);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));


// #31 — Attach a correlation ID to every request before anything else runs so
// all downstream middleware, handlers and logs can reference the same trace.
app.use(correlationId);
Expand Down Expand Up @@ -372,6 +394,18 @@ const registerLocalUser = async ({ username, address }) => {
};

// Expose /metrics endpoint for Prometheus to scrape

/**
* @openapi
* /metrics:
* get:
* tags:
* - v1
* description: GET /metrics
* responses:
* 200:
* description: Success
*/
app.get('/metrics', async (req, res) => {
try {
res.set('Content-Type', getContentType());
Expand All @@ -383,6 +417,18 @@ app.get('/metrics', async (req, res) => {
}
});


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

Expand Down Expand Up @@ -564,6 +610,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', 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 Expand Up @@ -744,6 +802,18 @@ app.post('/register', idempotencyMiddleware(redisClient), requireJson, validateS

app.all('/register', (req, res, next) => next(new ApiError('METHOD_NOT_ALLOWED')));


/**
* @openapi
* /lookup:
* get:
* tags:
* - v1
* description: GET /lookup
* responses:
* 200:
* description: Success
*/
app.get('/lookup', validateSchema({ query: lookupQuerySchema }), async (req, res, next) => {
const { address = '', search = '' } = req.query;

Expand Down Expand Up @@ -852,6 +922,18 @@ app.get('/lookup', validateSchema({ query: lookupQuerySchema }), async (req, res
}
});


/**
* @openapi
* /users:
* get:
* tags:
* - v1
* description: GET /users
* responses:
* 200:
* description: Success
*/
app.get('/users', validateSchema({ query: usersQuerySchema }), async (req, res, next) => {
const { limit: cursorLimit, cursor, invalid: invalidCursor } = parseCursorQuery(req.query);
const { page, limit, skip } = parsePagination(req.query);
Expand Down Expand Up @@ -944,6 +1026,18 @@ app.use('/auth/api-keys', require('./src/routes/v1/apiKeyRoutes')(redisClient));

// #497 — Expose RSA public key as a JWKS document so external services can
// verify RS256-signed tokens without sharing a secret.

/**
* @openapi
* /.well-known/jwks.json:
* get:
* tags:
* - v1
* description: GET /.well-known/jwks.json
* responses:
* 200:
* description: Success
*/
app.get('/.well-known/jwks.json', (_req, res) => {
try {
const { getJwks } = require('./src/utils/jwt');
Expand All @@ -959,16 +1053,52 @@ app.get('/.well-known/jwks.json', (_req, res) => {
}
});


/**
* @openapi
* /.well-known/stellar.toml:
* get:
* tags:
* - v1
* description: GET /.well-known/stellar.toml
* responses:
* 200:
* description: Success
*/
app.get('/.well-known/stellar.toml', (_req, res) => {
res.header("Access-Control-Allow-Origin", "*");
res.setHeader('Content-Type', 'text/plain');
res.send(`FEDERATION_SERVER="${process.env.FEDERATION_SERVER_URL || `https://${process.env.STELLAR_TAG_DOMAIN}/federation`}"\n`);
});


/**
* @openapi
* /api/v1/time:
* get:
* tags:
* - v1
* description: GET /api/v1/time
* responses:
* 200:
* description: Success
*/
app.get('/api/v1/time', (_req, res) => {
res.status(200).json({ time: new Date().toISOString() });
});


/**
* @openapi
* /health:
* get:
* tags:
* - v1
* description: GET /health
* responses:
* 200:
* description: Success
*/
app.get('/health', async (_req, res) => {
const checks = { database: null, redis: null };
let allOk = true;
Expand Down Expand Up @@ -1003,6 +1133,18 @@ app.get('/health', async (_req, res) => {
}
});


/**
* @openapi
* /health:
* get:
* tags:
* - v1
* description: GET /health
* responses:
* 200:
* description: Success
*/
app.get('/health', async (req, res) => {
try {
await prisma.$queryRaw`SELECT 1`;
Expand Down
42 changes: 39 additions & 3 deletions stellar-payment-platform/src/routes/v1/adminRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,19 @@ module.exports = (redisClient) => {
// Streams all payment records as CSV (default) or NDJSON.
// Supports optional startDate / endDate query params for filtering.
// Paginates internally using cursor-based pages so memory stays bounded.
router.get('/admin/export', adminAuth, asyncHandler(async (req, res, next) => {

/**
* @openapi
* /admin/export:
* get:
* tags:
* - v1
* description: GET /admin/export
* responses:
* 200:
* description: Success
*/
router.get('/admin/export', adminAuth, asyncHandler(async (req, res, next) => {
const { format = 'csv', startDate, endDate } = req.query;

// Validate date range when provided
Expand Down Expand Up @@ -115,7 +127,19 @@ module.exports = (redisClient) => {
}
}));

router.post('/admin/block', adminAuth, asyncHandler(async (req, res, next) => {

/**
* @openapi
* /admin/block:
* post:
* tags:
* - v1
* description: POST /admin/block
* responses:
* 200:
* description: Success
*/
router.post('/admin/block', adminAuth, asyncHandler(async (req, res, next) => {
const prisma = getPrisma();
const { address } = req.body;

Expand Down Expand Up @@ -154,7 +178,19 @@ module.exports = (redisClient) => {
* Query parameters:
* - limit (optional) integer between 1 and 100, default 50
*/
router.get(

/**
* @openapi
* /admin/audit-logs:
* get:
* tags:
* - v1
* description: GET /admin/audit-logs
* responses:
* 200:
* description: Success
*/
router.get(
'/admin/audit-logs',
adminAuth,
asyncHandler(async (req, res) => {
Expand Down
56 changes: 52 additions & 4 deletions stellar-payment-platform/src/routes/v1/apiKeyRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,19 @@ module.exports = (redisClient) => {

// POST /auth/api-keys
// Generate a new API key
router.post('/', requireAuth, requireJson, validateSchema({ body: createApiKeyBodySchema }), asyncHandler(async (req, res, next) => {

/**
* @openapi
* /:
* post:
* tags:
* - v1
* description: POST /
* responses:
* 200:
* description: Success
*/
router.post('/', requireAuth, requireJson, validateSchema({ body: createApiKeyBodySchema }), asyncHandler(async (req, res, next) => {
try {
const { name, owner_id, scopes: scopesStr, expires_in_hours } = req.body;

Expand Down Expand Up @@ -102,7 +114,19 @@ module.exports = (redisClient) => {

// GET /auth/api-keys
// List API keys for an owner
router.get('/', requireAuth, asyncHandler(async (req, res, next) => {

/**
* @openapi
* /:
* get:
* tags:
* - v1
* description: GET /
* responses:
* 200:
* description: Success
*/
router.get('/', requireAuth, asyncHandler(async (req, res, next) => {
try {
const ownerId = req.query.owner_id;
if (!ownerId) {
Expand Down Expand Up @@ -134,7 +158,19 @@ module.exports = (redisClient) => {

// POST /auth/api-keys/:id/revoke
// Revoke a specific API key
router.post('/:id/revoke', requireAuth, requireJson, validateSchema({ body: revokeApiKeyBodySchema }), asyncHandler(async (req, res, next) => {

/**
* @openapi
* /:id/revoke:
* post:
* tags:
* - v1
* description: POST /:id/revoke
* responses:
* 200:
* description: Success
*/
router.post('/:id/revoke', requireAuth, requireJson, validateSchema({ body: revokeApiKeyBodySchema }), asyncHandler(async (req, res, next) => {
try {
const { id } = req.params;
const { revoked_by } = req.body;
Expand Down Expand Up @@ -175,7 +211,19 @@ module.exports = (redisClient) => {

// POST /auth/api-keys/:id/rotate
// Rotate an API key: generate new key, revoke old one with grace period
router.post('/:id/rotate', requireAuth, requireJson, validateSchema({ body: rotateApiKeyBodySchema }), asyncHandler(async (req, res, next) => {

/**
* @openapi
* /:id/rotate:
* post:
* tags:
* - v1
* description: POST /:id/rotate
* responses:
* 200:
* description: Success
*/
router.post('/:id/rotate', requireAuth, requireJson, validateSchema({ body: rotateApiKeyBodySchema }), asyncHandler(async (req, res, next) => {
try {
const { id } = req.params;
const { name, grace_period_hours = 1 } = req.body;
Expand Down
Loading