Skip to content

Commit 888f789

Browse files
committed
fix ui
1 parent 78dcaf2 commit 888f789

File tree

10 files changed

+1509
-525
lines changed

10 files changed

+1509
-525
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { db } from '@sim/db'
2+
import { credentialSet, credentialSetInvitation, member, organization, user } from '@sim/db/schema'
3+
import { createLogger } from '@sim/logger'
4+
import { and, eq } from 'drizzle-orm'
5+
import { type NextRequest, NextResponse } from 'next/server'
6+
import { getEmailSubject, renderPollingGroupInvitationEmail } from '@/components/emails'
7+
import { getSession } from '@/lib/auth'
8+
import { getBaseUrl } from '@/lib/core/utils/urls'
9+
import { sendEmail } from '@/lib/messaging/email/mailer'
10+
11+
const logger = createLogger('CredentialSetInviteResend')
12+
13+
async function getCredentialSetWithAccess(credentialSetId: string, userId: string) {
14+
const [set] = await db
15+
.select({
16+
id: credentialSet.id,
17+
organizationId: credentialSet.organizationId,
18+
name: credentialSet.name,
19+
providerId: credentialSet.providerId,
20+
})
21+
.from(credentialSet)
22+
.where(eq(credentialSet.id, credentialSetId))
23+
.limit(1)
24+
25+
if (!set) return null
26+
27+
const [membership] = await db
28+
.select({ role: member.role })
29+
.from(member)
30+
.where(and(eq(member.userId, userId), eq(member.organizationId, set.organizationId)))
31+
.limit(1)
32+
33+
if (!membership) return null
34+
35+
return { set, role: membership.role }
36+
}
37+
38+
export async function POST(
39+
req: NextRequest,
40+
{ params }: { params: Promise<{ id: string; invitationId: string }> }
41+
) {
42+
const session = await getSession()
43+
44+
if (!session?.user?.id) {
45+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
46+
}
47+
48+
const { id, invitationId } = await params
49+
50+
try {
51+
const result = await getCredentialSetWithAccess(id, session.user.id)
52+
53+
if (!result) {
54+
return NextResponse.json({ error: 'Credential set not found' }, { status: 404 })
55+
}
56+
57+
if (result.role !== 'admin' && result.role !== 'owner') {
58+
return NextResponse.json({ error: 'Admin or owner permissions required' }, { status: 403 })
59+
}
60+
61+
const [invitation] = await db
62+
.select()
63+
.from(credentialSetInvitation)
64+
.where(
65+
and(
66+
eq(credentialSetInvitation.id, invitationId),
67+
eq(credentialSetInvitation.credentialSetId, id)
68+
)
69+
)
70+
.limit(1)
71+
72+
if (!invitation) {
73+
return NextResponse.json({ error: 'Invitation not found' }, { status: 404 })
74+
}
75+
76+
if (invitation.status !== 'pending') {
77+
return NextResponse.json({ error: 'Only pending invitations can be resent' }, { status: 400 })
78+
}
79+
80+
// Update expiration
81+
const newExpiresAt = new Date()
82+
newExpiresAt.setDate(newExpiresAt.getDate() + 7)
83+
84+
await db
85+
.update(credentialSetInvitation)
86+
.set({ expiresAt: newExpiresAt })
87+
.where(eq(credentialSetInvitation.id, invitationId))
88+
89+
const inviteUrl = `${getBaseUrl()}/credential-account/${invitation.token}`
90+
91+
// Send email if email address exists
92+
if (invitation.email) {
93+
try {
94+
const [inviter] = await db
95+
.select({ name: user.name })
96+
.from(user)
97+
.where(eq(user.id, session.user.id))
98+
.limit(1)
99+
100+
const [org] = await db
101+
.select({ name: organization.name })
102+
.from(organization)
103+
.where(eq(organization.id, result.set.organizationId))
104+
.limit(1)
105+
106+
const provider = (result.set.providerId as 'google-email' | 'outlook') || 'google-email'
107+
const emailHtml = await renderPollingGroupInvitationEmail({
108+
inviterName: inviter?.name || 'A team member',
109+
organizationName: org?.name || 'your organization',
110+
pollingGroupName: result.set.name,
111+
provider,
112+
inviteLink: inviteUrl,
113+
})
114+
115+
const emailResult = await sendEmail({
116+
to: invitation.email,
117+
subject: getEmailSubject('polling-group-invitation'),
118+
html: emailHtml,
119+
emailType: 'transactional',
120+
})
121+
122+
if (!emailResult.success) {
123+
logger.warn('Failed to resend invitation email', {
124+
email: invitation.email,
125+
error: emailResult.message,
126+
})
127+
return NextResponse.json({ error: 'Failed to send email' }, { status: 500 })
128+
}
129+
} catch (emailError) {
130+
logger.error('Error sending invitation email', emailError)
131+
return NextResponse.json({ error: 'Failed to send email' }, { status: 500 })
132+
}
133+
}
134+
135+
logger.info('Resent credential set invitation', {
136+
credentialSetId: id,
137+
invitationId,
138+
userId: session.user.id,
139+
})
140+
141+
return NextResponse.json({ success: true })
142+
} catch (error) {
143+
logger.error('Error resending invitation', error)
144+
return NextResponse.json({ error: 'Failed to resend invitation' }, { status: 500 })
145+
}
146+
}

apps/sim/app/api/organizations/[id]/invitations/[invitationId]/route.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@ import { createLogger } from '@sim/logger'
1515
import { and, eq } from 'drizzle-orm'
1616
import { type NextRequest, NextResponse } from 'next/server'
1717
import { z } from 'zod'
18+
import { getEmailSubject, renderInvitationEmail } from '@/components/emails'
1819
import { getSession } from '@/lib/auth'
1920
import { requireStripeClient } from '@/lib/billing/stripe-client'
21+
import { getBaseUrl } from '@/lib/core/utils/urls'
22+
import { sendEmail } from '@/lib/messaging/email/mailer'
2023

2124
const logger = createLogger('OrganizationInvitation')
2225

@@ -69,6 +72,102 @@ export async function GET(
6972
}
7073
}
7174

75+
// Resend invitation
76+
export async function POST(
77+
_request: NextRequest,
78+
{ params }: { params: Promise<{ id: string; invitationId: string }> }
79+
) {
80+
const { id: organizationId, invitationId } = await params
81+
const session = await getSession()
82+
83+
if (!session?.user?.id) {
84+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
85+
}
86+
87+
try {
88+
// Verify user is admin/owner
89+
const memberEntry = await db
90+
.select()
91+
.from(member)
92+
.where(and(eq(member.organizationId, organizationId), eq(member.userId, session.user.id)))
93+
.limit(1)
94+
95+
if (memberEntry.length === 0 || !['owner', 'admin'].includes(memberEntry[0].role)) {
96+
return NextResponse.json({ error: 'Forbidden - Admin access required' }, { status: 403 })
97+
}
98+
99+
const orgInvitation = await db
100+
.select()
101+
.from(invitation)
102+
.where(and(eq(invitation.id, invitationId), eq(invitation.organizationId, organizationId)))
103+
.then((rows) => rows[0])
104+
105+
if (!orgInvitation) {
106+
return NextResponse.json({ error: 'Invitation not found' }, { status: 404 })
107+
}
108+
109+
if (orgInvitation.status !== 'pending') {
110+
return NextResponse.json({ error: 'Can only resend pending invitations' }, { status: 400 })
111+
}
112+
113+
const org = await db
114+
.select({ name: organization.name })
115+
.from(organization)
116+
.where(eq(organization.id, organizationId))
117+
.then((rows) => rows[0])
118+
119+
const inviter = await db
120+
.select({ name: user.name })
121+
.from(user)
122+
.where(eq(user.id, session.user.id))
123+
.limit(1)
124+
125+
// Update expiration date
126+
const newExpiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7 days
127+
await db
128+
.update(invitation)
129+
.set({ expiresAt: newExpiresAt })
130+
.where(eq(invitation.id, invitationId))
131+
132+
// Send email
133+
const emailHtml = await renderInvitationEmail(
134+
inviter[0]?.name || 'Someone',
135+
org?.name || 'organization',
136+
`${getBaseUrl()}/invite/${invitationId}`
137+
)
138+
139+
const emailResult = await sendEmail({
140+
to: orgInvitation.email,
141+
subject: getEmailSubject('invitation'),
142+
html: emailHtml,
143+
emailType: 'transactional',
144+
})
145+
146+
if (!emailResult.success) {
147+
logger.error('Failed to resend invitation email', {
148+
email: orgInvitation.email,
149+
error: emailResult.message,
150+
})
151+
return NextResponse.json({ error: 'Failed to send invitation email' }, { status: 500 })
152+
}
153+
154+
logger.info('Organization invitation resent', {
155+
organizationId,
156+
invitationId,
157+
resentBy: session.user.id,
158+
email: orgInvitation.email,
159+
})
160+
161+
return NextResponse.json({
162+
success: true,
163+
message: 'Invitation resent successfully',
164+
})
165+
} catch (error) {
166+
logger.error('Error resending organization invitation:', error)
167+
return NextResponse.json({ error: 'Failed to resend invitation' }, { status: 500 })
168+
}
169+
}
170+
72171
export async function PUT(
73172
req: NextRequest,
74173
{ params }: { params: Promise<{ id: string; invitationId: string }> }

0 commit comments

Comments
 (0)