Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions app/api/cron/cleanup/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { NextResponse } from "next/server";
import { CleanupService } from "@/lib/cleanup.service";

export const runtime = "nodejs";

export async function GET(request: Request) {
try {
const authHeader = request.headers.get("authorization");

// Enforce cron job authorization
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const cleanupService = new CleanupService();
const result = await cleanupService.run();

return NextResponse.json({
success: true,
message: "Scheduled cleanup jobs executed successfully.",
data: result,
});
} catch (error) {
console.error("[Cron/Cleanup] Error executing cleanup jobs:", error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : "Cleanup execution failed",
},
{ status: 500 }
);
}
}
123 changes: 123 additions & 0 deletions app/api/snippets/import/json/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { NextRequest, NextResponse } from "next/server";
import { SnippetRepository } from "../../snippet.repository";
import { SnippetService } from "../../snippet.service";
import { OwnershipMiddleware } from "../../ownership.middleware";
import { ZodError } from "zod";

const repository = new SnippetRepository();
const service = new SnippetService(repository);

export async function POST(req: NextRequest) {
try {
// 1. Authenticate user wallet
const walletAddress = await OwnershipMiddleware.extractWalletAddress(req);
if (!walletAddress) {
return NextResponse.json(
{ error: "Unauthorized", message: "Missing or invalid wallet authentication." },
{ status: 401 }
);
}

const contentType = req.headers.get("content-type") || "";
let rawSnippets: any[] = [];

// 2. Parse payload based on content type
if (contentType.includes("application/json")) {
const body = await req.json();
if (Array.isArray(body)) {
rawSnippets = body;
} else if (body && typeof body === "object") {
// If it's a single snippet wrapper object containing a snippets array
if (Array.isArray(body.snippets)) {
rawSnippets = body.snippets;
} else {
rawSnippets = [body];
}
} else {
return NextResponse.json(
{ error: "Malformed Request", message: "Invalid JSON format." },
{ status: 400 }
);
}
} else if (contentType.includes("multipart/form-data")) {
const formData = await req.formData();
const file = formData.get("file") as File | null;
if (!file) {
return NextResponse.json(
{ error: "Bad Request", message: "No file was uploaded in the 'file' field." },
{ status: 400 }
);
}

const fileText = await file.text();
let parsedData;
try {
parsedData = JSON.parse(fileText);
} catch (e) {
return NextResponse.json(
{ error: "Malformed File", message: "Uploaded file is not a valid JSON document." },
{ status: 400 }
);
}

if (Array.isArray(parsedData)) {
rawSnippets = parsedData;
} else if (parsedData && typeof parsedData === "object") {
if (Array.isArray(parsedData.snippets)) {
rawSnippets = parsedData.snippets;
} else {
rawSnippets = [parsedData];
}
} else {
return NextResponse.json(
{ error: "Malformed File", message: "Invalid snippet list format in file." },
{ status: 400 }
);
}
} else {
return NextResponse.json(
{ error: "Unsupported Media Type", message: "Content-Type must be application/json or multipart/form-data." },
{ status: 415 }
);
}

if (rawSnippets.length === 0) {
return NextResponse.json(
{ error: "Bad Request", message: "No snippets found to import." },
{ status: 400 }
);
}

// 3. Delegate to service layer
const result = await service.importSnippets(rawSnippets, walletAddress);

// If there were only errors and no imports/duplicates
if (result.errors.length === rawSnippets.length) {
return NextResponse.json(
{
error: "Validation failed",
message: "All uploaded snippets failed validation.",
details: result.errors,
},
{ status: 400 }
);
}

return NextResponse.json({
success: true,
message: `Import processed. Successfully imported: ${result.imported.length}, Skipped duplicates: ${result.duplicates.length}, Failed validation: ${result.errors.length}`,
importedCount: result.imported.length,
skippedCount: result.duplicates.length,
failedCount: result.errors.length,
imported: result.imported.map(s => ({ id: s.id, title: s.title })),
duplicates: result.duplicates,
errors: result.errors,
});
} catch (error) {
console.error("[JSON Import API] Unexpected Error:", error);
return NextResponse.json(
{ error: "Internal Server Error", message: error instanceof Error ? error.message : "An unexpected error occurred." },
{ status: 500 }
);
}
}
184 changes: 184 additions & 0 deletions app/api/snippets/import/zip/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { NextRequest, NextResponse } from "next/server";
import { SnippetRepository } from "../../snippet.repository";
import { SnippetService } from "../../snippet.service";
import { OwnershipMiddleware } from "../../ownership.middleware";
import AdmZip from "adm-zip";

const repository = new SnippetRepository();
const service = new SnippetService(repository);

const EXTENSION_TO_LANGUAGE: Record<string, string> = {
js: "javascript",
ts: "typescript",
py: "python",
java: "java",
cs: "csharp",
cpp: "cpp",
go: "go",
rs: "rust",
php: "php",
rb: "ruby",
sql: "sql",
html: "html",
css: "css",
sh: "bash",
};

export async function POST(req: NextRequest) {
try {
// 1. Authenticate user wallet
const walletAddress = await OwnershipMiddleware.extractWalletAddress(req);
if (!walletAddress) {
return NextResponse.json(
{ error: "Unauthorized", message: "Missing or invalid wallet authentication." },
{ status: 401 }
);
}

const contentType = req.headers.get("content-type") || "";
if (!contentType.includes("multipart/form-data")) {
return NextResponse.json(
{ error: "Unsupported Media Type", message: "Content-Type must be multipart/form-data." },
{ status: 415 }
);
}

const formData = await req.formData();
const file = formData.get("file") as File | null;
if (!file) {
return NextResponse.json(
{ error: "Bad Request", message: "No file was uploaded in the 'file' field." },
{ status: 400 }
);
}

// 2. Parse ZIP entries in-memory
let zip;
try {
const buffer = Buffer.from(await file.arrayBuffer());
zip = new AdmZip(buffer);
} catch (e) {
return NextResponse.json(
{ error: "Malformed File", message: "Uploaded file is not a valid ZIP archive." },
{ status: 400 }
);
}

const zipEntries = zip.getEntries();
const entryMap: Record<string, string> = {};

for (const entry of zipEntries) {
if (entry.isDirectory || entry.entryName.startsWith(".") || entry.entryName.includes("__MACOSX")) {
continue;
}
entryMap[entry.entryName] = entry.getData().toString("utf8");
}

const rawSnippets: any[] = [];

for (const entryName in entryMap) {
if (entryName.endsWith(".json")) {
// Standalone JSON snippet import (check if it has companion code file to avoid double-processing)
const baseName = entryName.substring(0, entryName.length - 5);
const hasMatchingCodeFile = Object.keys(entryMap).some(
name => name.startsWith(baseName) && !name.endsWith(".json")
);
if (hasMatchingCodeFile) {
continue;
}

try {
const content = JSON.parse(entryMap[entryName]);
rawSnippets.push(content);
} catch (e) {
console.warn(`Failed to parse standalone JSON file ${entryName} in ZIP:`, e);
}
} else {
// Source code file import
const content = entryMap[entryName];
const lastDot = entryName.lastIndexOf(".");
const ext = lastDot !== -1 ? entryName.substring(lastDot + 1).toLowerCase() : "";
const baseName = lastDot !== -1 ? entryName.substring(0, lastDot) : entryName;
const cleanBaseName = baseName.split("/").pop() || baseName;

const language = EXTENSION_TO_LANGUAGE[ext] || "text";

// Generate user friendly title
const title = cleanBaseName
.replace(/[_-]+/g, " ")
.replace(/\b\w/g, c => c.toUpperCase());

let metadata: any = {
description: `Imported from ZIP file: ${entryName}`,
tags: ["imported"],
};

// Attempt to find companion JSON file for metadata
const companionName = baseName + ".json";
if (entryMap[companionName]) {
try {
const companionData = JSON.parse(entryMap[companionName]);
if (companionData.metadata) {
metadata = {
description: companionData.metadata.description || metadata.description,
tags: companionData.metadata.tags || metadata.tags,
};
} else {
metadata = {
description: companionData.description || metadata.description,
tags: companionData.tags || metadata.tags,
};
}
} catch (e) {
console.warn(`Failed to parse companion JSON file ${companionName}:`, e);
}
}

rawSnippets.push({
title,
code: content,
language,
metadata,
});
}
}

if (rawSnippets.length === 0) {
return NextResponse.json(
{ error: "Bad Request", message: "No valid snippets found in the ZIP archive." },
{ status: 400 }
);
}

// 3. Delegate to service layer
const result = await service.importSnippets(rawSnippets, walletAddress);

if (result.errors.length === rawSnippets.length) {
return NextResponse.json(
{
error: "Validation failed",
message: "All snippets in the ZIP file failed validation.",
details: result.errors,
},
{ status: 400 }
);
}

return NextResponse.json({
success: true,
message: `ZIP Import processed. Successfully imported: ${result.imported.length}, Skipped duplicates: ${result.duplicates.length}, Failed validation: ${result.errors.length}`,
importedCount: result.imported.length,
skippedCount: result.duplicates.length,
failedCount: result.errors.length,
imported: result.imported.map(s => ({ id: s.id, title: s.title })),
duplicates: result.duplicates,
errors: result.errors,
});
} catch (error) {
console.error("[ZIP Import API] Unexpected Error:", error);
return NextResponse.json(
{ error: "Internal Server Error", message: error instanceof Error ? error.message : "An unexpected error occurred." },
{ status: 500 }
);
}
}
43 changes: 43 additions & 0 deletions app/api/snippets/snippet.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,4 +354,47 @@ export class SnippetRepository {
`;
return result as any[];
}

/**
* Check which of the given snippet IDs already exist in the database.
*/
async checkExistingIds(ids: string[]): Promise<string[]> {
if (ids.length === 0) return [];
const result = await this.sql`
SELECT id FROM snippets WHERE id = ANY(${ids})
`;
return result.map((row: any) => row.id);
}

/**
* Fetch the list of existing snippets for a user with their titles, languages, and MD5 code hashes
* to check for content duplicates efficiently.
*/
async getUserSnippetHashes(ownerWalletAddress: string): Promise<Array<{ id: string; title: string; language: string; code_hash: string }>> {
const result = await this.sql`
SELECT id, title, language, md5(code) as code_hash
FROM snippets
WHERE owner_wallet_address = ${ownerWalletAddress}
AND is_deleted = false
`;
return result as any[];
}

/**
* Bulk insert snippets inside a loop.
*/
async createMany(snippets: Array<{ id: string; title: string; description: string; code: string; language: string; tags: string[]; ownerWalletAddress: string }>) {
const results = [];
const now = new Date();
for (const snippet of snippets) {
const result = await this.sql`
INSERT INTO snippets (id, title, description, code, language, tags, owner_wallet_address, created_at, updated_at)
VALUES (${snippet.id}, ${snippet.title}, ${snippet.description}, ${snippet.code}, ${snippet.language}, ${snippet.tags}, ${snippet.ownerWalletAddress}, ${now}, ${now})
RETURNING *
`;
results.push(result[0]);
}
return results;
}
}

Loading