Files
classroomio/apps/api/src/routes/invite/invite.ts
T

135 lines
4.1 KiB
TypeScript
Raw Normal View History

2026-03-09 11:18:22 +01:00
import { ZCourseInviteTokenParam } from '@cio/utils/validation/course/invite';
import { previewStudentInvite } from '@api/services/course/invite';
import { ZOrganizationInviteTokenParam } from '@cio/utils/validation/organization/invite';
import { acceptOrganizationInvite, previewOrganizationInvite } from '@api/services/organization/invite';
import { Hono } from '@api/utils/hono';
import { apiKeyMiddleware } from '@api/middlewares/api-key';
import { authMiddleware } from '@api/middlewares/auth';
import { createRateLimiter } from '@api/middlewares/rate-limiter';
import { handleError } from '@api/utils/errors';
import { extractClientIp } from '@api/utils/redis/key-generators';
import { zValidator } from '@hono/zod-validator';
const previewInviteRateLimit = createRateLimiter({
windowMs: 60 * 1000,
maxRequests: 30,
message: 'Too many invite preview requests. Please try again later.',
keyGenerator: (c) => `invite_preview:${extractClientIp(c)}:${c.req.param('token')}`
});
const previewOrganizationInviteRateLimit = createRateLimiter({
windowMs: 60 * 1000,
maxRequests: 30,
message: 'Too many organization invite preview requests. Please try again later.',
keyGenerator: (c) => `org_invite_preview:${extractClientIp(c)}:${c.req.param('token')}`
});
const acceptOrganizationInviteRateLimit = createRateLimiter({
windowMs: 60 * 60 * 1000,
maxRequests: 20,
message: 'Too many organization invite join attempts. Please try again later.',
keyGenerator: (c) => {
const user = c.get('user');
const actor = user?.id ? `user:${user.id}` : `ip:${extractClientIp(c)}`;
return `org_invite_accept:${actor}:${c.req.param('token')}`;
}
});
export const inviteRouter = new Hono()
/**
* GET /invite/student/:token
* Server-only preview for secure invite links (API key). Returns course, org, invite for enroll page.
*/
.get(
'/student/:token',
apiKeyMiddleware,
previewInviteRateLimit,
zValidator('param', ZCourseInviteTokenParam),
async (c) => {
try {
const { token } = c.req.valid('param');
const preview = await previewStudentInvite(token, {
ipAddress: extractClientIp(c),
userAgent: c.req.header('user-agent') || null
});
return c.json(
{
success: true,
data: preview
},
200
);
} catch (error) {
return handleError(c, error, 'Failed to load invite');
}
}
)
/**
* GET /invite/organization/:token/preview
* Server-only preview for organization role invites (API key)
*/
.get(
'/organization/:token/preview',
apiKeyMiddleware,
previewOrganizationInviteRateLimit,
zValidator('param', ZOrganizationInviteTokenParam),
async (c) => {
try {
const { token } = c.req.valid('param');
const preview = await previewOrganizationInvite(token, {
ipAddress: extractClientIp(c),
userAgent: c.req.header('user-agent') || null
});
return c.json(
{
success: true,
data: preview
},
200
);
} catch (error) {
return handleError(c, error, 'Failed to load organization invite');
}
}
)
/**
* POST /invite/organization/:token/accept
* Authenticated organization role invite acceptance
*/
.post(
'/organization/:token/accept',
authMiddleware,
acceptOrganizationInviteRateLimit,
zValidator('param', ZOrganizationInviteTokenParam),
async (c) => {
try {
const { token } = c.req.valid('param');
const user = c.get('user')!;
const result = await acceptOrganizationInvite(
token,
{
id: user.id,
email: user.email
},
{
ipAddress: extractClientIp(c),
userAgent: c.req.header('user-agent') || null
}
);
return c.json(
{
success: true,
data: result
},
200
);
} catch (error) {
return handleError(c, error, 'Failed to accept organization invite');
}
}
);