sync: add AI review pipeline, parallel skill-files fetch, Redis cache improvements
- feat(review): add skill_reviews table, review API endpoints (pending/submit/stats), admin auth - perf: parallel file fetching in skill-files API (was sequential → timeout) - fix: handle Date serialization from Redis cache - fix: align curation batch scripts with current browseReadyFilter Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
46
apps/web/lib/admin-auth.ts
Normal file
46
apps/web/lib/admin-auth.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
import { auth } from '@/lib/auth';
|
||||
import { createDb, userQueries } from '@skillhub/db';
|
||||
|
||||
/**
|
||||
* Check if the current request is from an admin user.
|
||||
* Supports two auth methods:
|
||||
* 1. API key via Authorization: Bearer <REVIEW_API_KEY> header (for automation/scripts)
|
||||
* 2. Session-based admin check (for dashboard)
|
||||
*
|
||||
* Pass the request object to enable API key auth.
|
||||
* Returns the user on success, or a NextResponse error to return early.
|
||||
*/
|
||||
export async function requireAdmin(request?: NextRequest): Promise<
|
||||
| { authorized: true; username: string }
|
||||
| { authorized: false; response: NextResponse }
|
||||
> {
|
||||
// Check API key auth first (for automation/scripting)
|
||||
if (request) {
|
||||
const authHeader = request.headers.get('authorization');
|
||||
const apiKey = process.env.REVIEW_API_KEY;
|
||||
if (apiKey && authHeader === `Bearer ${apiKey}`) {
|
||||
return { authorized: true, username: 'api-key' };
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to session auth
|
||||
const session = await auth();
|
||||
if (!session?.user?.githubId) {
|
||||
return {
|
||||
authorized: false,
|
||||
response: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }),
|
||||
};
|
||||
}
|
||||
|
||||
const db = createDb();
|
||||
const user = await userQueries.getByGithubId(db, session.user.githubId);
|
||||
if (!user?.isAdmin) {
|
||||
return {
|
||||
authorized: false,
|
||||
response: NextResponse.json({ error: 'Admin access required' }, { status: 403 }),
|
||||
};
|
||||
}
|
||||
|
||||
return { authorized: true, username: user.username };
|
||||
}
|
||||
@@ -168,6 +168,7 @@ export const cacheKeys = {
|
||||
skill: (id: string) => `skill:${id.replace(/\//g, ':')}`,
|
||||
skillView: (skillId: string, ip: string) => `view:${skillId.replace(/\//g, ':')}:${ip}`,
|
||||
skillDownload: (skillId: string, ip: string) => `download:${skillId.replace(/\//g, ':')}:${ip}`,
|
||||
reviewStats: () => 'review:stats',
|
||||
};
|
||||
|
||||
// TTL values in seconds
|
||||
@@ -184,6 +185,7 @@ export const cacheTTL = {
|
||||
pageCount: 60 * 60, // 1 hour
|
||||
view: 60 * 60, // 1 hour - same IP can only count as 1 view per hour
|
||||
download: 5 * 60, // 5 minutes - same IP can only count as 1 download per 5 min
|
||||
reviewStats: 60, // 1 minute - admin review stats
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user