feat: auth, persistence, and AI integration

- Cookie-based auth via backend proxy (httpOnly JWTs)
- Supabase Postgres persistence for sessions/messages/profiles + RLS
- Fix cross-user session leak (token cache keyed by full token, not 50-char prefix)
- Fix missing table grants (42501) via migration; auto-provision profiles on user creation
- Chat validation, ownership checks, rate limiting, /api/chat/sessions route ordering
- Frontend auth-state reset + credentials include
- OpenRouter AI provider (OpenAI-compatible base URL, reasoning disabled)
- Tests: chatValidation, appState
This commit is contained in:
2026-08-19 09:41:40 -04:00
parent 93a35a7343
commit ff9da4d324
43 changed files with 4174 additions and 249 deletions
+39 -33
View File
@@ -5,7 +5,7 @@
import { useState, useRef, useEffect } from "react";
import "./TopNav.css";
const TopNav = ({ activeGrade, onGradeChange, activeSubject, onSubjectChange, activeChapter, onChapterChange }) => {
const TopNav = ({ subjectItems, chapterData, activeGrade, onGradeChange, activeSubject, onSubjectChange, activeChapter, onChapterChange, user, onSignOut }) => {
const [openDropdown, setOpenDropdown] = useState(null);
const gradeRef = useRef(null);
const subjectRef = useRef(null);
@@ -54,25 +54,9 @@ const TopNav = ({ activeGrade, onGradeChange, activeSubject, onSubjectChange, ac
{ id: "Grade 12", label: "Grade 12" },
];
const subjects = [
{ id: "choose-subject", label: "Choose Subject" },
{ id: "math", label: "Mathematics" },
{ id: "science", label: "Science" },
{ id: "history", label: "History" },
{ id: "language", label: "Language Arts" },
{ id: "english", label: "English" },
{ id: "geography", label: "Geography" },
];
const chapters = [
{ id: "choose-chapter", label: "Choose Chapter" },
{ id: "chapter1", label: "Chapter 1: Rational Numbers" },
{ id: "chapter2", label: "Chapter 2: Linear Equations" },
{ id: "chapter3", label: "Chapter 3: Coordinate Geometry" },
{ id: "chapter4", label: "Chapter 4: Life Processes" },
{ id: "chapter5", label: "Chapter 5: Photosynthesis" },
{ id: "chapter6", label: "Chapter 6: Human Physiology" },
];
// Chapters are now generated dynamically from chapterData (see chapterOptions above)
// Subjects now come from subjectItems prop
// Static list removed — was not subject-aware.
const subjectIcons = {
math: "calculate",
@@ -83,6 +67,25 @@ const TopNav = ({ activeGrade, onGradeChange, activeSubject, onSubjectChange, ac
geography: "public",
};
/* ─── Resolve subject display (ID → label) ─── */
const resolvedSubjectLabel = activeSubject === "choose-subject"
? "Choose Subject"
: subjectItems?.find((s) => s.id === activeSubject)?.label || activeSubject || "Choose Subject";
/* ─── Resolve chapter pill label ─── */
const resolvedChapterLabel = activeChapter === "choose-chapter" || !activeChapter
? "Choose Chapter"
: activeChapter;
/* ─── Subject-aware chapter options (from chapterData prop) ─── */
const chapterOptions = (() => {
const chapters = chapterData?.[activeSubject] || [];
return [
{ id: "choose-chapter", label: "Choose Chapter" },
...chapters,
];
})();
return (
<header className="topnav">
<div className="topnav__selectors">
@@ -125,16 +128,12 @@ const TopNav = ({ activeGrade, onGradeChange, activeSubject, onSubjectChange, ac
<span className={`material-symbols-outlined topnav__pill-icon`}>
{activeSubject === "choose-subject" ? "science" : subjectIcons[activeSubject] || "science"}
</span>
<span className="topnav__pill-label">
{activeSubject === "choose-subject"
? "Choose Subject"
: subjects.find((s) => s.id === activeSubject)?.label || activeSubject}
</span>
<span className="topnav__pill-label">{resolvedSubjectLabel}</span>
<span className="material-symbols-outlined topnav__pill-arrow">expand_more</span>
</button>
{openDropdown === "subject" && (
<ul className="topnav__dropdown">
{subjects.map((subject) => (
{subjectItems?.map((subject) => (
<li key={subject.id}>
<button
className={`topnav__dropdown-item ${activeSubject === subject.id ? "topnav__dropdown-item--active" : ""}`}
@@ -160,15 +159,15 @@ const TopNav = ({ activeGrade, onGradeChange, activeSubject, onSubjectChange, ac
aria-expanded={openDropdown === "chapter"}
>
<span className="material-symbols-outlined topnav__pill-icon">menu_book</span>
<span className="topnav__pill-label">{activeChapter}</span>
<span className="topnav__pill-label">{resolvedChapterLabel}</span>
<span className="material-symbols-outlined topnav__pill-arrow">expand_more</span>
</button>
{openDropdown === "chapter" && (
<ul className="topnav__dropdown">
{chapters.map((chapter) => (
{chapterOptions.map((chapter) => (
<li key={chapter.id}>
<button
className={`topnav__dropdown-item ${activeChapter === chapter.label ? "topnav__dropdown-item--active" : ""}`}
className={`topnav__dropdown-item ${(activeChapter === chapter.label || activeChapter === "choose-chapter") ? "topnav__dropdown-item--active" : ""}`}
onClick={() => selectOption("chapter", chapter.label)}
>
{chapter.label}
@@ -191,10 +190,17 @@ const TopNav = ({ activeGrade, onGradeChange, activeSubject, onSubjectChange, ac
<span className="material-symbols-outlined">notifications_none</span>
</button>
{/* User Avatar */}
<div className="topnav__avatar">
<span className="topnav__avatar-initial">S</span>
</div>
{/* User Avatar — Sign out on click */}
<button
className="topnav__avatar-btn"
onClick={onSignOut}
aria-label="Sign out"
title="Sign out"
>
<span className="topnav__avatar-initial">
{user?.email?.charAt(0).toUpperCase() || "U"}
</span>
</button>
</div>
</header>
);