@morphika/webframe 0.1.0

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.
Files changed (319) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -0
  3. package/admin/assets.ts +4 -0
  4. package/admin/database.ts +4 -0
  5. package/admin/index.ts +6 -0
  6. package/admin/login.ts +4 -0
  7. package/admin/navigation.ts +4 -0
  8. package/admin/pages-editor.ts +4 -0
  9. package/admin/pages.ts +4 -0
  10. package/admin/projects-editor.ts +4 -0
  11. package/admin/projects.ts +4 -0
  12. package/admin/settings.ts +4 -0
  13. package/admin/setup.ts +4 -0
  14. package/admin/storage.ts +4 -0
  15. package/admin/styles.ts +4 -0
  16. package/app/(site)/[slug]/loading.tsx +20 -0
  17. package/app/(site)/[slug]/page.tsx +83 -0
  18. package/app/(site)/error.tsx +32 -0
  19. package/app/(site)/layout.tsx +53 -0
  20. package/app/(site)/loading.tsx +20 -0
  21. package/app/(site)/not-found.tsx +41 -0
  22. package/app/(site)/page.tsx +43 -0
  23. package/app/(site)/preview/page.tsx +99 -0
  24. package/app/(site)/work/[slug]/loading.tsx +23 -0
  25. package/app/(site)/work/[slug]/page.tsx +84 -0
  26. package/app/admin/assets/page.tsx +573 -0
  27. package/app/admin/database/page.tsx +302 -0
  28. package/app/admin/error.tsx +53 -0
  29. package/app/admin/layout.tsx +273 -0
  30. package/app/admin/login/page.tsx +88 -0
  31. package/app/admin/navigation/page.tsx +157 -0
  32. package/app/admin/page.tsx +17 -0
  33. package/app/admin/pages/[slug]/page.tsx +849 -0
  34. package/app/admin/pages/page.tsx +588 -0
  35. package/app/admin/projects/[slug]/page.tsx +3 -0
  36. package/app/admin/projects/page.tsx +669 -0
  37. package/app/admin/settings/page.tsx +132 -0
  38. package/app/admin/setup/page.tsx +64 -0
  39. package/app/admin/storage/page.tsx +518 -0
  40. package/app/admin/styles/page.tsx +243 -0
  41. package/app/api/admin/assets/file/route.ts +81 -0
  42. package/app/api/admin/assets/health/route.ts +170 -0
  43. package/app/api/admin/assets/register/route.ts +163 -0
  44. package/app/api/admin/assets/registry/route.ts +98 -0
  45. package/app/api/admin/assets/relink/confirm/route.ts +242 -0
  46. package/app/api/admin/assets/relink/route.ts +202 -0
  47. package/app/api/admin/assets/scan/route.ts +271 -0
  48. package/app/api/admin/auth/route.ts +160 -0
  49. package/app/api/admin/custom-sections/[slug]/route.ts +159 -0
  50. package/app/api/admin/custom-sections/route.ts +127 -0
  51. package/app/api/admin/database/route.ts +53 -0
  52. package/app/api/admin/pages/[slug]/duplicate/route.ts +91 -0
  53. package/app/api/admin/pages/[slug]/route.ts +617 -0
  54. package/app/api/admin/pages/[slug]/set-home/route.ts +76 -0
  55. package/app/api/admin/pages/route.ts +129 -0
  56. package/app/api/admin/preview/route.ts +53 -0
  57. package/app/api/admin/r2/connect/route.ts +181 -0
  58. package/app/api/admin/r2/delete/route.ts +198 -0
  59. package/app/api/admin/r2/disconnect/route.ts +42 -0
  60. package/app/api/admin/r2/rename/route.ts +265 -0
  61. package/app/api/admin/r2/status/route.ts +106 -0
  62. package/app/api/admin/r2/upload-url/route.ts +148 -0
  63. package/app/api/admin/revalidate/route.ts +55 -0
  64. package/app/api/admin/settings/route.ts +279 -0
  65. package/app/api/admin/setup/complete/route.ts +51 -0
  66. package/app/api/admin/setup/route.ts +118 -0
  67. package/app/api/admin/storage/switch/route.ts +117 -0
  68. package/app/api/admin/styles/fonts/route.ts +97 -0
  69. package/app/api/admin/styles/route.ts +304 -0
  70. package/app/api/assets/[...path]/route.ts +98 -0
  71. package/app/api/custom-sections/[id]/route.ts +43 -0
  72. package/app/api/draft-mode/disable/route.ts +10 -0
  73. package/app/api/draft-mode/enable/route.ts +26 -0
  74. package/app/api/projects/route.ts +42 -0
  75. package/app/api/styles/route.ts +88 -0
  76. package/app/favicon.ico +0 -0
  77. package/app/globals.css +7 -0
  78. package/app/layout.tsx +53 -0
  79. package/app/robots.ts +17 -0
  80. package/app/sitemap.ts +48 -0
  81. package/app/studio/[[...index]]/page.tsx +8 -0
  82. package/components/admin/MetadataEditor.tsx +173 -0
  83. package/components/admin/PublishToggle.tsx +130 -0
  84. package/components/admin/icons.tsx +40 -0
  85. package/components/admin/nav-builder/NavBuilder.tsx +182 -0
  86. package/components/admin/nav-builder/NavBuilderGrid.tsx +326 -0
  87. package/components/admin/nav-builder/NavGeneralSettings.tsx +275 -0
  88. package/components/admin/nav-builder/NavGridCell.tsx +48 -0
  89. package/components/admin/nav-builder/NavGridItem.tsx +189 -0
  90. package/components/admin/nav-builder/NavItemSettings.tsx +288 -0
  91. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -0
  92. package/components/admin/nav-builder/NavLivePreview.tsx +125 -0
  93. package/components/admin/nav-builder/NavSettingsFields.tsx +248 -0
  94. package/components/admin/nav-builder/NavSettingsPanel.tsx +127 -0
  95. package/components/admin/nav-builder/index.ts +10 -0
  96. package/components/admin/nav-builder/nav-builder-utils.ts +238 -0
  97. package/components/admin/setup-wizard/BrandingStep.tsx +218 -0
  98. package/components/admin/setup-wizard/DatabaseStep.tsx +331 -0
  99. package/components/admin/setup-wizard/DoneStep.tsx +187 -0
  100. package/components/admin/setup-wizard/SetupWizard.tsx +166 -0
  101. package/components/admin/setup-wizard/StorageStep.tsx +308 -0
  102. package/components/admin/setup-wizard/WelcomeStep.tsx +96 -0
  103. package/components/admin/setup-wizard/index.ts +9 -0
  104. package/components/admin/styles/ColorsEditor.tsx +214 -0
  105. package/components/admin/styles/FontsEditor.tsx +258 -0
  106. package/components/admin/styles/GridLayoutEditor.tsx +292 -0
  107. package/components/admin/styles/LinksButtonsEditor.tsx +120 -0
  108. package/components/admin/styles/TypographyEditor.tsx +266 -0
  109. package/components/admin/styles/index.ts +9 -0
  110. package/components/admin/styles/shared.tsx +68 -0
  111. package/components/blocks/BlockRenderer.tsx +404 -0
  112. package/components/blocks/ButtonBlockRenderer.tsx +52 -0
  113. package/components/blocks/CoverBlockRenderer.tsx +239 -0
  114. package/components/blocks/CustomSectionInstanceRenderer.tsx +82 -0
  115. package/components/blocks/EnterAnimationWrapper.tsx +140 -0
  116. package/components/blocks/HoverAnimationWrapper.tsx +308 -0
  117. package/components/blocks/ImageBlockRenderer.tsx +61 -0
  118. package/components/blocks/ImageGridBlockRenderer.tsx +545 -0
  119. package/components/blocks/PageBackground.tsx +28 -0
  120. package/components/blocks/PageNavAnimation.tsx +35 -0
  121. package/components/blocks/PageNavColor.tsx +24 -0
  122. package/components/blocks/PageRenderer.tsx +142 -0
  123. package/components/blocks/ParallaxGroupRenderer.tsx +448 -0
  124. package/components/blocks/ParallaxSlideRenderer.tsx +175 -0
  125. package/components/blocks/ProjectGridBlockRenderer.tsx +556 -0
  126. package/components/blocks/SectionRenderer.tsx +170 -0
  127. package/components/blocks/SectionV2Renderer.tsx +330 -0
  128. package/components/blocks/ShaderCanvas.tsx +392 -0
  129. package/components/blocks/SpacerBlockRenderer.tsx +17 -0
  130. package/components/blocks/TextBlockRenderer.tsx +87 -0
  131. package/components/blocks/TypewriterRichText.tsx +464 -0
  132. package/components/blocks/TypewriterWrapper.tsx +149 -0
  133. package/components/blocks/VideoBlockRenderer.tsx +304 -0
  134. package/components/blocks/index.ts +2 -0
  135. package/components/builder/AssetBrowser.tsx +2 -0
  136. package/components/builder/BlockLivePreview.tsx +101 -0
  137. package/components/builder/BlockTypePicker.tsx +178 -0
  138. package/components/builder/BuilderCanvas.tsx +354 -0
  139. package/components/builder/CanvasMinimap.tsx +200 -0
  140. package/components/builder/CanvasToolbar.tsx +202 -0
  141. package/components/builder/ColorPicker.tsx +243 -0
  142. package/components/builder/ColorSwatchPicker.tsx +274 -0
  143. package/components/builder/ColumnDragContext.tsx +51 -0
  144. package/components/builder/ColumnDragOverlay.tsx +110 -0
  145. package/components/builder/CustomSectionInstanceCard.tsx +97 -0
  146. package/components/builder/DeviceFrame.tsx +123 -0
  147. package/components/builder/DndWrapper.tsx +337 -0
  148. package/components/builder/InsertionLines.tsx +186 -0
  149. package/components/builder/ParallaxGroupCanvas.tsx +228 -0
  150. package/components/builder/ParallaxSlideHeader.tsx +113 -0
  151. package/components/builder/ReadOnlyFrame.tsx +417 -0
  152. package/components/builder/SectionEditorBar.tsx +288 -0
  153. package/components/builder/SectionTypePicker.tsx +422 -0
  154. package/components/builder/SectionV2Canvas.tsx +297 -0
  155. package/components/builder/SectionV2Column.tsx +488 -0
  156. package/components/builder/SettingsPanel.tsx +911 -0
  157. package/components/builder/SortableBlock.tsx +230 -0
  158. package/components/builder/SortableRow.tsx +362 -0
  159. package/components/builder/VirtualAssetGrid.tsx +397 -0
  160. package/components/builder/asset-browser/AssetBrowser.tsx +178 -0
  161. package/components/builder/asset-browser/FileLightbox.tsx +116 -0
  162. package/components/builder/asset-browser/FolderTreeItem.tsx +55 -0
  163. package/components/builder/asset-browser/R2BrowserContent.tsx +436 -0
  164. package/components/builder/asset-browser/R2ContextMenu.tsx +98 -0
  165. package/components/builder/asset-browser/VideoThumbnail.tsx +63 -0
  166. package/components/builder/asset-browser/helpers.ts +88 -0
  167. package/components/builder/asset-browser/index.ts +1 -0
  168. package/components/builder/asset-browser/types.ts +49 -0
  169. package/components/builder/asset-browser/useAssetBrowser.ts +344 -0
  170. package/components/builder/asset-browser/useR2DragDrop.ts +116 -0
  171. package/components/builder/asset-browser/useR2Operations.ts +189 -0
  172. package/components/builder/blockStyles.tsx +295 -0
  173. package/components/builder/editors/ButtonBlockEditor.tsx +184 -0
  174. package/components/builder/editors/CoverBlockEditor.tsx +488 -0
  175. package/components/builder/editors/EnterAnimationPicker.tsx +297 -0
  176. package/components/builder/editors/HoverEffectPicker.tsx +209 -0
  177. package/components/builder/editors/ImageBlockEditor.tsx +206 -0
  178. package/components/builder/editors/ImageGridBlockEditor.tsx +386 -0
  179. package/components/builder/editors/ProjectGridEditor.tsx +648 -0
  180. package/components/builder/editors/SpacerBlockEditor.tsx +167 -0
  181. package/components/builder/editors/StaggerSettings.tsx +108 -0
  182. package/components/builder/editors/TextAlignmentIcons.tsx +39 -0
  183. package/components/builder/editors/TextBlockEditor.tsx +462 -0
  184. package/components/builder/editors/TextStylePicker.tsx +183 -0
  185. package/components/builder/editors/VideoBlockEditor.tsx +278 -0
  186. package/components/builder/editors/index.ts +10 -0
  187. package/components/builder/editors/shared.tsx +345 -0
  188. package/components/builder/hooks/useColumnDrag.ts +472 -0
  189. package/components/builder/hooks/useColumnResize.ts +221 -0
  190. package/components/builder/index.ts +12 -0
  191. package/components/builder/live-preview/LiveButtonPreview.tsx +38 -0
  192. package/components/builder/live-preview/LiveCoverPreview.tsx +146 -0
  193. package/components/builder/live-preview/LiveImageGridPreview.tsx +123 -0
  194. package/components/builder/live-preview/LiveImagePreview.tsx +107 -0
  195. package/components/builder/live-preview/LiveProjectGridPreview.tsx +1010 -0
  196. package/components/builder/live-preview/LiveSpacerPreview.tsx +9 -0
  197. package/components/builder/live-preview/LiveTextEditor.tsx +198 -0
  198. package/components/builder/live-preview/LiveVideoPreview.tsx +98 -0
  199. package/components/builder/live-preview/index.ts +10 -0
  200. package/components/builder/live-preview/shared.tsx +153 -0
  201. package/components/builder/settings-panel/BlockLayoutTab.tsx +532 -0
  202. package/components/builder/settings-panel/BlockSettings.tsx +94 -0
  203. package/components/builder/settings-panel/ColumnV2Settings.tsx +160 -0
  204. package/components/builder/settings-panel/LayoutTab.tsx +310 -0
  205. package/components/builder/settings-panel/PageSettings.tsx +200 -0
  206. package/components/builder/settings-panel/ParallaxGroupSettings.tsx +118 -0
  207. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +178 -0
  208. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +103 -0
  209. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +312 -0
  210. package/components/builder/settings-panel/SectionV2Settings.tsx +323 -0
  211. package/components/builder/settings-panel/TRBLInputs.tsx +51 -0
  212. package/components/builder/settings-panel/index.ts +19 -0
  213. package/components/builder/settings-panel/responsive-helpers.ts +524 -0
  214. package/components/ui/CustomCursor.tsx +118 -0
  215. package/components/ui/NavContentLightbox.tsx +152 -0
  216. package/components/ui/Navbar.tsx +582 -0
  217. package/components/ui/PortfolioTracker.tsx +87 -0
  218. package/components/ui/ScrollToTop.tsx +47 -0
  219. package/lib/animation/enter-presets.ts +147 -0
  220. package/lib/animation/enter-resolve.ts +90 -0
  221. package/lib/animation/enter-types.ts +128 -0
  222. package/lib/animation/hover-effect-presets.ts +210 -0
  223. package/lib/animation/hover-effect-types.ts +126 -0
  224. package/lib/asset-retry.ts +111 -0
  225. package/lib/assets.ts +92 -0
  226. package/lib/audit.ts +35 -0
  227. package/lib/auth-token.ts +94 -0
  228. package/lib/auth.ts +13 -0
  229. package/lib/builder/cascade-helpers.ts +51 -0
  230. package/lib/builder/cascade.ts +533 -0
  231. package/lib/builder/constants.ts +103 -0
  232. package/lib/builder/defaults.ts +182 -0
  233. package/lib/builder/history.ts +48 -0
  234. package/lib/builder/index.ts +21 -0
  235. package/lib/builder/layout-styles.ts +344 -0
  236. package/lib/builder/masonry.ts +166 -0
  237. package/lib/builder/responsive.ts +156 -0
  238. package/lib/builder/serializer.ts +845 -0
  239. package/lib/builder/store-blocks.ts +193 -0
  240. package/lib/builder/store-canvas.ts +319 -0
  241. package/lib/builder/store-helpers.ts +490 -0
  242. package/lib/builder/store-sections.ts +709 -0
  243. package/lib/builder/store.ts +333 -0
  244. package/lib/builder/templates.ts +297 -0
  245. package/lib/builder/types.ts +374 -0
  246. package/lib/builder/utils.ts +37 -0
  247. package/lib/color-utils.ts +116 -0
  248. package/lib/config/index.ts +57 -0
  249. package/lib/config/types.ts +122 -0
  250. package/lib/contexts/AssetContext.tsx +79 -0
  251. package/lib/contexts/NavAnimationContext.tsx +44 -0
  252. package/lib/contexts/NavColorContext.tsx +38 -0
  253. package/lib/contexts/PageExitContext.tsx +194 -0
  254. package/lib/contexts/ThumbStatusContext.tsx +83 -0
  255. package/lib/csrf-client.ts +34 -0
  256. package/lib/csrf.ts +68 -0
  257. package/lib/format-utils.ts +24 -0
  258. package/lib/hooks/useViewport.ts +42 -0
  259. package/lib/logger.ts +81 -0
  260. package/lib/revalidate.ts +23 -0
  261. package/lib/sanitize.ts +91 -0
  262. package/lib/sanity/client.ts +8 -0
  263. package/lib/sanity/queries.ts +486 -0
  264. package/lib/sanity/types.ts +869 -0
  265. package/lib/sanity/writeClient.ts +24 -0
  266. package/lib/security.ts +402 -0
  267. package/lib/setup/detect.ts +156 -0
  268. package/lib/shader/glsl/index.ts +27 -0
  269. package/lib/shader/glsl/pixelate.ts +51 -0
  270. package/lib/shader/glsl/rgb-shift.ts +45 -0
  271. package/lib/shader/glsl/ripple.ts +46 -0
  272. package/lib/shader/glsl/vertex.ts +14 -0
  273. package/lib/storage/index.ts +211 -0
  274. package/lib/storage/r2-adapter.ts +286 -0
  275. package/lib/storage/types.ts +125 -0
  276. package/lib/styles/provider.tsx +267 -0
  277. package/lib/thumbnails/generate.ts +151 -0
  278. package/lib/utils.ts +6 -0
  279. package/package.json +212 -0
  280. package/sanity/compose.ts +65 -0
  281. package/sanity/sanity.config.ts +126 -0
  282. package/sanity/schemas/assetRegistry.ts +301 -0
  283. package/sanity/schemas/blocks/blockLayout.ts +90 -0
  284. package/sanity/schemas/blocks/buttonBlock.ts +82 -0
  285. package/sanity/schemas/blocks/coverBlock.ts +229 -0
  286. package/sanity/schemas/blocks/imageBlock.ts +58 -0
  287. package/sanity/schemas/blocks/imageGridBlock.ts +112 -0
  288. package/sanity/schemas/blocks/index.ts +9 -0
  289. package/sanity/schemas/blocks/projectGridBlock.ts +251 -0
  290. package/sanity/schemas/blocks/spacerBlock.ts +41 -0
  291. package/sanity/schemas/blocks/textBlock.ts +139 -0
  292. package/sanity/schemas/blocks/videoBlock.ts +80 -0
  293. package/sanity/schemas/customSection.ts +69 -0
  294. package/sanity/schemas/customSectionInstance.ts +163 -0
  295. package/sanity/schemas/index.ts +111 -0
  296. package/sanity/schemas/objects/enterAnimationConfig.ts +72 -0
  297. package/sanity/schemas/objects/hoverEffectConfig.ts +90 -0
  298. package/sanity/schemas/objects/parallaxGroup.ts +66 -0
  299. package/sanity/schemas/objects/parallaxSlide.ts +217 -0
  300. package/sanity/schemas/objects/typewriterConfig.ts +38 -0
  301. package/sanity/schemas/page.ts +162 -0
  302. package/sanity/schemas/pageSection.ts +157 -0
  303. package/sanity/schemas/pageSectionV2.ts +269 -0
  304. package/sanity/schemas/siteSettings.ts +256 -0
  305. package/sanity/schemas/siteStyles.ts +210 -0
  306. package/site/error.ts +4 -0
  307. package/site/index.ts +8 -0
  308. package/site/not-found.ts +4 -0
  309. package/site/page.ts +4 -0
  310. package/site/preview.ts +4 -0
  311. package/site/robots.ts +4 -0
  312. package/site/sitemap.ts +4 -0
  313. package/site/work.ts +4 -0
  314. package/studio/index.ts +4 -0
  315. package/styles/admin.css +85 -0
  316. package/styles/animations.css +237 -0
  317. package/styles/base.css +148 -0
  318. package/styles/globals.css +10 -0
  319. 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
+ }