fix(org): route admin and tutor invites to app domain (#731)

Team member invite emails and link-invite copy URLs were built with the
org tenant host (custom domain or siteName.myclassroomio.com). Admin and
tutor invites should always point at the admin dashboard origin
(app.classroomio.com in cloud). Student org invites are unchanged.
This commit is contained in:
Rotimi Best
2026-07-12 09:16:39 +01:00
committed by GitHub
parent f181bb483a
commit 0b5247bed3
4 changed files with 27 additions and 10 deletions
+4 -7
View File
@@ -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', {
@@ -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() {
+13 -1
View File
@@ -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<AccountOrg, 'customDomain' | 'isCustomDomainVerified' | 'siteName'>;
/**
* 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
@@ -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();
}