feat: implement invite onboarding and dynamic ticket fields
This commit is contained in:
parent
29a647f6c6
commit
f24a7f68ca
34 changed files with 2240 additions and 97 deletions
98
web/src/server/invite-utils.ts
Normal file
98
web/src/server/invite-utils.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import type { AuthInvite, AuthInviteEvent } from "@prisma/client"
|
||||
|
||||
import { ROLE_OPTIONS, type RoleOption, normalizeRole } from "@/lib/authz"
|
||||
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
||||
import { env } from "@/lib/env"
|
||||
|
||||
export type InviteStatus = "pending" | "accepted" | "revoked" | "expired"
|
||||
|
||||
export type InviteWithEvents = AuthInvite & {
|
||||
events: AuthInviteEvent[]
|
||||
}
|
||||
|
||||
export type InviteEventPayload = {
|
||||
id: string
|
||||
type: string
|
||||
createdAt: string
|
||||
actorId: string | null
|
||||
payload: unknown
|
||||
}
|
||||
|
||||
export type NormalizedInvite = {
|
||||
id: string
|
||||
email: string
|
||||
name: string | null
|
||||
role: RoleOption
|
||||
tenantId: string
|
||||
status: InviteStatus
|
||||
token: string
|
||||
inviteUrl: string
|
||||
expiresAt: string
|
||||
createdAt: string
|
||||
createdById: string | null
|
||||
acceptedAt: string | null
|
||||
acceptedById: string | null
|
||||
revokedAt: string | null
|
||||
revokedById: string | null
|
||||
revokedReason: string | null
|
||||
events: InviteEventPayload[]
|
||||
}
|
||||
|
||||
const DEFAULT_APP_URL = env.NEXT_PUBLIC_APP_URL ?? env.BETTER_AUTH_URL ?? "http://localhost:3000"
|
||||
|
||||
export function computeInviteStatus(invite: AuthInvite, now: Date = new Date()): InviteStatus {
|
||||
if (invite.status === "revoked") return "revoked"
|
||||
if (invite.status === "accepted") return "accepted"
|
||||
if (invite.status === "expired") return "expired"
|
||||
if (invite.expiresAt.getTime() <= now.getTime()) {
|
||||
return "expired"
|
||||
}
|
||||
return "pending"
|
||||
}
|
||||
|
||||
export function buildInviteUrl(token: string) {
|
||||
const base = DEFAULT_APP_URL.endsWith("/") ? DEFAULT_APP_URL.slice(0, -1) : DEFAULT_APP_URL
|
||||
return `${base}/invite/${token}`
|
||||
}
|
||||
|
||||
export function normalizeRoleOption(role?: string | null): RoleOption {
|
||||
const normalized = normalizeRole(role)
|
||||
if (normalized && (ROLE_OPTIONS as readonly string[]).includes(normalized)) {
|
||||
return normalized as RoleOption
|
||||
}
|
||||
return "agent"
|
||||
}
|
||||
|
||||
export function normalizeInvite(invite: InviteWithEvents, now: Date = new Date()): NormalizedInvite {
|
||||
const status = computeInviteStatus(invite, now)
|
||||
const inviteUrl = buildInviteUrl(invite.token)
|
||||
|
||||
return {
|
||||
id: invite.id,
|
||||
email: invite.email,
|
||||
name: invite.name ?? null,
|
||||
role: normalizeRoleOption(invite.role),
|
||||
tenantId: invite.tenantId ?? DEFAULT_TENANT_ID,
|
||||
status,
|
||||
token: invite.token,
|
||||
inviteUrl,
|
||||
expiresAt: invite.expiresAt.toISOString(),
|
||||
createdAt: invite.createdAt.toISOString(),
|
||||
createdById: invite.createdById ?? null,
|
||||
acceptedAt: invite.acceptedAt ? invite.acceptedAt.toISOString() : null,
|
||||
acceptedById: invite.acceptedById ?? null,
|
||||
revokedAt: invite.revokedAt ? invite.revokedAt.toISOString() : null,
|
||||
revokedById: invite.revokedById ?? null,
|
||||
revokedReason: invite.revokedReason ?? null,
|
||||
events: invite.events
|
||||
.slice()
|
||||
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())
|
||||
.map((event) => ({
|
||||
id: event.id,
|
||||
type: event.type,
|
||||
createdAt: event.createdAt.toISOString(),
|
||||
actorId: event.actorId ?? null,
|
||||
payload: event.payload,
|
||||
})),
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue