Function bodies 642 total
POST function · typescript · L6-L26 (21 LOC)app/api/init-db/route.ts
export async function POST(request: Request) {
if (!isAuthorized(request)) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
await initializeDatabase();
await initializeForecastTables();
await initializePriceCacheTable();
return NextResponse.json({
success: true,
message: 'Database initialized successfully (including forecast + price cache tables)'
});
} catch (error) {
console.error('Database initialization error:', error);
return NextResponse.json(
{ success: false, error: 'Failed to initialize database' },
{ status: 500 }
);
}
}GET function · typescript · L29-L49 (21 LOC)app/api/init-db/route.ts
export async function GET(request: Request) {
if (!isAuthorized(request)) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
await initializeDatabase();
await initializeForecastTables();
await initializePriceCacheTable();
return NextResponse.json({
success: true,
message: 'Database is ready'
});
} catch (error) {
console.error('Database check error:', error);
return NextResponse.json(
{ success: false, error: 'Database not ready' },
{ status: 500 }
);
}
}fetchYTDData function · typescript · L30-L35 (6 LOC)app/api/market-comparison/route.ts
async function fetchYTDData(symbol: string): Promise<{
price: number;
ytdStartPrice: number;
prevClose: number;
marketState: string;
} | null> {GET function · typescript · L106-L172 (67 LOC)app/api/market-comparison/route.ts
export async function GET() {
try {
const results = await Promise.all(
TICKERS.map(async (t) => {
const data = await fetchYTDData(t.symbol);
if (!data) return null;
const ytdChangePercent = ((data.price - data.ytdStartPrice) / data.ytdStartPrice) * 100;
const dailyChange = data.price - data.prevClose;
const dailyChangePercent = data.prevClose > 0 ? (dailyChange / data.prevClose) * 100 : 0;
return {
...t,
price: Math.round(data.price * 100) / 100,
ytdStartPrice: Math.round(data.ytdStartPrice * 100) / 100,
ytdChangePercent: Math.round(ytdChangePercent * 100) / 100,
dailyChange: Math.round(dailyChange * 100) / 100,
dailyChangePercent: Math.round(dailyChangePercent * 100) / 100,
marketState: data.marketState,
} as TickerData & { group: string };
})
);
const valid = results.filter(Boolean) as (TickerData & { group: string })[];
constGET function · typescript · L8-L54 (47 LOC)app/api/metals/[metal]/history/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ metal: string }> }
) {
try {
const { metal } = await params;
// Parse query params for days
const url = new URL(request.url);
const daysParam = url.searchParams.get('days');
const days = daysParam ? parseInt(daysParam, 10) : 90;
if (isNaN(days) || days < 1 || days > 365) {
return NextResponse.json(
{ success: false, error: 'Invalid days parameter (1-365)' },
{ status: 400 }
);
}
const history = await getMetalHistory(metal, days);
if (history.length === 0) {
return NextResponse.json(
{ success: false, error: `No history found for ${metal}` },
{ status: 404 }
);
}
return NextResponse.json({
success: true,
metal,
days,
data: history.map(h => ({
date: String(h.report_date).slice(0, 10),
registered: Number(h.registered),
eligible: Number(h.eligible),
GET function · typescript · L8-L37 (30 LOC)app/api/metals/route.ts
export async function GET() {
try {
const data = await getWarehouseDataWithChanges();
// Check if we have data
const snapshots = await getLatestSnapshots();
if (snapshots.length === 0) {
return NextResponse.json(
{
success: false,
error: 'No data available. Please run the data sync first.',
data: null
},
{ status: 404 }
);
}
return NextResponse.json({
success: true,
data,
lastUpdated: new Date().toISOString()
});
} catch (error) {
console.error('Error fetching metals data:', error);
return NextResponse.json(
{ success: false, error: 'Failed to fetch metals data' },
{ status: 500 }
);
}
}POST function · typescript · L26-L104 (79 LOC)app/api/metals/sync/route.ts
export async function POST(request: Request) {
if (!isAuthorized(request)) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
// Ensure database tables exist
await initializeDatabase();
const body = await request.json();
// Validate the data structure
if (!body || typeof body !== 'object') {
return NextResponse.json(
{ success: false, error: 'Invalid data format' },
{ status: 400 }
);
}
// Whitelist of valid metal keys
const VALID_METALS = ['Gold', 'Silver', 'Copper', 'Aluminum', 'Zinc', 'Lead', 'Platinum_Palladium'];
const results: { metal: string; success: boolean; error?: string }[] = [];
// Process each metal in the payload
for (const [metalKey, metalData] of Object.entries(body)) {
if (!VALID_METALS.includes(metalKey)) {
results.push({ metal: metalKey, success: false, error: 'Invalid metal' });
continue;
}
try {
const daPowered by Repobility — scan your code at https://repobility.com
getResend function · typescript · L13-L15 (3 LOC)app/api/newsletter/send/route.ts
function getResend() {
return new Resend(process.env.RESEND_API_KEY);
}POST function · typescript · L17-L115 (99 LOC)app/api/newsletter/send/route.ts
export async function POST(request: Request) {
if (!isAuthorized(request)) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
if (!isDatabaseAvailable()) {
return NextResponse.json({ error: 'Database unavailable' }, { status: 503 });
}
try {
// ─── Step 1: Generate the newsletter via the analysis engine ───
const newsletter = await generateNewsletter();
if (!newsletter) {
return NextResponse.json({ error: 'Analysis data not available — cannot generate newsletter' }, { status: 500 });
}
// ─── Step 2: Store the generated newsletter in the DB ───
try {
await saveNewsletter(
newsletter.reportDate,
newsletter.subject,
newsletter.html,
newsletter.metalsAnalyzed,
newsletter.avgRiskScore,
);
console.log(`Newsletter for ${newsletter.reportDate} saved to DB`);
} catch (err) {
console.error('Failed to save newsletter to DB (continuing with send):', err);
buildTrialExpiredEmail function · typescript · L119-L200 (82 LOC)app/api/newsletter/send/route.ts
function buildTrialExpiredEmail(_email: string, unsubscribeUrl: string): string {
const checkoutUrl = process.env.STRIPE_CHECKOUT_URL || 'https://buy.stripe.com/fZucN6fAj1e9fwugMpfw402';
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin:0;padding:0;background-color:#0f172a;font-family:'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color:#0f172a;padding:40px 20px;">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background-color:#1e293b;border-radius:12px;overflow:hidden;">
<!-- Header -->
<tr>
<td style="background:linear-gradient(135deg,#f59e0b,#d97706);padding:32px 40px;text-align:center;">
<h1 style="margin:0;font-size:24px;color:#0f172a;font-weight:800;letter-spacing:-0.5px;">
getResend function · typescript · L7-L9 (3 LOC)app/api/newsletter/subscribe/route.ts
function getResend() {
return new Resend(process.env.RESEND_API_KEY);
}isValidEmail function · typescript · L12-L15 (4 LOC)app/api/newsletter/subscribe/route.ts
function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}POST function · typescript · L17-L81 (65 LOC)app/api/newsletter/subscribe/route.ts
export async function POST(request: Request) {
// Rate limit: 3 subscribe attempts per IP per 15 minutes
const ip = getClientIp(request);
const { limited } = rateLimit(`subscribe:${ip}`, 3, 15 * 60 * 1000);
if (limited) {
return NextResponse.json(
{ success: false, error: 'Too many attempts. Please try again later.' },
{ status: 429 },
);
}
try {
const body = await request.json();
const { email } = body;
if (!email || !isValidEmail(email)) {
return NextResponse.json(
{ success: false, error: 'Please enter a valid email address.' },
{ status: 400 }
);
}
if (!isDatabaseAvailable()) {
return NextResponse.json(
{ success: false, error: 'Service temporarily unavailable. Please try again later.' },
{ status: 503 }
);
}
// Check if already subscribed
const alreadySubscribed = await isSubscribed(email);
if (alreadySubscribed) {
return NextResponse.json(
getWelcomeEmailHtml function · typescript · L83-L166 (84 LOC)app/api/newsletter/subscribe/route.ts
function getWelcomeEmailHtml(unsubscribeUrl: string): string {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin:0;padding:0;background-color:#0f172a;font-family:'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color:#0f172a;padding:40px 20px;">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background-color:#1e293b;border-radius:12px;overflow:hidden;">
<!-- Header -->
<tr>
<td style="background:linear-gradient(135deg,#f59e0b,#d97706);padding:32px 40px;text-align:center;">
<h1 style="margin:0;font-size:24px;color:#0f172a;font-weight:800;letter-spacing:-0.5px;">
HEAVY METAL STATS
</h1>
<p style="margin:8px 0 0;font-size:14px;color:#451a03;font-weight:GET function · typescript · L4-L43 (40 LOC)app/api/newsletter/unsubscribe/route.ts
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const token = searchParams.get('token');
if (!token) {
return new NextResponse(getUnsubscribeHtml(false, 'Invalid unsubscribe link.'), {
status: 400,
headers: { 'Content-Type': 'text/html' },
});
}
if (!isDatabaseAvailable()) {
return new NextResponse(getUnsubscribeHtml(false, 'Service temporarily unavailable. Please try again later.'), {
status: 503,
headers: { 'Content-Type': 'text/html' },
});
}
try {
const removed = await removeSubscriber(token);
if (removed) {
return new NextResponse(getUnsubscribeHtml(true, 'You have been successfully unsubscribed from the Heavy Metal Stats daily bulletin.'), {
status: 200,
headers: { 'Content-Type': 'text/html' },
});
} else {
return new NextResponse(getUnsubscribeHtml(false, 'This link has already been used or is no longer valid.'), {
status: Repobility — the code-quality scanner for AI-generated software · https://repobility.com
getUnsubscribeHtml function · typescript · L45-L121 (77 LOC)app/api/newsletter/unsubscribe/route.ts
function getUnsubscribeHtml(success: boolean, message: string): string {
const icon = success ? '✓' : '✗';
const iconColor = success ? '#22c55e' : '#ef4444';
const title = success ? 'Unsubscribed' : 'Error';
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title} — Heavy Metal Stats</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #0f172a;
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
padding: 20px;
}
.card {
background: #1e293b;
border-radius: 12px;
padding: 48px 40px;
max-width: 480px;
text-align: center;
}
.icon {
width: 64px;
height: 64px;
border-radius: 50%;
display: flex;
align-items: GET function · typescript · L8-L46 (39 LOC)app/api/paper-physical/[metal]/history/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ metal: string }> }
) {
try {
if (!isDatabaseAvailable()) {
return NextResponse.json(
{ success: false, error: 'Database not configured' },
{ status: 503 }
);
}
const { metal } = await params;
const { searchParams } = new URL(request.url);
const days = parseInt(searchParams.get('days') || '90', 10);
const history = await getPaperPhysicalHistory(metal, days);
return NextResponse.json({
success: true,
metal,
days,
data: history.map(h => ({
date: h.report_date,
openInterest: Number(h.open_interest),
openInterestUnits: Number(h.open_interest_units),
registeredInventory: Number(h.registered_inventory),
paperPhysicalRatio: Number(h.paper_physical_ratio),
riskLevel: h.risk_level,
})),
});
} catch (error) {
console.error('Error fetching paper/physical history:', erGET function · typescript · L12-L83 (72 LOC)app/api/paper-physical/route.ts
export async function GET() {
try {
if (!isDatabaseAvailable()) {
return NextResponse.json(
{ success: false, error: 'Database not configured' },
{ status: 503 }
);
}
const [paperPhysicalRatios, openInterestData] = await Promise.all([
getLatestPaperPhysicalRatios(),
getLatestOpenInterest(),
]);
// Format the response
const ratiosByMetal: Record<string, {
metal: string;
reportDate: string;
futuresSymbol: string;
openInterest: number;
openInterestUnits: number;
registeredInventory: number;
paperPhysicalRatio: number;
riskLevel: string;
}> = {};
for (const ratio of paperPhysicalRatios) {
ratiosByMetal[ratio.metal] = {
metal: ratio.metal,
reportDate: ratio.report_date,
futuresSymbol: ratio.futures_symbol,
openInterest: Number(ratio.open_interest),
openInterestUnits: Number(ratio.open_interest_units),
registeredInvgetRiskLevel function · typescript · L21-L26 (6 LOC)app/api/paper-physical/sync/route.ts
function getRiskLevel(ratio: number): string {
if (ratio <= 2) return 'LOW';
if (ratio <= 5) return 'MODERATE';
if (ratio <= 10) return 'HIGH';
return 'EXTREME';
}POST function · typescript · L29-L151 (123 LOC)app/api/paper-physical/sync/route.ts
export async function POST(request: Request) {
if (!isAuthorized(request)) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
if (!isDatabaseAvailable()) {
return NextResponse.json(
{ success: false, error: 'Database not configured' },
{ status: 503 }
);
}
// Initialize tables if they don't exist
await initializeOpenInterestTables();
// Read volume summary data (contains open interest)
const volumeSummaryPath = path.join(process.cwd(), 'public', 'volume_summary.json');
const volumeSummaryRaw = await fs.readFile(volumeSummaryPath, 'utf-8');
const volumeSummary = JSON.parse(volumeSummaryRaw);
// Read inventory data (contains registered inventory)
const dataPath = path.join(process.cwd(), 'public', 'data.json');
const dataRaw = await fs.readFile(dataPath, 'utf-8');
const inventoryData = JSON.parse(dataRaw);
const reportDate = volumeSummary.parsed_date || new Date().tGET function · typescript · L154-L178 (25 LOC)app/api/paper-physical/sync/route.ts
export async function GET() {
try {
if (!isDatabaseAvailable()) {
return NextResponse.json(
{ success: false, error: 'Database not configured' },
{ status: 503 }
);
}
// Initialize tables to ensure they exist
await initializeOpenInterestTables();
return NextResponse.json({
success: true,
message: 'Paper/physical tables ready',
tables: ['open_interest_snapshots', 'paper_physical_snapshots'],
});
} catch (error) {
console.error('Paper/physical status error:', error);
return NextResponse.json(
{ success: false, error: 'Failed to check paper/physical status' },
{ status: 500 }
);
}
}GET function · typescript · L6-L37 (32 LOC)app/api/prices/route.ts
export async function GET() {
try {
if (!isDatabaseAvailable()) {
return NextResponse.json(
{ success: false, error: 'Database not available' },
{ status: 503 },
);
}
const cached = await getCachedPrices('spot');
if (!cached) {
return NextResponse.json(
{ success: false, error: 'No cached data yet — cron has not run' },
{ status: 503 },
);
}
const { prices, fetched_at } = cached.data as { prices: Record<string, unknown>; fetched_at: string };
return NextResponse.json(
{ success: true, prices, fetched_at },
{
headers: {
'Cache-Control': 'public, s-maxage=10, stale-while-revalidate=60',
},
},
);
} catch (error: unknown) {
const msg = error instanceof Error ? error.message : 'Unknown error';
return NextResponse.json({ success: false, error: msg }, { status: 500 });
}
}GET function · typescript · L7-L53 (47 LOC)app/api/risk-score/[metal]/history/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ metal: string }> }
) {
try {
const { metal } = await params;
if (!isDatabaseAvailable()) {
return NextResponse.json(
{ error: 'Database not configured' },
{ status: 503 }
);
}
const { searchParams } = new URL(request.url);
const days = parseInt(searchParams.get('days') || '90', 10);
const history = await getRiskScoreHistory(metal, days);
return NextResponse.json({
success: true,
metal,
days,
count: history.length,
history: history.map(h => ({
date: h.report_date,
composite: h.composite_score,
level: h.risk_level,
breakdown: {
coverageRisk: h.coverage_risk,
paperPhysicalRisk: h.paper_physical_risk,
inventoryTrendRisk: h.inventory_trend_risk,
deliveryVelocityRisk: h.delivery_velocity_risk,
marketActivityRisk: h.market_activity_risk,
Repobility · severity-and-effort ranking · https://repobility.com
GET function · typescript · L7-L63 (57 LOC)app/api/risk-score/route.ts
export async function GET() {
try {
if (!isDatabaseAvailable()) {
return NextResponse.json(
{ error: 'Database not configured' },
{ status: 503 }
);
}
const riskScores = await getLatestRiskScores();
// Transform to a more usable format
const scores: Record<string, {
composite: number;
level: string;
breakdown: {
coverageRisk: number;
paperPhysicalRisk: number;
inventoryTrendRisk: number;
deliveryVelocityRisk: number;
marketActivityRisk: number;
};
dominantFactor: string;
commentary: string;
reportDate: string;
}> = {};
for (const score of riskScores) {
scores[score.metal] = {
composite: score.composite_score,
level: score.risk_level,
breakdown: {
coverageRisk: score.coverage_risk,
paperPhysicalRisk: score.paper_physical_risk,
inventoryTrendRisk: score.inventory_trend_risk,
delPOST function · typescript · L36-L147 (112 LOC)app/api/risk-score/sync/route.ts
export async function POST(request: Request) {
if (!isAuthorized(request)) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
if (!isDatabaseAvailable()) {
return NextResponse.json(
{ error: 'Database not configured' },
{ status: 503 }
);
}
// Initialize tables
await initializeRiskScoreTables();
// Read data files
const publicDir = path.join(process.cwd(), 'public');
const dataJson = await fs.readFile(path.join(publicDir, 'data.json'), 'utf-8');
const warehouseData: WarehouseStocksData = JSON.parse(dataJson);
const volumeJson = await fs.readFile(path.join(publicDir, 'volume_summary.json'), 'utf-8');
const volumeData: VolumeSummaryData = JSON.parse(volumeJson);
const reportDate = volumeData.date || new Date().toISOString().split('T')[0];
const results: Array<{ metal: string; score: number; level: string }> = [];
// Calculate and store risk scores for eachgetStripe function · typescript · L5-L9 (5 LOC)app/api/stripe/api-checkout/route.ts
function getStripe() {
return new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2026-01-28.clover',
});
}GET function · typescript · L12-L49 (38 LOC)app/api/stripe/api-checkout/route.ts
export async function GET() {
try {
const session = await auth();
if (!session?.user?.id || !session.user.email) {
return NextResponse.redirect(new URL('/auth/login', 'https://heavymetalstats.com'));
}
const priceId = process.env.STRIPE_API_PRICE_ID;
if (!priceId) {
console.error('STRIPE_API_PRICE_ID not set');
return NextResponse.json({ error: 'Payment configuration error' }, { status: 500 });
}
const stripe = getStripe();
const checkoutSession = await stripe.checkout.sessions.create({
mode: 'subscription',
payment_method_types: ['card'],
customer_email: session.user.email.toLowerCase().trim(),
line_items: [{ price: priceId, quantity: 1 }],
success_url: 'https://heavymetalstats.com/developer?upgraded=true',
cancel_url: 'https://heavymetalstats.com/developer?upgraded=cancelled',
metadata: {
product_type: 'api',
user_id: session.user.id,
},
});
if (checkoutSessgetStripe function · typescript · L6-L10 (5 LOC)app/api/stripe/checkout-redirect/route.ts
function getStripe() {
return new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2026-01-28.clover',
});
}isValidEmail function · typescript · L13-L15 (3 LOC)app/api/stripe/checkout-redirect/route.ts
function isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}GET function · typescript · L18-L79 (62 LOC)app/api/stripe/checkout-redirect/route.ts
export async function GET(request: Request) {
// Rate limit: 10 checkout attempts per IP per 15 minutes
const ip = getClientIp(request);
const { limited } = rateLimit(`checkout:${ip}`, 10, 15 * 60 * 1000);
if (limited) {
return NextResponse.redirect('https://heavymetalstats.com?error=rate-limited');
}
const stripe = getStripe();
const { searchParams } = new URL(request.url);
const email = searchParams.get('email');
if (!email || !isValidEmail(email)) {
return NextResponse.redirect('https://heavymetalstats.com?error=missing-email');
}
if (!isDatabaseAvailable()) {
return NextResponse.redirect('https://heavymetalstats.com?error=unavailable');
}
try {
const subscriber = await getSubscriberByEmail(email);
if (!subscriber) {
return NextResponse.redirect('https://heavymetalstats.com?error=not-found');
}
if (subscriber.subscription_status === 'paid') {
return NextResponse.redirect('https://heavymetalstats.com?subscribed=algetStripe function · typescript · L5-L9 (5 LOC)app/api/stripe/checkout/route.ts
function getStripe() {
return new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2026-01-28.clover',
});
}Source: Repobility analyzer · https://repobility.com
POST function · typescript · L11-L65 (55 LOC)app/api/stripe/checkout/route.ts
export async function POST(request: Request) {
try {
const stripe = getStripe();
const body = await request.json();
const { email } = body;
if (!email) {
return NextResponse.json({ error: 'Email is required' }, { status: 400 });
}
if (!isDatabaseAvailable()) {
return NextResponse.json({ error: 'Service unavailable' }, { status: 503 });
}
// Verify subscriber exists
const subscriber = await getSubscriberByEmail(email);
if (!subscriber) {
return NextResponse.json({ error: 'No subscription found for this email. Please sign up first.' }, { status: 404 });
}
// If already paid, no need for checkout
if (subscriber.subscription_status === 'paid') {
return NextResponse.json({ error: 'You already have an active subscription.' }, { status: 400 });
}
const priceId = process.env.STRIPE_PRICE_ID;
if (!priceId) {
console.error('STRIPE_PRICE_ID not set');
return NextResponse.json({ error: 'PaymegetStripe function · typescript · L12-L16 (5 LOC)app/api/stripe/webhook/route.ts
function getStripe() {
return new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2026-01-28.clover',
});
}getResend function · typescript · L18-L20 (3 LOC)app/api/stripe/webhook/route.ts
function getResend() {
return new Resend(process.env.RESEND_API_KEY);
}POST function · typescript · L22-L152 (131 LOC)app/api/stripe/webhook/route.ts
export async function POST(request: Request) {
const stripe = getStripe();
const body = await request.text();
const signature = request.headers.get('stripe-signature');
if (!signature) {
return NextResponse.json({ error: 'Missing signature' }, { status: 400 });
}
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
if (!webhookSecret) {
console.error('STRIPE_WEBHOOK_SECRET not set');
return NextResponse.json({ error: 'Webhook not configured' }, { status: 500 });
}
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
} catch (err) {
console.error('Webhook signature verification failed:', err);
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
}
try {
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session;
// Handle API Pro subscription
if (session.metadata?buildPaymentConfirmationEmail function · typescript · L154-L230 (77 LOC)app/api/stripe/webhook/route.ts
function buildPaymentConfirmationEmail(isNew: boolean, unsubscribeUrl: string): string {
const heading = isNew
? 'Welcome to Heavy Metal Stats!'
: 'Payment Confirmed';
const body = isNew
? 'Your subscription is now active. You\'ll receive the Daily Bulletin Analysis every trading day with COMEX risk scores, warehouse data, and market insights delivered to your inbox.'
: 'Your subscription has been renewed. You\'ll continue receiving the Daily Bulletin Analysis every trading day.';
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin:0;padding:0;background-color:#0f172a;font-family:'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color:#0f172a;padding:40px 20px;">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background-GET function · typescript · L7-L52 (46 LOC)app/api/v1/forecast/accuracy/route.ts
export async function GET(request: Request) {
const auth = await validateApiKey(request);
if (!auth) {
return NextResponse.json(
{ success: false, error: 'Invalid or missing API key.' },
{ status: 401 }
);
}
if (auth.tier !== 'paid') {
return NextResponse.json(
{ success: false, error: 'Forecast accuracy data requires a paid API plan.' },
{ status: 403 }
);
}
const rl = await checkApiRateLimit(auth.keyId, auth.tier);
if (rl) {
return NextResponse.json(
{ success: false, error: rl.error },
{ status: 429, headers: rl.retryAfter ? { 'Retry-After': String(rl.retryAfter) } : undefined }
);
}
await trackUsage(auth.keyId);
try {
if (!isDatabaseAvailable()) {
return NextResponse.json({ success: false, error: 'Service unavailable' }, { status: 503 });
}
const { searchParams } = new URL(request.url);
const days = Math.min(parseInt(searchParams.get('days') || '90', 10), 365);
const summGET function · typescript · L10-L74 (65 LOC)app/api/v1/forecast/[metal]/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ metal: string }> }
) {
const auth = await validateApiKey(request);
if (!auth) {
return NextResponse.json(
{ success: false, error: 'Invalid or missing API key.' },
{ status: 401 }
);
}
if (auth.tier !== 'paid') {
return NextResponse.json(
{ success: false, error: 'Per-metal forecast data requires a paid API plan. Use GET /api/v1/forecast for the free tier.' },
{ status: 403 }
);
}
const rl = await checkApiRateLimit(auth.keyId, auth.tier);
if (rl) {
return NextResponse.json(
{ success: false, error: rl.error },
{ status: 429, headers: rl.retryAfter ? { 'Retry-After': String(rl.retryAfter) } : undefined }
);
}
await trackUsage(auth.keyId);
try {
const { metal } = await params;
const normalized = metal.toLowerCase();
if (!VALID_METALS.includes(normalized)) {
return NextResponse.json(
{ success: falseGET function · typescript · L8-L41 (34 LOC)app/api/v1/forecast/route.ts
export async function GET(request: Request) {
const auth = await validateApiKey(request);
if (!auth) {
return NextResponse.json(
{ success: false, error: 'Invalid or missing API key.' },
{ status: 401 }
);
}
const rl = await checkApiRateLimit(auth.keyId, auth.tier);
if (rl) {
return NextResponse.json(
{ success: false, error: rl.error },
{ status: 429, headers: rl.retryAfter ? { 'Retry-After': String(rl.retryAfter) } : undefined }
);
}
await trackUsage(auth.keyId);
try {
const filePath = path.join(process.cwd(), 'public', 'forecast.json');
const raw = await fs.readFile(filePath, 'utf-8');
const data = JSON.parse(raw);
return NextResponse.json({
success: true,
data,
meta: { fetched_at: new Date().toISOString(), tier: auth.tier },
});
} catch (error) {
console.error('[API v1] Error reading forecast data:', error);
return NextResponse.json({ success: false, error: 'Forecast data notPowered by Repobility — scan your code at https://repobility.com
GET function · typescript · L7-L68 (62 LOC)app/api/v1/paper-physical/[metal]/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ metal: string }> }
) {
const auth = await validateApiKey(request);
if (!auth) {
return NextResponse.json(
{ success: false, error: 'Invalid or missing API key.' },
{ status: 401 }
);
}
if (auth.tier !== 'paid') {
return NextResponse.json(
{ success: false, error: 'Historical paper/physical data requires a paid API plan.' },
{ status: 403 }
);
}
const rl = await checkApiRateLimit(auth.keyId, auth.tier);
if (rl) {
return NextResponse.json(
{ success: false, error: rl.error },
{ status: 429, headers: rl.retryAfter ? { 'Retry-After': String(rl.retryAfter) } : undefined }
);
}
await trackUsage(auth.keyId);
try {
if (!isDatabaseAvailable()) {
return NextResponse.json({ success: false, error: 'Service unavailable' }, { status: 503 });
}
const { metal } = await params;
const { searchParams } = new URL(requestGET function · typescript · L7-L56 (50 LOC)app/api/v1/paper-physical/route.ts
export async function GET(request: Request) {
const auth = await validateApiKey(request);
if (!auth) {
return NextResponse.json(
{ success: false, error: 'Invalid or missing API key.' },
{ status: 401 }
);
}
const rl = await checkApiRateLimit(auth.keyId, auth.tier);
if (rl) {
return NextResponse.json(
{ success: false, error: rl.error },
{ status: 429, headers: rl.retryAfter ? { 'Retry-After': String(rl.retryAfter) } : undefined }
);
}
await trackUsage(auth.keyId);
try {
if (!isDatabaseAvailable()) {
return NextResponse.json({ success: false, error: 'Service unavailable' }, { status: 503 });
}
const ratios = await getLatestPaperPhysicalRatios();
const data: Record<string, unknown> = {};
for (const ratio of ratios) {
data[ratio.metal] = {
metal: ratio.metal,
reportDate: ratio.report_date,
futuresSymbol: ratio.futures_symbol,
openInterest: Number(ratio.open_interesGET function · typescript · L7-L74 (68 LOC)app/api/v1/risk-scores/[metal]/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ metal: string }> }
) {
const auth = await validateApiKey(request);
if (!auth) {
return NextResponse.json(
{ success: false, error: 'Invalid or missing API key.' },
{ status: 401 }
);
}
if (auth.tier !== 'paid') {
return NextResponse.json(
{ success: false, error: 'Historical risk score data requires a paid API plan.' },
{ status: 403 }
);
}
const rl = await checkApiRateLimit(auth.keyId, auth.tier);
if (rl) {
return NextResponse.json(
{ success: false, error: rl.error },
{ status: 429, headers: rl.retryAfter ? { 'Retry-After': String(rl.retryAfter) } : undefined }
);
}
await trackUsage(auth.keyId);
try {
if (!isDatabaseAvailable()) {
return NextResponse.json({ success: false, error: 'Service unavailable' }, { status: 503 });
}
const { metal } = await params;
const { searchParams } = new URL(request.urlGET function · typescript · L7-L60 (54 LOC)app/api/v1/risk-scores/route.ts
export async function GET(request: Request) {
const auth = await validateApiKey(request);
if (!auth) {
return NextResponse.json(
{ success: false, error: 'Invalid or missing API key. Pass via Authorization: Bearer hms_... header or ?apikey= parameter.' },
{ status: 401 }
);
}
const rl = await checkApiRateLimit(auth.keyId, auth.tier);
if (rl) {
return NextResponse.json(
{ success: false, error: rl.error },
{ status: 429, headers: rl.retryAfter ? { 'Retry-After': String(rl.retryAfter) } : undefined }
);
}
await trackUsage(auth.keyId);
try {
if (!isDatabaseAvailable()) {
return NextResponse.json({ success: false, error: 'Service unavailable' }, { status: 503 });
}
const riskScores = await getLatestRiskScores();
const scores: Record<string, unknown> = {};
for (const score of riskScores) {
scores[score.metal] = {
composite: score.composite_score,
level: score.risk_level,
breaLoginPage function · typescript · L15-L41 (27 LOC)app/auth/login/page.tsx
export default function LoginPage() {
const router = useRouter();
useEffect(() => {
if (process.env.NODE_ENV === 'production') {
router.replace('/');
}
}, [router]);
if (process.env.NODE_ENV === 'production') {
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-950 flex items-center justify-center">
<Loader2 className="w-6 h-6 text-amber-500 animate-spin" />
</div>
);
}
return (
<Suspense fallback={
<div className="min-h-screen bg-slate-50 dark:bg-slate-950 flex items-center justify-center">
<Loader2 className="w-6 h-6 text-amber-500 animate-spin" />
</div>
}>
<LoginForm />
</Suspense>
);
}LoginForm function · typescript · L43-L225 (183 LOC)app/auth/login/page.tsx
function LoginForm() {
const searchParams = useSearchParams();
const rawCallback = searchParams.get('callbackUrl') || '/discuss';
// Prevent open redirect: only allow relative paths on the same origin
const callbackUrl = rawCallback.startsWith('/') && !rawCallback.startsWith('//') ? rawCallback : '/discuss';
const errorParam = searchParams.get('error');
// Map known error codes to safe messages instead of displaying raw URL params
const ERROR_MESSAGES: Record<string, string> = {
CredentialsSignin: 'Invalid email or password.',
OAuthSignin: 'Could not start sign-in. Please try again.',
OAuthCallback: 'Sign-in callback failed. Please try again.',
OAuthAccountNotLinked: 'This email is already linked to another provider.',
SessionRequired: 'Please sign in to continue.',
Default: 'An authentication error occurred. Please try again.',
};
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoahandleCredentialsLogin function · typescript · L68-L91 (24 LOC)app/auth/login/page.tsx
async function handleCredentialsLogin(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
setError('');
try {
const result = await signIn('credentials', {
email: email.toLowerCase().trim(),
password,
callbackUrl,
redirect: false,
});
if (result?.error) {
setError('Invalid email or password.');
setLoading(false);
} else {
window.location.href = callbackUrl;
}
} catch {
setError('Something went wrong. Please try again.');
setLoading(false);
}
}handleOAuthLogin function · typescript · L93-L96 (4 LOC)app/auth/login/page.tsx
function handleOAuthLogin(providerId: string) {
setOauthLoading(providerId);
signIn(providerId, { callbackUrl });
}Repobility — the code-quality scanner for AI-generated software · https://repobility.com
GoogleIcon function · typescript · L227-L236 (10 LOC)app/auth/login/page.tsx
function GoogleIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" />
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
</svg>
);
}GitHubIcon function · typescript · L238-L244 (7 LOC)app/auth/login/page.tsx
function GitHubIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2z" />
</svg>
);
}DiscordIcon function · typescript · L246-L252 (7 LOC)app/auth/login/page.tsx
function DiscordIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.15