Files

157 lines
4.6 KiB
TypeScript
Raw Permalink Normal View History

import { zValidator } from '@hono/zod-validator';
import { createHash, randomBytes } from 'crypto';
import { Hono } from 'hono';
import { z } from 'zod';
import { auth } from '../auth';
import prisma from '../lib/db';
import { handleNotFound } from '../lib/utils';
import { sendWebhook } from '../lib/webhook';
import { authMiddleware } from '../middlewares/auth';
const createApiKeySchema = z.object({
name: z.string().min(1).max(100),
expiresInDays: z.number().int().min(1).max(365).optional(),
});
const deleteApiKeySchema = z.object({
id: z.string(),
});
function hashApiKey(key: string): string {
return createHash('sha256').update(key).digest('hex');
}
function generateApiKey(): string {
const prefix = 'hemmelig';
const key = randomBytes(24).toString('base64url');
return `${prefix}_${key}`;
}
const app = new Hono<{
Variables: {
user: typeof auth.$Infer.Session.user | null;
};
}>()
.use(authMiddleware)
.get('/', async (c) => {
const user = c.get('user');
if (!user) {
return c.json({ error: 'Unauthorized' }, 401);
}
try {
const apiKeys = await prisma.apiKey.findMany({
where: { userId: user.id },
select: {
id: true,
name: true,
keyPrefix: true,
lastUsedAt: true,
expiresAt: true,
createdAt: true,
},
orderBy: { createdAt: 'desc' },
});
return c.json(apiKeys);
} catch (error) {
console.error('Failed to list API keys:', error);
return c.json({ error: 'Failed to list API keys' }, 500);
}
})
.post('/', zValidator('json', createApiKeySchema), async (c) => {
const user = c.get('user');
if (!user) {
return c.json({ error: 'Unauthorized' }, 401);
}
const { name, expiresInDays } = c.req.valid('json');
try {
// Check API key limit (max 5 per user)
const existingCount = await prisma.apiKey.count({
where: { userId: user.id },
});
if (existingCount >= 5) {
return c.json({ error: 'Maximum API key limit reached (5)' }, 400);
}
const rawKey = generateApiKey();
const keyHash = hashApiKey(rawKey);
const keyPrefix = rawKey.substring(0, 16);
const expiresAt = expiresInDays
? new Date(Date.now() + expiresInDays * 24 * 60 * 60 * 1000)
: null;
const apiKey = await prisma.apiKey.create({
data: {
name,
keyHash,
keyPrefix,
userId: user.id,
expiresAt,
},
select: {
id: true,
name: true,
keyPrefix: true,
expiresAt: true,
createdAt: true,
},
});
// Send webhook for API key creation
sendWebhook('apikey.created', {
apiKeyId: apiKey.id,
name: apiKey.name,
expiresAt: apiKey.expiresAt?.toISOString() || null,
userId: user.id,
});
// Return the raw key only once - it cannot be retrieved again
return c.json(
{
...apiKey,
key: rawKey,
},
201
);
} catch (error) {
console.error('Failed to create API key:', error);
return c.json({ error: 'Failed to create API key' }, 500);
}
})
.delete('/:id', zValidator('param', deleteApiKeySchema), async (c) => {
const user = c.get('user');
if (!user) {
return c.json({ error: 'Unauthorized' }, 401);
}
const { id } = c.req.valid('param');
try {
// Ensure the API key belongs to the user
const apiKey = await prisma.apiKey.findFirst({
where: { id, userId: user.id },
});
if (!apiKey) {
return c.json({ error: 'API key not found' }, 404);
}
await prisma.apiKey.delete({ where: { id } });
return c.json({ success: true });
} catch (error) {
console.error('Failed to delete API key:', error);
return handleNotFound(error as Error & { code?: string }, c);
}
});
export default app;
// Export helper for middleware
export { hashApiKey };