create-nextblock 0.9.95 → 0.9.98

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 (42) hide show
  1. package/package.json +1 -1
  2. package/templates/nextblock-template/app/[slug]/page.tsx +9 -2
  3. package/templates/nextblock-template/app/actions/package-actions.ts +9 -4
  4. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3 -2
  5. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +5 -1
  6. package/templates/nextblock-template/app/api/draft/route.ts +5 -1
  7. package/templates/nextblock-template/app/api/media/library/route.ts +6 -2
  8. package/templates/nextblock-template/app/api/process-image/route.ts +58 -43
  9. package/templates/nextblock-template/app/api/revalidate/route.ts +21 -18
  10. package/templates/nextblock-template/app/api/upload/presigned-url/route.ts +20 -8
  11. package/templates/nextblock-template/app/api/upload/proxy/route.ts +34 -28
  12. package/templates/nextblock-template/app/article/[slug]/page.tsx +4 -13
  13. package/templates/nextblock-template/app/checkout/success/actions.ts +3 -2
  14. package/templates/nextblock-template/app/cms/media/actions.ts +47 -31
  15. package/templates/nextblock-template/app/cms/orders/actions.ts +29 -29
  16. package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +28 -28
  17. package/templates/nextblock-template/app/cms/users/actions.ts +119 -118
  18. package/templates/nextblock-template/app/cms/users/page.tsx +3 -3
  19. package/templates/nextblock-template/app/layout.tsx +10 -7
  20. package/templates/nextblock-template/app/lib/site-settings.ts +7 -4
  21. package/templates/nextblock-template/app/page.tsx +2 -1
  22. package/templates/nextblock-template/app/product/[slug]/page.tsx +9 -2
  23. package/templates/nextblock-template/app/robots.txt/route.ts +6 -3
  24. package/templates/nextblock-template/app/setup/SetupWizard.tsx +55 -8
  25. package/templates/nextblock-template/app/setup/page.tsx +17 -1
  26. package/templates/nextblock-template/app/sitemap.ts +5 -3
  27. package/templates/nextblock-template/context/language-rest-client.ts +3 -2
  28. package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +125 -32
  29. package/templates/nextblock-template/lib/app-secrets.ts +39 -0
  30. package/templates/nextblock-template/lib/auth/crypto.ts +4 -1
  31. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +23 -3
  32. package/templates/nextblock-template/lib/setup/actions.ts +16 -0
  33. package/templates/nextblock-template/lib/setup/env-status.ts +44 -5
  34. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +172 -0
  35. package/templates/nextblock-template/lib/setup/schema-apply.ts +23 -7
  36. package/templates/nextblock-template/lib/site-url.ts +48 -0
  37. package/templates/nextblock-template/lib/storage/provider.ts +66 -0
  38. package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -0
  39. package/templates/nextblock-template/lib/visual-editing/draft-content.ts +1 -1
  40. package/templates/nextblock-template/next.config.js +37 -2
  41. package/templates/nextblock-template/package.json +1 -1
  42. package/templates/nextblock-template/proxy.ts +39 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nextblock",
3
- "version": "0.9.95",
3
+ "version": "0.9.98",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -38,8 +38,15 @@ interface PageTranslation {
38
38
  }
39
39
 
40
40
  export async function generateStaticParams(): Promise<ResolvedPageParams[]> {
41
- // Unconfigured instance (pre-/setup): no DB to read slugs from.
42
- if (!process.env.NEXT_PUBLIC_SUPABASE_URL || !process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) {
41
+ // Unconfigured instance (pre-/setup): no DB to read slugs from. Accept every Supabase
42
+ // key alias the Vercel integration may inject (incl. the new publishable key).
43
+ const hasSupabaseEnv =
44
+ (process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL) &&
45
+ (process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ||
46
+ process.env.SUPABASE_ANON_KEY ||
47
+ process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY ||
48
+ process.env.SUPABASE_PUBLISHABLE_KEY);
49
+ if (!hasSupabaseEnv) {
43
50
  return [];
44
51
  }
45
52
 
@@ -4,6 +4,11 @@ import { createClient } from '@supabase/supabase-js';
4
4
  import { NEXTBLOCK_PACKAGES } from '@nextblock-cms/utils';
5
5
  import { headers } from 'next/headers';
6
6
  import { revalidatePath } from 'next/cache';
7
+ import {
8
+ resolveSupabaseAnonKey,
9
+ resolveSupabaseServiceKey,
10
+ resolveSupabaseUrl,
11
+ } from '../../lib/setup/env-status';
7
12
 
8
13
  // Freemius handles both Sandbox and Production keys on the same API domain.
9
14
  // The key itself determines the environment.
@@ -11,15 +16,15 @@ const FM_API_URL = 'https://api.freemius.com/v1';
11
16
 
12
17
  // Helper to get service role client
13
18
  const getServiceRoleClient = () => {
14
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
15
- const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
19
+ const supabaseUrl = resolveSupabaseUrl();
20
+ const supabaseServiceKey = resolveSupabaseServiceKey();
16
21
 
17
22
  if (!supabaseUrl || !supabaseServiceKey) {
18
23
  console.error('Missing Supabase credentials');
19
24
  throw new Error('Missing Supabase credentials (Service Key required for activation).');
20
25
  }
21
26
 
22
- if (supabaseServiceKey === process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) {
27
+ if (supabaseServiceKey === resolveSupabaseAnonKey()) {
23
28
  console.warn('CRITICAL WARNING: SUPABASE_SERVICE_ROLE_KEY matches NEXT_PUBLIC_SUPABASE_ANON_KEY. This will likely cause Permission Denied errors as RLS cannot be bypassed.');
24
29
  }
25
30
 
@@ -73,7 +78,7 @@ export async function activatePackage(key: string) {
73
78
  }
74
79
  });
75
80
 
76
- const responseData = await response.json();
81
+ const responseData = await response.json();
77
82
 
78
83
  // Freemius returns the license object directly if successful, or an error/api_response
79
84
  if (response.ok && responseData.install_id) {
@@ -2728,8 +2728,9 @@ export async function GET(request: NextRequest) {
2728
2728
  });
2729
2729
  }
2730
2730
 
2731
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
2732
- const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
2731
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL;
2732
+ const supabaseServiceKey =
2733
+ process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SECRET_KEY;
2733
2734
 
2734
2735
  const r2AccountId = process.env.R2_ACCOUNT_ID;
2735
2736
  const r2AccessKeyId = process.env.R2_ACCESS_KEY_ID;
@@ -6,9 +6,13 @@ export const dynamic = 'force-dynamic';
6
6
  export const maxDuration = 30;
7
7
 
8
8
  export async function GET(request: NextRequest) {
9
+ // CRON_SECRET is optional: when set we enforce Vercel's `Authorization: Bearer`
10
+ // header, but a zero-config deploy can leave it unset (the job only does low-harm
11
+ // FX-rate sync). Set CRON_SECRET in the project env to lock the endpoint down.
12
+ const cronSecret = process.env.CRON_SECRET?.trim();
9
13
  const authHeader = request.headers.get('authorization');
10
14
 
11
- if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
15
+ if (cronSecret && authHeader !== `Bearer ${cronSecret}`) {
12
16
  return new NextResponse('Unauthorized', {
13
17
  status: 401,
14
18
  });
@@ -7,6 +7,7 @@ import {
7
7
  resolveRequestOrigin,
8
8
  type DraftPathTarget,
9
9
  } from "../../../lib/visual-editing/draft-route";
10
+ import { resolveDraftModeSecret } from "../../../lib/app-secrets";
10
11
 
11
12
  export const runtime = "nodejs";
12
13
  export const dynamic = "force-dynamic";
@@ -54,7 +55,10 @@ async function targetExists(target: DraftPathTarget) {
54
55
  }
55
56
 
56
57
  export async function GET(request: NextRequest) {
57
- const configuredSecret = process.env.DRAFT_MODE_SECRET;
58
+ // Explicit DRAFT_MODE_SECRET wins; otherwise it is derived from the service-role
59
+ // key so this preview entry works on a zero-config deploy. Empty only when Supabase
60
+ // itself is unconfigured.
61
+ const configuredSecret = resolveDraftModeSecret();
58
62
 
59
63
  if (!configuredSecret) {
60
64
  return NextResponse.json(
@@ -7,8 +7,12 @@ const DEFAULT_MEDIA_LIBRARY_ITEMS = 50;
7
7
  export const dynamic = 'force-dynamic';
8
8
 
9
9
  function getMediaLibrarySupabaseClient() {
10
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
11
- const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
10
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL;
11
+ const supabaseAnonKey =
12
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ||
13
+ process.env.SUPABASE_ANON_KEY ||
14
+ process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY ||
15
+ process.env.SUPABASE_PUBLISHABLE_KEY;
12
16
 
13
17
  if (!supabaseUrl || !supabaseAnonKey) {
14
18
  throw new Error('Missing Supabase environment variables for media library API');
@@ -5,6 +5,15 @@ import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
5
5
  import sharp from 'sharp';
6
6
  import { Readable } from 'stream';
7
7
  import { getPlaiceholder } from 'plaiceholder';
8
+ import {
9
+ getStorageBackend,
10
+ getStorageBucket,
11
+ resolveMediaBaseUrl,
12
+ } from '../../../lib/storage/provider';
13
+ import {
14
+ supabaseDownloadObject,
15
+ supabaseUploadObject,
16
+ } from '../../../lib/storage/supabase-storage';
8
17
 
9
18
  // Helper to convert stream to buffer
10
19
  async function streamToBuffer(stream: Readable): Promise<Buffer> {
@@ -16,12 +25,6 @@ async function streamToBuffer(stream: Readable): Promise<Buffer> {
16
25
  });
17
26
  }
18
27
 
19
- const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME;
20
- // Construct the public URL base. Assumes your R2 bucket is set up for public access.
21
- // Example: https://<your-bucket>.<account-id>.r2.cloudflarestorage.com
22
- // Or if you use a custom domain: https://your.custom.domain
23
- const R2_PUBLIC_URL_BASE = process.env.NEXT_PUBLIC_R2_BASE_URL;
24
-
25
28
  interface ProcessedImageVariant {
26
29
  objectKey: string;
27
30
  url: string;
@@ -44,22 +47,45 @@ const TARGET_FORMAT = 'avif';
44
47
  const TARGET_MIME_TYPE = 'image/avif';
45
48
 
46
49
  export async function POST(request: NextRequest) {
47
- if (!R2_BUCKET_NAME) {
48
- return NextResponse.json({ error: 'R2 bucket name is not configured.' }, { status: 500 });
50
+ const backend = getStorageBackend();
51
+ const bucket = getStorageBucket();
52
+ const publicUrlBase = resolveMediaBaseUrl();
53
+
54
+ if (!bucket) {
55
+ return NextResponse.json({ error: 'Storage bucket is not configured.' }, { status: 500 });
56
+ }
57
+ // The native Supabase path derives the public base from the project URL — if that's
58
+ // somehow empty (e.g. STORAGE_PROVIDER=supabase forced without a URL), fail loudly
59
+ // rather than minting malformed `/key` URLs that won't resolve to the bucket.
60
+ if (backend === 'supabase' && !publicUrlBase) {
61
+ return NextResponse.json({ error: 'Supabase URL is required for image processing on the Supabase backend.' }, { status: 500 });
49
62
  }
50
- if (!process.env.R2_S3_ENDPOINT && !process.env.R2_ACCOUNT_ID) {
51
- console.error("R2_S3_ENDPOINT or R2_ACCOUNT_ID must be set to construct R2_PUBLIC_URL_BASE");
52
- return NextResponse.json({ error: 'Server configuration error for R2 public URL.' }, { status: 500 });
63
+ // The S3 path needs an endpoint to construct the public URL base; the native Supabase
64
+ // path derives it from the project URL, so this check only applies to S3/R2.
65
+ if (backend === 's3' && !process.env.R2_S3_ENDPOINT && !process.env.R2_ACCOUNT_ID) {
66
+ console.error('R2_S3_ENDPOINT or R2_ACCOUNT_ID must be set to construct the public URL base');
67
+ return NextResponse.json({ error: 'Server configuration error for storage public URL.' }, { status: 500 });
53
68
  }
54
69
 
55
70
 
56
71
  try {
57
- const s3Client = await getS3Client();
58
- if (!s3Client) {
72
+ const s3Client = backend === 's3' ? await getS3Client() : null;
73
+ if (backend === 's3' && !s3Client) {
59
74
  console.error('R2 client is not configured. Check your R2 environment variables.');
60
75
  return NextResponse.json({ error: 'Image processing is not configured on this server.' }, { status: 500 });
61
76
  }
62
77
 
78
+ // Backend-agnostic object writer used for every processed variant below.
79
+ const putObject = async (key: string, body: Buffer, contentType: string) => {
80
+ if (backend === 'supabase') {
81
+ await supabaseUploadObject(key, body, contentType);
82
+ } else {
83
+ await s3Client!.send(
84
+ new PutObjectCommand({ Bucket: bucket, Key: key, Body: body, ContentType: contentType }),
85
+ );
86
+ }
87
+ };
88
+
63
89
  const { objectKey: originalObjectKey, contentType: originalContentType } = await request.json();
64
90
 
65
91
  if (!originalObjectKey || !originalContentType) {
@@ -70,25 +96,25 @@ export async function POST(request: NextRequest) {
70
96
  // For now, we only process images. Could be extended for other file types if needed.
71
97
  return NextResponse.json({
72
98
  message: 'File is not an image. Skipping processing.',
73
- originalImage: { objectKey: originalObjectKey, fileType: originalContentType, url: `${R2_PUBLIC_URL_BASE}/${originalObjectKey}` },
99
+ originalImage: { objectKey: originalObjectKey, fileType: originalContentType, url: `${publicUrlBase}/${originalObjectKey}` },
74
100
  processedVariants: [],
75
101
  blurDataURL: null // Or an empty string, depending on how you want to handle non-images
76
102
  }, { status: 200 });
77
103
  }
78
104
 
79
- // 1. Fetch the original image from R2
80
- const getObjectParams = {
81
- Bucket: R2_BUCKET_NAME,
82
- Key: originalObjectKey,
83
- };
84
- const getObjectCommand = new GetObjectCommand(getObjectParams);
85
- const getObjectResponse = await s3Client.send(getObjectCommand);
86
-
87
- if (!getObjectResponse.Body) {
88
- throw new Error('Failed to retrieve image from R2: Empty body.');
105
+ // 1. Fetch the original image from storage
106
+ let imageBuffer: Buffer;
107
+ if (backend === 'supabase') {
108
+ imageBuffer = await supabaseDownloadObject(originalObjectKey);
109
+ } else {
110
+ const getObjectResponse = await s3Client!.send(
111
+ new GetObjectCommand({ Bucket: bucket, Key: originalObjectKey }),
112
+ );
113
+ if (!getObjectResponse.Body) {
114
+ throw new Error('Failed to retrieve image from storage: Empty body.');
115
+ }
116
+ imageBuffer = await streamToBuffer(getObjectResponse.Body as Readable);
89
117
  }
90
-
91
- let imageBuffer = await streamToBuffer(getObjectResponse.Body as Readable);
92
118
  let sharpInstance = sharp(imageBuffer);
93
119
  let originalMetadata = await sharpInstance.metadata();
94
120
 
@@ -132,15 +158,9 @@ export async function POST(request: NextRequest) {
132
158
  // already .avif — strip it so keys read `..._large.avif`, not `..._large_avif.avif`.
133
159
  const fileSuffix = size.label.replace(/_avif$/, '');
134
160
  const newObjectKey = `${baseName}_${fileSuffix}.${TARGET_FORMAT}`;
135
- const newPublicUrl = `${R2_PUBLIC_URL_BASE}/${newObjectKey}`;
161
+ const newPublicUrl = `${publicUrlBase}/${newObjectKey}`;
136
162
 
137
- const putObjectParams = {
138
- Bucket: R2_BUCKET_NAME,
139
- Key: newObjectKey,
140
- Body: processedImageBuffer,
141
- ContentType: TARGET_MIME_TYPE,
142
- };
143
- await s3Client.send(new PutObjectCommand(putObjectParams));
163
+ await putObject(newObjectKey, processedImageBuffer, TARGET_MIME_TYPE);
144
164
 
145
165
  const newMetadata = await sharp(processedImageBuffer).metadata();
146
166
  processedVariants.push({
@@ -163,14 +183,9 @@ export async function POST(request: NextRequest) {
163
183
  .toBuffer();
164
184
 
165
185
  const originalAvifObjectKey = `${baseName}_original.${TARGET_FORMAT}`;
166
- const originalAvifPublicUrl = `${R2_PUBLIC_URL_BASE}/${originalAvifObjectKey}`;
167
-
168
- await s3Client.send(new PutObjectCommand({
169
- Bucket: R2_BUCKET_NAME,
170
- Key: originalAvifObjectKey,
171
- Body: originalAvifBuffer,
172
- ContentType: TARGET_MIME_TYPE,
173
- }));
186
+ const originalAvifPublicUrl = `${publicUrlBase}/${originalAvifObjectKey}`;
187
+
188
+ await putObject(originalAvifObjectKey, originalAvifBuffer, TARGET_MIME_TYPE);
174
189
  const originalAvifMetadata = await sharp(originalAvifBuffer).metadata();
175
190
  processedVariants.push({
176
191
  objectKey: originalAvifObjectKey,
@@ -188,7 +203,7 @@ export async function POST(request: NextRequest) {
188
203
  // The client already has some of this, but good to have a consistent structure.
189
204
  const originalImageDetails: ProcessedImageVariant = {
190
205
  objectKey: originalObjectKey,
191
- url: `${R2_PUBLIC_URL_BASE}/${originalObjectKey}`,
206
+ url: `${publicUrlBase}/${originalObjectKey}`,
192
207
  width: originalMetadata.width || 0,
193
208
  height: originalMetadata.height || 0,
194
209
  fileType: originalContentType,
@@ -1,8 +1,7 @@
1
1
  // app/api/revalidate/route.ts
2
2
  import { NextRequest, NextResponse } from 'next/server';
3
3
  import { revalidatePath } from 'next/cache';
4
-
5
- const REVALIDATE_SECRET = process.env.REVALIDATE_SECRET_TOKEN;
4
+ import { resolveRevalidateSecret } from '../../../lib/app-secrets';
6
5
 
7
6
  // Define the expected structure of the Supabase webhook payload
8
7
  interface SupabaseWebhookPayload {
@@ -15,8 +14,12 @@ interface SupabaseWebhookPayload {
15
14
 
16
15
  export async function POST(request: NextRequest) {
17
16
  const secret = request.headers.get('x-revalidate-secret');
17
+ // Explicit REVALIDATE_SECRET_TOKEN wins; otherwise derived from the service-role
18
+ // key. Empty only when Supabase is unconfigured — reject in that case rather than
19
+ // allowing an empty-header match.
20
+ const expectedSecret = resolveRevalidateSecret();
18
21
 
19
- if (secret !== REVALIDATE_SECRET) {
22
+ if (!expectedSecret || secret !== expectedSecret) {
20
23
  console.warn("Revalidation attempt with invalid secret token.");
21
24
  return NextResponse.json({ message: 'Invalid secret token' }, { status: 401 });
22
25
  }
@@ -42,8 +45,8 @@ export async function POST(request: NextRequest) {
42
45
 
43
46
  if (table === 'pages') {
44
47
  pathToRevalidate = `/${relevantRecord.slug}`;
45
- } else if (table === 'posts') {
46
- pathToRevalidate = `/article/${relevantRecord.slug}`;
48
+ } else if (table === 'posts') {
49
+ pathToRevalidate = `/article/${relevantRecord.slug}`;
47
50
  } else {
48
51
  console.log(`Revalidation not configured for table: ${table}`);
49
52
  return NextResponse.json({ message: `Revalidation not configured for table: ${table}` }, { status: 200 }); // Acknowledge but don't process
@@ -59,19 +62,19 @@ export async function POST(request: NextRequest) {
59
62
  await revalidatePath(normalizedPath, 'page');
60
63
  console.log(`Successfully revalidated path: ${normalizedPath}`);
61
64
 
62
- // Additionally, if it's an article, you might want to revalidate the main listing page.
63
- if (table === 'posts') {
64
- // Assuming your main articles listing page is at '/articles' or similar.
65
- // This path needs to be known and consistent.
66
- // If your listing is at the root of the language segment (e.g. /en/articles),
67
- // and you are NOT using [lang] in URL, then the path is just '/articles'.
68
- // However, if your LanguageContext means /articles shows different content per lang,
69
- // revalidating just '/articles' will rebuild its default language version.
70
- // Client-side fetches would still get latest for other languages.
71
- // For now, let's revalidate a generic /articles path if it exists.
72
- // await revalidatePath('/articles', 'page'); // Example: revalidate main listing
73
- // console.log("Also attempted to revalidate /articles listing page.");
74
- }
65
+ // Additionally, if it's an article, you might want to revalidate the main listing page.
66
+ if (table === 'posts') {
67
+ // Assuming your main articles listing page is at '/articles' or similar.
68
+ // This path needs to be known and consistent.
69
+ // If your listing is at the root of the language segment (e.g. /en/articles),
70
+ // and you are NOT using [lang] in URL, then the path is just '/articles'.
71
+ // However, if your LanguageContext means /articles shows different content per lang,
72
+ // revalidating just '/articles' will rebuild its default language version.
73
+ // Client-side fetches would still get latest for other languages.
74
+ // For now, let's revalidate a generic /articles path if it exists.
75
+ // await revalidatePath('/articles', 'page'); // Example: revalidate main listing
76
+ // console.log("Also attempted to revalidate /articles listing page.");
77
+ }
75
78
 
76
79
  return NextResponse.json({ revalidated: true, revalidatedPath: normalizedPath, now: Date.now() });
77
80
  } catch (err: unknown) {
@@ -4,8 +4,8 @@ import { createClient } from "@nextblock-cms/db/server"; // Server client for au
4
4
  import { getS3PresignClient } from "@nextblock-cms/utils/server";
5
5
  import { PutObjectCommand } from "@aws-sdk/client-s3";
6
6
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
7
-
8
- const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME;
7
+ import { getStorageBackend, getStorageBucket } from "../../../../lib/storage/provider";
8
+ import { supabaseCreateSignedUpload } from "../../../../lib/storage/supabase-storage";
9
9
 
10
10
  export async function POST(request: NextRequest) {
11
11
  const supabase = createClient();
@@ -26,14 +26,16 @@ export async function POST(request: NextRequest) {
26
26
  return NextResponse.json({ error: "Forbidden: Insufficient permissions" }, { status: 403 });
27
27
  }
28
28
 
29
- if (!R2_BUCKET_NAME) {
30
- console.error("R2_BUCKET_NAME is not set.");
29
+ const backend = getStorageBackend();
30
+ const bucket = getStorageBucket();
31
+ if (!bucket) {
32
+ console.error("No storage bucket is configured.");
31
33
  return NextResponse.json({ error: "Server configuration error for file uploads." }, { status: 500 });
32
34
  }
33
35
 
34
36
  try {
35
- const s3Client = await getS3PresignClient();
36
- if (!s3Client) {
37
+ const s3Client = backend === 's3' ? await getS3PresignClient() : null;
38
+ if (backend === 's3' && !s3Client) {
37
39
  console.error('R2 client is not configured. Check your R2 environment variables.');
38
40
  return NextResponse.json({ error: 'File uploads are not configured on this server.' }, { status: 500 });
39
41
  }
@@ -79,8 +81,18 @@ export async function POST(request: NextRequest) {
79
81
 
80
82
  const uniqueKey = `${folder}${sanitizedBaseFilename}_${timestamp}${fileExtension ? '.' + fileExtension : ''}`;
81
83
 
84
+ if (backend === 'supabase') {
85
+ // Native Supabase Storage: hand the browser a signed upload URL to PUT to directly.
86
+ const { signedUrl } = await supabaseCreateSignedUpload(uniqueKey);
87
+ return NextResponse.json({
88
+ presignedUrl: signedUrl,
89
+ objectKey: uniqueKey,
90
+ method: "PUT",
91
+ });
92
+ }
93
+
82
94
  const command = new PutObjectCommand({
83
- Bucket: R2_BUCKET_NAME,
95
+ Bucket: bucket,
84
96
  Key: uniqueKey,
85
97
  ContentType: contentType,
86
98
  // ACL: 'public-read', // R2 objects are private by default unless bucket is public or presigned URL for GET is used
@@ -91,7 +103,7 @@ export async function POST(request: NextRequest) {
91
103
  });
92
104
 
93
105
  const expiresIn = 300; // 5 minutes
94
- const presignedUrl = await getSignedUrl(s3Client, command, { expiresIn });
106
+ const presignedUrl = await getSignedUrl(s3Client!, command, { expiresIn });
95
107
 
96
108
  return NextResponse.json({
97
109
  presignedUrl,
@@ -1,10 +1,10 @@
1
1
  // apps/nextblock/app/api/upload/proxy/route.ts
2
2
  import { NextRequest, NextResponse } from 'next/server';
3
3
  import { createClient } from '@nextblock-cms/db/server';
4
- import { getS3Client } from '@nextblock-cms/utils/server';
4
+ import { getS3Client } from '@nextblock-cms/utils/server';
5
5
  import { PutObjectCommand } from '@aws-sdk/client-s3';
6
-
7
- const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME;
6
+ import { getStorageBackend, getStorageBucket } from '../../../../lib/storage/provider';
7
+ import { supabaseUploadObject } from '../../../../lib/storage/supabase-storage';
8
8
 
9
9
  export async function POST(request: NextRequest) {
10
10
  const supabase = createClient();
@@ -14,19 +14,22 @@ export async function POST(request: NextRequest) {
14
14
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
15
15
  }
16
16
 
17
- if (!R2_BUCKET_NAME) {
18
- console.error('R2_BUCKET_NAME is not set.');
19
- return NextResponse.json({ error: 'Server configuration error for file uploads.' }, { status: 500 });
20
- }
21
-
22
- try {
23
- const s3Client = await getS3Client();
24
- if (!s3Client) {
25
- console.error('R2 client is not configured. Check your R2 environment variables.');
26
- return NextResponse.json({ error: 'File uploads are not configured on this server.' }, { status: 500 });
27
- }
28
-
29
- const formData = await request.formData();
17
+ const backend = getStorageBackend();
18
+ const bucket = getStorageBucket();
19
+ if (!bucket) {
20
+ console.error('No storage bucket is configured.');
21
+ return NextResponse.json({ error: 'Server configuration error for file uploads.' }, { status: 500 });
22
+ }
23
+
24
+ try {
25
+ // On the S3 path we need a configured client; the native Supabase path needs none.
26
+ const s3Client = backend === 's3' ? await getS3Client() : null;
27
+ if (backend === 's3' && !s3Client) {
28
+ console.error('R2 client is not configured. Check your R2 environment variables.');
29
+ return NextResponse.json({ error: 'File uploads are not configured on this server.' }, { status: 500 });
30
+ }
31
+
32
+ const formData = await request.formData();
30
33
  const file = formData.get('file') as File | null;
31
34
  const rawFolder = (formData.get('folder') as string | null) ?? undefined;
32
35
 
@@ -58,18 +61,21 @@ export async function POST(request: NextRequest) {
58
61
  const bytes = await file.arrayBuffer();
59
62
  const buffer = Buffer.from(bytes);
60
63
 
61
- const command = new PutObjectCommand({
62
- Bucket: R2_BUCKET_NAME,
63
- Key: uniqueKey,
64
- Body: buffer,
65
- ContentType: file.type,
66
- ContentLength: file.size,
67
- Metadata: {
68
- 'uploader-user-id': user.id,
69
- },
70
- });
71
-
72
- await s3Client.send(command);
64
+ if (backend === 'supabase') {
65
+ await supabaseUploadObject(uniqueKey, buffer, file.type);
66
+ } else {
67
+ const command = new PutObjectCommand({
68
+ Bucket: bucket,
69
+ Key: uniqueKey,
70
+ Body: buffer,
71
+ ContentType: file.type,
72
+ ContentLength: file.size,
73
+ Metadata: {
74
+ 'uploader-user-id': user.id,
75
+ },
76
+ });
77
+ await s3Client!.send(command);
78
+ }
73
79
 
74
80
  return NextResponse.json({
75
81
  objectKey: uniqueKey,
@@ -1,8 +1,5 @@
1
1
  // app/article/[slug]/page.tsx
2
2
  import React from 'react';
3
- // Remove or alias the problematic import if only used by other functions:
4
- // import { createClient } from "@nextblock-cms/db/server";
5
- import { createClient as createSupabaseJsClient } from '@supabase/supabase-js'; // Import base client
6
3
  import { notFound } from "next/navigation";
7
4
  import type { Metadata } from 'next';
8
5
  import PostClientContent from "./PostClientContent";
@@ -58,16 +55,10 @@ const resolveLanguageCode = (languagesField: PostTranslation["languages"]): stri
58
55
  };
59
56
 
60
57
  export async function generateStaticParams(): Promise<ResolvedPostParams[]> {
61
- // Use a new Supabase client instance that doesn't rely on cookies
62
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
63
- const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
64
-
65
- if (!supabaseUrl || !supabaseAnonKey) {
66
- console.warn('Missing Supabase environment variables in generateStaticParams (Posts)');
67
- return [];
68
- }
69
-
70
- const supabase = createSupabaseJsClient(supabaseUrl, supabaseAnonKey);
58
+ // Cookie-free SSG client. getSsgSupabaseClient() resolves the Supabase URL/anon key
59
+ // under every alias the Vercel integration may inject (incl. the new publishable key)
60
+ // and degrades to a dummy client when unconfigured (the query below then returns []).
61
+ const supabase = getSsgSupabaseClient();
71
62
 
72
63
  const { data: posts, error } = await supabase
73
64
  .from("posts")
@@ -8,10 +8,11 @@ import {
8
8
  stripe,
9
9
  syncStripeOrderFromSession,
10
10
  } from '@nextblock-cms/ecommerce/server';
11
+ import { resolveSupabaseServiceKey, resolveSupabaseUrl } from '../../../lib/setup/env-status';
11
12
 
12
13
  function getServiceRoleSupabaseClient() {
13
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
14
- const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
14
+ const supabaseUrl = resolveSupabaseUrl();
15
+ const serviceRoleKey = resolveSupabaseServiceKey();
15
16
 
16
17
  if (!supabaseUrl || !serviceRoleKey) {
17
18
  throw new Error('Missing Supabase Service Role environment variables');