api key authentication system implementation
This commit is contained in:
83
lib/api-auth.ts
Normal file
83
lib/api-auth.ts
Normal 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
48
middleware/auth.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
88
pages/api/admin/api-keys.ts
Normal file
88
pages/api/admin/api-keys.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user