diff --git a/lib/api-auth.ts b/lib/api-auth.ts new file mode 100644 index 0000000..c614543 --- /dev/null +++ b/lib/api-auth.ts @@ -0,0 +1,83 @@ +import sqlite3 from 'sqlite3' +import path from 'path' +import crypto from 'crypto' + +export interface ApiKey { + id: number + key_hash: string + name: string + permissions: string[] + rate_limit: number + is_active: boolean + last_used?: string + created_at: string +} + +export function generateApiKey(): string { + return 'ak_' + crypto.randomBytes(32).toString('hex') +} + +export function hashApiKey(key: string): string { + return crypto.createHash('sha256').update(key).digest('hex') +} + +export async function validateApiKey(key: string): Promise { + if (!key || !key.startsWith('ak_')) return null + + const keyHash = hashApiKey(key) + const dbPath = path.join(process.cwd(), 'database', 'antihoax.db') + const db = new sqlite3.Database(dbPath) + + try { + const apiKey = await new Promise((resolve, reject) => { + db.get( + 'SELECT * FROM api_keys WHERE key_hash = ? AND is_active = 1', + [keyHash], + (err, row: any) => { + if (err) reject(err) + else if (row) { + resolve({ + ...row, + permissions: row.permissions ? JSON.parse(row.permissions) : [] + }) + } else { + resolve(null) + } + } + ) + }) + + if (apiKey) { + // Update last_used timestamp + await new Promise((resolve, reject) => { + db.run( + 'UPDATE api_keys SET last_used = datetime("now") WHERE id = ?', + [apiKey.id], + (err) => { + if (err) reject(err) + else resolve() + } + ) + }) + } + + return apiKey + } catch (error) { + console.error('API key validation error:', error) + return null + } finally { + db.close() + } +} + +export function hasPermission(apiKey: ApiKey, permission: string): boolean { + return apiKey.permissions.includes('*') || apiKey.permissions.includes(permission) +} + +export const ApiPermissions = { + READ_SOURCES: 'sources:read', + WRITE_SOURCES: 'sources:write', + READ_REPORTS: 'reports:read', + WRITE_REPORTS: 'reports:write', + ADMIN: '*' +} as const \ No newline at end of file diff --git a/middleware/auth.ts b/middleware/auth.ts new file mode 100644 index 0000000..a221394 --- /dev/null +++ b/middleware/auth.ts @@ -0,0 +1,48 @@ +import type { NextApiRequest, NextApiResponse } from 'next' +import { validateApiKey, hasPermission, ApiKey } from '../lib/api-auth' + +export interface AuthenticatedRequest extends NextApiRequest { + apiKey?: ApiKey +} + +export function requireAuth(permission?: string) { + return async ( + req: AuthenticatedRequest, + res: NextApiResponse, + next: () => void + ) => { + const apiKeyHeader = req.headers['x-api-key'] as string + + if (!apiKeyHeader) { + return res.status(401).json({ error: 'API key required' }) + } + + const apiKey = await validateApiKey(apiKeyHeader) + + if (!apiKey) { + return res.status(401).json({ error: 'Invalid API key' }) + } + + if (permission && !hasPermission(apiKey, permission)) { + return res.status(403).json({ error: 'Insufficient permissions' }) + } + + req.apiKey = apiKey + next() + } +} + +export function withAuth( + handler: (req: AuthenticatedRequest, res: NextApiResponse) => Promise, + permission?: string +) { + return async (req: AuthenticatedRequest, res: NextApiResponse) => { + const authMiddleware = requireAuth(permission) + + return new Promise((resolve, reject) => { + authMiddleware(req, res, () => { + handler(req, res).then(resolve).catch(reject) + }) + }) + } +} \ No newline at end of file diff --git a/pages/api/admin/api-keys.ts b/pages/api/admin/api-keys.ts new file mode 100644 index 0000000..a5890f9 --- /dev/null +++ b/pages/api/admin/api-keys.ts @@ -0,0 +1,88 @@ +import type { NextApiRequest, NextApiResponse } from "next" +import sqlite3 from "sqlite3" +import path from "path" +import { generateApiKey, hashApiKey } from "../../../lib/api-auth" + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const dbPath = path.join(process.cwd(), "database", "antihoax.db") + const db = new sqlite3.Database(dbPath) + + try { + if (req.method === "GET") { + const keys = await new Promise((resolve, reject) => { + db.all( + `SELECT id, name, permissions, rate_limit, is_active, last_used, created_at + FROM api_keys ORDER BY created_at DESC`, + (err, rows) => { + if (err) reject(err) + else resolve(rows) + } + ) + }) + + res.json({ + keys: keys.map(key => ({ + ...key, + permissions: key.permissions ? JSON.parse(key.permissions) : [], + key_preview: '***...' + (key.id.toString().slice(-4)) + })) + }) + + } else if (req.method === "POST") { + const { name, permissions = [], rate_limit = 1000 } = req.body + + if (!name) { + return res.status(400).json({ error: "Name required" }) + } + + const apiKey = generateApiKey() + const keyHash = hashApiKey(apiKey) + + const result = await new Promise((resolve, reject) => { + db.run( + `INSERT INTO api_keys (key_hash, name, permissions, rate_limit, is_active, created_at) + VALUES (?, ?, ?, ?, 1, datetime('now'))`, + [keyHash, name, JSON.stringify(permissions), rate_limit], + function(err) { + if (err) reject(err) + else resolve({ id: this.lastID }) + } + ) + }) + + res.json({ + success: true, + id: result.id, + api_key: apiKey, // Only returned once during creation + name, + permissions, + rate_limit + }) + + } else if (req.method === "DELETE") { + const { id } = req.query + + await new Promise((resolve, reject) => { + db.run( + 'UPDATE api_keys SET is_active = 0 WHERE id = ?', + [id], + (err) => { + if (err) reject(err) + else resolve() + } + ) + }) + + res.json({ success: true }) + + } else { + res.status(405).json({ error: "Method not allowed" }) + } + + } catch (error) { + console.error('API keys error:', error) + res.status(500).json({ error: "Operation failed" }) + } finally { + db.close() + } +} \ No newline at end of file