diff --git a/next.config.ts b/next.config.ts
index 0bd2f11..9ae23a8 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -7,6 +7,9 @@ const nextConfig: NextConfig = {
ignoreBuildErrors: true,
},
reactStrictMode: false,
+ allowedDevOrigins: [
+ 'preview-chat-7eeacdfa-df81-4042-9a8e-7ddb7becdce4.space-z.ai',
+ ],
};
export default nextConfig;
diff --git a/public/sw.js b/public/sw.js
index fe8a2a6..ade69d1 100644
--- a/public/sw.js
+++ b/public/sw.js
@@ -1,8 +1,8 @@
// CompetenceTrack — Service Worker for PWA Offline Support
-const CACHE_NAME = 'competencetrack-v1';
-const STATIC_CACHE = 'competencetrack-static-v1';
-const DATA_CACHE = 'competencetrack-data-v1';
-const OFFLINE_CACHE = 'competencetrack-offline-v1';
+const CACHE_NAME = 'competencetrack-v2';
+const STATIC_CACHE = 'competencetrack-static-v2';
+const DATA_CACHE = 'competencetrack-data-v2';
+const OFFLINE_CACHE = 'competencetrack-offline-v2';
// Static assets to cache on install
const STATIC_ASSETS = [
@@ -81,9 +81,16 @@ self.addEventListener('fetch', (event) => {
}
// 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 (
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('.jpg') ||
url.pathname.endsWith('.svg') ||
diff --git a/src/app/api/auth/route.ts b/src/app/api/auth/route.ts
index c52938f..4fa7708 100644
--- a/src/app/api/auth/route.ts
+++ b/src/app/api/auth/route.ts
@@ -41,7 +41,7 @@ export const POST = withRateLimit(async function POST(request: Request) {
const { email, password } = parsed.data;
- const user = await db.user.findUnique({
+ const user = await db.user.findFirst({
where: { email, deletedAt: null },
});
diff --git a/src/components/app-layout.tsx b/src/components/app-layout.tsx
index 3acbe3f..64094c1 100644
--- a/src/components/app-layout.tsx
+++ b/src/components/app-layout.tsx
@@ -153,6 +153,7 @@ import ResourceLibraryView from './resource-library-view';
// Dynamic imports for heavy components with loading skeletons
const AnalyticsView = dynamic(() => import('./analytics-view'), {
+ ssr: false,
loading: () =>
,
});
diff --git a/src/components/dashboard-view.tsx b/src/components/dashboard-view.tsx
index 043c11a..6fee8db 100644
--- a/src/components/dashboard-view.tsx
+++ b/src/components/dashboard-view.tsx
@@ -101,6 +101,7 @@ import { useAppStore, type ViewName, type CurrentUser } from '@/lib/store';
import { t } from '@/lib/i18n';
import { fetchDashboard, type DashboardData, getStoredNotifications, type AppNotification, addNotification, markNotificationsRead, fetchParentLinks, type ParentStudentLinkData, fetchStudents } from '@/lib/api';
import { apiGet, apiPost, apiPut } from '@/lib/api';
+import { sanitizeHtml } from '@/lib/utils';
import { fetchNewsletters, createNewsletter, type NewsletterData } from '@/lib/api';
const containerVariants = {
@@ -2645,7 +2646,7 @@ function DashboardNewsletterCard({ currentUser, locale }: { currentUser: Current
)}
-
+
{viewingNewsletter.tags && (() => {
try {
const parsed = JSON.parse(viewingNewsletter.tags);
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
index 390c12a..39935c7 100644
--- a/src/lib/auth.ts
+++ b/src/lib/auth.ts
@@ -53,7 +53,7 @@ export async function getSession(): Promise<{
if (!userId) return null;
- const user = await db.user.findUnique({
+ const user = await db.user.findFirst({
where: { id: userId, deletedAt: null },
select: sessionUserSelect,
});
diff --git a/src/lib/rate-limit.ts b/src/lib/rate-limit.ts
index 653d350..2d44eb2 100644
--- a/src/lib/rate-limit.ts
+++ b/src/lib/rate-limit.ts
@@ -23,8 +23,8 @@ setInterval(() => {
// ─── Preset Limits ────────────────────────────────────────────────────
export const RATE_LIMITS = {
- // Auth endpoints: 5 requests per minute (strict)
- auth: { limit: 5, windowMs: 60 * 1000 },
+ // Auth endpoints: 20 requests per minute (allows retries & multiple tabs)
+ auth: { limit: 20, windowMs: 60 * 1000 },
// Data read endpoints: 60 requests per minute (generous)
dataRead: { limit: 60, windowMs: 60 * 1000 },
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index bd0c391..b9f8576 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -4,3 +4,24 @@ import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
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