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:
smueller
2026-07-23 11:28:58 +02:00
parent e24f99ad38
commit 04e9ffad28
27 changed files with 1274 additions and 207 deletions

View File

@@ -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>
);