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
+30 -11
View File
@@ -2,11 +2,18 @@ import { NextResponse } from 'next/server';
import { db, posts, likes, users, notifications } from '@/db';
import { requireAuth } from '@/lib/auth';
import { requireSignedAction, type SignedAction } from '@/lib/auth/verify-signature';
import { eq, and } from 'drizzle-orm';
import { eq, and, sql } from 'drizzle-orm';
import { z } from 'zod';
import crypto from 'crypto';
type RouteContext = { params: Promise<{ id: string }> };
// UUID or swarm post ID format (swarm:domain:uuid)
const postIdSchema = z.union([
z.string().uuid(),
z.string().regex(/^swarm:[^:]+:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, 'Invalid swarm post ID format'),
]);
/**
* Extract domain from a swarm post ID (swarm:domain:postId)
*/
@@ -43,9 +50,10 @@ export async function POST(request: Request, context: RouteContext) {
// Verify the signature and get the user
const user = await requireSignedAction(signedAction);
// Extract postId from the signed action data
// Extract and validate postId from the signed action data
const { postId: rawId } = signedAction.data;
const postId = decodeURIComponent(rawId);
const decodedId = decodeURIComponent(rawId);
const postId = postIdSchema.parse(decodedId);
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
if (user.isSuspended || user.isSilenced) {
@@ -118,9 +126,9 @@ export async function POST(request: Request, context: RouteContext) {
postId,
});
// Update post's like count
// Update post's like count (atomic increment)
await db.update(posts)
.set({ likesCount: post.likesCount + 1 })
.set({ likesCount: sql`${posts.likesCount} + 1` })
.where(eq(posts.id, postId));
if (post.userId !== user.id) {
@@ -187,7 +195,9 @@ export async function POST(request: Request, context: RouteContext) {
console.warn(`[Swarm] Like delivery failed: ${result.error}`);
}
} catch (err) {
console.error('[Swarm] Error delivering like:', err);
// Log error with context but don't fail the request - swarm delivery is best-effort
console.error('[Like] Error delivering like to swarm:', err);
console.error('[Like] Context:', { postId: originalPostId, userId: user.id, targetDomain });
}
})();
}
@@ -197,6 +207,9 @@ export async function POST(request: Request, context: RouteContext) {
return NextResponse.json({ success: true, liked: true });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: 'Invalid post ID', details: error.issues }, { status: 400 });
}
if (error instanceof Error) {
// Handle signature verification errors
if (error.message === 'User not found' ||
@@ -222,9 +235,10 @@ export async function DELETE(request: Request, context: RouteContext) {
// Verify the signature and get the user
const user = await requireSignedAction(signedAction);
// Extract postId from the signed action data
// Extract and validate postId from the signed action data
const { postId: rawId } = signedAction.data;
const postId = decodeURIComponent(rawId);
const decodedId = decodeURIComponent(rawId);
const postId = postIdSchema.parse(decodedId);
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
if (user.isSuspended || user.isSilenced) {
@@ -289,9 +303,9 @@ export async function DELETE(request: Request, context: RouteContext) {
// Remove like
await db.delete(likes).where(eq(likes.id, existingLike.id));
// Update post's like count
// Update post's like count (atomic decrement, clamped to 0)
await db.update(posts)
.set({ likesCount: Math.max(0, post.likesCount - 1) })
.set({ likesCount: sql`GREATEST(0, ${posts.likesCount} - 1)` })
.where(eq(posts.id, postId));
// SWARM-FIRST: Deliver unlike to swarm node
@@ -320,7 +334,9 @@ export async function DELETE(request: Request, context: RouteContext) {
console.warn(`[Swarm] Unlike delivery failed: ${result.error}`);
}
} catch (err) {
console.error('[Swarm] Error delivering unlike:', err);
// Log error with context but don't fail the request - swarm delivery is best-effort
console.error('[Like] Error delivering unlike to swarm:', err);
console.error('[Like] Context:', { postId: originalPostId, userId: user.id, targetDomain });
}
})();
}
@@ -328,6 +344,9 @@ export async function DELETE(request: Request, context: RouteContext) {
return NextResponse.json({ success: true, liked: false });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: 'Invalid post ID', details: error.issues }, { status: 400 });
}
if (error instanceof Error) {
// Handle signature verification errors
if (error.message === 'User not found' ||