Skip to content

Student Portal — Profile, Activity & Dashboard

The student-facing surface of the MAS suite: the profile a learner fills in, the dashboard they land on, the activity feed and progress roll-up that track their journey, the profile-view analytics mentors generate, and the consolidated AI student-profile endpoint an agent calls to reason about a single learner. This domain is the read/write hub that stitches together enrollments, applications, quizzes, the token/XP economy, and the external Mr Learn / Mr Test platforms into one coherent "what is this student doing" view.

Status: documented from source on this branch.


Overview

The Student Portal domain answers three questions for three audiences:

  • "Who am I and is my profile done?" — the learner editing their StudentProfile, uploading a resume, verifying email changes, watching a profile-completion percentage climb. Backed by StudentProfileService (src/services/StudentProfileService.ts) and the student.controller profile methods.
  • "What should I do next?" — the learner's dashboard: enrolled courses, upcoming classes, pending assignments, deadlines, recent activity, announcements, application/PAP status, and a streak/level ribbon. Assembled by StudentController.getDashboardData (src/controllers/student.controller.ts) with help from DashboardService (src/services/DashboardService.ts) and StudentProgressService.
  • "How is this student really doing?" — two analytic lenses: a mentor/expert viewing a student profile (tracked by ProfileService as ProfileView rows), and an AI agent or batch-lead pulling a 360 snapshot from AiStudentProfileService (src/services/AiStudentProfileService.ts), which fuses Mr Test diagnostics, Mr Learn course progress, and generated insights.

Personas / roles touched:

Persona Role(s) What they do here
Student / learner USER Edits profile, sees dashboard, accrues activity & XP
Mentor / expert EXPERT Views a student profile (/api/profile/student/:id), generating a ProfileView
Admin / batch lead ADMIN, SUPERADMIN Views student 360 via batch dashboards
AI agent / internal tooling (internal route, network-gated) Pulls consolidated profile from /api/ai/students/:identifier/profile

This domain sits in the Mr Mentor / MAS LMS slice of the backend. It owns a small set of entities but reads heavily from the LMS (Enrollment, Course, Module, Class, Quiz, Assignment), the application/onboarding flow (Application, MAS101 PAP), the gamification layer (StudentProgress, XpEvent, badges), and the external sync entities (MrLearn*, MrTest*).


Key concepts & entities

Glossary

  • StudentProfile — the rich profile a learner fills in on the portal's Profile page (personal, address, education, career, social links, resume, notification prefs). Distinct from the User row, which holds auth + a denormalized subset that StudentProfileService keeps in sync.
  • Profile completeness — two different notions co-exist (see Status lifecycles): User.isProfileComplete (a one-way flag flipped on when every profile section is filled) and the dashboard's profileCompletion percentage (derived from User + latest Application).
  • StudentActivity — an append-only feed of meaningful learning events (quiz completed, module completed, etc.) shown as "recent activity" on the dashboard.
  • StudentProgress — a per-student cached XP/streak/level snapshot (the gamification aggregate). Documented in depth in the engagement doc; surfaced here because the dashboard ribbon reads it.
  • ProfileView — an analytics row recorded whenever one user views another user's profile (deduped to one row per viewer→viewed per hour).
  • AI student profile — a read-only, computed 360 bundle (no table) that resolves a student by UUID / email / roll number and merges Mr Test + Mr Learn + insights.
  • StageUser.stage (UserStage: SIGNUP=1, OTP_VERIFIED=2, PROFILE_COMPLETE=3) — the signup funnel position.

TypeORM entities owned by this domain

Entity Table File Notes
StudentProfile student_profiles src/entities/StudentProfile.ts 1:1 with User; resume/profilePicture are text (signed S3 URLs exceed 500 chars)
StudentActivity student_activities src/entities/StudentActivity.ts Enum ActivityType; metadata JSON
StudentProgress student_progress src/entities/StudentProgress.ts PK is userId; cached XP/streak/level snapshot
ProfileView profile_views src/entities/ProfileView.ts Indexed on (viewerId, viewedUserId) and (viewedUserId, createdAt)

Closely-read companion entities (owned elsewhere, consumed here): User (src/entities/User.ts), Application, Enrollment, Course, Module, Class, Quiz, Assignment, Announcement, XpEvent, and the external MrLearn* / MrTest* entities.


Architecture

flowchart TD
    subgraph Clients["Clients"]
        FE["Student portal (mas-website-live)"]
        MENTOR["Mentor / Admin UI"]
        AGENT["AI agent / internal tooling"]
    end

    subgraph Routes["Express routes"]
        R1["/api/student (StudentRoutes)"]
        R2["/api/student (createDashboardRoutes)"]
        R3["/api/profile (ProfileRoutes)"]
        R4["/api/ai (AiRoutes)"]
    end

    subgraph Controllers["Controllers"]
        C1["StudentController"]
        C2["StudentProgressController"]
        C3["DashboardController (legacy)"]
        C4["ProfileController"]
        C5["AiStudentController"]
    end

    subgraph Services["Services"]
        S1["StudentProfileService"]
        S2["DashboardService"]
        S3["StudentProgressService"]
        S4["ProfileService"]
        S5["AiStudentProfileService"]
        S6["BadgeService"]
        S7["AuthService (OTP)"]
        S8["S3Service"]
    end

    subgraph Data["PostgreSQL (TypeORM)"]
        D1["student_profiles"]
        D2["student_activities"]
        D3["student_progress / xp_events"]
        D4["profile_views"]
        D5["users / applications / enrollments"]
        D6["mrtest_* / mrlearn_* (synced)"]
    end

    subgraph Ext["External"]
        E1["AWS S3 (resume, photo)"]
        E2["Groq LLM (AI recs)"]
        E3["Gmail SMTP (email OTP)"]
    end

    FE --> R1 --> C1
    FE --> R2 --> C1
    FE --> R1 --> C2
    MENTOR --> R3 --> C4
    AGENT --> R4 --> C5

    C1 --> S1 & S6 & S7 & S8
    C1 --> S2
    C2 --> S3 & S6
    C3 --> S2
    C4 --> S4
    C5 --> S5

    S1 --> D1 & D5
    S2 --> D2 & D5
    S3 --> D3
    S4 --> D4 & D5
    S5 --> D5 & D6 & E2
    S6 --> D3
    S7 --> E3
    S8 --> E1

Note: /api/student is mounted twice in src/routes/index.ts — first createDashboardRoutes(...) then StudentRoutes.router. Both define overlapping paths (e.g. GET/POST /profile, GET /dashboard) wired to the same StudentController methods, so Express's first-match wins but the behavior is identical. The dashboard-routes mount adds the webinar endpoints and the narrow PATCH /profile/contact.


Data model

erDiagram
    USER ||--o| STUDENT_PROFILE : "has one"
    USER ||--o{ STUDENT_ACTIVITY : "generates"
    USER ||--o| STUDENT_PROGRESS : "has snapshot"
    USER ||--o{ PROFILE_VIEW : "is viewed in"
    USER ||--o{ PROFILE_VIEW : "views"

    USER {
        uuid id PK
        string email
        string secondaryEmail
        string fullName
        string phone
        string college
        int graduationYear
        string targetProfile
        boolean isProfileComplete
        int stage
        int activeWarningCount
        boolean escalatedToBatchLead
    }

    STUDENT_PROFILE {
        uuid id PK
        uuid userId FK
        string firstName
        string lastName
        string email
        string phone
        date dateOfBirth
        string addressLine1
        string city
        string state
        string pincode
        string highestQualification
        string institutionName
        int yearOfPassing
        string fieldOfStudy
        string targetProfile
        string linkedinUrl
        text profilePicture
        text resume
        simplearray skills
        boolean emailNotifications
        boolean whatsappAlerts
        boolean calendarSync
        timestamp createdAt
        timestamp updatedAt
    }

    STUDENT_ACTIVITY {
        uuid id PK
        uuid userId FK
        enum type
        string title
        text description
        json metadata
        timestamp createdAt
    }

    STUDENT_PROGRESS {
        uuid userId PK
        int totalXp
        int meaningfulActions
        smallint level
        int currentStreak
        int longestStreak
        date lastActiveDate
        int version
    }

    PROFILE_VIEW {
        uuid id PK
        uuid viewerId FK
        uuid viewedUserId FK
        string viewerType
        string viewedUserType
        timestamp createdAt
    }

Notable enums / status fields

  • ActivityType (src/entities/StudentActivity.ts): course_started, module_completed, quiz_completed, assignment_submitted, class_attended, certification_earned.
  • ProfileView.viewerType / viewedUserType: 'user' | 'expert'.
  • UserStage (src/types/UserStage.ts): SIGNUP=1, OTP_VERIFIED=2, PROFILE_COMPLETE=3.
  • User.isProfileComplete: boolean, one-way latch (only ever flipped on).

API surface

All paths below are absolute (mount prefix + route). Auth column: auth = authMiddleware (JWT → req.user); expert = expertMiddleware; enrolled = isEnrolledMiddleware; none = no middleware on the route.

Profile (StudentController, mounted at /api/student)

Method Path Auth/role Purpose
POST /api/student/profile auth Create/update the rich StudentProfile (upsert); syncs subset to User; re-evaluates badges
GET /api/student/profile auth Fetch own profile; resume rewritten to a 1h signed S3 URL; merges secondaryEmail from User
PUT /api/student/profile auth Update User-level fields (name, phone, college, etc.); flips isProfileComplete=true
PATCH /api/student/profile/contact auth Narrow update: phone + address only; rejected (409) once enrollment is complete
GET /api/student/profile/complete auth Returns { isComplete } (basic + education + career check)
GET /api/student/profile/resume/download auth Streams the saved resume through the backend (CORS workaround)
DELETE /api/student/profile auth Delete the StudentProfile row
POST /api/student/profile/send-email-otp auth Send OTP to a candidate new email (primary or secondary)
POST /api/student/profile/verify-email-otp auth Verify OTP and commit the primary/secondary email change

Dashboard & progress

Method Path Auth/role Purpose
GET /api/student/dashboard auth Full dashboard payload (courses, deadlines, activity, application, profile completion)
GET /api/student/me/progress auth Streak/level/XP ribbon snapshot + unseen badge ids
GET /api/student/me/badges auth All badges for the user
POST /api/student/me/badges/seen auth Clear the unseen flag on all badges
GET /api/student/batches none Available batches (public listing)
GET /api/student/webinars auth Public webinars + per-user watch progress
GET /api/student/webinars/:id auth Single webinar + progress; increments view count
POST /api/student/webinars/:id/progress auth Upsert watch progress
POST /api/student/webinars/:id/complete auth Mark a webinar complete

Legacy dashboard (DashboardController — still mounted)

Method Path Auth/role Purpose
GET /api/student/upcoming-classes none* Upcoming classes (token verified inside handler)
GET /api/student/pending-assignments none* Pending assignments
GET /api/student/recent-activity none* Recent StudentActivity feed
GET /api/student/announcements none* Active announcements
GET /api/student/profile-completion none* Mock profile-completion checklist

* These legacy routes call authService.verifyToken(req) themselves rather than using authMiddleware; getProfileCompletion returns hardcoded mock data and is superseded by /api/student/dashboard.

Profile views (ProfileController, mounted at /api)

Method Path Auth/role Purpose
GET /api/profile/mentor/:mentorId auth View a mentor profile; masks contact unless viewer has a booking; records a ProfileView
GET /api/profile/student/:studentId auth + expert Mentor views a student profile; records a ProfileView
GET /api/profile/views auth Paginated list of who viewed me
GET /api/profile/views/stats auth { totalViews, uniqueViewers }

AI student profile (AiStudentController, mounted at /api/ai)

Method Path Auth/role Purpose
GET /api/ai/students/:identifier/profile none Consolidated 360 (Mr Test + Mr Learn + insights). :identifier = UUID, email, or roll number

User journeys

1. Student completes / edits their profile

The learner fills the Profile page and saves. StudentProfileService.saveOrUpdateProfile upserts the student_profiles row and mirrors a subset onto the User row (name, phone, college, graduation year, branch, target profile, secondary email) so other surfaces stay consistent. When every section (basic + contact + education + resume) is filled, User.isProfileComplete latches on and a non-blocking badge re-evaluation fires (unlocks the "All Star" badge).

sequenceDiagram
    participant FE as Student portal
    participant API as POST /api/student/profile
    participant SC as StudentController
    participant SPS as StudentProfileService
    participant DB as PostgreSQL
    participant BS as BadgeService

    FE->>API: profile fields in body
    API->>SC: saveProfile
    SC->>SPS: saveOrUpdateProfile userId and data
    SPS->>DB: find User by id
    alt user missing
        SPS-->>SC: throw User not found
        SC-->>FE: 500 Failed to save profile
    else user exists
        SPS->>SPS: sync name phone college etc onto User
        SPS->>SPS: validate secondaryEmail public domain
        SPS->>DB: upsert student_profiles row
        SPS->>SPS: isProfileFullyComplete check
        opt all sections filled
            SPS->>DB: set User.isProfileComplete true
        end
        SPS->>DB: save User if changed
        SPS-->>SC: saved profile
        SC->>BS: evaluateForUser fire and forget
        SC-->>FE: 200 saved profile
    end

Resume read-back. On GET /api/student/profile, if resume is set the controller lazily imports S3Service and rewrites the stored value into a 1-hour signed URL (getStudentDocumentSignedUrl). If signing fails it falls back to the raw stored value. GET /api/student/profile/resume/download instead streams the object through the backend, because the S3 bucket CORS only allows the production origin.

Narrow contact edit. PATCH /api/student/profile/contact whitelists only phone + address fields, validates phone (10 digits) and pincode (6 digits), and refuses (409) once the latest Application is ENROLLED / BATCH_ALLOCATED / PAID. It also keeps Application.phone in sync so the application view does not show a stale number.

2. Verified email change (OTP)

Changing the primary or secondary email is gated by an email OTP so a learner cannot silently take over another account's address.

sequenceDiagram
    participant FE as Student portal
    participant API as Backend
    participant SC as StudentController
    participant AS as AuthService
    participant MAIL as Gmail SMTP

    FE->>API: POST /profile/send-email-otp with new email
    API->>SC: sendEmailOtp
    SC->>SC: validate email format
    SC->>SC: check email not taken by another user
    alt email already used
        SC-->>FE: 409 already associated
    else free
        SC->>AS: sendOtp email
        AS->>MAIL: deliver OTP
        SC-->>FE: 200 OTP sent
    end

    FE->>API: POST /profile/verify-email-otp email otp field
    API->>SC: verifyEmailOtp
    SC->>AS: checkAndDeleteOtp no side effects
    alt invalid or expired
        SC-->>FE: 400 invalid OTP
    else valid and field primary
        SC->>SC: re-check email uniqueness
        SC->>API: set User.email and save
        SC-->>FE: 200 email updated
    else valid and field secondary
        SC->>SC: validate public domain
        SC->>API: set User.secondaryEmail and save
        SC-->>FE: 200 email updated
    end

3. Activity is recorded across the app

StudentActivity is an append-only feed written by whichever subsystem produced the event — there is no single "record activity" endpoint. The clearest example is quiz submission: StudentQuizService.logQuizActivity writes a QUIZ_COMPLETED row (wrapped in try/catch so a logging failure never breaks the submission). In parallel, the gamification layer grants XP through StudentProgressService.grantXp, which is idempotent per (userId, type, sourceId). The dashboard later reads the last 10 StudentActivity rows.

sequenceDiagram
    participant FE as Student portal
    participant API as POST /api/student/quizzes/:id/submit
    participant QS as StudentQuizService
    participant DB as PostgreSQL
    participant SPS as StudentProgressService

    FE->>API: quiz answers
    API->>QS: submitQuiz
    QS->>DB: save QuizSubmission
    QS->>DB: insert student_activities row QUIZ_COMPLETED
    Note over QS,DB: logQuizActivity is try-catch wrapped so a write failure is swallowed
    QS->>SPS: grantXp for the action
    SPS->>DB: idempotent xp_events insert plus student_progress update
    QS-->>FE: 200 submission result

Other writers seen in source: MissOzoneService, DemoSeedService, AskMasService, and the Mr Test sync controller. Activity rows for a user are bulk-deleted by database.worker during account cleanup.

4. Student dashboard analytics

The dashboard is one fat read. getDashboardData branches on whether the student has enrollments. Enrolled students get courses + stats + upcoming classes + pending assignments + a merged upcomingDeadlines feed + the recent-activity feed; non-enrolled students get a free-course discovery payload. Everyone gets the user block, profileCompletion, and (if present) the latest Application with its MAS101 PAP workflow.

sequenceDiagram
    participant FE as Student portal
    participant API as GET /api/student/dashboard
    participant SC as StudentController
    participant DB as PostgreSQL

    FE->>API: bearer token
    API->>SC: getDashboardData
    SC->>DB: load User
    alt user missing
        SC-->>FE: 404 User not found
    else found
        SC->>DB: load enrollments with courses
        SC->>DB: load global active announcements
        alt has enrollments
            SC->>DB: compute course stats
            SC->>DB: upcoming classes via modules
            SC->>DB: pending assignments via modules
            SC->>DB: last 10 student_activities
            SC->>SC: build upcomingDeadlines feed
        else no enrollments
            SC->>DB: load free published courses for discovery
        end
        SC->>DB: load latest Application
        SC->>SC: calculateProfileCompletion User plus Application
        opt application exists
            SC->>DB: resolve batch and course names plus PAP workflow
        end
        SC-->>FE: 200 dashboard payload
    end

The separate streak/level ribbon is fetched by GET /api/student/me/progress (see engagement doc): it reads the cached student_progress snapshot, computes per-axis percentages (xp / actions / streak), the blocking axis, and the list of unseen badges for the celebration modal.

5. Mentor views a student / student views a mentor (profile views)

Viewing a profile records a ProfileView, deduped to one row per viewer→viewed per hour. When a student views a mentor, contact info is masked with a lock glyph unless the viewer has a confirmed/completed/tentative Slots booking or an approved RequestedSlot with that mentor.

sequenceDiagram
    participant FE as Viewer UI
    participant API as GET /api/profile/mentor/:mentorId
    participant PC as ProfileController
    participant PS as ProfileService
    participant DB as PostgreSQL

    FE->>API: bearer token
    API->>PC: getMentorProfile
    PC->>PS: getMentorProfile mentorId and viewerId
    PS->>DB: load mentor with mentorProfile
    alt mentor missing
        PS-->>PC: throw Mentor not found
        PC-->>FE: 500 error
    else found
        PS->>DB: check Slots booking for viewer
        opt no slot booking
            PS->>DB: check approved RequestedSlot
        end
        alt no booking
            PS->>PS: mask email phone linkedin with lock glyph
        end
        PS-->>PC: mentor profile
        opt viewer not self
            PC->>PS: trackProfileView viewer and mentor
            PS->>DB: find recent view within last hour
            alt no recent view
                PS->>DB: insert profile_views row
            else recent exists
                PS-->>PC: reuse existing row no insert
            end
        end
        PC-->>FE: 200 mentor profile
    end

The reverse, GET /api/profile/student/:studentId, is expertMiddleware-gated and records a ProfileView with viewedUserType=user. Analytics for "who viewed me" come from GET /api/profile/views (paginated) and GET /api/profile/views/stats (totalViews + distinct uniqueViewers).

6. AI-generated student profile insights (360)

An agent or batch lead requests a single learner's full picture. AiStudentProfileService.getProfile first resolves the student by UUID, email, or roll number, then fans out to collect Mr Test submissions/analyses and Mr Learn enrollments/module progress in parallel, derives upcoming tests, builds deterministic insights, and finally appends a few Groq-generated recommendations grounded in the same facts. The AI step is fail-soft.

sequenceDiagram
    participant AGENT as AI agent
    participant API as GET /api/ai/students/:identifier/profile
    participant AC as AiStudentController
    participant AISP as AiStudentProfileService
    participant DB as PostgreSQL
    participant GROQ as Groq LLM

    AGENT->>API: identifier UUID email or roll number
    API->>AC: getProfile
    AC->>AISP: getProfile identifier
    AISP->>DB: resolveStudent try UUID then email then roll number
    alt not resolvable
        AISP-->>AC: null
        AC-->>AGENT: 404 Student not found
    else resolved
        par collect in parallel
            AISP->>DB: collectMrTest submissions and analyses
        and
            AISP->>DB: collectMrLearn enrollments and modules
        end
        AISP->>DB: collectUpcomingTests via batch and submissions
        AISP->>AISP: buildInsights summary recs riskFlags
        AISP->>GROQ: generateAiRecommendations grounded facts
        alt groq ok
            GROQ-->>AISP: 2 to 3 extra recs
            AISP->>AISP: append with AI prefix
        else no key or timeout or bad json
            AISP->>AISP: keep algorithmic recs only
        end
        AISP-->>AC: profile bundle plus generatedAt
        AC-->>AGENT: 200 data
    end

The insights block also emits riskFlags such as no-passing-test-scores, guessing-pattern, all-courses-unstarted, learning-stale-14d+, missed-scheduled-test, and blocked-by-prereq, plus a one-line summary.

7. Admin views a student 360

Admins/batch leads do not hit a dedicated single-student endpoint in this domain; they reach student detail through batch dashboards (GET /api/admin/dashboard/batch/:batchName/students, handled by the admin controller / AdminDashboardService, src/services/AdminDashboardService.ts). For a deep per-student diagnostic snapshot they consume the same /api/ai/students/:identifier/profile payload as journey 6. The dashboard user block additionally exposes activeWarningCount and escalatedToBatchLead so staff can see at-risk students.


Background jobs & async

  • Badge re-evaluation — fired non-blocking from StudentController.fireBadgeEvaluation after saveProfile and updateProfile; runs BadgeService.evaluateForUser in a detached async IIFE so it never delays or fails the HTTP response.
  • Daily-login XPStudentProgressService.recordDailyLogin is invoked on login (auth.controller) and from sync/quiz/slot-completion paths; idempotent per IST calendar day via sourceId.
  • XP grantsStudentProgressService.grantXp wraps each grant in a transaction with SELECT … FOR UPDATE on student_progress and dedupes against the xp_events unique constraint; streak/action milestones recurse once (bounded by skipStreakUpdate).
  • Account cleanupdatabase.worker (src/workers/database.worker.ts) deletes a user's StudentActivity rows during account teardown.
  • Webinar view countGET /api/student/webinars/:id increments the webinar view counter as a side effect (via WebinarService.incrementViewCount).

This domain has no dedicated BullMQ queue, cron, socket events, or inbound webhooks of its own.


External integrations

Integration Where Env / config Failure / fallback
AWS S3 resume + profile picture storage; signed-URL read; backend stream proxy AWS_*, AWS_S3_STUDENT_DOCUMENTS_BUCKET Signed-URL failure falls back to raw stored value; stream errors return 500
Groq LLM extra AI recommendations in AiStudentProfileService GROQ_API_KEY, GROQ_TEXT_ENDPOINT (default api.groq.com/openai/v1/chat/completions), GROQ_TEXT_MODEL (default llama-3.3-70b-versatile) Missing key / timeout / bad JSON → returns [], deterministic recs only
Gmail SMTP email-change OTP delivery (AuthService.sendOtp) EMAIL_USER, EMAIL_PASS OTP send failure surfaces as 500
Mr Learn / Mr Test source data for AI profile (synced tables mrlearn_*, mrtest_*) (sync jobs elsewhere) Empty collections degrade to "no tests / no courses" insights

No explicit feature flags gate this domain; the Groq enrichment self-disables when GROQ_API_KEY is unset.


Status lifecycles

User.isProfileComplete (one-way latch)

stateDiagram-v2
    [*] --> Incomplete
    Incomplete --> Complete : all sections filled (basic + contact + education + resume) OR PUT /profile
    Complete --> Complete : further edits never regress it

StudentProfileService only ever sets isProfileComplete to true (never back to false); PUT /api/student/profile also sets it true unconditionally.

UserStage signup funnel

stateDiagram-v2
    [*] --> SIGNUP
    SIGNUP --> OTP_VERIFIED : email OTP verified at signup
    OTP_VERIFIED --> PROFILE_COMPLETE : profile data added
    note right of PROFILE_COMPLETE
        Email-change OTP verify does NOT
        advance stage (no side effects)
    end note

Dashboard profile-completion checklist

calculateProfileCompletion computes a percentage from five items, derived from User + the latest Application:

stateDiagram-v2
    [*] --> Personal : name + phone
    Personal --> Education : college + graduationYear
    Education --> Professional : profession + domain
    Professional --> AppSubmitted : application not in DRAFT
    AppSubmitted --> PaymentDone : onboardingFeePaid or paid status
    PaymentDone --> [*]

Each completed item adds 20%. Note the legacy DashboardService.getProfileCompletion returns hardcoded mock data and is not the source of truth.


Edge cases, limits & gotchas

  • Two profile-completeness definitions. StudentProfileService.isProfileComplete (used by GET /profile/complete) requires basic + education + career (targetProfile). The latch in saveOrUpdateProfile.isProfileFullyComplete requires basic + contact + education + resume (no career). They are intentionally different checks — do not assume one implies the other.
  • /api/student mounted twice. Overlapping GET/POST /profile and /dashboard routes exist in both createDashboardRoutes and StudentRoutes, wired to the same handlers. Adding a new /profile-prefixed route in only one of them can be shadowed by the first mount.
  • Hardening note (internal). A security/hardening observation for this area is tracked in the team's private notes (internal/security-and-hardening-notes.md) and is intentionally not published on this site.
  • Secondary-email domain rule. A secondaryEmail must use a public domain (gmail/yahoo/outlook/…); the same validation runs in both StudentProfileService and updateProfile. Editing email requires OTP via the dedicated endpoints; the email-change OTP verify is side-effect-free (no stage change, no isVerified flip).
  • Profile-view dedup window is 1 hour and per viewer→viewed pair. Rapid repeat views do not inflate counts; getUniqueViewersCount uses COUNT(DISTINCT viewerId).
  • Resume/profilePicture columns are text, not varchar. Signed S3 URLs can exceed 500 chars and would otherwise fail the whole profile save.
  • Hardening note (internal). A security/hardening observation for this area is tracked in the team's private notes (internal/security-and-hardening-notes.md) and is intentionally not published on this site.
  • Resolver last resort. When an identifier matches no user and no Mr Test submission, resolveStudent still returns a stub identity (roll-number echo) rather than null, so the AI profile can render an empty-but-valid shape.
  • Activity logging is best-effort. logQuizActivity swallows errors; a failed activity write never blocks the underlying action. There is no idempotency on StudentActivity (unlike xp_events), so duplicate events are possible if a writer retries.
  • Multi-platform. The x-platform header routes products, but the profile/dashboard handlers here key only on req.user.id and are platform-agnostic.