import { ZCancelOrgPlan, ZCreateOrgPlan, ZCreateOrganization, ZGetCoursesBySiteName, ZGetOrgSetup, ZGetOrganizationCoursesQuery, ZGetOrganizations, ZGetUserAnalytics, ZInviteTeamMembers, ZLMSExercisesParam, ZRemoveTeamMember, ZUpdateOrgPlan, ZUpdateOrganization } from '@cio/utils/validation/organization'; import { cancelOrgPlan, createOrg, createOrgPlan, getFirstOrgForSelfHosted, getOrgAudience, getOrgSetupData, getOrgTeam, getOrganizationCourses, getOrganizationsWithFilters, getPublicCourses, getRecommendedCourses, getUserAnalytics, getUserEnrolledCourses, removeTeamMember, updateOrg, updateOrgPlan } from '@api/services/organization'; import { Hono } from '@api/utils/hono'; import { TOrganization } from '@db/types'; import { automationRouter } from '@api/routes/organization/automation'; import { assetsRouter } from '@api/routes/organization/assets'; import { authMiddleware } from '@api/middlewares/auth'; import { authOrAutomationKeyMiddleware } from '@api/middlewares/auth-or-automation-key'; import { authOrApiKeyMiddleware } from '@api/middlewares/auth-or-api-key'; import { assertMcpAutomationUsageAllowed, recordMcpAutomationUsage } from '@api/services/organization/automation-usage'; import { courseImportRouter } from '@api/routes/organization/course-import'; import { getLMSExercisesService } from '@api/services/exercise'; import { handleError } from '@api/utils/errors'; import { inviteTeamMembers } from '@api/services/organization/invite'; import { orgAdminMiddleware } from '@api/middlewares/org-admin'; import { orgMemberMiddleware } from '@api/middlewares/org-member'; import { orgMemberOrAutomationKeyMiddleware } from '@api/middlewares/org-member-or-automation-key'; import { quizRouter } from '@api/routes/organization/quiz'; import { tagsRouter } from '@api/routes/organization/tags'; import { ROLE } from '@cio/utils/constants'; import { zValidator } from '@hono/zod-validator'; export const organizationRouter = new Hono() /** * GET /organization * Gets organizations with optional filters * Query params: siteName (string, optional), customDomain (string, optional), isCustomDomainVerified (boolean, optional) * Accepts: User session OR API key (for server-side routes) */ .get('/', authOrApiKeyMiddleware, zValidator('query', ZGetOrganizations), async (c) => { try { const filters = c.req.valid('query'); const organizations = await getOrganizationsWithFilters(filters); return c.json( { success: true, data: organizations }, 200 ); } catch (error) { console.error('Error fetching organizations:', error); return handleError(c, error, 'Failed to fetch organizations'); } }) /** * GET /organization/first * Gets the first organization (for self-hosted single-org mode). Requires API key. */ .get('/first', authOrApiKeyMiddleware, async (c) => { try { const org = await getFirstOrgForSelfHosted(); return c.json( { success: true, data: org ? [org] : [] }, 200 ); } catch (error) { console.error('Error fetching first organization:', error); return handleError(c, error, 'Failed to fetch first organization'); } }) /** * GET /organization/team * Gets organization team members (non-students) * Requires authentication */ .get('/team', authMiddleware, orgMemberMiddleware, async (c) => { try { const orgId = c.req.header('cio-org-id')!; const team = await getOrgTeam(orgId); return c.json( { success: true, data: team }, 200 ); } catch (error) { return handleError(c, error, 'Failed to fetch organization team'); } }) /** * POST /organization/team/invite * Invites team members to an organization * Requires authentication and admin role */ .post('/team/invite', authMiddleware, orgAdminMiddleware, zValidator('json', ZInviteTeamMembers), async (c) => { try { const orgId = c.req.header('cio-org-id')!; const user = c.get('user')!; const { emails, roleId } = c.req.valid('json'); const members = await inviteTeamMembers(orgId, emails, roleId, user.id); return c.json( { success: true, data: members }, 201 ); } catch (error) { return handleError(c, error, 'Failed to invite team members'); } }) /** * DELETE /organization/team/:memberId * Removes a team member from an organization * Requires authentication and admin role */ .delete('/team/:memberId', authMiddleware, orgAdminMiddleware, zValidator('param', ZRemoveTeamMember), async (c) => { try { const orgId = c.req.header('cio-org-id')!; const user = c.get('user')!; const { memberId } = c.req.valid('param'); await removeTeamMember(orgId, memberId, user.id); return c.json( { success: true }, 200 ); } catch (error) { return handleError(c, error, 'Failed to remove team member'); } }) /** * GET /organization/audience * Gets organization audience (students) * Requires authentication */ .get('/audience', authMiddleware, orgMemberMiddleware, async (c) => { try { const orgId = c.req.header('cio-org-id')!; const audience = await getOrgAudience(orgId); return c.json( { success: true, data: audience }, 200 ); } catch (error) { return handleError(c, error, 'Failed to fetch organization audience'); } }) /** * GET /organization/audience/:userId/analytics * Gets user analytics for a specific user in an organization * Requires authentication and organization membership */ .get( '/audience/:userId/analytics', authMiddleware, orgMemberMiddleware, zValidator('param', ZGetUserAnalytics), async (c) => { try { const orgId = c.req.header('cio-org-id')!; const { userId } = c.req.valid('param'); const analytics = await getUserAnalytics(userId, orgId); return c.json( { success: true, data: analytics }, 200 ); } catch (error) { return handleError(c, error, 'Failed to fetch user analytics'); } } ) /** * GET /organization/courses/public * Gets published courses for an organization (public landing page) * Query params: siteName (string) * No authentication required - returns only published courses */ .get('/courses/public', zValidator('query', ZGetCoursesBySiteName), async (c) => { try { const { siteName, tags } = c.req.valid('query'); const tagSlugs = tags ?.split(',') .map((value) => value.trim()) .filter(Boolean); const courses = await getPublicCourses(siteName, tagSlugs && tagSlugs.length > 0 ? tagSlugs : undefined); return c.json( { success: true, data: courses }, 200 ); } catch (error) { return handleError(c, error, 'Failed to fetch public courses'); } }) /** * GET /organization/courses/enrolled * Gets enrolled courses for a user in an organization (used in lms) * Requires authentication and organization membership */ .get('/courses/enrolled', authMiddleware, orgMemberMiddleware, async (c) => { try { const user = c.get('user')!; const orgId = c.req.header('cio-org-id')!; const result = await getUserEnrolledCourses(orgId, user.id); return c.json( { success: true, data: result }, 200 ); } catch (error) { return handleError(c, error, 'Failed to fetch courses'); } }) /** * GET /organization/courses/recommended * Gets recommended courses (published courses user isn't enrolled in) for a user in an organization (used in lms) * Requires authentication and organization membership */ .get('/courses/recommended', authMiddleware, orgMemberMiddleware, async (c) => { try { const user = c.get('user')!; const orgId = c.req.header('cio-org-id')!; const result = await getRecommendedCourses(orgId, user.id); return c.json( { success: true, data: result }, 200 ); } catch (error) { return handleError(c, error, 'Failed to fetch recommended courses'); } }) /** * GET /organization/courses * Gets courses for an organization with role-based filtering (used in dashboard) * Requires authentication and organization membership */ .get( '/courses', authOrAutomationKeyMiddleware, orgMemberOrAutomationKeyMiddleware(['course:read']), zValidator('query', ZGetOrganizationCoursesQuery), async (c) => { try { const user = c.get('user'); const orgId = c.get('orgId'); const userRole = c.get('userRole'); const automationKey = c.get('automationKey'); const { tags } = c.req.valid('query'); if (!orgId) { return c.json( { success: false, error: 'Organization context not available', code: 'ORG_CONTEXT_MISSING' }, 500 ); } if (automationKey?.type === 'mcp') { await assertMcpAutomationUsageAllowed(automationKey, 'list_org_courses'); } const tagSlugs = tags ?.split(',') .map((value) => value.trim()) .filter(Boolean); const result = await getOrganizationCourses( orgId, user?.id ?? automationKey?.createdByProfileId ?? '', userRole ?? ROLE.ADMIN, tagSlugs && tagSlugs.length > 0 ? tagSlugs : undefined ); if (automationKey?.type === 'mcp') { await recordMcpAutomationUsage(automationKey, 'list_org_courses', { tags: tagSlugs }); } return c.json( { success: true, data: result }, 200 ); } catch (error) { return handleError(c, error, 'Failed to fetch courses'); } } ) /** * GET /organization/setup * Gets setup data for an organization (courses, lessons, exercises, org info) * Query params: siteName (string) */ .get('/setup', zValidator('query', ZGetOrgSetup), async (c) => { try { const { siteName } = c.req.valid('query'); const setupData = await getOrgSetupData(siteName); return c.json( { success: true, data: setupData }, 200 ); } catch (error) { return handleError(c, error, 'Failed to fetch setup data'); } }) /** * POST /organization/plan * Creates a new organization plan * Requires authentication (user session or API key) */ .post('/plan', authOrApiKeyMiddleware, zValidator('json', ZCreateOrgPlan), async (c) => { try { const data = c.req.valid('json'); const plan = await createOrgPlan(data); return c.json( { success: true, data: plan }, 201 ); } catch (error) { return handleError(c, error, 'Failed to create organization plan'); } }) /** * PUT /organization/plan * Updates an organization plan by subscription ID * Requires authentication (user session or API key) */ .put('/plan', authOrApiKeyMiddleware, zValidator('json', ZUpdateOrgPlan), async (c) => { try { const { subscriptionId, payload } = c.req.valid('json'); const plan = await updateOrgPlan(subscriptionId, payload); return c.json( { success: true, data: plan }, 200 ); } catch (error) { return handleError(c, error, 'Failed to update organization plan'); } }) /** * POST /organization/plan/cancel * Cancels an organization plan by subscription ID * Requires authentication (user session or API key) */ .post('/plan/cancel', authOrApiKeyMiddleware, zValidator('json', ZCancelOrgPlan), async (c) => { try { const { subscriptionId, payload } = c.req.valid('json'); const plan = await cancelOrgPlan(subscriptionId, payload); return c.json( { success: true, data: plan }, 200 ); } catch (error) { return handleError(c, error, 'Failed to cancel organization plan'); } }) /** * POST /organization * Creates a new organization with the current user as owner * Requires authentication */ .post('/', authMiddleware, zValidator('json', ZCreateOrganization), async (c) => { try { const user = c.get('user')!; const data = c.req.valid('json'); const result = await createOrg(user.id, data); return c.json( { success: true, data: result }, 201 ); } catch (error) { return handleError(c, error, 'Failed to create organization'); } }) /** * PUT /organization * Updates an organization (name, avatarUrl) * Requires authentication */ .put('/', authMiddleware, orgAdminMiddleware, zValidator('json', ZUpdateOrganization), async (c) => { try { console.log('updateOrg'); console.log(c.req.valid('json')); const orgId = c.req.header('cio-org-id')!; const data = c.req.valid('json') as Partial; const organization = await updateOrg(orgId, data); return c.json( { success: true, data: organization }, 200 ); } catch (error) { return handleError(c, error, 'Failed to update organization'); } }) /** * GET /organization/:orgId/exercises/lms * Gets exercises with submissions for a student in an organization * Returns exercises from unlocked lessons in courses where the student is a member * Requires authentication and organization membership */ .get('/:orgId/exercises/lms', authMiddleware, zValidator('param', ZLMSExercisesParam), async (c) => { try { const { orgId } = c.req.valid('param'); const user = c.get('user')!; // Check org membership manually since middleware expects header const { getUserOrgRole } = await import('@cio/db/queries/organization'); const roleId = await getUserOrgRole(orgId, user.id); if (roleId === null) { return c.json( { success: false, error: 'UNAUTHORIZED', code: 'UNAUTHORIZED' }, 403 ); } const exercises = await getLMSExercisesService(user.id, orgId); return c.json( { success: true, data: exercises }, 200 ); } catch (error) { return handleError(c, error, 'Failed to fetch LMS exercises'); } }) .route('/automation', automationRouter) .route('/course-import', courseImportRouter) .route('/tags', tagsRouter) .route('/assets', assetsRouter) .route('/:orgId/quiz', quizRouter);