From 9c45ff5b2fb39510d0bd633382c18a156783d752 Mon Sep 17 00:00:00 2001 From: Leothosine Date: Sun, 26 Apr 2026 01:11:21 +0100 Subject: [PATCH] feat: optimize context, add auth middleware, consolidate components, add query keys - Memoize ToastContext provider value with useMemo to prevent unnecessary re-renders (closes #162) - Add requireAuth middleware helper for consistent API route authentication (closes #163) - Add src/components/index.ts barrel as canonical component export point (closes #171) - Add queryKeys factory for consistent cache keys across all API calls (closes #174) --- src/components/index.ts | 9 +++++++++ src/context/ToastContext.tsx | 9 +++++++-- src/lib/authMiddleware.ts | 20 ++++++++++++++++++++ src/lib/queryKeys.ts | 29 +++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 src/components/index.ts create mode 100644 src/lib/authMiddleware.ts create mode 100644 src/lib/queryKeys.ts diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 00000000..1430e5b1 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,9 @@ +/** + * Canonical component barrel — all shared UI components are exported from src/components. + * App-specific components in src/app/components should be migrated here over time. + * Import from '@/components' rather than '@/app/components' for shared pieces. + */ + +export * from './ui/Toast'; +export * from './shared/EnvGuard'; +export * from './errors/ErrorBoundarySystem'; diff --git a/src/context/ToastContext.tsx b/src/context/ToastContext.tsx index 2ca7aa1b..354a62fa 100644 --- a/src/context/ToastContext.tsx +++ b/src/context/ToastContext.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react'; +import React, { createContext, useContext, useState, useCallback, useMemo, ReactNode } from 'react'; import { Toast, ToastType } from '@/components/ui/Toast'; export interface ToastMessage { @@ -55,8 +55,13 @@ export function ToastProvider({ children }: { children: ReactNode }) { [addToast], ); + const value = useMemo( + () => ({ toasts, addToast, removeToast, error, success, info }), + [toasts, addToast, removeToast, error, success, info], + ); + return ( - + {children}
{toasts.map((toast) => ( diff --git a/src/lib/authMiddleware.ts b/src/lib/authMiddleware.ts new file mode 100644 index 00000000..cdce28c9 --- /dev/null +++ b/src/lib/authMiddleware.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from 'next/server'; + +/** + * Validates the Authorization header and returns a 401 response if missing or invalid. + * Usage: const unauth = requireAuth(request); if (unauth) return unauth; + */ +export function requireAuth(request: NextRequest): NextResponse | null { + const authHeader = request.headers.get('authorization'); + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); + } + + const token = authHeader.slice(7); + if (!token) { + return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); + } + + return null; +} diff --git a/src/lib/queryKeys.ts b/src/lib/queryKeys.ts new file mode 100644 index 00000000..549eb74e --- /dev/null +++ b/src/lib/queryKeys.ts @@ -0,0 +1,29 @@ +/** + * Query key factory — provides consistent, type-safe cache keys for all API calls. + * Use these as the first argument to any data-fetching hook or cache invalidation call. + */ +export const queryKeys = { + courses: { + all: ['courses'] as const, + list: (params?: { cursor?: string; limit?: number }) => + ['courses', 'list', params] as const, + detail: (id: string) => ['courses', id] as const, + lessons: (courseId: string) => ['courses', courseId, 'lessons'] as const, + }, + user: { + all: ['user'] as const, + progress: () => ['user', 'progress'] as const, + }, + search: { + all: ['search'] as const, + results: (query: string) => ['search', query] as const, + }, + bookmarks: { + all: ['bookmarks'] as const, + byLesson: (lessonId: string) => ['bookmarks', lessonId] as const, + }, + notes: { + all: ['notes'] as const, + byLesson: (lessonId: string) => ['notes', lessonId] as const, + }, +} as const;