Skip to content

feat(parent): implement complete Parent Portal#273

Open
WissaalElakebii wants to merge 1 commit into
Open-TutorAi:mainfrom
WissaalElakebii:feature/parent-portal-clean
Open

feat(parent): implement complete Parent Portal#273
WissaalElakebii wants to merge 1 commit into
Open-TutorAi:mainfrom
WissaalElakebii:feature/parent-portal-clean

Conversation

@WissaalElakebii

Copy link
Copy Markdown

Summary

This PR implements the complete Parent Portal.

Features

  • Parent dashboard with real support data
  • Parent support creation workflow
  • Parent AI chat
  • Parent notifications
  • Parent evaluations
  • Parent sessions
  • Parent/student linking
  • Backend API endpoints
  • Frontend pages and components
  • Evaluation metrics integration

- Parent dashboard with real supports from DB, status management
- 6-step support creation form with email-based child linking
- AI chat sessions within parent space (no redirect to student)
- Auto-linking chat_id to support via POST /chats/new for parent role
- AI-generated QCM evaluations using Ollama gemma3:4b (2-step generation)
- Real sessions view with metrics calculated from chat messages
- Dynamic progress tracking updated every 15s and on each AI response
@Dakir-Ai Dakir-Ai self-requested a review June 27, 2026 08:31
@OumaLam

OumaLam commented Jun 27, 2026

Copy link
Copy Markdown

Hi, @WissaalElakebii

Security review — Parent portal feature

The feature direction is solid, but several issues must be fixed before this can ship to users


🔴 Critical (2) — Must fix before merge

🔴 [CRITICAL — Blocking] Multiple frontend files — hardcoded student_id UUID

The string 'e7081ab6-fce1-4111-ae63-74c0e6ae46b6' appears as a hardcoded default in at least 4 frontend files:

  • ParentDashboard.svelte: studentId = localStorage.getItem('parent_student_id') ?? 'e7081ab6-...'
  • ParentSessions.svelte: same pattern
  • ParentEvaluations.svelte: same pattern
  • /parent/c/[id]/+page.svelte: same pattern

This hardcoded UUID is a real student's ID committed to the public repository. Any visitor can use it to query /parent/supports/list/{id}, /parent/sessions-real/{id}, /parent/evaluations/by-student/{id} — exposing that student's AI sessions, learning data, and evaluations without authentication if the backend IDOR isn't airtight.

Required before merge:

  • Remove all hardcoded UUIDs immediately.
  • parent_student_id must be loaded from a server-side session or the parent's profile API, never defaulted to a literal UUID.
  • Rotate/invalidate the exposed student ID if this is production data.

🔴 [CRITICAL — Blocking] parent_dashboard.py — Full AI chat content accessible to parent

GET /parent/sessions-real/{student_id} and POST /parent/evaluations/generate return the full content of the student's AI conversations (questions asked, AI responses, up to 120 chars of each user message) to the parent, with no mechanism for the student to opt out or restrict access.

For minors, GDPR (Art. 8) and equivalent regulations require that children's personal data — including learning conversations — is processed with appropriate safeguards. Exposing a teenager's private AI tutoring session to a parent without any consent mechanism or age-based policy is a legal and ethical risk.

Required before merge:

  • Define a clear data-sharing policy: what parents can see, what is private to the student.
  • At minimum, truncate or summarize AI session content rather than returning raw messages.
  • Consider a student-facing consent toggle for "share sessions with parent".
  • Add a legal/privacy review before shipping this feature.

🟠 High (5)

🟠 [HIGH] parent_evaluations.py — Ollama called via plain HTTP with no auth

generate_qcm_with_ai() calls http://localhost:11434 (Ollama) with no authentication, no TLS, and a hardcoded URL in source code. If the service is ever exposed on a non-loopback interface (misconfigured Docker, cloud VM), any network-adjacent attacker can query or control the LLM.

Fix:

  • Move ollama_url to config/env (OLLAMA_BASE_URL).
  • Add an optional bearer token for Ollama auth.
  • Validate the URL scheme is http://localhost or a known-safe internal host at startup.

🟠 [HIGH] parent_supports.py/parent/supports/upload-file does not verify support ownership

The upload endpoint verifies the parent↔student link via assert_owns_student, but does not verify that support_id belongs to student_id. A parent linked to student A can upload files to a support belonging to student B by supplying student A's ID and student B's support ID.

Fix: Before calling svc.upload_file(), query the support and assert support.user_id == student_id.

🟠 [HIGH] chats.pycreate_chat links chat to support without ownership check

The new code in create_chat checks support.user_id == current_user.id OR current_user.role in ("parent", "admin"). Any parent can supply an arbitrary support_id and link their newly created chat to it — including supports belonging to students they are not linked to.

Fix: For parents, also verify via ParentService.assert_owns_student(current_user.id, support.user_id) before setting support.chat_id.

🟠 [HIGH] parent_dashboard.py/parent/dashboard returns hardcoded fake metrics

The dashboard response contains fabricated KPIs (score_moyen: 78, temps_etude_heures: 14, sessions_ia: 23, etc.) and hardcoded "recent activity" entries with static dates. Parents are making decisions about their child's education based on data that is entirely fictional.

This is a product integrity issue with potential legal implications (misleading parents about their child's actual performance).

Required before merge: Either compute real metrics from the database or clearly label the dashboard as "demo mode". Do not ship fake data as if it were real.

🟠 [HIGH] parent_dashboard.py — Notifications are hardcoded; PATCH /lire is a no-op

get_notifications returns a static hardcoded list. marquer_lue returns {"id": notif_id, "lu": True} without writing anything to the database. Notification state is lost on every page reload.

Fix: Persist notifications in a notifications table (or an equivalent store). The PATCH endpoint must update the database record.

🟡 Medium (4)

🟡 [MEDIUM] /parent/evaluation/[id]/+page.svelte — LLM output rendered in DOM without sanitization

QCM question text, answer choices, and explanations come directly from the Ollama LLM and are rendered in the DOM via Svelte's default text interpolation (safe by default). However, explanation text is also used as a fallback in questions.append({..., "explanation": fact[:150]}) and could contain raw chat content.

If any developer adds {@html} for Markdown rendering of explanations (a natural next step), the XSS vector is immediate.

Fix: Add a lint rule forbidding {@html} on any LLM-sourced field. Add a server-side max-length and charset validation on all questions fields before persistence.

🟡 [MEDIUM] parent_dashboard.py — Support auto-marked completed after 10 AI messages

get_support_progress computes progress = min(ai_message_count * 10, 100) and writes support.status = "completed" to the database when progress hits 100%. This side-effect happens on every GET request and uses a completely arbitrary threshold.

A student who has 10 short back-and-forth messages gets their support marked complete, even if they haven't finished learning. This is a write operation on a GET endpoint (violates HTTP semantics) and could close active learning paths unexpectedly.

Fix: Remove the status write from the GET endpoint. Status transitions should be explicit actions.

🟡 [MEDIUM] ParentNotifications.svelte — Notification preference toggles are visual only

The "Notification preferences" section renders toggle switches that look interactive but have no on:click handler and no backing API call. A parent who disables "Alert notifications" will still receive them — the toggle is purely decorative.

Fix: Either wire the toggles to a real preferences API, or remove the section entirely until it's implemented. Shipping fake interactive controls erodes user trust.

🟡 [MEDIUM — GDPR] Frontend parent pages — console.log statements expose student data

Multiple console.log calls in the new parent pages expose sensitive data in the browser console:

  • console.log('[Parent] chatCreated event reçu:', { chatId, success })
  • console.log('[Parent] pendingSupportData:', pendingRaw)
  • console.log('[Parent] Liaison chat ... → soutien ...')
  • console.error('Support lookup failed:', e)
  • console.error('Progress fetch failed:', e)

In production, these are readable by browser extensions and visible during screen sharing.

Fix: Remove or gate all console.log/error behind import.meta.env.DEV. Add the no-console ESLint rule.


@Oumaima-elkhoummassi

Copy link
Copy Markdown

Hi @WissaalElakebii
Two things are missing here before this can be reviewed:

  1. PR description: right now it's just a feature list. Could you add:

    • Why this is needed
    • How to test it (steps, by role)
    • A checklist (target branch, dependencies, testing done, code review)
    • A changelog entry (Added / Changed / Fixed)
  2. Documentation: there's no file in docs/ for the Parent Portal. Given
    the scope here (dashboard, support creation, AI chat, notifications,
    evaluations, sessions, parent/student linking), this likely needs more
    than one doc file - at least a technical doc and a user guide.

Could you complete both before this is ready for review?

@baaki-hicham baaki-hicham left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, thank you for your contribution.

Multiple blocking issues found during local testing
Tested locally with a fresh parent account. Screenshots attached.

1. Hardcoded student ID — entire portal broken for any real account

Found in both ParentEvaluations.svelte and parent/c/[id]/+page.svelte:

const studentId = localStorage.getItem('parent_student_id')
    ?? 'e7081ab6-fce1-4111-ae63-74c0e6ae46b6';

This UUID only exists in the author's local database. For any other parent account, assert_owns_student correctly rejects it → 403 Forbidden on every page that depends on studentId (Evaluations, AI Sessions, Dashboard, chat progress). Confirmed in the Network tab:

GET /api/v1/parent/evaluations/by-student/e7081ab6-... → 403 Forbidden
GET /api/v1/parent/sessions-real/e7081ab6-...           → 403 Forbidden

Suggestion: use the /parent/supports/find-student endpoint (already present in parent_supports.py) to resolve the linked student from the authenticated parent's account, instead of a hardcoded fallback.

2. Generated prompt is never passed to the chat

ParentSupportCreation.svelte builds a detailed prompt via generatePrompt() (subject, level, tutor style, learning objective…), but this prompt is never transmitted to <Chat />. The chat page instantiates the component with no context:

<Chat on:chatEvent={handleChatEvent} />

As a result, the AI starts from scratch and ignores everything the parent filled in the form — the core value proposition of this feature is broken.

Suggestion: pass the generated prompt as a prop to <Chat /> (e.g. initialPrompt or systemPrompt), or store it in a Svelte store/localStorage before navigating, and consume it on mount in the chat page.

3. Wrong redirect after support creation

After form submission, the code does:

goto('/student/supports');

This sends the parent into the student interface instead of /parent/chat. The parent loses the context of what they just created.

4. Fragile URL-polling hack in parent/c/[id]/+page.svelte

interceptInterval = setInterval(() => {
    if (path.startsWith('/student/c/')) {
        goto(`/parent/c/${id}`, { replaceState: true });
    }
}, 100);

setTimeout(() => clearInterval(interceptInterval), 30000);

Polling window.location.pathname every 100ms for 30 seconds to intercept a redirect is a fragile workaround. It indicates that <Chat /> internally forces navigation to /student/c/ after chat creation, and that the parent portal hasn't fixed this at the source. This should be addressed by either patching <Chat /> to accept a redirectTarget prop, or by not reusing the student <Chat /> component directly in the parent portal.

5. Branding inconsistency (non-blocking)

The sidebar displays a plain "OT" text badge instead of the actual Open TutorAI logo. Per the frontend charter, UI components must stay consistent with the project's established visual identity and not use placeholder branding.

6. Frontend design does not comply with the project's frontend charter

The entire Parent Portal uses inline styles exclusively (style="...") instead of Tailwind CSS utility classes, which are the standard throughout the rest of the codebase. This introduces several violations of the project's frontend charter.

a) Hardcoded hex colors outside the official palette

The project's design system is built on a single gray-50gray-950 scale defined in tailwind.config.js. The Parent Portal introduces at least 11 new arbitrary colors without a team design decision:

#2563EB (blue)   → buttons, card borders, spinner, active filters
#16A34A (green)  → "Actifs" stat card
#D97706 (orange) → "En attente" stat card
#7C3AED (violet) → "Terminés" stat card
#111827, #6B7280, #E5E7EB, #EFF6FF, #D1FAE5, #DBEAFE, #FEF3C7

Per the frontend charter:

"Introducing a new color outside the established gray palette requires a team design decision — it should not be done unilaterally in a component."

b) No Tailwind classes used

Every style in ParentDashboard.svelte and ParentLayout.svelte is written as inline style="...". This bypasses ESLint/Prettier checks, makes the code harder to maintain, and breaks consistency with the rest of the project where Tailwind utility classes are the standard.

c) No dark mode support

Since all styles are inline, there is no dark: variant anywhere in the Parent Portal. Switching to dark mode in the app will leave the entire portal with a broken white background and unreadable text, while the rest of the application adapts correctly.

d) Logo inconsistency

The sidebar shows a plain OT text badge (blue circle) instead of the actual Open TutorAI logo used elsewhere in the application. This creates a visual inconsistency between the Parent Portal and the rest of the product.

Suggestion: rewrite the Parent Portal components using Tailwind utility classes from the existing gray-* palette, add dark: variants, use the real logo asset, and open a separate discussion if new brand colors (e.g. a primary blue) are needed so the team can decide and add them to tailwind.config.js properly.

Summary: Issues 1–4 are blocking — the Parent Portal does not
function for any real account outside the author's local setup.
Issues 5–6 are non-blocking but should be addressed before merge
to stay consistent with the project's standards. Happy to help
review a follow-up once these are fixed! 🙌
Screenshot from 2026-06-30 20-37-05
Screenshot from 2026-06-30 22-54-29

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements a new “Parent Portal” vertical across the SvelteKit UI and FastAPI backend, adding parent-facing pages (dashboard, supports, AI sessions, notifications, evaluations) plus new API routes/models to support parent↔student workflows and evaluation generation.

Changes:

  • Added Parent Portal UI routes/layout and parent-specific pages/components for supports, sessions, notifications, evaluations, and chat.
  • Introduced new backend routers for parent supports, dashboard/notifications/sessions/progress, and AI-generated evaluations (with a new Evaluation ORM model).
  • Added parent↔student linking service/model and integrated parent routers into app registration.

Reviewed changes

Copilot reviewed 29 out of 30 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
ui/src/routes/parent/support/create/+page.svelte Route wrapper for parent support creation UI
ui/src/routes/parent/support/[id]/+page.svelte Parent support detail page (fetch + display)
ui/src/routes/parent/sessions/+page.svelte Route wrapper for parent sessions page
ui/src/routes/parent/notifications/+page.svelte Route wrapper for parent notifications page
ui/src/routes/parent/evaluations/+page.svelte Route wrapper for parent evaluations page
ui/src/routes/parent/evaluation/[id]/+page.svelte Parent evaluation detail/quiz + submission UI
ui/src/routes/parent/dashboard/+page.svelte Route wrapper for parent dashboard page
ui/src/routes/parent/chat/+page.svelte Parent chat launcher + chatCreated handling
ui/src/routes/parent/c/[id]/+page.svelte Parent chat view with support progress polling
ui/src/routes/parent/+page.svelte Parent landing page formatting tweaks
ui/src/routes/parent/+layout.svelte Parent portal layout (sidebar/header) + model loading
ui/src/lib/components/student/tutor/Chat.svelte Minor change (spacing)
ui/src/lib/components/parent/pages/ParentSessions.svelte Parent sessions UI fed by /parent/sessions-real
ui/src/lib/components/parent/pages/ParentNotifications.svelte Parent notifications UI + mark-as-read actions
ui/src/lib/components/parent/pages/ParentEvaluations.svelte Parent evaluations list + generate-eval modal/workflow
ui/src/lib/components/parent/pages/ParentDashboard.svelte Parent supports dashboard + start/view AI session actions
ui/src/lib/components/parent/elements/ParentSupportCreation.svelte Multi-step parent support creation form + uploads
ui/src/lib/components/parent/elements/ParentLayout.svelte Additional parent layout component (separate from route layout)
ui/src/lib/apis/parent/index.ts Added parent API client helpers (supports/notifications/link-chat/etc.)
learning/supports/service.py Sets support status active when chat_id is linked
gateway/http/routers/parent_supports.py New parent supports router (create/list/detail/upload/link-chat/find-student)
gateway/http/routers/parent_evaluations.py New evaluations router (generate/list/get/submit) + Ollama calls
gateway/http/routers/parent_dashboard.py New parent dashboard/notifications/sessions/progress endpoints
gateway/http/routers/chats.py Auto-link chat to support when support_id is provided
gateway/http/app.py Registers new parent routers under /api/v1
data/models/evaluation.py New Evaluation ORM model
data/models/init.py Registers ParentStudentLink and Evaluation models
accounts/parents/service.py New ParentService for parent↔student linkage and support creation delegation
accounts/parents/models.py New ParentStudentLink ORM model
accounts/parents/init.py Package init

Comment on lines +30 to +33
const res = await fetch(
`http://localhost:8080/api/v1/parent/supports/find-student?email=${encodeURIComponent(studentEmail.trim())}`,
{ headers: { authorization: `Bearer ${token}` } }
);
Comment on lines +162 to +165
$: canProceed =
currentStep === 0 ? true :
currentStep === 1 ? supportTitle.trim().length > 0 && (!!selectedSubject || customSubject.trim().length > 0) :
currentStep === 2 ? true :
Comment on lines +202 to +204
toast.success('Soutien créé avec succès ! 🎉');
goto('/student/supports');
} catch (err: any) {
Comment on lines +34 to +37
// Utiliser l'ID étudiant depuis localStorage
const studentId = localStorage.getItem('parent_student_id')
?? 'e7081ab6-fce1-4111-ae63-74c0e6ae46b6';

Comment on lines +27 to +30
const token = localStorage.getItem('token');
if (!token) { goto('/auth'); return; }
studentId = localStorage.getItem('parent_student_id') ?? 'e7081ab6-fce1-4111-ae63-74c0e6ae46b6';
studentName = localStorage.getItem('parent_student_name') ?? '';
Comment on lines +33 to +92
async def generate_qcm_with_ai(chat_content: str, subject: str, title: str) -> List[Dict]:
"""Génère 10 QCM en 2 étapes pour garantir la qualité."""

ollama_url = "http://localhost:11434"

# ── ÉTAPE 1 : Extraire les faits clés de la conversation ──────────────────
prompt_extract = f"""Lis cette conversation entre un élève et un tuteur IA sur le sujet: {subject} / {title}

CONVERSATION:
{chat_content[:3500]}

Liste les 10 points/faits importants appris dans cette conversation.
Format: une ligne par point, numéroté de 1 à 10.
Ne mets que les faits, pas de commentaires."""

facts = []
try:
async with httpx.AsyncClient(timeout=120.0) as client:
r = await client.post(
f"{ollama_url}/api/generate",
json={"model": "gemma3:4b", "prompt": prompt_extract, "stream": False,
"options": {"temperature": 0.2, "num_predict": 800}}
)
r.raise_for_status()
facts_text = r.json().get("response", "")
print(f"[QCM] Faits extraits: {facts_text[:200]}")
# Parser les faits numérotés
for line in facts_text.strip().split("\n"):
line = line.strip()
if line and (line[0].isdigit() or line.startswith("-")):
fact = line.lstrip("0123456789.-) ").strip()
if len(fact) > 10:
facts.append(fact)
except Exception as e:
print(f"[QCM] Erreur extraction faits: {e}")

if not facts:
# Utiliser directement le contenu de la conversation comme base
facts = [chat_content[i:i+200] for i in range(0, min(len(chat_content), 2000), 200)]

# ── ÉTAPE 2 : Générer un QCM par fait ─────────────────────────────────────
questions = []
facts_to_use = facts[:10]

for i, fact in enumerate(facts_to_use):
prompt_qcm = f"""Sur la base de ce fait appris en cours de {subject}:
"{fact}"

Génère UNE question QCM avec 4 choix (A, B, C, D).
Réponds UNIQUEMENT avec ce format JSON exact, une seule ligne:
{{"question": "...", "A": "...", "B": "...", "C": "...", "D": "...", "correct": "A", "explanation": "..."}}"""

try:
async with httpx.AsyncClient(timeout=60.0) as client:
r = await client.post(
f"{ollama_url}/api/generate",
json={"model": "gemma3:4b", "prompt": prompt_qcm, "stream": False,
"options": {"temperature": 0.3, "num_predict": 300}}
)
r.raise_for_status()
Comment on lines +101 to +107
@router.post("/create", response_model=ParentSupportResponse)
async def create_support_for_child(
data: ParentSupportCreateRequest,
current_user: User = Depends(get_current_user),
svc: SupportsService = Depends(get_supports_service),
db: Session = Depends(get_db),
):
Comment on lines +152 to +166
@router.post("/generate")
async def generate_evaluation(
data: GenerateEvalRequest,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""Génère un QCM de 10 questions basé sur une session de chat."""
_require_parent(current_user)

# Vérifier liaison parent-enfant
try:
ParentService(db).assert_owns_student(current_user.id, data.student_id)
except AuthorizationError as e:
raise HTTPException(status_code=403, detail=str(e))

Comment on lines +175 to +190
# Lier automatiquement le chat au soutien si support_id fourni
support_id = body.chat.get("support_id")
if support_id:
try:
from data.models import Support
from datetime import datetime
support = db.query(Support).filter(Support.id == support_id).first()
# Le soutien appartient a l'etudiant mais le parent peut aussi le lier
if support and (support.user_id == current_user.id or current_user.role in ("parent", "admin")):
support.chat_id = chat.id
support.status = "active"
support.updated_at = datetime.utcnow()
db.commit()
print(f"Chat {chat.id} lie au soutien {support_id}")
except Exception as e:
print(f"Support link error: {e}")
Comment on lines +183 to +185
contents = await file.read()
if len(contents) > max_bytes:
raise _too_large
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants