diff --git a/apps/api/src/services/organization/invite.ts b/apps/api/src/services/organization/invite.ts index b6f548ed..e91d8dc4 100644 --- a/apps/api/src/services/organization/invite.ts +++ b/apps/api/src/services/organization/invite.ts @@ -28,7 +28,7 @@ import { ROLE } from '@cio/utils/constants'; import type { TNewOrganizationInviteAudit } from '@db/types'; import crypto from 'node:crypto'; import { db, type DbOrTxClient } from '@cio/db/drizzle'; -import { getDashboardBaseUrl } from '@cio/core/config/dashboard-url'; +import { getAppBaseUrl } from '@cio/core/config/dashboard-url'; import { parseCourseIdsFromInviteMetadata, parseCohortIdsFromInviteMetadata } from '@api/utils/org'; import { getProfileById, markUserAndProfileEmailVerified } from '@cio/db/queries/auth/profile'; import { enqueueTransactionalEmail } from '@api/services/jobs'; @@ -61,11 +61,8 @@ function normalizeEmails(emails: string[]): string[] { return [...new Set(emails.map((email) => email.toLowerCase().trim()).filter(Boolean))]; } -function buildInviteLink( - token: string, - org?: { siteName?: string | null; customDomain?: string | null; isCustomDomainVerified?: boolean | null } -): string { - return `${getDashboardBaseUrl(org)}/invite/${encodeURIComponent(token)}`; +function buildTeamInviteLink(token: string): string { + return `${getAppBaseUrl()}/invite/${encodeURIComponent(token)}`; } function getRoleLabel(roleId: number): string { @@ -251,7 +248,7 @@ export async function inviteTeamMembers(orgId: string, emails: string[], roleId: } }); - const inviteLink = buildInviteLink(token, organization); + const inviteLink = buildTeamInviteLink(token); try { await enqueueTransactionalEmail('inviteTeacher', { diff --git a/apps/dashboard/src/lib/features/settings/pages/teams.svelte b/apps/dashboard/src/lib/features/settings/pages/teams.svelte index f7b65c6a..486ed212 100644 --- a/apps/dashboard/src/lib/features/settings/pages/teams.svelte +++ b/apps/dashboard/src/lib/features/settings/pages/teams.svelte @@ -4,7 +4,7 @@ import CopyIcon from '@lucide/svelte/icons/copy'; import { profile } from '$lib/utils/store/user'; - import { isFreePlan, currentOrg } from '$lib/utils/store/org'; + import { getAppOrigin, isFreePlan, currentOrg } from '$lib/utils/store/org'; import { orgApi } from '$features/org/api/org.svelte'; import { t } from '$lib/utils/functions/translations'; import { snackbar } from '$features/ui/snackbar/store'; @@ -25,7 +25,7 @@ let isRemoving: number | null = $state(null); function buildLinkInviteUrl(token: string): string { - return `${window.location.origin}/invite/link/${encodeURIComponent(token)}`; + return `${getAppOrigin()}/invite/link/${encodeURIComponent(token)}`; } async function onCopyInviteLink() { diff --git a/apps/dashboard/src/lib/utils/store/org.ts b/apps/dashboard/src/lib/utils/store/org.ts index 4427eae7..71eb85b7 100644 --- a/apps/dashboard/src/lib/utils/store/org.ts +++ b/apps/dashboard/src/lib/utils/store/org.ts @@ -6,7 +6,7 @@ import type { AccountOrg } from '$features/app/types'; import type { OrgTeamMember } from '../types/org'; import { canUsePublicApi, isOrgOnFreePlan, PLAN } from '@cio/utils/plans'; import { PUBLIC_IS_SELFHOSTED } from '$env/static/public'; -import { ROLE, TENANT_ROOT_DOMAIN } from '@cio/utils/constants'; +import { BRAND_ROOT_DOMAIN, ROLE, TENANT_ROOT_DOMAIN } from '@cio/utils/constants'; import { STEPS } from '../constants/quiz'; import type { Writable } from 'svelte/store'; @@ -79,6 +79,18 @@ export const currentOrgPath = derived(currentOrg, ($currentOrg) => type OrgPublicOrigin = Pick; +/** + * Admin dashboard origin (`app.classroomio.com` in cloud). Use for team invite links + * and other admin-only URLs — not student-facing tenant pages. + */ +export function getAppOrigin(): string { + if (PUBLIC_IS_SELFHOSTED === 'true' || dev) { + return browser ? window.location.origin : ''; + } + + return `https://app.${BRAND_ROOT_DOMAIN}`; +} + /** * Public origin for an org's tenant site (student LMS, public course pages, login-link handoff). * Self-hosted uses the current deployment URL. On cloud, verified custom domains win over the diff --git a/packages/core/src/config/dashboard-url.ts b/packages/core/src/config/dashboard-url.ts index 25fbf16d..ef973902 100644 --- a/packages/core/src/config/dashboard-url.ts +++ b/packages/core/src/config/dashboard-url.ts @@ -46,3 +46,11 @@ export function getDashboardBaseUrl(org?: DashboardOrg): string { return `https://app.${BRAND_ROOT_DOMAIN}`; } + +/** + * Returns the admin dashboard origin (`app.classroomio.com` in cloud). + * Use for team-member invite links (admin/tutor) — not student-facing URLs. + */ +export function getAppBaseUrl(): string { + return getDashboardBaseUrl(); +}