create-brainerce-store 1.27.5 → 1.28.0

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 (35) hide show
  1. package/dist/index.js +95 -22
  2. package/messages/en.json +12 -1
  3. package/messages/he.json +12 -1
  4. package/package.json +1 -1
  5. package/templates/nextjs/base/.env.local.ejs +3 -3
  6. package/templates/nextjs/base/next.config.ts +13 -12
  7. package/templates/nextjs/base/package.json.ejs +2 -1
  8. package/templates/nextjs/base/src/app/api/auth/logout/route.ts +15 -14
  9. package/templates/nextjs/base/src/app/api/auth/oauth-callback/route.ts +66 -59
  10. package/templates/nextjs/base/src/app/api/auth/reset-password/route.ts +76 -77
  11. package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +229 -198
  12. package/templates/nextjs/base/src/app/checkout/page.tsx +975 -972
  13. package/templates/nextjs/base/src/app/layout.tsx.ejs +29 -13
  14. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +271 -271
  15. package/templates/nextjs/base/src/app/payment-complete/page.tsx +59 -59
  16. package/templates/nextjs/base/src/app/products/[slug]/product-client-section.tsx +501 -486
  17. package/templates/nextjs/base/src/app/products/page.tsx +475 -475
  18. package/templates/nextjs/base/src/app/reset-password/page.tsx +138 -131
  19. package/templates/nextjs/base/src/components/auth/register-form.tsx +245 -232
  20. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +416 -415
  21. package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +258 -184
  22. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +84 -20
  23. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +86 -72
  24. package/templates/nextjs/base/src/lib/csrf.ts +11 -0
  25. package/templates/nextjs/base/src/lib/navigation.tsx.ejs +60 -60
  26. package/templates/nextjs/base/src/lib/nonce.ts +10 -0
  27. package/templates/nextjs/base/src/lib/safe-redirect.ts +45 -0
  28. package/templates/nextjs/base/src/lib/sanitize-html.ts +93 -0
  29. package/templates/nextjs/base/src/lib/validation.ts +37 -0
  30. package/templates/nextjs/base/src/middleware.ts.ejs +91 -8
  31. package/templates/nextjs/base/tsconfig.tsbuildinfo +1 -0
  32. package/templates/nextjs/themes/luxury/globals.css +399 -399
  33. package/templates/nextjs/themes/luxury/theme.json +23 -23
  34. package/templates/nextjs/themes/playful/globals.css +400 -400
  35. package/templates/nextjs/themes/playful/theme.json +23 -23
@@ -1,72 +1,86 @@
1
- import type { Product } from 'brainerce';
2
- import { getProductPriceInfo } from 'brainerce';
3
-
4
- interface ProductJsonLdProps {
5
- product: Product;
6
- url: string;
7
- currency?: string;
8
- }
9
-
10
- export function ProductJsonLd({ product, url, currency = 'USD' }: ProductJsonLdProps) {
11
- const priceInfo = getProductPriceInfo(product);
12
- const imageUrl = product.images?.[0]?.url;
13
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
14
-
15
- const productJsonLd = {
16
- '@context': 'https://schema.org',
17
- '@type': 'Product',
18
- name: product.name,
19
- description: product.description || product.name,
20
- image: imageUrl,
21
- url,
22
- sku: product.sku || product.id,
23
- offers: {
24
- '@type': 'Offer',
25
- price: priceInfo.price,
26
- priceCurrency: currency,
27
- availability:
28
- product.inventory?.canPurchase !== false
29
- ? 'https://schema.org/InStock'
30
- : 'https://schema.org/OutOfStock',
31
- url,
32
- },
33
- };
34
-
35
- const breadcrumbJsonLd = {
36
- '@context': 'https://schema.org',
37
- '@type': 'BreadcrumbList',
38
- itemListElement: [
39
- {
40
- '@type': 'ListItem',
41
- position: 1,
42
- name: 'Home',
43
- item: baseUrl || '/',
44
- },
45
- {
46
- '@type': 'ListItem',
47
- position: 2,
48
- name: 'Products',
49
- item: `${baseUrl}/products`,
50
- },
51
- {
52
- '@type': 'ListItem',
53
- position: 3,
54
- name: product.name,
55
- item: url,
56
- },
57
- ],
58
- };
59
-
60
- return (
61
- <>
62
- <script
63
- type="application/ld+json"
64
- dangerouslySetInnerHTML={{ __html: JSON.stringify(productJsonLd) }}
65
- />
66
- <script
67
- type="application/ld+json"
68
- dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
69
- />
70
- </>
71
- );
72
- }
1
+ import type { Product } from 'brainerce';
2
+ import { getProductPriceInfo } from 'brainerce';
3
+ import { getNonce } from '@/lib/nonce';
4
+
5
+ interface ProductJsonLdProps {
6
+ product: Product;
7
+ url: string;
8
+ currency?: string;
9
+ }
10
+
11
+ export async function ProductJsonLd({ product, url, currency = 'USD' }: ProductJsonLdProps) {
12
+ const nonce = await getNonce();
13
+ const priceInfo = getProductPriceInfo(product);
14
+ const imageUrl = product.images?.[0]?.url;
15
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
16
+
17
+ const productJsonLd = {
18
+ '@context': 'https://schema.org',
19
+ '@type': 'Product',
20
+ name: product.name,
21
+ description: product.description || product.name,
22
+ image: imageUrl,
23
+ url,
24
+ sku: product.sku || product.id,
25
+ offers: {
26
+ '@type': 'Offer',
27
+ price: priceInfo.price,
28
+ priceCurrency: currency,
29
+ availability:
30
+ product.inventory?.canPurchase !== false
31
+ ? 'https://schema.org/InStock'
32
+ : 'https://schema.org/OutOfStock',
33
+ url,
34
+ },
35
+ };
36
+
37
+ const breadcrumbJsonLd = {
38
+ '@context': 'https://schema.org',
39
+ '@type': 'BreadcrumbList',
40
+ itemListElement: [
41
+ {
42
+ '@type': 'ListItem',
43
+ position: 1,
44
+ name: 'Home',
45
+ item: baseUrl || '/',
46
+ },
47
+ {
48
+ '@type': 'ListItem',
49
+ position: 2,
50
+ name: 'Products',
51
+ item: `${baseUrl}/products`,
52
+ },
53
+ {
54
+ '@type': 'ListItem',
55
+ position: 3,
56
+ name: product.name,
57
+ item: url,
58
+ },
59
+ ],
60
+ };
61
+
62
+ return (
63
+ <>
64
+ <script
65
+ type="application/ld+json"
66
+ nonce={nonce}
67
+ dangerouslySetInnerHTML={{ __html: serializeJsonLd(productJsonLd) }}
68
+ />
69
+ <script
70
+ type="application/ld+json"
71
+ nonce={nonce}
72
+ dangerouslySetInnerHTML={{ __html: serializeJsonLd(breadcrumbJsonLd) }}
73
+ />
74
+ </>
75
+ );
76
+ }
77
+
78
+ // Serialize a JSON-LD object for embedding in a <script> tag. Escapes
79
+ // `<`, `>`, and `&` to \uXXXX so seller-controlled product fields cannot
80
+ // break out of the script element with `</script>` or inject HTML.
81
+ function serializeJsonLd(value: unknown): string {
82
+ return JSON.stringify(value)
83
+ .replace(/</g, '\\u003c')
84
+ .replace(/>/g, '\\u003e')
85
+ .replace(/&/g, '\\u0026');
86
+ }
@@ -0,0 +1,11 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ export const CSRF_HEADER = 'x-requested-with';
4
+ export const CSRF_VALUE = 'brainerce';
5
+
6
+ export function checkCsrf(request: NextRequest): NextResponse | null {
7
+ if (request.headers.get(CSRF_HEADER) !== CSRF_VALUE) {
8
+ return NextResponse.json({ error: 'CSRF validation failed' }, { status: 403 });
9
+ }
10
+ return null;
11
+ }
@@ -1,60 +1,60 @@
1
- <% if (i18nEnabled) { %>
2
- 'use client';
3
-
4
- import React from 'react';
5
- import NextLink from 'next/link';
6
- import { useRouter as useNextRouter, usePathname } from 'next/navigation';
7
- import type { ComponentProps } from 'react';
8
-
9
- const supportedLocales = <%- supportedLocales %>;
10
- const defaultLocale = '<%= defaultLocale %>';
11
-
12
- function getLocale(pathname: string): string {
13
- const segment = pathname.split('/')[1];
14
- return supportedLocales.includes(segment) ? segment : defaultLocale;
15
- }
16
-
17
- function localizePath(path: string, locale: string): string {
18
- if (!path.startsWith('/')) return path;
19
- // Don't double-prefix
20
- const firstSeg = path.split('/')[1];
21
- if (supportedLocales.includes(firstSeg)) return path;
22
- return `/${locale}${path}`;
23
- }
24
-
25
- /**
26
- * Locale-aware Link — automatically prepends current locale to href.
27
- */
28
- export function Link({ href, ...props }: ComponentProps<typeof NextLink>) {
29
- const pathname = usePathname();
30
- const locale = getLocale(pathname);
31
- const localizedHref =
32
- typeof href === 'string' ? localizePath(href, locale) : href;
33
- return <NextLink href={localizedHref} {...props} />;
34
- }
35
-
36
- /**
37
- * Locale-aware useRouter — push/replace automatically prepend locale.
38
- */
39
- export function useRouter() {
40
- const router = useNextRouter();
41
- const pathname = usePathname();
42
- const locale = getLocale(pathname);
43
-
44
- return {
45
- ...router,
46
- push: (url: string, options?: Parameters<typeof router.push>[1]) => {
47
- return router.push(localizePath(url, locale), options);
48
- },
49
- replace: (url: string, options?: Parameters<typeof router.replace>[1]) => {
50
- return router.replace(localizePath(url, locale), options);
51
- },
52
- };
53
- }
54
- <% } else { %>
55
- /* Single-language store — re-export Next.js navigation as-is */
56
- 'use client';
57
-
58
- export { default as Link } from 'next/link';
59
- export { useRouter } from 'next/navigation';
60
- <% } %>
1
+ <% if (i18nEnabled) { %>
2
+ 'use client';
3
+
4
+ import React from 'react';
5
+ import NextLink from 'next/link';
6
+ import { useRouter as useNextRouter, usePathname } from 'next/navigation';
7
+ import type { ComponentProps } from 'react';
8
+
9
+ const supportedLocales = <%- supportedLocales %>;
10
+ const defaultLocale = '<%= defaultLocale %>';
11
+
12
+ function getLocale(pathname: string): string {
13
+ const segment = pathname.split('/')[1];
14
+ return supportedLocales.includes(segment) ? segment : defaultLocale;
15
+ }
16
+
17
+ function localizePath(path: string, locale: string): string {
18
+ if (!path.startsWith('/')) return path;
19
+ // Don't double-prefix
20
+ const firstSeg = path.split('/')[1];
21
+ if (supportedLocales.includes(firstSeg)) return path;
22
+ return `/${locale}${path}`;
23
+ }
24
+
25
+ /**
26
+ * Locale-aware Link — automatically prepends current locale to href.
27
+ */
28
+ export function Link({ href, ...props }: ComponentProps<typeof NextLink>) {
29
+ const pathname = usePathname();
30
+ const locale = getLocale(pathname);
31
+ const localizedHref =
32
+ typeof href === 'string' ? localizePath(href, locale) : href;
33
+ return <NextLink href={localizedHref} {...props} />;
34
+ }
35
+
36
+ /**
37
+ * Locale-aware useRouter — push/replace automatically prepend locale.
38
+ */
39
+ export function useRouter() {
40
+ const router = useNextRouter();
41
+ const pathname = usePathname();
42
+ const locale = getLocale(pathname);
43
+
44
+ return {
45
+ ...router,
46
+ push: (url: string, options?: Parameters<typeof router.push>[1]) => {
47
+ return router.push(localizePath(url, locale), options);
48
+ },
49
+ replace: (url: string, options?: Parameters<typeof router.replace>[1]) => {
50
+ return router.replace(localizePath(url, locale), options);
51
+ },
52
+ };
53
+ }
54
+ <% } else { %>
55
+ /* Single-language store — re-export Next.js navigation as-is */
56
+ 'use client';
57
+
58
+ export { default as Link } from 'next/link';
59
+ export { useRouter } from 'next/navigation';
60
+ <% } %>
@@ -0,0 +1,10 @@
1
+ import { headers } from 'next/headers';
2
+
3
+ /**
4
+ * Reads the per-request CSP nonce set by middleware.
5
+ * Use this in server components that render inline `<script>` tags so
6
+ * they comply with the strict CSP (no `unsafe-inline`).
7
+ */
8
+ export async function getNonce(): Promise<string | undefined> {
9
+ return (await headers()).get('x-nonce') ?? undefined;
10
+ }
@@ -0,0 +1,45 @@
1
+ const ALLOWED_PAYMENT_HOSTS: readonly string[] = [
2
+ 'checkout.stripe.com',
3
+ 'js.stripe.com',
4
+ 'hooks.stripe.com',
5
+ 'www.paypal.com',
6
+ 'www.sandbox.paypal.com',
7
+ 'secure.cardcom.solutions',
8
+ 'meshulam.co.il',
9
+ 'grow.link',
10
+ 'grow.security',
11
+ 'creditguard.co.il',
12
+ ];
13
+
14
+ export function isAllowedPaymentUrl(url: string): boolean {
15
+ if (!url || typeof url !== 'string') return false;
16
+
17
+ let parsed: URL;
18
+ try {
19
+ parsed = new URL(url);
20
+ } catch {
21
+ return false;
22
+ }
23
+
24
+ if (parsed.protocol !== 'https:') return false;
25
+
26
+ const hostname = parsed.hostname.toLowerCase();
27
+ return ALLOWED_PAYMENT_HOSTS.some((host) => hostname === host || hostname.endsWith('.' + host));
28
+ }
29
+
30
+ export function safePaymentRedirect(url: string): void {
31
+ if (!isAllowedPaymentUrl(url)) {
32
+ throw new Error('Payment redirect URL is not in the allowlist');
33
+ }
34
+ if (typeof window !== 'undefined') {
35
+ window.location.href = url;
36
+ }
37
+ }
38
+
39
+ // CUID format used by Prisma for Checkout.id — c + 24 lowercase alphanumeric chars.
40
+ // Allow a small range to tolerate cuid2 (slightly different length).
41
+ const CHECKOUT_ID_RE = /^c[a-z0-9]{20,30}$/;
42
+
43
+ export function isValidCheckoutId(id: unknown): id is string {
44
+ return typeof id === 'string' && CHECKOUT_ID_RE.test(id);
45
+ }
@@ -0,0 +1,93 @@
1
+ import DOMPurify from 'isomorphic-dompurify';
2
+
3
+ const TRUSTED_IFRAME_PREFIXES = [
4
+ 'https://www.youtube.com/embed/',
5
+ 'https://www.youtube-nocookie.com/embed/',
6
+ 'https://player.vimeo.com/video/',
7
+ ];
8
+
9
+ let hooksRegistered = false;
10
+
11
+ function registerHooksOnce() {
12
+ if (hooksRegistered) return;
13
+ hooksRegistered = true;
14
+
15
+ DOMPurify.addHook('uponSanitizeElement', (node, data) => {
16
+ if (data.tagName !== 'iframe') return;
17
+ const el = node as Element;
18
+ const src = el.getAttribute('src') || '';
19
+ const isTrusted = TRUSTED_IFRAME_PREFIXES.some((prefix) => src.startsWith(prefix));
20
+ if (!isTrusted) {
21
+ el.parentNode?.removeChild(el);
22
+ }
23
+ });
24
+
25
+ DOMPurify.addHook('afterSanitizeAttributes', (node) => {
26
+ if (!('tagName' in node)) return;
27
+ const el = node as Element;
28
+ if (el.tagName === 'A' && el.getAttribute('target') === '_blank') {
29
+ el.setAttribute('rel', 'noopener noreferrer nofollow');
30
+ }
31
+ });
32
+ }
33
+
34
+ export function sanitizeProductHtml(html: string): string {
35
+ if (!html) return '';
36
+ registerHooksOnce();
37
+
38
+ return DOMPurify.sanitize(html, {
39
+ ALLOWED_TAGS: [
40
+ 'p',
41
+ 'br',
42
+ 'strong',
43
+ 'b',
44
+ 'em',
45
+ 'i',
46
+ 'u',
47
+ 's',
48
+ 'strike',
49
+ 'h1',
50
+ 'h2',
51
+ 'h3',
52
+ 'h4',
53
+ 'h5',
54
+ 'h6',
55
+ 'ul',
56
+ 'ol',
57
+ 'li',
58
+ 'a',
59
+ 'blockquote',
60
+ 'code',
61
+ 'pre',
62
+ 'div',
63
+ 'span',
64
+ 'img',
65
+ 'table',
66
+ 'thead',
67
+ 'tbody',
68
+ 'tr',
69
+ 'th',
70
+ 'td',
71
+ 'colgroup',
72
+ 'col',
73
+ 'iframe',
74
+ ],
75
+ ALLOWED_ATTR: [
76
+ 'href',
77
+ 'target',
78
+ 'rel',
79
+ 'class',
80
+ 'src',
81
+ 'alt',
82
+ 'width',
83
+ 'height',
84
+ 'colspan',
85
+ 'rowspan',
86
+ 'allow',
87
+ 'allowfullscreen',
88
+ 'frameborder',
89
+ 'loading',
90
+ ],
91
+ ALLOW_DATA_ATTR: false,
92
+ });
93
+ }
@@ -0,0 +1,37 @@
1
+ const EMAIL_REGEX =
2
+ /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/;
3
+
4
+ export function isValidEmail(email: string): boolean {
5
+ if (!email || typeof email !== 'string') return false;
6
+ if (email.length > 254) return false;
7
+ return EMAIL_REGEX.test(email);
8
+ }
9
+
10
+ export type PasswordErrorCode =
11
+ | 'passwordRequired'
12
+ | 'passwordTooShort'
13
+ | 'passwordNoUppercase'
14
+ | 'passwordNoNumber'
15
+ | 'passwordNoSymbol';
16
+
17
+ export function getPasswordError(password: string): PasswordErrorCode | null {
18
+ if (!password || typeof password !== 'string') return 'passwordRequired';
19
+ if (password.length < 8) return 'passwordTooShort';
20
+ if (!/[A-Z]/.test(password)) return 'passwordNoUppercase';
21
+ if (!/[0-9]/.test(password)) return 'passwordNoNumber';
22
+ if (!/[^A-Za-z0-9]/.test(password)) return 'passwordNoSymbol';
23
+ return null;
24
+ }
25
+
26
+ const PASSWORD_ERROR_MESSAGES: Record<PasswordErrorCode, string> = {
27
+ passwordRequired: 'Password is required',
28
+ passwordTooShort: 'Password must be at least 8 characters',
29
+ passwordNoUppercase: 'Password must contain at least one uppercase letter',
30
+ passwordNoNumber: 'Password must contain at least one number',
31
+ passwordNoSymbol: 'Password must contain at least one special character',
32
+ };
33
+
34
+ export function validatePassword(password: string): string | null {
35
+ const code = getPasswordError(password);
36
+ return code ? PASSWORD_ERROR_MESSAGES[code] : null;
37
+ }
@@ -11,6 +11,38 @@ function getLocaleFromPath(pathname: string): string | null {
11
11
  return supportedLocales.includes(segment) ? segment : null;
12
12
  }
13
13
 
14
+ function generateNonce(): string {
15
+ const bytes = new Uint8Array(16);
16
+ crypto.getRandomValues(bytes);
17
+ let binary = '';
18
+ for (const b of bytes) binary += String.fromCharCode(b);
19
+ return btoa(binary);
20
+ }
21
+
22
+ function buildCsp(nonce: string): string {
23
+ return [
24
+ "default-src 'self'",
25
+ `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
26
+ "style-src 'self' 'unsafe-inline' https://cdn.meshulam.co.il",
27
+ "img-src 'self' data: blob: https:",
28
+ "font-src 'self' data:",
29
+ "frame-src 'self' https://meshulam.co.il https://*.meshulam.co.il https://grow.link https://*.grow.link https://grow.security https://*.grow.security https://creditguard.co.il https://*.creditguard.co.il https://js.stripe.com https://hooks.stripe.com https://pay.google.com https://secure.cardcom.solutions https://checkout.stripe.com https://www.paypal.com https://www.sandbox.paypal.com",
30
+ "connect-src 'self' https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://pay.google.com https://*.stripe.com https://*.creditguard.co.il",
31
+ "worker-src 'self' blob:",
32
+ "frame-ancestors 'none'",
33
+ "base-uri 'self'",
34
+ "form-action 'self'",
35
+ "object-src 'none'",
36
+ 'upgrade-insecure-requests',
37
+ ].join('; ');
38
+ }
39
+
40
+ function applyCspHeaders(response: NextResponse, nonce: string): NextResponse {
41
+ response.headers.set('Content-Security-Policy', buildCsp(nonce));
42
+ response.headers.set('x-nonce', nonce);
43
+ return response;
44
+ }
45
+
14
46
  export function middleware(request: NextRequest) {
15
47
  const { pathname } = request.nextUrl;
16
48
 
@@ -23,13 +55,17 @@ export function middleware(request: NextRequest) {
23
55
  return NextResponse.next();
24
56
  }
25
57
 
58
+ const nonce = generateNonce();
59
+ const requestHeaders = new Headers(request.headers);
60
+ requestHeaders.set('x-nonce', nonce);
61
+
26
62
  const pathnameLocale = getLocaleFromPath(pathname);
27
63
 
28
64
  // Redirect to default locale if no locale prefix
29
65
  if (!pathnameLocale) {
30
66
  const url = request.nextUrl.clone();
31
67
  url.pathname = `/${defaultLocale}${pathname}`;
32
- return NextResponse.redirect(url);
68
+ return applyCspHeaders(NextResponse.redirect(url), nonce);
33
69
  }
34
70
 
35
71
  // Auth protection (with locale prefix)
@@ -39,14 +75,14 @@ export function middleware(request: NextRequest) {
39
75
  const token = request.cookies.get(TOKEN_COOKIE);
40
76
  if (!token?.value) {
41
77
  const loginUrl = new URL(`/${pathnameLocale}/login`, request.url);
42
- return NextResponse.redirect(loginUrl);
78
+ return applyCspHeaders(NextResponse.redirect(loginUrl), nonce);
43
79
  }
44
80
  }
45
81
 
46
- // Set locale header for server components
47
- const response = NextResponse.next();
82
+ // Set locale header for server components + forward nonce
83
+ const response = NextResponse.next({ request: { headers: requestHeaders } });
48
84
  response.headers.set('x-locale', pathnameLocale);
49
- return response;
85
+ return applyCspHeaders(response, nonce);
50
86
  }
51
87
 
52
88
  export const config = {
@@ -60,22 +96,69 @@ const TOKEN_COOKIE = 'brainerce_customer_token';
60
96
  /** Routes that require customer authentication */
61
97
  const PROTECTED_PATHS = ['/account'];
62
98
 
99
+ function generateNonce(): string {
100
+ const bytes = new Uint8Array(16);
101
+ crypto.getRandomValues(bytes);
102
+ let binary = '';
103
+ for (const b of bytes) binary += String.fromCharCode(b);
104
+ return btoa(binary);
105
+ }
106
+
107
+ function buildCsp(nonce: string): string {
108
+ return [
109
+ "default-src 'self'",
110
+ `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
111
+ "style-src 'self' 'unsafe-inline' https://cdn.meshulam.co.il",
112
+ "img-src 'self' data: blob: https:",
113
+ "font-src 'self' data:",
114
+ "frame-src 'self' https://meshulam.co.il https://*.meshulam.co.il https://grow.link https://*.grow.link https://grow.security https://*.grow.security https://creditguard.co.il https://*.creditguard.co.il https://js.stripe.com https://hooks.stripe.com https://pay.google.com https://secure.cardcom.solutions https://checkout.stripe.com https://www.paypal.com https://www.sandbox.paypal.com",
115
+ "connect-src 'self' https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://pay.google.com https://*.stripe.com https://*.creditguard.co.il",
116
+ "worker-src 'self' blob:",
117
+ "frame-ancestors 'none'",
118
+ "base-uri 'self'",
119
+ "form-action 'self'",
120
+ "object-src 'none'",
121
+ 'upgrade-insecure-requests',
122
+ ].join('; ');
123
+ }
124
+
125
+ function applyCspHeaders(response: NextResponse, nonce: string): NextResponse {
126
+ response.headers.set('Content-Security-Policy', buildCsp(nonce));
127
+ response.headers.set('x-nonce', nonce);
128
+ return response;
129
+ }
130
+
63
131
  export function middleware(request: NextRequest) {
64
132
  const { pathname } = request.nextUrl;
133
+
134
+ // Skip static files and API routes
135
+ if (
136
+ pathname.startsWith('/api/') ||
137
+ pathname.startsWith('/_next/') ||
138
+ pathname.includes('.')
139
+ ) {
140
+ return NextResponse.next();
141
+ }
142
+
143
+ const nonce = generateNonce();
144
+ const requestHeaders = new Headers(request.headers);
145
+ requestHeaders.set('x-nonce', nonce);
146
+
65
147
  const isProtected = PROTECTED_PATHS.some((p) => pathname.startsWith(p));
66
148
 
67
149
  if (isProtected) {
68
150
  const token = request.cookies.get(TOKEN_COOKIE);
69
151
  if (!token?.value) {
70
152
  const loginUrl = new URL('/login', request.url);
71
- return NextResponse.redirect(loginUrl);
153
+ return applyCspHeaders(NextResponse.redirect(loginUrl), nonce);
72
154
  }
73
155
  }
74
156
 
75
- return NextResponse.next();
157
+ const response = NextResponse.next({ request: { headers: requestHeaders } });
158
+ return applyCspHeaders(response, nonce);
76
159
  }
77
160
 
78
161
  export const config = {
79
- matcher: ['/account/:path*'],
162
+ matcher: ['/((?!_next|api|.*\\..*).*)'],
80
163
  };
81
164
  <% } %>