Fix: Auth 500 error, script tag warning, dynamic import, rate limit
This commit is contained in:
1 parent
8507984c7e
commit
55c916a9e9
8 files changed
+43
-10
No files matched your search
@@ -7,6 +7,9 @@ const nextConfig: NextConfig = {
|
|||||||
ignoreBuildErrors: true,
|
ignoreBuildErrors: true,
|
||||||
},
|
},
|
||||||
reactStrictMode: false,
|
reactStrictMode: false,
|
||||||
|
allowedDevOrigins: [
|
||||||
|
'preview-chat-7eeacdfa-df81-4042-9a8e-7ddb7becdce4.space-z.ai',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
+12
-5
@@ -1,8 +1,8 @@
|
|||||||
// CompetenceTrack — Service Worker for PWA Offline Support
|
// CompetenceTrack — Service Worker for PWA Offline Support
|
||||||
const CACHE_NAME = 'competencetrack-v1';
|
const CACHE_NAME = 'competencetrack-v2';
|
||||||
const STATIC_CACHE = 'competencetrack-static-v1';
|
const STATIC_CACHE = 'competencetrack-static-v2';
|
||||||
const DATA_CACHE = 'competencetrack-data-v1';
|
const DATA_CACHE = 'competencetrack-data-v2';
|
||||||
const OFFLINE_CACHE = 'competencetrack-offline-v1';
|
const OFFLINE_CACHE = 'competencetrack-offline-v2';
|
||||||
|
|
||||||
// Static assets to cache on install
|
// Static assets to cache on install
|
||||||
const STATIC_ASSETS = [
|
const STATIC_ASSETS = [
|
||||||
@@ -81,9 +81,16 @@ self.addEventListener('fetch', (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Static assets (CSS, JS, images) — CacheFirst strategy
|
// Static assets (CSS, JS, images) — CacheFirst strategy
|
||||||
|
// But use NetworkFirst for JS/CSS from Next.js dev server (_next/) to avoid stale module factories
|
||||||
if (
|
if (
|
||||||
url.pathname.endsWith('.css') ||
|
url.pathname.endsWith('.css') ||
|
||||||
url.pathname.endsWith('.js') ||
|
url.pathname.endsWith('.js')
|
||||||
|
) {
|
||||||
|
// Always use NetworkFirst for JS/CSS to avoid stale Turbopack chunks
|
||||||
|
event.respondWith(networkFirstStrategy(request, STATIC_CACHE));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
url.pathname.endsWith('.png') ||
|
url.pathname.endsWith('.png') ||
|
||||||
url.pathname.endsWith('.jpg') ||
|
url.pathname.endsWith('.jpg') ||
|
||||||
url.pathname.endsWith('.svg') ||
|
url.pathname.endsWith('.svg') ||
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const POST = withRateLimit(async function POST(request: Request) {
|
|||||||
|
|
||||||
const { email, password } = parsed.data;
|
const { email, password } = parsed.data;
|
||||||
|
|
||||||
const user = await db.user.findUnique({
|
const user = await db.user.findFirst({
|
||||||
where: { email, deletedAt: null },
|
where: { email, deletedAt: null },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ import ResourceLibraryView from './resource-library-view';
|
|||||||
|
|
||||||
// Dynamic imports for heavy components with loading skeletons
|
// Dynamic imports for heavy components with loading skeletons
|
||||||
const AnalyticsView = dynamic(() => import('./analytics-view'), {
|
const AnalyticsView = dynamic(() => import('./analytics-view'), {
|
||||||
|
ssr: false,
|
||||||
loading: () => <div className="space-y-4 p-4"><Skeleton className="h-8 w-48" /><div className="grid grid-cols-1 md:grid-cols-3 gap-4">{[1,2,3].map(i => <Skeleton key={i} className="h-32" />)}</div><Skeleton className="h-64" /></div>,
|
loading: () => <div className="space-y-4 p-4"><Skeleton className="h-8 w-48" /><div className="grid grid-cols-1 md:grid-cols-3 gap-4">{[1,2,3].map(i => <Skeleton key={i} className="h-32" />)}</div><Skeleton className="h-64" /></div>,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ import { useAppStore, type ViewName, type CurrentUser } from '@/lib/store';
|
|||||||
import { t } from '@/lib/i18n';
|
import { t } from '@/lib/i18n';
|
||||||
import { fetchDashboard, type DashboardData, getStoredNotifications, type AppNotification, addNotification, markNotificationsRead, fetchParentLinks, type ParentStudentLinkData, fetchStudents } from '@/lib/api';
|
import { fetchDashboard, type DashboardData, getStoredNotifications, type AppNotification, addNotification, markNotificationsRead, fetchParentLinks, type ParentStudentLinkData, fetchStudents } from '@/lib/api';
|
||||||
import { apiGet, apiPost, apiPut } from '@/lib/api';
|
import { apiGet, apiPost, apiPut } from '@/lib/api';
|
||||||
|
import { sanitizeHtml } from '@/lib/utils';
|
||||||
import { fetchNewsletters, createNewsletter, type NewsletterData } from '@/lib/api';
|
import { fetchNewsletters, createNewsletter, type NewsletterData } from '@/lib/api';
|
||||||
|
|
||||||
const containerVariants = {
|
const containerVariants = {
|
||||||
@@ -2645,7 +2646,7 @@ function DashboardNewsletterCard({ currentUser, locale }: { currentUser: Current
|
|||||||
<img src={viewingNewsletter.imageUrl} alt="" className="w-full h-48 object-cover" />
|
<img src={viewingNewsletter.imageUrl} alt="" className="w-full h-48 object-cover" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="prose prose-sm dark:prose-invert max-w-none" dangerouslySetInnerHTML={{ __html: viewingNewsletter.content }} />
|
<div className="prose prose-sm dark:prose-invert max-w-none" dangerouslySetInnerHTML={{ __html: sanitizeHtml(viewingNewsletter.content) }} />
|
||||||
{viewingNewsletter.tags && (() => {
|
{viewingNewsletter.tags && (() => {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(viewingNewsletter.tags);
|
const parsed = JSON.parse(viewingNewsletter.tags);
|
||||||
|
|||||||
+1
-1
@@ -53,7 +53,7 @@ export async function getSession(): Promise<{
|
|||||||
|
|
||||||
if (!userId) return null;
|
if (!userId) return null;
|
||||||
|
|
||||||
const user = await db.user.findUnique({
|
const user = await db.user.findFirst({
|
||||||
where: { id: userId, deletedAt: null },
|
where: { id: userId, deletedAt: null },
|
||||||
select: sessionUserSelect,
|
select: sessionUserSelect,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ setInterval(() => {
|
|||||||
// ─── Preset Limits ────────────────────────────────────────────────────
|
// ─── Preset Limits ────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const RATE_LIMITS = {
|
export const RATE_LIMITS = {
|
||||||
// Auth endpoints: 5 requests per minute (strict)
|
// Auth endpoints: 20 requests per minute (allows retries & multiple tabs)
|
||||||
auth: { limit: 5, windowMs: 60 * 1000 },
|
auth: { limit: 20, windowMs: 60 * 1000 },
|
||||||
|
|
||||||
// Data read endpoints: 60 requests per minute (generous)
|
// Data read endpoints: 60 requests per minute (generous)
|
||||||
dataRead: { limit: 60, windowMs: 60 * 1000 },
|
dataRead: { limit: 60, windowMs: 60 * 1000 },
|
||||||
|
|||||||
@@ -4,3 +4,24 @@ import { twMerge } from "tailwind-merge"
|
|||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize HTML content by removing script tags and dangerous event handlers.
|
||||||
|
* This prevents React's "Encountered a script tag" warning and XSS attacks
|
||||||
|
* when using dangerouslySetInnerHTML.
|
||||||
|
*/
|
||||||
|
export function sanitizeHtml(html: string): string {
|
||||||
|
if (!html) return '';
|
||||||
|
return html
|
||||||
|
// Remove <script> tags and their content
|
||||||
|
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
||||||
|
// Remove on* event handler attributes (onclick, onload, onerror, etc.)
|
||||||
|
.replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, '')
|
||||||
|
// Remove javascript: URLs
|
||||||
|
.replace(/href\s*=\s*(?:"javascript:[^"]*"|'javascript:[^']*')/gi, 'href="#"')
|
||||||
|
// Remove <iframe> tags
|
||||||
|
.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '')
|
||||||
|
// Remove <object> and <embed> tags
|
||||||
|
.replace(/<object\b[^<]*(?:(?!<\/object>)<[^<]*)*<\/object>/gi, '')
|
||||||
|
.replace(/<embed\b[^>]*>/gi, '');
|
||||||
|
}
|
||||||
Reference in new issue
Block a user