feat: user-owned S3-compatible storage with credential verification

This commit is contained in:
Christomatt
2026-02-01 02:51:52 +01:00
parent a8252a1809
commit f55dec9a60
38 changed files with 1845 additions and 902 deletions
+34 -3
View File
@@ -7,9 +7,19 @@
import { NextRequest, NextResponse } from 'next/server';
import { db, posts, likes, users, remoteLikes } from '@/db';
import { eq, and } from 'drizzle-orm';
import { z } from 'zod';
type RouteContext = { params: Promise<{ id: string }> };
// Schema for post ID parameter
const postIdSchema = z.string().uuid('Invalid post ID format');
// Schema for query parameters
const likesQuerySchema = z.object({
checkHandle: z.string().min(3).max(30).optional(),
checkDomain: z.string().min(1).max(100).optional(),
});
/**
* GET /api/swarm/posts/[id]/likes
*
@@ -24,10 +34,28 @@ export async function GET(request: NextRequest, context: RouteContext) {
return NextResponse.json({ error: 'Database not available' }, { status: 503 });
}
const { id: postId } = await context.params;
const { id: rawId } = await context.params;
// Validate post ID
const idResult = postIdSchema.safeParse(rawId);
if (!idResult.success) {
return NextResponse.json({ error: 'Invalid post ID', details: idResult.error.issues }, { status: 400 });
}
const postId = idResult.data;
const { searchParams } = new URL(request.url);
const checkHandle = searchParams.get('checkHandle');
const checkDomain = searchParams.get('checkDomain');
// Validate query parameters
const queryResult = likesQuerySchema.safeParse({
checkHandle: searchParams.get('checkHandle') || undefined,
checkDomain: searchParams.get('checkDomain') || undefined,
});
if (!queryResult.success) {
return NextResponse.json({ error: 'Invalid query parameters', details: queryResult.error.issues }, { status: 400 });
}
const { checkHandle, checkDomain } = queryResult.data;
// Find the post
const post = await db.query.posts.findFirst({
@@ -94,6 +122,9 @@ export async function GET(request: NextRequest, context: RouteContext) {
likesCount: post.likesCount,
});
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: 'Invalid input', details: error.issues }, { status: 400 });
}
console.error('[Swarm] Post likes error:', error);
return NextResponse.json({ error: 'Failed to get likes' }, { status: 500 });
}
+17 -1
View File
@@ -7,9 +7,12 @@
import { NextRequest, NextResponse } from 'next/server';
import { db, posts } from '@/db';
import { eq, desc, and } from 'drizzle-orm';
import { z } from 'zod';
type RouteContext = { params: Promise<{ id: string }> };
const uuidSchema = z.string().uuid();
/**
* GET /api/swarm/posts/[id]
*
@@ -21,7 +24,14 @@ export async function GET(request: NextRequest, context: RouteContext) {
return NextResponse.json({ error: 'Database not available' }, { status: 503 });
}
const { id: postId } = await context.params;
const { id: postIdRaw } = await context.params;
// Validate postId is a valid UUID
const postIdValidation = uuidSchema.safeParse(postIdRaw);
if (!postIdValidation.success) {
return NextResponse.json({ error: 'Invalid post ID format' }, { status: 400 });
}
const postId = postIdValidation.data;
// Find the post
const post = await db.query.posts.findFirst({
@@ -97,6 +107,12 @@ export async function GET(request: NextRequest, context: RouteContext) {
}),
});
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Invalid input', details: error.issues },
{ status: 400 }
);
}
console.error('[Swarm] Post detail error:', error);
return NextResponse.json({ error: 'Failed to get post' }, { status: 500 });
}