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