Files
skillhub/apps/web/app/api/early-access/route.ts
airano 97b427831a Initial release v1.0.0
Open-source marketplace for AI Agent skills.

Features:
- Next.js 15 web app with i18n (en/fa)
- CLI tool for skill installation (npx skillhub)
- GitHub crawler/indexer with multi-strategy discovery
- Security scanning for all indexed skills
- Self-hostable with Docker

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 06:42:07 +03:30

137 lines
3.9 KiB
TypeScript

import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { createDb, sql } from '@skillhub/db';
// Simple rate limiting
const rateLimitMap = new Map<string, { count: number; timestamp: number }>();
const RATE_LIMIT = 5; // 5 requests per minute
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
function checkRateLimit(ip: string): boolean {
const now = Date.now();
const record = rateLimitMap.get(ip);
if (!record || now - record.timestamp > RATE_LIMIT_WINDOW) {
rateLimitMap.set(ip, { count: 1, timestamp: now });
return true;
}
if (record.count >= RATE_LIMIT) {
return false;
}
record.count++;
return true;
}
export async function POST(request: NextRequest) {
try {
// Rate limiting
const ip = request.headers.get('x-forwarded-for') || 'unknown';
if (!checkRateLimit(ip)) {
return NextResponse.json(
{ error: 'Too many requests. Please try again later.' },
{ status: 429 }
);
}
const body = await request.json();
const { email, variant, locale, source } = body;
// Validate email
if (!email || typeof email !== 'string' || !email.includes('@')) {
return NextResponse.json(
{ error: 'Invalid email address' },
{ status: 400 }
);
}
// Sanitize email
const sanitizedEmail = email.toLowerCase().trim().slice(0, 255);
// Validate variant
const validVariant = variant === 'a' || variant === 'b' ? variant : 'a';
// Store in database
const db = createDb();
// Create table if not exists (for first-time setup)
await db.execute(sql`
CREATE TABLE IF NOT EXISTS early_access_signups (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
variant VARCHAR(1) NOT NULL DEFAULT 'a',
locale VARCHAR(10),
source VARCHAR(100),
ip_address VARCHAR(45),
user_agent TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
)
`);
// Insert or update (upsert)
await db.execute(sql`
INSERT INTO early_access_signups (email, variant, locale, source, ip_address, user_agent)
VALUES (
${sanitizedEmail},
${validVariant},
${locale || null},
${source || 'unknown'},
${ip},
${request.headers.get('user-agent') || null}
)
ON CONFLICT (email) DO UPDATE SET
variant = EXCLUDED.variant,
locale = EXCLUDED.locale,
source = EXCLUDED.source
`);
return NextResponse.json({ success: true });
} catch (error) {
console.error('Early access signup error:', error);
return NextResponse.json(
{ error: 'An error occurred. Please try again.' },
{ status: 500 }
);
}
}
// Get signup stats (for internal use)
export async function GET(request: NextRequest) {
try {
const authHeader = request.headers.get('authorization');
const expectedToken = process.env.ADMIN_API_TOKEN;
if (!expectedToken || authHeader !== `Bearer ${expectedToken}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const db = createDb();
// Get counts by variant
const stats = await db.execute(sql`
SELECT
variant,
COUNT(*) as count,
MAX(created_at) as last_signup
FROM early_access_signups
GROUP BY variant
`) as unknown as { rows: Array<{ variant: string; count: number; last_signup: Date }> };
const totalCount = await db.execute(sql`
SELECT COUNT(*) as total FROM early_access_signups
`) as unknown as { rows: Array<{ total: number }> };
return NextResponse.json({
total: totalCount.rows[0]?.total || 0,
byVariant: stats.rows,
});
} catch (error) {
console.error('Error fetching early access stats:', error);
return NextResponse.json(
{ error: 'Failed to fetch stats' },
{ status: 500 }
);
}
}