diff --git a/README.md b/README.md index 76a4283..2f41548 100644 --- a/README.md +++ b/README.md @@ -15,22 +15,26 @@ A platform for creating and solving coding challenges, built with TypeScript, Re ## Supported Programming Languages Currently supported: + - **TypeScript/JavaScript** - **PHP** - **Go** Coming soon: + - **Python** - Challenges in development ## Screenshots ### Challenge Interface + ![CodeQuest Challenge Interface](assets/codequest-challenge.png) -*Interactive coding environment with real-time testing and AI assistance* +_Interactive coding environment with real-time testing and AI assistance_ ### User Dashboard + ![CodeQuest Dashboard](assets/codequest-dashboard.png) -*Progress tracking dashboard with achievements and statistics* +_Progress tracking dashboard with achievements and statistics_ ## Coquest CLI @@ -38,6 +42,17 @@ If you want to solve challenges from the command line, you can use the [Coquest ## Getting Started +### Quick Start Summary + +1. **Clone** → `git clone https://github.com/crisecheverria/codequest-platform.git` +2. **Install** → `npm install` +3. **Start MongoDB** → `docker run --name mongodb -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME=admin -e MONGO_INITDB_ROOT_PASSWORD=password -d mongo` +4. **Copy env files** → `cp packages/backend/.env.example packages/backend/.env` & `cp packages/frontend/.env.example packages/frontend/.env` +5. **Setup GitHub OAuth** → Create OAuth app, update env files with real credentials +6. **Seed database** → `cd packages/backend && npx ts-node src/scripts/seedChallenges.ts && npx ts-node src/scripts/seedConcepts.ts` +7. **Start** → `npm run dev` +8. **Visit** → `http://localhost:5173` + ### Prerequisites - Node.js 18+ @@ -89,34 +104,124 @@ cp packages/frontend/.env.example packages/frontend/.env To enable user authentication, you'll need to set up GitHub OAuth: -1. Go to GitHub Settings > Developer settings > OAuth Apps -2. Create a new OAuth App with: - - Application name: Code Challenge Platform - - Homepage URL: `http://localhost:5173` - - Authorization callback URL: `http://localhost:3001/api/auth/github/callback` -3. Copy the Client ID and Client Secret -4. Add them to your environment files: +1. Go to [GitHub Settings → Developer settings → OAuth Apps](https://github.com/settings/developers) +2. Click **"New OAuth App"** +3. Fill in the application details: + - **Application name**: `CodeQuest Platform` (or your preferred name) + - **Homepage URL**: `http://localhost:5173` + - **Application description**: `Local development for CodeQuest Platform` (optional) + - **Authorization callback URL**: `http://localhost:3001/api/auth/github/callback` +4. Click **"Register application"** +5. Copy the **Client ID** from the app page +6. Click **"Generate a new client secret"** and copy the **Client Secret** +7. Update your environment files with the real values: -**Backend (.env)**: -``` -GITHUB_CLIENT_ID=your-client-id -GITHUB_CLIENT_SECRET=your-client-secret +**Backend (`packages/backend/.env`)**: + +```bash +# GitHub OAuth Configuration +GITHUB_CLIENT_ID=Ov23liXXXXXXXXXXXXX # Replace with your actual Client ID +GITHUB_CLIENT_SECRET=your-actual-client-secret # Replace with your actual Client Secret GITHUB_CALLBACK_URL=http://localhost:3001/api/auth/github/callback -``` -**Frontend (.env)**: +# MongoDB Configuration (should already be set) +MONGODB_URI=mongodb://admin:password@localhost:27017/code-challenges?authSource=admin +JWT_SECRET=your-secret-key-change-this-in-production +NODE_ENV=development ``` -VITE_GITHUB_CLIENT_ID=your-github-client-id + +**Frontend (`packages/frontend/.env`)**: + +```bash +# GitHub OAuth Configuration +VITE_GITHUB_CLIENT_ID=Ov23liXXXXXXXXXXXXX # Same as backend Client ID VITE_GITHUB_CALLBACK_URL=http://localhost:3001/api/auth/github/callback + +# API Configuration +VITE_API_URL=http://localhost:3001 ``` -6. Start the Development Environment +> ⚠️ **Important**: Replace the example values with your actual GitHub OAuth credentials! + +6. Seed the Database + +Before starting the application, you'll need to populate the database with challenges and concepts: ```bash +# Navigate to the backend package +cd packages/backend + +# Seed challenges +npx ts-node src/scripts/seedChallenges.ts + +# Seed concepts +npx ts-node src/scripts/seedConcepts.ts +``` + +7. Start the Development Environment + +```bash +# From the project root npm run dev ``` -This will start both the frontend and backend services. The frontend will be available at `http://localhost:5173` and the backend at `http://localhost:3001`. +This will start both the frontend and backend services: + +- **Frontend**: `http://localhost:5173` +- **Backend API**: `http://localhost:3001` + +You should now be able to: + +1. Visit `http://localhost:5173` +2. Click "Continue with GitHub" to log in +3. Start solving coding challenges! + +## Database Management + +### Re-seeding Data + +If you need to update or re-seed the database: + +```bash +cd packages/backend + +# Re-seed challenges (updates existing, adds new) +npx ts-node src/scripts/seedChallenges.ts + +# Re-seed concepts +npx ts-node src/scripts/seedConcepts.ts +``` + +### Reset User Progress + +You can reset user progress if needed: + +```bash +cd packages/backend +# Reset all users (with confirmation) +npx ts-node src/scripts/resetUserProgress.ts + +# Reset specific user +npx ts-node src/scripts/resetUserProgress.ts --userId=user_id_here + +# Skip confirmation +npx ts-node src/scripts/resetUserProgress.ts --yes + +# Completely remove progress data instead of resetting +npx ts-node src/scripts/resetUserProgress.ts --remove +``` + +## Viewing the Database with MongoDB Compass + +You can use MongoDB Compass (a GUI for MongoDB) to visualize and interact with your database: + +1. Download and install [MongoDB Compass](https://www.mongodb.com/products/compass) +2. Open MongoDB Compass +3. Connect to your local MongoDB instance with the connection string: + +``` +mongodb://admin:password@localhost:27017/code-challenges?authSource=admin +``` ## AI Assistant @@ -152,65 +257,25 @@ USE_EXTERNAL_AI_API=true The platform supports multiple code execution modes: ### Docker Execution (Default) + All languages (TypeScript, Go, PHP) are executed in secure Docker containers. This is the recommended mode for production. ### Native Go Execution (Optional) + For development environments, you can enable native Go execution for faster compile times: 1. Ensure Go is installed locally (`go version` should work) 2. Add to your `packages/backend/.env`: + ``` USE_NATIVE_GO_EXECUTOR=true NATIVE_GO_TIMEOUT=45000 ``` + 3. Restart the backend server **Note**: Native execution is only available for Go challenges and requires Go to be installed on the host system. -## Seeding the Database - -Before using the application, you'll need to seed the database with initial challenges and concepts: - -```bash -# Navigate to the backend package -cd packages/backend - -# Seed challenges -npx ts-node src/scripts/seedChallenges.ts - -# Seed concepts -npx ts-node src/scripts/seedConcepts.ts -``` - -Also is possible to reset user progress: - -```bash -cd packages/backend -# Reset all users (with confirmation) -npx ts-node src/scripts/resetUserProgress.ts - -# Reset specific user -npx ts-node src/scripts/resetUserProgress.ts --userId=user_id_here - -# Skip confirmation -npx ts-node src/scripts/resetUserProgress.ts --yes - -# Completely remove progress data instead of resetting -npx ts-node src/scripts/resetUserProgress.ts --remove -``` - -## Viewing the Database with MongoDB Compass - -You can use MongoDB Compass (a GUI for MongoDB) to visualize and interact with your database: - -1. Download and install [MongoDB Compass](https://www.mongodb.com/products/compass) -2. Open MongoDB Compass -3. Connect to your local MongoDB instance with the connection string: - -``` -mongodb://admin:password@localhost:27017/code-challenges?authSource=admin -``` - 4. Once connected, you can browse collections like `challenges`, `concepts`, `users`, and more ## Project Structure @@ -244,6 +309,56 @@ The AI Assistant helps users solve challenges without giving away the full solut ## Troubleshooting +### Common Setup Issues + +#### MongoDB Authentication Error + +If you see `Command find requires authentication`: + +1. **Check your backend `.env` file** has the correct MongoDB URI: + + ```bash + MONGODB_URI=mongodb://admin:password@localhost:27017/code-challenges?authSource=admin + ``` + +2. **Restart your development server** after updating environment variables: + ```bash + npm run dev + ``` + +#### GitHub OAuth "404 Not Found" Error + +If GitHub OAuth fails with a 404 error: + +1. **Verify your GitHub OAuth App settings**: + + - Homepage URL: `http://localhost:5173` + - Callback URL: `http://localhost:3001/api/auth/github/callback` + +2. **Check your environment variables** have real values (not placeholders): + + ```bash + # Backend .env should have real values like: + GITHUB_CLIENT_ID=Ov23li... # Not "your-client-id" + GITHUB_CLIENT_SECRET=ghp_... # Not "your-client-secret" + ``` + +3. **Restart the development server** after updating OAuth credentials + +#### Frontend "No routes matched" Error + +If you see routing errors during OAuth: + +- This was a known issue that has been fixed in the current version +- Make sure you're using the latest code + +#### Can't See User Profile Image + +The avatar should appear after logging in. If not: + +- The backend now automatically fetches user profile data including avatar +- Check browser localStorage for user data: `localStorage.getItem('user')` + ### Docker Issues If you encounter issues with MongoDB: @@ -257,6 +372,13 @@ docker logs mongodb # Restart the container if needed docker restart mongodb + +# If container doesn't exist, recreate it: +docker run --name mongodb \ + -p 27017:27017 \ + -e MONGO_INITDB_ROOT_USERNAME=admin \ + -e MONGO_INITDB_ROOT_PASSWORD=password \ + -d mongo ``` ### AI Assistant Issues @@ -294,19 +416,6 @@ code-challenge-platform/ 1. Challenge and concept data is stored in separate JSON files in the `data/` directory. 2. The seed scripts read from these JSON files instead of having hardcoded data. -### Running the Seeds - -You can run the seed scripts from the root of the monorepo: - -```bash -# Regular seeding (skips existing entries) -cd packages/backend -# Seed Challenges -npx ts-node src/scripts/seedChallenges.ts -# Seed Concepts -npx ts-node src/scripts/seedConcepts.ts -``` - ### Contributing New Challenges or Concepts To add new challenges or concepts: @@ -396,22 +505,26 @@ We welcome contributions to the Code Challenge Platform! Here's how you can help ### Types of Contributions #### Adding New Challenges + - Edit `data/challenges.json` to add new coding challenges - Follow the existing format and include comprehensive test cases - Run `npx ts-node src/scripts/seedChallenges.ts` to test your changes #### Adding New Concepts + - Edit `data/concepts.json` to add new learning concepts - Ensure proper categorization and dependencies - Run `npx ts-node src/scripts/seedConcepts.ts` to test your changes #### Code Improvements + - Bug fixes and performance improvements - New features and enhancements - UI/UX improvements - Documentation updates #### Testing + - Write tests for new features - Improve existing test coverage - Report bugs and issues diff --git a/packages/backend/src/routes/auth.ts b/packages/backend/src/routes/auth.ts index a034abd..bffd35b 100644 --- a/packages/backend/src/routes/auth.ts +++ b/packages/backend/src/routes/auth.ts @@ -3,26 +3,20 @@ import jwt, { Secret } from "jsonwebtoken"; import axios from "axios"; import { UserModel } from "../models/User"; import { config } from "../config"; +import { authMiddleware, AuthRequest } from "../middleware/auth"; const router = Router(); // GitHub OAuth callback route (GET) router.get("/github/callback", async (req, res) => { const { code } = req.query; - console.log("Processing GitHub OAuth callback for code:", code); if (!code) { return res.status(400).json({ error: "Authorization code is required" }); } try { - console.log("GitHub OAuth config:", { - clientId: config.github.clientId, - callbackUrl: config.github.callbackUrl, - }); - // Exchange code for access token - console.log("Exchanging code for access token..."); const tokenRes = await axios.post( "https://github.com/login/oauth/access_token", { @@ -35,21 +29,15 @@ router.get("/github/callback", async (req, res) => { headers: { Accept: "application/json", }, - } + }, ); - console.log("Token response received:", { - status: tokenRes.status, - hasAccessToken: !!tokenRes.data.access_token, - }); - const accessToken = tokenRes.data.access_token; if (!accessToken) { throw new Error("No access token received from GitHub"); } // Get user data from GitHub - console.log("Fetching user data from GitHub..."); const userRes = await axios.get("https://api.github.com/user", { headers: { Authorization: `token ${accessToken}`, @@ -57,49 +45,38 @@ router.get("/github/callback", async (req, res) => { }, }); - console.log("GitHub user data received:", { - id: userRes.data.id, - login: userRes.data.login, - hasEmail: !!userRes.data.email, - }); - const githubUser = userRes.data; // Find or create user let user = await UserModel.findOne({ githubId: githubUser.id.toString() }); - console.log("Existing user found:", !!user); if (!user) { - console.log("Creating new user..."); user = await UserModel.create({ githubId: githubUser.id.toString(), username: githubUser.login, email: githubUser.email, avatarUrl: githubUser.avatar_url, }); - console.log("New user created:", user._id); } // Update last login and handle data migration if needed user.lastLogin = new Date(); - + // Check if user has old submission data that needs migration if (user.submissions && user.submissions.length > 0) { - const hasOldSubmissions = user.submissions.some(sub => !sub.challengeSlug && (sub as any).challengeId); - + const hasOldSubmissions = user.submissions.some( + (sub) => !sub.challengeSlug && (sub as any).challengeId, + ); + if (hasOldSubmissions) { - console.log("User has old submission data, clearing submissions for schema compatibility..."); user.submissions = []; user.completedChallenges = []; - console.log("Old submission data cleared"); } } - + await user.save(); - console.log("User last login updated"); // Generate JWT - console.log("Generating JWT..."); const token = jwt.sign( { userId: user._id.toString(), @@ -108,9 +85,8 @@ router.get("/github/callback", async (req, res) => { config.jwt.secret as Secret, { expiresIn: "7d", - } + }, ); - console.log("JWT generated successfully"); // Redirect to frontend with token const frontendUrl = process.env.FRONTEND_URL || "http://localhost:5173"; @@ -129,23 +105,17 @@ router.get("/github/callback", async (req, res) => { }); const frontendUrl = process.env.FRONTEND_URL || "http://localhost:5173"; - res.redirect(`${frontendUrl}/auth/error?message=${encodeURIComponent("Authentication failed")}`); + res.redirect( + `${frontendUrl}/login?error=authentication_failed&message=${encodeURIComponent("Authentication failed")}`, + ); } }); router.post("/github", async (req, res) => { const { code } = req.body; - console.log("Processing GitHub authentication for code:", code); try { - console.log("GitHub OAuth config:", { - clientId: config.github.clientId, - callbackUrl: config.github.callbackUrl, - // Don't log the secret! - }); - // Exchange code for access token - console.log("Exchanging code for access token..."); const tokenRes = await axios.post( "https://github.com/login/oauth/access_token", { @@ -158,21 +128,15 @@ router.post("/github", async (req, res) => { headers: { Accept: "application/json", }, - } + }, ); - console.log("Token response received:", { - status: tokenRes.status, - hasAccessToken: !!tokenRes.data.access_token, - }); - const accessToken = tokenRes.data.access_token; if (!accessToken) { throw new Error("No access token received from GitHub"); } // Get user data from GitHub - console.log("Fetching user data from GitHub..."); const userRes = await axios.get("https://api.github.com/user", { headers: { Authorization: `token ${accessToken}`, @@ -180,51 +144,38 @@ router.post("/github", async (req, res) => { }, }); - console.log("GitHub user data received:", { - id: userRes.data.id, - login: userRes.data.login, - hasEmail: !!userRes.data.email, - }); - const githubUser = userRes.data; // Find or create user let user = await UserModel.findOne({ githubId: githubUser.id.toString() }); - console.log("Existing user found:", !!user); if (!user) { - console.log("Creating new user..."); user = await UserModel.create({ githubId: githubUser.id.toString(), username: githubUser.login, email: githubUser.email, avatarUrl: githubUser.avatar_url, }); - console.log("New user created:", user._id); } // Update last login and handle data migration if needed user.lastLogin = new Date(); - + // Check if user has old submission data that needs migration if (user.submissions && user.submissions.length > 0) { - const hasOldSubmissions = user.submissions.some(sub => !sub.challengeSlug && (sub as any).challengeId); - + const hasOldSubmissions = user.submissions.some( + (sub) => !sub.challengeSlug && (sub as any).challengeId, + ); + if (hasOldSubmissions) { - console.log("User has old submission data, clearing submissions for schema compatibility..."); - // For now, we'll clear old submissions to avoid validation errors - // In production, you'd want to create a proper migration script user.submissions = []; user.completedChallenges = []; - console.log("Old submission data cleared"); } } - + await user.save(); - console.log("User last login updated"); // Generate JWT - console.log("Generating JWT..."); const token = jwt.sign( { userId: user._id.toString(), @@ -233,9 +184,8 @@ router.post("/github", async (req, res) => { config.jwt.secret as Secret, { expiresIn: "7d", - } + }, ); - console.log("JWT generated successfully"); res.json({ token, user }); } catch (error) { @@ -258,4 +208,25 @@ router.post("/github", async (req, res) => { } }); +// Get current user profile +router.get("/me", authMiddleware, async (req, res) => { + try { + const authReq = req as AuthRequest; + const user = authReq.user; + // Return user data without sensitive information + res.json({ + id: user._id, + githubId: user.githubId, + username: user.username, + email: user.email, + avatarUrl: user.avatarUrl, + createdAt: user.createdAt, + lastLogin: user.lastLogin, + }); + } catch (error) { + console.error("Error fetching user profile:", error); + res.status(500).json({ error: "Failed to fetch user profile" }); + } +}); + export default router; diff --git a/packages/frontend/src/components/auth/index.tsx b/packages/frontend/src/components/auth/index.tsx index b542988..ca526dc 100644 --- a/packages/frontend/src/components/auth/index.tsx +++ b/packages/frontend/src/components/auth/index.tsx @@ -1,6 +1,6 @@ // packages/frontend/src/components/auth/index.tsx import { useNavigate, useLocation, useSearchParams } from "react-router-dom"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; import api from "../../api/config"; export const Login = () => { @@ -98,9 +98,16 @@ export const AuthCallback = () => { const navigate = useNavigate(); const [error, setError] = useState(null); const [status, setStatus] = useState("Initializing authentication..."); + const hasProcessed = useRef(false); useEffect(() => { const handleAuth = async () => { + // Prevent double execution in React Strict Mode + if (hasProcessed.current) { + return; + } + hasProcessed.current = true; + try { const urlParams = new URLSearchParams(window.location.search); const code = urlParams.get("code"); @@ -110,7 +117,21 @@ export const AuthCallback = () => { if (token) { setStatus("Processing authentication..."); localStorage.setItem("token", token); - + + // Fetch user data after getting token + try { + setStatus("Fetching user profile..."); + const userResponse = await api.get("/api/auth/me", { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + localStorage.setItem("user", JSON.stringify(userResponse.data)); + } catch (userError) { + console.error("Error fetching user profile:", userError); + // Continue anyway, user profile can be fetched later + } + // Check for return URL const returnTo = localStorage.getItem("returnTo") || "/dashboard"; localStorage.removeItem("returnTo"); // Clean up