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
16 changes: 16 additions & 0 deletions backend/src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> = {},
): void {
const ctx = requestContext.getStore();
logger.info('audit', {
audit: true,
actor,
action,
params,
requestId: ctx?.requestId,
});
}
5 changes: 4 additions & 1 deletion backend/src/middleware/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
});
}
7 changes: 6 additions & 1 deletion backend/src/routes/v1/admin.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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' });
Expand Down Expand Up @@ -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' });
Expand Down
Loading