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
80 changes: 80 additions & 0 deletions app/api/audit-logs/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* POST /api/audit-logs — create an immutable audit log entry
* GET /api/audit-logs — list audit logs with pagination & filtering
*
* Access: authenticated wallet. Results scoped to caller's contracts.
* Immutability: append-only at both DB and app layer.
*/
import { NextRequest, NextResponse } from 'next/server'
import { withAuth } from '@/lib/auth/middleware'
import { contractAuditLogService, AuditValidationError, AuditForbiddenError } from '@/lib/audit-log'

export const dynamic = 'force-dynamic'

export const POST = withAuth(async (request: NextRequest, auth) => {
let body: unknown
try {
body = await request.json()
} catch {
return NextResponse.json(
{ error: 'Request body must be valid JSON', code: 'INVALID_JSON' },
{ status: 400 }
)
}

try {
const log = await contractAuditLogService.createLog(body, auth.walletAddress)
return NextResponse.json({ log }, { status: 201 })
} catch (err) {
if (err instanceof AuditValidationError) {
return NextResponse.json(
{ error: err.message, code: 'VALIDATION_ERROR' },
{ status: 422 }
)
}
if (err instanceof AuditForbiddenError) {
return NextResponse.json(
{ error: err.message, code: 'FORBIDDEN' },
{ status: 403 }
)
}
console.error('[audit-logs POST]', err)
return NextResponse.json(
{ error: 'Internal server error', code: 'INTERNAL_ERROR' },
{ status: 500 }
)
}
})

export const GET = withAuth(async (request: NextRequest, _auth) => {
const { searchParams } = request.nextUrl

const query: Record<string, string | undefined> = {}
for (const key of ['limit', 'offset', 'contractId', 'projectId', 'action', 'actorUserId', 'fromDate', 'toDate']) {
const val = searchParams.get(key)
if (val !== null) query[key] = val
}

try {
const page = await contractAuditLogService.listLogs(query, request)
return NextResponse.json(page, { status: 200 })
} catch (err) {
if (err instanceof AuditValidationError) {
return NextResponse.json(
{ error: err.message, code: 'VALIDATION_ERROR' },
{ status: 422 }
)
}
if (err instanceof AuditForbiddenError) {
return NextResponse.json(
{ error: err.message, code: 'FORBIDDEN' },
{ status: 403 }
)
}
console.error('[audit-logs GET]', err)
return NextResponse.json(
{ error: 'Internal server error', code: 'INTERNAL_ERROR' },
{ status: 500 }
)
}
})
12 changes: 12 additions & 0 deletions app/api/contracts/deploy/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { withAuth } from '@/lib/auth/middleware'
import { sql } from '@/lib/db'
import { deploySorobanEscrow, SorobanDeployError } from '@/lib/soroban/deploy'
import { activityService } from '@/lib/activity'
import { contractAuditLogService } from '@/lib/audit-log'
import {
createContract,
createMilestones,
Expand Down Expand Up @@ -182,6 +183,17 @@ export const POST = withAuth(async (request: NextRequest, auth) => {
freelancerId: body.freelancerId,
},
}).catch((err: unknown) => console.error('[activity] Failed to log contract_created:', err))

// Audit log: contract_creation
contractAuditLogService.createLog({
contractId: contract.id,
projectId: Number(job.id),
action: 'contract_creation',
actorUserId: Number(actorId),
actorWallet: auth.walletAddress,
newState: 'active',
metadata: { totalAmount: body.totalAmount, currency, milestonesCount: milestones.length },
}, auth.walletAddress).catch((err: unknown) => console.error('[audit-log] Failed to log contract_creation:', err))
}

return NextResponse.json(
Expand Down
15 changes: 15 additions & 0 deletions app/api/milestones/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { withAuth } from '@/lib/auth/middleware'
import { sql } from '@/lib/db'
import { CreateMilestoneSchema } from '@/lib/validations'
import { activityService } from '@/lib/activity'
import { contractAuditLogService } from '@/lib/audit-log'

export const POST = withAuth(async (request: NextRequest, auth) => {
let body: unknown
Expand Down Expand Up @@ -60,6 +61,20 @@ export const POST = withAuth(async (request: NextRequest, auth) => {
metadata: { amount, currency, sort_order },
}).catch((err: unknown) => console.error('[activity] Failed to log milestone_created:', err))

// Audit log: milestone_creation (only if milestone is linked to a contract)
if (milestone.contract_id) {
contractAuditLogService.createLog({
contractId: milestone.contract_id,
projectId: project_id,
action: 'milestone_creation',
actorUserId: user.id,
actorWallet: auth.walletAddress,
milestoneId: milestone.id,
newState: 'pending',
metadata: { title, amount, currency, sort_order },
}, auth.walletAddress).catch((err: unknown) => console.error('[audit-log] Failed to log milestone_creation:', err))
}

return NextResponse.json({ milestone }, { status: 201 })
} catch {
return NextResponse.json({ error: 'Failed to create milestone', code: 'MILESTONE_CREATE_FAILED' }, { status: 500 })
Expand Down
Loading