323 lines
12 KiB
TypeScript
323 lines
12 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { prisma, projectService } from "@/lib/prisma";
|
|
import { requireSessionAuth } from "@/lib/auth";
|
|
import type { Prisma } from "@prisma/client";
|
|
|
|
type ImportSiteSettings = {
|
|
defaultLocale?: unknown;
|
|
locales?: unknown;
|
|
theme?: unknown;
|
|
};
|
|
|
|
type ImportContentPageTranslation = {
|
|
locale?: unknown;
|
|
title?: unknown;
|
|
slug?: unknown;
|
|
content?: unknown;
|
|
metaDescription?: unknown;
|
|
keywords?: unknown;
|
|
};
|
|
|
|
type ImportContentPage = {
|
|
key?: unknown;
|
|
status?: unknown;
|
|
translations?: unknown;
|
|
};
|
|
|
|
type ImportProject = {
|
|
id?: unknown;
|
|
slug?: unknown;
|
|
defaultLocale?: unknown;
|
|
title?: unknown;
|
|
description?: unknown;
|
|
content?: unknown;
|
|
tags?: unknown;
|
|
category?: unknown;
|
|
featured?: unknown;
|
|
github?: unknown;
|
|
live?: unknown;
|
|
published?: unknown;
|
|
imageUrl?: unknown;
|
|
difficulty?: unknown;
|
|
timeToComplete?: unknown;
|
|
technologies?: unknown;
|
|
challenges?: unknown;
|
|
lessonsLearned?: unknown;
|
|
futureImprovements?: unknown;
|
|
demoVideo?: unknown;
|
|
screenshots?: unknown;
|
|
colorScheme?: unknown;
|
|
accessibility?: unknown;
|
|
performance?: unknown;
|
|
analytics?: unknown;
|
|
};
|
|
|
|
type ImportProjectTranslation = {
|
|
projectId?: unknown;
|
|
locale?: unknown;
|
|
title?: unknown;
|
|
description?: unknown;
|
|
content?: unknown;
|
|
metaDescription?: unknown;
|
|
keywords?: unknown;
|
|
ogImage?: unknown;
|
|
schema?: unknown;
|
|
};
|
|
|
|
type ImportPayload = {
|
|
projects?: unknown;
|
|
siteSettings?: unknown;
|
|
contentPages?: unknown;
|
|
projectTranslations?: unknown;
|
|
};
|
|
|
|
function asString(v: unknown): string | null {
|
|
return typeof v === "string" ? v : null;
|
|
}
|
|
|
|
function asStringArray(v: unknown): string[] | null {
|
|
if (!Array.isArray(v)) return null;
|
|
const allStrings = v.filter((x) => typeof x === "string") as string[];
|
|
return allStrings.length === v.length ? allStrings : null;
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const isAdminRequest = request.headers.get("x-admin-request") === "true";
|
|
if (!isAdminRequest) {
|
|
return NextResponse.json({ error: "Admin access required" }, { status: 403 });
|
|
}
|
|
const authError = requireSessionAuth(request);
|
|
if (authError) return authError;
|
|
|
|
const body = (await request.json()) as ImportPayload;
|
|
|
|
// Validate import data structure
|
|
if (!Array.isArray(body.projects)) {
|
|
return NextResponse.json(
|
|
{ error: "Invalid import data format" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const results = {
|
|
imported: 0,
|
|
skipped: 0,
|
|
errors: [] as string[],
|
|
};
|
|
|
|
// Import SiteSettings (optional)
|
|
if (body.siteSettings && typeof body.siteSettings === "object") {
|
|
try {
|
|
const ss = body.siteSettings as ImportSiteSettings;
|
|
const defaultLocale = asString(ss.defaultLocale);
|
|
const locales = asStringArray(ss.locales);
|
|
const theme = ss.theme as Prisma.InputJsonValue | undefined;
|
|
|
|
await prisma.siteSettings.upsert({
|
|
where: { id: 1 },
|
|
create: {
|
|
id: 1,
|
|
...(defaultLocale ? { defaultLocale } : {}),
|
|
...(locales ? { locales } : {}),
|
|
...(theme ? { theme } : {}),
|
|
},
|
|
update: {
|
|
...(defaultLocale ? { defaultLocale } : {}),
|
|
...(locales ? { locales } : {}),
|
|
...(theme ? { theme } : {}),
|
|
},
|
|
});
|
|
} catch {
|
|
// non-blocking
|
|
}
|
|
}
|
|
|
|
// Import CMS content pages (optional)
|
|
if (Array.isArray(body.contentPages)) {
|
|
for (const page of body.contentPages) {
|
|
try {
|
|
const key = asString((page as ImportContentPage)?.key);
|
|
if (!key) continue;
|
|
const statusRaw = asString((page as ImportContentPage)?.status);
|
|
const status = statusRaw === "DRAFT" || statusRaw === "PUBLISHED" ? statusRaw : "PUBLISHED";
|
|
const upserted = await prisma.contentPage.upsert({
|
|
where: { key },
|
|
create: { key, status },
|
|
update: { status },
|
|
});
|
|
|
|
const translations = (page as ImportContentPage)?.translations;
|
|
if (Array.isArray(translations)) {
|
|
for (const tr of translations as ImportContentPageTranslation[]) {
|
|
const locale = asString(tr?.locale);
|
|
if (!locale || typeof tr?.content === "undefined" || tr?.content === null) continue;
|
|
await prisma.contentPageTranslation.upsert({
|
|
where: { pageId_locale: { pageId: upserted.id, locale } },
|
|
create: {
|
|
pageId: upserted.id,
|
|
locale,
|
|
title: asString(tr.title),
|
|
slug: asString(tr.slug),
|
|
content: tr.content as Prisma.InputJsonValue,
|
|
metaDescription: asString(tr.metaDescription),
|
|
keywords: asString(tr.keywords),
|
|
},
|
|
update: {
|
|
title: asString(tr.title),
|
|
slug: asString(tr.slug),
|
|
content: tr.content as Prisma.InputJsonValue,
|
|
metaDescription: asString(tr.metaDescription),
|
|
keywords: asString(tr.keywords),
|
|
},
|
|
});
|
|
}
|
|
}
|
|
} catch (error) {
|
|
const key = asString((page as ImportContentPage)?.key) ?? "unknown";
|
|
results.errors.push(
|
|
`Failed to import content page "${key}": ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Preload existing titles once (avoid O(n^2) DB reads during import)
|
|
const existingProjectsResult = await projectService.getAllProjects({ limit: 10000 });
|
|
const existingProjects = existingProjectsResult.projects || existingProjectsResult;
|
|
const existingTitles = new Set(existingProjects.map(p => p.title));
|
|
const existingSlugs = new Set(
|
|
existingProjects
|
|
.map((p) => (p as unknown as { slug?: string }).slug)
|
|
.filter((s): s is string => typeof s === "string" && s.length > 0),
|
|
);
|
|
|
|
// Process each project
|
|
for (const projectData of body.projects as ImportProject[]) {
|
|
try {
|
|
// Check if project already exists (by title)
|
|
const title = asString(projectData.title);
|
|
if (!title) continue;
|
|
const exists = existingTitles.has(title);
|
|
|
|
if (exists) {
|
|
results.skipped++;
|
|
results.errors.push(`Project "${title}" already exists`);
|
|
continue;
|
|
}
|
|
|
|
// Create new project
|
|
const created = await projectService.createProject({
|
|
slug: asString(projectData.slug) ?? undefined,
|
|
defaultLocale: asString(projectData.defaultLocale) ?? "en",
|
|
title,
|
|
description: asString(projectData.description) ?? "",
|
|
content: projectData.content as Prisma.InputJsonValue | undefined,
|
|
tags: (asStringArray(projectData.tags) ?? []) as string[],
|
|
category: asString(projectData.category) ?? "General",
|
|
featured: projectData.featured === true,
|
|
github: asString(projectData.github) ?? undefined,
|
|
live: asString(projectData.live) ?? undefined,
|
|
published: projectData.published !== false, // Default to true
|
|
imageUrl: asString(projectData.imageUrl) ?? undefined,
|
|
difficulty: asString(projectData.difficulty) ?? "Intermediate",
|
|
timeToComplete: asString(projectData.timeToComplete) ?? undefined,
|
|
technologies: (asStringArray(projectData.technologies) ?? []) as string[],
|
|
challenges: (asStringArray(projectData.challenges) ?? []) as string[],
|
|
lessonsLearned: (asStringArray(projectData.lessonsLearned) ?? []) as string[],
|
|
futureImprovements: (asStringArray(projectData.futureImprovements) ?? []) as string[],
|
|
demoVideo: asString(projectData.demoVideo) ?? undefined,
|
|
screenshots: (asStringArray(projectData.screenshots) ?? []) as string[],
|
|
colorScheme: asString(projectData.colorScheme) ?? "Dark",
|
|
accessibility: projectData.accessibility !== false, // Default to true
|
|
performance: (projectData.performance as Record<string, unknown> | null) || {
|
|
lighthouse: 0,
|
|
bundleSize: "0KB",
|
|
loadTime: "0s",
|
|
},
|
|
analytics: (projectData.analytics as Record<string, unknown> | null) || {
|
|
views: 0,
|
|
likes: 0,
|
|
shares: 0,
|
|
},
|
|
});
|
|
|
|
// Import translations (optional, from export v2)
|
|
if (Array.isArray(body.projectTranslations)) {
|
|
for (const tr of body.projectTranslations as ImportProjectTranslation[]) {
|
|
const projectId = typeof tr?.projectId === "number" ? tr.projectId : null;
|
|
const locale = asString(tr?.locale);
|
|
if (!projectId || !locale) continue;
|
|
// Map translation to created project by original slug/title when possible.
|
|
// We match by slug if available in exported project list; otherwise by title.
|
|
const exportedProject = (body.projects as ImportProject[]).find(
|
|
(p) => typeof p.id === "number" && p.id === projectId,
|
|
);
|
|
const exportedSlug = asString(exportedProject?.slug);
|
|
const matches =
|
|
(exportedSlug && (created as unknown as { slug?: string }).slug === exportedSlug) ||
|
|
(!!asString(exportedProject?.title) &&
|
|
(created as unknown as { title?: string }).title === asString(exportedProject?.title));
|
|
if (!matches) continue;
|
|
|
|
const trTitle = asString(tr.title);
|
|
const trDescription = asString(tr.description);
|
|
if (!trTitle || !trDescription) continue;
|
|
await prisma.projectTranslation.upsert({
|
|
where: {
|
|
projectId_locale: {
|
|
projectId: (created as unknown as { id: number }).id,
|
|
locale,
|
|
},
|
|
},
|
|
create: {
|
|
projectId: (created as unknown as { id: number }).id,
|
|
locale,
|
|
title: trTitle,
|
|
description: trDescription,
|
|
content: (tr.content as Prisma.InputJsonValue) ?? null,
|
|
metaDescription: asString(tr.metaDescription),
|
|
keywords: asString(tr.keywords),
|
|
ogImage: asString(tr.ogImage),
|
|
schema: (tr.schema as Prisma.InputJsonValue) ?? null,
|
|
},
|
|
update: {
|
|
title: trTitle,
|
|
description: trDescription,
|
|
content: (tr.content as Prisma.InputJsonValue) ?? null,
|
|
metaDescription: asString(tr.metaDescription),
|
|
keywords: asString(tr.keywords),
|
|
ogImage: asString(tr.ogImage),
|
|
schema: (tr.schema as Prisma.InputJsonValue) ?? null,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
results.imported++;
|
|
existingTitles.add(title);
|
|
const slug = asString(projectData.slug);
|
|
if (slug) existingSlugs.add(slug);
|
|
} catch (error) {
|
|
results.skipped++;
|
|
const title = asString(projectData.title) ?? "unknown";
|
|
results.errors.push(
|
|
`Failed to import "${title}": ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
message: `Import completed: ${results.imported} imported, ${results.skipped} skipped`,
|
|
results
|
|
});
|
|
} catch (error) {
|
|
console.error("Import error:", error);
|
|
return NextResponse.json(
|
|
{ error: "Failed to import projects" },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
}
|