feat: add resend capability for platform and account invite codes
Revoke-and-recreate flow for both invite systems with email delivery via Resend API. Includes account invite email template and audit logging. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,11 @@ export const accountsApi = {
|
||||
const response = await apiClient.get<AccountInvite[]>('/accounts/me/invites')
|
||||
return response.data
|
||||
},
|
||||
|
||||
async resendInvite(inviteId: string): Promise<AccountInvite> {
|
||||
const response = await apiClient.post<AccountInvite>(`/accounts/me/invites/${inviteId}/resend`)
|
||||
return response.data
|
||||
},
|
||||
}
|
||||
|
||||
export default accountsApi
|
||||
|
||||
@@ -56,6 +56,8 @@ export const adminApi = {
|
||||
api.post<InviteCodeResponse>('/invites', data).then(r => r.data),
|
||||
deleteInviteCode: (code: string) =>
|
||||
api.delete(`/invites/${code}`),
|
||||
resendInviteCode: (code: string) =>
|
||||
api.post<InviteCodeResponse>(`/invites/${code}/resend`).then(r => r.data),
|
||||
|
||||
// Audit Logs
|
||||
listAuditLogs: (params?: Record<string, unknown>) =>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Building2, Users, Mail, Crown, Loader2, AlertCircle, Check, X, Settings, FolderTree } from 'lucide-react'
|
||||
import { Building2, Users, Mail, Crown, Loader2, AlertCircle, Check, X, Settings, FolderTree, RefreshCw } from 'lucide-react'
|
||||
import { accountsApi } from '@/api/accounts'
|
||||
import type { Account, AccountMember, AccountInvite } from '@/types'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -102,6 +102,22 @@ export function AccountSettingsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const [resendingId, setResendingId] = useState<string | null>(null)
|
||||
|
||||
const handleResendInvite = async (inviteId: string) => {
|
||||
setResendingId(inviteId)
|
||||
try {
|
||||
await accountsApi.resendInvite(inviteId)
|
||||
toast.success('Invite resent with a new code')
|
||||
const invitesData = await accountsApi.getInvites()
|
||||
setInvites(invitesData)
|
||||
} catch {
|
||||
toast.error('Failed to resend invite')
|
||||
} finally {
|
||||
setResendingId(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveMember = async (userId: string) => {
|
||||
try {
|
||||
await accountsApi.removeMember(userId)
|
||||
@@ -433,12 +449,28 @@ export function AccountSettingsPage() {
|
||||
<div>
|
||||
<p className="text-sm text-white">{invite.email}</p>
|
||||
<p className="text-xs text-white/40">
|
||||
Expires {new Date(invite.expires_at).toLocaleDateString()}
|
||||
{invite.expires_at
|
||||
? `Expires ${new Date(invite.expires_at).toLocaleDateString()}`
|
||||
: 'No expiration'}
|
||||
</p>
|
||||
</div>
|
||||
<span className="rounded-full bg-white/10 px-2.5 py-0.5 text-xs text-white/70">
|
||||
{invite.role}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="rounded-full bg-white/10 px-2.5 py-0.5 text-xs text-white/70">
|
||||
{invite.role}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => handleResendInvite(invite.id)}
|
||||
disabled={resendingId === invite.id}
|
||||
className="text-white/40 hover:text-white disabled:opacity-50"
|
||||
title="Resend invite"
|
||||
>
|
||||
{resendingId === invite.id ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Plus, Copy, Trash2, Ticket, Mail, MailCheck } from 'lucide-react'
|
||||
import { Plus, Copy, Trash2, Ticket, Mail, MailCheck, RefreshCw } from 'lucide-react'
|
||||
import { DataTable, PageHeader, StatusBadge, ActionMenu, EmptyState } from '@/components/admin'
|
||||
import type { Column } from '@/components/admin'
|
||||
import { Modal } from '@/components/common/Modal'
|
||||
@@ -97,6 +97,16 @@ export function InviteCodesPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleResend = async (code: string) => {
|
||||
try {
|
||||
const newInvite = await adminApi.resendInviteCode(code)
|
||||
toast.success(`New code ${newInvite.code} sent to ${newInvite.email}`)
|
||||
fetchCodes()
|
||||
} catch {
|
||||
toast.error('Failed to resend invite code')
|
||||
}
|
||||
}
|
||||
|
||||
const inputClass = cn(
|
||||
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
|
||||
'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20'
|
||||
@@ -183,6 +193,11 @@ export function InviteCodesPage() {
|
||||
icon: <Copy className="h-4 w-4" />,
|
||||
onClick: () => handleCopy(c.code),
|
||||
},
|
||||
...(c.is_valid && c.email ? [{
|
||||
label: 'Resend',
|
||||
icon: <RefreshCw className="h-4 w-4" />,
|
||||
onClick: () => handleResend(c.code),
|
||||
}] : []),
|
||||
...(!c.is_used ? [{
|
||||
label: 'Delete',
|
||||
icon: <Trash2 className="h-4 w-4" />,
|
||||
|
||||
Reference in New Issue
Block a user