Files
paste.es/api/routes/invites.ts
Malin bc9f96cbd4 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>
2026-02-24 09:30:19 +01:00

152 lines
5.1 KiB
TypeScript

import { zValidator } from '@hono/zod-validator';
import { Hono } from 'hono';
import { nanoid } from 'nanoid';
import { z } from 'zod';
import { auth } from '../auth';
import { TIME } from '../lib/constants';
import prisma from '../lib/db';
import { handleNotFound } from '../lib/utils';
import { authMiddleware, checkAdmin } from '../middlewares/auth';
const createInviteSchema = z.object({
maxUses: z.number().int().min(1).max(100).optional().default(1),
expiresInDays: z.number().int().min(1).max(365).optional(),
});
const codeSchema = z.object({ code: z.string() });
// Public route for validating invite codes (no auth required)
export const invitePublicRoute = new Hono()
.post('/validate', zValidator('json', codeSchema), async (c) => {
const { code } = c.req.valid('json');
try {
const invite = await prisma.inviteCode.findUnique({
where: { code: code.toUpperCase() },
});
if (!invite || !invite.isActive) {
return c.json({ error: 'Invalid invite code' }, 400);
}
if (invite.expiresAt && new Date() > invite.expiresAt) {
return c.json({ error: 'Invite code has expired' }, 400);
}
if (invite.maxUses && invite.uses >= invite.maxUses) {
return c.json({ error: 'Invite code has reached maximum uses' }, 400);
}
return c.json({ valid: true });
} catch (error) {
console.error('Failed to validate invite code:', error);
return c.json({ error: 'Failed to validate invite code' }, 500);
}
})
.post('/use', zValidator('json', z.object({ code: z.string() })), async (c) => {
const { code } = c.req.valid('json');
const user = c.get('user');
if (!user) {
return c.json({ error: 'Unauthorized' }, 401);
}
const userId = user.id;
try {
const invite = await prisma.inviteCode.findUnique({
where: { code: code.toUpperCase() },
});
if (!invite || !invite.isActive) {
return c.json({ error: 'Invalid invite code' }, 400);
}
if (invite.expiresAt && new Date() > invite.expiresAt) {
return c.json({ error: 'Invite code has expired' }, 400);
}
if (invite.maxUses && invite.uses >= invite.maxUses) {
return c.json({ error: 'Invite code has reached maximum uses' }, 400);
}
await prisma.$transaction([
prisma.inviteCode.update({
where: { id: invite.id },
data: { uses: { increment: 1 } },
}),
prisma.user.update({
where: { id: userId },
data: { inviteCodeUsed: code.toUpperCase() },
}),
]);
return c.json({ success: true });
} catch (error) {
console.error('Failed to use invite code:', error);
return c.json({ error: 'Failed to use invite code' }, 500);
}
});
// Protected routes for admin invite management
export const inviteRoute = new Hono<{
Variables: {
user: typeof auth.$Infer.Session.user | null;
};
}>()
.use(authMiddleware)
.use(checkAdmin)
.get('/', async (c) => {
try {
const invites = await prisma.inviteCode.findMany({
orderBy: { createdAt: 'desc' },
});
return c.json(invites);
} catch (error) {
console.error('Failed to list invite codes:', error);
return c.json({ error: 'Failed to list invite codes' }, 500);
}
})
.post('/', zValidator('json', createInviteSchema), async (c) => {
const { maxUses, expiresInDays } = c.req.valid('json');
const user = c.get('user');
if (!user) {
return c.json({ error: 'Unauthorized' }, 401);
}
try {
const code = nanoid(12).toUpperCase();
const expiresAt = expiresInDays
? new Date(Date.now() + expiresInDays * TIME.DAY_MS)
: null;
const invite = await prisma.inviteCode.create({
data: {
code,
maxUses,
expiresAt,
createdBy: user.id,
},
});
return c.json(invite, 201);
} catch (error) {
console.error('Failed to create invite code:', error);
return c.json({ error: 'Failed to create invite code' }, 500);
}
})
.delete('/:id', zValidator('param', z.object({ id: z.string() })), async (c) => {
const { id } = c.req.valid('param');
try {
await prisma.inviteCode.update({
where: { id },
data: { isActive: false },
});
return c.json({ success: true });
} catch (error) {
console.error(`Failed to delete invite code ${id}:`, error);
return handleNotFound(error as Error & { code?: string }, c);
}
});