fix(portal): add invitation links and correct customer login routing
All checks were successful
Build and Release FEDEO / verify (push) Successful in 1m7s
Build and Release FEDEO / build-and-release (push) Successful in 6m24s

This commit is contained in:
2026-10-13 18:13:41 +02:00
parent 070be08207
commit 5463106938
10 changed files with 302 additions and 29 deletions

View File

@@ -0,0 +1,33 @@
import type { FastifyInstance } from "fastify"
import { and, eq } from "drizzle-orm"
import { authProfiles, authTenantUsers } from "../../db/schema"
export async function resolveLoginTenant(server: FastifyInstance, userId: string, domainTenantId: number | null) {
const memberships = await server.db
.selectDistinct({
tenantId: authTenantUsers.tenant_id,
portalCustomerId: authProfiles.customer_for_portal,
})
.from(authTenantUsers)
.leftJoin(authProfiles, and(
eq(authProfiles.user_id, authTenantUsers.user_id),
eq(authProfiles.tenant_id, authTenantUsers.tenant_id),
eq(authProfiles.active, true)
))
.where(eq(authTenantUsers.user_id, userId))
if (domainTenantId) {
if (!memberships.some((membership) => membership.tenantId === domainTenantId)) {
throw Object.assign(new Error("Not a member of this tenant"), { statusCode: 403 })
}
return domainTenantId
}
// Portal invitations assign one customer account. Load its profile immediately,
// even when the customer signs in through the shared FEDEO address.
if (memberships.length === 1 && memberships[0].portalCustomerId) {
return memberships[0].tenantId
}
return null
}

View File

@@ -0,0 +1,29 @@
type PortalUrlSettings = {
portalDomain?: string | null
publicUrl?: string
apiBaseUrl?: string
}
/** Use deployment configuration, never request headers, for links sent by email. */
export function getPortalLoginUrl({ portalDomain, publicUrl, apiBaseUrl }: PortalUrlSettings) {
const domain = portalDomain?.trim()
const candidates = [
{ value: domain ? (/^https?:\/\//i.test(domain) ? domain : `https://${domain}`) : undefined },
{ value: publicUrl?.trim() },
{ value: apiBaseUrl?.trim(), originOnly: true },
]
for (const { value, originOnly } of candidates) {
if (!value) continue
try {
const url = new URL(value)
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) continue
url.pathname = `${originOnly ? "" : url.pathname.replace(/\/+$/, "")}/login`
url.search = ""
url.hash = ""
return url.toString()
} catch { /* Try the next configured address. */ }
}
throw new Error("Für die Portal-Einladung fehlt eine gültige öffentliche FEDEO-Adresse.")
}