135 lines
4.1 KiB
TypeScript
135 lines
4.1 KiB
TypeScript
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');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
);
|