api key authentication system implementation

This commit is contained in:
2025-04-14 10:18:35 +02:00
parent 9a18e4bffa
commit 8022fceff4
3 changed files with 219 additions and 0 deletions

83
lib/api-auth.ts Normal file
View File

@@ -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<ApiKey | null> {
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<ApiKey | null>((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<void>((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

48
middleware/auth.ts Normal file
View File

@@ -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<void>,
permission?: string
) {
return async (req: AuthenticatedRequest, res: NextApiResponse) => {
const authMiddleware = requireAuth(permission)
return new Promise<void>((resolve, reject) => {
authMiddleware(req, res, () => {
handler(req, res).then(resolve).catch(reject)
})
})
}
}

View File

@@ -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<any[]>((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<any>((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<void>((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()
}
}