refactor: enhance security and performance in configuration and API routes

- Update Content Security Policy (CSP) in next.config.ts to avoid `unsafe-eval` in production, improving security against XSS attacks.
- Refactor API routes to enforce admin authentication and session validation, ensuring secure access to sensitive endpoints.
- Optimize analytics data retrieval by using database aggregation instead of loading all records into memory, improving performance and reducing memory usage.
- Implement session token creation and verification for better session management and security across the application.
- Enhance error handling and input validation in various API routes to ensure robustness and prevent potential issues.
This commit is contained in:
2026-01-11 22:44:26 +01:00
parent 9cc03bc475
commit 9072faae43
28 changed files with 433 additions and 288 deletions

View File

@@ -72,15 +72,16 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
try {
setLoading(true);
setError(null);
const sessionToken = sessionStorage.getItem('admin_session_token') || '';
// Add cache-busting parameter to ensure fresh data after reset
const cacheBust = `?nocache=true&t=${Date.now()}`;
const [analyticsRes, performanceRes] = await Promise.all([
fetch(`/api/analytics/dashboard${cacheBust}`, {
headers: { 'x-admin-request': 'true' }
headers: { 'x-admin-request': 'true', 'x-session-token': sessionToken }
}),
fetch(`/api/analytics/performance${cacheBust}`, {
headers: { 'x-admin-request': 'true' }
headers: { 'x-admin-request': 'true', 'x-session-token': sessionToken }
})
]);
@@ -128,11 +129,13 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
setResetting(true);
setError(null);
try {
const sessionToken = sessionStorage.getItem('admin_session_token') || '';
const response = await fetch('/api/analytics/reset', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-admin-request': 'true'
'x-admin-request': 'true',
'x-session-token': sessionToken
},
body: JSON.stringify({ type: resetType })
});

View File

@@ -189,6 +189,7 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
// Track scroll depth
let maxScrollDepth = 0;
const firedScrollMilestones = new Set<number>();
const handleScroll = () => {
try {
if (typeof window === 'undefined' || typeof document === 'undefined') return;
@@ -202,18 +203,14 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
(window.scrollY / (scrollHeight - innerHeight)) * 100
);
if (scrollDepth > maxScrollDepth) {
maxScrollDepth = scrollDepth;
// Track scroll milestones
if (scrollDepth >= 25 && scrollDepth < 50 && maxScrollDepth >= 25) {
trackEvent('scroll-depth', { depth: 25, url: window.location.pathname });
} else if (scrollDepth >= 50 && scrollDepth < 75 && maxScrollDepth >= 50) {
trackEvent('scroll-depth', { depth: 50, url: window.location.pathname });
} else if (scrollDepth >= 75 && scrollDepth < 90 && maxScrollDepth >= 75) {
trackEvent('scroll-depth', { depth: 75, url: window.location.pathname });
} else if (scrollDepth >= 90 && maxScrollDepth >= 90) {
trackEvent('scroll-depth', { depth: 90, url: window.location.pathname });
if (scrollDepth > maxScrollDepth) maxScrollDepth = scrollDepth;
// Track each milestone once (avoid spamming events on every scroll tick)
const milestones = [25, 50, 75, 90];
for (const milestone of milestones) {
if (maxScrollDepth >= milestone && !firedScrollMilestones.has(milestone)) {
firedScrollMilestones.add(milestone);
trackEvent('scroll-depth', { depth: milestone, url: window.location.pathname });
}
}
} catch (error) {

View File

@@ -42,9 +42,11 @@ export const EmailManager: React.FC = () => {
const loadMessages = async () => {
try {
setIsLoading(true);
const sessionToken = sessionStorage.getItem('admin_session_token') || '';
const response = await fetch('/api/contacts', {
headers: {
'x-admin-request': 'true'
'x-admin-request': 'true',
'x-session-token': sessionToken
}
});
@@ -100,10 +102,13 @@ export const EmailManager: React.FC = () => {
if (!selectedMessage || !replyContent.trim()) return;
try {
const sessionToken = sessionStorage.getItem('admin_session_token') || '';
const response = await fetch('/api/email/respond', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-admin-request': 'true',
'x-session-token': sessionToken,
},
body: JSON.stringify({
to: selectedMessage.email,

View File

@@ -23,7 +23,13 @@ export default function ImportExport() {
const handleExport = async () => {
setIsExporting(true);
try {
const response = await fetch('/api/projects/export');
const sessionToken = sessionStorage.getItem('admin_session_token') || '';
const response = await fetch('/api/projects/export', {
headers: {
'x-admin-request': 'true',
'x-session-token': sessionToken,
}
});
if (!response.ok) throw new Error('Export failed');
const blob = await response.blob();
@@ -63,9 +69,14 @@ export default function ImportExport() {
const text = await file.text();
const data = JSON.parse(text);
const sessionToken = sessionStorage.getItem('admin_session_token') || '';
const response = await fetch('/api/projects/import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
'x-admin-request': 'true',
'x-session-token': sessionToken,
},
body: JSON.stringify(data)
});

View File

@@ -17,10 +17,24 @@ import {
X
} from 'lucide-react';
import Link from 'next/link';
import { EmailManager } from './EmailManager';
import { AnalyticsDashboard } from './AnalyticsDashboard';
import ImportExport from './ImportExport';
import { ProjectManager } from './ProjectManager';
import dynamic from 'next/dynamic';
const EmailManager = dynamic(
() => import('./EmailManager').then((m) => m.EmailManager),
{ ssr: false, loading: () => <div className="p-6 text-stone-500">Loading emails</div> }
);
const AnalyticsDashboard = dynamic(
() => import('./AnalyticsDashboard').then((m) => m.default),
{ ssr: false, loading: () => <div className="p-6 text-stone-500">Loading analytics</div> }
);
const ImportExport = dynamic(
() => import('./ImportExport').then((m) => m.default),
{ ssr: false, loading: () => <div className="p-6 text-stone-500">Loading tools</div> }
);
const ProjectManager = dynamic(
() => import('./ProjectManager').then((m) => m.ProjectManager),
{ ssr: false, loading: () => <div className="p-6 text-stone-500">Loading projects</div> }
);
interface Project {
id: string;
@@ -178,9 +192,24 @@ const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthentic
};
useEffect(() => {
// Load all data (authentication disabled)
loadAllData();
}, [loadAllData]);
// Prioritize the data needed for the initial dashboard render
void (async () => {
await Promise.all([loadProjects(), loadSystemStats()]);
const idle = (cb: () => void) => {
if (typeof window !== 'undefined' && 'requestIdleCallback' in window) {
(window as unknown as { requestIdleCallback: (fn: () => void) => void }).requestIdleCallback(cb);
} else {
setTimeout(cb, 300);
}
};
idle(() => {
void loadAnalytics();
void loadEmails();
});
})();
}, [loadProjects, loadSystemStats, loadAnalytics, loadEmails]);
const navigation = [
{ id: 'overview', label: 'Dashboard', icon: Home, color: 'blue', description: 'Overview & Statistics' },

View File

@@ -80,10 +80,12 @@ export const ProjectManager: React.FC<ProjectManagerProps> = ({
if (!confirm('Are you sure you want to delete this project?')) return;
try {
const sessionToken = sessionStorage.getItem('admin_session_token') || '';
await fetch(`/api/projects/${projectId}`, {
method: 'DELETE',
headers: {
'x-admin-request': 'true'
'x-admin-request': 'true',
'x-session-token': sessionToken
}
});
onProjectsChange();