From 183afb0ea9c26a13b8690e9c99e7fd568ec871bc Mon Sep 17 00:00:00 2001 From: Banx17 Date: Mon, 29 Jun 2026 19:54:35 +0100 Subject: [PATCH] feat(backend): add structured audit logging for admin and auth actions Emit audit entries with actor, action, params, and requestId for auth success, admin access grant/deny, and successful indexer reset/replay. Keeps logs free of secrets/tokens. Co-authored-by: Cursor --- backend/src/logger.ts | 16 ++++++++++++++++ backend/src/middleware/auth.ts | 5 ++++- backend/src/routes/v1/admin.routes.ts | 7 ++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/backend/src/logger.ts b/backend/src/logger.ts index 565e13f9..939b39f6 100644 --- a/backend/src/logger.ts +++ b/backend/src/logger.ts @@ -18,3 +18,19 @@ const logger = createLogger({ }); export default logger; + +/** Structured audit entry for security-sensitive actions (no secrets/tokens). */ +export function auditLog( + actor: string, + action: string, + params: Record = {}, +): void { + const ctx = requestContext.getStore(); + logger.info('audit', { + audit: true, + actor, + action, + params, + requestId: ctx?.requestId, + }); +} diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts index b9694d12..047ee35d 100644 --- a/backend/src/middleware/auth.ts +++ b/backend/src/middleware/auth.ts @@ -2,7 +2,7 @@ import type { Request, Response, NextFunction } from 'express'; import * as crypto from 'crypto'; import * as StellarSdk from '@stellar/stellar-sdk'; import type { AuthenticatedRequest } from '../types/auth.types.js'; -import logger from '../logger.js'; +import logger, { auditLog } from '../logger.js'; const isProduction = process.env.NODE_ENV === 'production'; const rawJwtSecret = process.env.JWT_SECRET; @@ -181,6 +181,7 @@ export function verifyChallenge(req: Request, res: Response): void { const now = Math.floor(Date.now() / 1000); const token = signJwt({ sub: publicKey, iat: now, exp: now + JWT_EXPIRY_SECONDS }); + auditLog(publicKey, 'auth.verify.success'); res.json({ token, expiresIn: JWT_EXPIRY_SECONDS }); } catch (err) { logger.error('[Auth] verifyChallenge error:', err); @@ -210,9 +211,11 @@ export function requireAdmin(req: Request, res: Response, next: NextFunction): v const user = (req as AuthenticatedRequest).user; const adminKey = process.env.ADMIN_PUBLIC_KEY; if (!adminKey || user.publicKey !== adminKey) { + auditLog(user.publicKey, 'admin.access.denied'); res.status(403).json({ error: 'Forbidden', message: 'Admin access required' }); return; } + auditLog(user.publicKey, 'admin.access.granted'); next(); }); } diff --git a/backend/src/routes/v1/admin.routes.ts b/backend/src/routes/v1/admin.routes.ts index 253f1124..eb2e16ee 100644 --- a/backend/src/routes/v1/admin.routes.ts +++ b/backend/src/routes/v1/admin.routes.ts @@ -11,7 +11,8 @@ import { prisma } from '../../lib/prisma.js'; import { INDEXER_STATE_ID } from '../../lib/indexer-state.js'; import { sseService } from '../../services/sse.service.js'; import { cache } from '../../lib/redis.js'; -import logger from '../../logger.js'; +import type { AuthenticatedRequest } from '../../types/auth.types.js'; +import logger, { auditLog } from '../../logger.js'; const router = Router(); @@ -211,6 +212,8 @@ router.post('/indexer/reset', async (req: Request, res: Response) => { } try { await resetIndexer(ledger); + const actor = (req as AuthenticatedRequest).user.publicKey; + auditLog(actor, 'admin.indexer.reset', { ledger }); res.json({ ok: true, lastLedger: ledger }); } catch (err) { res.status(500).json({ error: 'Reset failed' }); @@ -242,6 +245,8 @@ router.post('/indexer/replay', async (req: Request, res: Response) => { } try { await replayFromLedger(fromLedger); + const actor = (req as AuthenticatedRequest).user.publicKey; + auditLog(actor, 'admin.indexer.replay', { fromLedger }); res.status(202).json({ ok: true, replayingFrom: fromLedger }); } catch (err) { res.status(500).json({ error: 'Replay failed' });