@morphika/andami 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/admin/assets.ts +4 -0
- package/admin/database.ts +4 -0
- package/admin/index.ts +6 -0
- package/admin/login.ts +4 -0
- package/admin/navigation.ts +4 -0
- package/admin/pages-editor.ts +4 -0
- package/admin/pages.ts +4 -0
- package/admin/projects-editor.ts +4 -0
- package/admin/projects.ts +4 -0
- package/admin/settings.ts +4 -0
- package/admin/setup.ts +4 -0
- package/admin/storage.ts +4 -0
- package/admin/styles.ts +4 -0
- package/app/(site)/[slug]/loading.tsx +20 -0
- package/app/(site)/[slug]/page.tsx +83 -0
- package/app/(site)/error.tsx +32 -0
- package/app/(site)/layout.tsx +53 -0
- package/app/(site)/loading.tsx +20 -0
- package/app/(site)/not-found.tsx +41 -0
- package/app/(site)/page.tsx +43 -0
- package/app/(site)/preview/page.tsx +99 -0
- package/app/(site)/work/[slug]/loading.tsx +23 -0
- package/app/(site)/work/[slug]/page.tsx +84 -0
- package/app/admin/assets/page.tsx +573 -0
- package/app/admin/database/page.tsx +302 -0
- package/app/admin/error.tsx +53 -0
- package/app/admin/layout.tsx +273 -0
- package/app/admin/login/page.tsx +88 -0
- package/app/admin/navigation/page.tsx +157 -0
- package/app/admin/page.tsx +17 -0
- package/app/admin/pages/[slug]/page.tsx +849 -0
- package/app/admin/pages/page.tsx +588 -0
- package/app/admin/projects/[slug]/page.tsx +3 -0
- package/app/admin/projects/page.tsx +669 -0
- package/app/admin/settings/page.tsx +132 -0
- package/app/admin/setup/page.tsx +64 -0
- package/app/admin/storage/page.tsx +518 -0
- package/app/admin/styles/page.tsx +243 -0
- package/app/api/admin/assets/file/route.ts +81 -0
- package/app/api/admin/assets/health/route.ts +170 -0
- package/app/api/admin/assets/register/route.ts +163 -0
- package/app/api/admin/assets/registry/route.ts +98 -0
- package/app/api/admin/assets/relink/confirm/route.ts +242 -0
- package/app/api/admin/assets/relink/route.ts +202 -0
- package/app/api/admin/assets/scan/route.ts +271 -0
- package/app/api/admin/auth/route.ts +160 -0
- package/app/api/admin/custom-sections/[slug]/route.ts +159 -0
- package/app/api/admin/custom-sections/route.ts +127 -0
- package/app/api/admin/database/route.ts +53 -0
- package/app/api/admin/pages/[slug]/duplicate/route.ts +91 -0
- package/app/api/admin/pages/[slug]/route.ts +617 -0
- package/app/api/admin/pages/[slug]/set-home/route.ts +76 -0
- package/app/api/admin/pages/route.ts +129 -0
- package/app/api/admin/preview/route.ts +53 -0
- package/app/api/admin/r2/connect/route.ts +181 -0
- package/app/api/admin/r2/delete/route.ts +198 -0
- package/app/api/admin/r2/disconnect/route.ts +42 -0
- package/app/api/admin/r2/rename/route.ts +265 -0
- package/app/api/admin/r2/status/route.ts +106 -0
- package/app/api/admin/r2/upload-url/route.ts +148 -0
- package/app/api/admin/revalidate/route.ts +55 -0
- package/app/api/admin/settings/route.ts +279 -0
- package/app/api/admin/setup/complete/route.ts +51 -0
- package/app/api/admin/setup/route.ts +118 -0
- package/app/api/admin/storage/switch/route.ts +117 -0
- package/app/api/admin/styles/fonts/route.ts +97 -0
- package/app/api/admin/styles/route.ts +304 -0
- package/app/api/assets/[...path]/route.ts +98 -0
- package/app/api/custom-sections/[id]/route.ts +43 -0
- package/app/api/draft-mode/disable/route.ts +10 -0
- package/app/api/draft-mode/enable/route.ts +26 -0
- package/app/api/projects/route.ts +42 -0
- package/app/api/styles/route.ts +88 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +7 -0
- package/app/layout.tsx +53 -0
- package/app/robots.ts +17 -0
- package/app/sitemap.ts +48 -0
- package/app/studio/[[...index]]/page.tsx +8 -0
- package/components/admin/MetadataEditor.tsx +173 -0
- package/components/admin/PublishToggle.tsx +130 -0
- package/components/admin/icons.tsx +40 -0
- package/components/admin/nav-builder/NavBuilder.tsx +182 -0
- package/components/admin/nav-builder/NavBuilderGrid.tsx +326 -0
- package/components/admin/nav-builder/NavGeneralSettings.tsx +275 -0
- package/components/admin/nav-builder/NavGridCell.tsx +48 -0
- package/components/admin/nav-builder/NavGridItem.tsx +189 -0
- package/components/admin/nav-builder/NavItemSettings.tsx +288 -0
- package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -0
- package/components/admin/nav-builder/NavLivePreview.tsx +125 -0
- package/components/admin/nav-builder/NavSettingsFields.tsx +248 -0
- package/components/admin/nav-builder/NavSettingsPanel.tsx +127 -0
- package/components/admin/nav-builder/index.ts +10 -0
- package/components/admin/nav-builder/nav-builder-utils.ts +238 -0
- package/components/admin/setup-wizard/BrandingStep.tsx +218 -0
- package/components/admin/setup-wizard/DatabaseStep.tsx +331 -0
- package/components/admin/setup-wizard/DoneStep.tsx +187 -0
- package/components/admin/setup-wizard/SetupWizard.tsx +166 -0
- package/components/admin/setup-wizard/StorageStep.tsx +308 -0
- package/components/admin/setup-wizard/WelcomeStep.tsx +96 -0
- package/components/admin/setup-wizard/index.ts +9 -0
- package/components/admin/styles/ColorsEditor.tsx +214 -0
- package/components/admin/styles/FontsEditor.tsx +258 -0
- package/components/admin/styles/GridLayoutEditor.tsx +292 -0
- package/components/admin/styles/LinksButtonsEditor.tsx +120 -0
- package/components/admin/styles/TypographyEditor.tsx +266 -0
- package/components/admin/styles/index.ts +9 -0
- package/components/admin/styles/shared.tsx +68 -0
- package/components/blocks/BlockRenderer.tsx +404 -0
- package/components/blocks/ButtonBlockRenderer.tsx +52 -0
- package/components/blocks/CoverBlockRenderer.tsx +239 -0
- package/components/blocks/CustomSectionInstanceRenderer.tsx +82 -0
- package/components/blocks/EnterAnimationWrapper.tsx +140 -0
- package/components/blocks/HoverAnimationWrapper.tsx +308 -0
- package/components/blocks/ImageBlockRenderer.tsx +61 -0
- package/components/blocks/ImageGridBlockRenderer.tsx +545 -0
- package/components/blocks/PageBackground.tsx +28 -0
- package/components/blocks/PageNavAnimation.tsx +35 -0
- package/components/blocks/PageNavColor.tsx +24 -0
- package/components/blocks/PageRenderer.tsx +142 -0
- package/components/blocks/ParallaxGroupRenderer.tsx +448 -0
- package/components/blocks/ParallaxSlideRenderer.tsx +175 -0
- package/components/blocks/ProjectGridBlockRenderer.tsx +556 -0
- package/components/blocks/SectionRenderer.tsx +170 -0
- package/components/blocks/SectionV2Renderer.tsx +330 -0
- package/components/blocks/ShaderCanvas.tsx +392 -0
- package/components/blocks/SpacerBlockRenderer.tsx +17 -0
- package/components/blocks/TextBlockRenderer.tsx +87 -0
- package/components/blocks/TypewriterRichText.tsx +464 -0
- package/components/blocks/TypewriterWrapper.tsx +149 -0
- package/components/blocks/VideoBlockRenderer.tsx +304 -0
- package/components/blocks/index.ts +2 -0
- package/components/builder/AssetBrowser.tsx +2 -0
- package/components/builder/BlockLivePreview.tsx +101 -0
- package/components/builder/BlockTypePicker.tsx +178 -0
- package/components/builder/BuilderCanvas.tsx +354 -0
- package/components/builder/CanvasMinimap.tsx +200 -0
- package/components/builder/CanvasToolbar.tsx +202 -0
- package/components/builder/ColorPicker.tsx +243 -0
- package/components/builder/ColorSwatchPicker.tsx +274 -0
- package/components/builder/ColumnDragContext.tsx +51 -0
- package/components/builder/ColumnDragOverlay.tsx +110 -0
- package/components/builder/CustomSectionInstanceCard.tsx +97 -0
- package/components/builder/DeviceFrame.tsx +123 -0
- package/components/builder/DndWrapper.tsx +337 -0
- package/components/builder/InsertionLines.tsx +186 -0
- package/components/builder/ParallaxGroupCanvas.tsx +228 -0
- package/components/builder/ParallaxSlideHeader.tsx +113 -0
- package/components/builder/ReadOnlyFrame.tsx +417 -0
- package/components/builder/SectionEditorBar.tsx +288 -0
- package/components/builder/SectionTypePicker.tsx +422 -0
- package/components/builder/SectionV2Canvas.tsx +297 -0
- package/components/builder/SectionV2Column.tsx +488 -0
- package/components/builder/SettingsPanel.tsx +911 -0
- package/components/builder/SortableBlock.tsx +230 -0
- package/components/builder/SortableRow.tsx +362 -0
- package/components/builder/VirtualAssetGrid.tsx +397 -0
- package/components/builder/asset-browser/AssetBrowser.tsx +178 -0
- package/components/builder/asset-browser/FileLightbox.tsx +116 -0
- package/components/builder/asset-browser/FolderTreeItem.tsx +55 -0
- package/components/builder/asset-browser/R2BrowserContent.tsx +436 -0
- package/components/builder/asset-browser/R2ContextMenu.tsx +98 -0
- package/components/builder/asset-browser/VideoThumbnail.tsx +63 -0
- package/components/builder/asset-browser/helpers.ts +88 -0
- package/components/builder/asset-browser/index.ts +1 -0
- package/components/builder/asset-browser/types.ts +49 -0
- package/components/builder/asset-browser/useAssetBrowser.ts +344 -0
- package/components/builder/asset-browser/useR2DragDrop.ts +116 -0
- package/components/builder/asset-browser/useR2Operations.ts +189 -0
- package/components/builder/blockStyles.tsx +295 -0
- package/components/builder/editors/ButtonBlockEditor.tsx +184 -0
- package/components/builder/editors/CoverBlockEditor.tsx +488 -0
- package/components/builder/editors/EnterAnimationPicker.tsx +297 -0
- package/components/builder/editors/HoverEffectPicker.tsx +209 -0
- package/components/builder/editors/ImageBlockEditor.tsx +206 -0
- package/components/builder/editors/ImageGridBlockEditor.tsx +386 -0
- package/components/builder/editors/ProjectGridEditor.tsx +648 -0
- package/components/builder/editors/SpacerBlockEditor.tsx +167 -0
- package/components/builder/editors/StaggerSettings.tsx +108 -0
- package/components/builder/editors/TextAlignmentIcons.tsx +39 -0
- package/components/builder/editors/TextBlockEditor.tsx +462 -0
- package/components/builder/editors/TextStylePicker.tsx +183 -0
- package/components/builder/editors/VideoBlockEditor.tsx +278 -0
- package/components/builder/editors/index.ts +10 -0
- package/components/builder/editors/shared.tsx +345 -0
- package/components/builder/hooks/useColumnDrag.ts +472 -0
- package/components/builder/hooks/useColumnResize.ts +221 -0
- package/components/builder/index.ts +12 -0
- package/components/builder/live-preview/LiveButtonPreview.tsx +38 -0
- package/components/builder/live-preview/LiveCoverPreview.tsx +146 -0
- package/components/builder/live-preview/LiveImageGridPreview.tsx +123 -0
- package/components/builder/live-preview/LiveImagePreview.tsx +107 -0
- package/components/builder/live-preview/LiveProjectGridPreview.tsx +1010 -0
- package/components/builder/live-preview/LiveSpacerPreview.tsx +9 -0
- package/components/builder/live-preview/LiveTextEditor.tsx +198 -0
- package/components/builder/live-preview/LiveVideoPreview.tsx +98 -0
- package/components/builder/live-preview/index.ts +10 -0
- package/components/builder/live-preview/shared.tsx +153 -0
- package/components/builder/settings-panel/BlockLayoutTab.tsx +532 -0
- package/components/builder/settings-panel/BlockSettings.tsx +94 -0
- package/components/builder/settings-panel/ColumnV2Settings.tsx +160 -0
- package/components/builder/settings-panel/LayoutTab.tsx +310 -0
- package/components/builder/settings-panel/PageSettings.tsx +200 -0
- package/components/builder/settings-panel/ParallaxGroupSettings.tsx +118 -0
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +178 -0
- package/components/builder/settings-panel/SectionV2AnimationTab.tsx +103 -0
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +312 -0
- package/components/builder/settings-panel/SectionV2Settings.tsx +323 -0
- package/components/builder/settings-panel/TRBLInputs.tsx +51 -0
- package/components/builder/settings-panel/index.ts +19 -0
- package/components/builder/settings-panel/responsive-helpers.ts +524 -0
- package/components/ui/CustomCursor.tsx +118 -0
- package/components/ui/NavContentLightbox.tsx +152 -0
- package/components/ui/Navbar.tsx +582 -0
- package/components/ui/PortfolioTracker.tsx +87 -0
- package/components/ui/ScrollToTop.tsx +47 -0
- package/lib/animation/enter-presets.ts +147 -0
- package/lib/animation/enter-resolve.ts +90 -0
- package/lib/animation/enter-types.ts +128 -0
- package/lib/animation/hover-effect-presets.ts +210 -0
- package/lib/animation/hover-effect-types.ts +126 -0
- package/lib/asset-retry.ts +111 -0
- package/lib/assets.ts +92 -0
- package/lib/audit.ts +35 -0
- package/lib/auth-token.ts +94 -0
- package/lib/auth.ts +13 -0
- package/lib/builder/cascade-helpers.ts +51 -0
- package/lib/builder/cascade.ts +533 -0
- package/lib/builder/constants.ts +103 -0
- package/lib/builder/defaults.ts +182 -0
- package/lib/builder/history.ts +48 -0
- package/lib/builder/index.ts +21 -0
- package/lib/builder/layout-styles.ts +344 -0
- package/lib/builder/masonry.ts +166 -0
- package/lib/builder/responsive.ts +156 -0
- package/lib/builder/serializer.ts +845 -0
- package/lib/builder/store-blocks.ts +193 -0
- package/lib/builder/store-canvas.ts +319 -0
- package/lib/builder/store-helpers.ts +490 -0
- package/lib/builder/store-sections.ts +709 -0
- package/lib/builder/store.ts +333 -0
- package/lib/builder/templates.ts +297 -0
- package/lib/builder/types.ts +374 -0
- package/lib/builder/utils.ts +37 -0
- package/lib/color-utils.ts +116 -0
- package/lib/config/index.ts +57 -0
- package/lib/config/types.ts +122 -0
- package/lib/contexts/AssetContext.tsx +79 -0
- package/lib/contexts/NavAnimationContext.tsx +44 -0
- package/lib/contexts/NavColorContext.tsx +38 -0
- package/lib/contexts/PageExitContext.tsx +194 -0
- package/lib/contexts/ThumbStatusContext.tsx +83 -0
- package/lib/csrf-client.ts +34 -0
- package/lib/csrf.ts +68 -0
- package/lib/format-utils.ts +24 -0
- package/lib/hooks/useViewport.ts +42 -0
- package/lib/logger.ts +81 -0
- package/lib/revalidate.ts +23 -0
- package/lib/sanitize.ts +91 -0
- package/lib/sanity/client.ts +8 -0
- package/lib/sanity/queries.ts +486 -0
- package/lib/sanity/types.ts +869 -0
- package/lib/sanity/writeClient.ts +24 -0
- package/lib/security.ts +402 -0
- package/lib/setup/detect.ts +156 -0
- package/lib/shader/glsl/index.ts +27 -0
- package/lib/shader/glsl/pixelate.ts +51 -0
- package/lib/shader/glsl/rgb-shift.ts +45 -0
- package/lib/shader/glsl/ripple.ts +46 -0
- package/lib/shader/glsl/vertex.ts +14 -0
- package/lib/storage/index.ts +211 -0
- package/lib/storage/r2-adapter.ts +286 -0
- package/lib/storage/types.ts +125 -0
- package/lib/styles/provider.tsx +267 -0
- package/lib/thumbnails/generate.ts +151 -0
- package/lib/utils.ts +6 -0
- package/package.json +212 -0
- package/sanity/compose.ts +65 -0
- package/sanity/sanity.config.ts +126 -0
- package/sanity/schemas/assetRegistry.ts +301 -0
- package/sanity/schemas/blocks/blockLayout.ts +90 -0
- package/sanity/schemas/blocks/buttonBlock.ts +82 -0
- package/sanity/schemas/blocks/coverBlock.ts +229 -0
- package/sanity/schemas/blocks/imageBlock.ts +58 -0
- package/sanity/schemas/blocks/imageGridBlock.ts +112 -0
- package/sanity/schemas/blocks/index.ts +9 -0
- package/sanity/schemas/blocks/projectGridBlock.ts +251 -0
- package/sanity/schemas/blocks/spacerBlock.ts +41 -0
- package/sanity/schemas/blocks/textBlock.ts +139 -0
- package/sanity/schemas/blocks/videoBlock.ts +80 -0
- package/sanity/schemas/customSection.ts +69 -0
- package/sanity/schemas/customSectionInstance.ts +163 -0
- package/sanity/schemas/index.ts +111 -0
- package/sanity/schemas/objects/enterAnimationConfig.ts +72 -0
- package/sanity/schemas/objects/hoverEffectConfig.ts +90 -0
- package/sanity/schemas/objects/parallaxGroup.ts +66 -0
- package/sanity/schemas/objects/parallaxSlide.ts +217 -0
- package/sanity/schemas/objects/typewriterConfig.ts +38 -0
- package/sanity/schemas/page.ts +162 -0
- package/sanity/schemas/pageSection.ts +157 -0
- package/sanity/schemas/pageSectionV2.ts +269 -0
- package/sanity/schemas/siteSettings.ts +256 -0
- package/sanity/schemas/siteStyles.ts +212 -0
- package/site/error.ts +4 -0
- package/site/index.ts +8 -0
- package/site/not-found.ts +4 -0
- package/site/page.ts +4 -0
- package/site/preview.ts +4 -0
- package/site/robots.ts +4 -0
- package/site/sitemap.ts +4 -0
- package/site/work.ts +4 -0
- package/studio/index.ts +4 -0
- package/styles/admin.css +85 -0
- package/styles/animations.css +237 -0
- package/styles/base.css +148 -0
- package/styles/globals.css +10 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { client } from "../../../../../lib/sanity/client";
|
|
3
|
+
import { writeClient } from "../../../../../lib/sanity/writeClient";
|
|
4
|
+
import { assetRegistryQuery } from "../../../../../lib/sanity/queries";
|
|
5
|
+
import { isAdminAuthenticated } from "../../../../../lib/auth";
|
|
6
|
+
import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
|
|
7
|
+
import { isBodyTooLarge, MAX_JSON_BODY_SIZE } from "../../../../../lib/security";
|
|
8
|
+
import { logger } from "../../../../../lib/logger";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* GET /api/admin/assets/registry — Fetch the full asset registry
|
|
12
|
+
*/
|
|
13
|
+
export async function GET() {
|
|
14
|
+
if (!(await isAdminAuthenticated())) {
|
|
15
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
let registry = await client.fetch(assetRegistryQuery);
|
|
20
|
+
|
|
21
|
+
// Auto-create if it doesn't exist
|
|
22
|
+
if (!registry) {
|
|
23
|
+
registry = await writeClient.createIfNotExists({
|
|
24
|
+
_id: "assetRegistry",
|
|
25
|
+
_type: "assetRegistry",
|
|
26
|
+
scan_status: "ready",
|
|
27
|
+
assets: [],
|
|
28
|
+
relink_log: [],
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return NextResponse.json({ registry });
|
|
33
|
+
} catch (err) {
|
|
34
|
+
logger.error("[Admin:Registry]", "Failed to fetch asset registry", err);
|
|
35
|
+
return NextResponse.json(
|
|
36
|
+
{ error: "Failed to fetch asset registry" },
|
|
37
|
+
{ status: 500 }
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* POST /api/admin/assets/registry — Update registry settings
|
|
44
|
+
* Body: { seed_url? }
|
|
45
|
+
*/
|
|
46
|
+
export async function POST(request: NextRequest) {
|
|
47
|
+
if (!(await isAdminAuthenticated())) {
|
|
48
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
49
|
+
}
|
|
50
|
+
if (!validateCsrf(request)) {
|
|
51
|
+
return csrfErrorResponse();
|
|
52
|
+
}
|
|
53
|
+
if (isBodyTooLarge(request, MAX_JSON_BODY_SIZE)) {
|
|
54
|
+
return NextResponse.json({ error: "Request body too large" }, { status: 413 });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const body = await request.json();
|
|
59
|
+
const { seed_url } = body;
|
|
60
|
+
|
|
61
|
+
// Validate seed_url if provided and non-empty
|
|
62
|
+
if (seed_url && typeof seed_url === "string" && seed_url.length > 0) {
|
|
63
|
+
if (!seed_url.startsWith("http://") && !seed_url.startsWith("https://")) {
|
|
64
|
+
return NextResponse.json(
|
|
65
|
+
{ error: "seed_url must start with http:// or https://" },
|
|
66
|
+
{ status: 400 }
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Ensure the document exists
|
|
72
|
+
await writeClient.createIfNotExists({
|
|
73
|
+
_id: "assetRegistry",
|
|
74
|
+
_type: "assetRegistry",
|
|
75
|
+
scan_status: "ready",
|
|
76
|
+
assets: [],
|
|
77
|
+
relink_log: [],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Build patch — only safe fields allowed
|
|
81
|
+
const patch: Record<string, unknown> = {};
|
|
82
|
+
if (seed_url !== undefined) patch.seed_url = seed_url;
|
|
83
|
+
|
|
84
|
+
if (Object.keys(patch).length > 0) {
|
|
85
|
+
await writeClient.patch("assetRegistry").set(patch).commit();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Return updated registry
|
|
89
|
+
const registry = await client.fetch(assetRegistryQuery);
|
|
90
|
+
return NextResponse.json({ registry });
|
|
91
|
+
} catch (err) {
|
|
92
|
+
logger.error("[Admin:Registry]", "Failed to update registry", err);
|
|
93
|
+
return NextResponse.json(
|
|
94
|
+
{ error: "Failed to update registry" },
|
|
95
|
+
{ status: 500 }
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { client } from "../../../../../../lib/sanity/client";
|
|
3
|
+
import { writeClient } from "../../../../../../lib/sanity/writeClient";
|
|
4
|
+
import { isAdminAuthenticated } from "../../../../../../lib/auth";
|
|
5
|
+
import { validateCsrf, csrfErrorResponse } from "../../../../../../lib/csrf";
|
|
6
|
+
import { logger } from "../../../../../../lib/logger";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Confirmed match from the user
|
|
10
|
+
*/
|
|
11
|
+
interface ConfirmedMatch {
|
|
12
|
+
old_path: string;
|
|
13
|
+
new_path: string | null; // null = mark as missing
|
|
14
|
+
strategy: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Recursively walk a JSON structure and replace asset path string values.
|
|
19
|
+
* Uses structured JSON traversal instead of fragile regex replacement.
|
|
20
|
+
*/
|
|
21
|
+
function replaceAssetPaths(obj: unknown, pathMap: Map<string, string>): { result: unknown; changed: boolean } {
|
|
22
|
+
if (obj === null || obj === undefined) return { result: obj, changed: false };
|
|
23
|
+
|
|
24
|
+
// If it's a string and matches a path in the map, replace it
|
|
25
|
+
if (typeof obj === "string") {
|
|
26
|
+
if (pathMap.has(obj)) {
|
|
27
|
+
return { result: pathMap.get(obj)!, changed: true };
|
|
28
|
+
}
|
|
29
|
+
return { result: obj, changed: false };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// If it's an array, recurse into each element
|
|
33
|
+
if (Array.isArray(obj)) {
|
|
34
|
+
let hasChanged = false;
|
|
35
|
+
const newArr = obj.map((item) => {
|
|
36
|
+
const { result, changed } = replaceAssetPaths(item, pathMap);
|
|
37
|
+
if (changed) hasChanged = true;
|
|
38
|
+
return result;
|
|
39
|
+
});
|
|
40
|
+
return { result: hasChanged ? newArr : obj, changed: hasChanged };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// If it's an object, recurse into each value
|
|
44
|
+
if (typeof obj === "object") {
|
|
45
|
+
let hasChanged = false;
|
|
46
|
+
const newObj: Record<string, unknown> = {};
|
|
47
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
48
|
+
const { result, changed } = replaceAssetPaths(value, pathMap);
|
|
49
|
+
newObj[key] = result;
|
|
50
|
+
if (changed) hasChanged = true;
|
|
51
|
+
}
|
|
52
|
+
return { result: hasChanged ? newObj : obj, changed: hasChanged };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Primitives (number, boolean) pass through
|
|
56
|
+
return { result: obj, changed: false };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* POST /api/admin/assets/relink/confirm — Apply confirmed relink matches
|
|
61
|
+
*
|
|
62
|
+
* Updates all Sanity documents that reference old asset paths,
|
|
63
|
+
* replaces with new paths, and updates the asset registry.
|
|
64
|
+
*
|
|
65
|
+
* Body: {
|
|
66
|
+
* new_seed_url: string,
|
|
67
|
+
* matches: ConfirmedMatch[]
|
|
68
|
+
* }
|
|
69
|
+
*/
|
|
70
|
+
export async function POST(request: NextRequest) {
|
|
71
|
+
if (!(await isAdminAuthenticated())) {
|
|
72
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
73
|
+
}
|
|
74
|
+
if (!validateCsrf(request)) {
|
|
75
|
+
return csrfErrorResponse();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const body = await request.json();
|
|
80
|
+
const { new_seed_url, matches } = body as {
|
|
81
|
+
new_seed_url: string;
|
|
82
|
+
matches: ConfirmedMatch[];
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (!matches || !Array.isArray(matches)) {
|
|
86
|
+
return NextResponse.json(
|
|
87
|
+
{ error: "matches array is required" },
|
|
88
|
+
{ status: 400 }
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Get current registry
|
|
93
|
+
const registry = await client.fetch(
|
|
94
|
+
`*[_type == "assetRegistry"][0]{ _id, seed_url, assets, relink_log }`
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (!registry) {
|
|
98
|
+
return NextResponse.json(
|
|
99
|
+
{ error: "Asset registry not found" },
|
|
100
|
+
{ status: 404 }
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const oldSeed = registry.seed_url || "";
|
|
105
|
+
|
|
106
|
+
// Build path mapping: old_path → new_path
|
|
107
|
+
const pathMap = new Map<string, string>();
|
|
108
|
+
for (const match of matches) {
|
|
109
|
+
if (match.new_path && match.old_path !== match.new_path) {
|
|
110
|
+
pathMap.set(match.old_path, match.new_path);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Find all page documents that might reference assets
|
|
115
|
+
const allPages = await client.fetch(
|
|
116
|
+
`*[_type == "page"]{ _id, content_rows }`
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// Find and update documents with old asset paths
|
|
120
|
+
let documentsUpdated = 0;
|
|
121
|
+
const transaction = writeClient.transaction();
|
|
122
|
+
|
|
123
|
+
for (const page of allPages) {
|
|
124
|
+
const rows = page.content_rows || [];
|
|
125
|
+
const { result, changed } = replaceAssetPaths(rows, pathMap);
|
|
126
|
+
|
|
127
|
+
if (changed) {
|
|
128
|
+
transaction.patch(page._id, { set: { content_rows: result } });
|
|
129
|
+
documentsUpdated++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Update asset registry
|
|
134
|
+
const now = new Date().toISOString();
|
|
135
|
+
const currentAssets = registry.assets || [];
|
|
136
|
+
|
|
137
|
+
const updatedAssets = currentAssets.map(
|
|
138
|
+
(asset: { _key: string; path: string; previous_paths?: string[] }) => {
|
|
139
|
+
const newPath = pathMap.get(asset.path);
|
|
140
|
+
const matchInfo = matches.find((m) => m.old_path === asset.path);
|
|
141
|
+
|
|
142
|
+
if (newPath) {
|
|
143
|
+
return {
|
|
144
|
+
...asset,
|
|
145
|
+
path: newPath,
|
|
146
|
+
status: "active",
|
|
147
|
+
previous_paths: [
|
|
148
|
+
...(asset.previous_paths || []),
|
|
149
|
+
asset.path,
|
|
150
|
+
],
|
|
151
|
+
last_checked_at: now,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (matchInfo && matchInfo.new_path === null) {
|
|
156
|
+
return {
|
|
157
|
+
...asset,
|
|
158
|
+
status: "missing",
|
|
159
|
+
last_checked_at: now,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Exact match (path didn't change) — keep active
|
|
164
|
+
if (matchInfo && matchInfo.new_path === matchInfo.old_path) {
|
|
165
|
+
return {
|
|
166
|
+
...asset,
|
|
167
|
+
status: "active",
|
|
168
|
+
last_checked_at: now,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return asset;
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// Build relink log entry
|
|
177
|
+
const relinkEntry = {
|
|
178
|
+
_key: crypto.randomUUID().replace(/-/g, "").slice(0, 12),
|
|
179
|
+
date: now,
|
|
180
|
+
old_seed: oldSeed,
|
|
181
|
+
new_seed: new_seed_url || oldSeed,
|
|
182
|
+
assets_relinked: matches.filter((m) => m.new_path !== null).length,
|
|
183
|
+
assets_missing: matches.filter((m) => m.new_path === null).length,
|
|
184
|
+
details: JSON.stringify({
|
|
185
|
+
documents_updated: documentsUpdated,
|
|
186
|
+
strategies: {
|
|
187
|
+
exact_path: matches.filter((m) => m.strategy === "exact_path").length,
|
|
188
|
+
hash_match: matches.filter((m) => m.strategy === "hash_match").length,
|
|
189
|
+
filename_size_match: matches.filter(
|
|
190
|
+
(m) => m.strategy === "filename_size_match"
|
|
191
|
+
).length,
|
|
192
|
+
filename_match: matches.filter(
|
|
193
|
+
(m) => m.strategy === "filename_match"
|
|
194
|
+
).length,
|
|
195
|
+
no_match: matches.filter((m) => m.strategy === "no_match").length,
|
|
196
|
+
},
|
|
197
|
+
}),
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Update registry in the same transaction
|
|
201
|
+
transaction.patch("assetRegistry", {
|
|
202
|
+
set: {
|
|
203
|
+
seed_url: new_seed_url || registry.seed_url,
|
|
204
|
+
assets: updatedAssets,
|
|
205
|
+
relink_log: [...(registry.relink_log || []), relinkEntry],
|
|
206
|
+
last_scanned_at: now,
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Also update siteSettings seed_url if it changed
|
|
211
|
+
if (new_seed_url && new_seed_url !== oldSeed) {
|
|
212
|
+
const settings = await client.fetch(
|
|
213
|
+
`*[_type == "siteSettings"][0]._id`
|
|
214
|
+
);
|
|
215
|
+
if (settings) {
|
|
216
|
+
transaction.patch(settings, {
|
|
217
|
+
set: {
|
|
218
|
+
seed_url: new_seed_url,
|
|
219
|
+
last_scanned_at: now,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Execute atomically
|
|
226
|
+
await transaction.commit();
|
|
227
|
+
|
|
228
|
+
return NextResponse.json({
|
|
229
|
+
success: true,
|
|
230
|
+
documents_updated: documentsUpdated,
|
|
231
|
+
assets_relinked: matches.filter((m) => m.new_path !== null).length,
|
|
232
|
+
assets_missing: matches.filter((m) => m.new_path === null).length,
|
|
233
|
+
relink_log_entry: relinkEntry,
|
|
234
|
+
});
|
|
235
|
+
} catch (err) {
|
|
236
|
+
logger.error("[Admin:Relink]", "Relink confirm failed", err);
|
|
237
|
+
return NextResponse.json(
|
|
238
|
+
{ error: "Relink operation failed" },
|
|
239
|
+
{ status: 500 }
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { client } from "../../../../../lib/sanity/client";
|
|
3
|
+
import { isAdminAuthenticated } from "../../../../../lib/auth";
|
|
4
|
+
import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
|
|
5
|
+
import { getStorageAdapter } from "../../../../../lib/storage";
|
|
6
|
+
import { logger } from "../../../../../lib/logger";
|
|
7
|
+
import type { RegisteredAsset } from "../../../../../lib/sanity/types";
|
|
8
|
+
import type { ScannedFile } from "../../../../../lib/storage/types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Relink match result for a single asset
|
|
12
|
+
*/
|
|
13
|
+
interface RelinkMatch {
|
|
14
|
+
old_path: string;
|
|
15
|
+
new_path: string | null;
|
|
16
|
+
strategy:
|
|
17
|
+
| "exact_path"
|
|
18
|
+
| "hash_match"
|
|
19
|
+
| "filename_size_match"
|
|
20
|
+
| "filename_match"
|
|
21
|
+
| "no_match";
|
|
22
|
+
confidence: number;
|
|
23
|
+
candidates?: { path: string; confidence: number }[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* POST /api/admin/assets/relink — Analyze relink candidates
|
|
28
|
+
*
|
|
29
|
+
* Compares current registry assets against a new file listing from
|
|
30
|
+
* the active storage provider. Returns match suggestions without
|
|
31
|
+
* applying any changes.
|
|
32
|
+
*
|
|
33
|
+
* Body: { new_seed_url?: string, new_files?: ScannedFile[] }
|
|
34
|
+
*/
|
|
35
|
+
export async function POST(request: NextRequest) {
|
|
36
|
+
if (!(await isAdminAuthenticated())) {
|
|
37
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
38
|
+
}
|
|
39
|
+
if (!validateCsrf(request)) {
|
|
40
|
+
return csrfErrorResponse();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const body = await request.json();
|
|
45
|
+
const { new_seed_url, new_files } = body;
|
|
46
|
+
|
|
47
|
+
// Get current registry
|
|
48
|
+
const registry = await client.fetch(
|
|
49
|
+
`*[_type == "assetRegistry"][0]{ seed_url, assets }`
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (!registry || !registry.assets?.length) {
|
|
53
|
+
return NextResponse.json(
|
|
54
|
+
{ error: "No assets in registry to relink" },
|
|
55
|
+
{ status: 400 }
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Get new file listing
|
|
60
|
+
let newFiles: ScannedFile[];
|
|
61
|
+
|
|
62
|
+
if (new_files && Array.isArray(new_files)) {
|
|
63
|
+
newFiles = new_files;
|
|
64
|
+
} else {
|
|
65
|
+
// Use the active storage adapter to list files
|
|
66
|
+
try {
|
|
67
|
+
const adapter = await getStorageAdapter();
|
|
68
|
+
newFiles = await adapter.listFiles();
|
|
69
|
+
} catch {
|
|
70
|
+
return NextResponse.json(
|
|
71
|
+
{ error: "Storage provider not connected. Go to /admin/storage to connect." },
|
|
72
|
+
{ status: 400 }
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Build lookup structures for new files
|
|
78
|
+
const newByPath = new Map<string, ScannedFile>();
|
|
79
|
+
const newByHash = new Map<string, ScannedFile[]>();
|
|
80
|
+
const newByFilename = new Map<string, ScannedFile[]>();
|
|
81
|
+
|
|
82
|
+
for (const file of newFiles) {
|
|
83
|
+
newByPath.set(file.path, file);
|
|
84
|
+
|
|
85
|
+
if (file.file_hash) {
|
|
86
|
+
const existing = newByHash.get(file.file_hash) || [];
|
|
87
|
+
existing.push(file);
|
|
88
|
+
newByHash.set(file.file_hash, existing);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const existing = newByFilename.get(file.filename) || [];
|
|
92
|
+
existing.push(file);
|
|
93
|
+
newByFilename.set(file.filename, existing);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Match each existing asset
|
|
97
|
+
const matches: RelinkMatch[] = [];
|
|
98
|
+
const currentAssets: RegisteredAsset[] = registry.assets;
|
|
99
|
+
|
|
100
|
+
for (const asset of currentAssets) {
|
|
101
|
+
// Strategy 1: Exact path match (100% confidence)
|
|
102
|
+
if (newByPath.has(asset.path)) {
|
|
103
|
+
matches.push({
|
|
104
|
+
old_path: asset.path,
|
|
105
|
+
new_path: asset.path,
|
|
106
|
+
strategy: "exact_path",
|
|
107
|
+
confidence: 1.0,
|
|
108
|
+
});
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Strategy 2: Hash match (99% confidence)
|
|
113
|
+
if (asset.file_hash) {
|
|
114
|
+
const hashMatches = newByHash.get(asset.file_hash);
|
|
115
|
+
if (hashMatches && hashMatches.length > 0) {
|
|
116
|
+
matches.push({
|
|
117
|
+
old_path: asset.path,
|
|
118
|
+
new_path: hashMatches[0].path,
|
|
119
|
+
strategy: "hash_match",
|
|
120
|
+
confidence: 0.99,
|
|
121
|
+
candidates:
|
|
122
|
+
hashMatches.length > 1
|
|
123
|
+
? hashMatches.map((f) => ({ path: f.path, confidence: 0.99 }))
|
|
124
|
+
: undefined,
|
|
125
|
+
});
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Strategy 3: Filename + size match (95% confidence)
|
|
131
|
+
const filenameMatches = newByFilename.get(asset.filename) || [];
|
|
132
|
+
const sizeMatch = filenameMatches.find(
|
|
133
|
+
(f) => asset.file_size && f.file_size === asset.file_size
|
|
134
|
+
);
|
|
135
|
+
if (sizeMatch) {
|
|
136
|
+
matches.push({
|
|
137
|
+
old_path: asset.path,
|
|
138
|
+
new_path: sizeMatch.path,
|
|
139
|
+
strategy: "filename_size_match",
|
|
140
|
+
confidence: 0.95,
|
|
141
|
+
});
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Strategy 4: Filename match only (60% confidence)
|
|
146
|
+
if (filenameMatches.length > 0) {
|
|
147
|
+
matches.push({
|
|
148
|
+
old_path: asset.path,
|
|
149
|
+
new_path: filenameMatches[0].path,
|
|
150
|
+
strategy: "filename_match",
|
|
151
|
+
confidence: 0.6,
|
|
152
|
+
candidates: filenameMatches.map((f) => ({
|
|
153
|
+
path: f.path,
|
|
154
|
+
confidence: 0.6,
|
|
155
|
+
})),
|
|
156
|
+
});
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// No match
|
|
161
|
+
matches.push({
|
|
162
|
+
old_path: asset.path,
|
|
163
|
+
new_path: null,
|
|
164
|
+
strategy: "no_match",
|
|
165
|
+
confidence: 0,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Summarize
|
|
170
|
+
const exact = matches.filter((m) => m.strategy === "exact_path").length;
|
|
171
|
+
const hashMatched = matches.filter((m) => m.strategy === "hash_match").length;
|
|
172
|
+
const filenameSizeMatched = matches.filter(
|
|
173
|
+
(m) => m.strategy === "filename_size_match"
|
|
174
|
+
).length;
|
|
175
|
+
const filenameOnly = matches.filter(
|
|
176
|
+
(m) => m.strategy === "filename_match"
|
|
177
|
+
).length;
|
|
178
|
+
const missing = matches.filter((m) => m.strategy === "no_match").length;
|
|
179
|
+
|
|
180
|
+
return NextResponse.json({
|
|
181
|
+
old_seed: registry.seed_url,
|
|
182
|
+
new_seed: new_seed_url,
|
|
183
|
+
matches,
|
|
184
|
+
summary: {
|
|
185
|
+
total: matches.length,
|
|
186
|
+
exact,
|
|
187
|
+
hash_matched: hashMatched,
|
|
188
|
+
filename_size_matched: filenameSizeMatched,
|
|
189
|
+
filename_only: filenameOnly,
|
|
190
|
+
missing,
|
|
191
|
+
auto_resolvable: exact + hashMatched + filenameSizeMatched,
|
|
192
|
+
needs_review: filenameOnly,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
} catch (err) {
|
|
196
|
+
logger.error("[Admin:Relink]", "Relink analysis failed", err);
|
|
197
|
+
return NextResponse.json(
|
|
198
|
+
{ error: "Relink analysis failed" },
|
|
199
|
+
{ status: 500 }
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|