From 3256181bc7e8d74eea768a2183b868d881c27ba6 Mon Sep 17 00:00:00 2001 From: veemakama Date: Sat, 27 Jun 2026 12:28:16 +0100 Subject: [PATCH] Updated file Project Updated file --- app/api/snippets/[id]/route.ts | 3 +- app/api/snippets/ipfs/route.ts | 73 ++++++++++++++++++++++++ app/api/snippets/route.ts | 9 +-- app/api/snippets/snippet.repository.ts | 48 ++++------------ app/api/snippets/snippet.service.ts | 60 ++++++++++++++++++-- lib/ipfs.ts | 78 ++++++++++++++++++++++++++ scripts/add-ipfs-cid.sql | 5 ++ scripts/init-db.sql | 2 + 8 files changed, 231 insertions(+), 47 deletions(-) create mode 100644 app/api/snippets/ipfs/route.ts create mode 100644 lib/ipfs.ts create mode 100644 scripts/add-ipfs-cid.sql diff --git a/app/api/snippets/[id]/route.ts b/app/api/snippets/[id]/route.ts index f760163..6d6f5c6 100644 --- a/app/api/snippets/[id]/route.ts +++ b/app/api/snippets/[id]/route.ts @@ -164,7 +164,8 @@ export async function PUT( } const body = await req.json(); - const snippet = await service.updateSnippet(id, body); + const { storeOnIpfs = false, ...snippetData } = body; + const snippet = await service.updateSnippet(id, snippetData, storeOnIpfs); // Log update if (walletAddress) { diff --git a/app/api/snippets/ipfs/route.ts b/app/api/snippets/ipfs/route.ts new file mode 100644 index 0000000..527f74f --- /dev/null +++ b/app/api/snippets/ipfs/route.ts @@ -0,0 +1,73 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { SnippetService } from '../snippet.service'; +import { SnippetRepository } from '../snippet.repository'; +import { storeOnIPFS, retrieveFromIPFS } from '@/lib/ipfs'; + +const snippetRepository = new SnippetRepository(); +const snippetService = new SnippetService(snippetRepository); + +// Store content on IPFS +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { content } = body; + + if (!content) { + return NextResponse.json( + { error: 'Content is required' }, + { status: 400 } + ); + } + + const cid = await storeOnIPFS(content); + + return NextResponse.json({ + success: true, + cid, + }); + } catch (error) { + console.error('[API] Error storing content on IPFS:', error); + return NextResponse.json( + { error: 'Failed to store content on IPFS' }, + { status: 500 } + ); + } +} + +// Retrieve content from IPFS +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams; + const cid = searchParams.get('cid'); + + if (!cid) { + return NextResponse.json( + { error: 'CID is required' }, + { status: 400 } + ); + } + + const content = await retrieveFromIPFS(cid); + + // Also try to get the snippet from database if it exists + let snippet = null; + try { + snippet = await snippetService.getSnippetByIpfsCid(cid); + } catch (err) { + // Snippet not found in database is okay + } + + return NextResponse.json({ + success: true, + cid, + content, + snippet, + }); + } catch (error) { + console.error('[API] Error retrieving content from IPFS:', error); + return NextResponse.json( + { error: 'Failed to retrieve content from IPFS' }, + { status: 500 } + ); + } +} diff --git a/app/api/snippets/route.ts b/app/api/snippets/route.ts index 3415303..9fcef32 100644 --- a/app/api/snippets/route.ts +++ b/app/api/snippets/route.ts @@ -113,14 +113,15 @@ export async function POST(req: NextRequest) { } const body = await req.json(); + const { storeOnIpfs = false, ...snippetData } = body; // Extract and inject the wallet address securely from headers const walletAddress = await OwnershipMiddleware.extractWalletAddress(req); if (walletAddress) { - body.ownerWalletAddress = walletAddress; + snippetData.ownerWalletAddress = walletAddress; } - const snippet = await service.createSnippet(body); + const snippet = await service.createSnippet(snippetData, storeOnIpfs); // Log transaction if wallet address provided if (walletAddress) { @@ -129,7 +130,7 @@ export async function POST(req: NextRequest) { walletAddress, "snippet_create", `Created snippet ${snippet.id}`, - { snippetId: snippet.id }, + { snippetId: snippet.id, ipfsCid: snippet.ipfs_cid }, ); } catch (err) { console.error("[transactions] Failed to log snippet_create:", err); @@ -139,7 +140,7 @@ export async function POST(req: NextRequest) { await appendActivityLog("snippet.created", "snippet", { actorWallet: walletAddress, resourceId: snippet.id, - metadata: { title: snippet.title, language: snippet.language, tags: snippet.tags }, + metadata: { title: snippet.title, language: snippet.language, tags: snippet.tags, ipfsCid: snippet.ipfs_cid }, ipAddress: extractIp(req.headers), userAgent: extractUserAgent(req.headers), }); diff --git a/app/api/snippets/snippet.repository.ts b/app/api/snippets/snippet.repository.ts index 4659d68..46eaa40 100644 --- a/app/api/snippets/snippet.repository.ts +++ b/app/api/snippets/snippet.repository.ts @@ -127,53 +127,21 @@ export class SnippetRepository { return result[0] || null; } - async create(data: CreateSnippetDTO) { + async create(data: CreateSnippetDTO & { ipfsCid?: string }) { const id = crypto.randomUUID(); const createdAt = new Date(); const result = await this.sql` - INSERT INTO snippets (id, title, description, code, language, tags, owner_wallet_address, created_at, updated_at) - VALUES (${id}, ${data.title}, ${data.description}, ${data.code}, ${data.language}, ${data.tags}, ${data.ownerWalletAddress}, ${createdAt}, ${createdAt}) + INSERT INTO snippets (id, title, description, code, language, tags, owner_wallet_address, ipfs_cid, created_at, updated_at) + VALUES (${id}, ${data.title}, ${data.description}, ${data.code}, ${data.language}, ${data.tags}, ${data.ownerWalletAddress}, ${data.ipfsCid}, ${createdAt}, ${createdAt}) RETURNING * `; return result[0]; } - async update(id: string, data: UpdateSnippetDTO) { + async update(id: string, data: UpdateSnippetDTO & { ipfsCid?: string }) { const updatedAt = new Date(); - // Build dynamic update query using tagged template - const updates: string[] = []; - const values: any[] = []; - - if (data.title !== undefined) { - updates.push("title = ${value}"); - values.push(data.title); - } - if (data.description !== undefined) { - updates.push("description = ${value}"); - values.push(data.description); - } - if (data.code !== undefined) { - updates.push("code = ${value}"); - values.push(data.code); - } - if (data.language !== undefined) { - updates.push("language = ${value}"); - values.push(data.language); - } - if (data.tags !== undefined) { - updates.push("tags = ${value}"); - values.push(data.tags); - } - - if (updates.length === 0) { - return this.findById(id); - } - - // Build the SET clause with proper parameter placeholders - const setClause = updates.join(", "); - // Use raw SQL for dynamic updates const result = await this.sql` UPDATE snippets @@ -182,6 +150,7 @@ export class SnippetRepository { code = COALESCE(${data.code}, code), language = COALESCE(${data.language}, language), tags = COALESCE(${data.tags}, tags), + ipfs_cid = COALESCE(${data.ipfsCid}, ipfs_cid), updated_at = ${updatedAt} WHERE id = ${id} RETURNING * @@ -189,6 +158,13 @@ export class SnippetRepository { return result[0] || null; } + async findByIpfsCid(ipfsCid: string) { + const result = await this.sql` + SELECT * FROM snippets WHERE ipfs_cid = ${ipfsCid} AND is_deleted = false + `; + return result[0] || null; + } + async delete(id: string) { const result = await this.sql` DELETE FROM snippets WHERE id = ${id} RETURNING * diff --git a/app/api/snippets/snippet.service.ts b/app/api/snippets/snippet.service.ts index 38c0719..dcb5aee 100644 --- a/app/api/snippets/snippet.service.ts +++ b/app/api/snippets/snippet.service.ts @@ -6,6 +6,7 @@ import { } from "./snippet.repository"; import { createSnippetSchema, updateSnippetSchema } from "./snippet.validator"; import { ActivityLogger } from "@/lib/activity-logger"; +import { storeOnIPFS, retrieveFromIPFS } from "@/lib/ipfs"; export class SnippetService { constructor(private snippetRepository: SnippetRepository) {} @@ -43,20 +44,51 @@ export class SnippetService { } } - async createSnippet(data: unknown) { + async getSnippetByIpfsCid(ipfsCid: string) { + try { + const snippet = await this.snippetRepository.findByIpfsCid(ipfsCid); + if (!snippet) { + throw new Error("Snippet not found for this IPFS CID"); + } + return snippet; + } catch (error) { + console.error("[Service] Error fetching snippet by IPFS CID:", error); + throw error instanceof Error + ? error + : new Error("Failed to fetch snippet"); + } + } + + async createSnippet(data: unknown, storeOnIpfs: boolean = false) { // 1. Validation (Throws ZodError if invalid) const validatedData = createSnippetSchema.parse(data); - // 2. Database interaction via Repository + let ipfsCid: string | undefined; + + // 2. Store code on IPFS if requested + if (storeOnIpfs && validatedData.code) { + try { + ipfsCid = await storeOnIPFS(validatedData.code); + console.log("[Service] Snippet stored on IPFS with CID:", ipfsCid); + } catch (error) { + console.error("[Service] Error storing snippet on IPFS:", error); + // Continue even if IPFS fails - we'll still store in database + } + } + + // 3. Database interaction via Repository try { - return await this.snippetRepository.create(validatedData); + return await this.snippetRepository.create({ + ...validatedData, + ipfsCid, + }); } catch (error) { console.error("[Service] Error creating snippet:", error); throw new Error("Failed to create snippet"); } } - async updateSnippet(id: string, data: unknown) { + async updateSnippet(id: string, data: unknown, storeOnIpfs: boolean = false) { // 1. Validation const validatedData = updateSnippetSchema.parse(data); @@ -66,9 +98,25 @@ export class SnippetService { throw new Error("Snippet not found"); } - // 3. Database interaction via Repository + let ipfsCid: string | undefined = existing.ipfs_cid; + + // 3. Store updated code on IPFS if requested + if (storeOnIpfs && validatedData.code) { + try { + ipfsCid = await storeOnIPFS(validatedData.code); + console.log("[Service] Updated snippet stored on IPFS with CID:", ipfsCid); + } catch (error) { + console.error("[Service] Error storing updated snippet on IPFS:", error); + // Continue even if IPFS fails + } + } + + // 4. Database interaction via Repository try { - return await this.snippetRepository.update(id, validatedData); + return await this.snippetRepository.update(id, { + ...validatedData, + ipfsCid, + }); } catch (error) { console.error("[Service] Error updating snippet:", error); throw new Error("Failed to update snippet"); diff --git a/lib/ipfs.ts b/lib/ipfs.ts new file mode 100644 index 0000000..e4a5127 --- /dev/null +++ b/lib/ipfs.ts @@ -0,0 +1,78 @@ +import { createHelia } from 'helia'; +import { unixfs } from '@helia/unixfs'; +import { CID } from 'multiformats/cid'; + +// Singleton instance to avoid creating multiple Helia nodes +let heliaInstance: any = null; +let unixfsInstance: any = null; + +/** + * Initialize and get the Helia instance + */ +async function getHelia() { + if (!heliaInstance) { + heliaInstance = await createHelia(); + unixfsInstance = unixfs(heliaInstance); + } + return { helia: heliaInstance, fs: unixfsInstance }; +} + +/** + * Store content on IPFS and return the CID + */ +export async function storeOnIPFS(content: string): Promise { + try { + const { fs } = await getHelia(); + const encoder = new TextEncoder(); + const bytes = encoder.encode(content); + + // Add the content to IPFS + const cid = await fs.addBytes(bytes); + return cid.toString(); + } catch (error) { + console.error('[IPFS] Error storing content:', error); + throw new Error('Failed to store content on IPFS'); + } +} + +/** + * Retrieve content from IPFS using a CID + */ +export async function retrieveFromIPFS(cidString: string): Promise { + try { + const { fs } = await getHelia(); + const cid = CID.parse(cidString); + + // Read the content from IPFS + const chunks: Uint8Array[] = []; + for await (const chunk of fs.cat(cid)) { + chunks.push(chunk); + } + + // Concatenate chunks and decode to string + const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0); + const result = new Uint8Array(totalLength); + let offset = 0; + for (const chunk of chunks) { + result.set(chunk, offset); + offset += chunk.length; + } + + const decoder = new TextDecoder(); + return decoder.decode(result); + } catch (error) { + console.error('[IPFS] Error retrieving content:', error); + throw new Error('Failed to retrieve content from IPFS'); + } +} + +/** + * Shutdown the Helia node (useful for cleanup) + */ +export async function shutdownIPFS() { + if (heliaInstance) { + await heliaInstance.stop(); + heliaInstance = null; + unixfsInstance = null; + } +} diff --git a/scripts/add-ipfs-cid.sql b/scripts/add-ipfs-cid.sql new file mode 100644 index 0000000..014c10d --- /dev/null +++ b/scripts/add-ipfs-cid.sql @@ -0,0 +1,5 @@ +-- Add ipfs_cid column to snippets table +ALTER TABLE snippets ADD COLUMN IF NOT EXISTS ipfs_cid VARCHAR(255); + +-- Create index for faster lookups by ipfs_cid +CREATE INDEX IF NOT EXISTS idx_snippets_ipfs_cid ON snippets(ipfs_cid); diff --git a/scripts/init-db.sql b/scripts/init-db.sql index 976205e..dec0a1c 100644 --- a/scripts/init-db.sql +++ b/scripts/init-db.sql @@ -10,6 +10,7 @@ CREATE TABLE snippets ( description TEXT, language VARCHAR(50) NOT NULL, code TEXT NOT NULL, + ipfs_cid VARCHAR(255), tags JSONB DEFAULT '[]'::jsonb, owner_wallet_address VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, @@ -31,3 +32,4 @@ CREATE INDEX IF NOT EXISTS idx_snippets_search_vector ON snippets USING GIN ( setweight(jsonb_to_tsvector('simple', COALESCE(tags, '[]'::jsonb), '["string"]'), 'B') ) ); +CREATE INDEX IF NOT EXISTS idx_snippets_ipfs_cid ON snippets(ipfs_cid);