296 lines
12 KiB
TypeScript
296 lines
12 KiB
TypeScript
'use client';
|
|
|
|
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';
|
|
import { DiscordChannelSelect } from '@/components/ui/discord-channel-select';
|
|
import { DiscordRoleSelect } from '@/components/ui/discord-role-select';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { Switch } from '@/components/ui/switch';
|
|
import type { SettingsSaveResult } from '@/components/settings/settings-form';
|
|
import { SettingsForm } from '@/components/settings/settings-form';
|
|
|
|
async function saveVerification(guildId: string, value: VerificationConfigDashboard): Promise<SettingsSaveResult> {
|
|
try {
|
|
const response = await fetch(`/api/guilds/${guildId}/verification`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(value)
|
|
});
|
|
if (!response.ok) {
|
|
const body = (await response.json().catch(() => null)) as { error?: string } | null;
|
|
return { ok: false, error: body?.error ?? `Request failed with status ${response.status}` };
|
|
}
|
|
return { ok: true };
|
|
} catch {
|
|
return { ok: false, error: 'Network error' };
|
|
}
|
|
}
|
|
|
|
interface VerificationFormProps {
|
|
guildId: string;
|
|
initialValue: VerificationConfigDashboard;
|
|
availableCaptchaProviders: CaptchaProvider[];
|
|
}
|
|
|
|
export function VerificationForm({
|
|
guildId,
|
|
initialValue,
|
|
availableCaptchaProviders
|
|
}: VerificationFormProps) {
|
|
const t = useTranslations();
|
|
|
|
return (
|
|
<SettingsForm<VerificationConfigDashboard>
|
|
initialValue={initialValue}
|
|
onSave={(value) => saveVerification(guildId, value)}
|
|
>
|
|
{({ value, setValue }) => (
|
|
<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>
|
|
</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>
|
|
|
|
{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>
|
|
</div>
|
|
</FieldAnchor>
|
|
) : null}
|
|
|
|
<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>
|
|
);
|
|
}
|