Skip to content

Latest commit

 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

SkillPath AI

Personalized, AI-driven learning paths — built in support of UN Sustainable Development Goal 4: Quality Education.

Access to good mentorship and a clear learning path is still a privilege, not a default. SkillPath AI addresses that gap: it generates a personalized learning roadmap based on a user's goal, current skill level, and available time, then supports them through it with an AI chat assistant for doubts, curated resources, and project recommendations matched to their progress.


Problem statement

Most self-learners face the same three obstacles:

  1. Decision fatigue — too many scattered tutorials, no clear sequence to follow.
  2. No one to ask — questions come up at odd hours with no mentor available.
  3. Theory without practice — courses teach concepts but rarely suggest what to actually build.

SkillPath AI targets each of these directly with a roadmap generator, a 24/7 AI assistant, and project recommendations calibrated to skill level.

Table of Contents

  1. Features
  2. Tech Stack
  3. Architecture Overview
  4. Project Structure
  5. Authentication and Security Model
  6. Data Model
  7. The AI Layer and Fallback Strategy
  8. API Reference
  9. Master App Flow
  10. Feature Flowcharts
  11. Environment Variables
  12. Local Development
  13. Deployment
  14. Known Limitations and Future Work

Features

  • Account system. Register and log in with email and password, JWT based sessions stored in an HttpOnly cookie.
  • Onboarding. New users set a learning goal, skill level, and weekly time commitment before reaching the dashboard.
  • AI generated roadmaps. A step by step learning path generated from the user's goal, level, and available hours, produced by an LLM and stored per user.
  • Progress tracking. Each roadmap step can be marked complete independently, persisted per user per roadmap.
  • AI chat mentor. A persistent chat conversation with an AI tutor, with full history stored and replayed on return visits.
  • AI generated lessons and quizzes. Per roadmap step, the app can generate a short markdown lesson and a multiple choice quiz on demand.
  • Project recommendations. AI suggested project ideas matched to the user's level and goal, with the ability to save favorites.
  • Curated resources. A shared resource library, readable by any logged in user, writable only by admins.
  • Admin panel. Platform wide stats, user management, and resource management, gated behind an admin only role check on both frontend and backend.
  • Graceful AI degradation. If no AI API key is configured, or the AI call fails, the app falls back to deterministic, hand written roadmap and project data instead of breaking.

Tech Stack

Layer Technology
Backend framework Node.js, Express 5
Backend auth JSON Web Tokens in an HttpOnly cookie
Database MongoDB via Mongoose
AI provider Groq (llama-3.3-70b-versatile)
Security middleware Helmet, CORS, express-rate-limit
Frontend framework React 19
Routing React Router v6
HTTP client Axios, with withCredentials: true
State management React Context (AuthContext, ThemeContext)
Build tool Vite
Backend hosting Render
Frontend hosting Vercel

Architecture Overview

Two independently deployed services, frontend and backend, on different domains, communicating over HTTPS with a JWT carried in a cross site cookie. The backend never renders HTML; it is a pure JSON API. The AI provider is a third external dependency the backend talks to server side, so the AI API key is never exposed to the browser.

flowchart LR
    subgraph Browser
        UI[React SPA<br/>Vite build, React Router]
    end

    subgraph Vercel
        Static[Static hosting]
    end

    subgraph Render
        API[Express API<br/>JWT auth, rate limiting]
        DB[(MongoDB)]
    end

    subgraph Groq
        LLM[llama-3.3-70b-versatile]
    end

    UI -- "loads bundle" --> Static
    UI -- "axios, withCredentials: true" --> API
    API -- "mongoose queries" --> DB
    API -- "chat completions" --> LLM
    LLM -. "on failure or missing key" .-> API
    API -- "Set-Cookie: token (HttpOnly, JWT)" --> UI
Loading

Key architectural decisions:

  • JWT in an HttpOnly cookie, not localStorage. The frontend never touches the token directly. It cannot be read or exfiltrated by injected JavaScript, since httpOnly: true makes it invisible to document.cookie. The browser just attaches it automatically on every request because Axios is configured with withCredentials: true.
  • No CSRF token dance. Unlike a typical session cookie setup, this backend does not implement CSRF tokens at all. The tradeoff is deliberate: it relies entirely on SameSite cookie policy and a strict CORS origin allowlist to block cross site request forgery, rather than a second token mechanism. This is simpler to reason about but means the CORS configuration is the actual security boundary and must be airtight.
  • AI calls happen only on the backend. The Groq API key lives in a server side environment variable. The frontend never calls Groq directly; it always goes through the Express API, which then decides whether to call the LLM or fall back to static logic.
  • Fallback over failure. Every AI dependent controller degrades to a deterministic function (fallbackService.js) rather than returning an error, whether the cause is a missing API key, a network failure, or a malformed AI response that fails JSON parsing.
  • Rate limiting at two tiers. A stricter limiter on /api/auth/* (20 requests per 15 minutes) resists credential stuffing and brute force login attempts, and a looser general limiter on /api overall (100 requests per 15 minutes) protects the rest of the API and, incidentally, the AI provider's usage quota.

Project Structure

SkillPathAI/
├── backend/
│   ├── server.js                 # app entrypoint, middleware chain, route mounting
│   ├── config/
│   │   ├── db.js                 # mongoose connection
│   │   └── env.js                # startup env validation, CORS origin parsing
│   ├── models/
│   │   ├── User.js               # email, hashed password, isAdmin, onboarded
│   │   ├── Profile.js            # goal, level, weeklyHours, bio
│   │   ├── Roadmap.js            # AI-generated steps, one active per user
│   │   ├── Progress.js           # completedSteps per user per roadmap
│   │   ├── ChatHistory.js        # one growing message thread per user
│   │   ├── SavedProject.js       # user's bookmarked project ideas
│   │   └── Resource.js           # admin-curated learning resources
│   ├── controllers/              # one file per resource, thin route handlers
│   ├── routes/                   # one file per resource, wires controllers to middleware
│   ├── middleware/
│   │   ├── authMiddleware.js     # protect (JWT check), admin (role check)
│   │   ├── errorMiddleware.js    # notFound + centralized error handler
│   │   └── rateLimiter.js        # authLimiter, apiLimiter
│   ├── services/
│   │   ├── aiService.js          # all Groq calls, with try/catch fallback
│   │   └── fallbackService.js    # deterministic roadmap/project/chat data
│   └── utils/
│       └── generateToken.js      # signs JWT, sets the cookie
└── frontend/
    └── src/
        ├── main.jsx
        ├── App.jsx                # route table
        ├── context/
        │   ├── AuthContext.jsx    # user state, login/register/logout, refreshUser
        │   └── ThemeContext.jsx
        ├── routes/
        │   ├── ProtectedRoute.jsx # redirects to /login if not authenticated
        │   └── AdminRoute.jsx     # redirects to /dashboard if not admin
        ├── utils/
        │   └── api.js             # axios instance, withCredentials: true
        ├── pages/                 # one file per route: Login, Register, Dashboard,
        │                          # Roadmap, Chat, Projects, Resources, Profile, Onboarding,
        │                          # and an admin/ subfolder for admin-only pages
        └── components/
            ├── landing/
            ├── layout/            # Navbar, Footer, Sidebar (AppShell)
            ├── learning/
            └── pages/

Authentication and Security Model

Token flow

Registration and login both call generateToken(res, userId), which signs a JWT ({ userId }, 30 day expiry) and sets it as a cookie named token:

res.cookie("token", token, {
  httpOnly: true,
  secure: process.env.NODE_ENV === "production",
  sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
  maxAge: 30 * 24 * 60 * 60 * 1000,
  path: "/",
});

httpOnly: true is the important part: JavaScript on the frontend, whether the app's own code or an injected script from an XSS vulnerability, cannot read this cookie. It can only be sent by the browser automatically on requests to the backend's domain, which is why Axios is configured with withCredentials: true on the frontend side.

In production, sameSite: "None" combined with secure: true is required for the cookie to be sent on the cross origin requests this architecture depends on, since frontend and backend are on different domains.

Admin gating

admin middleware runs after protect and simply checks req.user.isAdmin. It is applied at the router level for entire resources (router.use(protect, admin) in userRoutes.js and adminRoutes.js) and per route for the resource routes that mix public reads with admin only writes (resourceRoutes.js: anyone logged in can GET, only admins can POST or DELETE).

CORS as the actual security boundary

cors({
  origin(origin, callback) {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, origin || allowedOrigins[0]);
    } else {
      callback(new Error(`CORS blocked for origin: ${origin}`));
    }
  },
  credentials: true,
})

allowedOrigins comes from the CLIENT_URL environment variable. Because this app does not use CSRF tokens, this allowlist is doing real security work, not just convenience: it is what stops an arbitrary third party site from making authenticated, cookie carrying requests against the API on a logged in user's behalf.


Data Model

erDiagram
    USER ||--o| PROFILE : has
    USER ||--o{ ROADMAP : owns
    USER ||--o{ PROGRESS : tracks
    USER ||--o| CHATHISTORY : has
    USER ||--o{ SAVEDPROJECT : saves
    USER ||--o{ RESOURCE : "adds (if admin)"
    ROADMAP ||--o{ PROGRESS : "tracked by"

    USER {
        ObjectId _id
        string name
        string email
        string password_hash
        bool isAdmin
        bool onboarded
    }
    PROFILE {
        ObjectId user FK
        string goal
        string level
        number weeklyHours
        string bio
    }
    ROADMAP {
        ObjectId user FK
        string goal
        string level
        string estimatedDuration
        bool aiGenerated
        array steps
    }
    PROGRESS {
        ObjectId user FK
        ObjectId roadmap FK
        array completedSteps
    }
    CHATHISTORY {
        ObjectId user FK
        array messages
    }
    SAVEDPROJECT {
        ObjectId user FK
        string title
        string difficulty
        array techStack
    }
    RESOURCE {
        string title
        string category
        ObjectId addedBy FK
    }
Loading

Notable design choices in the schema:

  • One active roadmap per user. generateRoadmap deletes the user's existing Roadmap and Progress documents before creating new ones. There is no history of past roadmaps; generating a new one replaces the old.
  • Progress is decoupled from Roadmap steps. completedSteps is just an array of step numbers, not a reference into the roadmap's steps array. The frontend is responsible for matching step numbers back to step content when rendering.
  • ChatHistory is a single growing document per user, not one document per message. Every chat turn appends to the same messages array, capped only by MongoDB's document size limit in practice.

The AI Layer and Fallback Strategy

services/aiService.js is the only place in the codebase that talks to Groq. Every exported function follows the same shape:

flowchart TD
    Start([Controller calls an aiService function]) --> Check{GROQ_API_KEY set?}
    Check -- No --> Fallback[Return static fallback data]
    Check -- Yes --> Call[Call Groq chat.completions.create]
    Call --> Parse{Response parses as expected JSON or text?}
    Parse -- Yes --> Return[Return AI-generated result]
    Parse -- No / API error --> Fallback
    Fallback --> Return
Loading

This matters for two reasons worth mentioning in an interview. First, it means the app is fully demoable and testable without ever configuring an AI key, since fallbackService.js provides hand written, deterministic roadmap and project data. Second, it means a flaky or rate limited AI provider never surfaces as a 500 error to the user, a try/catch around every Groq call always resolves to something usable.

The five AI backed capabilities:

Function Purpose Fallback behavior
generateRoadmapWithAI Builds the step by step learning path Static roadmap template
generateProjectRecommendations Suggests 3 to 5 project ideas Static project list
chatWithAI Powers the chat mentor, full conversation history sent as context A single canned offline message
generateStepLesson Mini markdown lesson for one roadmap step Instructional text pointing to official docs
generateStepQuiz 3 to 5 question multiple choice quiz for one step Returns null, controller responds 503

API Reference

Method Endpoint Auth Description
POST /api/auth/register No Create account, sets JWT cookie
POST /api/auth/login No Log in, sets JWT cookie
POST /api/auth/logout No Clears the JWT cookie
GET /api/auth/me Yes Current user
GET /api/profiles/me Yes Get own profile
PUT /api/profiles/me Yes Update profile, flips onboarded to true once goal/level/hours are set
POST /api/roadmaps/generate Yes Generate (and replace) the user's roadmap
GET /api/roadmaps/me Yes Get current roadmap
GET /api/progress/me Yes Get progress on current roadmap
PUT /api/progress/me Yes Update completed steps
POST /api/chat Yes Send a chat message, get an AI reply
GET /api/chat/history Yes Full chat history
POST /api/learning/lesson Yes Generate a lesson for a step
POST /api/learning/quiz Yes Generate a quiz for a step
GET /api/projects/suggestions Yes AI project recommendations
POST /api/projects/save Yes Bookmark a project
GET /api/projects/saved Yes List bookmarked projects
GET /api/resources Yes List all resources
POST /api/resources Yes, admin Add a resource
DELETE /api/resources/:id Yes, admin Delete a resource
GET /api/users Yes, admin List all users
DELETE /api/users/:id Yes, admin Delete a user (not self)
GET /api/admin/stats Yes, admin Platform wide counts
GET /api/admin/users Yes, admin List users (admin view)
GET /health No Liveness and DB connectivity check

Master App Flow

This is the entire application, start to finish, as one diagram: every screen a user can land on and what pushes them from one to the next.

flowchart TD
    Visit([User visits the site]) --> SessionCheck{GET /api/auth/me<br/>valid session cookie?}

    SessionCheck -- No --> Landing[Landing Page]
    Landing --> ChooseAuth{Register or Login?}
    ChooseAuth -- Register --> RegisterPage
    ChooseAuth -- Login --> LoginPage
    RegisterPage -- success --> Onboarded{onboarded flag?}
    LoginPage -- success --> Onboarded

    SessionCheck -- Yes --> Onboarded

    Onboarded -- false --> OnboardingPage[Onboarding: set goal, level, weeklyHours]
    OnboardingPage -- profile saved, onboarded = true --> RoadmapGen[Auto-trigger roadmap generation]
    RoadmapGen --> Dashboard

    Onboarded -- true --> Dashboard[Dashboard]

    Dashboard --> RoadmapPage[Roadmap Page:<br/>view steps, mark complete,<br/>generate lesson / quiz per step]
    Dashboard --> ChatPage[Chat Page:<br/>AI mentor conversation]
    Dashboard --> ProjectsPage[Projects Page:<br/>AI suggestions, save favorites]
    Dashboard --> ResourcesPage[Resources Page:<br/>browse curated links]
    Dashboard --> ProfilePage[Profile Page:<br/>edit goal, level, hours, bio]

    RoadmapPage -- "regenerate roadmap" --> RoadmapGen

    Dashboard -- "isAdmin only" --> AdminDashboard[Admin Dashboard: platform stats]
    AdminDashboard --> AdminUsers[Admin: manage users]
    AdminDashboard --> AdminResources[Admin: manage resources]

    Dashboard --> Logout([Logout])
    RoadmapPage --> Logout
    ChatPage --> Logout
    ProjectsPage --> Logout
    ResourcesPage --> Logout
    ProfilePage --> Logout
    Logout --> Landing
Loading

The single fork that shapes the entire user experience is the onboarded boolean on the User document. Every authenticated entry point funnels through that check: incomplete profile always lands on Onboarding, regardless of which URL the user actually typed in or bookmarked, because ProtectedRoute guards the destination but does not itself redirect based on onboarded, individual pages and the post login flow handle that.


Feature Flowcharts

Each diagram below traces one user facing action from the click to the database and back.

1. Registration

flowchart TD
    A([User fills name, email, password]) --> B[POST /api/auth/register]
    B --> C{authLimiter: under 20 req/15min?}
    C -- No --> D[429 Too many auth attempts]
    C -- Yes --> E{Name, email, password all present?<br/>Password >= 6 chars?}
    E -- No --> F[400 validation error]
    E -- Yes --> G{Email already registered?}
    G -- Yes --> H[400 User already exists]
    G -- No --> I[User.create<br/>pre-save hook bcrypt-hashes password]
    I --> J[generateToken: sign JWT, set HttpOnly cookie]
    J --> K[201 Created, returns user object]
    K --> L[AuthContext.setUser]
    L --> M[Redirect to /onboarding]
Loading

2. Login

flowchart TD
    A([User submits email + password]) --> B[POST /api/auth/login]
    B --> C{authLimiter: under 20 req/15min?}
    C -- No --> D[429 Too many auth attempts]
    C -- Yes --> E{User with this email exists?}
    E -- No --> F[401 Invalid email or password]
    E -- Yes --> G[bcrypt.compare submitted password vs stored hash]
    G -- No match --> F
    G -- Match --> H[generateToken: sign JWT, set HttpOnly cookie]
    H --> I[200 OK, returns user object]
    I --> J[AuthContext.setUser]
    J --> K{user.onboarded?}
    K -- false --> L[Redirect to /onboarding]
    K -- true --> M[Redirect to /dashboard]
Loading

3. Session restore on app load

flowchart TD
    A([App mounts]) --> B[AuthProvider useEffect fires refreshUser]
    B --> C[GET /api/auth/me<br/>browser auto-attaches token cookie]
    C --> D{Cookie present and valid JWT?}
    D -- No cookie --> E[401 Not authorized, no token]
    D -- Invalid / expired --> F[401 Not authorized, token invalid]
    D -- Valid --> G[User.findById from decoded payload]
    G -- Not found --> H[401 Not authorized, user not found]
    G -- Found --> I[200, returns user object]
    E & F & H --> J[AuthContext: user = null]
    I --> K[AuthContext: user = returned user]
    J & K --> L[loading = false]
    L --> M[Route guards now evaluate correctly]
Loading

4. Route protection

flowchart TD
    A([User navigates to a route]) --> B{Route type?}
    B -- Public --> C[Render directly:<br/>Landing, About, Contact, Login, Register]
    B -- Protected --> D{AuthContext.loading?}
    D -- true --> E[Show loading state]
    D -- false --> F{user exists?}
    F -- No --> G[Navigate to /login, remember attempted path]
    F -- Yes --> H[Render the page inside AppShell]
    B -- Admin-only --> I{AuthContext.loading?}
    I -- true --> E
    I -- false --> J{user?.isAdmin?}
    J -- No --> K[Navigate to /dashboard]
    J -- Yes --> L[Delegate to ProtectedRoute check above]
    L --> H
Loading

5. Onboarding

flowchart TD
    A([New user lands on Onboarding page]) --> B[Fill goal, level, weeklyHours, optional bio]
    B --> C[PUT /api/profiles/me]
    C --> D{Profile document exists for user?}
    D -- No --> E[new Profile]
    D -- Yes --> F[load existing Profile]
    E & F --> G[apply only the fields sent in the request body]
    G --> H[profile.save]
    H --> I{goal AND level set AND weeklyHours > 0?}
    I -- Yes, and user.onboarded was false --> J[User.findByIdAndUpdate: onboarded = true]
    I -- No --> K[leave onboarded as-is]
    J & K --> L[200, returns updated profile]
    L --> M[Frontend triggers roadmap generation]
    M --> N[Redirect to /dashboard]
Loading

6. Roadmap generation

flowchart TD
    A([User clicks Generate Roadmap]) --> B[POST /api/roadmaps/generate]
    B --> C[Load user's Profile as fallback for goal/level/weeklyHours]
    C --> D{goal and level resolved from body or profile?}
    D -- No --> E[400 Goal and level are required]
    D -- Yes --> F[generateRoadmapWithAI: goal, level, weeklyHours]
    F --> G{GROQ_API_KEY set?}
    G -- No --> H[generateFallbackRoadmap: static template]
    G -- Yes --> I[Call Groq with strict JSON-only system prompt]
    I --> J{Response parses as valid JSON?}
    J -- No --> H
    J -- Yes --> K[Use AI-generated steps + estimatedDuration]
    H --> L[Delete user's old Roadmap and Progress documents]
    K --> L
    L --> M[Roadmap.create with new steps]
    M --> N[Progress.findOneAndUpdate upsert: fresh, empty completedSteps]
    N --> O[201, returns new roadmap]
    O --> P[Frontend renders roadmap steps]
Loading

7. Marking a roadmap step complete

flowchart TD
    A([User toggles a step's checkbox]) --> B[PUT /api/progress/me<br/>stepNumber, completed: true/false]
    B --> C[getOrCreateProgress: find latest Roadmap for user]
    C --> D{Roadmap exists?}
    D -- No --> E[404 No roadmap to track progress]
    D -- Yes --> F{Progress doc exists for this roadmap?}
    F -- No --> G[Progress.create with empty completedSteps]
    F -- Yes --> H[load existing Progress]
    G & H --> I{Request sent completedSteps array directly?}
    I -- Yes --> J[Replace completedSteps wholesale]
    I -- No, sent stepNumber + completed --> K{completed === false?}
    K -- Yes --> L[Remove stepNumber from the Set]
    K -- No --> M[Add stepNumber to the Set]
    L & M --> N[Sort and dedupe into completedSteps array]
    J & N --> O[progress.save]
    O --> P[200, returns updated progress]
    P --> Q[Frontend updates checklist / progress bar]
Loading

8. Generating a step lesson

flowchart TD
    A([User clicks 'Learn' on a roadmap step]) --> B[POST /api/learning/lesson<br/>stepTitle, optional goal/level]
    B --> C{stepTitle provided?}
    C -- No --> D[400 stepTitle is required]
    C -- Yes --> E[Load Profile to fill in missing goal/level]
    E --> F[generateStepLesson]
    F --> G{GROQ_API_KEY set?}
    G -- No --> H[Static markdown pointing to official docs]
    G -- Yes --> I[Call Groq: concise markdown mini-lesson]
    I --> J{Call succeeds?}
    J -- No --> H
    J -- Yes --> K[Return AI-written lesson]
    H & K --> L["200, { lesson }"]
    L --> M[Frontend renders markdown in a panel]
Loading

9. Generating a step quiz

flowchart TD
    A([User clicks 'Quiz me' on a roadmap step]) --> B[POST /api/learning/quiz<br/>stepTitle, optional goal]
    B --> C{stepTitle provided?}
    C -- No --> D[400 stepTitle is required]
    C -- Yes --> E[Load Profile to fill in missing goal]
    E --> F[generateStepQuiz]
    F --> G{GROQ_API_KEY set?}
    G -- No --> H[Return null]
    G -- Yes --> I[Call Groq: JSON array of 3-5 MCQs]
    I --> J{Parses as valid JSON array?}
    J -- No --> H
    J -- Yes --> K[Return parsed quiz array]
    H --> L[503 Quiz generation unavailable]
    K --> M["200, { quiz }"]
    M --> N[Frontend renders interactive multiple choice quiz]
Loading

10. AI chat mentor

flowchart TD
    A([User opens Chat page]) --> B[GET /api/chat/history]
    B --> C[Load or return empty ChatHistory.messages]
    C --> D[Render existing conversation]
    D --> E([User types a message and sends])
    E --> F[POST /api/chat, message text]
    F --> G[Find or create ChatHistory for user]
    G --> H[Append user message to DB messages array]
    H --> I[chatWithAI: system prompt + full message history]
    I --> J{GROQ_API_KEY set and call succeeds?}
    J -- No --> K[offlineChatMessage fallback text]
    J -- Yes --> L[AI-generated reply]
    K & L --> M[Append assistant message to DB messages array, save]
    M --> N["200, { response }"]
    N --> O[Frontend appends both messages to chat UI]
    O --> E
Loading

11. Project recommendations

flowchart TD
    A([User opens Projects page]) --> B[GET /api/projects/suggestions?level&goal]
    B --> C[Load Profile as fallback for level/goal if query params absent]
    C --> D[generateProjectRecommendations: level, goal]
    D --> E{GROQ_API_KEY set?}
    E -- No --> F[generateFallbackProjects: static list]
    E -- Yes --> G[Call Groq: JSON array of 3-5 project ideas]
    G --> H{Parses as valid JSON array?}
    H -- No --> F
    H -- Yes --> I[Use AI-generated project list]
    F & I --> J[200, returns array of projects]
    J --> K[Frontend renders project cards]
Loading

12. Saving a project

flowchart TD
    A([User clicks Save on a project card]) --> B[POST /api/projects/save<br/>title, description, difficulty, techStack, estimatedTime]
    B --> C{title present?}
    C -- No --> D[400 Title is required]
    C -- Yes --> E[SavedProject.create, tied to req.user._id]
    E --> F[201, returns saved project]
    F --> G[Frontend marks card as saved / adds to Saved list]

    H([User opens Saved Projects view]) --> I[GET /api/projects/saved]
    I --> J[SavedProject.find, sorted by savedAt descending]
    J --> K[200, returns array]
    K --> L[Frontend renders saved project list]
Loading

13. Viewing resources

flowchart TD
    A([User opens Resources page]) --> B[GET /api/resources]
    B --> C{protect: valid session?}
    C -- No --> D[401 Not authorized]
    C -- Yes --> E[Resource.find, sorted by createdAt descending]
    E --> F[200, returns array]
    F --> G[Frontend renders resource list, no admin controls shown to non-admins]
Loading

14. Admin adds a resource

flowchart TD
    A([Admin fills resource form]) --> B[POST /api/resources<br/>title, description, url, category]
    B --> C{protect: valid session?}
    C -- No --> D[401 Not authorized]
    C -- Yes --> E{admin: req.user.isAdmin?}
    E -- No --> F[403 Admin access required]
    E -- Yes --> G{title present?}
    G -- No --> H[400 Title is required]
    G -- Yes --> I[Resource.create, addedBy = req.user._id]
    I --> J[201, returns new resource]
    J --> K[Frontend prepends it to the resource list]
Loading

15. Admin deletes a resource

flowchart TD
    A([Admin clicks Delete on a resource]) --> B[DELETE /api/resources/:id]
    B --> C{protect + admin checks pass?}
    C -- No --> D[401 / 403]
    C -- Yes --> E{Resource.findById returns a document?}
    E -- No --> F[404 Resource not found]
    E -- Yes --> G[resource.deleteOne]
    G --> H["200, { message: Resource deleted }"]
    H --> I[Frontend removes it from the list]
Loading

16. Profile update

flowchart TD
    A([User edits goal, level, weeklyHours, bio, or avatar on Profile page]) --> B[PUT /api/profiles/me]
    B --> C{Profile exists for user?}
    C -- No --> D[new Profile instance]
    C -- Yes --> E[load existing Profile]
    D & E --> F[apply only fields present in the request body]
    F --> G[profile.save]
    G --> H{goal, level set and weeklyHours > 0, and user.onboarded was false?}
    H -- Yes --> I[flip User.onboarded to true]
    H -- No --> J[no change to onboarded]
    I & J --> K[200, returns updated profile]
    K --> L[Frontend reflects saved changes]
Loading

17. Admin platform stats

flowchart TD
    A([Admin opens Admin Dashboard]) --> B[GET /api/admin/stats]
    B --> C{protect + admin checks pass?}
    C -- No --> D[401 / 403]
    C -- Yes --> E[Promise.all: count Users, Roadmaps, Progress docs,<br/>ChatHistory docs, SavedProjects, Resources, onboarded Users]
    E --> F[200, returns aggregated counts object]
    F --> G[Frontend renders stat cards]
Loading

18. Admin deletes a user

flowchart TD
    A([Admin clicks Delete on a user row]) --> B[DELETE /api/users/:id]
    B --> C{protect + admin checks pass?}
    C -- No --> D[401 / 403]
    C -- Yes --> E{User.findById returns a document?}
    E -- No --> F[404 User not found]
    E -- Yes --> G{target _id equals req.user._id?}
    G -- Yes --> H[400 Cannot delete your own account]
    G -- No --> I[user.deleteOne]
    I --> J["200, { message: User removed }"]
    J --> K[Frontend removes them from the admin user list]
Loading

Note: deleting a User does not cascade to their Profile, Roadmap, Progress, ChatHistory, or SavedProject documents; those remain in the database, orphaned. Worth naming as a known gap if asked about data integrity.

19. Logout

flowchart TD
    A([User clicks Logout]) --> B[POST /api/auth/logout]
    B --> C[res.cookie 'token' = '', expires immediately]
    C --> D["200, { message: Logged out successfully }"]
    D --> E[AuthContext.setUser null]
    E --> F[Redirect to Landing page]
Loading

Environment Variables

Backend

Variable Required Purpose
MONGO_URI (or MONGO_URL) Yes MongoDB connection string
JWT_SECRET Yes Signs and verifies the auth JWT
CLIENT_URL Recommended in production Comma separated list of allowed CORS origins
GROQ_API_KEY No Enables real AI generation; app runs on fallback data without it
NODE_ENV No production toggles secure cookies, SameSite=None, trust proxy, combined logging
PORT No Defaults to 5000

config/env.js calls process.exit(1) at startup if MONGO_URI or JWT_SECRET is missing, so misconfiguration fails loudly at boot rather than surfacing as confusing runtime errors later.

Frontend

Variable Purpose
VITE_API_URL Base URL the Axios instance targets, e.g. https://your-backend.onrender.com

Local Development

Backend

cd backend
npm install
# create a .env file with MONGO_URI, JWT_SECRET, and optionally GROQ_API_KEY
npm run dev

Runs on http://localhost:5000 by default (or PORT if set), using nodemon for auto-restart.

Frontend

cd frontend
npm install
npm run dev

Runs on the Vite dev server, default http://localhost:5174 unless configured otherwise, matching the CLIENT_URL default in config/env.js.


Deployment

  • Backend on Render. Node/Express service; render.yaml in the backend directory suggests infrastructure-as-code config is checked in. Environment variables (MONGO_URI, JWT_SECRET, CLIENT_URL, GROQ_API_KEY, NODE_ENV=production) are set in Render's dashboard.
  • Frontend on Vercel. vercel.json in the frontend directory configures the deploy; VITE_API_URL points at the Render backend URL.
  • Database. MongoDB, typically MongoDB Atlas's free tier for a project at this scale.
  • Both platforms redeploy automatically on push to the main branch.

Known Limitations and Future Work

  • No CSRF token layer. Security currently rests entirely on SameSite cookie policy and the CORS origin allowlist. A stricter setup would add double submit CSRF tokens on top, as defense in depth.
  • Single active roadmap per user. Generating a new roadmap destroys the old one and its progress. No roadmap history or ability to run multiple learning tracks in parallel.
  • No password reset flow. Registration and login only.
  • AI output isn't schema validated beyond a regex JSON extraction. parseJsonFromText looks for the first {...} or [...] block in the model's response; a sufficiently malformed response still falls through to the fallback, which is safe, but there's no retry or repair step before giving up.
  • Chat history has no pagination or size cap. A very long running conversation grows one MongoDB document indefinitely.
  • No refresh token rotation. The JWT is long lived (30 days) with no revocation mechanism short of changing JWT_SECRET, which would log out every user at once.
  • No cascading deletes. Deleting a User leaves their Profile, Roadmap, Progress, ChatHistory, and SavedProject documents behind as orphaned records.

About

SkillPath AI is a, full-stack platform that generates personalized AI learning roadmaps, offers 24/7 AI doubt-resolution chat, and recommends hands-on projects, aligned with UN SDG 4: Quality Education. Built end-to-end (React, Node/Express, MongoDB, Groq AI, JWT auth) by one developer.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages