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:
151
api/routes/invites.ts
Normal file
151
api/routes/invites.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user