Enhance verification module with improved command handling and localization

- Updated the verification command to utilize a new `ephemeral` utility for consistent ephemeral message replies.
- Refactored the `buildVerificationPanel` function to include localization support for panel titles and descriptions.
- Added permission checks to ensure the bot can post in the designated verification channel.
- Introduced a new verification queue in the queue management system for better handling of verification tasks.
- Updated localization files to include new keys for verification-related messages in both English and German.
This commit is contained in:
smueller
2026-07-23 10:54:21 +02:00
parent 4b6736444c
commit e24f99ad38
8 changed files with 229 additions and 22 deletions

View File

@@ -0,0 +1,110 @@
import { NextResponse } from 'next/server';
import {
deleteCaptchaChallenge,
getCaptchaChallenge,
hashCaptchaAnswer
} from '@/lib/captcha';
import { getVerificationQueue } from '@/lib/queues';
function escapeHtml(value: string): string {
return value
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;');
}
function renderCaptchaPage(token: string, question: string, error?: string): string {
const errorBlock = error ? `<p style="color:#ef4444">${escapeHtml(error)}</p>` : '';
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Nexumi Verification</title>
<style>
body { font-family: Inter, system-ui, sans-serif; background:#111827; color:#f9fafb; display:flex; justify-content:center; align-items:center; min-height:100vh; margin:0; }
form { background:#1f2937; padding:2rem; border-radius:12px; width:min(420px, 90vw); }
input, button { width:100%; padding:0.75rem; margin-top:0.75rem; border-radius:8px; border:1px solid #374151; background:#111827; color:#f9fafb; box-sizing:border-box; }
button { background:#6366f1; border:none; cursor:pointer; font-weight:600; }
</style>
</head>
<body>
<form method="POST" action="/verify/captcha">
<h1>Nexumi Verification</h1>
<p>Solve: <strong>${escapeHtml(question)} = ?</strong></p>
${errorBlock}
<input type="hidden" name="token" value="${escapeHtml(token)}" />
<input type="number" name="answer" required autofocus />
<button type="submit">Verify</button>
</form>
</body>
</html>`;
}
function renderDonePage(): string {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Nexumi Verified</title>
<style>
body { font-family: Inter, system-ui, sans-serif; background:#111827; color:#f9fafb; display:flex; justify-content:center; align-items:center; min-height:100vh; margin:0; }
div { text-align:center; }
</style>
</head>
<body>
<div>
<h1>Verified</h1>
<p>You can return to Discord.</p>
</div>
</body>
</html>`;
}
function html(body: string, status = 200): NextResponse {
return new NextResponse(body, {
status,
headers: { 'Content-Type': 'text/html; charset=utf-8' }
});
}
export async function GET(request: Request): Promise<NextResponse> {
const token = new URL(request.url).searchParams.get('token');
if (!token) {
return new NextResponse('Missing token', { status: 400 });
}
const challenge = await getCaptchaChallenge(token);
if (!challenge) {
return new NextResponse('Captcha expired', { status: 404 });
}
return html(renderCaptchaPage(token, challenge.question));
}
export async function POST(request: Request): Promise<NextResponse> {
const form = await request.formData();
const token = String(form.get('token') ?? '');
const answer = String(form.get('answer') ?? '');
if (!token || !answer) {
return new NextResponse('Missing fields', { status: 400 });
}
const challenge = await getCaptchaChallenge(token);
if (!challenge) {
return new NextResponse('Captcha expired', { status: 404 });
}
if (hashCaptchaAnswer(answer) !== challenge.answerHash) {
return html(renderCaptchaPage(token, challenge.question, 'Wrong answer. Try again.'));
}
await deleteCaptchaChallenge(token);
await getVerificationQueue().add(
'verificationComplete',
{ guildId: challenge.guildId, userId: challenge.userId },
{ removeOnComplete: 1000, removeOnFail: 500 }
);
return html(renderDonePage());
}