diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 147dcc2e..c3acc678 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -1,6 +1,7 @@ import 'dotenv/config'; -import { API_SERVER_URL, TRUSTED_ORIGINS } from '@api/constants'; +import { API_SERVER_URL } from '@api/constants'; +import { resolveTrustedBrowserOriginForCors } from '@api/utils/origins'; import { Hono } from '@api/utils/hono'; import { accountRouter } from '@api/routes/account'; @@ -44,7 +45,7 @@ export const app = new Hono() .use( '*', cors({ - origin: TRUSTED_ORIGINS, + origin: (origin) => resolveTrustedBrowserOriginForCors(origin), allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowHeaders: ['Content-Type', 'Authorization', 'Cio-org-id'], exposeHeaders: ['Content-Length', 'X-Kuma-Revision'], diff --git a/apps/api/src/constants/index.ts b/apps/api/src/constants/index.ts index 666be2f1..50ffa991 100644 --- a/apps/api/src/constants/index.ts +++ b/apps/api/src/constants/index.ts @@ -6,7 +6,7 @@ export const API_PORT = env.PORT ? parseInt(env.PORT) : 3002; export const API_SERVER_URL = env.PUBLIC_SERVER_URL || `http://localhost:${API_PORT}`; export const TRUSTED_ORIGINS = env.TRUSTED_ORIGINS ? env.TRUSTED_ORIGINS.split(',') - : ['http://localhost:5173', 'http://localhost:5180', 'http://*.classroomio.*']; + : ['http://localhost:5173', 'http://localhost:5180', 'https://*.classroomio.com']; const { CLOUDFLARE_BUCKET_DOMAIN, diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 8b197d20..b781d6a5 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -2,9 +2,10 @@ import 'dotenv/config'; import { API_PORT } from '@api/constants'; import { app } from '@api/app'; -import { env } from '@api/config/env'; import { configureOpenAPI } from '@api/utils/openapi'; import { connectRedis } from '@api/utils/redis/redis'; +import { env } from '@api/config/env'; +import { preloadVerifiedCustomDomainOriginsRegistry } from '@api/utils/origins'; import { serve } from '@hono/node-server'; import { showRoutes } from 'hono/dev'; @@ -15,6 +16,10 @@ async function startServer() { // Connect to Redis (non-blocking: API starts even if Redis fails) await connectRedis(); + preloadVerifiedCustomDomainOriginsRegistry().then(() => { + console.log('Verified custom domain origins preloaded'); + }); + serve({ fetch: app.fetch, port: API_PORT }); if (env.NODE_ENV !== 'production') { diff --git a/apps/api/src/services/organization.ts b/apps/api/src/services/organization.ts index a6d3db8e..2fbaee3e 100644 --- a/apps/api/src/services/organization.ts +++ b/apps/api/src/services/organization.ts @@ -13,6 +13,7 @@ import { getOrgIdBySiteName, getOrganizationAudience, getOrganizationAudienceMember, + getOrganizationById, getOrganizationBySiteName, getOrganizationTeam, getOrganizations, @@ -42,6 +43,7 @@ import { createOrganizationWithOwner } from '@api/services/onboarding'; import { deriveAudienceMemberStatus } from '@api/utils/audience-member-status'; import { getProfileById } from '@cio/db/queries/auth'; import { inviteTeamMembers as inviteTeamMembersSecure } from './organization/invite'; +import { trustCustomDomainHostname, untrustCustomDomainHostname } from '@cio/db/utils'; const PUBLIC_ORG_LANDING_PAGE_COURSE_LIMIT = 4; @@ -572,10 +574,32 @@ export async function updateOrg(orgId: string, data: Partial) { } } + let previousCustomDomainHostname: string | undefined; + + if (data.customDomain !== undefined || data.isCustomDomainVerified !== undefined) { + const previousOrg = await getOrganizationById(orgId); + const oldHost = previousOrg?.customDomain?.trim().toLowerCase(); + + if (oldHost) { + previousCustomDomainHostname = oldHost; + } + } + const organization = await updateOrganization(orgId, data); if (!organization) { throw new AppError('Organization not found', ErrorCodes.ORGANIZATION_NOT_FOUND, 404); } + + if (previousCustomDomainHostname) { + untrustCustomDomainHostname(previousCustomDomainHostname); + } + + const newCustomDomainHostname = organization.customDomain?.trim().toLowerCase(); + + if (newCustomDomainHostname && organization.isCustomDomainVerified) { + trustCustomDomainHostname(newCustomDomainHostname); + } + return organization; } catch (error) { if (error instanceof AppError) { diff --git a/apps/api/src/utils/origins.ts b/apps/api/src/utils/origins.ts new file mode 100644 index 00000000..393ac063 --- /dev/null +++ b/apps/api/src/utils/origins.ts @@ -0,0 +1,21 @@ +import { + preloadVerifiedCustomDomainOrigins, + resolveTrustedBrowserOrigin, + trustCustomDomainHostname, + untrustCustomDomainHostname +} from '@cio/db/utils'; + +import { TRUSTED_ORIGINS } from '@api/constants'; + +const staticTrustedOriginEntries = TRUSTED_ORIGINS.map((entry) => entry.trim()).filter(Boolean) as readonly string[]; + +export async function preloadVerifiedCustomDomainOriginsRegistry(): Promise { + await preloadVerifiedCustomDomainOrigins(); +} + +export function resolveTrustedBrowserOriginForCors(origin: string): string | undefined { + console.log('check origin', origin); + return resolveTrustedBrowserOrigin(origin, staticTrustedOriginEntries); +} + +export { trustCustomDomainHostname, untrustCustomDomainHostname }; diff --git a/apps/website/src/lib/components/hero.svelte b/apps/website/src/lib/components/hero.svelte index eb70abcd..0bc20405 100644 --- a/apps/website/src/lib/components/hero.svelte +++ b/apps/website/src/lib/components/hero.svelte @@ -82,7 +82,7 @@ - +