@@ -7,7 +7,6 @@ import { getErrorMessage } from '../utils/helpers.js'
77
88export const adminRouter = Router ( )
99
10- // Predefined safe queries for EXPLAIN ANALYZE
1110const PREDEFINED_QUERIES : Record < string , string > = {
1211 'get_all_portfolios' : 'SELECT * FROM portfolios ORDER BY created_at DESC' ,
1312 'get_portfolio_count' : 'SELECT COUNT(*) as cnt FROM portfolios' ,
@@ -21,28 +20,14 @@ const PREDEFINED_QUERIES: Record<string, string> = {
2120 'get_portfolio_drafts' : 'SELECT * FROM portfolio_drafts WHERE user_address = ?'
2221}
2322
24- /**
25- * POST /api/v1/admin/db/explain
26- *
27- * Accepts a named query identifier and returns EXPLAIN ANALYZE output.
28- * Restricted to admin only.
29- * Only predefined queries are allowed to prevent SQL injection.
30- *
31- * Request body:
32- * {
33- * "queryId": "get_all_portfolios",
34- * "params": [] // Optional parameters for parameterized queries
35- * }
36- *
37- * Response:
38- * {
39- * "queryId": "get_all_portfolios",
40- * "explainPlan": "...",
41- * "executionTimeMs": 1.23,
42- * "estimatedRows": 100,
43- * "actualRows": 95
44- * }
45- */
23+ function logAdminAction ( actor : string , action : string , target : string | null , before ?: unknown , after ?: unknown ) : void {
24+ try {
25+ databaseService . recordAdminAuditEntry ( actor , action , target , before ?? null , after ?? null )
26+ } catch ( err ) {
27+ logger . warn ( '[ADMIN] Failed to record audit entry' , { error : getErrorMessage ( err ) , action, target } )
28+ }
29+ }
30+
4631adminRouter . post ( '/db/explain' , requireAdmin , async ( req : Request , res : Response ) => {
4732 try {
4833 const { queryId, params = [ ] } = req . body
@@ -56,41 +41,39 @@ adminRouter.post('/db/explain', requireAdmin, async (req: Request, res: Response
5641 return fail ( res , 400 , 'VALIDATION_ERROR' , `Unknown query identifier: ${ queryId } . Available queries: ${ Object . keys ( PREDEFINED_QUERIES ) . join ( ', ' ) } ` )
5742 }
5843
59- // Validate params is an array
6044 if ( ! Array . isArray ( params ) ) {
6145 return fail ( res , 400 , 'VALIDATION_ERROR' , 'params must be an array' )
6246 }
6347
64- logger . info ( '[ADMIN] EXPLAIN ANALYZE requested' , { queryId, adminPublicKey : req . adminPublicKey } )
48+ const actor = req . adminPublicKey ?? 'unknown'
49+ logger . info ( '[ADMIN] EXPLAIN ANALYZE requested' , { queryId, adminPublicKey : actor } )
6550
6651 const db = ( databaseService as any ) . db
6752 if ( ! db ) {
6853 return fail ( res , 500 , 'INTERNAL_ERROR' , 'Database connection not available' )
6954 }
7055
71- // First, run EXPLAIN ANALYZE on the query
7256 const explainQuery = `EXPLAIN ANALYZE ${ query } `
7357 const explainStart = Date . now ( )
7458
7559 try {
7660 const explainResult = db . prepare ( explainQuery ) . all ( ...params )
7761 const explainTimeMs = Date . now ( ) - explainStart
7862
79- // Parse the EXPLAIN ANALYZE output to extract estimated vs actual row counts
8063 const explainPlan = explainResult . map ( ( row : any ) => row . detail || JSON . stringify ( row ) ) . join ( '\n' )
8164
82- // Extract estimated and actual rows from the plan
8365 const estimatedRowsMatch = explainPlan . match ( / r o w s = ( \d + ) / )
8466 const actualRowsMatch = explainPlan . match ( / a c t u a l r o w s = ( \d + ) / )
8567
8668 const estimatedRows = estimatedRowsMatch ? parseInt ( estimatedRowsMatch [ 1 ] , 10 ) : null
8769 const actualRows = actualRowsMatch ? parseInt ( actualRowsMatch [ 1 ] , 10 ) : null
8870
89- // Also run the actual query to get the real row count
9071 const queryStart = Date . now ( )
9172 const actualResult = db . prepare ( query ) . all ( ...params )
9273 const queryTimeMs = Date . now ( ) - queryStart
9374
75+ logAdminAction ( actor , 'db_explain' , queryId , null , { rowCount : actualResult . length } )
76+
9477 return ok ( res , {
9578 queryId,
9679 query,
@@ -111,21 +94,33 @@ adminRouter.post('/db/explain', requireAdmin, async (req: Request, res: Response
11194 }
11295} )
11396
114- /**
115- * GET /api/v1/admin/db/queries
116- *
117- * Returns the list of available predefined query identifiers.
118- * Restricted to admin only.
119- */
120- adminRouter . get ( '/db/queries' , requireAdmin , async ( _req : Request , res : Response ) => {
97+ adminRouter . get ( '/db/queries' , requireAdmin , async ( req : Request , res : Response ) => {
12198 try {
12299 const queries = Object . keys ( PREDEFINED_QUERIES ) . map ( key => ( {
123100 id : key ,
124101 query : PREDEFINED_QUERIES [ key ]
125102 } ) )
103+ logAdminAction ( req . adminPublicKey ?? 'unknown' , 'list_queries' , null )
126104 return ok ( res , { queries } )
127105 } catch ( error ) {
128106 logger . error ( '[ADMIN] Failed to list queries' , { error : getErrorMessage ( error ) } )
129107 return fail ( res , 500 , 'INTERNAL_ERROR' , getErrorMessage ( error ) )
130108 }
131109} )
110+
111+ adminRouter . get ( '/audit-log' , requireAdmin , async ( req : Request , res : Response ) => {
112+ try {
113+ const actor = typeof req . query . actor === 'string' ? req . query . actor : undefined
114+ const action = typeof req . query . action === 'string' ? req . query . action : undefined
115+ const startDate = typeof req . query . startDate === 'string' ? req . query . startDate : undefined
116+ const endDate = typeof req . query . endDate === 'string' ? req . query . endDate : undefined
117+ const limit = req . query . limit ? parseInt ( req . query . limit as string , 10 ) : 50
118+ const offset = req . query . offset ? parseInt ( req . query . offset as string , 10 ) : 0
119+
120+ const result = databaseService . queryAdminAuditLog ( { actor, action, startDate, endDate, limit, offset } )
121+ return ok ( res , result )
122+ } catch ( error ) {
123+ logger . error ( '[ADMIN] Failed to query audit log' , { error : getErrorMessage ( error ) } )
124+ return fail ( res , 500 , 'INTERNAL_ERROR' , getErrorMessage ( error ) )
125+ }
126+ } )
0 commit comments