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
2 changes: 2 additions & 0 deletions app/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
DATABASE_URL=XXXXXXXXXXX
# SSL mode: 'prefer' (dev - tries SSL, falls back), 'require' (prod - AWS RDS), 'disable' (local dev)
DB_SSL_MODE=prefer
NEXT_PUBLIC_MICROSERVICE_URL=XXXXXXXXXXX
AWS_REGION=XXXXXXXXXXX
AWS_ACCESS_KEY_ID=XXXXXXXXXXX
Expand Down
24 changes: 17 additions & 7 deletions app/scripts/migrate.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,26 @@ if (!process.env.DATABASE_URL) {
throw new Error("DATABASE_URL is not set");
}

// SSL configuration for AWS RDS
const sslConfig = {
// AWS RDS requires SSL but uses self-signed certificates
// This keeps encryption enabled while accepting AWS certificates
rejectUnauthorized: false,
};
// SSL configuration - only use for production/AWS RDS
// Development: set DB_SSL_MODE=disable for local PostgreSQL without SSL
// Production: set DB_SSL_MODE=require for AWS RDS
const sslMode = process.env.DB_SSL_MODE || "prefer";

let sslConfig = false;
if (sslMode === "require") {
sslConfig = {
rejectUnauthorized: false,
};
} else if (sslMode === "disable") {
sslConfig = false;
} else if (sslMode === "prefer") {
// Use connectionString with sslmode parameter instead
sslConfig = "prefer";
}

const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: sslConfig,
ssl: sslConfig === "prefer" ? false : sslConfig,
});

const db = drizzle(pool);
Expand Down
53 changes: 53 additions & 0 deletions app/src/app/api/metrics/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { NextResponse } from 'next/server';
import { metricsCollector } from '@/lib/metrics';

/**
* GET /api/metrics
* Returns collected frontend metrics in Prometheus format
*/
export async function GET() {
try {
const prometheusFormat = metricsCollector.exportAsPrometheus();
return new NextResponse(prometheusFormat, {
status: 200,
headers: {
'Content-Type': 'text/plain; version=0.0.4; charset=utf-8',
},
});
} catch (error) {
console.error('Error generating frontend metrics:', error);
return NextResponse.json(
{ error: 'Failed to generate metrics' },
{ status: 500 }
);
}
}

/**
* GET /api/metrics/summary
* Returns a JSON summary of frontend metrics
*/
export async function POST(request: Request) {
try {
const { action } = await request.json();

if (action === 'clear') {
metricsCollector.clear();
return NextResponse.json({
message: 'Metrics cleared',
success: true,
});
}

return NextResponse.json(
{ error: 'Unknown action' },
{ status: 400 }
);
} catch (error) {
console.error('Error processing metrics request:', error);
return NextResponse.json(
{ error: 'Failed to process request' },
{ status: 500 }
);
}
}
152 changes: 105 additions & 47 deletions app/src/app/api/profile/summary/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,59 +3,117 @@ import { db } from "@/db";
import { transcriptions, subscriptionTable, userTable } from "@/db/schema";
import { validateRequest } from "@/auth";
import { sql, eq } from "drizzle-orm";
import { createMetricsRecorder } from "@/lib/metrics";

export async function GET() {
const { user } = await validateRequest();
const startTime = Date.now();
const requestId = Math.random().toString(36).substring(7);
const metricsRecorder = createMetricsRecorder("/api/profile/summary", "GET").start();
let statusCode = 200;
let userId: string | undefined;

// Log request
console.log(`\n${'='.repeat(80)}`);
console.log(`[REQUEST ${requestId}] GET /api/profile/summary`);
console.log(`${'='.repeat(80)}`);
console.log(`Timestamp: ${new Date().toISOString()}`);

try {
const { user } = await validateRequest();
userId = user?.id;
console.log(`User ID: ${user?.id || 'Anonymous'}`);

if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
if (!user) {
statusCode = 401;
console.log(`[RESPONSE ${requestId}] Status: 401 Unauthorized`);
console.log(`Duration: ${Date.now() - startTime}ms`);
console.log(`${'='.repeat(80)}\n`);
metricsRecorder.end(statusCode, userId);
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const userId = user.id;
console.log(`Fetching profile summary for user: ${userId}`);

// Count total transcriptions for this user
console.log(`Querying total transcriptions count...`);
const totalResult = await db
.select({ count: sql<number>`COUNT(*)` })
.from(transcriptions)
.where(eq(transcriptions.userID, userId));

const totalRecords = totalResult[0]?.count ?? 0;
console.log(`Total records found: ${totalRecords}`);

const userId = user.id;

// Count total transcriptions for this user
const totalResult = await db
.select({ count: sql<number>`COUNT(*)` })
.from(transcriptions)
.where(eq(transcriptions.userID, userId));

const totalRecords = totalResult[0]?.count ?? 0;

// Count demo/sample recordings (isDefault = true)
const sampleResult = await db
.select({ count: sql<number>`COUNT(*)` })
.from(transcriptions)
.where(eq(transcriptions.isDefault, true));

const sampleCount = sampleResult[0]?.count ?? 0;

const [subscriptionInfo] = await db
.select({
name: subscriptionTable.name,
recordingCount: subscriptionTable.recordingCount,
fileSizeLimitMB: subscriptionTable.fileSizeLimitMB,
durationDays: subscriptionTable.durationDays,
})
.from(subscriptionTable)
.innerJoin(userTable, eq(userTable.subscriptionId, subscriptionTable.id))
.where(eq(userTable.id, userId));
if (!subscriptionInfo) {
// Count demo/sample recordings (isDefault = true)
console.log(`Querying sample/demo recordings count...`);
const sampleResult = await db
.select({ count: sql<number>`COUNT(*)` })
.from(transcriptions)
.where(eq(transcriptions.isDefault, true));

const sampleCount = sampleResult[0]?.count ?? 0;
console.log(`Sample records found: ${sampleCount}`);

console.log(`Fetching subscription information...`);
const [subscriptionInfo] = await db
.select({
name: subscriptionTable.name,
recordingCount: subscriptionTable.recordingCount,
fileSizeLimitMB: subscriptionTable.fileSizeLimitMB,
durationDays: subscriptionTable.durationDays,
})
.from(subscriptionTable)
.innerJoin(userTable, eq(userTable.subscriptionId, subscriptionTable.id))
.where(eq(userTable.id, userId));

if (!subscriptionInfo) {
statusCode = 500;
console.log(`[RESPONSE ${requestId}] Status: 500 - Subscription not found`);
console.log(`Duration: ${Date.now() - startTime}ms`);
console.log(`${'='.repeat(80)}\n`);
metricsRecorder.end(statusCode, userId, "Subscription not found");
return NextResponse.json(
{ error: "Subscription not found for user" },
{ status: 500 }
);
}

const responseData = {
email: user.username,
sampleCount,
totalRecords,
subscription: {
name: subscriptionInfo.name,
limit: subscriptionInfo.recordingCount,
remaining: Math.max(subscriptionInfo.recordingCount - totalRecords, 0),
fileSizeLimitMB: subscriptionInfo.fileSizeLimitMB,
durationDays: subscriptionInfo.durationDays,
},
};

statusCode = 200;
console.log(`[RESPONSE ${requestId}] Status: 200 OK`);
console.log(`Response Data:`, JSON.stringify(responseData, null, 2));
console.log(`Duration: ${Date.now() - startTime}ms`);
console.log(`${'='.repeat(80)}\n`);

metricsRecorder.end(statusCode, userId);
return NextResponse.json(responseData);
} catch (error) {
statusCode = 500;
const duration = Date.now() - startTime;
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`\n[ERROR ${requestId}] Exception occurred after ${duration}ms`);
console.error(`Error Type: ${error instanceof Error ? error.constructor.name : 'Unknown'}`);
console.error(`Error Message: ${errorMessage}`);
console.error(`Stack:`, error instanceof Error ? error.stack : 'N/A');
console.log(`${'='.repeat(80)}\n`);

metricsRecorder.end(statusCode, userId, errorMessage);
return NextResponse.json(
{ error: "Subscription not found for user" },
{ error: "Internal server error" },
{ status: 500 }
);
}

return NextResponse.json({
email: user.username,
sampleCount,
totalRecords,
subscription: {
name: subscriptionInfo.name,
limit: subscriptionInfo.recordingCount,
remaining: Math.max(subscriptionInfo.recordingCount - totalRecords, 0),
fileSizeLimitMB: subscriptionInfo.fileSizeLimitMB,
durationDays: subscriptionInfo.durationDays,
},
});
}
Loading
Loading