Skip to content

Commit 2bf254d

Browse files
authored
Fix invite captcha handling for org invites (#1539)
* fix(invite): reset captcha and relax token checks * fix(invite): allow optional captcha token * refactor(invite): reuse captcha reset
1 parent 4c38b54 commit 2bf254d

File tree

2 files changed

+24
-15
lines changed

2 files changed

+24
-15
lines changed

src/pages/settings/organization/Members.vue

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@ async function checkRbacEnabled() {
123123
124124
const isInviteNewUserDialogOpen = ref(false)
125125
126+
function resetInviteCaptcha() {
127+
if (captchaElement.value) {
128+
captchaElement.value.reset()
129+
}
130+
captchaToken.value = ''
131+
updateInviteNewUserButton()
132+
}
133+
126134
function updateInviteNewUserButton() {
127135
const buttons = dialogStore.dialogOptions?.buttons
128136
if (!buttons)
@@ -1032,9 +1040,7 @@ async function showInviteNewUserDialog(email: string, roleType: Database['public
10321040
isInviteNewUserDialogOpen.value = true
10331041
10341042
// Reset captcha if available
1035-
if (captchaElement.value) {
1036-
captchaElement.value.reset()
1037-
}
1043+
resetInviteCaptcha()
10381044
10391045
dialogStore.openDialog({
10401046
title: t('invite-new-user-dialog-header', 'Invite New User'),
@@ -1101,6 +1107,7 @@ async function handleInviteNewUserSubmit() {
11011107
if (error) {
11021108
console.error('Invitation failed:', error)
11031109
toast.error(t('invitation-failed', 'Invitation failed'))
1110+
resetInviteCaptcha()
11041111
return false
11051112
}
11061113
@@ -1116,6 +1123,7 @@ async function handleInviteNewUserSubmit() {
11161123
catch (error) {
11171124
console.error('Invitation failed:', error)
11181125
toast.error(t('invitation-failed', 'Invitation failed'))
1126+
resetInviteCaptcha()
11191127
return false
11201128
}
11211129
finally {

supabase/functions/_backend/private/invite_new_user_to_org.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const inviteUserSchema = z.object({
2626
'org_admin',
2727
'org_super_admin',
2828
]),
29-
captcha_token: z.string().check(z.minLength(1)),
29+
captcha_token: z.optional(z.string().check(z.minLength(1))),
3030
first_name: z.string().check(z.minLength(1)),
3131
last_name: z.string().check(z.minLength(1)),
3232
})
@@ -108,7 +108,13 @@ async function validateInvite(c: Context, rawBody: any) {
108108
}
109109

110110
// Verify captcha token with Cloudflare Turnstile
111-
await verifyCaptchaToken(c, body.captcha_token)
111+
const captchaSecret = getEnv(c, 'CAPTCHA_SECRET_KEY')
112+
if (captchaSecret.length > 0) {
113+
if (!body.captcha_token) {
114+
throw simpleError('invalid_request', 'Captcha token is required')
115+
}
116+
await verifyCaptchaToken(c, body.captcha_token, captchaSecret)
117+
}
112118

113119
// Use authenticated client - RLS will enforce access based on JWT
114120
const supabase = supabaseClient(c, authorization)
@@ -236,29 +242,24 @@ app.post('/', middlewareAuth, async (c) => {
236242
invited_first_name: `${newInvitation?.first_name ?? body.first_name}`,
237243
invited_last_name: `${newInvitation?.last_name ?? body.last_name}`,
238244
}, 'org:invite_new_capgo_user_to_org')
239-
if (!bentoEvent) {
240-
throw simpleError('failed_to_invite_user', 'Failed to invite user', {}, 'Failed to track bento event')
245+
if (bentoEvent === false) {
246+
cloudlog({ requestId: c.get('requestId'), context: 'invite_new_user_to_org bento', message: 'Failed to track bento event' })
241247
}
242248
return c.json(BRES)
243249
})
244250

245251
// Function to verify Cloudflare Turnstile token
246-
async function verifyCaptchaToken(c: Context, token: string) {
247-
const captchaSecret = getEnv(c, 'CAPTCHA_SECRET_KEY')
248-
if (!captchaSecret) {
249-
throw simpleError('captcha_secret_key_not_set', 'CAPTCHA_SECRET_KEY not set')
250-
}
251-
252+
async function verifyCaptchaToken(c: Context, token: string, captchaSecret: string) {
252253
// "/siteverify" API endpoint.
253254
const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'
254255
const result = await fetch(url, {
255-
body: JSON.stringify({
256+
body: new URLSearchParams({
256257
secret: captchaSecret,
257258
response: token,
258259
}),
259260
method: 'POST',
260261
headers: {
261-
'Content-Type': 'application/json',
262+
'Content-Type': 'application/x-www-form-urlencoded',
262263
},
263264
})
264265

0 commit comments

Comments
 (0)