fix(api): use admin app URL for teacher auto-enroll email links (#771)

* fix(api): use admin app URL for teacher auto-enroll links

Teacher payment-request emails now build course and grant-access URLs
with getAppBaseUrl() instead of getDashboardBaseUrl(org), so links
point at app.classroomio.com rather than a custom or tenant domain.

Document the teacher vs student email link rule in AGENTS.md.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* docs: surface email link URL rules where emails are wired

Document getAppBaseUrl vs getDashboardBaseUrl in dashboard-url.ts,
packages/email/README.md, enqueueTransactionalEmail JSDoc, and AGENTS.md
so staff vs learner hosts are discoverable when building email links.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
This commit is contained in:
Rotimi Best
2026-07-19 13:50:12 +01:00
committed by GitHub
co-authored by Rotimi Best Cursor Agent
parent e09b3caa8a
commit 0f23453f91
5 changed files with 67 additions and 12 deletions
+27 -3
View File
@@ -2,6 +2,21 @@ import { TENANT_ROOT_DOMAIN, BRAND_ROOT_DOMAIN } from '@cio/utils/constants/doma
import { env } from './env';
/**
* Dashboard host helpers for links embedded in emails and other outbound URLs.
*
* Email templates only receive pre-built URLs in their `fields` — callers in
* `apps/api` (or other services) choose the host here before enqueueing mail.
*
* - **Student/learner links** (course enroll, audience invites, login):
* `getDashboardBaseUrl(org)` — org custom domain, tenant subdomain, or fallback.
* - **Teacher/tutor/admin dashboard actions** (course management, grading, team
* invites, auto-enroll): `getAppBaseUrl()` — admin app (`app.classroomio.com`
* in cloud). Do not pass org `customDomain` or tenant `siteName`.
*
* See `packages/email/README.md` and `AGENTS.md` § Email link URLs.
*/
/**
* Minimal org shape consumed by `getDashboardBaseUrl`. Accepts the three
* fields that decide which host the org's URLs live on. All optional so
@@ -14,8 +29,10 @@ export type DashboardOrg = {
};
/**
* Returns the base URL for the dashboard app, used for invite links, login
* URLs, course URLs, and email-embedded links.
* Returns the learner-facing dashboard base URL for an org.
*
* Use for **student/learner email links** (course enroll, audience invites,
* login) so links land on the org's public site.
*
* Resolution order (highest precedence first):
* 1. Org's verified `customDomain` → `https://<customDomain>`
@@ -26,6 +43,8 @@ export type DashboardOrg = {
*
* `customDomain` wins over `DASHBOARD_ORIGIN` because per-org BYOD must
* override the platform-wide self-hosted dashboard host for that org's URLs.
*
* For teacher/tutor/admin dashboard actions in email, use `getAppBaseUrl()`.
*/
export function getDashboardBaseUrl(org?: DashboardOrg): string {
if (org?.customDomain && org.isCustomDomainVerified) {
@@ -49,7 +68,12 @@ export function getDashboardBaseUrl(org?: DashboardOrg): string {
/**
* Returns the admin dashboard origin (`app.classroomio.com` in cloud).
* Use for team-member invite links (admin/tutor) — not student-facing URLs.
*
* Use for **teacher/tutor/admin email links** — course management, grading,
* team invites, auto-enroll, and any other staff dashboard action. Staff sign
* in on the admin app, not the org public site.
*
* Do not use for student/learner-facing links; use `getDashboardBaseUrl(org)`.
*/
export function getAppBaseUrl(): string {
return getDashboardBaseUrl();
+23
View File
@@ -46,6 +46,29 @@ await send('forgotPassword', {
});
```
## Email link URLs
Email templates receive fully-built URLs in their `fields` — templates do **not** choose the host. Callers in `apps/api` (or other services) must build links before calling `enqueueTransactionalEmail` or `send`.
Import helpers from `@cio/core/config/dashboard-url`:
| Recipient | Helper | Use for |
| --- | --- | --- |
| Teacher, tutor, admin | `getAppBaseUrl()` | Course management, grading, team invites, auto-enroll |
| Student, learner | `getDashboardBaseUrl(org)` | Course enroll, audience invites, login |
```typescript
import { getAppBaseUrl, getDashboardBaseUrl } from '@cio/core/config/dashboard-url';
// Staff dashboard action (admin app — not org custom domain)
const autoEnrollUrl = `${getAppBaseUrl()}/courses/${courseId}/people?grantAccess=${encodeURIComponent(email)}`;
// Learner-facing invite (org public site)
const inviteUrl = `${getDashboardBaseUrl(org)}/invite/${encodeURIComponent(token)}`;
```
See also `AGENTS.md` § Email link URLs.
## Creating Email Templates
### Basic Example