Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions FrontEnd/my-app/components/error/APIBootstrapErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,22 +100,36 @@ export class APIBootstrapErrorBoundary extends React.Component<
};

static getDerivedStateFromError(
error: Error | AppError
error: Error | AppError | Record<string, unknown>
): Partial<APIBootstrapErrorState> {
const errorWithCode = error as Record<string, unknown>;

let errorMessage = '';
if (error instanceof Error) {
errorMessage = error.message;
} else if (
error &&
typeof error === 'object' &&
'message' in error &&
typeof (error as Record<string, unknown>).message === 'string'
) {
errorMessage = (error as Record<string, unknown>).message as string;
}

const isNetworkError =
error.message.toLowerCase().includes('offline') ||
error.message.toLowerCase().includes('network') ||
error.message.toLowerCase().includes('unreachable') ||
(error as any).code === 'ERR_NETWORK' ||
(error as any).code === 'NETWORK_ERROR' ||
(error as any).code === 'TIMEOUT_ERROR';
errorMessage.toLowerCase().includes('offline') ||
errorMessage.toLowerCase().includes('network') ||
errorMessage.toLowerCase().includes('unreachable') ||
errorWithCode.code === 'ERR_NETWORK' ||
errorWithCode.code === 'NETWORK_ERROR' ||
errorWithCode.code === 'TIMEOUT_ERROR';

return {
hasError: true,
error,
error: error as Error | AppError,
errorInfo: null,
isOffline:
isNetworkError ||
!!isNetworkError ||
(typeof navigator !== 'undefined' && !navigator.onLine),
};
}
Expand Down
3 changes: 1 addition & 2 deletions FrontEnd/my-app/components/error/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

import React from 'react';
import { ErrorMessage } from './ErrorMessage';
import { logError, ERROR_CODES } from '@/lib/utils/error-handler';
import type { AppError } from '@/lib/utils/error-handler';
import { logError } from '@/lib/utils/error-handler';

interface ErrorBoundaryProps {
children: React.ReactNode;
Expand Down
2 changes: 1 addition & 1 deletion FrontEnd/my-app/lib/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
type AppError,
} from '@/lib/utils/error-handler';
import { mapApiError, inferDomainFromUrl } from '@/lib/api/api-error-mapper';
import type { ApiErrorResponse, AuthTokens } from '@/lib/types/api.types';
import type { AuthTokens } from '@/lib/types/api.types';
import { env } from '@/lib/config/env';

// ---------------------------------------------------------------------------
Expand Down
51 changes: 42 additions & 9 deletions FrontEnd/my-app/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,54 @@
import { NextRequest, NextResponse } from 'next/server';
import createMiddleware from 'next-intl/middleware';
import { locales, defaultLocale } from '@/lib/i18n/config';
import crypto from 'crypto';
import { cspHeaders } from './next.config.csp';

export default createMiddleware({
// A list of all locales that are supported
const intlMiddleware = createMiddleware({
locales,

// Used when no locale matches
defaultLocale,

// Language detection
localeDetection: true,

// Locale prefix - always add the locale to the URL
localePrefix: 'always',
});

export function middleware(request: NextRequest) {
// Generate nonce per request
const nonce = crypto.randomBytes(16).toString('base64');

// Run intl middleware first
const intlResponse = intlMiddleware(request);

// Determine CSP header key based on environment
const isDev = process.env.NODE_ENV !== 'production';
const headerKey = isDev
? 'Content-Security-Policy-Report-Only'
: 'Content-Security-Policy';

// Prepare response object
const response =
intlResponse instanceof NextResponse
? intlResponse
: NextResponse.next();

cspHeaders.forEach((config) => {
if (config.headers && Array.isArray(config.headers)) {
config.headers.forEach((header) => {
if (header.key === 'Content-Security-Policy') {
const value = header.value.replace(/'unsafe-inline'/g, `'nonce-${nonce}'`);
response.headers.set(headerKey, value);
} else {
response.headers.set(header.key, header.value);
}
});
}
});

// Expose nonce to client if needed
response.headers.set('X-Content-Nonce', nonce);

return response;
}

export const config = {
// Match only internationalized pathnames
matcher: ['/', '/(es|en)/:path*', '/((?!api|_next|_static|.*\\..*).*)'],
};
14 changes: 13 additions & 1 deletion FrontEnd/my-app/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,19 @@ const withAnalyzer = withBundleAnalyzer({
const nextConfig: NextConfig = {
outputFileTracingRoot: __dirname,
async headers() {
return cspHeaders;
const isDev = process.env.NODE_ENV !== 'production';
const headerKey = isDev
? 'Content-Security-Policy-Report-Only'
: 'Content-Security-Policy';
// Flatten cspHeaders and replace key based on environment
const baseHeaders = cspHeaders[0].headers;
const modifiedHeaders = baseHeaders.map((h) => {
if (h.key === 'Content-Security-Policy') {
return { key: headerKey, value: h.value };
}
return h;
});
return [{ source: '/(.*)', headers: modifiedHeaders }];
},
};

Expand Down
Loading