152 lines
5.1 KiB
TypeScript
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);
|
||
|
|
}
|
||
|
|
});
|