feat: rebrand Hemmelig to paste.es for cloudhost.es
- Set Spanish as default language with ephemeral/encrypted privacy focus - Translate all user-facing strings and legal pages to Spanish - Replace Norwegian flag with Spanish flag in footer - Remove Hemmelig/terces.cloud links, add cloudhost.es sponsorship - Rewrite PrivacyPage: zero data collection, ephemeral design emphasis - Rewrite TermsPage: Spanish law, RGPD, paste.es/CloudHost.es references - Update PWA manifest, HTML meta tags, package.json branding - Rename webhook headers to X-Paste-Event / X-Paste-Signature - Update API docs title and contact to paste.es / cloudhost.es Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
156
api/routes/api-keys.ts
Normal file
156
api/routes/api-keys.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user