feat(auth): Integrate Cloudflare Turnstile bot protection and enhance swarm tracking

- Add Cloudflare Turnstile integration for login and registration forms
- Create turnstile verification utility with server-side token validation
- Add turnstile_site_key and turnstile_secret_key fields to nodes table
- Implement admin panel UI for configuring Turnstile keys in settings
- Update login and register API routes to verify Turnstile tokens
- Expose turnstile_site_key via GET /api/node endpoint for frontend
- Add admin endpoint to save and update Turnstile configuration
- Create remote_likes and remote_reposts tables for federated interaction tracking
- Add swarm reply metadata fields (swarm_reply_to_id, swarm_reply_to_content, swarm_reply_to_author) to posts table
- Add comprehensive TURNSTILE_SETUP.md documentation with setup instructions and security notes
- Create database migration 0007 with schema updates and indexes
- Protects against bot registrations and automated attacks on federated nodes
This commit is contained in:
Christomatt
2026-01-26 18:10:48 +01:00
parent cf0dfa4b66
commit 5b0269af34
16 changed files with 4403 additions and 153 deletions
+4
View File
@@ -27,6 +27,8 @@ export async function PATCH(req: NextRequest) {
faviconUrl: data.faviconUrl,
accentColor: data.accentColor,
isNsfw: data.isNsfw ?? false,
turnstileSiteKey: data.turnstileSiteKey,
turnstileSecretKey: data.turnstileSecretKey,
}).returning();
} else {
[node] = await db.update(nodes)
@@ -40,6 +42,8 @@ export async function PATCH(req: NextRequest) {
faviconUrl: data.faviconUrl,
accentColor: data.accentColor,
isNsfw: data.isNsfw ?? node.isNsfw,
turnstileSiteKey: data.turnstileSiteKey !== undefined ? data.turnstileSiteKey : node.turnstileSiteKey,
turnstileSecretKey: data.turnstileSecretKey !== undefined ? data.turnstileSecretKey : node.turnstileSecretKey,
updatedAt: new Date(),
})
.where(eq(nodes.id, node.id))
+14
View File
@@ -1,10 +1,12 @@
import { NextResponse } from 'next/server';
import { authenticateUser, createSession } from '@/lib/auth';
import { verifyTurnstileToken } from '@/lib/turnstile';
import { z } from 'zod';
const loginSchema = z.object({
email: z.string().email(),
password: z.string(),
turnstileToken: z.string().optional(),
});
export async function POST(request: Request) {
@@ -12,6 +14,18 @@ export async function POST(request: Request) {
const body = await request.json();
const data = loginSchema.parse(body);
// Verify Turnstile token if provided
if (data.turnstileToken) {
const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || undefined;
const isValid = await verifyTurnstileToken(data.turnstileToken, ip);
if (!isValid) {
return NextResponse.json(
{ error: 'Bot verification failed. Please try again.' },
{ status: 400 }
);
}
}
const user = await authenticateUser(data.email, data.password);
// Create session
+14
View File
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
import { registerUser, createSession } from '@/lib/auth';
import { db, nodes, users } from '@/db';
import { eq } from 'drizzle-orm';
import { verifyTurnstileToken } from '@/lib/turnstile';
import { z } from 'zod';
const registerSchema = z.object({
@@ -9,6 +10,7 @@ const registerSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
displayName: z.string().optional(),
turnstileToken: z.string().optional(),
});
export async function POST(request: Request) {
@@ -16,6 +18,18 @@ export async function POST(request: Request) {
const body = await request.json();
const data = registerSchema.parse(body);
// Verify Turnstile token if provided
if (data.turnstileToken) {
const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || undefined;
const isValid = await verifyTurnstileToken(data.turnstileToken, ip);
if (!isValid) {
return NextResponse.json(
{ error: 'Bot verification failed. Please try again.' },
{ status: 400 }
);
}
}
const user = await registerUser(
data.handle,
data.email,
+7 -1
View File
@@ -38,10 +38,16 @@ export async function GET() {
accentColor: process.env.NEXT_PUBLIC_ACCENT_COLOR || '#FFFFFF',
domain,
admins,
turnstileSiteKey: null,
});
}
return NextResponse.json({ ...node, admins });
return NextResponse.json({
...node,
admins,
// Don't expose the secret key
turnstileSecretKey: undefined,
});
} catch (error) {
console.error('Node info error:', error);
return NextResponse.json({