Files
classroomio/apps/api/src/services/onboarding.ts
T
Rotimi Best d5b07d356e feat: HLS streaming, transcripts, agent course generation & landing-page course previews (#663)
New features:
- HLS video encoding/streaming (hls routes, hls-encoder) and a media
  transcripts pipeline (transcripts route + media-transcript queries)
- AI assistant chat vs. run model: agent-course-generation worker/queue,
  run-monitor pane, chat-changes card, content ask-ai bar
- Per-theme course-page previews across org landing-page themes (bold,
  classic, corporate, editorial, etc.) with a shared course token/component set
- View-as-student mode and login-link / link-invite invite flows

Refactor:
- Extract packages/core consolidating config (env, storage, dashboard-url),
  services and utils previously duplicated under apps/api/src/config
- Remove legacy agent service modules (chat-tools, chat-context,
  context-window, document, fetch-url, lesson-content)
- Relax Cross-Origin-Resource-Policy to allow cross-origin media embedding
- Add format script to tenant-router so turbo run format covers it
2026-06-07 18:43:05 +01:00

142 lines
4.2 KiB
TypeScript

import { AppError, ErrorCodes } from '@api/utils/errors';
import type { TProfile, TUser } from '@cio/db/types';
import {
checkSiteNameExists,
createOrganization,
createOrganizationMember,
createOrganizationPlan,
getOrganizationByProfileId,
getOrganizationCount
} from '@cio/db/queries';
import { getProfileById, updateProfile } from '@cio/db/queries/auth';
import { env } from '@cio/core/config/env';
import { ROLE } from '@cio/utils/constants';
import { PLAN } from '@cio/utils/plans';
import { db } from '@cio/db/drizzle';
import { enqueueTransactionalEmail } from '@api/services/jobs';
export async function createOrganizationWithOwner(
profileId: string,
input: {
fullname?: string;
orgName: string;
siteName: string;
}
) {
// Self-hosted: block org creation when an org already exists
if (env.PUBLIC_IS_SELFHOSTED === 'true') {
const count = await getOrganizationCount();
if (count > 0) {
throw new AppError('Self-hosted instances support only one organization', ErrorCodes.VALIDATION_ERROR, 403);
}
}
// Business Logic: Check sitename availability
const exists = await checkSiteNameExists(input.siteName);
if (exists) {
console.error('Site name already exists:', input.siteName);
throw new AppError(`Site name '${input.siteName}' already exists`, ErrorCodes.SITENAME_EXISTS, 409);
}
// Business Logic: Create org and member in a transaction
try {
const result = await db.transaction(async (tx) => {
const organization = await createOrganization(
{
name: input.orgName,
siteName: input.siteName
},
tx
);
const member = await createOrganizationMember(
{
organizationId: organization.id,
profileId,
roleId: ROLE.ADMIN,
verified: true
},
tx
);
// Self-hosted: assign Enterprise plan to the new org
if (env.PUBLIC_IS_SELFHOSTED === 'true') {
await createOrganizationPlan(
{
orgId: organization.id,
planName: PLAN.ENTERPRISE as 'ENTERPRISE',
subscriptionId: `selfhosted-${organization.id}`,
triggeredBy: member.id,
payload: {},
isActive: true,
provider: 'selfhosted'
},
tx
);
}
return { organization, member };
});
// Fetch updated organizations list
const organizations = await getOrganizationByProfileId(profileId);
return {
organization: result.organization,
member: result.member,
organizations
};
} catch (error) {
console.error('Error creating organization:', error);
// Handle database constraint violations
if (error && typeof error === 'object' && 'code' in error && error.code === '23505') {
throw new AppError(`Site name '${input.siteName}' already exists`, ErrorCodes.SITENAME_EXISTS, 409);
}
throw new AppError(error instanceof Error ? error : new Error('Unknown error'), ErrorCodes.ORG_CREATE_FAILED, 500);
}
}
export async function updateUserOnboarding(userId: string, data: Partial<TProfile>) {
try {
const updatedProfile = await updateProfile(userId, data);
if (!updatedProfile) {
throw new AppError('Failed to update profile - profile not found', ErrorCodes.PROFILE_NOT_FOUND, 404);
}
return updatedProfile;
} catch (error) {
if (error instanceof AppError) {
throw error;
}
throw new AppError(
error instanceof Error ? error : new Error('Unknown error'),
ErrorCodes.PROFILE_UPDATE_FAILED,
500
);
}
}
export async function completeOnboarding(user: Partial<TUser> & { id: string; email: string }) {
try {
const profile = await getProfileById(user.id);
if (!profile) {
throw new AppError('Profile not found', ErrorCodes.PROFILE_NOT_FOUND, 404);
}
await enqueueTransactionalEmail('welcome', {
to: user.email,
fields: {
name: profile.fullname
},
idempotencyKey: `onboarding:welcome:${user.id}`
});
} catch (error) {
if (error instanceof AppError) {
throw error;
}
throw new AppError(error instanceof Error ? error : new Error('Unknown error'), ErrorCodes.INTERNAL_ERROR, 500);
}
}