create-nextblock 0.9.95 → 0.9.99

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
@@ -7,6 +7,8 @@ import type { Database } from "@nextblock-cms/db";
7
7
  import { encodedRedirect } from "@nextblock-cms/utils/server";
8
8
  import { DeleteObjectCommand, DeleteObjectsCommand, CopyObjectCommand, ListObjectsV2Command, HeadObjectCommand } from "@aws-sdk/client-s3";
9
9
  import { getS3Client } from "@nextblock-cms/utils/server";
10
+ import { getStorageBackend, getStorageBucket } from "../../../lib/storage/provider";
11
+ import { supabaseRemoveObjects } from "../../../lib/storage/supabase-storage";
10
12
 
11
13
  type Media = Database['public']['Tables']['media']['Row'];
12
14
 
@@ -85,13 +87,14 @@ export async function deleteMediaItem(mediaId: string, objectKey: string) {
85
87
  return encodedRedirect("error", "/cms/media", "Forbidden: Insufficient permissions.");
86
88
  }
87
89
 
88
- const s3Client = await getS3Client();
89
- const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME;
90
+ const backend = getStorageBackend();
91
+ const bucket = getStorageBucket();
92
+ const s3Client = backend === 's3' ? await getS3Client() : null;
90
93
 
91
- if (!R2_BUCKET_NAME) {
92
- return encodedRedirect("error", "/cms/media", "R2 Bucket not configured for deletion.");
94
+ if (!bucket) {
95
+ return encodedRedirect("error", "/cms/media", "Storage bucket not configured for deletion.");
93
96
  }
94
- if (!s3Client) {
97
+ if (backend === 's3' && !s3Client) {
95
98
  return encodedRedirect("error", "/cms/media", "R2 client is not configured for deletion.");
96
99
  }
97
100
 
@@ -110,15 +113,18 @@ export async function deleteMediaItem(mediaId: string, objectKey: string) {
110
113
  }
111
114
 
112
115
  try {
113
- const deleteCommand = new DeleteObjectsCommand({
114
- Bucket: R2_BUCKET_NAME,
115
- Delete: {
116
- Objects: keysToDelete.map(key => ({ Key: key })),
117
- },
118
- });
119
- await s3Client.send(deleteCommand);
116
+ if (backend === 'supabase') {
117
+ await supabaseRemoveObjects(keysToDelete);
118
+ } else {
119
+ await s3Client!.send(new DeleteObjectsCommand({
120
+ Bucket: bucket,
121
+ Delete: {
122
+ Objects: keysToDelete.map(key => ({ Key: key })),
123
+ },
124
+ }));
125
+ }
120
126
  } catch (r2Error: unknown) {
121
- console.error("Error deleting from R2:", r2Error);
127
+ console.error("Error deleting from storage:", r2Error);
122
128
  // Decide if you want to proceed with DB deletion if R2 deletion fails
123
129
  // It's often better to proceed and log, or handle more gracefully.
124
130
  // For now, we'll let it proceed to DB deletion but the error is logged.
@@ -153,13 +159,14 @@ export async function deleteMultipleMediaItems(items: Array<{ id: string; object
153
159
  return { error: "No items selected for deletion." };
154
160
  }
155
161
 
156
- const s3Client = await getS3Client();
157
- const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME;
162
+ const backend = getStorageBackend();
163
+ const bucket = getStorageBucket();
164
+ const s3Client = backend === 's3' ? await getS3Client() : null;
158
165
 
159
- if (!R2_BUCKET_NAME) {
160
- return { error: "R2 Bucket not configured for deletion." };
166
+ if (!bucket) {
167
+ return { error: "Storage bucket not configured for deletion." };
161
168
  }
162
- if (!s3Client) {
169
+ if (backend === 's3' && !s3Client) {
163
170
  return { error: "R2 client is not configured for deletion." };
164
171
  }
165
172
 
@@ -191,23 +198,26 @@ export async function deleteMultipleMediaItems(items: Array<{ id: string; object
191
198
  let r2DeletionError = null;
192
199
  let dbDeletionError = null;
193
200
 
194
- // Batch delete from R2
201
+ // Batch delete from object storage
195
202
  try {
196
203
  if (r2ObjectsToDelete.length > 0) {
197
- const deleteCommand = new DeleteObjectsCommand({
198
- Bucket: R2_BUCKET_NAME,
199
- Delete: { Objects: r2ObjectsToDelete },
200
- });
201
- const output = await s3Client.send(deleteCommand);
202
- if (output.Errors && output.Errors.length > 0) {
203
- console.error("Errors deleting some objects from R2:", output.Errors);
204
- // Collect specific errors if needed, for now a general message
205
- r2DeletionError = `Some objects failed to delete from R2: ${output.Errors.map(e => e.Key).join(', ')}`;
204
+ if (backend === 'supabase') {
205
+ await supabaseRemoveObjects(allKeysToDelete);
206
+ } else {
207
+ const output = await s3Client!.send(new DeleteObjectsCommand({
208
+ Bucket: bucket,
209
+ Delete: { Objects: r2ObjectsToDelete },
210
+ }));
211
+ if (output.Errors && output.Errors.length > 0) {
212
+ console.error("Errors deleting some objects from storage:", output.Errors);
213
+ // Collect specific errors if needed, for now a general message
214
+ r2DeletionError = `Some objects failed to delete from storage: ${output.Errors.map(e => e.Key).join(', ')}`;
215
+ }
206
216
  }
207
217
  }
208
218
  } catch (error: unknown) {
209
- console.error("Error batch deleting from R2:", error);
210
- r2DeletionError = `Failed to delete objects from R2: ${error instanceof Error ? error.message : String(error)}`;
219
+ console.error("Error batch deleting from storage:", error);
220
+ r2DeletionError = `Failed to delete objects from storage: ${error instanceof Error ? error.message : String(error)}`;
211
221
  }
212
222
 
213
223
  // Batch delete from Supabase
@@ -264,8 +274,14 @@ export async function moveMultipleMediaItems(
264
274
  };
265
275
  const folder = sanitizeFolder(destinationFolder);
266
276
 
277
+ // Folder reorganisation uses S3 copy/list semantics that the native Supabase backend
278
+ // doesn't replicate yet — surface a clear message instead of a generic config error.
279
+ if (getStorageBackend() === 'supabase') {
280
+ return { error: "Moving media between folders isn't supported on Supabase Storage yet. Delete and re-upload to relocate a file." };
281
+ }
282
+
267
283
  const s3Client = await getS3Client();
268
- const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME;
284
+ const R2_BUCKET_NAME = getStorageBucket();
269
285
  const R2_PUBLIC_URL_BASE = process.env.NEXT_PUBLIC_R2_BASE_URL || '';
270
286
 
271
287
  if (!R2_BUCKET_NAME) {
@@ -1,12 +1,12 @@
1
- 'use server';
2
-
3
- import { createClient } from '@nextblock-cms/db/server';
4
- import {
5
- applyOrderInventoryDeduction,
6
- assignInvoiceMetadata,
7
- } from '@nextblock-cms/ecommerce/server';
8
- import { createClient as createSupabaseClient } from '@supabase/supabase-js';
9
- import { OrderWithDetails } from './types';
1
+ 'use server';
2
+
3
+ import { createClient } from '@nextblock-cms/db/server';
4
+ import {
5
+ applyOrderInventoryDeduction,
6
+ assignInvoiceMetadata,
7
+ } from '@nextblock-cms/ecommerce/server';
8
+ import { createClient as createSupabaseClient } from '@supabase/supabase-js';
9
+ import { OrderWithDetails } from './types';
10
10
 
11
11
  const ITEMS_PER_PAGE = 20;
12
12
 
@@ -150,7 +150,7 @@ export async function getOrderDetails(orderId: string): Promise<OrderWithDetails
150
150
  } as OrderWithDetails;
151
151
  }
152
152
 
153
- export async function markOrderAsPaid(orderId: string): Promise<{ success: boolean; error?: string }> {
153
+ export async function markOrderAsPaid(orderId: string): Promise<{ success: boolean; error?: string }> {
154
154
  const supabase = createClient();
155
155
 
156
156
  // 1. Verify Authentication (using user session)
@@ -162,8 +162,8 @@ export async function markOrderAsPaid(orderId: string): Promise<{ success: boole
162
162
 
163
163
  // 2. Perform Update using Service Role (Bypass RLS)
164
164
 
165
- const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
166
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
165
+ const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SECRET_KEY;
166
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL;
167
167
 
168
168
  if (!serviceRoleKey || !supabaseUrl) {
169
169
  return { success: false, error: 'Server configuration error' };
@@ -176,26 +176,26 @@ export async function markOrderAsPaid(orderId: string): Promise<{ success: boole
176
176
  }
177
177
  });
178
178
 
179
- const { data: updatedData, error } = await adminSupabase
180
- .from('orders')
181
- .update({ status: 'paid' })
182
- .eq('id', orderId)
183
- .select();
179
+ const { data: updatedData, error } = await adminSupabase
180
+ .from('orders')
181
+ .update({ status: 'paid' })
182
+ .eq('id', orderId)
183
+ .select();
184
184
 
185
185
  if (error) {
186
186
  console.error('Mark Paid DB Error:', error);
187
187
  return { success: false, error: error.message };
188
188
  }
189
189
 
190
- if (!updatedData || updatedData.length === 0) {
191
- return { success: false, error: 'Order not found or update failed.' };
192
- }
193
-
194
- await assignInvoiceMetadata({
195
- orderId,
196
- client: adminSupabase as any,
197
- });
198
- await applyOrderInventoryDeduction(adminSupabase as any, orderId);
199
-
200
- return { success: true };
201
- }
190
+ if (!updatedData || updatedData.length === 0) {
191
+ return { success: false, error: 'Order not found or update failed.' };
192
+ }
193
+
194
+ await assignInvoiceMetadata({
195
+ orderId,
196
+ client: adminSupabase as any,
197
+ });
198
+ await applyOrderInventoryDeduction(adminSupabase as any, orderId);
199
+
200
+ return { success: true };
201
+ }
@@ -1,23 +1,23 @@
1
1
  // app/cms/users/[id]/edit/page.tsx
2
- import UserForm from "../../components/UserForm";
3
- import { updateUserProfile } from "../../actions";
4
- import type { Database } from "@nextblock-cms/db";
5
- import { notFound } from "next/navigation";
6
- import { getDefaultUserAddresses } from "@nextblock-cms/ecommerce/server";
7
-
8
- type Profile = Database['public']['Tables']['profiles']['Row'];
9
- type AuthUser = {
10
- id: string;
11
- email: string | undefined;
2
+ import UserForm from "../../components/UserForm";
3
+ import { updateUserProfile } from "../../actions";
4
+ import type { Database } from "@nextblock-cms/db";
5
+ import { notFound } from "next/navigation";
6
+ import { getDefaultUserAddresses } from "@nextblock-cms/ecommerce/server";
7
+
8
+ type Profile = Database['public']['Tables']['profiles']['Row'];
9
+ type AuthUser = {
10
+ id: string;
11
+ email: string | undefined;
12
12
  created_at: string | undefined;
13
13
  last_sign_in_at: string | undefined;
14
14
  };
15
15
 
16
- async function getUserAndProfileData(userId: string): Promise<{
17
- authUser: AuthUser;
18
- profile: Profile | null;
19
- addresses: Awaited<ReturnType<typeof getDefaultUserAddresses>>;
20
- } | null> {
16
+ async function getUserAndProfileData(userId: string): Promise<{
17
+ authUser: AuthUser;
18
+ profile: Profile | null;
19
+ addresses: Awaited<ReturnType<typeof getDefaultUserAddresses>>;
20
+ } | null> {
21
21
 
22
22
  // Fetch user from auth.users
23
23
  // For admin operations, you might need a service_role client to fetch any user.
@@ -27,9 +27,9 @@ async function getUserAndProfileData(userId: string): Promise<{
27
27
 
28
28
  // Let's use the admin API for fetching the auth user.
29
29
  const { createClient: createServiceRoleClient } = await import('@supabase/supabase-js');
30
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
31
- const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
32
-
30
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL;
31
+ const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SECRET_KEY;
32
+
33
33
  if (!supabaseUrl || !serviceRoleKey) {
34
34
  throw new Error('Missing required environment variables');
35
35
  }
@@ -62,10 +62,10 @@ async function getUserAndProfileData(userId: string): Promise<{
62
62
  last_sign_in_at: authUserData.last_sign_in_at,
63
63
  };
64
64
 
65
- const addresses = await getDefaultUserAddresses(userId, serviceSupabase as any);
66
-
67
- return { authUser: simplifiedAuthUser, profile: profileData as Profile | null, addresses };
68
- }
65
+ const addresses = await getDefaultUserAddresses(userId, serviceSupabase as any);
66
+
67
+ return { authUser: simplifiedAuthUser, profile: profileData as Profile | null, addresses };
68
+ }
69
69
 
70
70
  export default async function EditUserPage(props: { params: Promise<{ id: string }> }) {
71
71
  const params = await props.params;
@@ -85,12 +85,12 @@ export default async function EditUserPage(props: { params: Promise<{ id: string
85
85
  return (
86
86
  <div className="max-w-4xl mx-auto">
87
87
  <h1 className="text-2xl font-bold mb-6">Edit User: {userData.authUser.email}</h1>
88
- <UserForm
89
- userToEditAuth={userData.authUser}
90
- userToEditProfile={userData.profile}
91
- userToEditAddresses={userData.addresses}
92
- formAction={updateUserActionWithId}
93
- />
88
+ <UserForm
89
+ userToEditAuth={userData.authUser}
90
+ userToEditProfile={userData.profile}
91
+ userToEditAddresses={userData.addresses}
92
+ formAction={updateUserActionWithId}
93
+ />
94
94
  </div>
95
95
  );
96
96
  }
@@ -1,4 +1,4 @@
1
- // app/cms/users/actions.ts
1
+ // app/cms/users/actions.ts
2
2
  "use server";
3
3
 
4
4
  import { createClient } from "@nextblock-cms/db/server";
@@ -8,30 +8,31 @@ import type { Database } from "@nextblock-cms/db";
8
8
  import { normalizeCustomerAddress } from "@nextblock-cms/ecommerce";
9
9
  import { upsertDefaultUserAddresses } from "@nextblock-cms/ecommerce/server";
10
10
  import { createClient as createSupabaseClient } from '@supabase/supabase-js';
11
-
12
- type UserRole = Database['public']['Enums']['user_role'];
13
-
14
- // Helper to check admin role using the server client
15
- async function verifyAdmin(supabase: ReturnType<typeof createClient>): Promise<{ isAdmin: boolean; error?: string; userId?: string }> {
16
- const { data: { user }, error: authError } = await supabase.auth.getUser();
17
- if (authError || !user) {
18
- return { isAdmin: false, error: "Authentication required." };
19
- }
20
- const { data: profile, error: profileError } = await supabase
21
- .from("profiles")
22
- .select("role")
23
- .eq("id", user.id)
24
- .single();
25
-
26
- if (profileError || !profile) {
27
- return { isAdmin: false, error: "Profile not found or error fetching profile." };
28
- }
29
- if (profile.role !== "ADMIN") {
30
- return { isAdmin: false, error: "Admin privileges required." };
31
- }
32
- return { isAdmin: true, userId: user.id };
33
- }
34
-
11
+ import { resolveSupabaseServiceKey, resolveSupabaseUrl } from '../../../lib/setup/env-status';
12
+
13
+ type UserRole = Database['public']['Enums']['user_role'];
14
+
15
+ // Helper to check admin role using the server client
16
+ async function verifyAdmin(supabase: ReturnType<typeof createClient>): Promise<{ isAdmin: boolean; error?: string; userId?: string }> {
17
+ const { data: { user }, error: authError } = await supabase.auth.getUser();
18
+ if (authError || !user) {
19
+ return { isAdmin: false, error: "Authentication required." };
20
+ }
21
+ const { data: profile, error: profileError } = await supabase
22
+ .from("profiles")
23
+ .select("role")
24
+ .eq("id", user.id)
25
+ .single();
26
+
27
+ if (profileError || !profile) {
28
+ return { isAdmin: false, error: "Profile not found or error fetching profile." };
29
+ }
30
+ if (profile.role !== "ADMIN") {
31
+ return { isAdmin: false, error: "Admin privileges required." };
32
+ }
33
+ return { isAdmin: true, userId: user.id };
34
+ }
35
+
35
36
  type UpdateUserProfilePayload = {
36
37
  role: UserRole;
37
38
  full_name?: string | null;
@@ -42,8 +43,8 @@ type UpdateUserProfilePayload = {
42
43
  };
43
44
 
44
45
  function createServiceRoleClient() {
45
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
46
- const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
46
+ const supabaseUrl = resolveSupabaseUrl();
47
+ const serviceRoleKey = resolveSupabaseServiceKey();
47
48
 
48
49
  if (!supabaseUrl || !serviceRoleKey) {
49
50
  throw new Error('Missing required environment variables');
@@ -60,10 +61,10 @@ function createServiceRoleClient() {
60
61
  export async function updateUserProfile(userIdToUpdate: string, formData: FormData) {
61
62
  const supabase = createClient();
62
63
  const adminCheck = await verifyAdmin(supabase);
63
- if (!adminCheck.isAdmin) {
64
- return { error: adminCheck.error || "Unauthorized" };
65
- }
66
-
64
+ if (!adminCheck.isAdmin) {
65
+ return { error: adminCheck.error || "Unauthorized" };
66
+ }
67
+
67
68
  const parseAddressField = (fieldName: string) => {
68
69
  const rawValue = formData.get(fieldName) as string | null;
69
70
  if (!rawValue) {
@@ -99,27 +100,27 @@ export async function updateUserProfile(userIdToUpdate: string, formData: FormDa
99
100
  github_username: formData.get("github_username") as string || null,
100
101
  phone: formData.get("phone") as string || null,
101
102
  };
102
-
103
- if (!rawFormData.role) {
104
- return { error: "Role is a required field." };
105
- }
106
- if (!['ADMIN', 'WRITER', 'USER'].includes(rawFormData.role)) {
107
- return { error: "Invalid role specified." };
108
- }
109
-
110
- // Prevent an admin from accidentally removing their own admin role if they are the only admin
111
- // This is a basic check; a more robust system might count admins.
112
- if (userIdToUpdate === adminCheck.userId && rawFormData.role !== 'ADMIN') {
113
- const { count } = await supabase
114
- .from('profiles')
115
- .select('*', { count: 'exact', head: true })
116
- .eq('role', 'ADMIN');
117
- if (count === 1) {
118
- return { error: "Cannot remove the last admin's role." };
119
- }
120
- }
121
-
122
-
103
+
104
+ if (!rawFormData.role) {
105
+ return { error: "Role is a required field." };
106
+ }
107
+ if (!['ADMIN', 'WRITER', 'USER'].includes(rawFormData.role)) {
108
+ return { error: "Invalid role specified." };
109
+ }
110
+
111
+ // Prevent an admin from accidentally removing their own admin role if they are the only admin
112
+ // This is a basic check; a more robust system might count admins.
113
+ if (userIdToUpdate === adminCheck.userId && rawFormData.role !== 'ADMIN') {
114
+ const { count } = await supabase
115
+ .from('profiles')
116
+ .select('*', { count: 'exact', head: true })
117
+ .eq('role', 'ADMIN');
118
+ if (count === 1) {
119
+ return { error: "Cannot remove the last admin's role." };
120
+ }
121
+ }
122
+
123
+
123
124
  const profileData: UpdateUserProfilePayload = {
124
125
  role: rawFormData.role,
125
126
  full_name: rawFormData.full_name,
@@ -135,8 +136,8 @@ export async function updateUserProfile(userIdToUpdate: string, formData: FormDa
135
136
  .from("profiles")
136
137
  .update(profileData)
137
138
  .eq("id", userIdToUpdate);
138
-
139
- if (error) {
139
+
140
+ if (error) {
140
141
  console.error("Error updating user profile:", error);
141
142
  return { error: `Failed to update profile: ${error.message}` };
142
143
  }
@@ -154,68 +155,68 @@ export async function updateUserProfile(userIdToUpdate: string, formData: FormDa
154
155
  revalidatePath("/checkout");
155
156
  redirect(`/cms/users/${userIdToUpdate}/edit?success=User profile updated successfully`);
156
157
  }
157
-
158
- export async function deleteUserAndProfile(userIdToDelete: string) {
159
-
160
- // For deleting a user, we need to use the Supabase Admin API,
161
- // which requires a client initialized with the SERVICE_ROLE_KEY.
162
- // This ensures the operation has the necessary privileges.
163
- // IMPORTANT: Ensure SUPABASE_SERVICE_ROLE_KEY is set in your .env.local and Vercel env vars.
164
- const supabaseAdmin = createClient(
165
- // Re-create client with service role. This is a common pattern.
166
- // Ensure your createClient function can be called without args to use env vars,
167
- // or pass them explicitly if needed. The one from the template should work.
168
- // If your createClient is specific to user context (cookies), you might need a separate
169
- // admin client factory. For now, assuming `createClient()` can make a service client
170
- // if called in a server action without user cookie context, or if it defaults to service key.
171
- // A safer way:
172
- // const supabaseAdmin = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!);
173
- // However, the template's createClient for server components/actions should handle this by not having cookie access.
174
- // Let's assume for now createClient() is sufficient if it can use service role.
175
- // A more explicit way for admin actions:
176
- // import { createClient as createAdminClient } from '@supabase/supabase-js';
177
- // const supabaseAdmin = createAdminClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!);
178
- );
179
-
180
-
181
- const adminCheck = await verifyAdmin(supabaseAdmin); // Verify current user is admin
182
- if (!adminCheck.isAdmin) {
183
- return { error: adminCheck.error || "Unauthorized" };
184
- }
185
-
186
- if (userIdToDelete === adminCheck.userId) {
187
- return { error: "Admins cannot delete their own account through this panel." };
188
- }
189
-
190
- // Use the Supabase Auth Admin API to delete the user
191
- // This requires the `SERVICE_ROLE_KEY` to be configured for the Supabase client.
192
- // The standard `createClient` from `utils/supabase/server` might not use the service role by default.
193
- // You might need a dedicated admin client instance.
194
- // For this example, we'll assume `supabase.auth.admin.deleteUser` is available and configured.
195
- // If not, this part needs adjustment to use a service_role client.
196
-
197
- // The `createClient()` from `@supabase/ssr` for server context doesn't directly expose `auth.admin`.
198
- // We need to create a standard Supabase client with the service role key.
199
- const { createClient: createServiceRoleClient } = await import('@supabase/supabase-js');
200
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
201
- const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
202
-
203
- if (!supabaseUrl || !serviceRoleKey) {
204
- throw new Error('Missing required environment variables');
205
- }
206
-
207
- const serviceSupabase = createServiceRoleClient(supabaseUrl, serviceRoleKey);
208
-
209
-
210
- const { error: deletionError } = await serviceSupabase.auth.admin.deleteUser(userIdToDelete);
211
-
212
- if (deletionError) {
213
- console.error("Error deleting user:", deletionError);
214
- // If the profile was deleted by cascade but auth user deletion failed, this is an inconsistent state.
215
- return { error: `Failed to delete user: ${deletionError.message}` };
216
- }
217
-
218
- // The `profiles` table has ON DELETE CASCADE for the user ID, so it should be deleted automatically.
219
- revalidatePath("/cms/users");
220
- redirect("/cms/users?success=User deleted successfully");
158
+
159
+ export async function deleteUserAndProfile(userIdToDelete: string) {
160
+
161
+ // For deleting a user, we need to use the Supabase Admin API,
162
+ // which requires a client initialized with the SERVICE_ROLE_KEY.
163
+ // This ensures the operation has the necessary privileges.
164
+ // IMPORTANT: Ensure SUPABASE_SERVICE_ROLE_KEY is set in your .env.local and Vercel env vars.
165
+ const supabaseAdmin = createClient(
166
+ // Re-create client with service role. This is a common pattern.
167
+ // Ensure your createClient function can be called without args to use env vars,
168
+ // or pass them explicitly if needed. The one from the template should work.
169
+ // If your createClient is specific to user context (cookies), you might need a separate
170
+ // admin client factory. For now, assuming `createClient()` can make a service client
171
+ // if called in a server action without user cookie context, or if it defaults to service key.
172
+ // A safer way:
173
+ // const supabaseAdmin = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!);
174
+ // However, the template's createClient for server components/actions should handle this by not having cookie access.
175
+ // Let's assume for now createClient() is sufficient if it can use service role.
176
+ // A more explicit way for admin actions:
177
+ // import { createClient as createAdminClient } from '@supabase/supabase-js';
178
+ // const supabaseAdmin = createAdminClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!);
179
+ );
180
+
181
+
182
+ const adminCheck = await verifyAdmin(supabaseAdmin); // Verify current user is admin
183
+ if (!adminCheck.isAdmin) {
184
+ return { error: adminCheck.error || "Unauthorized" };
185
+ }
186
+
187
+ if (userIdToDelete === adminCheck.userId) {
188
+ return { error: "Admins cannot delete their own account through this panel." };
189
+ }
190
+
191
+ // Use the Supabase Auth Admin API to delete the user
192
+ // This requires the `SERVICE_ROLE_KEY` to be configured for the Supabase client.
193
+ // The standard `createClient` from `utils/supabase/server` might not use the service role by default.
194
+ // You might need a dedicated admin client instance.
195
+ // For this example, we'll assume `supabase.auth.admin.deleteUser` is available and configured.
196
+ // If not, this part needs adjustment to use a service_role client.
197
+
198
+ // The `createClient()` from `@supabase/ssr` for server context doesn't directly expose `auth.admin`.
199
+ // We need to create a standard Supabase client with the service role key.
200
+ const { createClient: createServiceRoleClient } = await import('@supabase/supabase-js');
201
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL;
202
+ const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SECRET_KEY;
203
+
204
+ if (!supabaseUrl || !serviceRoleKey) {
205
+ throw new Error('Missing required environment variables');
206
+ }
207
+
208
+ const serviceSupabase = createServiceRoleClient(supabaseUrl, serviceRoleKey);
209
+
210
+
211
+ const { error: deletionError } = await serviceSupabase.auth.admin.deleteUser(userIdToDelete);
212
+
213
+ if (deletionError) {
214
+ console.error("Error deleting user:", deletionError);
215
+ // If the profile was deleted by cascade but auth user deletion failed, this is an inconsistent state.
216
+ return { error: `Failed to delete user: ${deletionError.message}` };
217
+ }
218
+
219
+ // The `profiles` table has ON DELETE CASCADE for the user ID, so it should be deleted automatically.
220
+ revalidatePath("/cms/users");
221
+ redirect("/cms/users?success=User deleted successfully");
221
222
  }
@@ -37,9 +37,9 @@ import { DeleteUserButtonClient } from "./components/DeleteUserButton";
37
37
  async function getUsersData(currentAdminId: string): Promise<UserWithProfile[]> {
38
38
  // This needs to use a service role client to list all users from auth.users
39
39
  const { createClient: createServiceRoleClient } = await import('@supabase/supabase-js');
40
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
41
- const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
42
-
40
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL;
41
+ const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SECRET_KEY;
42
+
43
43
  if (!supabaseUrl || !serviceRoleKey) {
44
44
  throw new Error('Missing required environment variables');
45
45
  }
@@ -26,7 +26,12 @@ import { verifyPackageOnline } from '@nextblock-cms/db/server';
26
26
  import { unstable_cache } from 'next/cache';
27
27
  import { createStaticSupabaseClient, getSiteSettings } from './lib/site-settings';
28
28
  import { DEFAULT_OG_IMAGE } from './lib/seo';
29
- import { isSupabaseConfigured } from '../lib/setup/env-status';
29
+ import {
30
+ isSupabaseConfigured,
31
+ resolveSupabaseAnonKey,
32
+ resolveSupabaseUrl,
33
+ } from '../lib/setup/env-status';
34
+ import { resolveMediaBaseUrl } from '../lib/storage/provider';
30
35
 
31
36
  const defaultUrl = process.env.NEXT_PUBLIC_URL || 'http://localhost:3000';
32
37
 
@@ -320,9 +325,7 @@ async function loadLayoutData() {
320
325
  const globalCss = typeof globalCssResult === 'string' ? globalCssResult : '';
321
326
  const translations = Array.isArray(translationsResult) ? translationsResult : [];
322
327
 
323
- const hasSupabaseEnv = Boolean(
324
- process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
325
- );
328
+ const hasSupabaseEnv = isSupabaseConfigured();
326
329
 
327
330
  const [profile, headerNavItems, footerNavItems, logo] = await Promise.all([
328
331
  user ? getProfileWithRoleServerSide(user.id) : Promise.resolve(null),
@@ -453,8 +456,8 @@ export default async function RootLayout({
453
456
  // build-time-inlined NEXT_PUBLIC_* and these just match; it only matters in local dev,
454
457
  // where the wizard writes those vars at runtime and the loaded bundle would otherwise
455
458
  // hold stale empties until a dev-server restart. Read from server process.env (fresh).
456
- const publicSupabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || '';
457
- const publicSupabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || '';
459
+ const publicSupabaseUrl = resolveSupabaseUrl() || '';
460
+ const publicSupabaseAnonKey = resolveSupabaseAnonKey() || '';
458
461
 
459
462
  return (
460
463
  <html lang={serverDeterminedLocale} suppressHydrationWarning>
@@ -468,7 +471,7 @@ export default async function RootLayout({
468
471
  <PublicEnvBootstrap
469
472
  url={publicSupabaseUrl}
470
473
  anonKey={publicSupabaseAnonKey}
471
- r2Base={process.env.NEXT_PUBLIC_R2_BASE_URL || ''}
474
+ r2Base={resolveMediaBaseUrl()}
472
475
  />
473
476
  {/* In development this loads after hydration to avoid browser-hidden nonce comparisons. */}
474
477
  <Script