Enhance verification system with multi-provider support and alt-account detection
- Added support for multiple captcha providers including Google reCAPTCHA (v2 and v3), hCaptcha, and Cloudflare Turnstile. - Introduced new fields in the verification configuration for selecting captcha providers and enabling alt-account detection. - Implemented logic to handle verification attempts and flag potential alt accounts based on IP and invite code analysis. - Updated environment configuration to include necessary keys for captcha providers. - Enhanced the user interface to allow selection of captcha providers in the verification setup. - Improved backend handling of verification records to store additional data related to captcha provider usage.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import type { VerificationConfigDashboard } from '@nexumi/shared';
|
||||
import type { CaptchaProvider, VerificationConfigDashboard } from '@nexumi/shared';
|
||||
import { FieldAnchor } from '@/components/layout/field-anchor';
|
||||
import { useTranslations } from '@/components/locale-provider';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@@ -33,9 +33,14 @@ async function saveVerification(guildId: string, value: VerificationConfigDashbo
|
||||
interface VerificationFormProps {
|
||||
guildId: string;
|
||||
initialValue: VerificationConfigDashboard;
|
||||
availableCaptchaProviders: CaptchaProvider[];
|
||||
}
|
||||
|
||||
export function VerificationForm({ guildId, initialValue }: VerificationFormProps) {
|
||||
export function VerificationForm({
|
||||
guildId,
|
||||
initialValue,
|
||||
availableCaptchaProviders
|
||||
}: VerificationFormProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
@@ -44,113 +49,249 @@ export function VerificationForm({ guildId, initialValue }: VerificationFormProp
|
||||
onSave={(value) => saveVerification(guildId, value)}
|
||||
>
|
||||
{({ value, setValue }) => (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('modulePages.verification.title')}</CardTitle>
|
||||
<CardDescription>{t('modulePages.verification.description')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<FieldAnchor id="field-verify-enabled">
|
||||
<div className="flex items-center justify-between rounded-md border border-border p-4">
|
||||
<Label>{t('modulePages.verification.enabledLabel')}</Label>
|
||||
<Switch
|
||||
checked={value.enabled}
|
||||
onCheckedChange={(checked) => setValue((prev) => ({ ...prev, enabled: checked }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('modulePages.verification.title')}</CardTitle>
|
||||
<CardDescription>{t('modulePages.verification.description')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<FieldAnchor id="field-verify-enabled">
|
||||
<div className="flex items-center justify-between rounded-md border border-border p-4">
|
||||
<Label>{t('modulePages.verification.enabledLabel')}</Label>
|
||||
<Switch
|
||||
checked={value.enabled}
|
||||
onCheckedChange={(checked) => setValue((prev) => ({ ...prev, enabled: checked }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<FieldAnchor id="field-verify-mode">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.verification.mode')}</Label>
|
||||
<Select
|
||||
value={value.mode}
|
||||
onValueChange={(mode) =>
|
||||
setValue((prev) => ({ ...prev, mode: mode as VerificationConfigDashboard['mode'] }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="BUTTON">{t('modulePages.verification.modes.BUTTON')}</SelectItem>
|
||||
<SelectItem value="CAPTCHA">{t('modulePages.verification.modes.CAPTCHA')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<FieldAnchor id="field-verify-mode">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.verification.mode')}</Label>
|
||||
<Select
|
||||
value={value.mode}
|
||||
onValueChange={(mode) =>
|
||||
setValue((prev) => ({ ...prev, mode: mode as VerificationConfigDashboard['mode'] }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="BUTTON">{t('modulePages.verification.modes.BUTTON')}</SelectItem>
|
||||
<SelectItem value="CAPTCHA">{t('modulePages.verification.modes.CAPTCHA')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
<FieldAnchor id="field-verify-fail">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.verification.failAction')}</Label>
|
||||
<Select
|
||||
value={value.failAction}
|
||||
onValueChange={(failAction) =>
|
||||
setValue((prev) => ({
|
||||
...prev,
|
||||
failAction: failAction as VerificationConfigDashboard['failAction']
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="KICK">{t('modulePages.verification.failActions.KICK')}</SelectItem>
|
||||
<SelectItem value="BAN">{t('modulePages.verification.failActions.BAN')}</SelectItem>
|
||||
<SelectItem value="NONE">{t('modulePages.verification.failActions.NONE')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
<FieldAnchor id="field-verify-fail">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.verification.failAction')}</Label>
|
||||
<Select
|
||||
value={value.failAction}
|
||||
onValueChange={(failAction) =>
|
||||
setValue((prev) => ({ ...prev, failAction: failAction as VerificationConfigDashboard['failAction'] }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="KICK">{t('modulePages.verification.failActions.KICK')}</SelectItem>
|
||||
<SelectItem value="BAN">{t('modulePages.verification.failActions.BAN')}</SelectItem>
|
||||
<SelectItem value="NONE">{t('modulePages.verification.failActions.NONE')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<FieldAnchor id="field-verify-channel">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.verification.channelId')}</Label>
|
||||
<DiscordChannelSelect
|
||||
guildId={guildId}
|
||||
value={value.channelId}
|
||||
onChange={(channelId) => setValue((prev) => ({ ...prev, channelId }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
<FieldAnchor id="field-verify-role">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.verification.verifiedRoleId')}</Label>
|
||||
<DiscordRoleSelect
|
||||
guildId={guildId}
|
||||
value={value.verifiedRoleId}
|
||||
onChange={(verifiedRoleId) => setValue((prev) => ({ ...prev, verifiedRoleId }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
<FieldAnchor id="field-verify-unverified">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.verification.unverifiedRoleId')}</Label>
|
||||
<DiscordRoleSelect
|
||||
guildId={guildId}
|
||||
value={value.unverifiedRoleId}
|
||||
onChange={(unverifiedRoleId) => setValue((prev) => ({ ...prev, unverifiedRoleId }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
</div>
|
||||
{value.mode === 'CAPTCHA' ? (
|
||||
<FieldAnchor id="field-verify-captcha-provider">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.verification.captchaProvider')}</Label>
|
||||
<Select
|
||||
value={value.captchaProvider}
|
||||
onValueChange={(captchaProvider) =>
|
||||
setValue((prev) => ({
|
||||
...prev,
|
||||
captchaProvider: captchaProvider as CaptchaProvider
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableCaptchaProviders.map((provider) => (
|
||||
<SelectItem key={provider} value={provider}>
|
||||
{t(`modulePages.verification.captchaProviders.${provider}`)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('modulePages.verification.captchaProviderHint')}
|
||||
</p>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
) : null}
|
||||
|
||||
<FieldAnchor id="field-verify-age">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.verification.minAccountAgeDays')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
className="w-40"
|
||||
value={value.minAccountAgeDays}
|
||||
onChange={(event) =>
|
||||
setValue((prev) => ({ ...prev, minAccountAgeDays: Number(event.target.value) || 0 }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<FieldAnchor id="field-verify-channel">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.verification.channelId')}</Label>
|
||||
<DiscordChannelSelect
|
||||
guildId={guildId}
|
||||
value={value.channelId}
|
||||
onChange={(channelId) => setValue((prev) => ({ ...prev, channelId }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
<FieldAnchor id="field-verify-role">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.verification.verifiedRoleId')}</Label>
|
||||
<DiscordRoleSelect
|
||||
guildId={guildId}
|
||||
value={value.verifiedRoleId}
|
||||
onChange={(verifiedRoleId) => setValue((prev) => ({ ...prev, verifiedRoleId }))}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
<FieldAnchor id="field-verify-unverified">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.verification.unverifiedRoleId')}</Label>
|
||||
<DiscordRoleSelect
|
||||
guildId={guildId}
|
||||
value={value.unverifiedRoleId}
|
||||
onChange={(unverifiedRoleId) =>
|
||||
setValue((prev) => ({ ...prev, unverifiedRoleId }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
</div>
|
||||
|
||||
<FieldAnchor id="field-verify-age">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.verification.minAccountAgeDays')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
className="w-40"
|
||||
value={value.minAccountAgeDays}
|
||||
onChange={(event) =>
|
||||
setValue((prev) => ({
|
||||
...prev,
|
||||
minAccountAgeDays: Number(event.target.value) || 0
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('modulePages.verification.altTitle')}</CardTitle>
|
||||
<CardDescription>{t('modulePages.verification.altDescription')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<FieldAnchor id="field-verify-alt-enabled">
|
||||
<div className="flex items-center justify-between rounded-md border border-border p-4">
|
||||
<Label>{t('modulePages.verification.altDetectionEnabled')}</Label>
|
||||
<Switch
|
||||
checked={value.altDetectionEnabled}
|
||||
onCheckedChange={(checked) =>
|
||||
setValue((prev) => ({ ...prev, altDetectionEnabled: checked }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
|
||||
<FieldAnchor id="field-verify-alt-block">
|
||||
<div className="flex items-center justify-between rounded-md border border-border p-4">
|
||||
<div className="space-y-1 pr-4">
|
||||
<Label>{t('modulePages.verification.altBlockOnMatch')}</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('modulePages.verification.altBlockOnMatchHint')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={value.altBlockOnMatch}
|
||||
onCheckedChange={(checked) =>
|
||||
setValue((prev) => ({ ...prev, altBlockOnMatch: checked }))
|
||||
}
|
||||
disabled={!value.altDetectionEnabled}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<FieldAnchor id="field-verify-alt-ip-days">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.verification.altIpWindowDays')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={365}
|
||||
value={value.altIpWindowDays}
|
||||
disabled={!value.altDetectionEnabled}
|
||||
onChange={(event) =>
|
||||
setValue((prev) => ({
|
||||
...prev,
|
||||
altIpWindowDays: Number(event.target.value) || 1
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
<FieldAnchor id="field-verify-alt-invite-hours">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.verification.altInviteWindowHours')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={720}
|
||||
value={value.altInviteWindowHours}
|
||||
disabled={!value.altDetectionEnabled}
|
||||
onChange={(event) =>
|
||||
setValue((prev) => ({
|
||||
...prev,
|
||||
altInviteWindowHours: Number(event.target.value) || 1
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
<FieldAnchor id="field-verify-alt-max-invite">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.verification.altMaxAccountsPerInvite')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={50}
|
||||
value={value.altMaxAccountsPerInvite}
|
||||
disabled={!value.altDetectionEnabled}
|
||||
onChange={(event) =>
|
||||
setValue((prev) => ({
|
||||
...prev,
|
||||
altMaxAccountsPerInvite: Number(event.target.value) || 1
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</SettingsForm>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user