create-nextblock 0.13.7 → 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/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/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/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
|
@@ -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.',
|
|
@@ -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
|
}
|