'use client' import { useEffect, useMemo, useState } from 'react' import { useRouter } from 'next/navigation' import { authFetch, clearToken, getApiBase, getToken } from '../../lib/auth' type ProfileInfo = { username: string role: string auth_provider: string invite_management_enabled?: boolean } type ProfileResponse = { user: ProfileInfo } type OwnedInvite = { id: number code: string label?: string | null description?: string | null recipient_email?: string | null max_uses?: number | null use_count: number remaining_uses?: number | null enabled: boolean expires_at?: string | null is_expired?: boolean is_usable?: boolean created_at?: string | null updated_at?: string | null } type OwnedInvitesResponse = { invites?: OwnedInvite[] count?: number invite_access?: { enabled?: boolean managed_by_master?: boolean } master_invite?: { id: number code: string label?: string | null description?: string | null max_uses?: number | null enabled?: boolean expires_at?: string | null is_usable?: boolean } | null } type OwnedInviteForm = { code: string label: string description: string recipient_email: string max_uses: string expires_at: string enabled: boolean send_email: boolean message: string } const defaultOwnedInviteForm = (): OwnedInviteForm => ({ code: '', label: '', description: '', recipient_email: '', max_uses: '', expires_at: '', enabled: true, send_email: false, message: '', }) const formatDate = (value?: string | null) => { if (!value) return 'Never' const date = new Date(value) if (Number.isNaN(date.valueOf())) return value return date.toLocaleString() } export default function ProfileInvitesPage() { const router = useRouter() const [profile, setProfile] = useState(null) const [inviteStatus, setInviteStatus] = useState(null) const [inviteError, setInviteError] = useState(null) const [invites, setInvites] = useState([]) const [inviteSaving, setInviteSaving] = useState(false) const [inviteEditingId, setInviteEditingId] = useState(null) const [inviteForm, setInviteForm] = useState(defaultOwnedInviteForm()) const [inviteAccessEnabled, setInviteAccessEnabled] = useState(false) const [inviteManagedByMaster, setInviteManagedByMaster] = useState(false) const [masterInviteTemplate, setMasterInviteTemplate] = useState(null) const [loading, setLoading] = useState(true) const signupBaseUrl = useMemo(() => { if (typeof window === 'undefined') return '/signup' return `${window.location.origin}/signup` }, []) const loadPage = async () => { const baseUrl = getApiBase() const [profileResponse, invitesResponse] = await Promise.all([ authFetch(`${baseUrl}/auth/profile`), authFetch(`${baseUrl}/auth/profile/invites`), ]) if (!profileResponse.ok || !invitesResponse.ok) { if (profileResponse.status === 401 || invitesResponse.status === 401) { clearToken() router.push('/login') return } throw new Error('Could not load invite tools.') } const [profileData, inviteData] = (await Promise.all([ profileResponse.json(), invitesResponse.json(), ])) as [ProfileResponse, OwnedInvitesResponse] const user = profileData?.user ?? {} setProfile({ username: user?.username ?? 'Unknown', role: user?.role ?? 'user', auth_provider: user?.auth_provider ?? 'local', invite_management_enabled: Boolean(user?.invite_management_enabled ?? false), }) setInvites(Array.isArray(inviteData?.invites) ? inviteData.invites : []) setInviteAccessEnabled(Boolean(inviteData?.invite_access?.enabled ?? false)) setInviteManagedByMaster(Boolean(inviteData?.invite_access?.managed_by_master ?? false)) setMasterInviteTemplate(inviteData?.master_invite ?? null) } useEffect(() => { if (!getToken()) { router.push('/login') return } const load = async () => { try { await loadPage() } catch (err) { console.error(err) setInviteError(err instanceof Error ? err.message : 'Could not load invite tools.') } finally { setLoading(false) } } void load() }, [router]) const resetInviteEditor = () => { setInviteEditingId(null) setInviteForm(defaultOwnedInviteForm()) } const editInvite = (invite: OwnedInvite) => { setInviteEditingId(invite.id) setInviteError(null) setInviteStatus(null) setInviteForm({ code: invite.code ?? '', label: invite.label ?? '', description: invite.description ?? '', recipient_email: invite.recipient_email ?? '', max_uses: typeof invite.max_uses === 'number' ? String(invite.max_uses) : '', expires_at: invite.expires_at ?? '', enabled: invite.enabled !== false, send_email: false, message: '', }) } const reloadInvites = async () => { const baseUrl = getApiBase() const response = await authFetch(`${baseUrl}/auth/profile/invites`) if (!response.ok) { if (response.status === 401) { clearToken() router.push('/login') return } throw new Error(`Invite refresh failed: ${response.status}`) } const data = (await response.json()) as OwnedInvitesResponse setInvites(Array.isArray(data?.invites) ? data.invites : []) setInviteAccessEnabled(Boolean(data?.invite_access?.enabled ?? false)) setInviteManagedByMaster(Boolean(data?.invite_access?.managed_by_master ?? false)) setMasterInviteTemplate(data?.master_invite ?? null) } const saveInvite = async (event: React.FormEvent) => { event.preventDefault() setInviteSaving(true) setInviteError(null) setInviteStatus(null) try { const baseUrl = getApiBase() const response = await authFetch( inviteEditingId == null ? `${baseUrl}/auth/profile/invites` : `${baseUrl}/auth/profile/invites/${inviteEditingId}`, { method: inviteEditingId == null ? 'POST' : 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code: inviteForm.code || null, label: inviteForm.label || null, description: inviteForm.description || null, recipient_email: inviteForm.recipient_email || null, max_uses: inviteForm.max_uses || null, expires_at: inviteForm.expires_at || null, enabled: inviteForm.enabled, send_email: inviteForm.send_email, message: inviteForm.message || null, }), } ) if (!response.ok) { if (response.status === 401) { clearToken() router.push('/login') return } const text = await response.text() throw new Error(text || 'Invite save failed') } const data = await response.json().catch(() => ({})) if (data?.email?.status === 'ok') { setInviteStatus( `${inviteEditingId == null ? 'Invite created' : 'Invite updated'} and email sent to ${data.email.recipient_email}.` ) } else if (data?.email?.status === 'error') { setInviteStatus( `${inviteEditingId == null ? 'Invite created' : 'Invite updated'}, but email failed: ${data.email.detail}` ) } else { setInviteStatus(inviteEditingId == null ? 'Invite created.' : 'Invite updated.') } resetInviteEditor() await reloadInvites() } catch (err) { console.error(err) setInviteError(err instanceof Error ? err.message : 'Could not save invite.') } finally { setInviteSaving(false) } } const deleteInvite = async (invite: OwnedInvite) => { if (!window.confirm(`Delete invite "${invite.code}"?`)) return setInviteError(null) setInviteStatus(null) try { const baseUrl = getApiBase() const response = await authFetch(`${baseUrl}/auth/profile/invites/${invite.id}`, { method: 'DELETE', }) if (!response.ok) { if (response.status === 401) { clearToken() router.push('/login') return } const text = await response.text() throw new Error(text || 'Invite delete failed') } if (inviteEditingId === invite.id) { resetInviteEditor() } setInviteStatus(`Deleted invite ${invite.code}.`) await reloadInvites() } catch (err) { console.error(err) setInviteError(err instanceof Error ? err.message : 'Could not delete invite.') } } const copyInviteLink = async (invite: OwnedInvite) => { const url = `${signupBaseUrl}?code=${encodeURIComponent(invite.code)}` try { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(url) setInviteStatus(`Copied invite link for ${invite.code}.`) } else { window.prompt('Copy invite link', url) } } catch (err) { console.error(err) window.prompt('Copy invite link', url) } } const canManageInvites = profile?.role === 'admin' || inviteAccessEnabled if (loading) { return
Loading invite tools...
} return (

My invites

Create invite links, email them directly, and track who you have invited.

{profile ? (
Signed in as {profile.username} ({profile.role}).
) : null}
{inviteError &&
{inviteError}
} {inviteStatus &&
{inviteStatus}
} {!canManageInvites ? (

Invite access is disabled

Your account is not currently allowed to create self-service invites. Ask an administrator to enable invite access for your profile.

) : (

Invite workspace

{inviteManagedByMaster ? 'Create and manage invite links you have issued. New invites use the admin master invite rule.' : 'Create and manage invite links you have issued. New invites use your account defaults.'}

{inviteEditingId == null ? 'Create invite' : 'Edit invite'}

Save a recipient email, send the invite immediately, and keep the generated link ready to copy.

{inviteManagedByMaster && masterInviteTemplate ? (
Using master invite rule {masterInviteTemplate.code} {masterInviteTemplate.label ? ` (${masterInviteTemplate.label})` : ''}. Limits and status are managed by admin.
) : null}
Identity Optional code and label for easier tracking.
Description Optional note shown on the signup page.