feat: user-owned S3-compatible storage with credential verification
This commit is contained in:
@@ -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' ||
|
||||
|
||||
Reference in New Issue
Block a user