create-nextblock 0.13.6 → 0.13.8
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/package.json +1 -1
- package/templates/nextblock-template/app/(auth-pages)/two-factor/actions.ts +21 -1
- package/templates/nextblock-template/app/(auth-pages)/two-factor/components/TwoFactorForm.tsx +34 -10
- package/templates/nextblock-template/app/actions/email.ts +78 -7
- package/templates/nextblock-template/app/actions/feedback.ts +57 -14
- package/templates/nextblock-template/app/actions/interactions.test.ts +3 -0
- package/templates/nextblock-template/app/actions.ts +17 -4
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +1 -1
- package/templates/nextblock-template/app/cms/pages/page.tsx +3 -6
- package/templates/nextblock-template/app/cms/posts/page.tsx +5 -7
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +12 -10
- package/templates/nextblock-template/app/cms/settings/bot-protection/actions.ts +9 -6
- package/templates/nextblock-template/app/cms/settings/bot-protection/components/BotProtectionForm.tsx +1 -5
- package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +9 -6
- package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +1 -5
- package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +18 -8
- package/templates/nextblock-template/app/cms/settings/email/actions.ts +59 -29
- package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +5 -1
- package/templates/nextblock-template/app/cms/settings/global-css/actions.ts +6 -5
- package/templates/nextblock-template/app/cms/settings/global-css/components/GlobalCssForm.tsx +2 -1
- package/templates/nextblock-template/app/cms/settings/google-analytics/actions.ts +18 -7
- package/templates/nextblock-template/app/cms/settings/google-analytics/components/GoogleAnalyticsForm.tsx +1 -1
- package/templates/nextblock-template/app/cms/settings/languages/page.tsx +5 -7
- package/templates/nextblock-template/app/cms/settings/logos/page.tsx +4 -6
- package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +16 -7
- package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +1 -1
- package/templates/nextblock-template/app/cms/settings/registration/actions.ts +18 -7
- package/templates/nextblock-template/app/cms/settings/registration/components/RegistrationForm.tsx +1 -1
- package/templates/nextblock-template/app/cms/settings/security/actions.ts +227 -131
- package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +134 -18
- package/templates/nextblock-template/app/cms/users/page.tsx +5 -7
- package/templates/nextblock-template/lib/auth/twoFactor.test.ts +254 -0
- package/templates/nextblock-template/lib/auth/twoFactor.ts +56 -13
- package/templates/nextblock-template/lib/cms/action-result.ts +12 -0
- package/templates/nextblock-template/lib/config/email-settings.ts +40 -3
- package/templates/nextblock-template/next-env.d.ts +1 -1
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
|
@@ -50,6 +50,16 @@ type CortexAiSettingsStatus = {
|
|
|
50
50
|
unsplashAppName: string | null;
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* How every action in this file reports back: the page reads `?success=` / `?error=` and
|
|
55
|
+
* renders it. These are plain `<form action={fn}>` submissions, so a return value would be
|
|
56
|
+
* discarded and a thrown error would take out the page (with its message replaced by a
|
|
57
|
+
* generic string in production).
|
|
58
|
+
*
|
|
59
|
+
* Must be called OUTSIDE the try blocks below. `redirect()` signals itself by throwing
|
|
60
|
+
* NEXT_REDIRECT, so calling this inside a `try` would have the `catch` swallow the
|
|
61
|
+
* navigation and re-redirect with the framework's digest as the user-facing message.
|
|
62
|
+
*/
|
|
53
63
|
function redirectWithStatus(status: 'success' | 'error', message: string): never {
|
|
54
64
|
redirect(`${CORTEX_AI_SETTINGS_PATH}?${status}=${encodeURIComponent(message)}`);
|
|
55
65
|
}
|
|
@@ -173,7 +183,7 @@ export async function getCortexAiSettingsStatus(): Promise<CortexAiSettingsStatu
|
|
|
173
183
|
|
|
174
184
|
export async function saveOpenRouterApiKeyAction(formData: FormData) {
|
|
175
185
|
if (process.env.NEXT_PUBLIC_IS_SANDBOX === 'true') {
|
|
176
|
-
|
|
186
|
+
redirectWithStatus('error', 'Sandbox environment cannot save keys to the database.');
|
|
177
187
|
}
|
|
178
188
|
|
|
179
189
|
try {
|
|
@@ -206,7 +216,7 @@ export async function saveOpenRouterApiKeyAction(formData: FormData) {
|
|
|
206
216
|
|
|
207
217
|
export async function clearOpenRouterApiKeyAction() {
|
|
208
218
|
if (process.env.NEXT_PUBLIC_IS_SANDBOX === 'true') {
|
|
209
|
-
|
|
219
|
+
redirectWithStatus('error', 'Sandbox environment cannot clear keys from the database.');
|
|
210
220
|
}
|
|
211
221
|
|
|
212
222
|
try {
|
|
@@ -235,7 +245,7 @@ export async function clearOpenRouterApiKeyAction() {
|
|
|
235
245
|
|
|
236
246
|
export async function saveStockPhotoKeysAction(formData: FormData) {
|
|
237
247
|
if (process.env.NEXT_PUBLIC_IS_SANDBOX === 'true') {
|
|
238
|
-
|
|
248
|
+
redirectWithStatus('error', 'Sandbox environment cannot save keys to the database.');
|
|
239
249
|
}
|
|
240
250
|
|
|
241
251
|
try {
|
|
@@ -287,7 +297,7 @@ export async function saveStockPhotoKeysAction(formData: FormData) {
|
|
|
287
297
|
|
|
288
298
|
export async function clearStockPhotoKeysAction() {
|
|
289
299
|
if (process.env.NEXT_PUBLIC_IS_SANDBOX === 'true') {
|
|
290
|
-
|
|
300
|
+
redirectWithStatus('error', 'Sandbox environment cannot clear keys from the database.');
|
|
291
301
|
}
|
|
292
302
|
|
|
293
303
|
try {
|
|
@@ -313,7 +323,7 @@ export async function clearStockPhotoKeysAction() {
|
|
|
313
323
|
|
|
314
324
|
export async function saveCortexAiAgentSettingsAction(formData: FormData) {
|
|
315
325
|
if (process.env.NEXT_PUBLIC_IS_SANDBOX === 'true') {
|
|
316
|
-
|
|
326
|
+
redirectWithStatus('error', 'Sandbox environment cannot save settings to the database.');
|
|
317
327
|
}
|
|
318
328
|
|
|
319
329
|
try {
|
|
@@ -357,7 +367,7 @@ export async function saveCortexAiAgentSettingsAction(formData: FormData) {
|
|
|
357
367
|
|
|
358
368
|
export async function resetCortexAiAgentSettingsAction() {
|
|
359
369
|
if (process.env.NEXT_PUBLIC_IS_SANDBOX === 'true') {
|
|
360
|
-
|
|
370
|
+
redirectWithStatus('error', 'Sandbox environment cannot change settings in the database.');
|
|
361
371
|
}
|
|
362
372
|
|
|
363
373
|
try {
|
|
@@ -383,7 +393,7 @@ export async function resetCortexAiAgentSettingsAction() {
|
|
|
383
393
|
|
|
384
394
|
export async function saveCortexAiModelSelectionAction(formData: FormData) {
|
|
385
395
|
if (process.env.NEXT_PUBLIC_IS_SANDBOX === 'true') {
|
|
386
|
-
|
|
396
|
+
redirectWithStatus('error', 'Sandbox environment cannot save model selection to the database.');
|
|
387
397
|
}
|
|
388
398
|
|
|
389
399
|
try {
|
|
@@ -440,7 +450,7 @@ export async function saveCortexAiModelSelectionAction(formData: FormData) {
|
|
|
440
450
|
|
|
441
451
|
export async function clearCortexAiModelSelectionAction() {
|
|
442
452
|
if (process.env.NEXT_PUBLIC_IS_SANDBOX === 'true') {
|
|
443
|
-
|
|
453
|
+
redirectWithStatus('error', 'Sandbox environment cannot clear model selection from the database.');
|
|
444
454
|
}
|
|
445
455
|
|
|
446
456
|
try {
|
|
@@ -6,13 +6,23 @@ import { revalidatePath } from 'next/cache';
|
|
|
6
6
|
import { saveEmailSettings } from '../../../../lib/config/email-settings';
|
|
7
7
|
import { sendEmail } from '../../../actions/email';
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Returned rather than thrown: Next replaces uncaught Server Action error messages with a
|
|
11
|
+
* generic string in production, and the relay's own rejection ("535 authentication failed")
|
|
12
|
+
* is the single most useful thing an admin can see while getting SMTP working.
|
|
13
|
+
*/
|
|
14
|
+
export type EmailActionResult =
|
|
15
|
+
| { ok: true; message: string }
|
|
16
|
+
| { ok: false; error: string };
|
|
17
|
+
|
|
18
|
+
/** Null when the caller is an ADMIN, otherwise the failure to return. */
|
|
19
|
+
async function adminCheck(): Promise<EmailActionResult | null> {
|
|
10
20
|
const supabase = createClient();
|
|
11
21
|
const {
|
|
12
22
|
data: { user },
|
|
13
23
|
} = await supabase.auth.getUser();
|
|
14
24
|
if (!user) {
|
|
15
|
-
|
|
25
|
+
return { ok: false, error: 'You must be logged in to update settings.' };
|
|
16
26
|
}
|
|
17
27
|
const { data: profile, error } = await supabase
|
|
18
28
|
.from('profiles')
|
|
@@ -20,45 +30,65 @@ async function assertAdmin() {
|
|
|
20
30
|
.eq('id', user.id)
|
|
21
31
|
.single();
|
|
22
32
|
if (error || !profile || profile.role !== 'ADMIN') {
|
|
23
|
-
|
|
33
|
+
return { ok: false, error: 'You do not have permission to perform this action.' };
|
|
24
34
|
}
|
|
35
|
+
return null;
|
|
25
36
|
}
|
|
26
37
|
|
|
27
|
-
export async function updateEmailSettings(formData: FormData) {
|
|
28
|
-
await
|
|
38
|
+
export async function updateEmailSettings(formData: FormData): Promise<EmailActionResult> {
|
|
39
|
+
const denied = await adminCheck();
|
|
40
|
+
if (denied) return denied;
|
|
29
41
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
try {
|
|
43
|
+
await saveEmailSettings({
|
|
44
|
+
host: String(formData.get('host') ?? ''),
|
|
45
|
+
port: String(formData.get('port') ?? ''),
|
|
46
|
+
fromEmail: String(formData.get('fromEmail') ?? ''),
|
|
47
|
+
fromName: String(formData.get('fromName') ?? ''),
|
|
48
|
+
secure: formData.get('secure') === 'on' || formData.get('secure') === 'true',
|
|
49
|
+
user: String(formData.get('user') ?? ''),
|
|
50
|
+
pass: String(formData.get('pass') ?? ''),
|
|
51
|
+
});
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error('Failed to save email settings:', error);
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
error: error instanceof Error ? error.message : 'Failed to save email settings.',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
39
59
|
|
|
40
60
|
revalidatePath('/cms/settings/email');
|
|
41
|
-
return {
|
|
61
|
+
return { ok: true, message: 'Email settings saved.' };
|
|
42
62
|
}
|
|
43
63
|
|
|
44
|
-
export async function sendTestEmail(formData: FormData) {
|
|
45
|
-
await
|
|
64
|
+
export async function sendTestEmail(formData: FormData): Promise<EmailActionResult> {
|
|
65
|
+
const denied = await adminCheck();
|
|
66
|
+
if (denied) return denied;
|
|
46
67
|
|
|
47
68
|
const to = String(formData.get('to') ?? '').trim();
|
|
48
69
|
if (!to) {
|
|
49
|
-
|
|
70
|
+
return { ok: false, error: 'Enter a recipient email address.' };
|
|
50
71
|
}
|
|
51
72
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
73
|
+
try {
|
|
74
|
+
await sendEmail({
|
|
75
|
+
to,
|
|
76
|
+
subject: 'Test email',
|
|
77
|
+
text: 'This is a test email from your CMS. SMTP is configured correctly.',
|
|
78
|
+
html:
|
|
79
|
+
'<div style="font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;max-width:480px;margin:0 auto;padding:24px;">' +
|
|
80
|
+
'{{brand_header}}' +
|
|
81
|
+
'<p>This is a test email from your CMS. SMTP is configured correctly. 🎉</p>' +
|
|
82
|
+
'</div>',
|
|
83
|
+
});
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error('Test email failed:', error);
|
|
86
|
+
// Surface the relay's verbatim complaint — that is the whole point of a test send.
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
error: error instanceof Error ? error.message : 'Failed to send test email.',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
62
92
|
|
|
63
|
-
return {
|
|
93
|
+
return { ok: true, message: `Test email sent to ${to}.` };
|
|
64
94
|
}
|
|
@@ -52,6 +52,10 @@ export default function EmailForm({ initialSettings }: EmailFormProps) {
|
|
|
52
52
|
startTransition(async () => {
|
|
53
53
|
try {
|
|
54
54
|
const result = await updateEmailSettings(formData);
|
|
55
|
+
if (!result.ok) {
|
|
56
|
+
setMessage({ error: result.error });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
55
59
|
setMessage({ success: result.message });
|
|
56
60
|
setUser('');
|
|
57
61
|
setPass('');
|
|
@@ -68,7 +72,7 @@ export default function EmailForm({ initialSettings }: EmailFormProps) {
|
|
|
68
72
|
startTestTransition(async () => {
|
|
69
73
|
try {
|
|
70
74
|
const result = await sendTestEmail(formData);
|
|
71
|
-
setMessage({ success: result.message });
|
|
75
|
+
setMessage(result.ok ? { success: result.message } : { error: result.error });
|
|
72
76
|
} catch (error) {
|
|
73
77
|
setMessage({ error: error instanceof Error ? error.message : 'Failed to send test email.' });
|
|
74
78
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { createClient } from '@nextblock-cms/db/server';
|
|
5
5
|
import { revalidatePath } from 'next/cache';
|
|
6
|
+
import type { SettingsActionResult } from '../../../../lib/cms/action-result';
|
|
6
7
|
|
|
7
8
|
export async function getGlobalCss(): Promise<string> {
|
|
8
9
|
const supabase = createClient();
|
|
@@ -33,12 +34,12 @@ export async function getGlobalCss(): Promise<string> {
|
|
|
33
34
|
return String(data.value);
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
export async function updateGlobalCss(css: string) {
|
|
37
|
+
export async function updateGlobalCss(css: string): Promise<SettingsActionResult> {
|
|
37
38
|
const supabase = createClient();
|
|
38
39
|
|
|
39
40
|
const { data: { user } } = await supabase.auth.getUser();
|
|
40
41
|
if (!user) {
|
|
41
|
-
|
|
42
|
+
return { ok: false, error: 'You must be logged in to update settings.' };
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
const { data: profile, error: profileError } = await supabase
|
|
@@ -48,7 +49,7 @@ export async function updateGlobalCss(css: string) {
|
|
|
48
49
|
.single();
|
|
49
50
|
|
|
50
51
|
if (profileError || !profile || !['ADMIN', 'WRITER'].includes(profile.role)) {
|
|
51
|
-
|
|
52
|
+
return { ok: false, error: 'You do not have permission to perform this action.' };
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
const { error } = await supabase
|
|
@@ -57,9 +58,9 @@ export async function updateGlobalCss(css: string) {
|
|
|
57
58
|
|
|
58
59
|
if (error) {
|
|
59
60
|
console.error('Error updating global CSS:', error);
|
|
60
|
-
|
|
61
|
+
return { ok: false, error: 'Failed to update CSS.' };
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
revalidatePath('/', 'layout');
|
|
64
|
-
return {
|
|
65
|
+
return { ok: true, message: 'Global CSS updated successfully.' };
|
|
65
66
|
}
|
package/templates/nextblock-template/app/cms/settings/global-css/components/GlobalCssForm.tsx
CHANGED
|
@@ -15,7 +15,8 @@ export default function GlobalCssForm({ initialCss }: { initialCss: string }) {
|
|
|
15
15
|
setIsSubmitting(true);
|
|
16
16
|
try {
|
|
17
17
|
const res = await updateGlobalCss(css);
|
|
18
|
-
toast.success(res.message);
|
|
18
|
+
if (res.ok) toast.success(res.message);
|
|
19
|
+
else toast.error(res.error);
|
|
19
20
|
} catch (err: any) {
|
|
20
21
|
toast.error(err.message || 'An error occurred.');
|
|
21
22
|
} finally {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
mergePrivacySettings,
|
|
8
8
|
} from '../../../../lib/privacy/settings';
|
|
9
9
|
import type { PrivacySettings } from '../../../../lib/privacy/types';
|
|
10
|
+
import type { SettingsActionResult } from '../../../../lib/cms/action-result';
|
|
10
11
|
|
|
11
12
|
export interface GoogleAnalyticsSettings {
|
|
12
13
|
gtm_id: string;
|
|
@@ -23,13 +24,14 @@ export async function getGoogleAnalyticsSettings(): Promise<GoogleAnalyticsSetti
|
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
/** Null when the caller is an ADMIN, otherwise the failure to return. */
|
|
28
|
+
async function adminCheck(): Promise<SettingsActionResult | null> {
|
|
27
29
|
const supabase = createClient();
|
|
28
30
|
const {
|
|
29
31
|
data: { user },
|
|
30
32
|
} = await supabase.auth.getUser();
|
|
31
33
|
if (!user) {
|
|
32
|
-
|
|
34
|
+
return { ok: false, error: 'You must be logged in to update settings.' };
|
|
33
35
|
}
|
|
34
36
|
const { data: profile } = await supabase
|
|
35
37
|
.from('profiles')
|
|
@@ -37,12 +39,16 @@ async function assertAdmin(): Promise<void> {
|
|
|
37
39
|
.eq('id', user.id)
|
|
38
40
|
.single();
|
|
39
41
|
if (!profile || profile.role !== 'ADMIN') {
|
|
40
|
-
|
|
42
|
+
return { ok: false, error: 'You do not have permission to perform this action.' };
|
|
41
43
|
}
|
|
44
|
+
return null;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
export async function updateGoogleAnalyticsSettings(
|
|
45
|
-
|
|
47
|
+
export async function updateGoogleAnalyticsSettings(
|
|
48
|
+
formData: FormData,
|
|
49
|
+
): Promise<SettingsActionResult> {
|
|
50
|
+
const denied = await adminCheck();
|
|
51
|
+
if (denied) return denied;
|
|
46
52
|
|
|
47
53
|
// Only the analytics fields are touched; mergePrivacySettings preserves the
|
|
48
54
|
// banner/corporate fields owned by the Privacy & Consent page.
|
|
@@ -52,9 +58,14 @@ export async function updateGoogleAnalyticsSettings(formData: FormData) {
|
|
|
52
58
|
custom_scripts: formData.get('custom_scripts')?.toString() ?? '',
|
|
53
59
|
};
|
|
54
60
|
|
|
55
|
-
|
|
61
|
+
try {
|
|
62
|
+
await mergePrivacySettings(patch);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Failed to save Google Analytics settings:', error);
|
|
65
|
+
return { ok: false, error: 'Failed to save Google Analytics settings.' };
|
|
66
|
+
}
|
|
56
67
|
// The analytics guard (GTM/GA4 + custom scripts) lives in the root layout.
|
|
57
68
|
revalidatePath('/', 'layout');
|
|
58
69
|
|
|
59
|
-
return {
|
|
70
|
+
return { ok: true, message: 'Google Analytics settings saved.' };
|
|
60
71
|
}
|
|
@@ -44,7 +44,7 @@ export default function GoogleAnalyticsForm({ initialSettings }: GoogleAnalytics
|
|
|
44
44
|
startTransition(async () => {
|
|
45
45
|
try {
|
|
46
46
|
const result = await updateGoogleAnalyticsSettings(formData);
|
|
47
|
-
setMessage({ success: result.message });
|
|
47
|
+
setMessage(result.ok ? { success: result.message } : { error: result.error });
|
|
48
48
|
} catch (error) {
|
|
49
49
|
setMessage({
|
|
50
50
|
error: error instanceof Error ? error.message : 'An unknown error occurred.',
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
DropdownMenu,
|
|
19
19
|
DropdownMenuContent,
|
|
20
20
|
DropdownMenuItem,
|
|
21
|
-
|
|
21
|
+
DropdownMenuButtonTrigger,
|
|
22
22
|
DropdownMenuSeparator,
|
|
23
23
|
} from "@nextblock-cms/ui";
|
|
24
24
|
import type { Database } from "@nextblock-cms/db";
|
|
@@ -126,12 +126,10 @@ export default async function CmsLanguagesListPage() {
|
|
|
126
126
|
</TableCell>
|
|
127
127
|
<TableCell className="text-right">
|
|
128
128
|
<DropdownMenu>
|
|
129
|
-
<
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
</Button>
|
|
134
|
-
</DropdownMenuTrigger>
|
|
129
|
+
<DropdownMenuButtonTrigger id={`lang-trigger-${lang.id}`}>
|
|
130
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
131
|
+
<span className="sr-only">Language actions</span>
|
|
132
|
+
</DropdownMenuButtonTrigger>
|
|
135
133
|
<DropdownMenuContent align="end">
|
|
136
134
|
<DropdownMenuItem asChild>
|
|
137
135
|
<Link href={`/cms/settings/languages/${lang.id}/edit`} className="flex items-center cursor-pointer">
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
DropdownMenu,
|
|
15
15
|
DropdownMenuContent,
|
|
16
16
|
DropdownMenuItem,
|
|
17
|
-
|
|
17
|
+
DropdownMenuButtonTrigger,
|
|
18
18
|
DropdownMenuSeparator,
|
|
19
19
|
} from '@nextblock-cms/ui'
|
|
20
20
|
import { getActiveLogoId, getLogos, getSiteSeoSettings } from './actions'
|
|
@@ -131,11 +131,9 @@ export default async function CmsLogosListPage() {
|
|
|
131
131
|
</TableCell>
|
|
132
132
|
<TableCell className="text-right">
|
|
133
133
|
<DropdownMenu>
|
|
134
|
-
<
|
|
135
|
-
<
|
|
136
|
-
|
|
137
|
-
</Button>
|
|
138
|
-
</DropdownMenuTrigger>
|
|
134
|
+
<DropdownMenuButtonTrigger id={`logo-trigger-${logo.id}`}>
|
|
135
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
136
|
+
</DropdownMenuButtonTrigger>
|
|
139
137
|
<DropdownMenuContent align="end">
|
|
140
138
|
{logo.id !== activeLogoId ? (
|
|
141
139
|
<>
|
|
@@ -7,18 +7,20 @@ import {
|
|
|
7
7
|
mergePrivacySettings,
|
|
8
8
|
} from '../../../../lib/privacy/settings';
|
|
9
9
|
import type { PrivacySettings } from '../../../../lib/privacy/types';
|
|
10
|
+
import type { SettingsActionResult } from '../../../../lib/cms/action-result';
|
|
10
11
|
|
|
11
12
|
export async function getPrivacySettings(): Promise<PrivacySettings> {
|
|
12
13
|
return readPrivacySettings();
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
/** Null when the caller is an ADMIN, otherwise the failure to return. */
|
|
17
|
+
async function adminCheck(): Promise<SettingsActionResult | null> {
|
|
16
18
|
const supabase = createClient();
|
|
17
19
|
const {
|
|
18
20
|
data: { user },
|
|
19
21
|
} = await supabase.auth.getUser();
|
|
20
22
|
if (!user) {
|
|
21
|
-
|
|
23
|
+
return { ok: false, error: 'You must be logged in to update settings.' };
|
|
22
24
|
}
|
|
23
25
|
const { data: profile } = await supabase
|
|
24
26
|
.from('profiles')
|
|
@@ -26,12 +28,14 @@ async function assertAdmin(): Promise<void> {
|
|
|
26
28
|
.eq('id', user.id)
|
|
27
29
|
.single();
|
|
28
30
|
if (!profile || profile.role !== 'ADMIN') {
|
|
29
|
-
|
|
31
|
+
return { ok: false, error: 'You do not have permission to perform this action.' };
|
|
30
32
|
}
|
|
33
|
+
return null;
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
export async function updatePrivacySettings(formData: FormData) {
|
|
34
|
-
await
|
|
36
|
+
export async function updatePrivacySettings(formData: FormData): Promise<SettingsActionResult> {
|
|
37
|
+
const denied = await adminCheck();
|
|
38
|
+
if (denied) return denied;
|
|
35
39
|
|
|
36
40
|
// Analytics fields (GTM/GA4/custom scripts) are owned by the Google Analytics
|
|
37
41
|
// settings page; merge only the consent + corporate fields so they aren't clobbered.
|
|
@@ -44,9 +48,14 @@ export async function updatePrivacySettings(formData: FormData) {
|
|
|
44
48
|
},
|
|
45
49
|
};
|
|
46
50
|
|
|
47
|
-
|
|
51
|
+
try {
|
|
52
|
+
await mergePrivacySettings(patch);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error('Failed to save privacy settings:', error);
|
|
55
|
+
return { ok: false, error: 'Failed to save privacy settings.' };
|
|
56
|
+
}
|
|
48
57
|
// Footer (corporate identity) and the analytics guard live in the root layout.
|
|
49
58
|
revalidatePath('/', 'layout');
|
|
50
59
|
|
|
51
|
-
return {
|
|
60
|
+
return { ok: true, message: 'Privacy settings saved.' };
|
|
52
61
|
}
|
|
@@ -46,7 +46,7 @@ export default function PrivacyForm({ initialSettings }: PrivacyFormProps) {
|
|
|
46
46
|
startTransition(async () => {
|
|
47
47
|
try {
|
|
48
48
|
const result = await updatePrivacySettings(formData);
|
|
49
|
-
setMessage({ success: result.message });
|
|
49
|
+
setMessage(result.ok ? { success: result.message } : { error: result.error });
|
|
50
50
|
} catch (error) {
|
|
51
51
|
setMessage({
|
|
52
52
|
error: error instanceof Error ? error.message : 'An unknown error occurred.',
|
|
@@ -7,19 +7,21 @@ import {
|
|
|
7
7
|
getSystemConfiguration,
|
|
8
8
|
updateSystemConfiguration,
|
|
9
9
|
} from '../../../../lib/setup/system-config';
|
|
10
|
+
import type { SettingsActionResult } from '../../../../lib/cms/action-result';
|
|
10
11
|
|
|
11
12
|
export async function getRegistrationSettings(): Promise<{ autoAcceptSignups: boolean }> {
|
|
12
13
|
const config = await getSystemConfiguration();
|
|
13
14
|
return { autoAcceptSignups: config.auto_accept_signups };
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
/** Null when the caller is an ADMIN, otherwise the failure to return. */
|
|
18
|
+
async function adminCheck(): Promise<SettingsActionResult | null> {
|
|
17
19
|
const supabase = createClient();
|
|
18
20
|
const {
|
|
19
21
|
data: { user },
|
|
20
22
|
} = await supabase.auth.getUser();
|
|
21
23
|
if (!user) {
|
|
22
|
-
|
|
24
|
+
return { ok: false, error: 'You must be logged in to update settings.' };
|
|
23
25
|
}
|
|
24
26
|
const { data: profile, error } = await supabase
|
|
25
27
|
.from('profiles')
|
|
@@ -27,18 +29,27 @@ async function assertAdmin() {
|
|
|
27
29
|
.eq('id', user.id)
|
|
28
30
|
.single();
|
|
29
31
|
if (error || !profile || profile.role !== 'ADMIN') {
|
|
30
|
-
|
|
32
|
+
return { ok: false, error: 'You do not have permission to perform this action.' };
|
|
31
33
|
}
|
|
34
|
+
return null;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
export async function updateRegistrationSettings(
|
|
35
|
-
|
|
37
|
+
export async function updateRegistrationSettings(
|
|
38
|
+
formData: FormData,
|
|
39
|
+
): Promise<SettingsActionResult> {
|
|
40
|
+
const denied = await adminCheck();
|
|
41
|
+
if (denied) return denied;
|
|
36
42
|
|
|
37
43
|
const autoAcceptSignups =
|
|
38
44
|
formData.get('autoAcceptSignups') === 'on' || formData.get('autoAcceptSignups') === 'true';
|
|
39
45
|
|
|
40
|
-
|
|
46
|
+
try {
|
|
47
|
+
await updateSystemConfiguration({ auto_accept_signups: autoAcceptSignups });
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('Failed to save registration settings:', error);
|
|
50
|
+
return { ok: false, error: 'Failed to save registration settings.' };
|
|
51
|
+
}
|
|
41
52
|
|
|
42
53
|
revalidatePath('/cms/settings/registration');
|
|
43
|
-
return {
|
|
54
|
+
return { ok: true, message: 'Registration settings saved.' };
|
|
44
55
|
}
|
package/templates/nextblock-template/app/cms/settings/registration/components/RegistrationForm.tsx
CHANGED
|
@@ -27,7 +27,7 @@ export default function RegistrationForm({ initialSettings }: RegistrationFormPr
|
|
|
27
27
|
startTransition(async () => {
|
|
28
28
|
try {
|
|
29
29
|
const result = await updateRegistrationSettings(formData);
|
|
30
|
-
setMessage({ success: result.message });
|
|
30
|
+
setMessage(result.ok ? { success: result.message } : { error: result.error });
|
|
31
31
|
} catch (error) {
|
|
32
32
|
setMessage({ error: error instanceof Error ? error.message : 'Failed to save settings.' });
|
|
33
33
|
}
|