Skip to content

Commit 66b186f

Browse files
dengzhaofunclaude
andauthored
fix(email): set INVITE_FROM_ADDRESS to verified o-x-o.ai domain (#164)
* fix(email): set INVITE_FROM_ADDRESS to verified o-x-o.ai domain Cloudflare Email Routing has been enabled for o-x-o.ai with MX, SPF, and DKIM records auto-configured. Update the from address from the placeholder invites@example.com to invites@o-x-o.ai so the send_email binding can actually deliver invitation, verification, and alert emails in production. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(members): resend invitation + project-scope invitation list - Add `teamId` field to `OrgInvitationRow` (Better Auth returns it, we just weren't surfacing it) - Add `useResendInvitation` hook: cancel + re-invite with the same email/role/teamId (Better Auth has no native resend endpoint) - `InvitationsTable` project scope: replace the placeholder stub with real data — filter `listInvitations` results by `session.activeTeamId`; no new server endpoint needed since Better Auth's invitation table already carries `teamId` and the existing `listInvitations` API returns it - Add "重新发送" dropdown item alongside "撤销邀请" (pending-only, shared busy-state guard across both actions) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 1202239 commit 66b186f

3 files changed

Lines changed: 72 additions & 19 deletions

File tree

apps/admin/src/components/members/InvitationsTable.tsx

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MailQuestionIcon, MoreHorizontalIcon } from "lucide-react"
1+
import { MoreHorizontalIcon, RefreshCwIcon } from "lucide-react"
22
import { toast } from "sonner"
33
import * as m from "#/paraglide/messages.js"
44

@@ -24,6 +24,7 @@ import { cn } from "#/lib/utils"
2424
import {
2525
useCancelInvitation,
2626
useOrgInvitations,
27+
useResendInvitation,
2728
} from "#/hooks/use-members"
2829

2930
interface Props {
@@ -46,28 +47,22 @@ const STATUS_LABEL: Record<string, string> = {
4647
canceled: "已撤销",
4748
}
4849

49-
/**
50-
* 邀请列表 — 当前只接 org-level Better Auth invitation。
51-
* 项目级邀请(scope="project")依赖自家 server endpoint,PR 4 接入。
52-
*/
5350
export function InvitationsTable({ scope }: Props) {
5451
const { data: session } = authClient.useSession()
5552
const orgId = session?.session.activeOrganizationId ?? null
56-
const orgInvitations = useOrgInvitations(scope === "org" ? orgId : null)
53+
const activeTeamId = (session?.session as { activeTeamId?: string | null })?.activeTeamId ?? null
54+
55+
// Always fetch all org invitations; filter client-side for project scope.
56+
const orgInvitations = useOrgInvitations(orgId)
5757
const cancel = useCancelInvitation()
58+
const resend = useResendInvitation()
5859

59-
if (scope === "project") {
60-
return (
61-
<div className="rounded-md border p-12 text-center">
62-
<MailQuestionIcon className="mx-auto size-8 text-muted-foreground/60" />
63-
<p className="mt-2 text-sm text-muted-foreground">
64-
项目级邀请后端正在接入中。当前可在 组织 → 邀请 中向项目邀请成员。
65-
</p>
66-
</div>
67-
)
68-
}
60+
const allRows = orgInvitations.data ?? []
61+
const rows =
62+
scope === "project"
63+
? allRows.filter((r) => r.teamId === activeTeamId)
64+
: allRows
6965

70-
const rows = orgInvitations.data ?? []
7166
const isLoading = orgInvitations.isLoading
7267

7368
return (
@@ -113,6 +108,7 @@ export function InvitationsTable({ scope }: Props) {
113108
? new Date(row.expiresAt).toLocaleDateString()
114109
: "—"
115110
const isPending = row.status === "pending"
111+
const isBusy = cancel.isPending || resend.isPending
116112
return (
117113
<TableRow key={row.id}>
118114
<TableCell className="font-medium">{row.email}</TableCell>
@@ -144,9 +140,24 @@ export function InvitationsTable({ scope }: Props) {
144140
}
145141
/>
146142
<DropdownMenuContent align="end">
143+
<DropdownMenuItem
144+
disabled={isBusy}
145+
onClick={() =>
146+
resend.mutate(
147+
{ invitation: row, organizationId: orgId! },
148+
{
149+
onSuccess: () => toast.success("已重新发送邀请"),
150+
onError: (err) => toast.error(err.message),
151+
},
152+
)
153+
}
154+
>
155+
<RefreshCwIcon className="size-4" />
156+
重新发送
157+
</DropdownMenuItem>
147158
<DropdownMenuItem
148159
variant="destructive"
149-
disabled={cancel.isPending}
160+
disabled={isBusy}
150161
onClick={() =>
151162
cancel.mutate(
152163
{ invitationId: row.id },

apps/admin/src/hooks/use-members.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export type OrgInvitationRow = {
3030
organizationId: string
3131
email: string
3232
role: string | null
33+
teamId: string | null
3334
status: string
3435
expiresAt: string | null
3536
inviterId: string | null
@@ -180,6 +181,47 @@ export function useCancelInvitation() {
180181
})
181182
}
182183

184+
export function useResendInvitation() {
185+
const qc = useQueryClient()
186+
return useMutation({
187+
mutationFn: async (args: {
188+
invitation: OrgInvitationRow
189+
organizationId: string
190+
}) => {
191+
const { invitation, organizationId } = args
192+
// Better Auth has no native resend — cancel then re-invite with the same params.
193+
const { error: cancelErr } = await (
194+
authClient.organization as unknown as {
195+
cancelInvitation: (a: {
196+
invitationId: string
197+
}) => Promise<{ error?: { message?: string } | null }>
198+
}
199+
).cancelInvitation({ invitationId: invitation.id })
200+
if (cancelErr) throw new Error(cancelErr.message ?? "撤销失败")
201+
202+
const { error: inviteErr } = await (
203+
authClient.organization as unknown as {
204+
inviteMember: (a: {
205+
email: string
206+
role: string
207+
organizationId: string
208+
teamId?: string
209+
}) => Promise<{ error?: { message?: string } | null }>
210+
}
211+
).inviteMember({
212+
email: invitation.email,
213+
role: invitation.role ?? "member",
214+
organizationId,
215+
...(invitation.teamId ? { teamId: invitation.teamId } : {}),
216+
})
217+
if (inviteErr) throw new Error(inviteErr.message ?? "重发失败")
218+
},
219+
onSuccess: () => {
220+
qc.invalidateQueries({ queryKey: ["org-invitations"] })
221+
},
222+
})
223+
}
224+
183225
/**
184226
* 项目成员(teamMember 表)hooks —— 当前 Better Auth client 没有
185227
* 现成的 listTeamMembers,我们通过自家 server endpoint

apps/server/wrangler.jsonc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@
128128
"vars": {
129129
"APP_VERSION": "0.1.13",
130130
"ADMIN_URL": "https://apollokit-admin.limitless-ai.workers.dev",
131-
"INVITE_FROM_ADDRESS": "invites@example.com",
131+
"INVITE_FROM_ADDRESS": "invites@o-x-o.ai",
132132
"SENTRY_ENVIRONMENT": "production",
133133
// Cookie domain shared between server end-user auth and the pages
134134
// worker. Empty in dev (chrome accepts cookies on bare `localhost`

0 commit comments

Comments
 (0)