fix(admin): invite magic link is absolute, not relative

The Create Invitation flow rendered "/invite?t=…" instead of
"https://host/invite?t=…" because the origin was gated on an unset
PUBLIC_ORIGIN env var.

Solution: OpContext now carries `origin` (always set by the route
handler from Astro.url.origin), and invitations.ts builds the magic
link from it. No env vars required.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan Hvid 2026-05-13 10:29:58 +02:00
parent 4aaf0957dd
commit a520e8534e
3 changed files with 10 additions and 4 deletions

View file

@ -221,6 +221,9 @@ export interface FormConfig {
// ── Op context — passed to CRUD ops and actions ───────────────────────────── // ── Op context — passed to CRUD ops and actions ─────────────────────────────
export interface OpContext { export interface OpContext {
user: { id: number; role: string }; user: { id: number; role: string };
/** Request origin (e.g. "https://bifrost.fenja.ai") used to build absolute
* URLs in ActionResults like invite links. Always set by the route handler. */
origin: string;
/** /**
* Raw POST FormData opt-in escape hatch for resources whose form has * Raw POST FormData opt-in escape hatch for resources whose form has
* embedded sub-forms (e.g. the pulse fieldset inside dispatches). Most * embedded sub-forms (e.g. the pulse fieldset inside dispatches). Most

View file

@ -166,11 +166,11 @@ export const invitationsResource: Resource<InviteRow> = {
// Surface the one-shot magic link via the result mechanism — the route // Surface the one-shot magic link via the result mechanism — the route
// handler propagates it as ?invite_url= and the panel renders a copy // handler propagates it as ?invite_url= and the panel renders a copy
// block on the next page load. // block on the next page load. ctx.origin comes from Astro.url.origin
const origin = process.env.PUBLIC_ORIGIN ?? ''; // so the link is always absolute and clickable.
ctx.result = { ctx.result = {
kind: 'invite-link', kind: 'invite-link',
url: `${origin}/invite?t=${token}`, url: `${ctx.origin}/invite?t=${token}`,
}; };
return id; return id;
}, },

View file

@ -38,7 +38,10 @@ if (!resource) {
} }
const resourceBase = `/admin/${resource.key}`; const resourceBase = `/admin/${resource.key}`;
const opCtx: OpContext = { user: { id: user.id, role: user.role } }; const opCtx: OpContext = {
user: { id: user.id, role: user.role },
origin: Astro.url.origin,
};
// ── Form-data → typed record (driven by the field configs) ──────────────── // ── Form-data → typed record (driven by the field configs) ────────────────
function parseFormData( function parseFormData(