diff --git a/docs/phase-1-foundation-plan.md b/docs/phase-1-foundation-plan.md new file mode 100644 index 000000000..c79ce30d9 --- /dev/null +++ b/docs/phase-1-foundation-plan.md @@ -0,0 +1,1334 @@ +# Phase 1: Foundation & Database Implementation Plan + +**Timeline**: Week 1-2 +**Status**: Planning +**Last Updated**: 2025-11-13 + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Prerequisites](#prerequisites) +3. [Database Schema Design](#database-schema-design) +4. [Step-by-Step Implementation](#step-by-step-implementation) +5. [Verification Steps](#verification-steps) +6. [Troubleshooting](#troubleshooting) +7. [Next Steps](#next-steps) + +--- + +## Overview + +This phase establishes the foundational database structure and authentication system for the VetsWhoCode Learning Management System (LMS). We'll create all necessary database models, seed data, and core utilities needed for subsequent phases. + +### Goals + +- ✅ Complete database schema with all LMS models +- ✅ Migration system setup with proper versioning +- ✅ Seed data for testing and development +- ✅ Core utilities (Prisma client, RBAC middleware) +- ✅ Authentication with NextAuth and GitHub OAuth + +### Success Criteria + +- All Prisma models defined and migrated successfully +- Seed script runs without errors and populates test data +- RBAC middleware protects API routes +- Users can authenticate via GitHub OAuth +- All tables visible in Prisma Studio + +--- + +## Prerequisites + +### Required Tools + +- Node.js 18+ installed +- PostgreSQL (default) or SQLite +- Git for version control +- VS Code (recommended) with Prisma extension + +### Existing Codebase + +Check current setup: + +```bash +# Verify Prisma is installed +npm list @prisma/client prisma + +# Check existing schema +cat prisma/schema.prisma + +# Verify NextAuth setup +ls src/pages/api/auth +``` + +--- + +## Database Schema Design + +### Models Overview + +``` +Course (1) ──────< Enrollment (N) ──────> User (1) + │ │ + │ └──────< Progress (N) ──────> Lesson (1) + │ + ├──────< Module (N) + │ │ + │ └──────< Lesson (N) + │ + ├──────< Assignment (N) + │ │ + │ └──────< Submission (N) ──────> User (1) + │ + └──────< Certificate (N) ──────> User (1) + +Cohort (1) ──────< User (N) + +User (1) ──────< Bookmark (N) ──────> Lesson (1) +User (1) ──────< Note (N) ──────> Lesson (1) +``` + +### Core Models + +#### 1. **Course** +```prisma +model Course { + id String @id @default(cuid()) + title String + description String + category String + difficulty String // BEGINNER, INTERMEDIATE, ADVANCED + estimatedHours Int + prerequisites String[] + tags String[] + isPublished Boolean @default(false) + imageUrl String? + modules Module[] + enrollments Enrollment[] + assignments Assignment[] + certificates Certificate[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} +``` + +#### 2. **Module** +```prisma +model Module { + id String @id @default(cuid()) + title String + description String + order Int + courseId String + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + lessons Lesson[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} +``` + +#### 3. **Lesson** +```prisma +model Lesson { + id String @id @default(cuid()) + title String + description String + content String? // Markdown/HTML content + videoUrl String? + duration Int? // in minutes + order Int + type String // CONTENT, VIDEO, EXERCISE, QUIZ, PROJECT + moduleId String + module Module @relation(fields: [moduleId], references: [id], onDelete: Cascade) + progress Progress[] + bookmarks Bookmark[] + notes Note[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} +``` + +#### 4. **Enrollment** +```prisma +model Enrollment { + id String @id @default(cuid()) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + courseId String + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + status String @default("ACTIVE") // ACTIVE, COMPLETED, DROPPED + progress Int @default(0) // 0-100 percentage + startedAt DateTime @default(now()) + completedAt DateTime? + lastAccessedAt DateTime @default(now()) + progressRecords Progress[] + + @@unique([userId, courseId]) +} +``` + +#### 5. **Progress** +```prisma +model Progress { + id String @id @default(cuid()) + enrollmentId String + enrollment Enrollment @relation(fields: [enrollmentId], references: [id], onDelete: Cascade) + lessonId String + lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade) + completed Boolean @default(false) + completedAt DateTime? + timeSpent Int @default(0) // in minutes + + @@unique([enrollmentId, lessonId]) +} +``` + +#### 6. **Assignment** +```prisma +model Assignment { + id String @id @default(cuid()) + title String + description String + courseId String + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + dueDate DateTime? + maxPoints Int @default(100) + submissions Submission[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} +``` + +#### 7. **Submission** +```prisma +model Submission { + id String @id @default(cuid()) + assignmentId String + assignment Assignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + content String // Submission text/link + status String @default("SUBMITTED") // SUBMITTED, GRADED, RETURNED + score Int? + feedback String? + submittedAt DateTime @default(now()) + gradedAt DateTime? + + @@unique([assignmentId, userId]) +} +``` + +#### 8. **Certificate** +```prisma +model Certificate { + id String @id @default(cuid()) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + courseId String + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + issuedAt DateTime @default(now()) + certificateUrl String? + + @@unique([userId, courseId]) +} +``` + +#### 9. **Cohort** +```prisma +model Cohort { + id String @id @default(cuid()) + name String + description String? + startDate DateTime + endDate DateTime? + isElite Boolean @default(false) + members User[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} +``` + +#### 10. **Bookmark** +```prisma +model Bookmark { + id String @id @default(cuid()) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + lessonId String + lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + + @@unique([userId, lessonId]) +} +``` + +#### 11. **Note** +```prisma +model Note { + id String @id @default(cuid()) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + lessonId String + lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade) + content String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} +``` + +--- + +## Step-by-Step Implementation + +### Step 1: Update User Model + +**Task**: Add LMS-related relations to existing User model + +**File**: `prisma/schema.prisma` + +```prisma +model User { + id String @id @default(cuid()) + email String @unique + name String? + image String? + role String @default("user") // user, admin, instructor + cohortId String? + cohort Cohort? @relation(fields: [cohortId], references: [id]) + enrollments Enrollment[] + submissions Submission[] + certificates Certificate[] + bookmarks Bookmark[] + notes Note[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} +``` + +**Checklist**: +- [ ] Add new fields to User model +- [ ] Add all relation fields +- [ ] Verify existing User fields are preserved + +--- + +### Step 2: Define All LMS Models + +**Task**: Add all 11 models to Prisma schema + +**Action**: +1. Open `prisma/schema.prisma` +2. Add each model from the schema design above +3. Ensure proper relations and cascading deletes +4. Add indexes where needed + +**Key Points**: +- Use `@default(cuid())` for IDs +- Use `onDelete: Cascade` for dependent records +- Add `@@unique` constraints for composite keys +- Include `createdAt` and `updatedAt` timestamps + +**Checklist**: +- [ ] Course model defined +- [ ] Module model defined +- [ ] Lesson model defined +- [ ] Enrollment model defined +- [ ] Progress model defined +- [ ] Assignment model defined +- [ ] Submission model defined +- [ ] Certificate model defined +- [ ] Cohort model defined +- [ ] Bookmark model defined +- [ ] Note model defined + +--- + +### Step 3: Create Initial Migration + +**Task**: Generate first migration with all LMS tables + +**Commands**: +```bash +# Generate migration +npx prisma migrate dev --name init_with_lms + +# This will: +# 1. Create migration file in prisma/migrations/ +# 2. Apply migration to database +# 3. Generate Prisma Client +``` + +**Verify**: +```bash +# Check migration was created +ls prisma/migrations/ + +# Should see folder like: 20250113_init_with_lms/ +``` + +**Checklist**: +- [ ] Migration file created +- [ ] Migration applied successfully +- [ ] No errors in terminal +- [ ] Prisma Client regenerated + +--- + +### Step 4: Add Certificates Migration + +**Task**: Ensure Certificate model is properly migrated + +**Note**: If certificates were included in Step 3, this might be a no-op. Otherwise: + +```bash +npx prisma migrate dev --name add_certificates +``` + +**Checklist**: +- [ ] Certificate table exists in database +- [ ] Relations to User and Course working + +--- + +### Step 5: Add Elite Cohort Migration + +**Task**: Add elite cohort functionality + +**Note**: The `isElite` field should already be in Cohort model. Verify: + +```bash +# Check if field exists +npx prisma studio +# Navigate to Cohort model and check for isElite field +``` + +If missing: +```bash +npx prisma migrate dev --name add_elite_cohort_model +``` + +**Checklist**: +- [ ] isElite field exists in Cohort model +- [ ] Can filter cohorts by elite status + +--- + +### Step 6: Verify Database Tables + +**Task**: Confirm all tables created correctly + +**Method 1: Prisma Studio** +```bash +npx prisma studio +``` + +Navigate to each model and verify: +- [ ] Course table +- [ ] Module table +- [ ] Lesson table +- [ ] Enrollment table +- [ ] Progress table +- [ ] Assignment table +- [ ] Submission table +- [ ] Certificate table +- [ ] Cohort table +- [ ] Bookmark table +- [ ] Note table +- [ ] User table (with new fields) + +**Method 2: SQLite CLI** (if using SQLite) +```bash +sqlite3 prisma/dev.db ".tables" +``` + +Should show all model tables. + +--- + +### Step 7: Write Seed Script + +**Task**: Create comprehensive seed data for development + +**File**: `prisma/seed.ts` + +**Structure**: +```typescript +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +async function main() { + console.log('🌱 Starting seed...'); + + // 1. Clean existing data (development only) + await prisma.progress.deleteMany(); + await prisma.enrollment.deleteMany(); + await prisma.submission.deleteMany(); + await prisma.assignment.deleteMany(); + await prisma.lesson.deleteMany(); + await prisma.module.deleteMany(); + await prisma.certificate.deleteMany(); + await prisma.course.deleteMany(); + await prisma.bookmark.deleteMany(); + await prisma.note.deleteMany(); + await prisma.user.deleteMany(); + await prisma.cohort.deleteMany(); + + // 2. Create test cohort + const cohort = await prisma.cohort.create({ + data: { + name: 'Class #13', + description: 'Elite cohort for advanced veterans', + startDate: new Date('2025-01-15'), + endDate: new Date('2025-07-15'), + isElite: true, + }, + }); + + // 3. Create test users + const adminUser = await prisma.user.create({ + data: { + email: 'admin@vetswhocode.io', + name: 'Admin User', + role: 'admin', + cohortId: cohort.id, + }, + }); + + const studentUser = await prisma.user.create({ + data: { + email: 'student@vetswhocode.io', + name: 'Student User', + role: 'user', + cohortId: cohort.id, + }, + }); + + // 4. Create course + const webDevCourse = await prisma.course.create({ + data: { + title: 'Web Development Fundamentals', + description: 'Complete web development course covering HTML, CSS, JavaScript, React, and Node.js', + category: 'Web Development', + difficulty: 'BEGINNER', + estimatedHours: 120, + prerequisites: [], + tags: ['html', 'css', 'javascript', 'react', 'nodejs'], + isPublished: true, + imageUrl: '/images/courses/web-dev.jpg', + }, + }); + + // 5. Create modules with lessons + const htmlModule = await prisma.module.create({ + data: { + title: 'HTML Fundamentals', + description: 'Learn the building blocks of web pages', + order: 1, + courseId: webDevCourse.id, + lessons: { + create: [ + { + title: 'Introduction to HTML', + description: 'Understanding HTML structure and syntax', + content: '# Introduction to HTML\n\nHTML (HyperText Markup Language)...', + type: 'CONTENT', + order: 1, + duration: 30, + }, + { + title: 'HTML Elements and Tags', + description: 'Common HTML elements', + content: '# HTML Elements\n\nLearn about headings, paragraphs, links...', + type: 'CONTENT', + order: 2, + duration: 45, + }, + { + title: 'HTML Forms', + description: 'Creating interactive forms', + videoUrl: 'https://www.youtube.com/watch?v=example', + type: 'VIDEO', + order: 3, + duration: 60, + }, + { + title: 'HTML Practice Exercise', + description: 'Build your first HTML page', + content: '# Exercise\n\nCreate a personal profile page...', + type: 'EXERCISE', + order: 4, + duration: 90, + }, + ], + }, + }, + }); + + const cssModule = await prisma.module.create({ + data: { + title: 'CSS Fundamentals', + description: 'Style your web pages with CSS', + order: 2, + courseId: webDevCourse.id, + lessons: { + create: [ + { + title: 'Introduction to CSS', + description: 'CSS syntax and selectors', + type: 'CONTENT', + order: 1, + duration: 30, + }, + { + title: 'CSS Box Model', + description: 'Understanding margin, padding, border', + type: 'VIDEO', + order: 2, + duration: 45, + }, + { + title: 'CSS Flexbox', + description: 'Modern layout with Flexbox', + type: 'CONTENT', + order: 3, + duration: 60, + }, + { + title: 'CSS Grid', + description: 'Advanced layouts with Grid', + type: 'VIDEO', + order: 4, + duration: 60, + }, + { + title: 'CSS Practice Project', + description: 'Style your HTML page', + type: 'PROJECT', + order: 5, + duration: 120, + }, + ], + }, + }, + }); + + // Add 7 more modules (JavaScript, React, Node.js, etc.) + // ... (abbreviated for brevity) + + // 6. Create assignments + const assignments = await Promise.all([ + prisma.assignment.create({ + data: { + title: 'HTML Portfolio Page', + description: 'Create a personal portfolio page using semantic HTML', + courseId: webDevCourse.id, + dueDate: new Date('2025-02-01'), + maxPoints: 100, + }, + }), + prisma.assignment.create({ + data: { + title: 'CSS Styled Calculator', + description: 'Build a calculator UI using CSS Grid/Flexbox', + courseId: webDevCourse.id, + dueDate: new Date('2025-03-01'), + maxPoints: 100, + }, + }), + prisma.assignment.create({ + data: { + title: 'JavaScript Todo App', + description: 'Create an interactive todo application', + courseId: webDevCourse.id, + dueDate: new Date('2025-04-01'), + maxPoints: 150, + }, + }), + prisma.assignment.create({ + data: { + title: 'Full Stack Blog', + description: 'Build a blog with React frontend and Node.js backend', + courseId: webDevCourse.id, + dueDate: new Date('2025-06-01'), + maxPoints: 200, + }, + }), + ]); + + // 7. Create enrollment for student + const enrollment = await prisma.enrollment.create({ + data: { + userId: studentUser.id, + courseId: webDevCourse.id, + status: 'ACTIVE', + progress: 0, + }, + }); + + console.log('✅ Seed completed successfully!'); + console.log({ + cohort, + users: { admin: adminUser.email, student: studentUser.email }, + course: webDevCourse.title, + modulesCount: 2, // Update with actual count + lessonsCount: 9, // Update with actual count + assignmentsCount: assignments.length, + }); +} + +main() + .catch((e) => { + console.error('❌ Seed failed:', e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); +``` + +**Package.json Configuration**: +```json +{ + "prisma": { + "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts" + } +} +``` + +**Checklist**: +- [ ] seed.ts file created +- [ ] Includes 1 course +- [ ] Includes 9 modules (add 7 more) +- [ ] Includes 39 lessons (add 30 more) +- [ ] Includes 4 assignments +- [ ] Includes test users (admin, student) +- [ ] Includes test cohort +- [ ] package.json configured + +--- + +### Step 8: Run Seed Script + +**Commands**: +```bash +# Install ts-node if not present +npm install -D ts-node + +# Run seed +npx prisma db seed +``` + +**Expected Output**: +``` +🌱 Starting seed... +✅ Seed completed successfully! +{ + cohort: { id: 'xxx', name: 'Class #13' }, + users: { + admin: 'admin@vetswhocode.io', + student: 'student@vetswhocode.io' + }, + course: 'Web Development Fundamentals', + modulesCount: 9, + lessonsCount: 39, + assignmentsCount: 4 +} +``` + +**Verification**: +```bash +# Open Prisma Studio +npx prisma studio + +# Check: +# - 1 Course record +# - 9 Module records +# - 39 Lesson records +# - 4 Assignment records +# - 2 User records +# - 1 Cohort record +# - 1 Enrollment record +``` + +**Checklist**: +- [ ] Seed runs without errors +- [ ] All records created +- [ ] Data visible in Prisma Studio +- [ ] Relations working correctly + +--- + +### Step 9: Create Prisma Client Utility + +**Task**: Create singleton Prisma client for API routes + +**File**: `src/lib/prisma.ts` + +```typescript +import { PrismaClient } from '@prisma/client'; + +const globalForPrisma = global as unknown as { prisma: PrismaClient }; + +export const prisma = + globalForPrisma.prisma || + new PrismaClient({ + log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], + }); + +if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma; + +export default prisma; +``` + +**Usage in API Routes**: +```typescript +import prisma from '@/lib/prisma'; + +export default async function handler(req, res) { + const courses = await prisma.course.findMany(); + res.json(courses); +} +``` + +**Checklist**: +- [ ] File created at `src/lib/prisma.ts` +- [ ] Singleton pattern implemented +- [ ] Logging configured for development +- [ ] Can import in API routes + +--- + +### Step 10: Create RBAC Middleware + +**Task**: Role-based access control for API routes + +**File**: `src/lib/rbac.ts` + +```typescript +import { NextApiRequest, NextApiResponse } from 'next'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/pages/api/auth/[...nextauth]'; + +export type Role = 'user' | 'instructor' | 'admin'; + +export interface AuthenticatedRequest extends NextApiRequest { + user?: { + id: string; + email: string; + role: Role; + }; +} + +/** + * Require authentication for API route + */ +export function requireAuth( + handler: (req: AuthenticatedRequest, res: NextApiResponse) => Promise +) { + return async (req: AuthenticatedRequest, res: NextApiResponse) => { + const session = await getServerSession(req, res, authOptions); + + if (!session?.user) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + req.user = { + id: session.user.id, + email: session.user.email, + role: session.user.role as Role, + }; + + return handler(req, res); + }; +} + +/** + * Require specific role(s) for API route + */ +export function requireRole(roles: Role | Role[]) { + const allowedRoles = Array.isArray(roles) ? roles : [roles]; + + return (handler: (req: AuthenticatedRequest, res: NextApiResponse) => Promise) => { + return requireAuth(async (req, res) => { + if (!req.user || !allowedRoles.includes(req.user.role)) { + return res.status(403).json({ error: 'Forbidden: Insufficient permissions' }); + } + + return handler(req, res); + }); + }; +} + +/** + * Check if user has specific role + */ +export function hasRole(userRole: Role, requiredRoles: Role | Role[]): boolean { + const required = Array.isArray(requiredRoles) ? requiredRoles : [requiredRoles]; + return required.includes(userRole); +} + +/** + * Check if user is admin + */ +export function isAdmin(userRole: Role): boolean { + return userRole === 'admin'; +} + +/** + * Check if user is instructor or admin + */ +export function isInstructorOrAdmin(userRole: Role): boolean { + return ['instructor', 'admin'].includes(userRole); +} +``` + +**Usage Examples**: +```typescript +// Require any authenticated user +export default requireAuth(async (req, res) => { + const courses = await prisma.course.findMany(); + res.json(courses); +}); + +// Require admin role +export default requireRole('admin')(async (req, res) => { + await prisma.course.create({ data: req.body }); + res.json({ success: true }); +}); + +// Require instructor or admin +export default requireRole(['instructor', 'admin'])(async (req, res) => { + await prisma.assignment.create({ data: req.body }); + res.json({ success: true }); +}); +``` + +**Checklist**: +- [ ] File created at `src/lib/rbac.ts` +- [ ] requireAuth function implemented +- [ ] requireRole function implemented +- [ ] Helper functions (hasRole, isAdmin, etc.) +- [ ] TypeScript types defined + +--- + +### Step 11: Configure NextAuth + +**Task**: Setup authentication with GitHub OAuth + +**File**: `src/pages/api/auth/[...nextauth].ts` + +**Prerequisites**: +```bash +# Install dependencies +npm install next-auth @next-auth/prisma-adapter +``` + +**GitHub OAuth Setup**: +1. Go to GitHub Settings > Developer Settings > OAuth Apps +2. Create new OAuth App +3. Set Authorization callback URL: `http://localhost:3000/api/auth/callback/github` +4. Copy Client ID and generate Client Secret + +**Environment Variables**: +```env +# .env +NEXTAUTH_URL=http://localhost:3000 +NEXTAUTH_SECRET=your-random-secret-here + +GITHUB_ID=your_github_client_id +GITHUB_SECRET=your_github_client_secret +``` + +**Implementation**: +```typescript +import NextAuth, { NextAuthOptions } from 'next-auth'; +import GithubProvider from 'next-auth/providers/github'; +import { PrismaAdapter } from '@next-auth/prisma-adapter'; +import prisma from '@/lib/prisma'; + +export const authOptions: NextAuthOptions = { + adapter: PrismaAdapter(prisma), + providers: [ + GithubProvider({ + clientId: process.env.GITHUB_ID!, + clientSecret: process.env.GITHUB_SECRET!, + }), + ], + session: { + strategy: 'jwt', + }, + callbacks: { + async jwt({ token, user }) { + if (user) { + token.id = user.id; + token.role = user.role || 'user'; + } + return token; + }, + async session({ session, token }) { + if (session.user) { + session.user.id = token.id as string; + session.user.role = token.role as string; + } + return session; + }, + }, + pages: { + signIn: '/auth/signin', + error: '/auth/error', + }, +}; + +export default NextAuth(authOptions); +``` + +**TypeScript Types**: + +**File**: `src/types/next-auth.d.ts` +```typescript +import 'next-auth'; + +declare module 'next-auth' { + interface User { + role?: string; + } + + interface Session { + user: { + id: string; + email: string; + name?: string; + image?: string; + role: string; + }; + } +} + +declare module 'next-auth/jwt' { + interface JWT { + id: string; + role: string; + } +} +``` + +**Prisma Schema Update**: +```prisma +// Add NextAuth models +model Account { + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? + access_token String? + expires_at Int? + token_type String? + scope String? + id_token String? + session_state String? + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) +} + +model Session { + id String @id @default(cuid()) + sessionToken String @unique + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +model User { + // ... existing fields + accounts Account[] + sessions Session[] +} + +model VerificationToken { + identifier String + token String @unique + expires DateTime + + @@unique([identifier, token]) +} +``` + +**Migration**: +```bash +npx prisma migrate dev --name add_nextauth_models +``` + +**Checklist**: +- [ ] NextAuth installed +- [ ] GitHub OAuth app created +- [ ] Environment variables set +- [ ] [...nextauth].ts configured +- [ ] TypeScript types created +- [ ] Prisma schema updated +- [ ] Migration applied +- [ ] Can sign in with GitHub + +--- + +## Verification Steps + +### Complete Verification Checklist + +Run through these steps to ensure Phase 1 is complete: + +#### Database Verification + +```bash +# 1. Check all tables exist +npx prisma studio +``` + +Verify tables: +- [ ] User (with role, cohortId) +- [ ] Cohort +- [ ] Course +- [ ] Module +- [ ] Lesson +- [ ] Enrollment +- [ ] Progress +- [ ] Assignment +- [ ] Submission +- [ ] Certificate +- [ ] Bookmark +- [ ] Note +- [ ] Account (NextAuth) +- [ ] Session (NextAuth) +- [ ] VerificationToken (NextAuth) + +#### Seed Data Verification + +Check in Prisma Studio: +- [ ] 1 Course record exists +- [ ] 9 Module records exist +- [ ] 39 Lesson records exist +- [ ] 4 Assignment records exist +- [ ] 2 User records exist (admin, student) +- [ ] 1 Cohort record exists +- [ ] 1 Enrollment record exists + +#### File Structure Verification + +```bash +# Check all files created +ls -la src/lib/ +ls -la prisma/ +``` + +Files should exist: +- [ ] `src/lib/prisma.ts` +- [ ] `src/lib/rbac.ts` +- [ ] `src/pages/api/auth/[...nextauth].ts` +- [ ] `src/types/next-auth.d.ts` +- [ ] `prisma/schema.prisma` (updated) +- [ ] `prisma/seed.ts` +- [ ] `prisma/migrations/` (with migration files) + +#### Authentication Verification + +```bash +# Start dev server +npm run dev +``` + +Test: +- [ ] Navigate to http://localhost:3000 +- [ ] Click "Sign In" (or navigate to /api/auth/signin) +- [ ] GitHub OAuth flow works +- [ ] User is redirected back after auth +- [ ] Session persists on refresh + +#### API Route Test + +Create test file: `src/pages/api/courses/index.ts` + +```typescript +import { NextApiResponse } from 'next'; +import { requireAuth, AuthenticatedRequest } from '@/lib/rbac'; +import prisma from '@/lib/prisma'; + +export default requireAuth(async (req: AuthenticatedRequest, res: NextApiResponse) => { + const courses = await prisma.course.findMany({ + include: { + modules: { + include: { + lessons: true, + }, + }, + }, + }); + + res.json({ + user: req.user, + courses, + }); +}); +``` + +Test: +```bash +# Should return 401 without auth +curl http://localhost:3000/api/courses + +# Sign in via browser, then test with session +# Should return courses data +``` + +- [ ] Unauthorized request returns 401 +- [ ] Authenticated request returns data +- [ ] User info attached to request +- [ ] Prisma queries work + +--- + +## Troubleshooting + +### Common Issues + +#### 1. Migration Fails + +**Error**: `Foreign key constraint failed` + +**Solution**: +```bash +# Reset database (development only!) +npx prisma migrate reset + +# Re-run migrations +npx prisma migrate dev +``` + +#### 2. Seed Script Fails + +**Error**: `Cannot find module 'ts-node'` + +**Solution**: +```bash +npm install -D ts-node @types/node +``` + +**Error**: `Unique constraint failed` + +**Solution**: The seed script is trying to create duplicate data. Run: +```bash +npx prisma migrate reset --skip-seed +npx prisma db seed +``` + +#### 3. Prisma Client Not Found + +**Error**: `Cannot find module '@prisma/client'` + +**Solution**: +```bash +npm install @prisma/client +npx prisma generate +``` + +#### 4. NextAuth Session Not Working + +**Error**: Session is null + +**Checklist**: +- [ ] NEXTAUTH_URL set correctly in .env +- [ ] NEXTAUTH_SECRET is set (generate with: `openssl rand -base64 32`) +- [ ] GitHub OAuth credentials correct +- [ ] Callback URL matches in GitHub app settings +- [ ] NextAuth models in Prisma schema +- [ ] Migration applied for NextAuth tables + +#### 5. RBAC Middleware Not Working + +**Error**: `req.user is undefined` + +**Solution**: Ensure getServerSession is called and authOptions are imported: +```typescript +import { authOptions } from '@/pages/api/auth/[...nextauth]'; +const session = await getServerSession(req, res, authOptions); +``` + +#### 6. TypeScript Errors + +**Error**: Property 'role' does not exist on type 'User' + +**Solution**: +- Ensure `src/types/next-auth.d.ts` exists +- Restart TypeScript server in VS Code: Cmd+Shift+P > "TypeScript: Restart TS Server" + +--- + +## Next Steps + +Once Phase 1 is complete, proceed to: + +### Phase 2: Core API Endpoints (Week 3-4) +- Course CRUD endpoints +- Enrollment endpoints +- Progress tracking endpoints +- Assignment submission endpoints + +### Phase 3: Frontend Components (Week 5-6) +- Course catalog UI +- Course detail pages +- Lesson viewer +- Progress dashboard + +### Phase 4: Advanced Features (Week 7-8) +- Certificate generation +- Cohort management +- Bookmarks and notes +- Search and filtering + +--- + +## Resources + +### Documentation +- [Prisma Docs](https://www.prisma.io/docs) +- [NextAuth Docs](https://next-auth.js.org) +- [Next.js API Routes](https://nextjs.org/docs/api-routes/introduction) + +### Tools +- [Prisma Studio](https://www.prisma.io/studio) - Database GUI +- [SQLite Browser](https://sqlitebrowser.org) - SQLite database viewer + +### Code Examples +- Check existing codebase in `/src/pages/api/` for API patterns +- Reference similar projects for NextAuth integration + +--- + +## Completion Criteria + +Phase 1 is complete when: + +- ✅ All 11 LMS models defined in Prisma schema +- ✅ Migrations applied successfully +- ✅ Database verified in Prisma Studio +- ✅ Seed script runs and populates test data +- ✅ Prisma client utility created +- ✅ RBAC middleware implemented +- ✅ NextAuth configured with GitHub OAuth +- ✅ Test API route works with authentication +- ✅ All verification steps pass + +**Sign-off**: Review this checklist with your team before proceeding to Phase 2. + +--- + +**Document Version**: 1.0 +**Last Updated**: 2025-11-13 +**Author**: VetsWhoCode Development Team diff --git a/package.json b/package.json index 6d618eaab..4a16f16ee 100644 --- a/package.json +++ b/package.json @@ -118,5 +118,8 @@ "lint-staged": { "*.{js,jsx,ts,tsx}": "eslint --cache --fix", "*": "prettier --write --ignore-unknown" + }, + "prisma": { + "seed": "tsx prisma/seed.ts" } } diff --git a/prisma/migrations/20251113110344_add_lms_tables/migration.sql b/prisma/migrations/20251113110344_add_lms_tables/migration.sql new file mode 100644 index 000000000..ced504c7a --- /dev/null +++ b/prisma/migrations/20251113110344_add_lms_tables/migration.sql @@ -0,0 +1,269 @@ +-- CreateEnum +CREATE TYPE "UserRole" AS ENUM ('STUDENT', 'INSTRUCTOR', 'ADMIN', 'MENTOR'); + +-- CreateEnum +CREATE TYPE "SkillLevel" AS ENUM ('NEWBIE', 'BEGINNER', 'JUNIOR', 'MID', 'SENIOR'); + +-- CreateEnum +CREATE TYPE "Difficulty" AS ENUM ('BEGINNER', 'INTERMEDIATE', 'ADVANCED'); + +-- CreateEnum +CREATE TYPE "LessonType" AS ENUM ('CONTENT', 'VIDEO', 'EXERCISE', 'QUIZ', 'PROJECT'); + +-- CreateEnum +CREATE TYPE "EnrollmentStatus" AS ENUM ('ACTIVE', 'COMPLETED', 'DROPPED', 'PAUSED'); + +-- CreateEnum +CREATE TYPE "AssignmentType" AS ENUM ('PROJECT', 'HOMEWORK', 'CAPSTONE', 'QUIZ'); + +-- CreateEnum +CREATE TYPE "SubmissionStatus" AS ENUM ('DRAFT', 'SUBMITTED', 'GRADED', 'NEEDS_REVISION'); + +-- AlterTable +ALTER TABLE "User" ADD COLUMN "assessmentDate" TIMESTAMP(3), +ADD COLUMN "assessmentScore" INTEGER, +ADD COLUMN "bio" TEXT, +ADD COLUMN "branch" TEXT, +ADD COLUMN "cohortId" TEXT, +ADD COLUMN "deployments" TEXT, +ADD COLUMN "githubUrl" TEXT, +ADD COLUMN "graduationDate" TIMESTAMP(3), +ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true, +ADD COLUMN "linkedinUrl" TEXT, +ADD COLUMN "location" TEXT, +ADD COLUMN "mos" TEXT, +ADD COLUMN "rank" TEXT, +ADD COLUMN "role" "UserRole" NOT NULL DEFAULT 'STUDENT', +ADD COLUMN "skillLevel" "SkillLevel", +ADD COLUMN "skills" TEXT, +ADD COLUMN "title" TEXT, +ADD COLUMN "websiteUrl" TEXT, +ADD COLUMN "yearsServed" INTEGER; + +-- CreateTable +CREATE TABLE "Course" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "imageUrl" TEXT, + "difficulty" "Difficulty" NOT NULL DEFAULT 'BEGINNER', + "category" TEXT NOT NULL, + "isPublished" BOOLEAN NOT NULL DEFAULT false, + "estimatedHours" INTEGER, + "prerequisites" TEXT[], + "tags" TEXT[], + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Course_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Module" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "order" INTEGER NOT NULL, + "courseId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Module_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Lesson" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "videoUrl" TEXT, + "duration" INTEGER, + "order" INTEGER NOT NULL, + "type" "LessonType" NOT NULL DEFAULT 'CONTENT', + "codeExample" TEXT, + "resources" TEXT, + "moduleId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Lesson_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Enrollment" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "courseId" TEXT NOT NULL, + "status" "EnrollmentStatus" NOT NULL DEFAULT 'ACTIVE', + "progress" DOUBLE PRECISION NOT NULL DEFAULT 0, + "enrolledAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "completedAt" TIMESTAMP(3), + "lastActivity" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Enrollment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Progress" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "lessonId" TEXT NOT NULL, + "completed" BOOLEAN NOT NULL DEFAULT false, + "timeSpent" INTEGER NOT NULL DEFAULT 0, + "startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "completedAt" TIMESTAMP(3), + + CONSTRAINT "Progress_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Assignment" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT NOT NULL, + "instructions" TEXT NOT NULL, + "dueDate" TIMESTAMP(3), + "maxPoints" INTEGER NOT NULL DEFAULT 100, + "type" "AssignmentType" NOT NULL DEFAULT 'PROJECT', + "githubRepo" BOOLEAN NOT NULL DEFAULT false, + "liveDemo" BOOLEAN NOT NULL DEFAULT false, + "courseId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Assignment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Submission" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "assignmentId" TEXT NOT NULL, + "githubUrl" TEXT, + "liveUrl" TEXT, + "notes" TEXT, + "files" TEXT, + "status" "SubmissionStatus" NOT NULL DEFAULT 'SUBMITTED', + "score" INTEGER, + "feedback" TEXT, + "submittedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "gradedAt" TIMESTAMP(3), + + CONSTRAINT "Submission_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Cohort" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "startDate" TIMESTAMP(3) NOT NULL, + "endDate" TIMESTAMP(3), + "isElite" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Cohort_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Certificate" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "courseId" TEXT NOT NULL, + "certificateUrl" TEXT, + "issuedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Certificate_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Bookmark" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "lessonId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Bookmark_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Note" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "lessonId" TEXT NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Note_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Module_courseId_order_key" ON "Module"("courseId", "order"); + +-- CreateIndex +CREATE UNIQUE INDEX "Lesson_moduleId_order_key" ON "Lesson"("moduleId", "order"); + +-- CreateIndex +CREATE UNIQUE INDEX "Enrollment_userId_courseId_key" ON "Enrollment"("userId", "courseId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Progress_userId_lessonId_key" ON "Progress"("userId", "lessonId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Submission_userId_assignmentId_key" ON "Submission"("userId", "assignmentId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Certificate_userId_courseId_key" ON "Certificate"("userId", "courseId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Bookmark_userId_lessonId_key" ON "Bookmark"("userId", "lessonId"); + +-- AddForeignKey +ALTER TABLE "User" ADD CONSTRAINT "User_cohortId_fkey" FOREIGN KEY ("cohortId") REFERENCES "Cohort"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Module" ADD CONSTRAINT "Module_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Lesson" ADD CONSTRAINT "Lesson_moduleId_fkey" FOREIGN KEY ("moduleId") REFERENCES "Module"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Enrollment" ADD CONSTRAINT "Enrollment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Enrollment" ADD CONSTRAINT "Enrollment_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Progress" ADD CONSTRAINT "Progress_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Progress" ADD CONSTRAINT "Progress_lessonId_fkey" FOREIGN KEY ("lessonId") REFERENCES "Lesson"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Submission" ADD CONSTRAINT "Submission_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Submission" ADD CONSTRAINT "Submission_assignmentId_fkey" FOREIGN KEY ("assignmentId") REFERENCES "Assignment"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Certificate" ADD CONSTRAINT "Certificate_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Certificate" ADD CONSTRAINT "Certificate_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Bookmark" ADD CONSTRAINT "Bookmark_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Bookmark" ADD CONSTRAINT "Bookmark_lessonId_fkey" FOREIGN KEY ("lessonId") REFERENCES "Lesson"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Note" ADD CONSTRAINT "Note_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Note" ADD CONSTRAINT "Note_lessonId_fkey" FOREIGN KEY ("lessonId") REFERENCES "Lesson"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8a3fe102c..4617b6cbb 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -10,7 +10,7 @@ generator client { } datasource db { - provider = "sqlite" + provider = "postgresql" url = env("DATABASE_URL") } @@ -44,19 +44,23 @@ model User { // Platform metadata role UserRole @default(STUDENT) isActive Boolean @default(true) - cohort String? // Which cohort they belong to + cohortId String? // Which cohort they belong to + cohort Cohort? @relation(fields: [cohortId], references: [id]) graduationDate DateTime? // When they graduated/completed program // Assessment and skill level skillLevel SkillLevel? // Assessed coding skill level assessmentDate DateTime? // When last assessed assessmentScore Int? // Score from last assessment (0-100) - + // Learning relationships enrollments Enrollment[] submissions Submission[] progress Progress[] - + certificates Certificate[] + bookmarks Bookmark[] + notes Note[] + createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } @@ -140,19 +144,21 @@ model Course { imageUrl String? difficulty Difficulty @default(BEGINNER) category String // "Web Development", "DevOps", "Data Science", etc. - + // Course structure modules Module[] - + // Metadata isPublished Boolean @default(false) - duration Int? // Estimated duration in hours - prerequisites String? // JSON string of prerequisite skills/courses - + estimatedHours Int? // Estimated duration in hours + prerequisites String[] // Array of prerequisite skills/courses + tags String[] // Array of tags for categorization and search + // Relationships enrollments Enrollment[] assignments Assignment[] - + certificates Certificate[] + createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } @@ -197,10 +203,12 @@ model Lesson { moduleId String module Module @relation(fields: [moduleId], references: [id], onDelete: Cascade) progress Progress[] - + bookmarks Bookmark[] + notes Note[] + createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - + @@unique([moduleId, order]) } @@ -317,4 +325,69 @@ enum SubmissionStatus { SUBMITTED GRADED NEEDS_REVISION +} + +// Cohort Model - Groups of students learning together +model Cohort { + id String @id @default(cuid()) + name String // e.g., "Class #13", "Spring 2025 Cohort" + description String? // Description of the cohort + startDate DateTime // When the cohort starts + endDate DateTime? // When the cohort ends (optional for ongoing) + isElite Boolean @default(false) // Elite/advanced cohort designation + + // Relationships + members User[] // Students in this cohort + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// Certificate Model - Course completion certificates +model Certificate { + id String @id @default(cuid()) + userId String + courseId String + certificateUrl String? // URL to the certificate image/PDF + + // Timestamps + issuedAt DateTime @default(now()) + + // Relationships + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + + @@unique([userId, courseId]) +} + +// Bookmark Model - Students can bookmark lessons for quick access +model Bookmark { + id String @id @default(cuid()) + userId String + lessonId String + + // Timestamps + createdAt DateTime @default(now()) + + // Relationships + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade) + + @@unique([userId, lessonId]) +} + +// Note Model - Students can take notes on lessons +model Note { + id String @id @default(cuid()) + userId String + lessonId String + content String // The note content + + // Timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Relationships + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade) } \ No newline at end of file diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 000000000..943d24d17 --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,478 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +async function main() { + console.log('🌱 Starting seed...'); + + // 1. Clean existing LMS data (in correct order to avoid foreign key constraints) + console.log('📝 Cleaning existing data...'); + await prisma.progress.deleteMany(); + await prisma.bookmark.deleteMany(); + await prisma.note.deleteMany(); + await prisma.submission.deleteMany(); + await prisma.assignment.deleteMany(); + await prisma.lesson.deleteMany(); + await prisma.module.deleteMany(); + await prisma.certificate.deleteMany(); + await prisma.enrollment.deleteMany(); + await prisma.course.deleteMany(); + + // 2. Create test cohort + console.log('👥 Creating test cohort...'); + const cohort = await prisma.cohort.create({ + data: { + name: 'Class #13', + description: 'Elite cohort for advanced veterans transitioning to tech careers', + startDate: new Date('2025-01-15'), + endDate: new Date('2025-07-15'), + isElite: true, + }, + }); + + // 3. Create test users + console.log('👤 Creating test users...'); + const adminUser = await prisma.user.upsert({ + where: { email: 'admin@vetswhocode.io' }, + update: { + role: 'ADMIN', + cohortId: cohort.id, + }, + create: { + email: 'admin@vetswhocode.io', + name: 'Admin User', + role: 'ADMIN', + cohortId: cohort.id, + bio: 'VetsWhoCode platform administrator', + }, + }); + + const instructorUser = await prisma.user.upsert({ + where: { email: 'instructor@vetswhocode.io' }, + update: { + role: 'INSTRUCTOR', + cohortId: cohort.id, + }, + create: { + email: 'instructor@vetswhocode.io', + name: 'Instructor User', + role: 'INSTRUCTOR', + cohortId: cohort.id, + bio: 'Senior instructor teaching web development', + }, + }); + + const studentUser = await prisma.user.upsert({ + where: { email: 'student@vetswhocode.io' }, + update: { + role: 'STUDENT', + cohortId: cohort.id, + }, + create: { + email: 'student@vetswhocode.io', + name: 'Student User', + role: 'STUDENT', + cohortId: cohort.id, + bio: 'Army veteran learning full-stack web development', + branch: 'Army', + rank: 'Sergeant', + yearsServed: 6, + skillLevel: 'BEGINNER', + }, + }); + + // 4. Create comprehensive Web Development course + console.log('📚 Creating Web Development Fundamentals course...'); + const webDevCourse = await prisma.course.create({ + data: { + title: 'Web Development Fundamentals', + description: 'Complete web development course covering HTML, CSS, JavaScript, React, and Node.js. Perfect for veterans transitioning to tech careers.', + category: 'Web Development', + difficulty: 'BEGINNER', + estimatedHours: 120, + prerequisites: [], + tags: ['html', 'css', 'javascript', 'react', 'nodejs', 'fullstack'], + isPublished: true, + imageUrl: '/images/courses/web-dev.jpg', + }, + }); + + // 5. Create modules with lessons + console.log('📖 Creating modules and lessons...'); + + // MODULE 1: HTML Fundamentals + const htmlModule = await prisma.module.create({ + data: { + title: 'HTML Fundamentals', + description: 'Learn the building blocks of web pages with HTML', + order: 1, + courseId: webDevCourse.id, + lessons: { + create: [ + { + title: 'Introduction to HTML', + content: '# Introduction to HTML\n\nHTML (HyperText Markup Language) is the standard markup language for creating web pages. In this lesson, you will learn the basics of HTML structure and syntax.', + type: 'CONTENT', + order: 1, + duration: 30, + }, + { + title: 'HTML Elements and Tags', + content: '# HTML Elements\n\nLearn about common HTML elements including headings, paragraphs, links, and images. Practice using semantic HTML for better accessibility.', + type: 'CONTENT', + order: 2, + duration: 45, + }, + { + title: 'HTML Forms', + content: '# HTML Forms\n\nForms are essential for user interaction. Learn how to create forms with inputs, buttons, and validation.', + videoUrl: 'https://www.youtube.com/watch?v=fNcJuPIZ2WE', + type: 'VIDEO', + order: 3, + duration: 60, + }, + { + title: 'HTML Practice Exercise', + content: '# Exercise: Build Your First HTML Page\n\nCreate a personal profile page using semantic HTML elements. Include:\n- Header with your name\n- Navigation menu\n- About section\n- Contact form', + type: 'EXERCISE', + order: 4, + duration: 90, + }, + ], + }, + }, + }); + + // MODULE 2: CSS Fundamentals + const cssModule = await prisma.module.create({ + data: { + title: 'CSS Fundamentals', + description: 'Style your web pages with modern CSS techniques', + order: 2, + courseId: webDevCourse.id, + lessons: { + create: [ + { + title: 'Introduction to CSS', + content: '# CSS Basics\n\nCSS (Cascading Style Sheets) controls the visual presentation of HTML. Learn selectors, properties, and values.', + type: 'CONTENT', + order: 1, + duration: 30, + }, + { + title: 'CSS Box Model', + content: '# The Box Model\n\nUnderstanding margin, padding, border, and content is crucial for layout. Master the CSS box model.', + type: 'VIDEO', + videoUrl: 'https://www.youtube.com/watch?v=rIO5326FgPE', + order: 2, + duration: 45, + }, + { + title: 'CSS Flexbox', + content: '# Flexbox Layout\n\nFlexbox is a powerful layout system for creating responsive designs. Learn how to align and distribute items.', + type: 'CONTENT', + order: 3, + duration: 60, + }, + { + title: 'CSS Grid', + content: '# CSS Grid Layout\n\nCSS Grid enables complex two-dimensional layouts. Master grid for professional designs.', + type: 'VIDEO', + videoUrl: 'https://www.youtube.com/watch?v=EFafSYg-PkI', + order: 4, + duration: 60, + }, + { + title: 'CSS Styling Project', + content: '# Project: Style Your Profile Page\n\nApply CSS to the HTML page you created. Use Flexbox or Grid for layout.', + type: 'PROJECT', + order: 5, + duration: 120, + }, + ], + }, + }, + }); + + // MODULE 3: JavaScript Basics + const jsModule = await prisma.module.create({ + data: { + title: 'JavaScript Basics', + description: 'Add interactivity to your web pages with JavaScript', + order: 3, + courseId: webDevCourse.id, + lessons: { + create: [ + { + title: 'JavaScript Introduction', + content: '# Getting Started with JavaScript\n\nJavaScript brings your web pages to life. Learn variables, data types, and basic syntax.', + type: 'CONTENT', + order: 1, + duration: 45, + }, + { + title: 'Functions and Scope', + content: '# JavaScript Functions\n\nFunctions are reusable blocks of code. Understand function declaration, parameters, and scope.', + type: 'CONTENT', + order: 2, + duration: 60, + }, + { + title: 'DOM Manipulation', + content: '# Working with the DOM\n\nThe Document Object Model (DOM) allows JavaScript to interact with HTML. Learn to select and modify elements.', + type: 'VIDEO', + videoUrl: 'https://www.youtube.com/watch?v=y17RuWkWdn8', + order: 3, + duration: 75, + }, + { + title: 'Events and Event Handlers', + content: '# JavaScript Events\n\nRespond to user actions like clicks, hovers, and form submissions with event handlers.', + type: 'CONTENT', + order: 4, + duration: 60, + }, + { + title: 'Interactive Todo App', + content: '# Project: Build a Todo App\n\nCreate an interactive todo application using vanilla JavaScript. Add, remove, and mark items complete.', + type: 'PROJECT', + order: 5, + duration: 150, + }, + ], + }, + }, + }); + + // MODULE 4: React Fundamentals + const reactModule = await prisma.module.create({ + data: { + title: 'React Fundamentals', + description: 'Build modern user interfaces with React', + order: 4, + courseId: webDevCourse.id, + lessons: { + create: [ + { + title: 'Introduction to React', + content: '# What is React?\n\nReact is a JavaScript library for building user interfaces. Learn about components and the virtual DOM.', + type: 'CONTENT', + order: 1, + duration: 45, + }, + { + title: 'React Components and Props', + content: '# Components and Props\n\nComponents are the building blocks of React apps. Learn to create and compose components.', + type: 'VIDEO', + videoUrl: 'https://www.youtube.com/watch?v=Tn6-PIqc4UM', + order: 2, + duration: 60, + }, + { + title: 'State and Hooks', + content: '# React State Management\n\nState allows components to be dynamic. Master useState and other React hooks.', + type: 'CONTENT', + order: 3, + duration: 90, + }, + { + title: 'React Router', + content: '# Navigation with React Router\n\nCreate multi-page applications with React Router. Learn routing and navigation.', + type: 'CONTENT', + order: 4, + duration: 60, + }, + ], + }, + }, + }); + + // MODULE 5: Node.js and APIs + const nodeModule = await prisma.module.create({ + data: { + title: 'Node.js and Backend Development', + description: 'Build server-side applications with Node.js', + order: 5, + courseId: webDevCourse.id, + lessons: { + create: [ + { + title: 'Introduction to Node.js', + content: '# Node.js Basics\n\nNode.js allows JavaScript to run on the server. Learn about modules, npm, and the Node ecosystem.', + type: 'CONTENT', + order: 1, + duration: 45, + }, + { + title: 'Express.js Framework', + content: '# Building APIs with Express\n\nExpress is a minimal web framework for Node.js. Create RESTful APIs.', + type: 'VIDEO', + videoUrl: 'https://www.youtube.com/watch?v=L72fhGm1tfE', + order: 2, + duration: 90, + }, + { + title: 'Databases and ORMs', + content: '# Working with Databases\n\nConnect to databases using ORMs like Prisma. Perform CRUD operations.', + type: 'CONTENT', + order: 3, + duration: 75, + }, + ], + }, + }, + }); + + // 6. Create assignments + console.log('📝 Creating assignments...'); + const assignments = await Promise.all([ + prisma.assignment.create({ + data: { + title: 'HTML Portfolio Page', + description: 'Create a personal portfolio page using semantic HTML', + instructions: 'Build a complete portfolio page that includes:\n- Semantic HTML structure\n- Header with navigation\n- About section\n- Projects section\n- Contact form\n\nUse proper HTML5 semantic elements.', + courseId: webDevCourse.id, + dueDate: new Date('2025-02-01'), + maxPoints: 100, + type: 'PROJECT', + }, + }), + prisma.assignment.create({ + data: { + title: 'CSS Styled Calculator', + description: 'Build a calculator UI using CSS Grid/Flexbox', + instructions: 'Create a fully styled calculator interface:\n- Use CSS Grid or Flexbox\n- Responsive design\n- Modern styling with hover effects\n- Clean, professional appearance', + courseId: webDevCourse.id, + dueDate: new Date('2025-03-01'), + maxPoints: 100, + type: 'PROJECT', + }, + }), + prisma.assignment.create({ + data: { + title: 'JavaScript Todo App', + description: 'Create an interactive todo application', + instructions: 'Build a functional todo app with:\n- Add new todos\n- Mark todos as complete\n- Delete todos\n- Filter todos (all, active, completed)\n- Use local storage for persistence', + courseId: webDevCourse.id, + dueDate: new Date('2025-04-01'), + maxPoints: 150, + type: 'PROJECT', + githubRepo: true, + }, + }), + prisma.assignment.create({ + data: { + title: 'Full Stack Blog Application', + description: 'Build a blog with React frontend and Node.js backend', + instructions: 'Create a full-stack blog application:\n- React frontend with routing\n- Node.js/Express backend\n- Database integration\n- CRUD operations for posts\n- User authentication\n- Deploy to a hosting platform', + courseId: webDevCourse.id, + dueDate: new Date('2025-06-01'), + maxPoints: 200, + type: 'CAPSTONE', + githubRepo: true, + liveDemo: true, + }, + }), + ]); + + // 7. Create enrollment for student + console.log('🎓 Creating enrollments...'); + const enrollment = await prisma.enrollment.create({ + data: { + userId: studentUser.id, + courseId: webDevCourse.id, + status: 'ACTIVE', + progress: 15.5, + }, + }); + + // 8. Create some progress records + console.log('📊 Creating progress records...'); + const htmlLessons = await prisma.lesson.findMany({ + where: { moduleId: htmlModule.id }, + orderBy: { order: 'asc' }, + }); + + // Mark first 2 HTML lessons as completed + for (let i = 0; i < 2; i++) { + await prisma.progress.create({ + data: { + userId: studentUser.id, + lessonId: htmlLessons[i].id, + completed: true, + completedAt: new Date(), + timeSpent: htmlLessons[i].duration || 30, + }, + }); + } + + // 9. Create a bookmark + console.log('🔖 Creating bookmarks...'); + await prisma.bookmark.create({ + data: { + userId: studentUser.id, + lessonId: htmlLessons[2].id, + }, + }); + + // 10. Create a note + console.log('📓 Creating notes...'); + await prisma.note.create({ + data: { + userId: studentUser.id, + lessonId: htmlLessons[1].id, + content: 'Remember: Always use semantic HTML for better accessibility and SEO. The
,