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
38 changes: 38 additions & 0 deletions app/api/auth/login/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Keypair, StrKey } from '@stellar/stellar-sdk';
import { getAndClearNonce } from '@/lib/auth-cache';
import { createSession, getSessionCookieHeader } from '@/lib/session';
import { prisma } from '@/lib/prisma';
import { auditLog, createAuditEvent, extractIp, AuditAction } from '@/lib/audit';

// Force dynamic rendering for this route
export const dynamic = 'force-dynamic';
Expand Down Expand Up @@ -35,6 +36,15 @@ export async function POST(request: NextRequest) {
// Verify nonce exists and matches (atomic read + delete)
const storedNonce = getAndClearNonce(address);
if (!storedNonce || storedNonce !== message) {
// Log failed login attempt
await auditLog(
createAuditEvent(AuditAction.LOGIN_FAIL, 'failure', {
address,
ip: extractIp(request),
error: 'Invalid or expired nonce',
})
);

return NextResponse.json(
{ error: 'Invalid or expired nonce' },
{ status: 401 }
Expand All @@ -50,6 +60,15 @@ export async function POST(request: NextRequest) {
const isValid = keypair.verify(messageBuffer, signatureBuffer);

if (!isValid) {
// Log failed login attempt
await auditLog(
createAuditEvent(AuditAction.LOGIN_FAIL, 'failure', {
address,
ip: extractIp(request),
error: 'Invalid signature',
})
);

return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
Expand Down Expand Up @@ -85,10 +104,29 @@ export async function POST(request: NextRequest) {
getSessionCookieHeader(sealed)
);

// Log successful login
await auditLog(
createAuditEvent(AuditAction.LOGIN_SUCCESS, 'success', {
address,
ip: extractIp(request),
})
);

return response;

} catch (error) {
console.error('Login error:', error);

// Log failed login due to internal error
const body = await request.clone().json().catch(() => ({}));
await auditLog(
createAuditEvent(AuditAction.LOGIN_FAIL, 'failure', {
address: body.address,
ip: extractIp(request),
error: 'Internal server error',
})
);

return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
Expand Down
20 changes: 20 additions & 0 deletions app/api/auth/logout/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,28 @@
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { getSession } from '@/lib/session';
import { auditLog, createAuditEvent, extractIp, AuditAction } from '@/lib/audit';

export async function POST(request: NextRequest) {
// Get user address before clearing session
let address: string | undefined;
try {
const session = await getSession();
address = session?.address;
} catch {
// Session not found or expired
}

const cookieStore = await cookies();
cookieStore.delete('session');

// Log logout event
await auditLog(
createAuditEvent(AuditAction.LOGOUT, 'success', {
address,
ip: extractIp(request),
})
);

return NextResponse.json({ success: true });
}
10 changes: 10 additions & 0 deletions app/api/auth/nonce/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { setNonce } from "@/lib/auth-cache";
import { randomBytes } from "crypto";
import { auditLog, createAuditEvent, extractIp, AuditAction } from '@/lib/audit';

export async function POST(request: NextRequest) {
const { publicKey } = await request.json();
Expand All @@ -19,5 +20,14 @@ export async function POST(request: NextRequest) {
// Store nonce in cache for later verification
setNonce(publicKey, nonce);

// Log nonce request (optional - can be high volume)
// Uncomment if you want to track authentication attempts
await auditLog(
createAuditEvent(AuditAction.NONCE_REQUESTED, 'success', {
address: publicKey,
ip: extractIp(request),
})
);

return NextResponse.json({ nonce });
}
11 changes: 11 additions & 0 deletions app/api/goals/[id]/add/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
validateGoalId
} from '@/lib/validation/savings-goals';
import { ApiSuccessResponse } from '@/lib/types/savings-goals';
import { auditLog, createAuditEvent, extractIp, AuditAction } from '@/lib/audit';

export async function POST(
request: NextRequest,
Expand Down Expand Up @@ -61,6 +62,16 @@ export async function POST(
// Build transaction
const result = await buildAddToGoalTx(publicKey, goalId, amount);

// Log goal funding
await auditLog(
createAuditEvent(AuditAction.GOAL_ADD_FUNDS, 'success', {
address: publicKey,
ip: extractIp(request),
resource: goalId,
metadata: { amount },
})
);

// Return success response
const response: ApiSuccessResponse = {
xdr: result.xdr
Expand Down
10 changes: 10 additions & 0 deletions app/api/goals/[id]/lock/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '@/lib/errors/api-errors';
import { validateGoalId } from '@/lib/validation/savings-goals';
import { ApiSuccessResponse } from '@/lib/types/savings-goals';
import { auditLog, createAuditEvent, extractIp, AuditAction } from '@/lib/audit';

export async function POST(
request: NextRequest,
Expand Down Expand Up @@ -39,6 +40,15 @@ export async function POST(
// Build transaction
const result = await buildLockGoalTx(publicKey, goalId);

// Log goal lock
await auditLog(
createAuditEvent(AuditAction.GOAL_LOCK, 'success', {
address: publicKey,
ip: extractIp(request),
resource: goalId,
})
);

// Return success response
const response: ApiSuccessResponse = {
xdr: result.xdr
Expand Down
10 changes: 10 additions & 0 deletions app/api/goals/[id]/unlock/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '@/lib/errors/api-errors';
import { validateGoalId } from '@/lib/validation/savings-goals';
import { ApiSuccessResponse } from '@/lib/types/savings-goals';
import { auditLog, createAuditEvent, extractIp, AuditAction } from '@/lib/audit';

export async function POST(
request: NextRequest,
Expand Down Expand Up @@ -39,6 +40,15 @@ export async function POST(
// Build transaction
const result = await buildUnlockGoalTx(publicKey, goalId);

// Log goal unlock
await auditLog(
createAuditEvent(AuditAction.GOAL_UNLOCK, 'success', {
address: publicKey,
ip: extractIp(request),
resource: goalId,
})
);

// Return success response
const response: ApiSuccessResponse = {
xdr: result.xdr
Expand Down
11 changes: 11 additions & 0 deletions app/api/goals/[id]/withdraw/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
validateGoalId
} from '@/lib/validation/savings-goals';
import { ApiSuccessResponse } from '@/lib/types/savings-goals';
import { auditLog, createAuditEvent, extractIp, AuditAction } from '@/lib/audit';

export async function POST(
request: NextRequest,
Expand Down Expand Up @@ -61,6 +62,16 @@ export async function POST(
// Build transaction
const result = await buildWithdrawFromGoalTx(publicKey, goalId, amount);

// Log goal withdrawal
await auditLog(
createAuditEvent(AuditAction.GOAL_WITHDRAW, 'success', {
address: publicKey,
ip: extractIp(request),
resource: goalId,
metadata: { amount },
})
);

// Return success response
const response: ApiSuccessResponse = {
xdr: result.xdr
Expand Down
52 changes: 23 additions & 29 deletions app/api/goals/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,7 @@ import {
validateGoalName
} from '@/lib/validation/savings-goals';
import { ApiSuccessResponse } from '@/lib/types/savings-goals';


export async function GET(req: Request) {
try {
const publicKey = req.headers.get("x-public-key");

if (!publicKey) {
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
);
}

const goals = await getAllGoals(publicKey);

return NextResponse.json(goals, { status: 200 });

} catch (error) {
console.error("GET /api/goals error:", error);

return NextResponse.json(
{ error: "Failed to fetch goals" },
{ status: 500 }
);
}
}


import { auditLog, createAuditEvent, extractIp, AuditAction } from '@/lib/audit';


// ===== Goal Interface & Mock Data =====
Expand Down Expand Up @@ -83,6 +56,14 @@ async function getHandler(request: NextRequest, session: string) {
cursor
);

// Log goals list retrieval
await auditLog(
createAuditEvent(AuditAction.GOAL_LIST, 'success', {
address: session,
ip: extractIp(request),
})
);

return NextResponse.json(paginatedResult);
} catch (error) {
console.error('Error fetching goals:', error);
Expand Down Expand Up @@ -131,13 +112,26 @@ async function postHandler(request: NextRequest, session: string) {

const response: ApiSuccessResponse = { xdr: result.xdr };

// Log goal creation
await auditLog(
createAuditEvent(AuditAction.GOAL_CREATE, 'success', {
address: session,
ip: extractIp(request),
metadata: {
name,
targetAmount,
targetDate,
},
})
);

return NextResponse.json(response, { status: 200 });

} catch (error) {
return handleUnexpectedError(error);
}
}

// Remove the old unprotected GET export and use only auth-wrapped versions
export const GET = withAuth(getHandler);

export const POST = withAuth(postHandler);
24 changes: 24 additions & 0 deletions app/api/split/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { withAuth, ApiError } from '@/lib/auth';
import { getSplit } from '@/lib/contracts/remittance-split-cached';
import { auditLog, createAuditEvent, extractIp, AuditAction } from '@/lib/audit';

async function getHandler(request: NextRequest, session: string) {
try {
Expand All @@ -11,6 +12,14 @@ async function getHandler(request: NextRequest, session: string) {
throw new ApiError(404, 'Split configuration not found');
}

// Log split configuration read
await auditLog(
createAuditEvent(AuditAction.SPLIT_GET, 'success', {
address: session,
ip: extractIp(request),
})
);

return NextResponse.json({
percentages: {
savings: config.savings_percent,
Expand All @@ -27,6 +36,21 @@ async function getHandler(request: NextRequest, session: string) {

async function postHandler(request: NextRequest, session: string) {
const body = await request.json();

// Log split configuration update
await auditLog(
createAuditEvent(AuditAction.SPLIT_UPDATE, 'success', {
address: session,
ip: extractIp(request),
metadata: {
savings: body.savings,
bills: body.bills,
insurance: body.insurance,
family: body.family,
},
})
);

// TODO: Call Soroban remittance_split contract to update config
return NextResponse.json({ success: true });
}
Expand Down
20 changes: 19 additions & 1 deletion app/api/v1/bills/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getTranslator } from '../../../../lib/i18n'
import { buildCreateBillTx } from '../../../../lib/contracts/bill-payments'
import { StrKey } from '@stellar/stellar-sdk'
import { ApiRouteError, withApiErrorHandler } from '@/lib/api/error-handler'
import { auditLog, createAuditEvent, extractIp, AuditAction } from '@/lib/audit'

export const POST = withApiErrorHandler(async function POST(req: Request) {
const t = getTranslator(req.headers.get('accept-language'));
Expand All @@ -29,6 +30,23 @@ export const POST = withApiErrorHandler(async function POST(req: Request) {
throw new ApiRouteError(400, 'VALIDATION_ERROR', t('errors.invalid_due_date') || 'Invalid dueDate')
}

const xdr = await buildCreateBillTx(caller, name, numAmount, dueDate, Boolean(recurring), frequencyDays ? Number(frequencyDays) : undefined)
// For non-recurring bills, pass 0 as frequencyDays (it won't be used)
const numFrequencyDays = frequencyDays ? Number(frequencyDays) : 0;
const xdr = await buildCreateBillTx(caller, name, numAmount, dueDate, Boolean(recurring), numFrequencyDays)

// Log bill creation
await auditLog(
createAuditEvent(AuditAction.BILL_CREATE, 'success', {
address: caller,
ip: extractIp(req),
metadata: {
name,
amount: numAmount,
dueDate,
recurring,
},
})
);

return NextResponse.json({ xdr })
})
9 changes: 9 additions & 0 deletions lib/audit/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Audit Logging Module
*
* Centralized exports for audit logging functionality.
*/

export { auditLog, extractIp, createAuditEvent } from './logger';
export { AuditAction, type AuditEvent, type AuditResult, type AuditConfig } from './types';
export { sanitize, sanitizeError, maskAddress, extractSafeMetadata } from './sanitize';
Loading