create-nextblock 0.0.1
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/bin/create-nextblock.js +997 -0
- package/package.json +25 -0
- package/scripts/sync-template.js +284 -0
- package/templates/nextblock-template/.env.example +37 -0
- package/templates/nextblock-template/.swcrc +30 -0
- package/templates/nextblock-template/README.md +194 -0
- package/templates/nextblock-template/app/(auth-pages)/forgot-password/page.tsx +57 -0
- package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -0
- package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +28 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +67 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +70 -0
- package/templates/nextblock-template/app/ToasterProvider.tsx +17 -0
- package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +147 -0
- package/templates/nextblock-template/app/[slug]/page.tsx +145 -0
- package/templates/nextblock-template/app/[slug]/page.utils.ts +183 -0
- package/templates/nextblock-template/app/actions/email.ts +31 -0
- package/templates/nextblock-template/app/actions/formActions.ts +65 -0
- package/templates/nextblock-template/app/actions/languageActions.ts +130 -0
- package/templates/nextblock-template/app/actions/postActions.ts +80 -0
- package/templates/nextblock-template/app/actions.ts +146 -0
- package/templates/nextblock-template/app/api/process-image/route.ts +210 -0
- package/templates/nextblock-template/app/api/revalidate/route.ts +86 -0
- package/templates/nextblock-template/app/api/revalidate-log/route.ts +23 -0
- package/templates/nextblock-template/app/api/upload/presigned-url/route.ts +106 -0
- package/templates/nextblock-template/app/api/upload/proxy/route.ts +84 -0
- package/templates/nextblock-template/app/auth/callback/route.ts +58 -0
- package/templates/nextblock-template/app/blog/[slug]/PostClientContent.tsx +169 -0
- package/templates/nextblock-template/app/blog/[slug]/page.tsx +177 -0
- package/templates/nextblock-template/app/blog/[slug]/page.utils.ts +136 -0
- package/templates/nextblock-template/app/blog/page.tsx +77 -0
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +321 -0
- package/templates/nextblock-template/app/cms/blocks/actions.ts +434 -0
- package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +567 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +98 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +58 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +62 -0
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +276 -0
- package/templates/nextblock-template/app/cms/blocks/components/DeleteBlockButtonClient.tsx +47 -0
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +182 -0
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +120 -0
- package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +133 -0
- package/templates/nextblock-template/app/cms/blocks/components/SortableBlockItem.tsx +46 -0
- package/templates/nextblock-template/app/cms/blocks/editors/ButtonBlockEditor.tsx +85 -0
- package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +182 -0
- package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +111 -0
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +150 -0
- package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +79 -0
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +337 -0
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -0
- package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +64 -0
- package/templates/nextblock-template/app/cms/components/ConfirmationModal.tsx +51 -0
- package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +145 -0
- package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +203 -0
- package/templates/nextblock-template/app/cms/components/LanguageFilterSelect.tsx +69 -0
- package/templates/nextblock-template/app/cms/dashboard/page.tsx +247 -0
- package/templates/nextblock-template/app/cms/layout.tsx +10 -0
- package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -0
- package/templates/nextblock-template/app/cms/media/[id]/edit/page.tsx +80 -0
- package/templates/nextblock-template/app/cms/media/actions.ts +577 -0
- package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +53 -0
- package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -0
- package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -0
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +157 -0
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +275 -0
- package/templates/nextblock-template/app/cms/media/components/MediaImage.tsx +70 -0
- package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +195 -0
- package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +362 -0
- package/templates/nextblock-template/app/cms/media/page.tsx +120 -0
- package/templates/nextblock-template/app/cms/navigation/[id]/edit/page.tsx +101 -0
- package/templates/nextblock-template/app/cms/navigation/actions.ts +358 -0
- package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +52 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +248 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationLanguageSwitcher.tsx +132 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +701 -0
- package/templates/nextblock-template/app/cms/navigation/components/SortableNavItem.tsx +98 -0
- package/templates/nextblock-template/app/cms/navigation/new/page.tsx +26 -0
- package/templates/nextblock-template/app/cms/navigation/page.tsx +102 -0
- package/templates/nextblock-template/app/cms/navigation/utils.ts +51 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +121 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -0
- package/templates/nextblock-template/app/cms/pages/actions.ts +241 -0
- package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +47 -0
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +253 -0
- package/templates/nextblock-template/app/cms/pages/new/page.tsx +52 -0
- package/templates/nextblock-template/app/cms/pages/page.tsx +232 -0
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +183 -0
- package/templates/nextblock-template/app/cms/posts/actions.ts +309 -0
- package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +55 -0
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +419 -0
- package/templates/nextblock-template/app/cms/posts/new/page.tsx +21 -0
- package/templates/nextblock-template/app/cms/posts/page.tsx +192 -0
- package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -0
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +201 -0
- package/templates/nextblock-template/app/cms/revisions/actions.ts +84 -0
- package/templates/nextblock-template/app/cms/revisions/service.ts +344 -0
- package/templates/nextblock-template/app/cms/revisions/utils.ts +127 -0
- package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +68 -0
- package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +78 -0
- package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +32 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +117 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +216 -0
- package/templates/nextblock-template/app/cms/settings/languages/[id]/edit/page.tsx +77 -0
- package/templates/nextblock-template/app/cms/settings/languages/actions.ts +261 -0
- package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +76 -0
- package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +167 -0
- package/templates/nextblock-template/app/cms/settings/languages/new/page.tsx +34 -0
- package/templates/nextblock-template/app/cms/settings/languages/page.tsx +156 -0
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +19 -0
- package/templates/nextblock-template/app/cms/settings/logos/actions.ts +114 -0
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +177 -0
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +11 -0
- package/templates/nextblock-template/app/cms/settings/logos/page.tsx +118 -0
- package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -0
- package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +91 -0
- package/templates/nextblock-template/app/cms/users/actions.ts +156 -0
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +71 -0
- package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +138 -0
- package/templates/nextblock-template/app/cms/users/page.tsx +183 -0
- package/templates/nextblock-template/app/favicon.ico +0 -0
- package/templates/nextblock-template/app/globals.css +401 -0
- package/templates/nextblock-template/app/layout.tsx +191 -0
- package/templates/nextblock-template/app/lib/sitemap-utils.ts +68 -0
- package/templates/nextblock-template/app/page.tsx +109 -0
- package/templates/nextblock-template/app/providers.tsx +43 -0
- package/templates/nextblock-template/app/robots.txt/route.ts +19 -0
- package/templates/nextblock-template/app/sitemap.xml/route.ts +63 -0
- package/templates/nextblock-template/app/unauthorized/page.tsx +27 -0
- package/templates/nextblock-template/backup/backup_2025-06-19.sql +8057 -0
- package/templates/nextblock-template/backup/backup_2025-06-20.sql +8159 -0
- package/templates/nextblock-template/backup/backup_2025-07-08.sql +8411 -0
- package/templates/nextblock-template/backup/backup_2025-07-09.sql +8442 -0
- package/templates/nextblock-template/backup/backup_2025-07-10.sql +8442 -0
- package/templates/nextblock-template/backup/backup_2025-10-01.sql +8803 -0
- package/templates/nextblock-template/backup/backup_2025-10-02.sql +9749 -0
- package/templates/nextblock-template/components/BlockRenderer.tsx +119 -0
- package/templates/nextblock-template/components/FooterNavigation.tsx +33 -0
- package/templates/nextblock-template/components/Header.tsx +42 -0
- package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -0
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +103 -0
- package/templates/nextblock-template/components/ResponsiveNav.tsx +372 -0
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +17 -0
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +93 -0
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +180 -0
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -0
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +69 -0
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +98 -0
- package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +41 -0
- package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +240 -0
- package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +79 -0
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +33 -0
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +189 -0
- package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +31 -0
- package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +59 -0
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +51 -0
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +40 -0
- package/templates/nextblock-template/components/blocks/types.ts +8 -0
- package/templates/nextblock-template/components/env-var-warning.tsx +33 -0
- package/templates/nextblock-template/components/form-message.tsx +26 -0
- package/templates/nextblock-template/components/header-auth.tsx +71 -0
- package/templates/nextblock-template/components/submit-button.tsx +23 -0
- package/templates/nextblock-template/components/theme-switcher.tsx +78 -0
- package/templates/nextblock-template/context/AuthContext.tsx +138 -0
- package/templates/nextblock-template/context/CurrentContentContext.tsx +42 -0
- package/templates/nextblock-template/context/LanguageContext.tsx +206 -0
- package/templates/nextblock-template/docs/cms-application-overview.md +56 -0
- package/templates/nextblock-template/docs/cms-architecture-overview.md +73 -0
- package/templates/nextblock-template/docs/files-structure.md +426 -0
- package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +174 -0
- package/templates/nextblock-template/eslint.config.mjs +28 -0
- package/templates/nextblock-template/index.d.ts +5 -0
- package/templates/nextblock-template/lib/blocks/README.md +670 -0
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +1001 -0
- package/templates/nextblock-template/lib/ui/ColorPicker.ts +1 -0
- package/templates/nextblock-template/lib/ui/ConfirmationDialog.ts +1 -0
- package/templates/nextblock-template/lib/ui/CustomSelectWithInput.ts +1 -0
- package/templates/nextblock-template/lib/ui/Skeleton.ts +1 -0
- package/templates/nextblock-template/lib/ui/avatar.ts +1 -0
- package/templates/nextblock-template/lib/ui/badge.ts +1 -0
- package/templates/nextblock-template/lib/ui/button.ts +1 -0
- package/templates/nextblock-template/lib/ui/card.ts +1 -0
- package/templates/nextblock-template/lib/ui/checkbox.ts +1 -0
- package/templates/nextblock-template/lib/ui/dialog.ts +1 -0
- package/templates/nextblock-template/lib/ui/dropdown-menu.ts +1 -0
- package/templates/nextblock-template/lib/ui/input.ts +1 -0
- package/templates/nextblock-template/lib/ui/label.ts +1 -0
- package/templates/nextblock-template/lib/ui/popover.ts +1 -0
- package/templates/nextblock-template/lib/ui/progress.ts +1 -0
- package/templates/nextblock-template/lib/ui/select.ts +1 -0
- package/templates/nextblock-template/lib/ui/separator.ts +1 -0
- package/templates/nextblock-template/lib/ui/table.ts +1 -0
- package/templates/nextblock-template/lib/ui/textarea.ts +1 -0
- package/templates/nextblock-template/lib/ui/tooltip.ts +1 -0
- package/templates/nextblock-template/lib/ui/ui.ts +1 -0
- package/templates/nextblock-template/middleware.ts +206 -0
- package/templates/nextblock-template/next-env.d.ts +6 -0
- package/templates/nextblock-template/next.config.js +99 -0
- package/templates/nextblock-template/package.json +52 -0
- package/templates/nextblock-template/postcss.config.js +6 -0
- package/templates/nextblock-template/project.json +7 -0
- package/templates/nextblock-template/public/.gitkeep +0 -0
- package/templates/nextblock-template/scripts/backfill-image-meta.ts +149 -0
- package/templates/nextblock-template/scripts/backup.js +53 -0
- package/templates/nextblock-template/scripts/test-bundle-optimization.js +114 -0
- package/templates/nextblock-template/tailwind.config.ts +19 -0
- package/templates/nextblock-template/tsconfig.json +62 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@nextblock-cms/ui';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@nextblock-cms/ui';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@nextblock-cms/ui';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@nextblock-cms/ui';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@nextblock-cms/ui';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@nextblock-cms/ui';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@nextblock-cms/ui';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@nextblock-cms/ui';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@nextblock-cms/ui';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@nextblock-cms/ui';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@nextblock-cms/ui';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@nextblock-cms/ui';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@nextblock-cms/ui';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@nextblock-cms/ui';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@nextblock-cms/ui';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@nextblock-cms/ui';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@nextblock-cms/ui';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@nextblock-cms/ui';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@nextblock-cms/ui';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@nextblock-cms/ui';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@nextblock-cms/ui';
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// middleware.ts
|
|
2
|
+
import { createServerClient, type CookieOptions } from '@supabase/ssr';
|
|
3
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
4
|
+
import type { Database } from "@nextblock-cms/db";
|
|
5
|
+
|
|
6
|
+
type Profile = Database["public"]["Tables"]["profiles"]["Row"];
|
|
7
|
+
type UserRole = Database["public"]["Enums"]["user_role"];
|
|
8
|
+
|
|
9
|
+
const LANGUAGE_COOKIE_KEY = 'NEXT_USER_LOCALE';
|
|
10
|
+
const DEFAULT_LOCALE = 'en';
|
|
11
|
+
const SUPPORTED_LOCALES = ['en', 'fr']; // Keep this in sync with DB or make dynamic
|
|
12
|
+
|
|
13
|
+
const cmsRoutePermissions: Record<string, UserRole[]> = {
|
|
14
|
+
'/cms': ['WRITER', 'ADMIN'],
|
|
15
|
+
'/cms/admin': ['ADMIN'],
|
|
16
|
+
'/cms/users': ['ADMIN'],
|
|
17
|
+
'/cms/settings': ['ADMIN'],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function getRequiredRolesForPath(pathname: string): UserRole[] | null {
|
|
21
|
+
const sortedPaths = Object.keys(cmsRoutePermissions).sort((a, b) => b.length - a.length);
|
|
22
|
+
for (const specificPath of sortedPaths) {
|
|
23
|
+
if (pathname === specificPath || pathname.startsWith(specificPath + (specificPath === '/' ? '' : '/'))) {
|
|
24
|
+
return cmsRoutePermissions[specificPath];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function middleware(request: NextRequest) {
|
|
31
|
+
const requestHeaders = new Headers(request.headers);
|
|
32
|
+
const nonce = crypto.randomUUID();
|
|
33
|
+
requestHeaders.set('x-nonce', nonce);
|
|
34
|
+
|
|
35
|
+
let response = NextResponse.next({
|
|
36
|
+
request: {
|
|
37
|
+
headers: requestHeaders,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
42
|
+
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
43
|
+
|
|
44
|
+
if (!supabaseUrl || !supabaseAnonKey) {
|
|
45
|
+
throw new Error('Missing required Supabase environment variables');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const supabase = createServerClient(
|
|
49
|
+
supabaseUrl,
|
|
50
|
+
supabaseAnonKey,
|
|
51
|
+
{
|
|
52
|
+
cookies: {
|
|
53
|
+
get(name: string) { return request.cookies.get(name)?.value; },
|
|
54
|
+
set(name: string, value: string, options: CookieOptions) {
|
|
55
|
+
request.cookies.set({ name, value, ...options });
|
|
56
|
+
response = NextResponse.next({ request: { headers: requestHeaders } });
|
|
57
|
+
response.cookies.set({ name, value, ...options });
|
|
58
|
+
},
|
|
59
|
+
remove(name: string, options: CookieOptions) {
|
|
60
|
+
request.cookies.set({ name, value: '', ...options });
|
|
61
|
+
response = NextResponse.next({ request: { headers: requestHeaders } });
|
|
62
|
+
response.cookies.set({ name, value: '', ...options });
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
await supabase.auth.getSession();
|
|
69
|
+
|
|
70
|
+
const cookieLocale = request.cookies.get(LANGUAGE_COOKIE_KEY)?.value;
|
|
71
|
+
let currentLocale = cookieLocale;
|
|
72
|
+
|
|
73
|
+
if (!currentLocale || !SUPPORTED_LOCALES.includes(currentLocale)) {
|
|
74
|
+
currentLocale = DEFAULT_LOCALE;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
requestHeaders.set('X-User-Locale', currentLocale);
|
|
78
|
+
|
|
79
|
+
const { data: { user }, error: userError } = await supabase.auth.getUser(); // Use getUser for revalidation
|
|
80
|
+
const { pathname } = request.nextUrl;
|
|
81
|
+
|
|
82
|
+
// CMS route protection
|
|
83
|
+
if (pathname.startsWith('/cms')) { // Ensure this check is broad enough for all CMS paths
|
|
84
|
+
if (userError || !user) { // Check for error or no user
|
|
85
|
+
return NextResponse.redirect(new URL(`/sign-in?redirect=${pathname}`, request.url));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const requiredRoles = getRequiredRolesForPath(pathname);
|
|
89
|
+
|
|
90
|
+
if (requiredRoles && requiredRoles.length > 0) {
|
|
91
|
+
const { data: profile, error: profileError } = await supabase
|
|
92
|
+
.from('profiles')
|
|
93
|
+
.select('role')
|
|
94
|
+
.eq('id', user.id)
|
|
95
|
+
.single<Pick<Profile, 'role'>>();
|
|
96
|
+
|
|
97
|
+
if (profileError || !profile) {
|
|
98
|
+
console.error(`Middleware: Profile error for user ${user.id} accessing ${pathname}. Error: ${profileError?.message}. Redirecting to unauthorized.`);
|
|
99
|
+
return NextResponse.redirect(new URL('/unauthorized?error=profile_issue', request.url));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const userRole = profile.role as UserRole;
|
|
103
|
+
if (!requiredRoles.includes(userRole)) {
|
|
104
|
+
console.warn(`Middleware: User ${user.id} (Role: ${userRole}) denied access to ${pathname}. Required: ${requiredRoles.join(' OR ')}. Redirecting to unauthorized.`);
|
|
105
|
+
return NextResponse.redirect(new URL(`/unauthorized?path=${pathname}&required=${requiredRoles.join(',')}`, request.url));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (response.headers.get('location')) {
|
|
111
|
+
return response;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const finalResponse = NextResponse.next({
|
|
115
|
+
request: {
|
|
116
|
+
headers: requestHeaders,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
response.cookies.getAll().forEach(cookie => {
|
|
121
|
+
finalResponse.cookies.set(cookie.name, cookie.value, cookie);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (request.cookies.get(LANGUAGE_COOKIE_KEY)?.value !== currentLocale) {
|
|
125
|
+
finalResponse.cookies.set(LANGUAGE_COOKIE_KEY, currentLocale, { path: '/', maxAge: 31536000, sameSite: 'lax' });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (pathname === '/sign-in' || pathname === '/sign-up' || pathname === '/forgot-password') {
|
|
129
|
+
finalResponse.headers.set('X-Page-Type', 'auth');
|
|
130
|
+
finalResponse.headers.set('X-Prefetch-Priority', 'critical');
|
|
131
|
+
} else if (pathname === '/') {
|
|
132
|
+
finalResponse.headers.set('X-Page-Type', 'home');
|
|
133
|
+
finalResponse.headers.set('X-Prefetch-Priority', 'high');
|
|
134
|
+
} else if (pathname === '/blog') {
|
|
135
|
+
finalResponse.headers.set('X-Page-Type', 'blog-index');
|
|
136
|
+
finalResponse.headers.set('X-Prefetch-Priority', 'high');
|
|
137
|
+
} else if (pathname.startsWith('/blog/')) {
|
|
138
|
+
finalResponse.headers.set('X-Page-Type', 'blog-post');
|
|
139
|
+
finalResponse.headers.set('X-Prefetch-Priority', 'medium');
|
|
140
|
+
} else {
|
|
141
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
142
|
+
if (segments.length === 1 && !pathname.startsWith('/cms')) {
|
|
143
|
+
finalResponse.headers.set('X-Page-Type', 'dynamic-page');
|
|
144
|
+
finalResponse.headers.set('X-Prefetch-Priority', 'medium');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const acceptHeader = request.headers.get('accept');
|
|
149
|
+
if (
|
|
150
|
+
acceptHeader &&
|
|
151
|
+
acceptHeader.includes('text/html') &&
|
|
152
|
+
!pathname.startsWith('/api/')
|
|
153
|
+
) {
|
|
154
|
+
finalResponse.headers.set('Cache-Control', 'public, max-age=0, must-revalidate');
|
|
155
|
+
finalResponse.headers.set('X-BFCache-Applied', 'true');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
finalResponse.headers.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');
|
|
159
|
+
finalResponse.headers.set('X-Frame-Options', 'SAMEORIGIN');
|
|
160
|
+
finalResponse.headers.set('X-Content-Type-Options', 'nosniff');
|
|
161
|
+
finalResponse.headers.set('Referrer-Policy', 'origin-when-cross-origin');
|
|
162
|
+
finalResponse.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
|
163
|
+
finalResponse.headers.set('Cross-Origin-Opener-Policy', 'same-origin');
|
|
164
|
+
|
|
165
|
+
// Only send nonce-based CSP in production. In development, Next.js
|
|
166
|
+
// Dev Overlay injects inline scripts without a nonce, which would be blocked.
|
|
167
|
+
if (process.env.NODE_ENV === 'production') {
|
|
168
|
+
const nonceValue = requestHeaders.get('x-nonce');
|
|
169
|
+
if (nonceValue) {
|
|
170
|
+
const csp = [
|
|
171
|
+
"default-src 'self'",
|
|
172
|
+
`script-src 'self' blob: data: 'nonce-${nonceValue}'`,
|
|
173
|
+
"style-src 'self' 'unsafe-inline'",
|
|
174
|
+
"img-src 'self' data: blob: https://nrh-next-cms.e260676f72b0b18314b868f136ed72ae.r2.cloudflarestorage.com https://pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev",
|
|
175
|
+
"font-src 'self'",
|
|
176
|
+
"object-src 'none'",
|
|
177
|
+
"connect-src 'self' https://ppcppwsfnrptznvbxnsz.supabase.co wss://ppcppwsfnrptznvbxnsz.supabase.co https://nrh-next-cms.e260676f72b0b18314b868f136ed72ae.r2.cloudflarestorage.com https://pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev",
|
|
178
|
+
"frame-src 'self' blob: data: https://www.youtube.com",
|
|
179
|
+
"form-action 'self'",
|
|
180
|
+
"base-uri 'self'",
|
|
181
|
+
].join('; ');
|
|
182
|
+
|
|
183
|
+
finalResponse.headers.set('Content-Security-Policy', csp);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const responseForLogging = finalResponse.clone();
|
|
188
|
+
const cacheStatus = responseForLogging.headers.get('x-vercel-cache') || 'none';
|
|
189
|
+
|
|
190
|
+
if (!pathname.startsWith('/api/')) {
|
|
191
|
+
console.log(JSON.stringify({
|
|
192
|
+
type: 'cache',
|
|
193
|
+
status: cacheStatus,
|
|
194
|
+
path: pathname,
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return finalResponse;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export const config = {
|
|
202
|
+
matcher: [
|
|
203
|
+
'/((?!_next/static|_next/image|favicon.ico|auth/.*|sign-in|sign-up|forgot-password|unauthorized|api/auth/.*|api/revalidate|api/revalidate-log).*)',
|
|
204
|
+
'/cms/:path*',
|
|
205
|
+
],
|
|
206
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/// <reference types="next" />
|
|
2
|
+
/// <reference types="next/image-types/global" />
|
|
3
|
+
/// <reference path="./../../dist/apps/nextblock/.next/types/routes.d.ts" />
|
|
4
|
+
|
|
5
|
+
// NOTE: This file should not be edited
|
|
6
|
+
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
const { composePlugins, withNx } = require('@nx/next');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @type {import('@nx/next/plugins/with-nx').WithNxOptions}
|
|
7
|
+
**/
|
|
8
|
+
const nextConfig = {
|
|
9
|
+
// Use this to set Nx-specific options
|
|
10
|
+
// See: https://nx.dev/recipes/next/next-config-setup
|
|
11
|
+
nx: {svgr: false},
|
|
12
|
+
env: {
|
|
13
|
+
NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,
|
|
14
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
|
|
15
|
+
},
|
|
16
|
+
images: {
|
|
17
|
+
formats: ['image/avif', 'image/webp'],
|
|
18
|
+
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384, 512],
|
|
19
|
+
deviceSizes: [320, 480, 640, 750, 828, 1080, 1200, 1440, 1920, 2048, 2560],
|
|
20
|
+
minimumCacheTTL: 31536000,
|
|
21
|
+
dangerouslyAllowSVG: false,
|
|
22
|
+
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
|
|
23
|
+
remotePatterns: [
|
|
24
|
+
{
|
|
25
|
+
protocol: 'https',
|
|
26
|
+
hostname: 'pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
protocol: 'https',
|
|
30
|
+
hostname: 'e260676f72b0b18314b868f136ed72ae.r2.cloudflarestorage.com',
|
|
31
|
+
},
|
|
32
|
+
// Add other necessary hostnames, for example, from NEXT_PUBLIC_URL if it's different
|
|
33
|
+
// and used for images. This example assumes NEXT_PUBLIC_R2_BASE_URL's hostname is the one above.
|
|
34
|
+
// If NEXT_PUBLIC_URL is also an image source and has a different hostname:
|
|
35
|
+
...(process.env.NEXT_PUBLIC_URL
|
|
36
|
+
? [
|
|
37
|
+
{
|
|
38
|
+
protocol: /** @type {'http' | 'https'} */ (new URL(process.env.NEXT_PUBLIC_URL).protocol.slice(0, -1)),
|
|
39
|
+
hostname: new URL(process.env.NEXT_PUBLIC_URL).hostname,
|
|
40
|
+
},
|
|
41
|
+
]
|
|
42
|
+
: []),
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
experimental: {
|
|
46
|
+
optimizeCss: true,
|
|
47
|
+
cssChunking: 'strict',
|
|
48
|
+
},
|
|
49
|
+
transpilePackages: ['@nextblock-cms/utils', '@nextblock-cms/ui', '@nextblock-cms/editor'],
|
|
50
|
+
webpack: (config, { isServer }) => {
|
|
51
|
+
if (!isServer) {
|
|
52
|
+
config.module.rules.push({
|
|
53
|
+
test: /\.svg$/i,
|
|
54
|
+
issuer: /\.[jt]sx?$/,
|
|
55
|
+
use: ['@svgr/webpack'],
|
|
56
|
+
});
|
|
57
|
+
// Optimize TipTap bundle separation for client-side
|
|
58
|
+
config.optimization = {
|
|
59
|
+
...config.optimization,
|
|
60
|
+
splitChunks: {
|
|
61
|
+
...config.optimization.splitChunks,
|
|
62
|
+
cacheGroups: {
|
|
63
|
+
...config.optimization.splitChunks?.cacheGroups,
|
|
64
|
+
// Create a separate chunk for TipTap and related dependencies
|
|
65
|
+
tiptap: {
|
|
66
|
+
test: /[\\/]node_modules[\\/](@tiptap|prosemirror)[\\/]/,
|
|
67
|
+
name: 'tiptap',
|
|
68
|
+
chunks: 'async', // Only include in async chunks (dynamic imports)
|
|
69
|
+
priority: 30,
|
|
70
|
+
reuseExistingChunk: true,
|
|
71
|
+
},
|
|
72
|
+
// Separate chunk for TipTap extensions and custom components
|
|
73
|
+
tiptapExtensions: {
|
|
74
|
+
test: /[\\/](tiptap-extensions|RichTextEditor|MenuBar|MediaLibraryModal)[\\/]/,
|
|
75
|
+
name: 'tiptap-extensions',
|
|
76
|
+
chunks: 'async',
|
|
77
|
+
priority: 25,
|
|
78
|
+
reuseExistingChunk: true,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return config;
|
|
85
|
+
},
|
|
86
|
+
turbopack: {
|
|
87
|
+
// Turbopack-specific options can be placed here if needed in the future
|
|
88
|
+
},
|
|
89
|
+
compiler: {
|
|
90
|
+
removeConsole: process.env.NODE_ENV === 'production',
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const plugins = [
|
|
95
|
+
// Add more Next.js plugins to this list if needed.
|
|
96
|
+
withNx,
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
module.exports = composePlugins(...plugins)(nextConfig);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nextblock-cms/template",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start",
|
|
9
|
+
"lint": "next lint"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@aws-sdk/client-s3": "^3.810.0",
|
|
13
|
+
"@dnd-kit/core": "^6.3.1",
|
|
14
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
15
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
16
|
+
"@supabase/ssr": "^0.6.1",
|
|
17
|
+
"@supabase/supabase-js": "^2.47.2",
|
|
18
|
+
"@tiptap/react": "^3.3.0",
|
|
19
|
+
"date-fns": "^3.6.0",
|
|
20
|
+
"dotenv": "^16.5.0",
|
|
21
|
+
"fast-json-patch": "^3.1.1",
|
|
22
|
+
"html-react-parser": "^5.2.6",
|
|
23
|
+
"js-cookie": "^3.0.5",
|
|
24
|
+
"lodash.debounce": "^4.0.8",
|
|
25
|
+
"lucide-react": "^0.534.0",
|
|
26
|
+
"next": "^15.5.4",
|
|
27
|
+
"nodemailer": "^7.0.4",
|
|
28
|
+
"plaiceholder": "^3.0.0",
|
|
29
|
+
"react": "19.0.0",
|
|
30
|
+
"react-dom": "19.0.0",
|
|
31
|
+
"react-hot-toast": "^2.4.1",
|
|
32
|
+
"sharp": "^0.34.2",
|
|
33
|
+
"uuid": "^10.0.0",
|
|
34
|
+
"zod": "^3.25.76",
|
|
35
|
+
"@nextblock-cms/ui": "workspace:*",
|
|
36
|
+
"@nextblock-cms/utils": "workspace:*",
|
|
37
|
+
"@nextblock-cms/db": "workspace:*",
|
|
38
|
+
"@nextblock-cms/editor": "workspace:*",
|
|
39
|
+
"@nextblock-cms/sdk": "workspace:*"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "22.10.2",
|
|
43
|
+
"@types/react": "^19.0.0",
|
|
44
|
+
"@types/react-dom": "19.0.2",
|
|
45
|
+
"autoprefixer": "^10.4.13",
|
|
46
|
+
"eslint": "^9.8.0",
|
|
47
|
+
"eslint-config-next": "^15.5.4",
|
|
48
|
+
"postcss": "^8.4.38",
|
|
49
|
+
"tailwindcss": "^3.4.3",
|
|
50
|
+
"typescript": "~5.8.2"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// scripts/backfill-image-meta.ts
|
|
2
|
+
import { createClient } from '@supabase/supabase-js';
|
|
3
|
+
import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
4
|
+
import sharp from 'sharp';
|
|
5
|
+
import { Readable } from 'stream';
|
|
6
|
+
import { getPlaiceholder } from 'plaiceholder';
|
|
7
|
+
import 'dotenv/config';
|
|
8
|
+
import * as dotenv from 'dotenv';
|
|
9
|
+
|
|
10
|
+
dotenv.config({ path: '.env.local' });
|
|
11
|
+
|
|
12
|
+
// Helper to convert stream to buffer
|
|
13
|
+
async function streamToBuffer(stream: Readable): Promise<Buffer> {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const chunks: Buffer[] = [];
|
|
16
|
+
stream.on('data', (chunk) => chunks.push(chunk as Buffer));
|
|
17
|
+
stream.on('error', reject);
|
|
18
|
+
stream.on('end', () => resolve(Buffer.concat(chunks)));
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME;
|
|
23
|
+
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
24
|
+
const SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
25
|
+
const MAX_WIDTH = 2560;
|
|
26
|
+
|
|
27
|
+
async function backfillImageMeta() {
|
|
28
|
+
const {
|
|
29
|
+
R2_ACCOUNT_ID,
|
|
30
|
+
R2_ACCESS_KEY_ID,
|
|
31
|
+
R2_SECRET_ACCESS_KEY,
|
|
32
|
+
R2_BUCKET_NAME: Bucket,
|
|
33
|
+
} = process.env;
|
|
34
|
+
|
|
35
|
+
if (
|
|
36
|
+
!R2_ACCOUNT_ID ||
|
|
37
|
+
!R2_ACCESS_KEY_ID ||
|
|
38
|
+
!R2_SECRET_ACCESS_KEY ||
|
|
39
|
+
!Bucket
|
|
40
|
+
) {
|
|
41
|
+
console.error('Cloudflare R2 environment variables are not fully set.');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
if (!SUPABASE_URL || !SUPABASE_SERVICE_ROLE_KEY) {
|
|
45
|
+
console.error('Supabase environment variables are not set.');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const s3Client = new S3Client({
|
|
50
|
+
region: 'auto',
|
|
51
|
+
endpoint: `https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
|
52
|
+
credentials: {
|
|
53
|
+
accessKeyId: R2_ACCESS_KEY_ID,
|
|
54
|
+
secretAccessKey: R2_SECRET_ACCESS_KEY,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY);
|
|
59
|
+
|
|
60
|
+
// Fetch all media records
|
|
61
|
+
const { data: mediaItems, error } = await supabase.from('media').select('id, object_key, width');
|
|
62
|
+
|
|
63
|
+
if (error) {
|
|
64
|
+
console.error('Error fetching media items:', error);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!mediaItems || mediaItems.length === 0) {
|
|
69
|
+
console.log('No images to backfill.');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log(`Found ${mediaItems.length} images to process.`);
|
|
74
|
+
|
|
75
|
+
for (const item of mediaItems) {
|
|
76
|
+
try {
|
|
77
|
+
if (item.width && item.width > MAX_WIDTH) {
|
|
78
|
+
console.log(`Resizing image ${item.id} (${item.object_key}) from ${item.width}px wide.`);
|
|
79
|
+
|
|
80
|
+
// 1. Download the image from R2
|
|
81
|
+
const getObjectParams = {
|
|
82
|
+
Bucket: R2_BUCKET_NAME,
|
|
83
|
+
Key: item.object_key,
|
|
84
|
+
};
|
|
85
|
+
const getObjectResponse = await s3Client.send(new GetObjectCommand(getObjectParams));
|
|
86
|
+
|
|
87
|
+
if (!getObjectResponse.Body) {
|
|
88
|
+
throw new Error('Failed to retrieve image from R2: Empty body.');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const imageBuffer = await streamToBuffer(getObjectResponse.Body as Readable);
|
|
92
|
+
|
|
93
|
+
// 2. Resize the image
|
|
94
|
+
const resizedImageBuffer = await sharp(imageBuffer)
|
|
95
|
+
.resize({
|
|
96
|
+
width: MAX_WIDTH,
|
|
97
|
+
withoutEnlargement: true,
|
|
98
|
+
})
|
|
99
|
+
.toBuffer();
|
|
100
|
+
|
|
101
|
+
// 3. Upload the resized image back to R2, overwriting the original
|
|
102
|
+
const putObjectParams = {
|
|
103
|
+
Bucket: R2_BUCKET_NAME,
|
|
104
|
+
Key: item.object_key,
|
|
105
|
+
Body: resizedImageBuffer,
|
|
106
|
+
ContentType: getObjectResponse.ContentType,
|
|
107
|
+
};
|
|
108
|
+
await s3Client.send(new PutObjectCommand(putObjectParams));
|
|
109
|
+
|
|
110
|
+
// 4. Get new metadata from the resized image
|
|
111
|
+
const sharpInstance = sharp(resizedImageBuffer);
|
|
112
|
+
const metadata = await sharpInstance.metadata();
|
|
113
|
+
const { base64: blurDataURL } = await getPlaiceholder(resizedImageBuffer, { size: 10 });
|
|
114
|
+
|
|
115
|
+
const { width, height } = metadata;
|
|
116
|
+
|
|
117
|
+
if (!width || !height) {
|
|
118
|
+
console.warn(`Could not extract new width/height for ${item.object_key}. Skipping update.`);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 5. Update the record in the media table
|
|
123
|
+
const { error: updateError } = await supabase
|
|
124
|
+
.from('media')
|
|
125
|
+
.update({
|
|
126
|
+
width,
|
|
127
|
+
height,
|
|
128
|
+
blur_data_url: blurDataURL,
|
|
129
|
+
updated_at: new Date().toISOString(),
|
|
130
|
+
})
|
|
131
|
+
.eq('id', item.id);
|
|
132
|
+
|
|
133
|
+
if (updateError) {
|
|
134
|
+
console.error(`Failed to update item ${item.id}:`, updateError);
|
|
135
|
+
} else {
|
|
136
|
+
console.log(`Successfully resized and updated item ${item.id}.`);
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
console.log(`Skipping image ${item.id} (${item.object_key}) - not oversized.`);
|
|
140
|
+
}
|
|
141
|
+
} catch (e: unknown) {
|
|
142
|
+
console.error(`An error occurred while processing item ${item.id}:`, e instanceof Error ? e.message : String(e));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log('Backfill complete.');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
backfillImageMeta();
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const { exec } = require('child_process');
|
|
2
|
+
const { URL } = require('url');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
require('dotenv').config({ path: '.env.local' });
|
|
6
|
+
|
|
7
|
+
// Backup directory
|
|
8
|
+
const backupDir = path.join(__dirname, '..', 'backup');
|
|
9
|
+
|
|
10
|
+
// Create backup directory if it doesn't exist
|
|
11
|
+
if (!fs.existsSync(backupDir)) {
|
|
12
|
+
fs.mkdirSync(backupDir);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Backup file name
|
|
16
|
+
const backupFile = path.join(backupDir, `backup_${new Date().toISOString().slice(0, 10)}.sql`);
|
|
17
|
+
|
|
18
|
+
// Function to perform the backup
|
|
19
|
+
const backupDatabase = () => {
|
|
20
|
+
const connectionString = process.env.POSTGRES_URL;
|
|
21
|
+
|
|
22
|
+
if (!connectionString) {
|
|
23
|
+
console.error('Error: Missing POSTGRES_URL in .env.local file.');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Parse the URL and remove non-standard parameters for pg_dump
|
|
28
|
+
const url = new URL(connectionString);
|
|
29
|
+
url.searchParams.delete('supa'); // Remove supa parameter
|
|
30
|
+
const finalConnectionString = url.toString();
|
|
31
|
+
|
|
32
|
+
const command = `pg_dump "${finalConnectionString}" > "${backupFile}"`;
|
|
33
|
+
|
|
34
|
+
console.log('Starting database backup...');
|
|
35
|
+
|
|
36
|
+
exec(command, (error, stdout, stderr) => {
|
|
37
|
+
if (error) {
|
|
38
|
+
console.error(`Error executing backup: ${error.message}`);
|
|
39
|
+
if (stderr) {
|
|
40
|
+
console.error(`pg_dump error: ${stderr}`);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (stderr && stderr.toLowerCase().includes('error')) {
|
|
45
|
+
console.error(`Error: ${stderr}`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
console.log(`Backup successful! File saved as ${backupFile}`);
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Run the backup
|
|
53
|
+
backupDatabase();
|