diff --git a/controllers/ai/index.ts b/controllers/ai/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..436217f7c5ef2d456c737ed3d917d7f1ec325e76
--- /dev/null
+++ b/controllers/ai/index.ts
@@ -0,0 +1,219 @@
+import { Request, Response } from "express";
+import prisma from "../../prisma/client";
+import { getGeminiResponse } from "../../utils/gemini";
+
+export const chatWithAI = async (req: Request, res: Response) => {
+ const userId = (req as any).user?.id;
+ const { message, projectId } = req.body;
+
+ try {
+ let projectContext = "";
+ let difficultyMode: string | null = null;
+
+ // 1. Fetch project context if projectId is provided
+ if (projectId) {
+ const project = await prisma.project.findUnique({
+ where: { id: projectId },
+ include: {
+ milestones: true,
+ userProjects: { where: { userId } },
+ },
+ });
+
+ if (project) {
+ difficultyMode =
+ project.userProjects?.[0]?.difficultyModeChosen || "STANDARD";
+ projectContext = `
+You are helping a student with the following project:
+Project Title: ${project.title}
+Description: ${project.description}
+Difficulty Level: ${project.difficultyLevel}
+Technologies: ${project.technologies.join(", ")}
+Estimated Time: ${project.estimatedTime}
+Chosen Difficulty Mode: ${difficultyMode}
+
+Milestones:
+${project.milestones
+ .sort((a, b) => a.milestoneNumber - b.milestoneNumber)
+ .map((m) => `${m.milestoneNumber}. ${m.title}: ${m.description}`)
+ .join("\n")}
+
+Learning Objectives:
+${project.learningObjectives.map((obj, i) => `${i + 1}. ${obj}`).join("\n")}
+`;
+ }
+ }
+
+ // 2. Define mode-specific instructions
+ let modeInstruction = "";
+ if (difficultyMode === "GUIDED") {
+ modeInstruction =
+ "The user is in GUIDED mode. Be very helpful, provide detailed explanations, and don't hesitate to give small code snippets if they are stuck.";
+ } else if (difficultyMode === "HARDCORE") {
+ modeInstruction =
+ "The user is in HARDCORE mode. Be very brief, provide only high-level conceptual hints, and never provide code solutions.";
+ } else {
+ modeInstruction =
+ "The user is in STANDARD mode. Provide balanced guidance, focus on concepts, and only provide code as a last resort.";
+ }
+
+ // 3. Build System Prompt
+ const systemPrompt = `You are an AI Guide for a project-based learning platform.
+Your goal is to help students learn by providing guidance, not just giving away answers.
+${projectContext}
+${modeInstruction}
+Keep your responses concise and educational. Always respond in markdown format.`;
+
+ // 4. Fetch chat history
+ const history = await prisma.chatMessage.findMany({
+ where: { userId, projectId: projectId || null },
+ orderBy: { createdAt: "asc" },
+ take: 20,
+ });
+
+ const geminiHistory = history.map((msg) => ({
+ role: msg.role === "user" ? "user" : "model",
+ parts: [{ text: msg.content }],
+ }));
+
+ // 5. Get AI Response
+ const aiResponse = await getGeminiResponse(
+ `${systemPrompt}\n\nUser Question: ${message}`,
+ geminiHistory
+ );
+
+ // 6. Persist Conversation
+ await prisma.chatMessage.createMany({
+ data: [
+ {
+ userId,
+ projectId: projectId || null,
+ role: "user",
+ content: message,
+ },
+ {
+ userId,
+ projectId: projectId || null,
+ role: "assistant",
+ content: aiResponse,
+ },
+ ],
+ });
+
+ // 7. Stream response (simulated streaming)
+ res.setHeader("Content-Type", "text/plain");
+ res.setHeader("Transfer-Encoding", "chunked");
+ res.setHeader("Cache-Control", "no-cache");
+ res.setHeader("Connection", "keep-alive");
+
+ const words = aiResponse.split(" ");
+ for (let i = 0; i < words.length; i++) {
+ res.write(words[i] + " ");
+ await new Promise((resolve) => setTimeout(resolve, 20));
+ }
+
+ res.end();
+ } catch (error: any) {
+ console.error("Chat error:", error);
+ if (!res.headersSent) {
+ res.status(500).json({ success: false, message: error.message });
+ } else {
+ res.end();
+ }
+ }
+};
+
+export const getAllChatMessages = async (req: Request, res: Response) => {
+ const userId = (req as any).user?.id;
+
+ try {
+ const messages = await prisma.chatMessage.findMany({
+ where: { userId },
+ orderBy: { createdAt: "asc" },
+ });
+
+ res.json({ success: true, data: messages });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const requestHint = async (req: Request, res: Response) => {
+ const userId = (req as any).user?.id;
+ const { projectId, milestoneNumber, difficultyMode } = req.body;
+
+ try {
+ const milestone = await prisma.projectMilestone.findUnique({
+ where: { projectId_milestoneNumber: { projectId, milestoneNumber } },
+ include: { project: true },
+ });
+
+ if (!milestone)
+ return res
+ .status(404)
+ .json({ success: false, message: "Milestone not found" });
+
+ let modeInstruction = "";
+ if (difficultyMode === "GUIDED") {
+ modeInstruction =
+ "The user is in GUIDED mode. Be very helpful and detailed.";
+ } else {
+ modeInstruction =
+ "The user is in STANDARD mode. Provide a balanced, conceptual hint.";
+ }
+
+ const modeHintsForPrompt = Array.isArray(milestone.hints)
+ ? milestone.hints
+ : (milestone.hints as any)?.[difficultyMode] || [];
+
+ const prompt = `The user is stuck on Milestone ${milestoneNumber} ("${
+ milestone.title
+ }") of the project "${milestone.project.title}".
+ Milestone description: ${milestone.description}.
+ Difficulty Mode: ${difficultyMode}.
+ ${modeInstruction}
+ Existing hints: ${modeHintsForPrompt.join(", ")}.
+ Please provide a new, progressive hint that helps them move forward without revealing the full solution.
+ Format the response as a single, clear hint string.`;
+
+ const hint = await getGeminiResponse(prompt);
+
+ // Persist the hint by mode in the JSON object
+ const existingHints = (milestone.hints as any) || {};
+ const modeHints = existingHints[difficultyMode] || [];
+ modeHints.push(hint);
+
+ await prisma.projectMilestone.update({
+ where: { id: milestone.id },
+ data: {
+ hints: {
+ ...existingHints,
+ [difficultyMode]: modeHints,
+ },
+ },
+ });
+
+ res.json({ success: true, data: hint });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const getChatHistory = async (req: Request, res: Response) => {
+ const { projectId } = req.params;
+ const userId = (req as any).user?.id;
+
+ try {
+ const messages = await prisma.chatMessage.findMany({
+ where: {
+ userId,
+ projectId: projectId || null,
+ },
+ orderBy: { createdAt: "asc" },
+ });
+
+ res.json({ success: true, data: messages });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
diff --git a/controllers/analytics/index.ts b/controllers/analytics/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f5405fd18d9029c2270afac115e46fc3343368f0
--- /dev/null
+++ b/controllers/analytics/index.ts
@@ -0,0 +1,72 @@
+import { Request, Response, NextFunction } from "express";
+import prisma from "../../prisma/client";
+
+export const getUserAnalytics = async (req: Request, res: Response, next: NextFunction) => {
+ const userId = (req as any).user?.id;
+
+ try {
+ const user = await prisma.user.findUnique({
+ where: { id: userId },
+ select: {
+ xp: true,
+ streak: true,
+ _count: {
+ select: {
+ projects: true,
+ submissions: true,
+ achievements: true
+ }
+ }
+ }
+ });
+
+ const projectCompletion = await prisma.userProject.groupBy({
+ by: ["status"],
+ where: { userId },
+ _count: true
+ });
+
+ res.json({
+ success: true,
+ data: {
+ stats: user,
+ projectCompletion
+ }
+ });
+ } catch (error: any) {
+ next(error);
+ }
+};
+
+export const getAdminAnalytics = async (req: Request, res: Response, next: NextFunction) => {
+ const userId = (req as any).user?.id;
+
+ try {
+ const user = await prisma.user.findUnique({ where: { id: userId } });
+ if (user?.role !== "ADMIN") {
+ return res.status(403).json({ success: false, message: "Forbidden" });
+ }
+
+ const totalUsers = await prisma.user.count();
+ const totalProjects = await prisma.project.count();
+ const totalSubmissions = await prisma.submission.count();
+
+ const popularProjects = await prisma.project.findMany({
+ orderBy: { submissionCount: "desc" },
+ take: 5,
+ select: { title: true, submissionCount: true }
+ });
+
+ res.json({
+ success: true,
+ data: {
+ totalUsers,
+ totalProjects,
+ totalSubmissions,
+ popularProjects
+ }
+ });
+ } catch (error: any) {
+ next(error);
+ }
+};
diff --git a/controllers/auth/index.ts b/controllers/auth/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2e07f820eae5980f81d48e142732bbd0f5f3fa82
--- /dev/null
+++ b/controllers/auth/index.ts
@@ -0,0 +1,448 @@
+import { Request, Response } from "express";
+import prisma from "../../prisma/client";
+import * as argon2 from "argon2";
+import { sendEmail } from "../../middlewares/email";
+import { generateOTP } from "../../middlewares/generateOTP";
+import jwt from "jsonwebtoken";
+import dotenv from "dotenv";
+import redis from "../../utils/redis";
+import { Difficulty, Role } from "@prisma/client";
+
+dotenv.config();
+const JWT_SECRET: string = process.env.JWT_SECRET || "default_secret";
+
+export const signUp = async (req: Request, res: Response) => {
+ const { firstName, lastName, skillLevel, email, password, role } = req.body;
+ try {
+ const existingUser = await prisma.user.findUnique({ where: { email } });
+ if (existingUser) {
+ return res
+ .status(400)
+ .json({ success: false, message: "Email already registered" });
+ }
+
+ const hashedPassword = await argon2.hash(password);
+ const user = await prisma.user.create({
+ data: {
+ firstName,
+ lastName,
+ email,
+ skillLevel: (skillLevel as Difficulty) || "BEGINNER",
+ password: hashedPassword,
+ role: (role as Role) || "STUDENT",
+ },
+ });
+
+ res.json({
+ success: true,
+ message: "User registered successfully",
+ data: { id: user.id },
+ });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const login = async (req: Request, res: Response) => {
+ const { email, password } = req.body;
+ try {
+ const user = await prisma.user.findUnique({ where: { email } });
+ if (!user || !user.password) {
+ return res
+ .status(401)
+ .json({ success: false, message: "Invalid Email or Password" });
+ }
+
+ const verifyPassword = await argon2.verify(user.password, password);
+ if (!verifyPassword) {
+ return res
+ .status(401)
+ .json({ success: false, message: "Invalid Email or Password" });
+ }
+
+ const token = jwt.sign({ id: user.id, role: user.role }, JWT_SECRET, {
+ expiresIn: "7d",
+ });
+
+ res.cookie("token", token, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "strict",
+ maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
+ });
+
+ res.json({
+ success: true,
+ message: "Login successful",
+ data: {
+ id: user.id,
+ firstName: user.firstName,
+ lastName: user.lastName,
+ email: user.email,
+ role: user.role,
+ token,
+ },
+ });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const logout = async (req: Request, res: Response) => {
+ res.clearCookie("token");
+ res.json({ success: true, message: "Logged out successfully" });
+};
+
+export const me = async (req: Request, res: Response) => {
+ const userId = (req as any).user?.id;
+ if (!userId) {
+ return res.status(401).json({ success: false, message: "Unauthorized" });
+ }
+
+ try {
+ const user = await prisma.user.findUnique({
+ where: { id: userId },
+ select: {
+ id: true,
+ email: true,
+ firstName: true,
+ lastName: true,
+ skillLevel: true,
+ bio: true,
+ portfolioLinks: true,
+ xp: true,
+ streak: true,
+ role: true,
+ avatarUrl: true,
+ createdAt: true,
+ projects: {
+ include: {
+ project: true,
+ },
+ },
+ achievements: {
+ include: {
+ achievement: true,
+ },
+ },
+ },
+ });
+
+ if (!user) {
+ return res
+ .status(404)
+ .json({ success: false, message: "User not found" });
+ }
+
+ res.json({ success: true, data: user });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const forgetPassword = async (req: Request, res: Response) => {
+ const { email } = req.body;
+ const appName = "Dev Drill";
+
+ try {
+ const user = await prisma.user.findUnique({ where: { email } });
+ if (!user) {
+ return res
+ .status(404)
+ .json({ success: false, message: "Account is not registered" });
+ }
+
+ const { otp, otpDate } = generateOTP();
+ // Store OTP in Redis with 1 hour expiry
+ await redis.setex(`otp:${email}`, 3600, otp.toString());
+
+ const htmlMessage = `
+
+
+
${appName}
+
+ Hello ${user.lastName},
+ We received a request to reset your password for your ${appName} account.
+
+
+
+ This verification code will expire in 1 hour.
+ If you didn't request this password reset, please ignore this email.
+
+
+
+ © ${new Date().getFullYear()} ${appName}. All rights reserved.
+
+
+ `;
+
+ const textMessage =
+ `${appName} Password Reset\n\n` +
+ `Hello ${user.lastName},\n\n` +
+ `Use this OTP to reset your password: ${otp}\n` +
+ `This code expires in 1 hour.`;
+
+ sendEmail(
+ user.email,
+ `Password Reset Request - ${appName}`,
+ htmlMessage,
+ textMessage
+ );
+
+ res.json({
+ success: true,
+ message: `An OTP has been sent to your mail. Check your mail for your verification code`,
+ });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const verifyOTP = async (req: Request, res: Response) => {
+ const { email, otp: userOtp } = req.body;
+
+ try {
+ const storedOtp = await redis.get(`otp:${email}`);
+ if (!storedOtp) {
+ return res
+ .status(400)
+ .json({ success: false, message: "OTP expired or not found" });
+ }
+
+ if (storedOtp !== userOtp.toString()) {
+ return res
+ .status(400)
+ .json({ success: false, message: "Invalid Verification code!" });
+ }
+
+ // OTP verified, can remove it now
+ await redis.del(`otp:${email}`);
+ // Optionally set a flag in Redis that OTP was verified for this email to allow password reset
+ await redis.setex(`otp_verified:${email}`, 600, "true");
+
+ res.json({ success: true, message: "Verification successful" });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const resetPassword = async (req: Request, res: Response) => {
+ const { email, newPassword } = req.body;
+
+ try {
+ const isVerified = await redis.get(`otp_verified:${email}`);
+ if (!isVerified) {
+ return res
+ .status(400)
+ .json({ success: false, message: "OTP not verified" });
+ }
+
+ const user = await prisma.user.findUnique({ where: { email } });
+ if (!user)
+ return res
+ .status(404)
+ .json({ success: false, message: "User not found" });
+
+ if (user.password) {
+ const verifyPassword = await argon2.verify(user.password, newPassword);
+ if (verifyPassword) {
+ return res
+ .status(400)
+ .json({ success: false, message: "You entered your old password" });
+ }
+ }
+
+ const newPass = await argon2.hash(newPassword);
+ await prisma.user.update({
+ where: { email },
+ data: { password: newPass },
+ });
+
+ await redis.del(`otp_verified:${email}`);
+
+ res.json({ success: true, message: "Password successfully reset" });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const getUser = async (req: Request, res: Response) => {
+ const { id } = req.params;
+ try {
+ const user = await prisma.user.findUnique({
+ where: { id },
+ select: {
+ id: true,
+ email: true,
+ firstName: true,
+ lastName: true,
+ skillLevel: true,
+ bio: true,
+ portfolioLinks: true,
+ xp: true,
+ streak: true,
+ role: true,
+ avatarUrl: true,
+ createdAt: true,
+ projects: {
+ include: {
+ project: true,
+ },
+ },
+ achievements: {
+ include: {
+ achievement: true,
+ },
+ },
+ },
+ });
+ if (!user)
+ return res
+ .status(404)
+ .json({ success: false, message: "User not found" });
+ res.json({ success: true, data: user });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const updateUser = async (req: Request, res: Response) => {
+ const { id } = req.params;
+ const authenticatedUserId = (req as any).user?.id;
+
+ if (authenticatedUserId !== id) {
+ return res.status(403).json({ success: false, message: "Forbidden" });
+ }
+
+ let {
+ firstName,
+ lastName,
+ email,
+ password,
+ bio,
+ skillLevel,
+ portfolioLinks,
+ avatarUrl,
+ } = req.body;
+ try {
+ const user = await prisma.user.findUnique({ where: { id } });
+ if (!user) {
+ return res
+ .status(404)
+ .json({ success: false, message: "User not found" });
+ }
+
+ const data: any = {
+ firstName,
+ lastName,
+ bio,
+ skillLevel,
+ portfolioLinks,
+ avatarUrl,
+ };
+
+ if (email && email !== user.email) {
+ const existingUser = await prisma.user.findUnique({ where: { email } });
+ if (existingUser) {
+ return res
+ .status(400)
+ .json({ success: false, message: "Email already in use" });
+ }
+ data.email = email;
+ }
+
+ if (password) {
+ data.password = await argon2.hash(password);
+ }
+
+ const updatedUser = await prisma.user.update({
+ where: { id },
+ data,
+ select: {
+ id: true,
+ email: true,
+ firstName: true,
+ lastName: true,
+ skillLevel: true,
+ bio: true,
+ portfolioLinks: true,
+ xp: true,
+ streak: true,
+ role: true,
+ avatarUrl: true,
+ createdAt: true,
+ projects: {
+ include: {
+ project: true,
+ },
+ },
+ achievements: {
+ include: {
+ achievement: true,
+ },
+ },
+ },
+ });
+
+ res.json({
+ success: true,
+ data: updatedUser,
+ });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const googleAuthCallback = async (req: Request, res: Response) => {
+ try {
+ const user = req.user as any;
+ if (!user) {
+ return res.redirect(
+ `${process.env.CLIENT_URL}/auth/signin?error=auth_failed`
+ );
+ }
+
+ const token = jwt.sign({ id: user.id, role: user.role }, JWT_SECRET, {
+ expiresIn: "7d",
+ });
+
+ res.cookie("token", token, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax",
+ maxAge: 7 * 24 * 60 * 60 * 1000,
+ });
+
+ res.redirect(`${process.env.CLIENT_URL}/dashboard`);
+ } catch (error: any) {
+ res.redirect(`${process.env.CLIENT_URL}/auth/signin?error=server_error`);
+ }
+};
+
+export const githubAuthCallback = async (req: Request, res: Response) => {
+ try {
+ const user = req.user as any;
+ if (!user) {
+ return res.redirect(
+ `${process.env.CLIENT_URL}/auth/signin?error=auth_failed`
+ );
+ }
+
+ const token = jwt.sign({ id: user.id, role: user.role }, JWT_SECRET, {
+ expiresIn: "7d",
+ });
+
+ res.cookie("token", token, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax",
+ maxAge: 7 * 24 * 60 * 60 * 1000,
+ });
+
+ res.redirect(`${process.env.CLIENT_URL}/dashboard`);
+ } catch (error: any) {
+ res.redirect(`${process.env.CLIENT_URL}/auth/signin?error=server_error`);
+ }
+};
diff --git a/controllers/code-review/index.ts b/controllers/code-review/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..392f3b08a87aef408069b9c7be714dbe46344e52
--- /dev/null
+++ b/controllers/code-review/index.ts
@@ -0,0 +1,71 @@
+import { Request, Response } from "express";
+import prisma from "../../prisma/client";
+import { getGeminiResponse } from "../../utils/gemini";
+import axios from "axios";
+
+export const analyzeCode = async (req: Request, res: Response) => {
+ const userId = (req as any).user?.id;
+ const { repoUrl, projectId } = req.body;
+
+ try {
+ // 1. Fetch code from GitHub (simplified: fetch README or a few files for demo)
+ // In a real app, you'd use a GitHub App or User Token to clone or fetch via API
+ const repoPath = repoUrl.replace("https://github.com/", "");
+ const [owner, repo] = repoPath.split("/");
+
+ // Fetch repo info as a placeholder for analysis
+ const githubApiUrl = `https://api.github.com/repos/${owner}/${repo}`;
+ const repoInfo = await axios.get(githubApiUrl);
+
+ // 2. Automated analysis (simulated)
+ const metrics = {
+ complexity: "Medium",
+ securityIssues: 0,
+ lintErrors: 0
+ };
+
+ // 3. AI-powered feedback
+ const prompt = `Please review this GitHub repository: ${repoUrl}.
+ Owner: ${owner}
+ Repo: ${repo}
+ Description: ${repoInfo.data.description}
+ Provide constructive feedback on the project structure and suggest improvements for a student.`;
+
+ const feedback = await getGeminiResponse(prompt);
+
+ // 4. Store review results
+ const submission = await prisma.submission.create({
+ data: {
+ userId,
+ projectId,
+ repoUrl,
+ feedback,
+ score: 85 // Simulated score
+ }
+ });
+
+ res.json({ success: true, data: submission });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const getSubmissionReview = async (req: Request, res: Response) => {
+ const { id } = req.params;
+
+ try {
+ const submission = await prisma.submission.findUnique({
+ where: { id },
+ include: {
+ project: { select: { title: true } },
+ user: { select: { firstName: true, lastName: true } }
+ }
+ });
+
+ if (!submission) return res.status(404).json({ success: false, message: "Submission not found" });
+
+ res.json({ success: true, data: submission });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
diff --git a/controllers/community/index.ts b/controllers/community/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b8a880277fa94c5cde68e6dfef08a8a4cd4cca27
--- /dev/null
+++ b/controllers/community/index.ts
@@ -0,0 +1,152 @@
+import { Request, Response, NextFunction } from "express";
+import prisma from "../../prisma/client";
+
+export const getPublicSubmissions = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const submissions = await prisma.submission.findMany({
+ where: { isPublic: true },
+ include: {
+ user: {
+ select: {
+ id: true,
+ firstName: true,
+ lastName: true,
+ avatarUrl: true,
+ },
+ },
+ project: {
+ select: {
+ id: true,
+ title: true,
+ },
+ },
+ _count: {
+ select: {
+ comments: true,
+ votes: true,
+ },
+ },
+ },
+ orderBy: { createdAt: "desc" },
+ });
+
+ res.json({ success: true, data: submissions });
+ } catch (error: any) {
+ next(error);
+ }
+};
+
+export const commentOnSubmission = async (req: Request, res: Response, next: NextFunction) => {
+ const { id: submissionId } = req.params;
+ const { content } = req.body;
+ const userId = (req as any).user?.id;
+
+ try {
+ const comment = await prisma.comment.create({
+ data: {
+ content,
+ userId,
+ submissionId,
+ },
+ include: {
+ user: {
+ select: {
+ id: true,
+ firstName: true,
+ lastName: true,
+ avatarUrl: true,
+ },
+ },
+ },
+ });
+
+ res.status(201).json({ success: true, data: comment });
+ } catch (error: any) {
+ next(error);
+ }
+};
+
+export const voteOnSubmission = async (req: Request, res: Response, next: NextFunction) => {
+ const { id: submissionId } = req.params;
+ const { value } = req.body; // 1 for upvote, -1 for downvote
+ const userId = (req as any).user?.id;
+
+ try {
+ const vote = await prisma.vote.upsert({
+ where: {
+ userId_submissionId: {
+ userId,
+ submissionId,
+ },
+ },
+ update: { value },
+ create: {
+ value,
+ userId,
+ submissionId,
+ },
+ });
+
+ res.json({ success: true, data: vote });
+ } catch (error: any) {
+ next(error);
+ }
+};
+
+export const getForumThreads = async (req: Request, res: Response, next: NextFunction) => {
+ const { projectId } = req.query;
+
+ try {
+ const threads = await prisma.forumThread.findMany({
+ where: projectId ? { projectId: String(projectId) } : {},
+ include: {
+ user: {
+ select: {
+ id: true,
+ firstName: true,
+ lastName: true,
+ avatarUrl: true,
+ },
+ },
+ _count: {
+ select: { posts: true },
+ },
+ },
+ orderBy: { createdAt: "desc" },
+ });
+
+ res.json({ success: true, data: threads });
+ } catch (error: any) {
+ next(error);
+ }
+};
+
+export const createForumThread = async (req: Request, res: Response, next: NextFunction) => {
+ const { title, content, projectId } = req.body;
+ const userId = (req as any).user?.id;
+
+ try {
+ const thread = await prisma.forumThread.create({
+ data: {
+ title,
+ content,
+ userId,
+ projectId: projectId || null,
+ },
+ include: {
+ user: {
+ select: {
+ id: true,
+ firstName: true,
+ lastName: true,
+ avatarUrl: true,
+ },
+ },
+ },
+ });
+
+ res.status(201).json({ success: true, data: thread });
+ } catch (error: any) {
+ next(error);
+ }
+};
diff --git a/controllers/events/index.ts b/controllers/events/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9055b209b361cf3dc7092019f4712986c857a127
--- /dev/null
+++ b/controllers/events/index.ts
@@ -0,0 +1,221 @@
+import { Request, Response } from "express";
+import prisma from "../../prisma/client";
+import { EventStatus } from "@prisma/client";
+
+export const createEvent = async (req: Request, res: Response) => {
+ const {
+ title,
+ description,
+ type,
+ startDate,
+ endDate,
+ prizePool,
+ maxParticipants,
+ bannerUrl,
+ } = req.body;
+
+ try {
+ const event = await prisma.event.create({
+ data: {
+ title,
+ description,
+ type,
+ startDate: new Date(startDate),
+ endDate: new Date(endDate),
+ prizePool,
+ maxParticipants,
+ bannerUrl,
+ },
+ });
+
+ res.json({ success: true, data: event });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const getEvents = async (req: Request, res: Response) => {
+ const { status, type, page = 1, limit = 10 } = req.query;
+
+ try {
+ const where: any = {};
+ if (status) where.status = status as EventStatus;
+ if (type) where.type = type;
+
+ const events = await prisma.event.findMany({
+ where,
+ include: {
+ _count: {
+ select: { participants: true },
+ },
+ },
+ orderBy: { startDate: "desc" },
+ take: Number(limit),
+ skip: (Number(page) - 1) * Number(limit),
+ });
+
+ res.json({ success: true, data: events });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const getEvent = async (req: Request, res: Response) => {
+ const { id } = req.params;
+
+ try {
+ const event = await prisma.event.findUnique({
+ where: { id },
+ include: {
+ participants: {
+ include: {
+ user: {
+ select: {
+ id: true,
+ firstName: true,
+ lastName: true,
+ avatarUrl: true,
+ },
+ },
+ },
+ },
+ leaderboard: {
+ include: {
+ user: {
+ select: {
+ id: true,
+ firstName: true,
+ lastName: true,
+ avatarUrl: true,
+ },
+ },
+ },
+ orderBy: { score: "desc" },
+ take: 100,
+ },
+ },
+ });
+
+ if (!event) {
+ return res
+ .status(404)
+ .json({ success: false, message: "Event not found" });
+ }
+
+ res.json({ success: true, data: event });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const joinEvent = async (req: Request, res: Response) => {
+ const userId = (req as any).user?.id;
+ const { eventId } = req.params;
+
+ try {
+ const event = await prisma.event.findUnique({
+ where: { id: eventId },
+ include: { _count: { select: { participants: true } } },
+ });
+
+ if (!event) {
+ return res
+ .status(404)
+ .json({ success: false, message: "Event not found" });
+ }
+
+ if (
+ event.maxParticipants &&
+ event._count.participants >= event.maxParticipants
+ ) {
+ return res.status(400).json({ success: false, message: "Event is full" });
+ }
+
+ const participant = await prisma.eventParticipant.create({
+ data: { eventId, userId },
+ });
+
+ await prisma.eventLeaderboard.create({
+ data: { eventId, userId, score: 0 },
+ });
+
+ res.json({ success: true, data: participant });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const leaveEvent = async (req: Request, res: Response) => {
+ const userId = (req as any).user?.id;
+ const { eventId } = req.params;
+
+ try {
+ await prisma.eventParticipant.delete({
+ where: { eventId_userId: { eventId, userId } },
+ });
+
+ await prisma.eventLeaderboard.delete({
+ where: { eventId_userId: { eventId, userId } },
+ });
+
+ res.json({ success: true, message: "Left event successfully" });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const updateEventScore = async (req: Request, res: Response) => {
+ const { eventId, userId } = req.params;
+ const { score } = req.body;
+
+ try {
+ const leaderboard = await prisma.eventLeaderboard.update({
+ where: { eventId_userId: { eventId, userId } },
+ data: { score },
+ });
+
+ const allScores = await prisma.eventLeaderboard.findMany({
+ where: { eventId },
+ orderBy: { score: "desc" },
+ });
+
+ for (let i = 0; i < allScores.length; i++) {
+ await prisma.eventLeaderboard.update({
+ where: { id: allScores[i].id },
+ data: { rank: i + 1 },
+ });
+ }
+
+ res.json({ success: true, data: leaderboard });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const getLeaderboard = async (req: Request, res: Response) => {
+ const { eventId } = req.params;
+ const { limit = 100 } = req.query;
+
+ try {
+ const leaderboard = await prisma.eventLeaderboard.findMany({
+ where: { eventId },
+ include: {
+ user: {
+ select: {
+ id: true,
+ firstName: true,
+ lastName: true,
+ avatarUrl: true,
+ xp: true,
+ },
+ },
+ },
+ orderBy: { score: "desc" },
+ take: Number(limit),
+ });
+
+ res.json({ success: true, data: leaderboard });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
diff --git a/controllers/gamification/index.ts b/controllers/gamification/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3856d8b55aec377ea20af6b6ef8c3a1b781c3d2c
--- /dev/null
+++ b/controllers/gamification/index.ts
@@ -0,0 +1,69 @@
+import { Request, Response, NextFunction } from "express";
+import prisma from "../../prisma/client";
+import redis from "../../utils/redis";
+
+const LEADERBOARD_CACHE_KEY = "leaderboard:top20";
+const CACHE_TTL = 3600; // 1 hour
+
+export const getLeaderboard = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ // Try to get from cache
+ const cachedLeaderboard = await redis.get(LEADERBOARD_CACHE_KEY);
+ if (cachedLeaderboard) {
+ return res.json({ success: true, data: JSON.parse(cachedLeaderboard), cached: true });
+ }
+
+ const leaderboard = await prisma.user.findMany({
+ select: {
+ id: true,
+ firstName: true,
+ lastName: true,
+ avatarUrl: true,
+ xp: true,
+ streak: true
+ },
+ orderBy: { xp: "desc" },
+ take: 20
+ });
+
+ // Save to cache
+ await redis.setex(LEADERBOARD_CACHE_KEY, CACHE_TTL, JSON.stringify(leaderboard));
+
+ res.json({ success: true, data: leaderboard });
+ } catch (error: any) {
+ next(error);
+ }
+};
+
+export const getUserAchievements = async (req: Request, res: Response, next: NextFunction) => {
+ const userId = (req as any).user?.id;
+
+ try {
+ const achievements = await prisma.userAchievement.findMany({
+ where: { userId },
+ include: {
+ achievement: true
+ }
+ });
+
+ res.json({ success: true, data: achievements });
+ } catch (error: any) {
+ next(error);
+ }
+};
+
+export const awardXP = async (userId: string, amount: number) => {
+ try {
+ await prisma.user.update({
+ where: { id: userId },
+ data: {
+ xp: { increment: amount }
+ }
+ });
+
+ // Invalidate leaderboard cache when XP changes
+ await redis.del(LEADERBOARD_CACHE_KEY);
+ } catch (error) {
+ console.error("Error awarding XP:", error);
+ }
+};
diff --git a/controllers/github/index.ts b/controllers/github/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0bee9f6f7987fc50afa688283cd6e52396ffbf78
--- /dev/null
+++ b/controllers/github/index.ts
@@ -0,0 +1,109 @@
+import { Request, Response } from "express";
+import prisma from "../../prisma/client";
+import axios from "axios";
+
+export const linkRepository = async (req: Request, res: Response) => {
+ const userId = (req as any).user?.id;
+ const { projectId, repoUrl } = req.body;
+
+ try {
+ if (!repoUrl.includes("github.com")) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid GitHub repository URL",
+ });
+ }
+
+ // Extract owner and repo from URL
+ const repoPath = repoUrl
+ .replace("https://github.com/", "")
+ .replace(".git", "");
+ const [owner, repo] = repoPath.split("/");
+
+ // Verify repository exists (optional - requires GitHub token)
+ try {
+ const githubApiUrl = `https://api.github.com/repos/${owner}/${repo}`;
+ await axios.get(githubApiUrl);
+ } catch (error) {
+ return res.status(404).json({
+ success: false,
+ message: "Repository not found or not accessible",
+ });
+ }
+
+ // Update user project with repo URL
+ const userProject = await prisma.userProject.findFirst({
+ where: {
+ userId,
+ projectId,
+ },
+ });
+
+ if (!userProject) {
+ return res.status(404).json({
+ success: false,
+ message: "Project not started by user",
+ });
+ }
+
+ const updated = await prisma.userProject.update({
+ where: { id: userProject.id },
+ data: { repoUrl },
+ });
+
+ res.json({ success: true, data: updated });
+ } catch (error: any) {
+ console.error("Link repository error:", error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const getRepositoryCommits = async (req: Request, res: Response) => {
+ const userId = (req as any).user?.id;
+ const { projectId } = req.params;
+
+ try {
+ const userProject = await prisma.userProject.findFirst({
+ where: {
+ userId,
+ projectId,
+ },
+ });
+
+ if (!userProject || !userProject.repoUrl) {
+ return res.status(404).json({
+ success: false,
+ message: "No repository linked",
+ });
+ }
+
+ // Extract owner and repo
+ const repoPath = userProject.repoUrl
+ .replace("https://github.com/", "")
+ .replace(".git", "");
+ const [owner, repo] = repoPath.split("/");
+
+ // Fetch commits from GitHub API
+ const githubApiUrl = `https://api.github.com/repos/${owner}/${repo}/commits`;
+ const response = await axios.get(githubApiUrl, {
+ params: { per_page: 10 },
+ headers: {
+ // Add GitHub token if available for higher rate limits
+ Authorization: `token ${process.env.GITHUB_TOKEN}`,
+ },
+ });
+
+ const commits = response.data.map((commit: any) => ({
+ id: commit.sha,
+ message: commit.commit.message,
+ author: commit.commit.author.name,
+ date: commit.commit.author.date,
+ url: commit.html_url,
+ }));
+
+ res.json({ success: true, data: commits });
+ } catch (error: any) {
+ console.error("Get commits error:", error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
diff --git a/controllers/notifications/index.ts b/controllers/notifications/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..225286b4ae7f597191ce52fe00a2f61045a201ce
--- /dev/null
+++ b/controllers/notifications/index.ts
@@ -0,0 +1,75 @@
+import { Request, Response } from "express";
+import prisma from "../../prisma/client";
+
+export const getNotifications = async (req: Request, res: Response) => {
+ const userId = (req as any).user?.id;
+ const { page = 1, limit = 20, unreadOnly = false } = req.query;
+
+ try {
+ const where: any = { userId };
+ if (unreadOnly === "true") {
+ where.read = false;
+ }
+
+ const notifications = await prisma.notification.findMany({
+ where,
+ orderBy: { createdAt: "desc" },
+ take: Number(limit),
+ skip: (Number(page) - 1) * Number(limit),
+ });
+
+ const unreadCount = await prisma.notification.count({
+ where: { userId, read: false },
+ });
+
+ res.json({ success: true, data: { notifications, unreadCount } });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const markAsRead = async (req: Request, res: Response) => {
+ const userId = (req as any).user?.id;
+ const { notificationId } = req.params;
+
+ try {
+ await prisma.notification.updateMany({
+ where: { id: notificationId, userId },
+ data: { read: true },
+ });
+
+ res.json({ success: true, message: "Notification marked as read" });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const markAllAsRead = async (req: Request, res: Response) => {
+ const userId = (req as any).user?.id;
+
+ try {
+ await prisma.notification.updateMany({
+ where: { userId, read: false },
+ data: { read: true },
+ });
+
+ res.json({ success: true, message: "All notifications marked as read" });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const deleteNotification = async (req: Request, res: Response) => {
+ const userId = (req as any).user?.id;
+ const { notificationId } = req.params;
+
+ try {
+ await prisma.notification.deleteMany({
+ where: { id: notificationId, userId },
+ });
+
+ res.json({ success: true, message: "Notification deleted" });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
diff --git a/controllers/paths/index.ts b/controllers/paths/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..12fca1b6deeba2f66e8551f3aeabe8bfd802c302
--- /dev/null
+++ b/controllers/paths/index.ts
@@ -0,0 +1,82 @@
+import { Request, Response, NextFunction } from "express";
+import prisma from "../../prisma/client";
+
+export const getLearningPaths = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const paths = await prisma.learningPath.findMany({
+ include: {
+ projects: {
+ include: {
+ project: {
+ select: {
+ title: true,
+ difficultyLevel: true,
+ },
+ },
+ },
+ orderBy: { orderIndex: "asc" },
+ },
+ },
+ });
+
+ res.json({ success: true, data: paths });
+ } catch (error: any) {
+ next(error);
+ }
+};
+
+export const getPathProgress = async (req: Request, res: Response, next: NextFunction) => {
+ const { id: pathId } = req.params;
+ const userId = (req as any).user?.id;
+
+ try {
+ const progress = await prisma.userPathProgress.findUnique({
+ where: {
+ userId_pathId: {
+ userId,
+ pathId,
+ },
+ },
+ include: {
+ path: {
+ include: {
+ projects: {
+ include: { project: true },
+ orderBy: { orderIndex: "asc" },
+ },
+ },
+ },
+ },
+ });
+
+ res.json({ success: true, data: progress });
+ } catch (error: any) {
+ next(error);
+ }
+};
+
+export const startPath = async (req: Request, res: Response, next: NextFunction) => {
+ const { id: pathId } = req.params;
+ const userId = (req as any).user?.id;
+
+ try {
+ const progress = await prisma.userPathProgress.upsert({
+ where: {
+ userId_pathId: {
+ userId,
+ pathId
+ }
+ },
+ update: {},
+ create: {
+ userId,
+ pathId,
+ currentProjectIndex: 0,
+ },
+ });
+
+ res.status(201).json({ success: true, data: progress });
+ } catch (error: any) {
+ next(error);
+ }
+};
diff --git a/controllers/projects/index.ts b/controllers/projects/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..892949103136411d5b2d9f508f4c0fb35b246746
--- /dev/null
+++ b/controllers/projects/index.ts
@@ -0,0 +1,706 @@
+import { Request, Response } from "express";
+import prisma from "../../prisma/client";
+import { Difficulty, DifficultyMode, ProjectStatus } from "@prisma/client";
+import { checkAuth } from "../../middlewares/auth";
+import { upLoadFiles, uploadBuffer } from "../../middlewares/file";
+import { generateSlug, parseIfString } from "../../utils/helper";
+import { getGeminiResponse } from "../../utils/gemini";
+
+export const createProject = async (req: Request, res: Response) => {
+ const userId = (req as any).user?.id;
+ let {
+ title,
+ description,
+ difficultyLevel,
+ technologies,
+ categories,
+ estimatedTime,
+ learningObjectives,
+ resourceLinks,
+ starterRepoUrl,
+ difficultyModes,
+ coverImage,
+ milestones,
+ } = req.body;
+
+ // Parse JSON strings from FormData
+ technologies = parseIfString(technologies);
+ categories = parseIfString(categories);
+ learningObjectives = parseIfString(learningObjectives);
+ resourceLinks = parseIfString(resourceLinks);
+ difficultyModes = parseIfString(difficultyModes);
+ milestones = parseIfString(milestones);
+
+ try {
+ if (!userId)
+ return res.status(401).json({ success: false, message: "Unauthorized" });
+
+ const user = await prisma.user.findUnique({ where: { id: userId } });
+ if (!user || (user.role !== "ADMIN" && user.role !== "CONTRIBUTOR")) {
+ return res.status(403).json({
+ success: false,
+ message: "Only Admins or Contributors can create projects",
+ });
+ }
+
+ let imageUrl = coverImage;
+ if (req.file) {
+ imageUrl = await uploadBuffer(req.file.buffer, req.file.originalname);
+ } else if (coverImage && !coverImage.startsWith("http")) {
+ imageUrl = await upLoadFiles(coverImage);
+ }
+
+ const project = await prisma.project.create({
+ data: {
+ title,
+ slug: generateSlug(title),
+ description,
+ difficultyLevel: difficultyLevel as Difficulty,
+ technologies,
+ categories,
+ estimatedTime,
+ learningObjectives,
+ resourceLinks: resourceLinks || [],
+ starterRepoUrl,
+ difficultyModes: difficultyModes || ["GUIDED", "STANDARD", "HARDCORE"],
+ createdBy: { connect: { id: userId } },
+ coverImage: imageUrl,
+ milestones: {
+ create: milestones?.map((m: any, index: number) => ({
+ milestoneNumber: index + 1,
+ title: m.title,
+ description: m.description,
+ hints: Array.isArray(m.hints) ? { GUIDED: m.hints } : m.hints || {},
+ validationCriteria: m.validationCriteria,
+ })),
+ },
+ },
+ include: { milestones: true },
+ });
+
+ res.status(201).json({ success: true, data: project });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const updateProject = async (req: Request, res: Response) => {
+ const { id } = req.params;
+ const userId = (req as any).user?.id;
+
+ let updateData = req.body;
+
+ updateData.technologies = parseIfString(updateData.technologies);
+ updateData.categories = parseIfString(updateData.categories);
+ updateData.learningObjectives = parseIfString(updateData.learningObjectives);
+ updateData.resourceLinks = parseIfString(updateData.resourceLinks);
+ updateData.difficultyModes = parseIfString(updateData.difficultyModes);
+
+ const milestonesData = parseIfString(updateData.milestones);
+ delete updateData.milestones;
+
+ try {
+ const project = await prisma.project.findUnique({
+ where: { id },
+ include: { milestones: true, userProjects: true },
+ });
+
+ if (!project) {
+ return res.status(404).json({
+ success: false,
+ message: "Project not found",
+ });
+ }
+
+ const user = await prisma.user.findUnique({ where: { id: userId } });
+
+ if (!user || (user.role !== "ADMIN" && project.createdById !== userId)) {
+ return res.status(403).json({
+ success: false,
+ message: "Unauthorized to update this project",
+ });
+ }
+
+ if (req.file) {
+ updateData.coverImage = await uploadBuffer(
+ req.file.buffer,
+ req.file.originalname
+ );
+ } else if (
+ updateData.coverImage &&
+ !updateData.coverImage.startsWith("http")
+ ) {
+ updateData.coverImage = await upLoadFiles(updateData.coverImage);
+ }
+
+ // Update slug if title changed
+ if (updateData.title && updateData.title !== project.title) {
+ updateData.slug = generateSlug(updateData.title);
+ }
+
+ // Clean up any undefined or empty string values
+ Object.keys(updateData).forEach((key) => {
+ if (updateData[key] === undefined || updateData[key] === "") {
+ delete updateData[key];
+ }
+ });
+
+ // Update project with milestones handled separately
+ const updatedProject = await prisma.project.update({
+ where: { id },
+ data: {
+ ...updateData,
+ ...(milestonesData && {
+ milestones: {
+ deleteMany: {},
+ create: milestonesData
+ .filter((m: any) => m.title && m.description)
+ .map((m: any, index: number) => ({
+ milestoneNumber: index + 1,
+ title: m.title,
+ description: m.description,
+ hints: typeof m.hints === "object" ? m.hints : {},
+ validationCriteria:
+ typeof m.validationCriteria === "string"
+ ? m.validationCriteria
+ : JSON.stringify(m.validationCriteria || {}),
+ })),
+ },
+ }),
+ },
+ include: {
+ milestones: {
+ orderBy: { milestoneNumber: "asc" },
+ },
+ createdBy: {
+ select: {
+ id: true,
+ firstName: true,
+ lastName: true,
+ email: true,
+ avatarUrl: true,
+ },
+ },
+ userProjects: { where: { userId: user.id } },
+ },
+ });
+
+ // Build progressByMode
+ let progressByMode: any = {};
+ let userProgress = null;
+
+ if (updatedProject.userProjects && updatedProject.userProjects.length > 0) {
+ for (const up of updatedProject.userProjects) {
+ const completedMilestones = await prisma.userMilestone.findMany({
+ where: {
+ userId: user.id,
+ userProjectId: up.id,
+ },
+ select: { milestoneId: true },
+ });
+
+ progressByMode[up.difficultyModeChosen] = {
+ status: up.status,
+ completedMilestones: completedMilestones.map((m) => m.milestoneId),
+ userProjectId: up.id,
+ startedAt: up.startedAt,
+ completedAt: up.completedAt,
+ };
+ }
+
+ userProgress =
+ updatedProject.userProjects[updatedProject.userProjects.length - 1];
+ }
+
+ res.json({
+ success: true,
+ data: {
+ ...updatedProject,
+ progressByMode,
+ userProgress,
+ },
+ });
+ } catch (error: any) {
+ console.error("Update project error:", error);
+ res.status(500).json({
+ success: false,
+ message: error.message,
+ });
+ }
+};
+
+export const deleteProject = async (req: Request, res: Response) => {
+ const { id } = req.params;
+ const userId = (req as any).user?.id;
+
+ try {
+ const project = await prisma.project.findUnique({ where: { id } });
+ if (!project)
+ return res
+ .status(404)
+ .json({ success: false, message: "Project not found" });
+
+ const user = await prisma.user.findUnique({ where: { id: userId } });
+ if (!user || user.role !== "ADMIN") {
+ return res
+ .status(403)
+ .json({ success: false, message: "Only Admins can delete projects" });
+ }
+
+ await prisma.project.delete({ where: { id } });
+ res.json({ success: true, message: "Project deleted successfully" });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const getProjects = async (req: Request, res: Response) => {
+ const { difficulty, tech, category, title } = req.query;
+
+ try {
+ const where: any = {};
+ if (difficulty) where.difficultyLevel = difficulty as Difficulty;
+ if (tech) where.technologies = { has: tech as string };
+ if (category) where.categories = { has: category as string };
+ if (title) where.title = { contains: title as string, mode: "insensitive" };
+
+ const projects = await prisma.project.findMany({
+ where,
+ include: {
+ createdBy: {
+ select: { firstName: true, lastName: true },
+ },
+ },
+ orderBy: { createdAt: "desc" },
+ });
+
+ res.json({ success: true, data: projects });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const getProjectById = async (req: Request, res: Response) => {
+ const { id } = req.params;
+
+ try {
+ const user = await checkAuth(req);
+ const project = await prisma.project.findUnique({
+ where: { id },
+ include: {
+ milestones: { orderBy: { milestoneNumber: "asc" } },
+ createdBy: {
+ select: {
+ id: true,
+ firstName: true,
+ lastName: true,
+ avatarUrl: true,
+ },
+ },
+ userProjects: user ? { where: { userId: user.id } } : false,
+ },
+ });
+
+ if (!project)
+ return res
+ .status(404)
+ .json({ success: false, message: "Project not found" });
+
+ // If user is authenticated, fetch their progress and completed milestones for each mode
+ let progressByMode: any = {};
+ let userProgress = null;
+
+ if (user && project.userProjects && project.userProjects.length > 0) {
+ for (const up of project.userProjects) {
+ const completedMilestones = await prisma.userMilestone.findMany({
+ where: {
+ userId: user.id,
+ userProjectId: up.id,
+ },
+ select: { milestoneId: true },
+ });
+
+ progressByMode[up.difficultyModeChosen] = {
+ status: up.status,
+ completedMilestones: completedMilestones.map((m) => m.milestoneId),
+ userProjectId: up.id,
+ startedAt: up.startedAt,
+ completedAt: up.completedAt,
+ };
+ }
+
+ // Set the most recent userProject as the default userProgress
+ userProgress = project.userProjects[project.userProjects.length - 1];
+ }
+
+ res.json({
+ success: true,
+ data: {
+ ...project,
+ progressByMode,
+ userProgress,
+ },
+ });
+ } catch (error: any) {
+ console.error("Get project error:", error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+// User Progress Endpoints
+export const startProject = async (req: Request, res: Response) => {
+ const { id: projectId } = req.params;
+ const userId = (req as any).user?.id;
+ const { difficultyModeChosen } = req.body;
+
+ try {
+ const mode = (difficultyModeChosen as DifficultyMode) || "STANDARD";
+
+ const existingProgress = await prisma.userProject.findUnique({
+ where: {
+ userId_projectId_difficultyModeChosen: {
+ userId,
+ projectId,
+ difficultyModeChosen: mode,
+ },
+ },
+ });
+
+ if (existingProgress) {
+ return res.status(400).json({
+ success: false,
+ message: "Project already started in this mode",
+ });
+ }
+
+ const progress = await prisma.userProject.create({
+ data: {
+ userId,
+ projectId,
+ difficultyModeChosen: mode,
+ status: "IN_PROGRESS",
+ },
+ });
+
+ res.status(201).json({ success: true, data: progress });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const updateProgress = async (req: Request, res: Response) => {
+ const { id: projectId } = req.params;
+ const userId = (req as any).user?.id;
+ const { status, repoUrl, difficultyMode } = req.body;
+
+ try {
+ const progress = await prisma.userProject.update({
+ where: {
+ userId_projectId_difficultyModeChosen: {
+ userId,
+ projectId,
+ difficultyModeChosen: difficultyMode as DifficultyMode,
+ },
+ },
+ data: { status: status as ProjectStatus, repoUrl },
+ });
+
+ res.json({ success: true, data: progress });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const completeProject = async (req: Request, res: Response) => {
+ const { id: projectId } = req.params;
+ const userId = (req as any).user?.id;
+ const { repoUrl, difficultyMode } = req.body;
+
+ try {
+ const progress = await prisma.userProject.update({
+ where: {
+ userId_projectId_difficultyModeChosen: {
+ userId,
+ projectId,
+ difficultyModeChosen: difficultyMode as DifficultyMode,
+ },
+ },
+ data: {
+ status: "COMPLETED",
+ completedAt: new Date(),
+ repoUrl,
+ },
+ });
+
+ // Award XP (simple example)
+ await prisma.user.update({
+ where: { id: userId },
+ data: { xp: { increment: 100 } },
+ });
+
+ // Update project completion rate
+ const totalStarted = await prisma.userProject.count({
+ where: { projectId },
+ });
+ const totalCompleted = await prisma.userProject.count({
+ where: { projectId, status: "COMPLETED" },
+ });
+
+ await prisma.project.update({
+ where: { id: projectId },
+ data: {
+ completionRate: (totalCompleted / totalStarted) * 100,
+ submissionCount: { increment: 1 },
+ },
+ });
+
+ res.json({ success: true, data: progress });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const getUserProgress = async (req: Request, res: Response) => {
+ const userId = (req as any).user?.id;
+
+ try {
+ const progress = await prisma.userProject.findMany({
+ where: { userId },
+ include: {
+ project: {
+ include: {
+ milestones: {
+ orderBy: { milestoneNumber: "asc" },
+ },
+ createdBy: {
+ select: { firstName: true, lastName: true, avatarUrl: true },
+ },
+ },
+ },
+ // Correct relation name from schema.prisma
+ milestoneProgress: {
+ select: { milestoneId: true, completedAt: true },
+ },
+ },
+ });
+
+ res.json({ success: true, data: progress });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+// Submissions
+export const submitProject = async (req: Request, res: Response) => {
+ const { id: projectId } = req.params;
+ const userId = (req as any).user?.id;
+ const { repoUrl } = req.body;
+
+ try {
+ const submission = await prisma.submission.create({
+ data: {
+ userId,
+ projectId,
+ repoUrl,
+ },
+ });
+
+ res.status(201).json({ success: true, data: submission });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const completeMilestone = async (req: Request, res: Response) => {
+ const { id: projectId, milestoneId } = req.params;
+ const userId = (req as any).user?.id;
+ const { difficultyMode } = req.body;
+
+ try {
+ const userProject = await prisma.userProject.findUnique({
+ where: {
+ userId_projectId_difficultyModeChosen: {
+ userId,
+ projectId,
+ difficultyModeChosen: difficultyMode as DifficultyMode,
+ },
+ },
+ });
+
+ if (!userProject) {
+ return res.status(400).json({
+ success: false,
+ message: "Project not started by user in this mode",
+ });
+ }
+
+ const existingMilestone = await prisma.userMilestone.findUnique({
+ where: {
+ userId_milestoneId_userProjectId: {
+ userId,
+ milestoneId,
+ userProjectId: userProject.id,
+ },
+ },
+ });
+
+ if (existingMilestone) {
+ return res.status(400).json({
+ success: false,
+ message: "Milestone already completed in this mode",
+ });
+ }
+
+ // 1. Create the milestone completion record
+ const milestone = await prisma.userMilestone.create({
+ data: {
+ userId,
+ milestoneId,
+ userProjectId: userProject.id,
+ },
+ });
+
+ // 2. CHECK FOR PROJECT COMPLETION
+ const totalMilestones = await prisma.projectMilestone.count({
+ where: { projectId },
+ });
+
+ const completedCount = await prisma.userMilestone.count({
+ where: {
+ userId,
+ userProjectId: userProject.id,
+ },
+ });
+
+ // 3. AUTO-COMPLETE PROJECT if all milestones are done
+ if (completedCount === totalMilestones) {
+ await prisma.userProject.update({
+ where: { id: userProject.id },
+ data: {
+ status: "COMPLETED",
+ completedAt: new Date(),
+ },
+ });
+
+ await prisma.user.update({
+ where: { id: userId },
+ data: { xp: { increment: 50 } },
+ });
+ }
+
+ res.status(201).json({ success: true, data: milestone });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const getFeaturedProjects = async (req: Request, res: Response) => {
+ try {
+ const featuredProjects = await prisma.project.findMany({
+ take: 4,
+ orderBy: { submissionCount: "desc" },
+ include: {
+ createdBy: {
+ select: { firstName: true, lastName: true },
+ },
+ },
+ });
+
+ res.json({ success: true, data: featuredProjects });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const submitCode = async (req: Request, res: Response) => {
+ const userId = (req as any).user?.id;
+ const { projectId, milestoneId, code, language } = req.body;
+
+ try {
+ // Fetch milestone details for validation criteria
+ const milestone = await prisma.projectMilestone.findUnique({
+ where: { id: milestoneId },
+ include: { project: true },
+ });
+
+ if (!milestone) {
+ return res.status(404).json({
+ success: false,
+ message: "Milestone not found",
+ });
+ }
+
+ // AI-powered code review
+ const reviewPrompt = `
+You are a code reviewer for a learning platform. Review this code submission:
+
+Project: ${milestone.project.title}
+Milestone: ${milestone.title}
+Description: ${milestone.description}
+Validation Criteria: ${milestone.validationCriteria || "General best practices"}
+
+Code:
+\`\`\`${language}
+${code}
+\`\`\`
+
+Provide:
+1. Does this code meet the milestone requirements? (YES/NO)
+2. Code quality score (0-100)
+3. Specific feedback on what's good
+4. Areas for improvement
+5. Security concerns (if any)
+
+Format as JSON:
+{
+ "meetsRequirements": boolean,
+ "score": number,
+ "strengths": string[],
+ "improvements": string[],
+ "securityIssues": string[]
+}
+`;
+
+ const reviewResult = await getGeminiResponse(reviewPrompt);
+
+ // Parse AI response
+ let parsedReview;
+ try {
+ // Extract JSON from potential markdown code blocks
+ const jsonMatch = reviewResult.match(/```json\n?([\s\S]*?)\n?```/);
+ const jsonStr = jsonMatch ? jsonMatch[1] : reviewResult;
+ parsedReview = JSON.parse(jsonStr);
+ } catch (e) {
+ parsedReview = {
+ meetsRequirements: false,
+ score: 0,
+ strengths: [],
+ improvements: ["Unable to parse AI review"],
+ securityIssues: [],
+ };
+ }
+
+ // Save code submission
+ const submission = await prisma.submission.create({
+ data: {
+ userId,
+ projectId,
+ repoUrl: `code-submission-${Date.now()}`, // Placeholder
+ feedback: JSON.stringify(parsedReview),
+ score: parsedReview.score,
+ aiReviewData: parsedReview,
+ },
+ });
+
+ res.json({
+ success: true,
+ data: {
+ submission,
+ review: parsedReview,
+ },
+ });
+ } catch (error: any) {
+ console.error("Code submission error:", error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
diff --git a/controllers/social/index.ts b/controllers/social/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cbaa63b8eac3692b2be9f8e6e5fa6ed922512a84
--- /dev/null
+++ b/controllers/social/index.ts
@@ -0,0 +1,187 @@
+import { Request, Response } from "express";
+import prisma from "../../prisma/client";
+
+export const followUser = async (req: Request, res: Response) => {
+ const userId = (req as any).user?.id;
+ const { followingId } = req.body;
+
+ try {
+ if (userId === followingId) {
+ return res
+ .status(400)
+ .json({ success: false, message: "Cannot follow yourself" });
+ }
+
+ const existingFollow = await prisma.follow.findUnique({
+ where: { followerId_followingId: { followerId: userId, followingId } },
+ });
+
+ if (existingFollow) {
+ return res
+ .status(400)
+ .json({ success: false, message: "Already following this user" });
+ }
+
+ const follow = await prisma.follow.create({
+ data: { followerId: userId, followingId },
+ });
+
+ await prisma.notification.create({
+ data: {
+ userId: followingId,
+ type: "FOLLOW",
+ title: "New Follower",
+ message: "Someone started following you",
+ link: `/profile/${userId}`,
+ },
+ });
+
+ res.json({ success: true, data: follow });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const unfollowUser = async (req: Request, res: Response) => {
+ const userId = (req as any).user?.id;
+ const { followingId } = req.params;
+
+ try {
+ await prisma.follow.delete({
+ where: { followerId_followingId: { followerId: userId, followingId } },
+ });
+
+ res.json({ success: true, message: "Unfollowed successfully" });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const getFollowers = async (req: Request, res: Response) => {
+ const { userId } = req.params;
+
+ try {
+ const followers = await prisma.follow.findMany({
+ where: { followingId: userId },
+ include: {
+ follower: {
+ select: {
+ id: true,
+ firstName: true,
+ lastName: true,
+ avatarUrl: true,
+ xp: true,
+ },
+ },
+ },
+ });
+
+ res.json({ success: true, data: followers });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const getFollowing = async (req: Request, res: Response) => {
+ const { userId } = req.params;
+
+ try {
+ const following = await prisma.follow.findMany({
+ where: { followerId: userId },
+ include: {
+ following: {
+ select: {
+ id: true,
+ firstName: true,
+ lastName: true,
+ avatarUrl: true,
+ xp: true,
+ },
+ },
+ },
+ });
+
+ res.json({ success: true, data: following });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const getActivityFeed = async (req: Request, res: Response) => {
+ const userId = (req as any).user?.id;
+ const { page = 1, limit = 20 } = req.query;
+
+ try {
+ const following = await prisma.follow.findMany({
+ where: { followerId: userId },
+ select: { followingId: true },
+ });
+
+ const followingIds = following.map((f) => f.followingId);
+
+ const activities = await prisma.userProject.findMany({
+ where: {
+ userId: { in: followingIds },
+ status: "COMPLETED",
+ },
+ include: {
+ user: {
+ select: {
+ id: true,
+ firstName: true,
+ lastName: true,
+ avatarUrl: true,
+ },
+ },
+ project: {
+ select: {
+ id: true,
+ title: true,
+ slug: true,
+ },
+ },
+ },
+ orderBy: { completedAt: "desc" },
+ take: Number(limit),
+ skip: (Number(page) - 1) * Number(limit),
+ });
+
+ res.json({ success: true, data: activities });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+
+export const shareProject = async (req: Request, res: Response) => {
+ const userId = (req as any).user?.id;
+ const { projectId, platform } = req.body;
+
+ try {
+ const project = await prisma.project.findUnique({
+ where: { id: projectId },
+ });
+
+ if (!project) {
+ return res
+ .status(404)
+ .json({ success: false, message: "Project not found" });
+ }
+
+ const shareUrl = `${process.env.CLIENT_URL}/projects/${project.slug}`;
+ const ogImageUrl = `${process.env.CLIENT_URL}/api/og-image/${project.slug}`;
+
+ const share = await prisma.socialShare.create({
+ data: {
+ userId,
+ projectId,
+ platform,
+ shareUrl,
+ ogImageUrl,
+ },
+ });
+
+ res.json({ success: true, data: { shareUrl, ogImageUrl, share } });
+ } catch (error: any) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
diff --git a/controllers/teams/index.ts b/controllers/teams/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..780b7747ebf5410fd4ebb3c603790c8cac844e26
--- /dev/null
+++ b/controllers/teams/index.ts
@@ -0,0 +1,80 @@
+import { Request, Response, NextFunction } from "express";
+import prisma from "../../prisma/client";
+
+export const createTeam = async (
+ req: Request,
+ res: Response,
+ next: NextFunction
+) => {
+ const { name, projectId } = req.body;
+ const userId = (req as any).user?.id;
+
+ try {
+ const team = await prisma.team.create({
+ data: {
+ name,
+ projectId,
+ members: {
+ create: {
+ userId,
+ },
+ },
+ },
+ include: {
+ members: true,
+ },
+ });
+
+ res.status(201).json({ success: true, data: team });
+ } catch (error: any) {
+ next(error);
+ }
+};
+
+export const joinTeam = async (
+ req: Request,
+ res: Response,
+ next: NextFunction
+) => {
+ const { id: teamId } = req.params;
+ const userId = (req as any).user?.id;
+
+ try {
+ const teamMember = await prisma.teamMember.create({
+ data: {
+ teamId,
+ userId,
+ },
+ });
+
+ res.status(201).json({ success: true, data: teamMember });
+ } catch (error: any) {
+ next(error);
+ }
+};
+
+export const getTeamProjects = async (
+ req: Request,
+ res: Response,
+ next: NextFunction
+) => {
+ const { id: teamId } = req.params;
+
+ try {
+ const team = await prisma.team.findUnique({
+ where: { id: teamId },
+ include: {
+ project: true,
+ },
+ });
+
+ if (!team)
+ return res
+ .status(404)
+ .json({ success: false, message: "Team not found" });
+
+ res.json({ success: true, data: team.project });
+ } catch (error: any) {
+ next(error);
+ }
+};
diff --git a/dist/controllers/ai/index.js b/dist/controllers/ai/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..c13978ea81e9d90836e18a37f4418b4b932eee40
--- /dev/null
+++ b/dist/controllers/ai/index.js
@@ -0,0 +1,146 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.requestHint = exports.getChatHistory = exports.getAllChatMessages = exports.chatWithAI = void 0;
+const client_1 = __importDefault(require("../../prisma/client"));
+const gemini_1 = require("../../utils/gemini");
+const chatWithAI = async (req, res) => {
+ var _a, _b, _c;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ const { message, projectId } = req.body;
+ try {
+ // Fetch project context if projectId is provided
+ let projectContext = "";
+ let difficultyMode = null;
+ if (projectId) {
+ const project = await client_1.default.project.findUnique({
+ where: { id: projectId },
+ include: {
+ milestones: true,
+ userProjects: { where: { userId } },
+ },
+ });
+ if (project) {
+ difficultyMode =
+ ((_c = (_b = project.userProjects) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.difficultyModeChosen) || "STANDARD";
+ projectContext = `The user is working on the project: "${project.title}".
+ Description: ${project.description}.
+ Difficulty Level: ${project.difficultyLevel}.
+ User's chosen mode: ${difficultyMode}.
+ Milestones: ${project.milestones
+ .map((m) => `${m.milestoneNumber}. ${m.title}`)
+ .join(", ")}.`;
+ }
+ }
+ // Fetch chat history from DB
+ const history = await client_1.default.chatMessage.findMany({
+ where: { userId, projectId: projectId || null },
+ orderBy: { createdAt: "asc" },
+ take: 20,
+ });
+ const geminiHistory = history.map((msg) => ({
+ role: msg.role === "user" ? "user" : "model",
+ parts: [{ text: msg.content }],
+ }));
+ let modeInstruction = "";
+ if (difficultyMode === "GUIDED") {
+ modeInstruction =
+ "The user is in GUIDED mode. Be very helpful, provide detailed explanations, and don't hesitate to give small code snippets if they are stuck.";
+ }
+ else if (difficultyMode === "HARDCORE") {
+ modeInstruction =
+ "The user is in HARDCORE mode. Be very brief, provide only high-level conceptual hints, and never provide code solutions.";
+ }
+ else {
+ modeInstruction =
+ "The user is in STANDARD mode. Provide balanced guidance, focus on concepts, and only provide code as a last resort.";
+ }
+ const systemPrompt = `You are an AI Guide for a project-based learning platform.
+ Your goal is to help students learn by providing guidance, not just giving away answers.
+ ${projectContext}
+ ${modeInstruction}
+ Keep your responses concise and educational.`;
+ const fullPrompt = `${systemPrompt}\n\nUser: ${message}`;
+ const aiResponse = await (0, gemini_1.getGeminiResponse)(fullPrompt, geminiHistory);
+ // Save messages to DB
+ await client_1.default.chatMessage.createMany({
+ data: [
+ {
+ userId,
+ projectId: projectId || null,
+ role: "user",
+ content: message,
+ },
+ {
+ userId,
+ projectId: projectId || null,
+ role: "assistant",
+ content: aiResponse,
+ },
+ ],
+ });
+ res.json({ success: true, data: aiResponse });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.chatWithAI = chatWithAI;
+const getAllChatMessages = async (req, res) => {
+ var _a;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ try {
+ const messages = await client_1.default.chatMessage.findMany({
+ where: { userId },
+ orderBy: { createdAt: "asc" },
+ });
+ res.json({ success: true, data: messages });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.getAllChatMessages = getAllChatMessages;
+const getChatHistory = async (req, res) => {
+ var _a;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ const { projectId } = req.params;
+ try {
+ const history = await client_1.default.chatMessage.findMany({
+ where: { userId, projectId: projectId === "null" ? null : projectId },
+ orderBy: { createdAt: "asc" },
+ });
+ res.json({ success: true, data: history });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.getChatHistory = getChatHistory;
+const requestHint = async (req, res) => {
+ var _a;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ const { projectId, milestoneNumber } = req.body;
+ try {
+ const milestone = await client_1.default.projectMilestone.findUnique({
+ where: { projectId_milestoneNumber: { projectId, milestoneNumber } },
+ include: { project: true },
+ });
+ if (!milestone)
+ return res
+ .status(404)
+ .json({ success: false, message: "Milestone not found" });
+ const prompt = `The user is stuck on Milestone ${milestoneNumber} ("${milestone.title}") of the project "${milestone.project.title}".
+ Milestone description: ${milestone.description}.
+ Existing hints: ${milestone.hints.join(", ")}.
+ Please provide a new, progressive hint that helps them move forward without revealing the full solution.`;
+ const hint = await (0, gemini_1.getGeminiResponse)(prompt);
+ res.json({ success: true, data: hint });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.requestHint = requestHint;
diff --git a/dist/controllers/analytics/index.js b/dist/controllers/analytics/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..3d64a65139cbbaffe0ae2be036da1101cfd3fcf8
--- /dev/null
+++ b/dist/controllers/analytics/index.js
@@ -0,0 +1,74 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.getAdminAnalytics = exports.getUserAnalytics = void 0;
+const client_1 = __importDefault(require("../../prisma/client"));
+const getUserAnalytics = async (req, res, next) => {
+ var _a;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ try {
+ const user = await client_1.default.user.findUnique({
+ where: { id: userId },
+ select: {
+ xp: true,
+ streak: true,
+ _count: {
+ select: {
+ projects: true,
+ submissions: true,
+ achievements: true
+ }
+ }
+ }
+ });
+ const projectCompletion = await client_1.default.userProject.groupBy({
+ by: ["status"],
+ where: { userId },
+ _count: true
+ });
+ res.json({
+ success: true,
+ data: {
+ stats: user,
+ projectCompletion
+ }
+ });
+ }
+ catch (error) {
+ next(error);
+ }
+};
+exports.getUserAnalytics = getUserAnalytics;
+const getAdminAnalytics = async (req, res, next) => {
+ var _a;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ try {
+ const user = await client_1.default.user.findUnique({ where: { id: userId } });
+ if ((user === null || user === void 0 ? void 0 : user.role) !== "ADMIN") {
+ return res.status(403).json({ success: false, message: "Forbidden" });
+ }
+ const totalUsers = await client_1.default.user.count();
+ const totalProjects = await client_1.default.project.count();
+ const totalSubmissions = await client_1.default.submission.count();
+ const popularProjects = await client_1.default.project.findMany({
+ orderBy: { submissionCount: "desc" },
+ take: 5,
+ select: { title: true, submissionCount: true }
+ });
+ res.json({
+ success: true,
+ data: {
+ totalUsers,
+ totalProjects,
+ totalSubmissions,
+ popularProjects
+ }
+ });
+ }
+ catch (error) {
+ next(error);
+ }
+};
+exports.getAdminAnalytics = getAdminAnalytics;
diff --git a/dist/controllers/auth/index.js b/dist/controllers/auth/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..a2868e8c2b9de594411aa810740a4853d215d473
--- /dev/null
+++ b/dist/controllers/auth/index.js
@@ -0,0 +1,397 @@
+"use strict";
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ var desc = Object.getOwnPropertyDescriptor(m, k);
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+ desc = { enumerable: true, get: function() { return m[k]; } };
+ }
+ Object.defineProperty(o, k2, desc);
+}) : (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ o[k2] = m[k];
+}));
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
+}) : function(o, v) {
+ o["default"] = v;
+});
+var __importStar = (this && this.__importStar) || (function () {
+ var ownKeys = function(o) {
+ ownKeys = Object.getOwnPropertyNames || function (o) {
+ var ar = [];
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
+ return ar;
+ };
+ return ownKeys(o);
+ };
+ return function (mod) {
+ if (mod && mod.__esModule) return mod;
+ var result = {};
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
+ __setModuleDefault(result, mod);
+ return result;
+ };
+})();
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.githubAuthCallback = exports.googleAuthCallback = exports.updateUser = exports.getUser = exports.resetPassword = exports.verifyOTP = exports.forgetPassword = exports.me = exports.logout = exports.login = exports.signUp = void 0;
+const client_1 = __importDefault(require("../../prisma/client"));
+const argon2 = __importStar(require("argon2"));
+const email_1 = require("../../middlewares/email");
+const generateOTP_1 = require("../../middlewares/generateOTP");
+const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
+const dotenv_1 = __importDefault(require("dotenv"));
+const redis_1 = __importDefault(require("../../utils/redis"));
+dotenv_1.default.config();
+const JWT_SECRET = process.env.JWT_SECRET || "default_secret";
+const signUp = async (req, res) => {
+ const { firstName, lastName, skillLevel, email, password, role } = req.body;
+ try {
+ const existingUser = await client_1.default.user.findUnique({ where: { email } });
+ if (existingUser) {
+ return res
+ .status(400)
+ .json({ success: false, message: "Email already registered" });
+ }
+ const hashedPassword = await argon2.hash(password);
+ const user = await client_1.default.user.create({
+ data: {
+ firstName,
+ lastName,
+ email,
+ skillLevel: skillLevel || "BEGINNER",
+ password: hashedPassword,
+ role: role || "STUDENT",
+ },
+ });
+ res.json({
+ success: true,
+ message: "User registered successfully",
+ data: { id: user.id },
+ });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.signUp = signUp;
+const login = async (req, res) => {
+ const { email, password } = req.body;
+ try {
+ const user = await client_1.default.user.findUnique({ where: { email } });
+ if (!user || !user.password) {
+ return res
+ .status(401)
+ .json({ success: false, message: "Invalid Email or Password" });
+ }
+ const verifyPassword = await argon2.verify(user.password, password);
+ if (!verifyPassword) {
+ return res
+ .status(401)
+ .json({ success: false, message: "Invalid Email or Password" });
+ }
+ const token = jsonwebtoken_1.default.sign({ id: user.id, role: user.role }, JWT_SECRET, {
+ expiresIn: "7d",
+ });
+ res.cookie("token", token, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "strict",
+ maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
+ });
+ res.json({
+ success: true,
+ message: "Login successful",
+ data: {
+ id: user.id,
+ firstName: user.firstName,
+ lastName: user.lastName,
+ email: user.email,
+ role: user.role,
+ token,
+ },
+ });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.login = login;
+const logout = async (req, res) => {
+ res.clearCookie("token");
+ res.json({ success: true, message: "Logged out successfully" });
+};
+exports.logout = logout;
+const me = async (req, res) => {
+ var _a;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ if (!userId) {
+ return res.status(401).json({ success: false, message: "Unauthorized" });
+ }
+ try {
+ const user = await client_1.default.user.findUnique({
+ where: { id: userId },
+ select: {
+ id: true,
+ email: true,
+ firstName: true,
+ lastName: true,
+ skillLevel: true,
+ bio: true,
+ portfolioLinks: true,
+ xp: true,
+ streak: true,
+ role: true,
+ avatarUrl: true,
+ createdAt: true,
+ },
+ });
+ if (!user) {
+ return res
+ .status(404)
+ .json({ success: false, message: "User not found" });
+ }
+ res.json({ success: true, data: user });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.me = me;
+const forgetPassword = async (req, res) => {
+ const { email } = req.body;
+ const appName = "Dev Drill";
+ try {
+ const user = await client_1.default.user.findUnique({ where: { email } });
+ if (!user) {
+ return res
+ .status(404)
+ .json({ success: false, message: "Account is not registered" });
+ }
+ const { otp, otpDate } = (0, generateOTP_1.generateOTP)();
+ // Store OTP in Redis with 1 hour expiry
+ await redis_1.default.setex(`otp:${email}`, 3600, otp.toString());
+ const htmlMessage = `
+
+
+
${appName}
+
+ Hello ${user.lastName},
+ We received a request to reset your password for your ${appName} account.
+
+
+
+ This verification code will expire in 1 hour.
+ If you didn't request this password reset, please ignore this email.
+
+
+
+ © ${new Date().getFullYear()} ${appName}. All rights reserved.
+
+
+ `;
+ const textMessage = `${appName} Password Reset\n\n` +
+ `Hello ${user.lastName},\n\n` +
+ `Use this OTP to reset your password: ${otp}\n` +
+ `This code expires in 1 hour.`;
+ (0, email_1.sendEmail)(user.email, `Password Reset Request - ${appName}`, htmlMessage, textMessage);
+ res.json({
+ success: true,
+ message: `An OTP has been sent to your mail. Check your mail for your verification code`,
+ });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.forgetPassword = forgetPassword;
+const verifyOTP = async (req, res) => {
+ const { email, otp: userOtp } = req.body;
+ try {
+ const storedOtp = await redis_1.default.get(`otp:${email}`);
+ if (!storedOtp) {
+ return res
+ .status(400)
+ .json({ success: false, message: "OTP expired or not found" });
+ }
+ if (storedOtp !== userOtp.toString()) {
+ return res
+ .status(400)
+ .json({ success: false, message: "Invalid Verification code!" });
+ }
+ // OTP verified, can remove it now
+ await redis_1.default.del(`otp:${email}`);
+ // Optionally set a flag in Redis that OTP was verified for this email to allow password reset
+ await redis_1.default.setex(`otp_verified:${email}`, 600, "true");
+ res.json({ success: true, message: "Verification successful" });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.verifyOTP = verifyOTP;
+const resetPassword = async (req, res) => {
+ const { email, newPassword } = req.body;
+ try {
+ const isVerified = await redis_1.default.get(`otp_verified:${email}`);
+ if (!isVerified) {
+ return res
+ .status(400)
+ .json({ success: false, message: "OTP not verified" });
+ }
+ const user = await client_1.default.user.findUnique({ where: { email } });
+ if (!user)
+ return res
+ .status(404)
+ .json({ success: false, message: "User not found" });
+ if (user.password) {
+ const verifyPassword = await argon2.verify(user.password, newPassword);
+ if (verifyPassword) {
+ return res
+ .status(400)
+ .json({ success: false, message: "You entered your old password" });
+ }
+ }
+ const newPass = await argon2.hash(newPassword);
+ await client_1.default.user.update({
+ where: { email },
+ data: { password: newPass },
+ });
+ await redis_1.default.del(`otp_verified:${email}`);
+ res.json({ success: true, message: "Password successfully reset" });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.resetPassword = resetPassword;
+const getUser = async (req, res) => {
+ const { id } = req.params;
+ try {
+ const user = await client_1.default.user.findUnique({
+ where: { id },
+ select: {
+ id: true,
+ email: true,
+ firstName: true,
+ lastName: true,
+ skillLevel: true,
+ bio: true,
+ portfolioLinks: true,
+ xp: true,
+ streak: true,
+ role: true,
+ avatarUrl: true,
+ createdAt: true,
+ },
+ });
+ if (!user)
+ return res
+ .status(404)
+ .json({ success: false, message: "User not found" });
+ res.json({ success: true, data: user });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.getUser = getUser;
+const updateUser = async (req, res) => {
+ const { id } = req.params;
+ let { firstName, lastName, email, password, bio, skillLevel, portfolioLinks, avatarUrl, } = req.body;
+ try {
+ const user = await client_1.default.user.findUnique({ where: { id } });
+ if (!user) {
+ return res
+ .status(404)
+ .json({ success: false, message: "User not found" });
+ }
+ const data = {
+ firstName,
+ lastName,
+ bio,
+ skillLevel,
+ portfolioLinks,
+ avatarUrl,
+ };
+ if (email && email !== user.email) {
+ const existingUser = await client_1.default.user.findUnique({ where: { email } });
+ if (existingUser) {
+ return res
+ .status(400)
+ .json({ success: false, message: "Email already in use" });
+ }
+ data.email = email;
+ }
+ if (password) {
+ data.password = await argon2.hash(password);
+ }
+ const updatedUser = await client_1.default.user.update({
+ where: { googleId: user.googleId || undefined },
+ data,
+ });
+ res.json({
+ success: true,
+ data: {
+ id: updatedUser.id,
+ firstName: updatedUser.firstName,
+ lastName: updatedUser.lastName,
+ email: updatedUser.email,
+ role: updatedUser.role,
+ skillLevel: updatedUser.skillLevel,
+ },
+ });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.updateUser = updateUser;
+const googleAuthCallback = async (req, res) => {
+ try {
+ const user = req.user;
+ if (!user) {
+ return res.redirect(`${process.env.CLIENT_URL}/auth/signin?error=auth_failed`);
+ }
+ const token = jsonwebtoken_1.default.sign({ id: user.id, role: user.role }, JWT_SECRET, {
+ expiresIn: "7d",
+ });
+ res.cookie("token", token, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax",
+ maxAge: 7 * 24 * 60 * 60 * 1000,
+ });
+ res.redirect(`${process.env.CLIENT_URL}/dashboard`);
+ }
+ catch (error) {
+ res.redirect(`${process.env.CLIENT_URL}/auth/signin?error=server_error`);
+ }
+};
+exports.googleAuthCallback = googleAuthCallback;
+const githubAuthCallback = async (req, res) => {
+ try {
+ const user = req.user;
+ if (!user) {
+ return res.redirect(`${process.env.CLIENT_URL}/auth/signin?error=auth_failed`);
+ }
+ const token = jsonwebtoken_1.default.sign({ id: user.id, role: user.role }, JWT_SECRET, {
+ expiresIn: "7d",
+ });
+ res.cookie("token", token, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax",
+ maxAge: 7 * 24 * 60 * 60 * 1000,
+ });
+ res.redirect(`${process.env.CLIENT_URL}/dashboard`);
+ }
+ catch (error) {
+ res.redirect(`${process.env.CLIENT_URL}/auth/signin?error=server_error`);
+ }
+};
+exports.githubAuthCallback = githubAuthCallback;
diff --git a/dist/controllers/code-review/index.js b/dist/controllers/code-review/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..ce6c9edcee3274598495098342a9ccdb9767eb71
--- /dev/null
+++ b/dist/controllers/code-review/index.js
@@ -0,0 +1,70 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.getSubmissionReview = exports.analyzeCode = void 0;
+const client_1 = __importDefault(require("../../prisma/client"));
+const gemini_1 = require("../../utils/gemini");
+const axios_1 = __importDefault(require("axios"));
+const analyzeCode = async (req, res) => {
+ var _a;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ const { repoUrl, projectId } = req.body;
+ try {
+ // 1. Fetch code from GitHub (simplified: fetch README or a few files for demo)
+ // In a real app, you'd use a GitHub App or User Token to clone or fetch via API
+ const repoPath = repoUrl.replace("https://github.com/", "");
+ const [owner, repo] = repoPath.split("/");
+ // Fetch repo info as a placeholder for analysis
+ const githubApiUrl = `https://api.github.com/repos/${owner}/${repo}`;
+ const repoInfo = await axios_1.default.get(githubApiUrl);
+ // 2. Automated analysis (simulated)
+ const metrics = {
+ complexity: "Medium",
+ securityIssues: 0,
+ lintErrors: 0
+ };
+ // 3. AI-powered feedback
+ const prompt = `Please review this GitHub repository: ${repoUrl}.
+ Owner: ${owner}
+ Repo: ${repo}
+ Description: ${repoInfo.data.description}
+ Provide constructive feedback on the project structure and suggest improvements for a student.`;
+ const feedback = await (0, gemini_1.getGeminiResponse)(prompt);
+ // 4. Store review results
+ const submission = await client_1.default.submission.create({
+ data: {
+ userId,
+ projectId,
+ repoUrl,
+ feedback,
+ score: 85 // Simulated score
+ }
+ });
+ res.json({ success: true, data: submission });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.analyzeCode = analyzeCode;
+const getSubmissionReview = async (req, res) => {
+ const { id } = req.params;
+ try {
+ const submission = await client_1.default.submission.findUnique({
+ where: { id },
+ include: {
+ project: { select: { title: true } },
+ user: { select: { firstName: true, lastName: true } }
+ }
+ });
+ if (!submission)
+ return res.status(404).json({ success: false, message: "Submission not found" });
+ res.json({ success: true, data: submission });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.getSubmissionReview = getSubmissionReview;
diff --git a/dist/controllers/community/index.js b/dist/controllers/community/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..304960f7d7eedb9896e9042b5da1cb0639fb09c3
--- /dev/null
+++ b/dist/controllers/community/index.js
@@ -0,0 +1,156 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.createForumThread = exports.getForumThreads = exports.voteOnSubmission = exports.commentOnSubmission = exports.getPublicSubmissions = void 0;
+const client_1 = __importDefault(require("../../prisma/client"));
+const getPublicSubmissions = async (req, res, next) => {
+ try {
+ const submissions = await client_1.default.submission.findMany({
+ where: { isPublic: true },
+ include: {
+ user: {
+ select: {
+ id: true,
+ firstName: true,
+ lastName: true,
+ avatarUrl: true,
+ },
+ },
+ project: {
+ select: {
+ id: true,
+ title: true,
+ },
+ },
+ _count: {
+ select: {
+ comments: true,
+ votes: true,
+ },
+ },
+ },
+ orderBy: { createdAt: "desc" },
+ });
+ res.json({ success: true, data: submissions });
+ }
+ catch (error) {
+ next(error);
+ }
+};
+exports.getPublicSubmissions = getPublicSubmissions;
+const commentOnSubmission = async (req, res, next) => {
+ var _a;
+ const { id: submissionId } = req.params;
+ const { content } = req.body;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ try {
+ const comment = await client_1.default.comment.create({
+ data: {
+ content,
+ userId,
+ submissionId,
+ },
+ include: {
+ user: {
+ select: {
+ id: true,
+ firstName: true,
+ lastName: true,
+ avatarUrl: true,
+ },
+ },
+ },
+ });
+ res.status(201).json({ success: true, data: comment });
+ }
+ catch (error) {
+ next(error);
+ }
+};
+exports.commentOnSubmission = commentOnSubmission;
+const voteOnSubmission = async (req, res, next) => {
+ var _a;
+ const { id: submissionId } = req.params;
+ const { value } = req.body; // 1 for upvote, -1 for downvote
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ try {
+ const vote = await client_1.default.vote.upsert({
+ where: {
+ userId_submissionId: {
+ userId,
+ submissionId,
+ },
+ },
+ update: { value },
+ create: {
+ value,
+ userId,
+ submissionId,
+ },
+ });
+ res.json({ success: true, data: vote });
+ }
+ catch (error) {
+ next(error);
+ }
+};
+exports.voteOnSubmission = voteOnSubmission;
+const getForumThreads = async (req, res, next) => {
+ const { projectId } = req.query;
+ try {
+ const threads = await client_1.default.forumThread.findMany({
+ where: projectId ? { projectId: String(projectId) } : {},
+ include: {
+ user: {
+ select: {
+ id: true,
+ firstName: true,
+ lastName: true,
+ avatarUrl: true,
+ },
+ },
+ _count: {
+ select: { posts: true },
+ },
+ },
+ orderBy: { createdAt: "desc" },
+ });
+ res.json({ success: true, data: threads });
+ }
+ catch (error) {
+ next(error);
+ }
+};
+exports.getForumThreads = getForumThreads;
+const createForumThread = async (req, res, next) => {
+ var _a;
+ const { title, content, projectId } = req.body;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ try {
+ const thread = await client_1.default.forumThread.create({
+ data: {
+ title,
+ content,
+ userId,
+ projectId: projectId || null,
+ },
+ include: {
+ user: {
+ select: {
+ id: true,
+ firstName: true,
+ lastName: true,
+ avatarUrl: true,
+ },
+ },
+ },
+ });
+ res.status(201).json({ success: true, data: thread });
+ }
+ catch (error) {
+ next(error);
+ }
+};
+exports.createForumThread = createForumThread;
diff --git a/dist/controllers/events/index.js b/dist/controllers/events/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..906dadc215f1c5bb528dc5655c3aff8e11361a29
--- /dev/null
+++ b/dist/controllers/events/index.js
@@ -0,0 +1,202 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.getLeaderboard = exports.updateEventScore = exports.leaveEvent = exports.joinEvent = exports.getEvent = exports.getEvents = exports.createEvent = void 0;
+const client_1 = __importDefault(require("../../prisma/client"));
+const createEvent = async (req, res) => {
+ const { title, description, type, startDate, endDate, prizePool, maxParticipants, bannerUrl, } = req.body;
+ try {
+ const event = await client_1.default.event.create({
+ data: {
+ title,
+ description,
+ type,
+ startDate: new Date(startDate),
+ endDate: new Date(endDate),
+ prizePool,
+ maxParticipants,
+ bannerUrl,
+ },
+ });
+ res.json({ success: true, data: event });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.createEvent = createEvent;
+const getEvents = async (req, res) => {
+ const { status, type, page = 1, limit = 10 } = req.query;
+ try {
+ const where = {};
+ if (status)
+ where.status = status;
+ if (type)
+ where.type = type;
+ const events = await client_1.default.event.findMany({
+ where,
+ include: {
+ _count: {
+ select: { participants: true },
+ },
+ },
+ orderBy: { startDate: "desc" },
+ take: Number(limit),
+ skip: (Number(page) - 1) * Number(limit),
+ });
+ res.json({ success: true, data: events });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.getEvents = getEvents;
+const getEvent = async (req, res) => {
+ const { id } = req.params;
+ try {
+ const event = await client_1.default.event.findUnique({
+ where: { id },
+ include: {
+ participants: {
+ include: {
+ user: {
+ select: {
+ id: true,
+ firstName: true,
+ lastName: true,
+ avatarUrl: true,
+ },
+ },
+ },
+ },
+ leaderboard: {
+ include: {
+ user: {
+ select: {
+ id: true,
+ firstName: true,
+ lastName: true,
+ avatarUrl: true,
+ },
+ },
+ },
+ orderBy: { score: "desc" },
+ take: 100,
+ },
+ },
+ });
+ if (!event) {
+ return res
+ .status(404)
+ .json({ success: false, message: "Event not found" });
+ }
+ res.json({ success: true, data: event });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.getEvent = getEvent;
+const joinEvent = async (req, res) => {
+ var _a;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ const { eventId } = req.params;
+ try {
+ const event = await client_1.default.event.findUnique({
+ where: { id: eventId },
+ include: { _count: { select: { participants: true } } },
+ });
+ if (!event) {
+ return res
+ .status(404)
+ .json({ success: false, message: "Event not found" });
+ }
+ if (event.maxParticipants &&
+ event._count.participants >= event.maxParticipants) {
+ return res.status(400).json({ success: false, message: "Event is full" });
+ }
+ const participant = await client_1.default.eventParticipant.create({
+ data: { eventId, userId },
+ });
+ await client_1.default.eventLeaderboard.create({
+ data: { eventId, userId, score: 0 },
+ });
+ res.json({ success: true, data: participant });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.joinEvent = joinEvent;
+const leaveEvent = async (req, res) => {
+ var _a;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ const { eventId } = req.params;
+ try {
+ await client_1.default.eventParticipant.delete({
+ where: { eventId_userId: { eventId, userId } },
+ });
+ await client_1.default.eventLeaderboard.delete({
+ where: { eventId_userId: { eventId, userId } },
+ });
+ res.json({ success: true, message: "Left event successfully" });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.leaveEvent = leaveEvent;
+const updateEventScore = async (req, res) => {
+ const { eventId, userId } = req.params;
+ const { score } = req.body;
+ try {
+ const leaderboard = await client_1.default.eventLeaderboard.update({
+ where: { eventId_userId: { eventId, userId } },
+ data: { score },
+ });
+ const allScores = await client_1.default.eventLeaderboard.findMany({
+ where: { eventId },
+ orderBy: { score: "desc" },
+ });
+ for (let i = 0; i < allScores.length; i++) {
+ await client_1.default.eventLeaderboard.update({
+ where: { id: allScores[i].id },
+ data: { rank: i + 1 },
+ });
+ }
+ res.json({ success: true, data: leaderboard });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.updateEventScore = updateEventScore;
+const getLeaderboard = async (req, res) => {
+ const { eventId } = req.params;
+ const { limit = 100 } = req.query;
+ try {
+ const leaderboard = await client_1.default.eventLeaderboard.findMany({
+ where: { eventId },
+ include: {
+ user: {
+ select: {
+ id: true,
+ firstName: true,
+ lastName: true,
+ avatarUrl: true,
+ xp: true,
+ },
+ },
+ },
+ orderBy: { score: "desc" },
+ take: Number(limit),
+ });
+ res.json({ success: true, data: leaderboard });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.getLeaderboard = getLeaderboard;
diff --git a/dist/controllers/gamification/index.js b/dist/controllers/gamification/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..c4f8ccc7be95a201542e2508943970665c077bf3
--- /dev/null
+++ b/dist/controllers/gamification/index.js
@@ -0,0 +1,71 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.awardXP = exports.getUserAchievements = exports.getLeaderboard = void 0;
+const client_1 = __importDefault(require("../../prisma/client"));
+const redis_1 = __importDefault(require("../../utils/redis"));
+const LEADERBOARD_CACHE_KEY = "leaderboard:top20";
+const CACHE_TTL = 3600; // 1 hour
+const getLeaderboard = async (req, res, next) => {
+ try {
+ // Try to get from cache
+ const cachedLeaderboard = await redis_1.default.get(LEADERBOARD_CACHE_KEY);
+ if (cachedLeaderboard) {
+ return res.json({ success: true, data: JSON.parse(cachedLeaderboard), cached: true });
+ }
+ const leaderboard = await client_1.default.user.findMany({
+ select: {
+ id: true,
+ firstName: true,
+ lastName: true,
+ avatarUrl: true,
+ xp: true,
+ streak: true
+ },
+ orderBy: { xp: "desc" },
+ take: 20
+ });
+ // Save to cache
+ await redis_1.default.setex(LEADERBOARD_CACHE_KEY, CACHE_TTL, JSON.stringify(leaderboard));
+ res.json({ success: true, data: leaderboard });
+ }
+ catch (error) {
+ next(error);
+ }
+};
+exports.getLeaderboard = getLeaderboard;
+const getUserAchievements = async (req, res, next) => {
+ var _a;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ try {
+ const achievements = await client_1.default.userAchievement.findMany({
+ where: { userId },
+ include: {
+ achievement: true
+ }
+ });
+ res.json({ success: true, data: achievements });
+ }
+ catch (error) {
+ next(error);
+ }
+};
+exports.getUserAchievements = getUserAchievements;
+const awardXP = async (userId, amount) => {
+ try {
+ await client_1.default.user.update({
+ where: { id: userId },
+ data: {
+ xp: { increment: amount }
+ }
+ });
+ // Invalidate leaderboard cache when XP changes
+ await redis_1.default.del(LEADERBOARD_CACHE_KEY);
+ }
+ catch (error) {
+ console.error("Error awarding XP:", error);
+ }
+};
+exports.awardXP = awardXP;
diff --git a/dist/controllers/notifications/index.js b/dist/controllers/notifications/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..f096318c82c920646345cf8a8e1408967d29d99d
--- /dev/null
+++ b/dist/controllers/notifications/index.js
@@ -0,0 +1,78 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.deleteNotification = exports.markAllAsRead = exports.markAsRead = exports.getNotifications = void 0;
+const client_1 = __importDefault(require("../../prisma/client"));
+const getNotifications = async (req, res) => {
+ var _a;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ const { page = 1, limit = 20, unreadOnly = false } = req.query;
+ try {
+ const where = { userId };
+ if (unreadOnly === "true") {
+ where.read = false;
+ }
+ const notifications = await client_1.default.notification.findMany({
+ where,
+ orderBy: { createdAt: "desc" },
+ take: Number(limit),
+ skip: (Number(page) - 1) * Number(limit),
+ });
+ const unreadCount = await client_1.default.notification.count({
+ where: { userId, read: false },
+ });
+ res.json({ success: true, data: { notifications, unreadCount } });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.getNotifications = getNotifications;
+const markAsRead = async (req, res) => {
+ var _a;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ const { notificationId } = req.params;
+ try {
+ await client_1.default.notification.updateMany({
+ where: { id: notificationId, userId },
+ data: { read: true },
+ });
+ res.json({ success: true, message: "Notification marked as read" });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.markAsRead = markAsRead;
+const markAllAsRead = async (req, res) => {
+ var _a;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ try {
+ await client_1.default.notification.updateMany({
+ where: { userId, read: false },
+ data: { read: true },
+ });
+ res.json({ success: true, message: "All notifications marked as read" });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.markAllAsRead = markAllAsRead;
+const deleteNotification = async (req, res) => {
+ var _a;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ const { notificationId } = req.params;
+ try {
+ await client_1.default.notification.deleteMany({
+ where: { id: notificationId, userId },
+ });
+ res.json({ success: true, message: "Notification deleted" });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.deleteNotification = deleteNotification;
diff --git a/dist/controllers/paths/index.js b/dist/controllers/paths/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..e6c3a1bf7a0e1092893c954891918dd915f0d2af
--- /dev/null
+++ b/dist/controllers/paths/index.js
@@ -0,0 +1,87 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.startPath = exports.getPathProgress = exports.getLearningPaths = void 0;
+const client_1 = __importDefault(require("../../prisma/client"));
+const getLearningPaths = async (req, res, next) => {
+ try {
+ const paths = await client_1.default.learningPath.findMany({
+ include: {
+ projects: {
+ include: {
+ project: {
+ select: {
+ title: true,
+ difficultyLevel: true,
+ },
+ },
+ },
+ orderBy: { orderIndex: "asc" },
+ },
+ },
+ });
+ res.json({ success: true, data: paths });
+ }
+ catch (error) {
+ next(error);
+ }
+};
+exports.getLearningPaths = getLearningPaths;
+const getPathProgress = async (req, res, next) => {
+ var _a;
+ const { id: pathId } = req.params;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ try {
+ const progress = await client_1.default.userPathProgress.findUnique({
+ where: {
+ userId_pathId: {
+ userId,
+ pathId,
+ },
+ },
+ include: {
+ path: {
+ include: {
+ projects: {
+ include: { project: true },
+ orderBy: { orderIndex: "asc" },
+ },
+ },
+ },
+ },
+ });
+ res.json({ success: true, data: progress });
+ }
+ catch (error) {
+ next(error);
+ }
+};
+exports.getPathProgress = getPathProgress;
+const startPath = async (req, res, next) => {
+ var _a;
+ const { id: pathId } = req.params;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ try {
+ const progress = await client_1.default.userPathProgress.upsert({
+ where: {
+ userId_pathId: {
+ userId,
+ pathId
+ }
+ },
+ update: {},
+ create: {
+ userId,
+ pathId,
+ currentProjectIndex: 0,
+ },
+ });
+ res.status(201).json({ success: true, data: progress });
+ }
+ catch (error) {
+ next(error);
+ }
+};
+exports.startPath = startPath;
diff --git a/dist/controllers/projects/index.js b/dist/controllers/projects/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..3c01ecc493fa34076a2730c3da3ced6fc8b33ba0
--- /dev/null
+++ b/dist/controllers/projects/index.js
@@ -0,0 +1,416 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.getFeaturedProjects = exports.completeMilestone = exports.submitProject = exports.getUserProgress = exports.completeProject = exports.updateProgress = exports.startProject = exports.getProjectById = exports.getProjects = exports.deleteProject = exports.updateProject = exports.createProject = void 0;
+const client_1 = __importDefault(require("../../prisma/client"));
+const file_1 = require("../../middlewares/file");
+const auth_1 = require("../../middlewares/auth");
+function generateSlug(title) {
+ return title
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "-")
+ .replace(/^-+|-+$/g, "");
+}
+const createProject = async (req, res) => {
+ var _a;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ const { title, description, difficultyLevel, technologies, categories, estimatedTime, learningObjectives, resourceLinks, starterRepoUrl, difficultyModes, coverImage, milestones, } = req.body;
+ try {
+ if (!userId)
+ return res.status(401).json({ success: false, message: "Unauthorized" });
+ const user = await client_1.default.user.findUnique({ where: { id: userId } });
+ if (!user || (user.role !== "ADMIN" && user.role !== "CONTRIBUTOR")) {
+ return res.status(403).json({
+ success: false,
+ message: "Only Admins or Contributors can create projects",
+ });
+ }
+ let imageUrl = coverImage;
+ if (coverImage && !coverImage.startsWith("http")) {
+ imageUrl = await (0, file_1.upLoadFiles)(coverImage);
+ }
+ const project = await client_1.default.project.create({
+ data: {
+ title,
+ slug: generateSlug(title),
+ description,
+ difficultyLevel: difficultyLevel,
+ technologies,
+ categories,
+ estimatedTime,
+ learningObjectives,
+ resourceLinks: resourceLinks || [],
+ starterRepoUrl,
+ difficultyModes: difficultyModes || ["GUIDED", "STANDARD", "HARDCORE"],
+ createdBy: { connect: { id: userId } },
+ milestones: {
+ create: milestones === null || milestones === void 0 ? void 0 : milestones.map((m, index) => ({
+ milestoneNumber: index + 1,
+ title: m.title,
+ description: m.description,
+ hints: m.hints || [],
+ validationCriteria: m.validationCriteria,
+ })),
+ },
+ },
+ include: {
+ milestones: true,
+ },
+ });
+ res.status(201).json({ success: true, data: project });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.createProject = createProject;
+const updateProject = async (req, res) => {
+ var _a;
+ const { id } = req.params;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ const updateData = req.body;
+ try {
+ const project = await client_1.default.project.findUnique({ where: { id } });
+ if (!project)
+ return res
+ .status(404)
+ .json({ success: false, message: "Project not found" });
+ // Check permissions
+ const user = await client_1.default.user.findUnique({ where: { id: userId } });
+ if (!user || (user.role !== "ADMIN" && project.createdById !== userId)) {
+ return res.status(403).json({
+ success: false,
+ message: "Unauthorized to update this project",
+ });
+ }
+ const updatedProject = await client_1.default.project.update({
+ where: { id },
+ data: updateData,
+ include: { milestones: true },
+ });
+ res.json({ success: true, data: updatedProject });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.updateProject = updateProject;
+const deleteProject = async (req, res) => {
+ var _a;
+ const { id } = req.params;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ try {
+ const project = await client_1.default.project.findUnique({ where: { id } });
+ if (!project)
+ return res
+ .status(404)
+ .json({ success: false, message: "Project not found" });
+ const user = await client_1.default.user.findUnique({ where: { id: userId } });
+ if (!user || user.role !== "ADMIN") {
+ return res
+ .status(403)
+ .json({ success: false, message: "Only Admins can delete projects" });
+ }
+ await client_1.default.project.delete({ where: { id } });
+ res.json({ success: true, message: "Project deleted successfully" });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.deleteProject = deleteProject;
+const getProjects = async (req, res) => {
+ const { difficulty, tech, category, title } = req.query;
+ try {
+ const where = {};
+ if (difficulty)
+ where.difficultyLevel = difficulty;
+ if (tech)
+ where.technologies = { has: tech };
+ if (category)
+ where.categories = { has: category };
+ if (title)
+ where.title = { contains: title, mode: "insensitive" };
+ const projects = await client_1.default.project.findMany({
+ where,
+ include: {
+ createdBy: {
+ select: { firstName: true, lastName: true },
+ },
+ },
+ orderBy: { createdAt: "desc" },
+ });
+ res.json({ success: true, data: projects });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.getProjects = getProjects;
+const getProjectById = async (req, res) => {
+ var _a;
+ const { id } = req.params;
+ try {
+ const user = await (0, auth_1.checkAuth)(req);
+ const project = (await client_1.default.project.findUnique({
+ where: { id },
+ include: {
+ milestones: { orderBy: { milestoneNumber: "asc" } },
+ createdBy: { select: { firstName: true, lastName: true } },
+ userProjects: user ? { where: { userId: user.id } } : false,
+ },
+ }));
+ if (!project)
+ return res
+ .status(404)
+ .json({ success: false, message: "Project not found" });
+ // If user is authenticated, fetch their progress and completed milestones for each mode
+ let progressByMode = {};
+ if (user && project.userProjects) {
+ for (const up of project.userProjects) {
+ const completedMilestones = await client_1.default.userMilestone.findMany({
+ where: {
+ userId: user.id,
+ userProjectId: up.id,
+ },
+ select: { milestoneId: true },
+ });
+ progressByMode[up.difficultyModeChosen] = {
+ status: up.status,
+ completedMilestones: completedMilestones.map((m) => m.milestoneId),
+ userProjectId: up.id,
+ };
+ }
+ }
+ res.json({
+ success: true,
+ data: {
+ ...project,
+ progressByMode,
+ // For backward compatibility
+ userProgress: ((_a = project.userProjects) === null || _a === void 0 ? void 0 : _a[0]) || null,
+ },
+ });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.getProjectById = getProjectById;
+// User Progress Endpoints
+const startProject = async (req, res) => {
+ var _a;
+ const { id: projectId } = req.params;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ const { difficultyModeChosen } = req.body;
+ try {
+ const mode = difficultyModeChosen || "STANDARD";
+ const existingProgress = await client_1.default.userProject.findUnique({
+ where: {
+ userId_projectId_difficultyModeChosen: {
+ userId,
+ projectId,
+ difficultyModeChosen: mode,
+ },
+ },
+ });
+ if (existingProgress) {
+ return res.status(400).json({
+ success: false,
+ message: "Project already started in this mode",
+ });
+ }
+ const progress = await client_1.default.userProject.create({
+ data: {
+ userId,
+ projectId,
+ difficultyModeChosen: mode,
+ status: "IN_PROGRESS",
+ },
+ });
+ res.status(201).json({ success: true, data: progress });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.startProject = startProject;
+const updateProgress = async (req, res) => {
+ var _a;
+ const { id: projectId } = req.params;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ const { status, repoUrl, difficultyMode } = req.body;
+ try {
+ const progress = await client_1.default.userProject.update({
+ where: {
+ userId_projectId_difficultyModeChosen: {
+ userId,
+ projectId,
+ difficultyModeChosen: difficultyMode,
+ },
+ },
+ data: { status: status, repoUrl },
+ });
+ res.json({ success: true, data: progress });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.updateProgress = updateProgress;
+const completeProject = async (req, res) => {
+ var _a;
+ const { id: projectId } = req.params;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ const { repoUrl, difficultyMode } = req.body;
+ try {
+ const progress = await client_1.default.userProject.update({
+ where: {
+ userId_projectId_difficultyModeChosen: {
+ userId,
+ projectId,
+ difficultyModeChosen: difficultyMode,
+ },
+ },
+ data: {
+ status: "COMPLETED",
+ completedAt: new Date(),
+ repoUrl,
+ },
+ });
+ // Award XP (simple example)
+ await client_1.default.user.update({
+ where: { id: userId },
+ data: { xp: { increment: 100 } },
+ });
+ // Update project completion rate
+ const totalStarted = await client_1.default.userProject.count({
+ where: { projectId },
+ });
+ const totalCompleted = await client_1.default.userProject.count({
+ where: { projectId, status: "COMPLETED" },
+ });
+ await client_1.default.project.update({
+ where: { id: projectId },
+ data: {
+ completionRate: (totalCompleted / totalStarted) * 100,
+ submissionCount: { increment: 1 },
+ },
+ });
+ res.json({ success: true, data: progress });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.completeProject = completeProject;
+const getUserProgress = async (req, res) => {
+ var _a;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ try {
+ const progress = await client_1.default.userProject.findMany({
+ where: { userId },
+ include: {
+ project: {
+ select: { title: true, difficultyLevel: true },
+ },
+ },
+ });
+ res.json({ success: true, data: progress });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.getUserProgress = getUserProgress;
+// Submissions
+const submitProject = async (req, res) => {
+ var _a;
+ const { id: projectId } = req.params;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ const { repoUrl } = req.body;
+ try {
+ const submission = await client_1.default.submission.create({
+ data: {
+ userId,
+ projectId,
+ repoUrl,
+ },
+ });
+ res.status(201).json({ success: true, data: submission });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.submitProject = submitProject;
+const completeMilestone = async (req, res) => {
+ var _a;
+ const { id: projectId, milestoneId } = req.params;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ const { difficultyMode } = req.body;
+ try {
+ const userProject = await client_1.default.userProject.findUnique({
+ where: {
+ userId_projectId_difficultyModeChosen: {
+ userId,
+ projectId,
+ difficultyModeChosen: difficultyMode,
+ },
+ },
+ });
+ if (!userProject) {
+ return res.status(400).json({
+ success: false,
+ message: "Project not started by user in this mode",
+ });
+ }
+ const existingMilestone = await client_1.default.userMilestone.findUnique({
+ where: {
+ userId_milestoneId_userProjectId: {
+ userId,
+ milestoneId,
+ userProjectId: userProject.id,
+ },
+ },
+ });
+ if (existingMilestone) {
+ return res.status(400).json({
+ success: false,
+ message: "Milestone already completed in this mode",
+ });
+ }
+ const milestone = await client_1.default.userMilestone.create({
+ data: {
+ userId,
+ milestoneId,
+ userProjectId: userProject.id,
+ },
+ });
+ res.status(201).json({ success: true, data: milestone });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.completeMilestone = completeMilestone;
+const getFeaturedProjects = async (req, res) => {
+ try {
+ const featuredProjects = await client_1.default.project.findMany({
+ take: 4,
+ orderBy: { submissionCount: "desc" },
+ include: {
+ createdBy: {
+ select: { firstName: true, lastName: true },
+ },
+ },
+ });
+ res.json({ success: true, data: featuredProjects });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.getFeaturedProjects = getFeaturedProjects;
diff --git a/dist/controllers/social/index.js b/dist/controllers/social/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..374ccd0e95f7adae3f911cdc72ad252a1e2d7f07
--- /dev/null
+++ b/dist/controllers/social/index.js
@@ -0,0 +1,181 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.shareProject = exports.getActivityFeed = exports.getFollowing = exports.getFollowers = exports.unfollowUser = exports.followUser = void 0;
+const client_1 = __importDefault(require("../../prisma/client"));
+const followUser = async (req, res) => {
+ var _a;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ const { followingId } = req.body;
+ try {
+ if (userId === followingId) {
+ return res
+ .status(400)
+ .json({ success: false, message: "Cannot follow yourself" });
+ }
+ const existingFollow = await client_1.default.follow.findUnique({
+ where: { followerId_followingId: { followerId: userId, followingId } },
+ });
+ if (existingFollow) {
+ return res
+ .status(400)
+ .json({ success: false, message: "Already following this user" });
+ }
+ const follow = await client_1.default.follow.create({
+ data: { followerId: userId, followingId },
+ });
+ await client_1.default.notification.create({
+ data: {
+ userId: followingId,
+ type: "FOLLOW",
+ title: "New Follower",
+ message: "Someone started following you",
+ link: `/profile/${userId}`,
+ },
+ });
+ res.json({ success: true, data: follow });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.followUser = followUser;
+const unfollowUser = async (req, res) => {
+ var _a;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ const { followingId } = req.params;
+ try {
+ await client_1.default.follow.delete({
+ where: { followerId_followingId: { followerId: userId, followingId } },
+ });
+ res.json({ success: true, message: "Unfollowed successfully" });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.unfollowUser = unfollowUser;
+const getFollowers = async (req, res) => {
+ const { userId } = req.params;
+ try {
+ const followers = await client_1.default.follow.findMany({
+ where: { followingId: userId },
+ include: {
+ follower: {
+ select: {
+ id: true,
+ firstName: true,
+ lastName: true,
+ avatarUrl: true,
+ xp: true,
+ },
+ },
+ },
+ });
+ res.json({ success: true, data: followers });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.getFollowers = getFollowers;
+const getFollowing = async (req, res) => {
+ const { userId } = req.params;
+ try {
+ const following = await client_1.default.follow.findMany({
+ where: { followerId: userId },
+ include: {
+ following: {
+ select: {
+ id: true,
+ firstName: true,
+ lastName: true,
+ avatarUrl: true,
+ xp: true,
+ },
+ },
+ },
+ });
+ res.json({ success: true, data: following });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.getFollowing = getFollowing;
+const getActivityFeed = async (req, res) => {
+ var _a;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ const { page = 1, limit = 20 } = req.query;
+ try {
+ const following = await client_1.default.follow.findMany({
+ where: { followerId: userId },
+ select: { followingId: true },
+ });
+ const followingIds = following.map((f) => f.followingId);
+ const activities = await client_1.default.userProject.findMany({
+ where: {
+ userId: { in: followingIds },
+ status: "COMPLETED",
+ },
+ include: {
+ user: {
+ select: {
+ id: true,
+ firstName: true,
+ lastName: true,
+ avatarUrl: true,
+ },
+ },
+ project: {
+ select: {
+ id: true,
+ title: true,
+ slug: true,
+ },
+ },
+ },
+ orderBy: { completedAt: "desc" },
+ take: Number(limit),
+ skip: (Number(page) - 1) * Number(limit),
+ });
+ res.json({ success: true, data: activities });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.getActivityFeed = getActivityFeed;
+const shareProject = async (req, res) => {
+ var _a;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ const { projectId, platform } = req.body;
+ try {
+ const project = await client_1.default.project.findUnique({
+ where: { id: projectId },
+ });
+ if (!project) {
+ return res
+ .status(404)
+ .json({ success: false, message: "Project not found" });
+ }
+ const shareUrl = `${process.env.CLIENT_URL}/projects/${project.slug}`;
+ const ogImageUrl = `${process.env.CLIENT_URL}/api/og-image/${project.slug}`;
+ const share = await client_1.default.socialShare.create({
+ data: {
+ userId,
+ projectId,
+ platform,
+ shareUrl,
+ ogImageUrl,
+ },
+ });
+ res.json({ success: true, data: { shareUrl, ogImageUrl, share } });
+ }
+ catch (error) {
+ res.status(500).json({ success: false, message: error.message });
+ }
+};
+exports.shareProject = shareProject;
diff --git a/dist/controllers/teams/index.js b/dist/controllers/teams/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..12494eb8e655b29e51062c8249cd5a71f68557a4
--- /dev/null
+++ b/dist/controllers/teams/index.js
@@ -0,0 +1,71 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.getTeamProjects = exports.joinTeam = exports.createTeam = void 0;
+const client_1 = __importDefault(require("../../prisma/client"));
+const createTeam = async (req, res, next) => {
+ var _a;
+ const { name, projectId } = req.body;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ try {
+ const team = await client_1.default.team.create({
+ data: {
+ name,
+ projectId,
+ members: {
+ create: {
+ userId,
+ },
+ },
+ },
+ include: {
+ members: true,
+ },
+ });
+ res.status(201).json({ success: true, data: team });
+ }
+ catch (error) {
+ next(error);
+ }
+};
+exports.createTeam = createTeam;
+const joinTeam = async (req, res, next) => {
+ var _a;
+ const { id: teamId } = req.params;
+ const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
+ try {
+ const teamMember = await client_1.default.teamMember.create({
+ data: {
+ teamId,
+ userId,
+ },
+ });
+ res.status(201).json({ success: true, data: teamMember });
+ }
+ catch (error) {
+ next(error);
+ }
+};
+exports.joinTeam = joinTeam;
+const getTeamProjects = async (req, res, next) => {
+ const { id: teamId } = req.params;
+ try {
+ const team = await client_1.default.team.findUnique({
+ where: { id: teamId },
+ include: {
+ project: true,
+ },
+ });
+ if (!team)
+ return res
+ .status(404)
+ .json({ success: false, message: "Team not found" });
+ res.json({ success: true, data: team.project });
+ }
+ catch (error) {
+ next(error);
+ }
+};
+exports.getTeamProjects = getTeamProjects;
diff --git a/dist/index.js b/dist/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..d6f116d5afe6ff3a0570c65cdcdb9a99432347e7
--- /dev/null
+++ b/dist/index.js
@@ -0,0 +1,70 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.appRoute = void 0;
+const dotenv_1 = __importDefault(require("dotenv"));
+const express_1 = __importDefault(require("express"));
+const cors_1 = __importDefault(require("cors"));
+const cookie_parser_1 = __importDefault(require("cookie-parser"));
+exports.appRoute = (0, express_1.default)();
+const auth_1 = __importDefault(require("./routes/auth"));
+const project_1 = __importDefault(require("./routes/project"));
+const user_1 = __importDefault(require("./routes/user"));
+const ai_1 = __importDefault(require("./routes/ai"));
+const code_review_1 = __importDefault(require("./routes/code-review"));
+const community_1 = __importDefault(require("./routes/community"));
+const teams_1 = __importDefault(require("./routes/teams"));
+const gamification_1 = __importDefault(require("./routes/gamification"));
+const paths_1 = __importDefault(require("./routes/paths"));
+const analytics_1 = __importDefault(require("./routes/analytics"));
+const social_1 = __importDefault(require("./routes/social"));
+const notifications_1 = __importDefault(require("./routes/notifications"));
+const events_1 = __importDefault(require("./routes/events"));
+const errorHandler_1 = require("./middlewares/errorHandler");
+const passport_1 = __importDefault(require("./utils/passport"));
+dotenv_1.default.config();
+const allowedOrigins = process.env.CLIENT_URL
+ ? process.env.CLIENT_URL.split(",").map((url) => url.trim())
+ : ["http://localhost:3002"];
+console.log("CORS Allowed Origins:", allowedOrigins);
+exports.appRoute.use((0, cors_1.default)({
+ origin: (origin, callback) => {
+ console.log("Incoming origin:", origin);
+ if (!origin ||
+ allowedOrigins.some((allowed) => origin.startsWith(allowed))) {
+ callback(null, true);
+ }
+ else {
+ console.warn("Blocked by CORS:", origin);
+ callback(new Error("Not allowed by CORS"));
+ }
+ },
+ credentials: true,
+ allowedHeaders: ["Content-Type", "Authorization"],
+ methods: ["GET", "POST", "PUT", "DELETE"],
+ exposedHeaders: ["Content-Disposition"],
+}));
+console.log("ENV:", process.env.CLIENT_URL);
+exports.appRoute.use(express_1.default.json({ limit: "50mb" }));
+exports.appRoute.use(express_1.default.urlencoded({ limit: "50mb", extended: true }));
+exports.appRoute.use((0, cookie_parser_1.default)());
+exports.appRoute.use(passport_1.default.initialize());
+exports.appRoute.get("/", (req, res) => {
+ res.send("Backend is working!");
+});
+exports.appRoute.use("/api/auth", auth_1.default);
+exports.appRoute.use("/api/projects", project_1.default);
+exports.appRoute.use("/api/user", user_1.default);
+exports.appRoute.use("/api/ai", ai_1.default);
+exports.appRoute.use("/api/code-review", code_review_1.default);
+exports.appRoute.use("/api/community", community_1.default);
+exports.appRoute.use("/api/teams", teams_1.default);
+exports.appRoute.use("/api/gamification", gamification_1.default);
+exports.appRoute.use("/api/paths", paths_1.default);
+exports.appRoute.use("/api/analytics", analytics_1.default);
+exports.appRoute.use("/api/social", social_1.default);
+exports.appRoute.use("/api/notifications", notifications_1.default);
+exports.appRoute.use("/api/events", events_1.default);
+exports.appRoute.use(errorHandler_1.errorHandler);
diff --git a/dist/middlewares/auth.js b/dist/middlewares/auth.js
new file mode 100644
index 0000000000000000000000000000000000000000..5ea3b6691a4148cd3fd4936d420008ebfcc0baae
--- /dev/null
+++ b/dist/middlewares/auth.js
@@ -0,0 +1,32 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.checkAuth = exports.authenticateToken = void 0;
+const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
+const authenticateToken = (req, res, next) => {
+ var _a;
+ const token = req.cookies.token || ((_a = req.headers["authorization"]) === null || _a === void 0 ? void 0 : _a.split(" ")[1]);
+ if (!token)
+ res.sendStatus(401);
+ jsonwebtoken_1.default.verify(token, process.env.JWT_SECRET, (err, user) => {
+ if (err)
+ res.sendStatus(403);
+ req.user = user;
+ next();
+ });
+};
+exports.authenticateToken = authenticateToken;
+const checkAuth = async (req) => {
+ var _a;
+ let user = { id: "" };
+ const token = req.cookies.token || ((_a = req.headers["authorization"]) === null || _a === void 0 ? void 0 : _a.split(" ")[1]);
+ if (!token)
+ return null;
+ jsonwebtoken_1.default.verify(token, process.env.JWT_SECRET, (err, decoded) => {
+ user = decoded;
+ });
+ return user;
+};
+exports.checkAuth = checkAuth;
diff --git a/dist/middlewares/email.js b/dist/middlewares/email.js
new file mode 100644
index 0000000000000000000000000000000000000000..d8c2be0f2dda752e7d4d2d19a33633007e28dd43
--- /dev/null
+++ b/dist/middlewares/email.js
@@ -0,0 +1,36 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.sendEmail = void 0;
+const dotenv_1 = __importDefault(require("dotenv"));
+const nodemailer_1 = __importDefault(require("nodemailer"));
+dotenv_1.default.config();
+const sendEmail = async (email, subject, html, text) => {
+ try {
+ const transporter = nodemailer_1.default.createTransport({
+ host: "smtp.gmail.com",
+ secure: true,
+ service: "gmail",
+ auth: {
+ user: process.env.EMAIL_USERNAME,
+ pass: process.env.EMAIL_PASSWORD,
+ },
+ });
+ const options = () => {
+ return {
+ from: `CV Builder <${process.env.EMAIL_USERNAME}>`,
+ to: email,
+ subject,
+ html: html,
+ text: text,
+ };
+ };
+ await transporter.sendMail(options());
+ }
+ catch (error) {
+ return error;
+ }
+};
+exports.sendEmail = sendEmail;
diff --git a/dist/middlewares/errorHandler.js b/dist/middlewares/errorHandler.js
new file mode 100644
index 0000000000000000000000000000000000000000..9c24b501fe5f2614e887d7036b2c9b9e94923b42
--- /dev/null
+++ b/dist/middlewares/errorHandler.js
@@ -0,0 +1,51 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.errorHandler = void 0;
+const client_1 = require("@prisma/client");
+const errorHandler = (err, req, res, next) => {
+ var _a, _b;
+ console.error("Error:", err);
+ let status = 500;
+ let message = "Something went wrong. Please try again.";
+ let errors = undefined;
+ // Prisma Error Handling
+ if (err instanceof client_1.Prisma.PrismaClientKnownRequestError) {
+ switch (err.code) {
+ case "P2002": // Unique constraint violation
+ status = 409;
+ const target = ((_a = err.meta) === null || _a === void 0 ? void 0 : _a.target) || [];
+ message = `${target.join(", ")} already exists.`;
+ break;
+ case "P2025": // Record not found
+ status = 404;
+ message = ((_b = err.meta) === null || _b === void 0 ? void 0 : _b.cause) || "Record not found.";
+ break;
+ case "P2003": // Foreign key constraint violation
+ status = 400;
+ message = "Foreign key constraint failed.";
+ break;
+ default:
+ status = 400;
+ message = `Database error: ${err.message}`;
+ }
+ }
+ else if (err instanceof client_1.Prisma.PrismaClientValidationError) {
+ status = 400;
+ message = "Validation error in database request.";
+ }
+ else if (err.name === "ValidationError") {
+ status = 400;
+ message = err.message;
+ }
+ else if (err.status) {
+ status = err.status;
+ message = err.message;
+ }
+ res.status(status).json({
+ success: false,
+ message,
+ errors,
+ stack: process.env.NODE_ENV === "development" ? err.stack : undefined,
+ });
+};
+exports.errorHandler = errorHandler;
diff --git a/dist/middlewares/file.js b/dist/middlewares/file.js
new file mode 100644
index 0000000000000000000000000000000000000000..0212a56ce594886c7935bbcf8552cd6ce85f7d89
--- /dev/null
+++ b/dist/middlewares/file.js
@@ -0,0 +1,32 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.upLoadFiles = exports.mapFiles = void 0;
+const dotenv = require("dotenv");
+dotenv.config();
+const cloudinary = require("cloudinary").v2;
+cloudinary.config({
+ cloud_name: process.env.CLOUDINARY_NAME,
+ api_key: process.env.CLOUDINARY_KEY,
+ api_secret: process.env.CLOUDINARY_SECRET,
+});
+const mapFiles = async (files) => {
+ let fls = [];
+ if (files && (files === null || files === void 0 ? void 0 : files.length) > 0) {
+ for await (let file of files) {
+ fls.push({
+ name: file === null || file === void 0 ? void 0 : file.name,
+ type: file === null || file === void 0 ? void 0 : file.type,
+ uri: (file === null || file === void 0 ? void 0 : file.uri.includes("res.cloudinary.com"))
+ ? file === null || file === void 0 ? void 0 : file.uri
+ : await (0, exports.upLoadFiles)(file === null || file === void 0 ? void 0 : file.uri, file === null || file === void 0 ? void 0 : file.name),
+ });
+ }
+ }
+ return fls;
+};
+exports.mapFiles = mapFiles;
+const upLoadFiles = async (file, fileName) => {
+ const uri = await cloudinary.uploader.upload(file, { public_id: fileName });
+ return uri === null || uri === void 0 ? void 0 : uri.secure_url;
+};
+exports.upLoadFiles = upLoadFiles;
diff --git a/dist/middlewares/generateOTP.js b/dist/middlewares/generateOTP.js
new file mode 100644
index 0000000000000000000000000000000000000000..8de3e40dff96c8b5c30fa115764b579db81760bf
--- /dev/null
+++ b/dist/middlewares/generateOTP.js
@@ -0,0 +1,13 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.generateOTP = void 0;
+const generateOTP = () => {
+ let otp;
+ const otpDate = Date.now();
+ const numbers = 123456;
+ // const length: number = Math.floor(Math.random() * 2)
+ const randomNumbers = Math.floor(numbers + Math.random() * 100000);
+ otp = randomNumbers;
+ return { otp, otpDate };
+};
+exports.generateOTP = generateOTP;
diff --git a/dist/models/auth/index.js b/dist/models/auth/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..984bca91ca6cfd34b666496e43ff433bf843380c
--- /dev/null
+++ b/dist/models/auth/index.js
@@ -0,0 +1,26 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+const mongoose_1 = __importDefault(require("mongoose"));
+const { isEmail } = require("validator");
+const Schema = mongoose_1.default.Schema;
+const auth = new Schema({
+ firstName: { type: String, required: true, min: 3 },
+ lastName: { type: String, required: true, min: 3 },
+ email: {
+ type: String,
+ required: true,
+ unique: true,
+ lowercase: true,
+ validate: isEmail,
+ },
+ password: { type: String, required: true, min: 6 },
+ manageOTP: {
+ otp: { type: Number },
+ otpDate: { type: Number },
+ },
+}, { timestamps: true });
+const userAuth = mongoose_1.default.model("user", auth);
+exports.default = userAuth;
diff --git a/dist/models/project/index.js b/dist/models/project/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..b90ff406ea1c4371173141b1402fd2f898652913
--- /dev/null
+++ b/dist/models/project/index.js
@@ -0,0 +1,21 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.Project = void 0;
+const mongoose_1 = require("mongoose");
+const resourceSchema = new mongoose_1.Schema({
+ type: { type: String, enum: ["video", "article", "course"], required: true },
+ url: { type: String, required: true },
+ title: { type: String, required: true },
+});
+const projectSchema = new mongoose_1.Schema({
+ title: { type: String, required: true },
+ difficulty: {
+ type: String,
+ enum: ["beginner", "intermediate", "advanced"],
+ required: true,
+ },
+ description: { type: String, required: true },
+ requirements: [{ type: String }],
+ resources: [resourceSchema],
+}, { timestamps: true });
+exports.Project = (0, mongoose_1.model)("Project", projectSchema);
diff --git a/dist/prisma.config.js b/dist/prisma.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..27a342bac802c2b1c541e35b354454e0bf4e7d09
--- /dev/null
+++ b/dist/prisma.config.js
@@ -0,0 +1,16 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+// This file was generated by Prisma and assumes you have installed the following:
+// npm install --save-dev prisma dotenv
+require("dotenv/config");
+const config_1 = require("prisma/config");
+exports.default = (0, config_1.defineConfig)({
+ schema: "prisma/schema.prisma",
+ migrations: {
+ path: "prisma/migrations",
+ },
+ engine: "classic",
+ datasource: {
+ url: (0, config_1.env)("DATABASE_URL"),
+ },
+});
diff --git a/dist/prisma/client.js b/dist/prisma/client.js
new file mode 100644
index 0000000000000000000000000000000000000000..a3a357632447eebee58f9dc8c7d79a92a6e23b1f
--- /dev/null
+++ b/dist/prisma/client.js
@@ -0,0 +1,5 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const client_1 = require("@prisma/client");
+const prisma = new client_1.PrismaClient();
+exports.default = prisma;
diff --git a/dist/prisma/migrations/generate-slugs.js b/dist/prisma/migrations/generate-slugs.js
new file mode 100644
index 0000000000000000000000000000000000000000..da581bc04f964e36cfb5dbdaebb5dc794bf7a691
--- /dev/null
+++ b/dist/prisma/migrations/generate-slugs.js
@@ -0,0 +1,34 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const client_1 = require("@prisma/client");
+const prisma = new client_1.PrismaClient();
+function generateSlug(title) {
+ return title
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "-")
+ .replace(/^-+|-+$/g, "");
+}
+async function generateSlugs() {
+ const projects = await prisma.project.findMany({
+ where: { slug: null },
+ });
+ console.log(`Generating slugs for ${projects.length} projects...`);
+ for (const project of projects) {
+ let slug = generateSlug(project.title);
+ let counter = 1;
+ // Handle duplicates
+ while (await prisma.project.findUnique({ where: { slug } })) {
+ slug = `${generateSlug(project.title)}-${counter}`;
+ counter++;
+ }
+ await prisma.project.update({
+ where: { id: project.id },
+ data: { slug },
+ });
+ console.log(`✓ ${project.title} -> ${slug}`);
+ }
+ console.log("Done!");
+}
+generateSlugs()
+ .catch(console.error)
+ .finally(() => prisma.$disconnect());
diff --git a/dist/prisma/seed.js b/dist/prisma/seed.js
new file mode 100644
index 0000000000000000000000000000000000000000..0e4ba249ce23b001fbbf532fe2f3591d428797e8
--- /dev/null
+++ b/dist/prisma/seed.js
@@ -0,0 +1,303 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const client_1 = require("@prisma/client");
+const prisma = new client_1.PrismaClient();
+function generateSlug(title) {
+ return title
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "-")
+ .replace(/^-+|-+$/g, "");
+}
+const projects = [
+ {
+ title: "Landing Page Template",
+ slug: "landing-page-template",
+ difficultyLevel: client_1.Difficulty.BEGINNER,
+ description: "Build a responsive landing page with modern design principles",
+ technologies: ["HTML", "CSS"],
+ categories: ["Frontend", "Web Design"],
+ estimatedTime: "4-6 hours",
+ learningObjectives: [
+ "Responsive design",
+ "CSS Flexbox/Grid",
+ "Modern UI patterns",
+ ],
+ resourceLinks: [
+ { type: "video", url: "#", title: "HTML & CSS Crash Course" },
+ ],
+ featured: true,
+ },
+ {
+ title: "E-Commerce Dashboard",
+ slug: "ecommerce-dashboard",
+ difficultyLevel: client_1.Difficulty.INTERMEDIATE,
+ description: "Create dynamic admin dashboard with React and Chart.js",
+ technologies: ["React", "Chart.js"],
+ categories: ["Frontend", "Data Visualization"],
+ estimatedTime: "12-16 hours",
+ learningObjectives: [
+ "React state management",
+ "Data visualization",
+ "Dashboard UX",
+ ],
+ resourceLinks: [
+ { type: "article", url: "#", title: "React State Management Guide" },
+ ],
+ featured: true,
+ },
+ {
+ title: "E-Commerce REST API",
+ slug: "ecommerce-rest-api",
+ difficultyLevel: client_1.Difficulty.INTERMEDIATE,
+ description: "Develop a Node.js API with Express and MongoDB for product management",
+ technologies: ["Node.js", "Express", "MongoDB"],
+ categories: ["Backend", "API Development"],
+ estimatedTime: "16-20 hours",
+ learningObjectives: [
+ "RESTful API design",
+ "Database modeling",
+ "Authentication",
+ ],
+ resourceLinks: [
+ { type: "course", url: "#", title: "Node.js Fundamentals" },
+ ],
+ featured: false,
+ },
+ {
+ title: "Social Media Dashboard",
+ slug: "social-media-dashboard",
+ difficultyLevel: client_1.Difficulty.ADVANCED,
+ description: "Build real-time dashboard with React, GraphQL and WebSockets",
+ technologies: ["React", "GraphQL", "WebSockets"],
+ categories: ["Full-stack", "Real-time"],
+ estimatedTime: "24-32 hours",
+ learningObjectives: [
+ "GraphQL subscriptions",
+ "Real-time data",
+ "Advanced state management",
+ ],
+ resourceLinks: [
+ { type: "video", url: "#", title: "GraphQL Subscriptions" },
+ ],
+ featured: true,
+ },
+ {
+ title: "Portfolio Website",
+ slug: "portfolio-website",
+ difficultyLevel: client_1.Difficulty.BEGINNER,
+ description: "Create a personal portfolio with Next.js and Tailwind CSS",
+ technologies: ["Next.js", "Tailwind CSS"],
+ categories: ["Frontend", "Web Design"],
+ estimatedTime: "8-10 hours",
+ learningObjectives: [
+ "Next.js basics",
+ "Tailwind CSS",
+ "Portfolio best practices",
+ ],
+ resourceLinks: [
+ { type: "article", url: "#", title: "Next.js Deployment Guide" },
+ ],
+ featured: false,
+ },
+ {
+ title: "React Todo App with Firebase",
+ slug: "react-todo-firebase",
+ difficultyLevel: client_1.Difficulty.INTERMEDIATE,
+ description: "Create a collaborative todo application with real-time sync",
+ technologies: ["React", "Firebase"],
+ categories: ["Frontend", "Real-time"],
+ estimatedTime: "10-14 hours",
+ learningObjectives: [
+ "Firebase integration",
+ "Real-time sync",
+ "CRUD operations",
+ ],
+ resourceLinks: [
+ { type: "article", url: "#", title: "Firebase Realtime Database Guide" },
+ ],
+ featured: false,
+ },
+ {
+ title: "Authentication System",
+ slug: "authentication-system",
+ difficultyLevel: client_1.Difficulty.ADVANCED,
+ description: "Implement OAuth 2.0 flow with Node.js and Passport.js",
+ technologies: ["Node.js", "Passport.js"],
+ categories: ["Backend", "Security"],
+ estimatedTime: "20-24 hours",
+ learningObjectives: ["OAuth 2.0", "JWT tokens", "Security best practices"],
+ resourceLinks: [
+ { type: "course", url: "#", title: "Web Security Fundamentals" },
+ ],
+ featured: false,
+ },
+ {
+ title: "Mobile Recipe App",
+ slug: "mobile-recipe-app",
+ difficultyLevel: client_1.Difficulty.INTERMEDIATE,
+ description: "Build cross-platform recipe manager with React Native",
+ technologies: ["React Native"],
+ categories: ["Mobile", "Frontend"],
+ estimatedTime: "18-22 hours",
+ learningObjectives: [
+ "React Native basics",
+ "Mobile navigation",
+ "API integration",
+ ],
+ resourceLinks: [
+ { type: "video", url: "#", title: "React Native Navigation Tutorial" },
+ ],
+ featured: false,
+ },
+ {
+ title: "CMS Platform",
+ slug: "cms-platform",
+ difficultyLevel: client_1.Difficulty.ADVANCED,
+ description: "Headless CMS with Next.js and Sanity.io",
+ technologies: ["Next.js", "Sanity.io"],
+ categories: ["Full-stack", "CMS"],
+ estimatedTime: "28-36 hours",
+ learningObjectives: ["Headless CMS", "Content modeling", "Next.js ISR"],
+ resourceLinks: [
+ { type: "article", url: "#", title: "Sanity Studio Customization" },
+ ],
+ featured: true,
+ },
+ {
+ title: "AI Chat Interface",
+ slug: "ai-chat-interface",
+ difficultyLevel: client_1.Difficulty.ADVANCED,
+ description: "Build GPT-4 chatbot with streaming responses",
+ technologies: ["GPT-4", "Streaming"],
+ categories: ["AI/ML", "Full-stack"],
+ estimatedTime: "24-30 hours",
+ learningObjectives: ["LLM integration", "Streaming responses", "Chat UX"],
+ resourceLinks: [
+ { type: "course", url: "#", title: "LLM Integration Patterns" },
+ ],
+ featured: true,
+ },
+ {
+ title: "Real-time Chat Application",
+ slug: "realtime-chat-app",
+ difficultyLevel: client_1.Difficulty.INTERMEDIATE,
+ description: "Build chat app with Socket.io and React",
+ technologies: ["Socket.io", "React"],
+ categories: ["Full-stack", "Real-time"],
+ estimatedTime: "14-18 hours",
+ learningObjectives: [
+ "WebSocket protocol",
+ "Real-time messaging",
+ "Chat features",
+ ],
+ resourceLinks: [
+ { type: "video", url: "#", title: "WebSocket Fundamentals" },
+ ],
+ featured: false,
+ },
+ {
+ title: "Serverless API Gateway",
+ slug: "serverless-api-gateway",
+ difficultyLevel: client_1.Difficulty.ADVANCED,
+ description: "Create REST API using AWS Lambda & API Gateway",
+ technologies: ["AWS Lambda", "API Gateway"],
+ categories: ["Backend", "Cloud"],
+ estimatedTime: "20-26 hours",
+ learningObjectives: [
+ "Serverless architecture",
+ "AWS services",
+ "API design",
+ ],
+ resourceLinks: [
+ { type: "article", url: "#", title: "AWS Serverless Patterns" },
+ ],
+ featured: false,
+ },
+ {
+ title: "E-commerce Product Search",
+ slug: "ecommerce-product-search",
+ difficultyLevel: client_1.Difficulty.ADVANCED,
+ description: "Implement search functionality with Algolia",
+ technologies: ["Algolia"],
+ categories: ["Frontend", "Search"],
+ estimatedTime: "16-20 hours",
+ learningObjectives: [
+ "Search algorithms",
+ "Algolia integration",
+ "Search UX",
+ ],
+ resourceLinks: [
+ { type: "course", url: "#", title: "Search Engine Optimization" },
+ ],
+ featured: false,
+ },
+ {
+ title: "Mobile Payment Integration",
+ slug: "mobile-payment-integration",
+ difficultyLevel: client_1.Difficulty.ADVANCED,
+ description: "Add Stripe payments to React Native app",
+ technologies: ["React Native", "Stripe"],
+ categories: ["Mobile", "Payments"],
+ estimatedTime: "18-24 hours",
+ learningObjectives: ["Payment processing", "Stripe API", "Mobile security"],
+ resourceLinks: [
+ { type: "video", url: "#", title: "Mobile Payment Systems" },
+ ],
+ featured: false,
+ },
+ {
+ title: "Data Visualization Dashboard",
+ slug: "data-visualization-dashboard",
+ difficultyLevel: client_1.Difficulty.INTERMEDIATE,
+ description: "Create analytics dashboard with D3.js",
+ technologies: ["D3.js"],
+ categories: ["Frontend", "Data Visualization"],
+ estimatedTime: "14-18 hours",
+ learningObjectives: [
+ "D3.js fundamentals",
+ "Data visualization",
+ "Interactive charts",
+ ],
+ resourceLinks: [{ type: "article", url: "#", title: "D3.js Essentials" }],
+ featured: false,
+ },
+];
+async function main() {
+ console.log("Start seeding...");
+ // Create a default user if not exists
+ const user = await prisma.user.upsert({
+ where: { email: "admin@devresource.com" },
+ update: {},
+ create: {
+ email: "admin@devresource.com",
+ firstName: "Admin",
+ lastName: "User",
+ password: "password123", // In a real app, this should be hashed
+ role: "ADMIN",
+ },
+ });
+ console.log(`Created user with id: ${user.id}`);
+ for (const project of projects) {
+ const createdProject = await prisma.project.upsert({
+ where: { slug: project.slug },
+ update: {
+ ...project,
+ createdById: user.id,
+ },
+ create: {
+ ...project,
+ createdById: user.id,
+ },
+ });
+ console.log(`Created project with id: ${createdProject.id}`);
+ }
+ console.log("Seeding finished.");
+}
+main()
+ .catch((e) => {
+ console.error(e);
+ process.exit(1);
+})
+ .finally(async () => {
+ await prisma.$disconnect();
+});
diff --git a/dist/routes/ai.js b/dist/routes/ai.js
new file mode 100644
index 0000000000000000000000000000000000000000..aeb1936a0492e4866ebe702202e362a19d9e5e6f
--- /dev/null
+++ b/dist/routes/ai.js
@@ -0,0 +1,11 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const express_1 = require("express");
+const ai_1 = require("../controllers/ai");
+const auth_1 = require("../middlewares/auth");
+const router = (0, express_1.Router)();
+router.post("/chat", auth_1.authenticateToken, ai_1.chatWithAI);
+router.get("/chat", auth_1.authenticateToken, ai_1.getAllChatMessages);
+router.get("/chat/history/:projectId", auth_1.authenticateToken, ai_1.getChatHistory);
+router.post("/hint-request", auth_1.authenticateToken, ai_1.requestHint);
+exports.default = router;
diff --git a/dist/routes/analytics.js b/dist/routes/analytics.js
new file mode 100644
index 0000000000000000000000000000000000000000..2e7c1dfe9cfe7ed26dda120cf4bf4882df50fde3
--- /dev/null
+++ b/dist/routes/analytics.js
@@ -0,0 +1,9 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const express_1 = require("express");
+const analytics_1 = require("../controllers/analytics");
+const auth_1 = require("../middlewares/auth");
+const router = (0, express_1.Router)();
+router.get("/user", auth_1.authenticateToken, analytics_1.getUserAnalytics);
+router.get("/admin", auth_1.authenticateToken, analytics_1.getAdminAnalytics);
+exports.default = router;
diff --git a/dist/routes/auth.js b/dist/routes/auth.js
new file mode 100644
index 0000000000000000000000000000000000000000..f56e0df4918e1d4c6f4d128741c11cc648c94646
--- /dev/null
+++ b/dist/routes/auth.js
@@ -0,0 +1,30 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+const express_1 = __importDefault(require("express"));
+const auth_1 = require("../controllers/auth");
+const auth_2 = require("../middlewares/auth");
+const passport_1 = __importDefault(require("../utils/passport"));
+const router = express_1.default.Router();
+router.post("/register", auth_1.signUp);
+router.post("/login", auth_1.login);
+router.post("/logout", auth_1.logout);
+router.get("/me", auth_2.authenticateToken, auth_1.me);
+router.post("/update/:id", auth_2.authenticateToken, auth_1.updateUser);
+router.get("/user/:id", auth_2.authenticateToken, auth_1.getUser);
+router.post("/forgetPassword", auth_1.forgetPassword);
+router.post("/verifyOtp", auth_1.verifyOTP);
+router.post("/resetPassword", auth_1.resetPassword);
+router.get("/google", passport_1.default.authenticate("google", { scope: ["profile", "email"] }));
+router.get("/google/callback", passport_1.default.authenticate("google", {
+ session: false,
+ failureRedirect: `${process.env.CLIENT_URL}/auth/signin?error=auth_failed`,
+}), auth_1.googleAuthCallback);
+router.get("/github", passport_1.default.authenticate("github", { scope: ["user:email"] }));
+router.get("/github/callback", passport_1.default.authenticate("github", {
+ session: false,
+ failureRedirect: `${process.env.CLIENT_URL}/auth/signin?error=auth_failed`,
+}), auth_1.githubAuthCallback);
+exports.default = router;
diff --git a/dist/routes/code-review.js b/dist/routes/code-review.js
new file mode 100644
index 0000000000000000000000000000000000000000..fabfe9bcc55023e1cdd5f02801492e7650fa7f38
--- /dev/null
+++ b/dist/routes/code-review.js
@@ -0,0 +1,9 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const express_1 = require("express");
+const code_review_1 = require("../controllers/code-review");
+const auth_1 = require("../middlewares/auth");
+const router = (0, express_1.Router)();
+router.post("/analyze", auth_1.authenticateToken, code_review_1.analyzeCode);
+router.get("/:id", auth_1.authenticateToken, code_review_1.getSubmissionReview);
+exports.default = router;
diff --git a/dist/routes/community.js b/dist/routes/community.js
new file mode 100644
index 0000000000000000000000000000000000000000..e28d9a9ede58c4d409f369570614af9c3388aa5e
--- /dev/null
+++ b/dist/routes/community.js
@@ -0,0 +1,12 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const express_1 = require("express");
+const community_1 = require("../controllers/community");
+const auth_1 = require("../middlewares/auth");
+const router = (0, express_1.Router)();
+router.get("/submissions/public", community_1.getPublicSubmissions);
+router.post("/submissions/:id/comment", auth_1.authenticateToken, community_1.commentOnSubmission);
+router.post("/submissions/:id/vote", auth_1.authenticateToken, community_1.voteOnSubmission);
+router.get("/forum/threads", community_1.getForumThreads);
+router.post("/forum/threads", auth_1.authenticateToken, community_1.createForumThread);
+exports.default = router;
diff --git a/dist/routes/events.js b/dist/routes/events.js
new file mode 100644
index 0000000000000000000000000000000000000000..0b416aa8644c57824222a4ab5239d363a67b84dd
--- /dev/null
+++ b/dist/routes/events.js
@@ -0,0 +1,17 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+const express_1 = __importDefault(require("express"));
+const events_1 = require("../controllers/events");
+const auth_1 = require("../middlewares/auth");
+const router = express_1.default.Router();
+router.post("/", auth_1.authenticateToken, events_1.createEvent);
+router.get("/", events_1.getEvents);
+router.get("/:id", events_1.getEvent);
+router.post("/:eventId/join", auth_1.authenticateToken, events_1.joinEvent);
+router.delete("/:eventId/leave", auth_1.authenticateToken, events_1.leaveEvent);
+router.patch("/:eventId/score/:userId", auth_1.authenticateToken, events_1.updateEventScore);
+router.get("/:eventId/leaderboard", events_1.getLeaderboard);
+exports.default = router;
diff --git a/dist/routes/gamification.js b/dist/routes/gamification.js
new file mode 100644
index 0000000000000000000000000000000000000000..04e298d44642a50d4a02ed12a4a62646d9191f97
--- /dev/null
+++ b/dist/routes/gamification.js
@@ -0,0 +1,9 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const express_1 = require("express");
+const gamification_1 = require("../controllers/gamification");
+const auth_1 = require("../middlewares/auth");
+const router = (0, express_1.Router)();
+router.get("/leaderboard", gamification_1.getLeaderboard);
+router.get("/achievements", auth_1.authenticateToken, gamification_1.getUserAchievements);
+exports.default = router;
diff --git a/dist/routes/notifications.js b/dist/routes/notifications.js
new file mode 100644
index 0000000000000000000000000000000000000000..b8f482429940ddc7f7949873be210f81efe5f6d5
--- /dev/null
+++ b/dist/routes/notifications.js
@@ -0,0 +1,14 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+const express_1 = __importDefault(require("express"));
+const notifications_1 = require("../controllers/notifications");
+const auth_1 = require("../middlewares/auth");
+const router = express_1.default.Router();
+router.get("/", auth_1.authenticateToken, notifications_1.getNotifications);
+router.patch("/:notificationId/read", auth_1.authenticateToken, notifications_1.markAsRead);
+router.patch("/read-all", auth_1.authenticateToken, notifications_1.markAllAsRead);
+router.delete("/:notificationId", auth_1.authenticateToken, notifications_1.deleteNotification);
+exports.default = router;
diff --git a/dist/routes/paths.js b/dist/routes/paths.js
new file mode 100644
index 0000000000000000000000000000000000000000..bd5a2d54b3966b756fb4a549b9cd3e1f6d2dd509
--- /dev/null
+++ b/dist/routes/paths.js
@@ -0,0 +1,10 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const express_1 = require("express");
+const paths_1 = require("../controllers/paths");
+const auth_1 = require("../middlewares/auth");
+const router = (0, express_1.Router)();
+router.get("/", paths_1.getLearningPaths);
+router.get("/:id/progress", auth_1.authenticateToken, paths_1.getPathProgress);
+router.post("/:id/start", auth_1.authenticateToken, paths_1.startPath);
+exports.default = router;
diff --git a/dist/routes/project.js b/dist/routes/project.js
new file mode 100644
index 0000000000000000000000000000000000000000..6a89c5f6310969c48fa2d217b9ed6a056ad12f7d
--- /dev/null
+++ b/dist/routes/project.js
@@ -0,0 +1,22 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const express_1 = require("express");
+const projects_1 = require("../controllers/projects");
+const auth_1 = require("../middlewares/auth");
+const router = (0, express_1.Router)();
+router.get("/", projects_1.getProjects);
+router.get("/progress", auth_1.authenticateToken, projects_1.getUserProgress);
+router.get("/featured", projects_1.getFeaturedProjects);
+router.get("/:id", projects_1.getProjectById);
+router.post("/", auth_1.authenticateToken, projects_1.createProject);
+router.put("/:id", auth_1.authenticateToken, projects_1.updateProject);
+router.delete("/:id", auth_1.authenticateToken, projects_1.deleteProject);
+// User Progress
+router.post("/:id/start", auth_1.authenticateToken, projects_1.startProject);
+router.put("/:id/progress", auth_1.authenticateToken, projects_1.updateProgress);
+router.post("/:id/complete", auth_1.authenticateToken, projects_1.completeProject);
+// Milestones
+router.post("/:id/milestones/:milestoneId/complete", auth_1.authenticateToken, projects_1.completeMilestone);
+// Submissions
+router.post("/:id/submit", auth_1.authenticateToken, projects_1.submitProject);
+exports.default = router;
diff --git a/dist/routes/social.js b/dist/routes/social.js
new file mode 100644
index 0000000000000000000000000000000000000000..6347e49a557fafaeb3aabce2bb72bb3c44df3589
--- /dev/null
+++ b/dist/routes/social.js
@@ -0,0 +1,16 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+const express_1 = __importDefault(require("express"));
+const social_1 = require("../controllers/social");
+const auth_1 = require("../middlewares/auth");
+const router = express_1.default.Router();
+router.post("/follow", auth_1.authenticateToken, social_1.followUser);
+router.delete("/unfollow/:followingId", auth_1.authenticateToken, social_1.unfollowUser);
+router.get("/followers/:userId", auth_1.authenticateToken, social_1.getFollowers);
+router.get("/following/:userId", auth_1.authenticateToken, social_1.getFollowing);
+router.get("/activity-feed", auth_1.authenticateToken, social_1.getActivityFeed);
+router.post("/share", auth_1.authenticateToken, social_1.shareProject);
+exports.default = router;
diff --git a/dist/routes/teams.js b/dist/routes/teams.js
new file mode 100644
index 0000000000000000000000000000000000000000..256add0d9da212467dfe0c5f8874f17dab2da63d
--- /dev/null
+++ b/dist/routes/teams.js
@@ -0,0 +1,10 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const express_1 = require("express");
+const teams_1 = require("../controllers/teams");
+const auth_1 = require("../middlewares/auth");
+const router = (0, express_1.Router)();
+router.post("/create", auth_1.authenticateToken, teams_1.createTeam);
+router.post("/:id/join", auth_1.authenticateToken, teams_1.joinTeam);
+router.get("/:id/projects", auth_1.authenticateToken, teams_1.getTeamProjects);
+exports.default = router;
diff --git a/dist/routes/user.js b/dist/routes/user.js
new file mode 100644
index 0000000000000000000000000000000000000000..bc0ccac76d255e8303e23cc360dd0f71873078bc
--- /dev/null
+++ b/dist/routes/user.js
@@ -0,0 +1,11 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const express_1 = require("express");
+const projects_1 = require("../controllers/projects");
+const auth_1 = require("../middlewares/auth");
+const router = (0, express_1.Router)();
+router.get("/progress", auth_1.authenticateToken, projects_1.getUserProgress);
+router.post("/projects/:id/start", auth_1.authenticateToken, projects_1.startProject);
+router.put("/projects/:id/progress", auth_1.authenticateToken, projects_1.updateProgress);
+router.post("/projects/:id/complete", auth_1.authenticateToken, projects_1.completeProject);
+exports.default = router;
diff --git a/dist/server.js b/dist/server.js
new file mode 100644
index 0000000000000000000000000000000000000000..989359263544cf24810ee5b13dd4b77dbb9f9ab7
--- /dev/null
+++ b/dist/server.js
@@ -0,0 +1,19 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.io = void 0;
+const dotenv_1 = __importDefault(require("dotenv"));
+const index_1 = require("./index");
+const http_1 = require("http");
+const socket_1 = require("./utils/socket");
+dotenv_1.default.config();
+const port = process.env.PORT || 4000;
+const httpServer = (0, http_1.createServer)(index_1.appRoute);
+const io = (0, socket_1.initSocket)(httpServer);
+exports.io = io;
+httpServer.listen(port, () => {
+ console.log(`Server is listening on ${port}`);
+});
+exports.default = httpServer;
diff --git a/dist/types/project.js b/dist/types/project.js
new file mode 100644
index 0000000000000000000000000000000000000000..c8ad2e549bdc6801e0d1c80b0308d4b9bd4985ce
--- /dev/null
+++ b/dist/types/project.js
@@ -0,0 +1,2 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
diff --git a/dist/utils/gemini.js b/dist/utils/gemini.js
new file mode 100644
index 0000000000000000000000000000000000000000..619bdd9b8463c8f5ac877c698f9fa386398e2002
--- /dev/null
+++ b/dist/utils/gemini.js
@@ -0,0 +1,15 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.getGeminiResponse = void 0;
+const generative_ai_1 = require("@google/generative-ai");
+const genAI = new generative_ai_1.GoogleGenerativeAI(process.env.GEMINI_API_KEY || "");
+const getGeminiResponse = async (prompt, history = []) => {
+ const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" });
+ const chat = model.startChat({
+ history: history,
+ });
+ const result = await chat.sendMessage(prompt);
+ const response = await result.response;
+ return response.text();
+};
+exports.getGeminiResponse = getGeminiResponse;
diff --git a/dist/utils/passport.js b/dist/utils/passport.js
new file mode 100644
index 0000000000000000000000000000000000000000..14bacc9432678829dee8fcb4860ae9073fa7303a
--- /dev/null
+++ b/dist/utils/passport.js
@@ -0,0 +1,100 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+const passport_1 = __importDefault(require("passport"));
+const passport_google_oauth20_1 = require("passport-google-oauth20");
+const passport_github2_1 = require("passport-github2");
+const client_1 = __importDefault(require("../prisma/client"));
+const dotenv_1 = __importDefault(require("dotenv"));
+dotenv_1.default.config();
+passport_1.default.use(new passport_google_oauth20_1.Strategy({
+ clientID: process.env.GOOGLE_CLIENT_ID,
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
+ callbackURL: `${process.env.BACKEND_URL}/api/auth/google/callback`,
+}, async (accessToken, refreshToken, profile, done) => {
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
+ try {
+ const email = (_b = (_a = profile.emails) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.value;
+ if (!email) {
+ return done(new Error("No email found in Google profile"));
+ }
+ let user = await client_1.default.user.findUnique({
+ where: { googleId: profile.id },
+ });
+ if (!user) {
+ user = await client_1.default.user.upsert({
+ where: { email },
+ update: {
+ googleId: profile.id,
+ firstName: ((_c = profile.name) === null || _c === void 0 ? void 0 : _c.givenName) || profile.displayName || "User",
+ lastName: ((_d = profile.name) === null || _d === void 0 ? void 0 : _d.familyName) || "",
+ avatarUrl: (_f = (_e = profile.photos) === null || _e === void 0 ? void 0 : _e[0]) === null || _f === void 0 ? void 0 : _f.value,
+ },
+ create: {
+ googleId: profile.id,
+ email,
+ firstName: ((_g = profile.name) === null || _g === void 0 ? void 0 : _g.givenName) || profile.displayName || "User",
+ lastName: ((_h = profile.name) === null || _h === void 0 ? void 0 : _h.familyName) || "",
+ avatarUrl: (_k = (_j = profile.photos) === null || _j === void 0 ? void 0 : _j[0]) === null || _k === void 0 ? void 0 : _k.value,
+ },
+ });
+ }
+ return done(null, user);
+ }
+ catch (error) {
+ return done(error);
+ }
+}));
+passport_1.default.use(new passport_github2_1.Strategy({
+ clientID: process.env.GITHUB_CLIENT_ID,
+ clientSecret: process.env.GITHUB_CLIENT_SECRET,
+ callbackURL: `${process.env.BACKEND_URL}/api/auth/github/callback`,
+}, async (accessToken, refreshToken, profile, done) => {
+ var _a, _b, _c, _d, _e, _f;
+ try {
+ const email = (_b = (_a = profile.emails) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.value;
+ if (!email) {
+ return done(new Error("No email found in GitHub profile"));
+ }
+ let user = await client_1.default.user.findUnique({
+ where: { githubId: profile.id },
+ });
+ if (!user) {
+ user = await client_1.default.user.upsert({
+ where: { email },
+ update: {
+ githubId: profile.id,
+ firstName: profile.displayName || profile.username || "User",
+ lastName: "",
+ avatarUrl: (_d = (_c = profile.photos) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.value,
+ },
+ create: {
+ githubId: profile.id,
+ email,
+ firstName: profile.displayName || profile.username || "User",
+ lastName: "",
+ avatarUrl: (_f = (_e = profile.photos) === null || _e === void 0 ? void 0 : _e[0]) === null || _f === void 0 ? void 0 : _f.value,
+ },
+ });
+ }
+ return done(null, user);
+ }
+ catch (error) {
+ return done(error);
+ }
+}));
+passport_1.default.serializeUser((user, done) => {
+ done(null, user.id);
+});
+passport_1.default.deserializeUser(async (id, done) => {
+ try {
+ const user = await client_1.default.user.findUnique({ where: { id } });
+ done(null, user);
+ }
+ catch (error) {
+ done(error);
+ }
+});
+exports.default = passport_1.default;
diff --git a/dist/utils/provider/index.js b/dist/utils/provider/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..61879f961a73a2b46e47d4f34829151580652a64
--- /dev/null
+++ b/dist/utils/provider/index.js
@@ -0,0 +1,31 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.refinePrompt = exports.aiProvider = void 0;
+const openai_1 = require("openai");
+const openRouter = new openai_1.OpenAI({
+ baseURL: "https://openrouter.ai/api/v1",
+ apiKey: process.env.OPENROUTER_API_KEY,
+});
+const client = openRouter;
+const aiProvider = async (messages) => {
+ var _a;
+ const completion = await client.chat.completions.create({
+ model: "meta-llama/llama-4-maverick:free",
+ messages: messages,
+ });
+ return (_a = completion.choices[0].message.content) !== null && _a !== void 0 ? _a : "";
+};
+exports.aiProvider = aiProvider;
+const refinePrompt = async (userInput) => {
+ var _a;
+ const systemPrompt = `You are an expert AI career and project advisor. Refine the user's input to be clear, specific, and optimized for accurate responses about tech career guidance or realistic project ideas. Include relevant keywords (e.g., frontend, backend, React, APIs), clarify user goals or skill level if implied, and remove ambiguity. Return a single, concise prompt in plain text (max 100 words), ready for AI processing.`;
+ const response = await client.chat.completions.create({
+ model: "meta-llama/llama-4-maverick:free",
+ messages: [
+ { role: "system", content: systemPrompt },
+ { role: "user", content: userInput },
+ ],
+ });
+ return (_a = response.choices[0].message.content) !== null && _a !== void 0 ? _a : "";
+};
+exports.refinePrompt = refinePrompt;
diff --git a/dist/utils/provider/models.js b/dist/utils/provider/models.js
new file mode 100644
index 0000000000000000000000000000000000000000..35b8e1c46c7c072cb7f35706b2cdbf3b60075ccf
--- /dev/null
+++ b/dist/utils/provider/models.js
@@ -0,0 +1,71 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.RENDER_MODELS = void 0;
+exports.RENDER_MODELS = {
+ // OpenRouter free models
+ default: {
+ model: "mistralai/mistral-7b-instruct:free",
+ provider: "openrouter",
+ },
+ premium: {
+ model: "cognitivecomputations/dolphin3.0-r1-mistral-24b:free",
+ provider: "openrouter",
+ },
+ mistral: {
+ model: "mistralai/mistral-7b-instruct:free",
+ provider: "openrouter",
+ },
+ grok: {
+ model: "xai/grok-1:free",
+ provider: "openrouter",
+ },
+ cinematika: {
+ model: "openrouter/cinematika-7b:free",
+ provider: "openrouter",
+ },
+ "nous-capybara": {
+ model: "nousresearch/nous-capybara-7b:free",
+ provider: "openrouter",
+ },
+ "command-r": {
+ model: "cohere/command-r:free",
+ provider: "openrouter",
+ },
+ dolphin: {
+ model: "cognitivecomputations/dolphin3.0-r1-mistral-24b:free",
+ provider: "openrouter",
+ },
+ microsoft: {
+ model: "microsoft/phi-2:free",
+ provider: "openrouter",
+ },
+ qwen: {
+ model: "qwen/qwen3-4b:free",
+ provider: "openrouter",
+ },
+ deepseek: {
+ model: "deepseek/deepseek-v3-base:free",
+ provider: "openrouter",
+ },
+ // Note: The following models are not free and have been removed
+ gemini: {
+ model: "google/gemini-2.5-pro-exp-03-25",
+ provider: "openrouter",
+ },
+ amazon: {
+ model: "amazon/nova-micro-v1",
+ provider: "openrouter",
+ },
+ "gpt-4": {
+ model: "openai/gpt-4.1-nano",
+ provider: "openrouter",
+ },
+ llama3: {
+ model: "llama3-70b-8192",
+ provider: "groq",
+ },
+ gemma: {
+ model: "gemma-7b-it",
+ provider: "groq",
+ },
+};
diff --git a/dist/utils/provider/types.js b/dist/utils/provider/types.js
new file mode 100644
index 0000000000000000000000000000000000000000..c8ad2e549bdc6801e0d1c80b0308d4b9bd4985ce
--- /dev/null
+++ b/dist/utils/provider/types.js
@@ -0,0 +1,2 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
diff --git a/dist/utils/redis.js b/dist/utils/redis.js
new file mode 100644
index 0000000000000000000000000000000000000000..88bcf0fbbd609f2b42f1646b4d1990dcf2da6eca
--- /dev/null
+++ b/dist/utils/redis.js
@@ -0,0 +1,14 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+const ioredis_1 = __importDefault(require("ioredis"));
+const redis = new ioredis_1.default(process.env.REDIS_URL || "redis://localhost:6379");
+redis.on("error", (err) => {
+ console.error("Redis error:", err);
+});
+redis.on("connect", () => {
+ console.log("Connected to Redis");
+});
+exports.default = redis;
diff --git a/dist/utils/socket.js b/dist/utils/socket.js
new file mode 100644
index 0000000000000000000000000000000000000000..8a0678855213223967b3629981bd4a2f2ce48196
--- /dev/null
+++ b/dist/utils/socket.js
@@ -0,0 +1,51 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.initSocket = void 0;
+const socket_io_1 = require("socket.io");
+const initSocket = (server) => {
+ const io = new socket_io_1.Server(server, {
+ cors: {
+ origin: process.env.CLIENT_URL
+ ? process.env.CLIENT_URL.split(",")
+ : ["http://localhost:3002"],
+ methods: ["GET", "POST"],
+ credentials: true,
+ },
+ });
+ io.on("connection", (socket) => {
+ console.log("A user connected:", socket.id);
+ // Join a collaboration room for a specific project
+ socket.on("join-project", (projectId) => {
+ socket.join(`project:${projectId}`);
+ console.log(`User ${socket.id} joined project ${projectId}`);
+ });
+ // Leave project room
+ socket.on("leave-project", (projectId) => {
+ socket.leave(`project:${projectId}`);
+ console.log(`User ${socket.id} left project ${projectId}`);
+ });
+ // Handle code changes (if not using Yjs directly via y-websocket server)
+ socket.on("code-change", (data) => {
+ socket.to(`project:${data.projectId}`).emit("code-update", data.change);
+ });
+ // Real-time cursor/presence
+ socket.on("cursor-move", (data) => {
+ socket.to(`data.projectId`).emit("cursor-update", {
+ userId: socket.id,
+ ...data,
+ });
+ });
+ // Video Chat Signaling
+ socket.on("video-signal", (data) => {
+ io.to(data.target).emit("video-signal", {
+ sender: socket.id,
+ signal: data.signal,
+ });
+ });
+ socket.on("disconnect", () => {
+ console.log("User disconnected:", socket.id);
+ });
+ });
+ return io;
+};
+exports.initSocket = initSocket;
diff --git a/middlewares/auth.ts b/middlewares/auth.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2ba566773eafbbe6a30dd1e2f4368a013a0914ae
--- /dev/null
+++ b/middlewares/auth.ts
@@ -0,0 +1,33 @@
+import jwt from "jsonwebtoken";
+import { Request, Response, NextFunction } from "express";
+
+interface JwtPayload {
+ id: string;
+}
+
+export const authenticateToken = (
+ req: Request,
+ res: Response,
+ next: NextFunction
+) => {
+ const token =
+ req.cookies.token || req.headers["authorization"]?.split(" ")[1];
+ if (!token) res.sendStatus(401);
+
+ jwt.verify(token as string, process.env.JWT_SECRET!, (err, user) => {
+ if (err) res.sendStatus(403);
+ (req as any).user = user as JwtPayload;
+ next();
+ });
+};
+
+export const checkAuth = async (req: any) => {
+ let user: JwtPayload = { id: "" };
+ const token =
+ req.cookies.token || req.headers["authorization"]?.split(" ")[1];
+ if (!token) return null;
+ jwt.verify(token as string, process.env.JWT_SECRET!, (err, decoded) => {
+ user = decoded as JwtPayload;
+ });
+ return user;
+};
diff --git a/middlewares/email.ts b/middlewares/email.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6e96463013f063e321ccf226a06dd8f3c7d2ea25
--- /dev/null
+++ b/middlewares/email.ts
@@ -0,0 +1,36 @@
+import dotenv from "dotenv";
+import nodemailer from "nodemailer";
+dotenv.config();
+
+export const sendEmail = async (
+ email: any,
+ subject: any,
+ html: any,
+ text: any
+) => {
+ try {
+ const transporter = nodemailer.createTransport({
+ host: "smtp.gmail.com",
+ secure: true,
+ service: "gmail",
+ auth: {
+ user: process.env.EMAIL_USERNAME,
+ pass: process.env.EMAIL_PASSWORD,
+ },
+ });
+
+ const options = () => {
+ return {
+ from: `CV Builder <${process.env.EMAIL_USERNAME}>`,
+ to: email,
+ subject,
+ html: html,
+ text: text,
+ };
+ };
+
+ await transporter.sendMail(options());
+ } catch (error) {
+ return error;
+ }
+};
diff --git a/middlewares/errorHandler.ts b/middlewares/errorHandler.ts
new file mode 100644
index 0000000000000000000000000000000000000000..339d094e13de29ab0d0edfa3efd7fcd5295a3c34
--- /dev/null
+++ b/middlewares/errorHandler.ts
@@ -0,0 +1,53 @@
+import { Request, Response, NextFunction } from "express";
+import { Prisma } from "@prisma/client";
+
+export const errorHandler = (
+ err: any,
+ req: Request,
+ res: Response,
+ next: NextFunction
+) => {
+ console.error("Error:", err);
+
+ let status = 500;
+ let message = "Something went wrong. Please try again.";
+ let errors: any = undefined;
+
+ // Prisma Error Handling
+ if (err instanceof Prisma.PrismaClientKnownRequestError) {
+ switch (err.code) {
+ case "P2002": // Unique constraint violation
+ status = 409;
+ const target = (err.meta?.target as string[]) || [];
+ message = `${target.join(", ")} already exists.`;
+ break;
+ case "P2025": // Record not found
+ status = 404;
+ message = err.meta?.cause as string || "Record not found.";
+ break;
+ case "P2003": // Foreign key constraint violation
+ status = 400;
+ message = "Foreign key constraint failed.";
+ break;
+ default:
+ status = 400;
+ message = `Database error: ${err.message}`;
+ }
+ } else if (err instanceof Prisma.PrismaClientValidationError) {
+ status = 400;
+ message = "Validation error in database request.";
+ } else if (err.name === "ValidationError") {
+ status = 400;
+ message = err.message;
+ } else if (err.status) {
+ status = err.status;
+ message = err.message;
+ }
+
+ res.status(status).json({
+ success: false,
+ message,
+ errors,
+ stack: process.env.NODE_ENV === "development" ? err.stack : undefined,
+ });
+};
diff --git a/middlewares/file.ts b/middlewares/file.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b042a1275f5f2113d452eea7bb7506ddb1681e3f
--- /dev/null
+++ b/middlewares/file.ts
@@ -0,0 +1,57 @@
+const dotenv = require("dotenv");
+dotenv.config();
+const cloudinary = require("cloudinary").v2;
+const multer = require("multer");
+
+cloudinary.config({
+ cloud_name: process.env.CLOUDINARY_NAME,
+ api_key: process.env.CLOUDINARY_KEY,
+ api_secret: process.env.CLOUDINARY_SECRET,
+});
+
+// Multer storage configuration
+const storage = multer.memoryStorage();
+export const upload = multer({ storage });
+
+export interface IFile {
+ uri: string;
+ name: string;
+ type: string;
+}
+
+export const mapFiles = async (files: any) => {
+ let fls: IFile[] = [];
+
+ if (files && files?.length > 0) {
+ for await (let file of files) {
+ fls.push({
+ name: file?.name,
+ type: file?.type,
+ uri: file?.uri.includes("res.cloudinary.com")
+ ? file?.uri
+ : await upLoadFiles(file?.uri, file?.name),
+ });
+ }
+ }
+
+ return fls;
+};
+
+export const upLoadFiles = async (file: any, fileName?: any) => {
+ const uri = await cloudinary.uploader.upload(file, { public_id: fileName });
+
+ return uri?.secure_url as string;
+};
+
+export const uploadBuffer = async (buffer: Buffer, fileName?: string) => {
+ return new Promise((resolve, reject) => {
+ const uploadStream = cloudinary.uploader.upload_stream(
+ { public_id: fileName },
+ (error: any, result: any) => {
+ if (error) return reject(error);
+ resolve(result.secure_url);
+ }
+ );
+ uploadStream.end(buffer);
+ });
+};
diff --git a/middlewares/generateOTP.ts b/middlewares/generateOTP.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ea8407e2402f9897fbbe7ab5815356becde26f11
--- /dev/null
+++ b/middlewares/generateOTP.ts
@@ -0,0 +1,9 @@
+export const generateOTP = () => {
+ let otp;
+ const otpDate = Date.now();
+ const numbers: number = 123456;
+ // const length: number = Math.floor(Math.random() * 2)
+ const randomNumbers: number = Math.floor(numbers + Math.random() * 100000);
+ otp = randomNumbers;
+ return { otp, otpDate };
+};
diff --git a/prisma/client.ts b/prisma/client.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f97faa0a551aa4ddd2b58845941bce57e14653f3
--- /dev/null
+++ b/prisma/client.ts
@@ -0,0 +1,6 @@
+import { PrismaClient } from "@prisma/client";
+import { withAccelerate } from "@prisma/extension-accelerate";
+
+const prisma = new PrismaClient();
+
+export default prisma;
diff --git a/prisma/migrations/20251229161018_final_json_hints/migration.sql b/prisma/migrations/20251229161018_final_json_hints/migration.sql
new file mode 100644
index 0000000000000000000000000000000000000000..f8049f3ce24465b1210b12e40325a01b01507571
--- /dev/null
+++ b/prisma/migrations/20251229161018_final_json_hints/migration.sql
@@ -0,0 +1,643 @@
+-- CreateEnum
+CREATE TYPE "Role" AS ENUM ('STUDENT', 'MENTOR', 'ADMIN', 'CONTRIBUTOR');
+
+-- CreateEnum
+CREATE TYPE "Difficulty" AS ENUM ('BEGINNER', 'INTERMEDIATE', 'ADVANCED');
+
+-- CreateEnum
+CREATE TYPE "DifficultyMode" AS ENUM ('GUIDED', 'STANDARD', 'HARDCORE');
+
+-- CreateEnum
+CREATE TYPE "ProjectStatus" AS ENUM ('NOT_STARTED', 'IN_PROGRESS', 'COMPLETED');
+
+-- CreateEnum
+CREATE TYPE "NotificationType" AS ENUM ('FOLLOW', 'ACHIEVEMENT', 'PROJECT_COMPLETE', 'COMMENT', 'UPVOTE', 'TEAM_INVITE', 'EVENT_REMINDER', 'CHALLENGE_START');
+
+-- CreateEnum
+CREATE TYPE "EventStatus" AS ENUM ('UPCOMING', 'ONGOING', 'COMPLETED');
+
+-- CreateTable
+CREATE TABLE "User" (
+ "id" TEXT NOT NULL,
+ "email" TEXT NOT NULL,
+ "password" TEXT,
+ "firstName" TEXT NOT NULL,
+ "lastName" TEXT NOT NULL,
+ "skillLevel" "Difficulty" NOT NULL DEFAULT 'BEGINNER',
+ "bio" TEXT,
+ "portfolioLinks" TEXT[] DEFAULT ARRAY[]::TEXT[],
+ "xp" INTEGER NOT NULL DEFAULT 0,
+ "streak" INTEGER NOT NULL DEFAULT 0,
+ "role" "Role" NOT NULL DEFAULT 'STUDENT',
+ "avatarUrl" TEXT,
+ "githubId" TEXT,
+ "googleId" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "User_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Project" (
+ "id" TEXT NOT NULL,
+ "title" TEXT NOT NULL,
+ "slug" TEXT NOT NULL,
+ "description" TEXT NOT NULL,
+ "difficultyLevel" "Difficulty" NOT NULL,
+ "technologies" TEXT[],
+ "categories" TEXT[],
+ "estimatedTime" TEXT,
+ "learningObjectives" TEXT[],
+ "resourceLinks" JSONB[] DEFAULT ARRAY[]::JSONB[],
+ "starterRepoUrl" TEXT,
+ "difficultyModes" "DifficultyMode"[] DEFAULT ARRAY['GUIDED', 'STANDARD', 'HARDCORE']::"DifficultyMode"[],
+ "submissionCount" INTEGER NOT NULL DEFAULT 0,
+ "completionRate" DOUBLE PRECISION NOT NULL DEFAULT 0,
+ "featured" BOOLEAN NOT NULL DEFAULT false,
+ "createdById" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "Project_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "UserProject" (
+ "id" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "projectId" TEXT NOT NULL,
+ "status" "ProjectStatus" NOT NULL DEFAULT 'NOT_STARTED',
+ "difficultyModeChosen" "DifficultyMode" NOT NULL,
+ "repoUrl" TEXT,
+ "startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "completedAt" TIMESTAMP(3),
+
+ CONSTRAINT "UserProject_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "ProjectMilestone" (
+ "id" TEXT NOT NULL,
+ "projectId" TEXT NOT NULL,
+ "milestoneNumber" INTEGER NOT NULL,
+ "title" TEXT NOT NULL,
+ "description" TEXT NOT NULL,
+ "hints" JSONB NOT NULL DEFAULT '{}',
+ "validationCriteria" TEXT,
+
+ CONSTRAINT "ProjectMilestone_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "UserMilestone" (
+ "id" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "userProjectId" TEXT,
+ "milestoneId" TEXT NOT NULL,
+ "completedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "UserMilestone_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Submission" (
+ "id" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "projectId" TEXT NOT NULL,
+ "repoUrl" TEXT NOT NULL,
+ "liveUrl" TEXT,
+ "description" TEXT,
+ "feedback" TEXT,
+ "score" INTEGER,
+ "upvotes" INTEGER NOT NULL DEFAULT 0,
+ "isPublic" BOOLEAN NOT NULL DEFAULT false,
+ "aiReviewData" JSONB,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "Submission_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Comment" (
+ "id" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "submissionId" TEXT NOT NULL,
+ "parentId" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "Comment_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Vote" (
+ "id" TEXT NOT NULL,
+ "value" INTEGER NOT NULL,
+ "userId" TEXT NOT NULL,
+ "submissionId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "Vote_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "ForumThread" (
+ "id" TEXT NOT NULL,
+ "title" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "projectId" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "ForumThread_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "ForumPost" (
+ "id" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "threadId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "ForumPost_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Team" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "projectId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "Team_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "TeamMember" (
+ "id" TEXT NOT NULL,
+ "teamId" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "joinedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "TeamMember_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Achievement" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "description" TEXT NOT NULL,
+ "icon" TEXT,
+ "xpReward" INTEGER NOT NULL,
+ "criteria" JSONB NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "Achievement_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "UserAchievement" (
+ "id" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "achievementId" TEXT NOT NULL,
+ "earnedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "UserAchievement_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "LearningPath" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "description" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "LearningPath_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "LearningPathProject" (
+ "id" TEXT NOT NULL,
+ "pathId" TEXT NOT NULL,
+ "projectId" TEXT NOT NULL,
+ "orderIndex" INTEGER NOT NULL,
+ "prerequisites" TEXT[] DEFAULT ARRAY[]::TEXT[],
+
+ CONSTRAINT "LearningPathProject_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "UserPathProgress" (
+ "id" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "pathId" TEXT NOT NULL,
+ "currentProjectIndex" INTEGER NOT NULL DEFAULT 0,
+ "completedAt" TIMESTAMP(3),
+
+ CONSTRAINT "UserPathProgress_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "ChatMessage" (
+ "id" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "projectId" TEXT,
+ "role" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "ChatMessage_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Follow" (
+ "id" TEXT NOT NULL,
+ "followerId" TEXT NOT NULL,
+ "followingId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "Follow_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Notification" (
+ "id" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "type" "NotificationType" NOT NULL,
+ "title" TEXT NOT NULL,
+ "message" TEXT NOT NULL,
+ "link" TEXT,
+ "read" BOOLEAN NOT NULL DEFAULT false,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "Notification_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Event" (
+ "id" TEXT NOT NULL,
+ "title" TEXT NOT NULL,
+ "description" TEXT NOT NULL,
+ "type" TEXT NOT NULL,
+ "startDate" TIMESTAMP(3) NOT NULL,
+ "endDate" TIMESTAMP(3) NOT NULL,
+ "status" "EventStatus" NOT NULL DEFAULT 'UPCOMING',
+ "prizePool" TEXT,
+ "maxParticipants" INTEGER,
+ "bannerUrl" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "Event_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "EventParticipant" (
+ "id" TEXT NOT NULL,
+ "eventId" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "joinedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "EventParticipant_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "EventLeaderboard" (
+ "id" TEXT NOT NULL,
+ "eventId" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "score" INTEGER NOT NULL DEFAULT 0,
+ "rank" INTEGER,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "EventLeaderboard_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "SocialShare" (
+ "id" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "projectId" TEXT NOT NULL,
+ "platform" TEXT NOT NULL,
+ "shareUrl" TEXT NOT NULL,
+ "ogImageUrl" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "SocialShare_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_githubId_key" ON "User"("githubId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_googleId_key" ON "User"("googleId");
+
+-- CreateIndex
+CREATE INDEX "User_email_idx" ON "User"("email");
+
+-- CreateIndex
+CREATE INDEX "User_role_idx" ON "User"("role");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Project_slug_key" ON "Project"("slug");
+
+-- CreateIndex
+CREATE INDEX "Project_difficultyLevel_idx" ON "Project"("difficultyLevel");
+
+-- CreateIndex
+CREATE INDEX "Project_featured_idx" ON "Project"("featured");
+
+-- CreateIndex
+CREATE INDEX "Project_slug_idx" ON "Project"("slug");
+
+-- CreateIndex
+CREATE INDEX "Project_createdAt_idx" ON "Project"("createdAt");
+
+-- CreateIndex
+CREATE INDEX "UserProject_userId_idx" ON "UserProject"("userId");
+
+-- CreateIndex
+CREATE INDEX "UserProject_projectId_idx" ON "UserProject"("projectId");
+
+-- CreateIndex
+CREATE INDEX "UserProject_status_idx" ON "UserProject"("status");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "UserProject_userId_projectId_difficultyModeChosen_key" ON "UserProject"("userId", "projectId", "difficultyModeChosen");
+
+-- CreateIndex
+CREATE INDEX "ProjectMilestone_projectId_idx" ON "ProjectMilestone"("projectId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "ProjectMilestone_projectId_milestoneNumber_key" ON "ProjectMilestone"("projectId", "milestoneNumber");
+
+-- CreateIndex
+CREATE INDEX "UserMilestone_userId_idx" ON "UserMilestone"("userId");
+
+-- CreateIndex
+CREATE INDEX "UserMilestone_milestoneId_idx" ON "UserMilestone"("milestoneId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "UserMilestone_userId_milestoneId_userProjectId_key" ON "UserMilestone"("userId", "milestoneId", "userProjectId");
+
+-- CreateIndex
+CREATE INDEX "Submission_userId_idx" ON "Submission"("userId");
+
+-- CreateIndex
+CREATE INDEX "Submission_projectId_idx" ON "Submission"("projectId");
+
+-- CreateIndex
+CREATE INDEX "Submission_isPublic_idx" ON "Submission"("isPublic");
+
+-- CreateIndex
+CREATE INDEX "Submission_createdAt_idx" ON "Submission"("createdAt");
+
+-- CreateIndex
+CREATE INDEX "Comment_submissionId_idx" ON "Comment"("submissionId");
+
+-- CreateIndex
+CREATE INDEX "Comment_userId_idx" ON "Comment"("userId");
+
+-- CreateIndex
+CREATE INDEX "Comment_parentId_idx" ON "Comment"("parentId");
+
+-- CreateIndex
+CREATE INDEX "Vote_submissionId_idx" ON "Vote"("submissionId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Vote_userId_submissionId_key" ON "Vote"("userId", "submissionId");
+
+-- CreateIndex
+CREATE INDEX "ForumThread_userId_idx" ON "ForumThread"("userId");
+
+-- CreateIndex
+CREATE INDEX "ForumThread_projectId_idx" ON "ForumThread"("projectId");
+
+-- CreateIndex
+CREATE INDEX "ForumThread_createdAt_idx" ON "ForumThread"("createdAt");
+
+-- CreateIndex
+CREATE INDEX "ForumPost_threadId_idx" ON "ForumPost"("threadId");
+
+-- CreateIndex
+CREATE INDEX "ForumPost_userId_idx" ON "ForumPost"("userId");
+
+-- CreateIndex
+CREATE INDEX "Team_projectId_idx" ON "Team"("projectId");
+
+-- CreateIndex
+CREATE INDEX "TeamMember_teamId_idx" ON "TeamMember"("teamId");
+
+-- CreateIndex
+CREATE INDEX "TeamMember_userId_idx" ON "TeamMember"("userId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "TeamMember_teamId_userId_key" ON "TeamMember"("teamId", "userId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Achievement_name_key" ON "Achievement"("name");
+
+-- CreateIndex
+CREATE INDEX "UserAchievement_userId_idx" ON "UserAchievement"("userId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "UserAchievement_userId_achievementId_key" ON "UserAchievement"("userId", "achievementId");
+
+-- CreateIndex
+CREATE INDEX "LearningPathProject_pathId_idx" ON "LearningPathProject"("pathId");
+
+-- CreateIndex
+CREATE INDEX "LearningPathProject_projectId_idx" ON "LearningPathProject"("projectId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "LearningPathProject_pathId_orderIndex_key" ON "LearningPathProject"("pathId", "orderIndex");
+
+-- CreateIndex
+CREATE INDEX "UserPathProgress_userId_idx" ON "UserPathProgress"("userId");
+
+-- CreateIndex
+CREATE INDEX "UserPathProgress_pathId_idx" ON "UserPathProgress"("pathId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "UserPathProgress_userId_pathId_key" ON "UserPathProgress"("userId", "pathId");
+
+-- CreateIndex
+CREATE INDEX "ChatMessage_userId_idx" ON "ChatMessage"("userId");
+
+-- CreateIndex
+CREATE INDEX "ChatMessage_projectId_idx" ON "ChatMessage"("projectId");
+
+-- CreateIndex
+CREATE INDEX "ChatMessage_createdAt_idx" ON "ChatMessage"("createdAt");
+
+-- CreateIndex
+CREATE INDEX "Follow_followerId_idx" ON "Follow"("followerId");
+
+-- CreateIndex
+CREATE INDEX "Follow_followingId_idx" ON "Follow"("followingId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Follow_followerId_followingId_key" ON "Follow"("followerId", "followingId");
+
+-- CreateIndex
+CREATE INDEX "Notification_userId_idx" ON "Notification"("userId");
+
+-- CreateIndex
+CREATE INDEX "Notification_read_idx" ON "Notification"("read");
+
+-- CreateIndex
+CREATE INDEX "Notification_createdAt_idx" ON "Notification"("createdAt");
+
+-- CreateIndex
+CREATE INDEX "Event_status_idx" ON "Event"("status");
+
+-- CreateIndex
+CREATE INDEX "Event_startDate_idx" ON "Event"("startDate");
+
+-- CreateIndex
+CREATE INDEX "Event_type_idx" ON "Event"("type");
+
+-- CreateIndex
+CREATE INDEX "EventParticipant_eventId_idx" ON "EventParticipant"("eventId");
+
+-- CreateIndex
+CREATE INDEX "EventParticipant_userId_idx" ON "EventParticipant"("userId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "EventParticipant_eventId_userId_key" ON "EventParticipant"("eventId", "userId");
+
+-- CreateIndex
+CREATE INDEX "EventLeaderboard_eventId_score_idx" ON "EventLeaderboard"("eventId", "score");
+
+-- CreateIndex
+CREATE INDEX "EventLeaderboard_userId_idx" ON "EventLeaderboard"("userId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "EventLeaderboard_eventId_userId_key" ON "EventLeaderboard"("eventId", "userId");
+
+-- CreateIndex
+CREATE INDEX "SocialShare_userId_idx" ON "SocialShare"("userId");
+
+-- CreateIndex
+CREATE INDEX "SocialShare_projectId_idx" ON "SocialShare"("projectId");
+
+-- AddForeignKey
+ALTER TABLE "Project" ADD CONSTRAINT "Project_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "UserProject" ADD CONSTRAINT "UserProject_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "UserProject" ADD CONSTRAINT "UserProject_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ProjectMilestone" ADD CONSTRAINT "ProjectMilestone_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "UserMilestone" ADD CONSTRAINT "UserMilestone_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "UserMilestone" ADD CONSTRAINT "UserMilestone_userProjectId_fkey" FOREIGN KEY ("userProjectId") REFERENCES "UserProject"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "UserMilestone" ADD CONSTRAINT "UserMilestone_milestoneId_fkey" FOREIGN KEY ("milestoneId") REFERENCES "ProjectMilestone"("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_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Comment" ADD CONSTRAINT "Comment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Comment" ADD CONSTRAINT "Comment_submissionId_fkey" FOREIGN KEY ("submissionId") REFERENCES "Submission"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Comment" ADD CONSTRAINT "Comment_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Comment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Vote" ADD CONSTRAINT "Vote_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Vote" ADD CONSTRAINT "Vote_submissionId_fkey" FOREIGN KEY ("submissionId") REFERENCES "Submission"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ForumThread" ADD CONSTRAINT "ForumThread_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ForumThread" ADD CONSTRAINT "ForumThread_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ForumPost" ADD CONSTRAINT "ForumPost_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ForumPost" ADD CONSTRAINT "ForumPost_threadId_fkey" FOREIGN KEY ("threadId") REFERENCES "ForumThread"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Team" ADD CONSTRAINT "Team_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "TeamMember" ADD CONSTRAINT "TeamMember_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "TeamMember" ADD CONSTRAINT "TeamMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "UserAchievement" ADD CONSTRAINT "UserAchievement_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "UserAchievement" ADD CONSTRAINT "UserAchievement_achievementId_fkey" FOREIGN KEY ("achievementId") REFERENCES "Achievement"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "LearningPathProject" ADD CONSTRAINT "LearningPathProject_pathId_fkey" FOREIGN KEY ("pathId") REFERENCES "LearningPath"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "LearningPathProject" ADD CONSTRAINT "LearningPathProject_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "UserPathProgress" ADD CONSTRAINT "UserPathProgress_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "UserPathProgress" ADD CONSTRAINT "UserPathProgress_pathId_fkey" FOREIGN KEY ("pathId") REFERENCES "LearningPath"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ChatMessage" ADD CONSTRAINT "ChatMessage_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ChatMessage" ADD CONSTRAINT "ChatMessage_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Follow" ADD CONSTRAINT "Follow_followerId_fkey" FOREIGN KEY ("followerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Follow" ADD CONSTRAINT "Follow_followingId_fkey" FOREIGN KEY ("followingId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Notification" ADD CONSTRAINT "Notification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "EventParticipant" ADD CONSTRAINT "EventParticipant_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "Event"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "EventParticipant" ADD CONSTRAINT "EventParticipant_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "EventLeaderboard" ADD CONSTRAINT "EventLeaderboard_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "Event"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "EventLeaderboard" ADD CONSTRAINT "EventLeaderboard_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "SocialShare" ADD CONSTRAINT "SocialShare_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/migrations/20251229162958_add_project_cover_image/migration.sql b/prisma/migrations/20251229162958_add_project_cover_image/migration.sql
new file mode 100644
index 0000000000000000000000000000000000000000..45e695c01d9ff11b0c63f759556eeaad3b3fdb70
--- /dev/null
+++ b/prisma/migrations/20251229162958_add_project_cover_image/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Project" ADD COLUMN "coverImage" TEXT;
diff --git a/prisma/migrations/generate-slugs.ts b/prisma/migrations/generate-slugs.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4ab14869d3baa440e2d44141e286d6d3ea08f780
--- /dev/null
+++ b/prisma/migrations/generate-slugs.ts
@@ -0,0 +1,42 @@
+import { PrismaClient } from "@prisma/client";
+
+const prisma = new PrismaClient();
+
+function generateSlug(title: string): string {
+ return title
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "-")
+ .replace(/^-+|-+$/g, "");
+}
+
+async function generateSlugs() {
+ const projects = await prisma.project.findMany({
+ where: { slug: null } as any,
+ });
+
+ console.log(`Generating slugs for ${projects.length} projects...`);
+
+ for (const project of projects) {
+ let slug = generateSlug(project.title);
+ let counter = 1;
+
+ // Handle duplicates
+ while (await prisma.project.findUnique({ where: { slug } })) {
+ slug = `${generateSlug(project.title)}-${counter}`;
+ counter++;
+ }
+
+ await prisma.project.update({
+ where: { id: project.id },
+ data: { slug },
+ });
+
+ console.log(`✓ ${project.title} -> ${slug}`);
+ }
+
+ console.log("Done!");
+}
+
+generateSlugs()
+ .catch(console.error)
+ .finally(() => prisma.$disconnect());
diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml
new file mode 100644
index 0000000000000000000000000000000000000000..044d57cdb0d5b5bff3475112359a4ee70439bf78
--- /dev/null
+++ b/prisma/migrations/migration_lock.toml
@@ -0,0 +1,3 @@
+# Please do not edit this file manually
+# It should be added in your version-control system (e.g., Git)
+provider = "postgresql"
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
new file mode 100644
index 0000000000000000000000000000000000000000..ac3d5a79a29a090ac78e1ff683bf1d9ae279fea3
--- /dev/null
+++ b/prisma/schema.prisma
@@ -0,0 +1,487 @@
+// This is your Prisma schema file,
+// learn more about it in the docs: https://pris.ly/d/prisma-schema
+
+generator client {
+ provider = "prisma-client-js"
+}
+
+datasource db {
+ provider = "postgresql"
+ url = env("DATABASE_URL")
+ directUrl = env("DIRECT_DATABASE_URL")
+}
+
+enum Role {
+ STUDENT
+ MENTOR
+ ADMIN
+ CONTRIBUTOR
+}
+
+enum Difficulty {
+ BEGINNER
+ INTERMEDIATE
+ ADVANCED
+}
+
+enum DifficultyMode {
+ GUIDED
+ STANDARD
+ HARDCORE
+}
+
+enum ProjectStatus {
+ NOT_STARTED
+ IN_PROGRESS
+ COMPLETED
+}
+
+model User {
+ id String @id @default(uuid())
+ email String @unique
+ password String?
+ firstName String
+ lastName String
+ skillLevel Difficulty @default(BEGINNER)
+ bio String?
+ portfolioLinks String[] @default([])
+ xp Int @default(0)
+ streak Int @default(0)
+ role Role @default(STUDENT)
+ avatarUrl String?
+ githubId String? @unique
+ googleId String? @unique
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ projects UserProject[]
+ createdProjects Project[] @relation("ProjectCreator")
+ submissions Submission[]
+ comments Comment[]
+ votes Vote[]
+ forumThreads ForumThread[]
+ forumPosts ForumPost[]
+ chatMessages ChatMessage[]
+
+ teams TeamMember[]
+ achievements UserAchievement[]
+ pathProgress UserPathProgress[]
+ milestoneProgress UserMilestone[]
+
+ followers Follow[] @relation("UserFollowing")
+ following Follow[] @relation("UserFollowers")
+ notifications Notification[]
+ eventParticipations EventParticipant[]
+ eventLeaderboards EventLeaderboard[]
+ socialShares SocialShare[]
+
+ @@index([email])
+ @@index([role])
+}
+
+model Project {
+ id String @id @default(uuid())
+ title String
+ slug String @unique
+ description String
+ difficultyLevel Difficulty
+ technologies String[]
+ categories String[]
+ estimatedTime String?
+ learningObjectives String[]
+ resourceLinks Json[] @default([])
+ starterRepoUrl String?
+ difficultyModes DifficultyMode[] @default([GUIDED, STANDARD, HARDCORE])
+ submissionCount Int @default(0)
+ completionRate Float @default(0)
+ featured Boolean @default(false)
+ coverImage String?
+
+ createdById String
+ createdBy User @relation("ProjectCreator", fields: [createdById], references: [id], onDelete: Cascade)
+
+ milestones ProjectMilestone[]
+ userProjects UserProject[]
+ submissions Submission[]
+ forumThreads ForumThread[]
+ chatMessages ChatMessage[]
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ teams Team[]
+ learningPaths LearningPathProject[]
+
+ @@index([difficultyLevel])
+ @@index([featured])
+ @@index([slug])
+ @@index([createdAt])
+}
+
+model UserProject {
+ id String @id @default(uuid())
+ userId String
+ projectId String
+ status ProjectStatus @default(NOT_STARTED)
+ difficultyModeChosen DifficultyMode
+ repoUrl String?
+ startedAt DateTime @default(now())
+ completedAt DateTime?
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
+ milestoneProgress UserMilestone[]
+
+ @@unique([userId, projectId, difficultyModeChosen])
+ @@index([userId])
+ @@index([projectId])
+ @@index([status])
+}
+
+model ProjectMilestone {
+ id String @id @default(uuid())
+ projectId String
+ milestoneNumber Int
+ title String
+ description String
+ hints Json @default("{}")
+ validationCriteria String?
+
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
+ userProgress UserMilestone[]
+
+ @@unique([projectId, milestoneNumber])
+ @@index([projectId])
+}
+
+model UserMilestone {
+ id String @id @default(uuid())
+ userId String
+ userProjectId String?
+ milestoneId String
+ completedAt DateTime @default(now())
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ userProject UserProject? @relation(fields: [userProjectId], references: [id], onDelete: Cascade)
+ milestone ProjectMilestone @relation(fields: [milestoneId], references: [id], onDelete: Cascade)
+
+ @@unique([userId, milestoneId, userProjectId])
+ @@index([userId])
+ @@index([milestoneId])
+}
+
+model Submission {
+ id String @id @default(uuid())
+ userId String
+ projectId String
+ repoUrl String
+ liveUrl String?
+ description String?
+ feedback String?
+ score Int?
+ upvotes Int @default(0)
+ isPublic Boolean @default(false)
+ aiReviewData Json?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
+ comments Comment[]
+ votes Vote[]
+
+ @@index([userId])
+ @@index([projectId])
+ @@index([isPublic])
+ @@index([createdAt])
+}
+
+model Comment {
+ id String @id @default(uuid())
+ content String
+ userId String
+ submissionId String
+ parentId String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ submission Submission @relation(fields: [submissionId], references: [id], onDelete: Cascade)
+ parent Comment? @relation("CommentReplies", fields: [parentId], references: [id], onDelete: Cascade)
+ replies Comment[] @relation("CommentReplies")
+
+ @@index([submissionId])
+ @@index([userId])
+ @@index([parentId])
+}
+
+model Vote {
+ id String @id @default(uuid())
+ value Int // 1 for upvote, -1 for downvote
+ userId String
+ submissionId String
+ createdAt DateTime @default(now())
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ submission Submission @relation(fields: [submissionId], references: [id], onDelete: Cascade)
+
+ @@unique([userId, submissionId])
+ @@index([submissionId])
+}
+
+model ForumThread {
+ id String @id @default(uuid())
+ title String
+ content String
+ userId String
+ projectId String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
+ posts ForumPost[]
+
+ @@index([userId])
+ @@index([projectId])
+ @@index([createdAt])
+}
+
+model ForumPost {
+ id String @id @default(uuid())
+ content String
+ userId String
+ threadId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ thread ForumThread @relation(fields: [threadId], references: [id], onDelete: Cascade)
+
+ @@index([threadId])
+ @@index([userId])
+}
+
+model Team {
+ id String @id @default(uuid())
+ name String
+ projectId String
+ createdAt DateTime @default(now())
+
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
+ members TeamMember[]
+
+ @@index([projectId])
+}
+
+model TeamMember {
+ id String @id @default(uuid())
+ teamId String
+ userId String
+ joinedAt DateTime @default(now())
+
+ team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@unique([teamId, userId])
+ @@index([teamId])
+ @@index([userId])
+}
+
+model Achievement {
+ id String @id @default(uuid())
+ name String @unique
+ description String
+ icon String?
+ xpReward Int
+ criteria Json
+ createdAt DateTime @default(now())
+
+ users UserAchievement[]
+}
+
+model UserAchievement {
+ id String @id @default(uuid())
+ userId String
+ achievementId String
+ earnedAt DateTime @default(now())
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ achievement Achievement @relation(fields: [achievementId], references: [id], onDelete: Cascade)
+
+ @@unique([userId, achievementId])
+ @@index([userId])
+}
+
+model LearningPath {
+ id String @id @default(uuid())
+ name String
+ description String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ projects LearningPathProject[]
+ userProgress UserPathProgress[]
+}
+
+model LearningPathProject {
+ id String @id @default(uuid())
+ pathId String
+ projectId String
+ orderIndex Int
+ prerequisites String[] @default([])
+
+ path LearningPath @relation(fields: [pathId], references: [id], onDelete: Cascade)
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
+
+ @@unique([pathId, orderIndex])
+ @@index([pathId])
+ @@index([projectId])
+}
+
+model UserPathProgress {
+ id String @id @default(uuid())
+ userId String
+ pathId String
+ currentProjectIndex Int @default(0)
+ completedAt DateTime?
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ path LearningPath @relation(fields: [pathId], references: [id], onDelete: Cascade)
+
+ @@unique([userId, pathId])
+ @@index([userId])
+ @@index([pathId])
+}
+
+model ChatMessage {
+ id String @id @default(uuid())
+ userId String
+ projectId String?
+ role String
+ content String
+ createdAt DateTime @default(now())
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
+
+ @@index([userId])
+ @@index([projectId])
+ @@index([createdAt])
+}
+
+model Follow {
+ id String @id @default(uuid())
+ followerId String
+ followingId String
+ createdAt DateTime @default(now())
+
+ follower User @relation("UserFollowers", fields: [followerId], references: [id], onDelete: Cascade)
+ following User @relation("UserFollowing", fields: [followingId], references: [id], onDelete: Cascade)
+
+ @@unique([followerId, followingId])
+ @@index([followerId])
+ @@index([followingId])
+}
+
+enum NotificationType {
+ FOLLOW
+ ACHIEVEMENT
+ PROJECT_COMPLETE
+ COMMENT
+ UPVOTE
+ TEAM_INVITE
+ EVENT_REMINDER
+ CHALLENGE_START
+}
+
+model Notification {
+ id String @id @default(uuid())
+ userId String
+ type NotificationType
+ title String
+ message String
+ link String?
+ read Boolean @default(false)
+ createdAt DateTime @default(now())
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@index([userId])
+ @@index([read])
+ @@index([createdAt])
+}
+
+enum EventStatus {
+ UPCOMING
+ ONGOING
+ COMPLETED
+}
+
+model Event {
+ id String @id @default(uuid())
+ title String
+ description String
+ type String
+ startDate DateTime
+ endDate DateTime
+ status EventStatus @default(UPCOMING)
+ prizePool String?
+ maxParticipants Int?
+ bannerUrl String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ participants EventParticipant[]
+ leaderboard EventLeaderboard[]
+
+ @@index([status])
+ @@index([startDate])
+ @@index([type])
+}
+
+model EventParticipant {
+ id String @id @default(uuid())
+ eventId String
+ userId String
+ joinedAt DateTime @default(now())
+
+ event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@unique([eventId, userId])
+ @@index([eventId])
+ @@index([userId])
+}
+
+model EventLeaderboard {
+ id String @id @default(uuid())
+ eventId String
+ userId String
+ score Int @default(0)
+ rank Int?
+ updatedAt DateTime @updatedAt
+
+ event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@unique([eventId, userId])
+ @@index([eventId, score])
+ @@index([userId])
+}
+
+model SocialShare {
+ id String @id @default(uuid())
+ userId String
+ projectId String
+ platform String
+ shareUrl String
+ ogImageUrl String?
+ createdAt DateTime @default(now())
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@index([userId])
+ @@index([projectId])
+}
+
diff --git a/prisma/seed.ts b/prisma/seed.ts
new file mode 100644
index 0000000000000000000000000000000000000000..474da7e73d98e34c09c713e6a2fc466741a3903e
--- /dev/null
+++ b/prisma/seed.ts
@@ -0,0 +1,369 @@
+import { PrismaClient, Difficulty } from "@prisma/client";
+import * as argon2 from "argon2";
+
+const prisma = new PrismaClient();
+
+function generateSlug(title: string): string {
+ return title
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "-")
+ .replace(/^-+|-+$/g, "");
+}
+
+const projects = [
+ {
+ title: "Landing Page Template",
+ slug: "landing-page-template",
+ difficultyLevel: Difficulty.BEGINNER,
+ description:
+ "Build a responsive landing page with modern design principles",
+ technologies: ["HTML", "CSS"],
+ categories: ["Frontend", "Web Design"],
+ estimatedTime: "4-6 hours",
+ learningObjectives: [
+ "Responsive design",
+ "CSS Flexbox/Grid",
+ "Modern UI patterns",
+ ],
+ resourceLinks: [
+ { type: "video", url: "#", title: "HTML & CSS Crash Course" },
+ ],
+ featured: true,
+ },
+ {
+ title: "E-Commerce Dashboard",
+ slug: "ecommerce-dashboard",
+ difficultyLevel: Difficulty.INTERMEDIATE,
+ description: "Create dynamic admin dashboard with React and Chart.js",
+ technologies: ["React", "Chart.js"],
+ categories: ["Frontend", "Data Visualization"],
+ estimatedTime: "12-16 hours",
+ learningObjectives: [
+ "React state management",
+ "Data visualization",
+ "Dashboard UX",
+ ],
+ resourceLinks: [
+ { type: "article", url: "#", title: "React State Management Guide" },
+ ],
+ featured: true,
+ },
+ {
+ title: "E-Commerce REST API",
+ slug: "ecommerce-rest-api",
+ difficultyLevel: Difficulty.INTERMEDIATE,
+ description:
+ "Develop a Node.js API with Express and MongoDB for product management",
+ technologies: ["Node.js", "Express", "MongoDB"],
+ categories: ["Backend", "API Development"],
+ estimatedTime: "16-20 hours",
+ learningObjectives: [
+ "RESTful API design",
+ "Database modeling",
+ "Authentication",
+ ],
+ resourceLinks: [
+ { type: "course", url: "#", title: "Node.js Fundamentals" },
+ ],
+ featured: false,
+ milestones: [
+ {
+ title: "Database Models",
+ description: "Create Mongoose/Prisma models.",
+ hints: {
+ GUIDED: [
+ "Define a schema for Products.",
+ "Include fields for price and stock.",
+ ],
+ },
+ },
+ ],
+ },
+ {
+ title: "Social Media Dashboard",
+ slug: "social-media-dashboard",
+ difficultyLevel: Difficulty.ADVANCED,
+ description: "Build real-time dashboard with React, GraphQL and WebSockets",
+ technologies: ["React", "GraphQL", "WebSockets"],
+ categories: ["Full-stack", "Real-time"],
+ estimatedTime: "24-32 hours",
+ learningObjectives: [
+ "GraphQL subscriptions",
+ "Real-time data",
+ "Advanced state management",
+ ],
+ resourceLinks: [
+ { type: "video", url: "#", title: "GraphQL Subscriptions" },
+ ],
+ featured: true,
+ milestones: [
+ {
+ title: "WebSocket Server Setup",
+ description: "Configure Socket.io on the backend.",
+ hints: {
+ GUIDED: [
+ "Listen for 'connection' events.",
+ "Use rooms for specific threads.",
+ ],
+ },
+ },
+ ],
+ },
+ {
+ title: "Portfolio Website",
+ slug: "portfolio-website",
+ difficultyLevel: Difficulty.BEGINNER,
+ description: "Create a personal portfolio with Next.js and Tailwind CSS",
+ technologies: ["Next.js", "Tailwind CSS"],
+ categories: ["Frontend", "Web Design"],
+ estimatedTime: "8-10 hours",
+ learningObjectives: [
+ "Next.js basics",
+ "Tailwind CSS",
+ "Portfolio best practices",
+ ],
+ resourceLinks: [
+ { type: "video", url: "#", title: "Portfolio Best Practices" },
+ ],
+ featured: false,
+ milestones: [
+ {
+ title: "Hero Section",
+ description: "Build a stunning hero section.",
+ hints: {
+ GUIDED: ["Use a high-quality headshot.", "Include a strong CTA."],
+ },
+ },
+ ],
+ },
+ {
+ title: "React Todo App with Firebase",
+ slug: "react-todo-firebase",
+ difficultyLevel: Difficulty.INTERMEDIATE,
+ description: "Create a collaborative todo application with real-time sync",
+ technologies: ["React", "Firebase"],
+ categories: ["Frontend", "Real-time"],
+ estimatedTime: "10-14 hours",
+ learningObjectives: [
+ "Firebase integration",
+ "Real-time sync",
+ "CRUD operations",
+ ],
+ resourceLinks: [
+ { type: "article", url: "#", title: "Firebase Realtime Database Guide" },
+ ],
+ featured: false,
+ },
+ {
+ title: "Authentication System",
+ slug: "authentication-system",
+ difficultyLevel: Difficulty.ADVANCED,
+ description: "Implement OAuth 2.0 flow with Node.js and Passport.js",
+ technologies: ["Node.js", "Passport.js"],
+ categories: ["Backend", "Security"],
+ estimatedTime: "20-24 hours",
+ learningObjectives: ["OAuth 2.0", "JWT tokens", "Security best practices"],
+ resourceLinks: [
+ { type: "course", url: "#", title: "Web Security Fundamentals" },
+ ],
+ featured: false,
+ },
+ {
+ title: "Mobile Recipe App",
+ slug: "mobile-recipe-app",
+ difficultyLevel: Difficulty.INTERMEDIATE,
+ description: "Build cross-platform recipe manager with React Native",
+ technologies: ["React Native"],
+ categories: ["Mobile", "Frontend"],
+ estimatedTime: "18-22 hours",
+ learningObjectives: [
+ "React Native basics",
+ "Mobile navigation",
+ "API integration",
+ ],
+ resourceLinks: [
+ { type: "video", url: "#", title: "React Native Navigation Tutorial" },
+ ],
+ featured: false,
+ },
+ {
+ title: "CMS Platform",
+ slug: "cms-platform",
+ difficultyLevel: Difficulty.ADVANCED,
+ description: "Headless CMS with Next.js and Sanity.io",
+ technologies: ["Next.js", "Sanity.io"],
+ categories: ["Full-stack", "CMS"],
+ estimatedTime: "28-36 hours",
+ learningObjectives: ["Headless CMS", "Content modeling", "Next.js ISR"],
+ resourceLinks: [
+ { type: "article", url: "#", title: "Sanity Studio Customization" },
+ ],
+ featured: true,
+ },
+ {
+ title: "AI Chat Interface",
+ slug: "ai-chat-interface",
+ difficultyLevel: Difficulty.ADVANCED,
+ description: "Build GPT-4 chatbot with streaming responses",
+ technologies: ["GPT-4", "Streaming"],
+ categories: ["AI/ML", "Full-stack"],
+ estimatedTime: "24-30 hours",
+ learningObjectives: ["LLM integration", "Streaming responses", "Chat UX"],
+ resourceLinks: [
+ { type: "course", url: "#", title: "LLM Integration Patterns" },
+ ],
+ featured: true,
+ },
+ {
+ title: "Real-time Chat Application",
+ slug: "realtime-chat-app",
+ difficultyLevel: Difficulty.INTERMEDIATE,
+ description: "Build chat app with Socket.io and React",
+ technologies: ["Socket.io", "React"],
+ categories: ["Full-stack", "Real-time"],
+ estimatedTime: "14-18 hours",
+ learningObjectives: [
+ "WebSocket protocol",
+ "Real-time messaging",
+ "Chat features",
+ ],
+ resourceLinks: [
+ { type: "video", url: "#", title: "WebSocket Fundamentals" },
+ ],
+ featured: false,
+ },
+ {
+ title: "Serverless API Gateway",
+ slug: "serverless-api-gateway",
+ difficultyLevel: Difficulty.ADVANCED,
+ description: "Create REST API using AWS Lambda & API Gateway",
+ technologies: ["AWS Lambda", "API Gateway"],
+ categories: ["Backend", "Cloud"],
+ estimatedTime: "20-26 hours",
+ learningObjectives: [
+ "Serverless architecture",
+ "AWS services",
+ "API design",
+ ],
+ resourceLinks: [
+ { type: "article", url: "#", title: "AWS Serverless Patterns" },
+ ],
+ featured: false,
+ },
+ {
+ title: "E-commerce Product Search",
+ slug: "ecommerce-product-search",
+ difficultyLevel: Difficulty.ADVANCED,
+ description: "Implement search functionality with Algolia",
+ technologies: ["Algolia"],
+ categories: ["Frontend", "Search"],
+ estimatedTime: "16-20 hours",
+ learningObjectives: [
+ "Search algorithms",
+ "Algolia integration",
+ "Search UX",
+ ],
+ resourceLinks: [
+ { type: "course", url: "#", title: "Search Engine Optimization" },
+ ],
+ featured: false,
+ },
+ {
+ title: "Mobile Payment Integration",
+ slug: "mobile-payment-integration",
+ difficultyLevel: Difficulty.ADVANCED,
+ description: "Add Stripe payments to React Native app",
+ technologies: ["React Native", "Stripe"],
+ categories: ["Mobile", "Payments"],
+ estimatedTime: "18-24 hours",
+ learningObjectives: ["Payment processing", "Stripe API", "Mobile security"],
+ resourceLinks: [
+ { type: "video", url: "#", title: "Mobile Payment Systems" },
+ ],
+ featured: false,
+ },
+ {
+ title: "Data Visualization Dashboard",
+ slug: "data-visualization-dashboard",
+ difficultyLevel: Difficulty.INTERMEDIATE,
+ description: "Create analytics dashboard with D3.js",
+ technologies: ["D3.js"],
+ categories: ["Frontend", "Data Visualization"],
+ estimatedTime: "14-18 hours",
+ learningObjectives: [
+ "D3.js fundamentals",
+ "Data visualization",
+ "Interactive charts",
+ ],
+ resourceLinks: [{ type: "article", url: "#", title: "D3.js Essentials" }],
+ featured: false,
+ },
+];
+
+async function main() {
+ console.log("Start seeding...");
+
+ const hashedPassword = await argon2.hash("password123");
+
+ // Create a default user if not exists
+ const user = await prisma.user.upsert({
+ where: { email: "admin@devresource.com" },
+ update: {
+ password: hashedPassword,
+ },
+ create: {
+ email: "admin@devresource.com",
+ firstName: "Admin",
+ lastName: "User",
+ password: hashedPassword,
+ role: "ADMIN",
+ },
+ });
+
+ console.log(`Created user with id: ${user.id}`);
+
+ for (const projectData of projects) {
+ const { milestones, ...project } = projectData as any;
+
+ const createdProject = await prisma.project.upsert({
+ where: { slug: project.slug },
+ update: {
+ ...project,
+ createdById: user.id,
+ milestones: {
+ deleteMany: {},
+ create: milestones?.map((m: any, index: number) => ({
+ milestoneNumber: index + 1,
+ title: m.title,
+ description: m.description,
+ hints: m.hints || {},
+ })),
+ },
+ },
+ create: {
+ ...project,
+ createdById: user.id,
+ milestones: {
+ create: milestones?.map((m: any, index: number) => ({
+ milestoneNumber: index + 1,
+ title: m.title,
+ description: m.description,
+ hints: m.hints || {},
+ })),
+ },
+ },
+ });
+ console.log(`Created project with id: ${createdProject.id}`);
+ }
+
+ console.log("Seeding finished.");
+}
+
+main()
+ .catch((e) => {
+ console.error(e);
+ process.exit(1);
+ })
+ .finally(async () => {
+ await prisma.$disconnect();
+ });
diff --git a/routes/ai.ts b/routes/ai.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6b25ae666bc3acb47de5cb6035f21bb081fa7559
--- /dev/null
+++ b/routes/ai.ts
@@ -0,0 +1,17 @@
+import { Router } from "express";
+import {
+ chatWithAI,
+ getChatHistory,
+ getAllChatMessages,
+ requestHint,
+} from "../controllers/ai";
+import { authenticateToken } from "../middlewares/auth";
+
+const router = Router();
+
+router.post("/chat", authenticateToken, chatWithAI);
+router.get("/chat", authenticateToken, getAllChatMessages);
+router.get("/chat/history/:projectId", authenticateToken, getChatHistory);
+router.post("/hint-request", authenticateToken, requestHint);
+
+export default router;
diff --git a/routes/analytics.ts b/routes/analytics.ts
new file mode 100644
index 0000000000000000000000000000000000000000..121ba6ada2bc5c323a849491292b3560fd5d2ba7
--- /dev/null
+++ b/routes/analytics.ts
@@ -0,0 +1,13 @@
+import { Router } from "express";
+import {
+ getUserAnalytics,
+ getAdminAnalytics
+} from "../controllers/analytics";
+import { authenticateToken } from "../middlewares/auth";
+
+const router = Router();
+
+router.get("/user", authenticateToken, getUserAnalytics);
+router.get("/admin", authenticateToken, getAdminAnalytics);
+
+export default router;
diff --git a/routes/auth.ts b/routes/auth.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9cf8d9386e1ce2517ce08a2d19d6456c2a9a9215
--- /dev/null
+++ b/routes/auth.ts
@@ -0,0 +1,58 @@
+import express from "express";
+import {
+ forgetPassword,
+ getUser,
+ login,
+ logout,
+ me,
+ resetPassword,
+ signUp,
+ updateUser,
+ verifyOTP,
+ googleAuthCallback,
+ githubAuthCallback,
+} from "../controllers/auth";
+import { authenticateToken } from "../middlewares/auth";
+import passport from "../utils/passport";
+
+const router = express.Router();
+
+router.post("/register", signUp);
+router.post("/login", login);
+router.post("/logout", logout);
+router.get("/me", authenticateToken, me);
+
+router.post("/update/:id", authenticateToken, updateUser);
+router.get("/user/:id", authenticateToken, getUser);
+
+router.post("/forgetPassword", forgetPassword);
+router.post("/verifyOtp", verifyOTP);
+router.post("/resetPassword", resetPassword);
+
+router.get(
+ "/google",
+ passport.authenticate("google", { scope: ["profile", "email"] })
+);
+router.get(
+ "/google/callback",
+ passport.authenticate("google", {
+ session: false,
+ failureRedirect: `${process.env.CLIENT_URL}/auth/signin?error=auth_failed`,
+ }),
+ googleAuthCallback
+);
+
+router.get(
+ "/github",
+ passport.authenticate("github", { scope: ["user:email"] })
+);
+router.get(
+ "/github/callback",
+ passport.authenticate("github", {
+ session: false,
+ failureRedirect: `${process.env.CLIENT_URL}/auth/signin?error=auth_failed`,
+ }),
+ githubAuthCallback
+);
+
+export default router;
diff --git a/routes/code-review.ts b/routes/code-review.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6a9fd76a3b5cb176ce7658deded5f16f1c3b0f7b
--- /dev/null
+++ b/routes/code-review.ts
@@ -0,0 +1,10 @@
+import { Router } from "express";
+import { analyzeCode, getSubmissionReview } from "../controllers/code-review";
+import { authenticateToken } from "../middlewares/auth";
+
+const router = Router();
+
+router.post("/analyze", authenticateToken, analyzeCode);
+router.get("/:id", authenticateToken, getSubmissionReview);
+
+export default router;
diff --git a/routes/community.ts b/routes/community.ts
new file mode 100644
index 0000000000000000000000000000000000000000..47d6a58a8d4f5c4639489de2100e3ab295b88f34
--- /dev/null
+++ b/routes/community.ts
@@ -0,0 +1,19 @@
+import { Router } from "express";
+import {
+ getPublicSubmissions,
+ commentOnSubmission,
+ voteOnSubmission,
+ getForumThreads,
+ createForumThread
+} from "../controllers/community";
+import { authenticateToken } from "../middlewares/auth";
+
+const router = Router();
+
+router.get("/submissions/public", getPublicSubmissions);
+router.post("/submissions/:id/comment", authenticateToken, commentOnSubmission);
+router.post("/submissions/:id/vote", authenticateToken, voteOnSubmission);
+router.get("/forum/threads", getForumThreads);
+router.post("/forum/threads", authenticateToken, createForumThread);
+
+export default router;
diff --git a/routes/events.ts b/routes/events.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7d5ad1b310765e19d24dcba1a138007649828482
--- /dev/null
+++ b/routes/events.ts
@@ -0,0 +1,23 @@
+import express from "express";
+import {
+ createEvent,
+ getEvents,
+ getEvent,
+ joinEvent,
+ leaveEvent,
+ updateEventScore,
+ getLeaderboard,
+} from "../controllers/events";
+import { authenticateToken } from "../middlewares/auth";
+
+const router = express.Router();
+
+router.post("/", authenticateToken, createEvent);
+router.get("/", getEvents);
+router.get("/:id", getEvent);
+router.post("/:eventId/join", authenticateToken, joinEvent);
+router.delete("/:eventId/leave", authenticateToken, leaveEvent);
+router.patch("/:eventId/score/:userId", authenticateToken, updateEventScore);
+router.get("/:eventId/leaderboard", getLeaderboard);
+
+export default router;
diff --git a/routes/gamification.ts b/routes/gamification.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ba910508271b146f5a2d42b461e554f73353a4ac
--- /dev/null
+++ b/routes/gamification.ts
@@ -0,0 +1,13 @@
+import { Router } from "express";
+import {
+ getLeaderboard,
+ getUserAchievements
+} from "../controllers/gamification";
+import { authenticateToken } from "../middlewares/auth";
+
+const router = Router();
+
+router.get("/leaderboard", getLeaderboard);
+router.get("/achievements", authenticateToken, getUserAchievements);
+
+export default router;
diff --git a/routes/github.ts b/routes/github.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fe7a98976e25ebfdfa72f32d6a19ce569ab0ae88
--- /dev/null
+++ b/routes/github.ts
@@ -0,0 +1,10 @@
+import express from "express";
+import { authenticateToken } from "../middlewares/auth";
+import { linkRepository, getRepositoryCommits } from "../controllers/github";
+
+const router = express.Router();
+
+router.post("/link", authenticateToken, linkRepository);
+router.get("/:projectId/commits", authenticateToken, getRepositoryCommits);
+
+export default router;
diff --git a/routes/notifications.ts b/routes/notifications.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fc3616b09381e222f603a886543c93050fbb99cc
--- /dev/null
+++ b/routes/notifications.ts
@@ -0,0 +1,17 @@
+import express from "express";
+import {
+ getNotifications,
+ markAsRead,
+ markAllAsRead,
+ deleteNotification,
+} from "../controllers/notifications";
+import { authenticateToken } from "../middlewares/auth";
+
+const router = express.Router();
+
+router.get("/", authenticateToken, getNotifications);
+router.patch("/:notificationId/read", authenticateToken, markAsRead);
+router.patch("/read-all", authenticateToken, markAllAsRead);
+router.delete("/:notificationId", authenticateToken, deleteNotification);
+
+export default router;
diff --git a/routes/paths.ts b/routes/paths.ts
new file mode 100644
index 0000000000000000000000000000000000000000..815cc2e4e8e69153473b502154ac7fee605fe4d6
--- /dev/null
+++ b/routes/paths.ts
@@ -0,0 +1,15 @@
+import { Router } from "express";
+import {
+ getLearningPaths,
+ getPathProgress,
+ startPath
+} from "../controllers/paths";
+import { authenticateToken } from "../middlewares/auth";
+
+const router = Router();
+
+router.get("/", getLearningPaths);
+router.get("/:id/progress", authenticateToken, getPathProgress);
+router.post("/:id/start", authenticateToken, startPath);
+
+export default router;
diff --git a/routes/project.ts b/routes/project.ts
new file mode 100644
index 0000000000000000000000000000000000000000..52da282095c209a2c01cffcb5c39ac6795f9d12e
--- /dev/null
+++ b/routes/project.ts
@@ -0,0 +1,51 @@
+import { Router } from "express";
+import {
+ createProject,
+ updateProject,
+ deleteProject,
+ getProjects,
+ getProjectById,
+ submitProject,
+ startProject,
+ updateProgress,
+ completeProject,
+ getUserProgress,
+ completeMilestone,
+ getFeaturedProjects,
+ submitCode,
+} from "../controllers/projects";
+import { authenticateToken } from "../middlewares/auth";
+import { upload } from "../middlewares/file";
+
+const router = Router();
+
+router.get("/", getProjects);
+router.get("/progress", authenticateToken, getUserProgress);
+router.get("/featured", getFeaturedProjects);
+router.get("/:id", getProjectById);
+router.post("/", authenticateToken, upload.single("coverImage"), createProject);
+router.put(
+ "/:id",
+ authenticateToken,
+ upload.single("coverImage"),
+ updateProject
+);
+router.delete("/:id", authenticateToken, deleteProject);
+
+// User Progress
+router.post("/:id/start", authenticateToken, startProject);
+router.put("/:id/progress", authenticateToken, updateProgress);
+router.post("/:id/complete", authenticateToken, completeProject);
+
+// Milestones
+router.post(
+ "/:id/milestones/:milestoneId/complete",
+ authenticateToken,
+ completeMilestone
+);
+
+// Submissions
+router.post("/:id/submit", authenticateToken, submitProject);
+router.post("/:id/code/submit", authenticateToken, submitCode);
+
+export default router;
diff --git a/routes/social.ts b/routes/social.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8f87c9ae120e25d46fa95405d99e073c92a977d8
--- /dev/null
+++ b/routes/social.ts
@@ -0,0 +1,21 @@
+import express from "express";
+import {
+ followUser,
+ unfollowUser,
+ getFollowers,
+ getFollowing,
+ getActivityFeed,
+ shareProject,
+} from "../controllers/social";
+import { authenticateToken } from "../middlewares/auth";
+
+const router = express.Router();
+
+router.post("/follow", authenticateToken, followUser);
+router.delete("/unfollow/:followingId", authenticateToken, unfollowUser);
+router.get("/followers/:userId", authenticateToken, getFollowers);
+router.get("/following/:userId", authenticateToken, getFollowing);
+router.get("/activity-feed", authenticateToken, getActivityFeed);
+router.post("/share", authenticateToken, shareProject);
+
+export default router;
diff --git a/routes/teams.ts b/routes/teams.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ceab66dadb88f843f4d1d8b6537b1e700c595ceb
--- /dev/null
+++ b/routes/teams.ts
@@ -0,0 +1,15 @@
+import { Router } from "express";
+import {
+ createTeam,
+ joinTeam,
+ getTeamProjects
+} from "../controllers/teams";
+import { authenticateToken } from "../middlewares/auth";
+
+const router = Router();
+
+router.post("/create", authenticateToken, createTeam);
+router.post("/:id/join", authenticateToken, joinTeam);
+router.get("/:id/projects", authenticateToken, getTeamProjects);
+
+export default router;
diff --git a/routes/user.ts b/routes/user.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7ea3aaecab78ae151e7ee6f9abcd3c4e8d339362
--- /dev/null
+++ b/routes/user.ts
@@ -0,0 +1,17 @@
+import { Router } from "express";
+import {
+ startProject,
+ updateProgress,
+ completeProject,
+ getUserProgress
+} from "../controllers/projects";
+import { authenticateToken } from "../middlewares/auth";
+
+const router = Router();
+
+router.get("/progress", authenticateToken, getUserProgress);
+router.post("/projects/:id/start", authenticateToken, startProject);
+router.put("/projects/:id/progress", authenticateToken, updateProgress);
+router.post("/projects/:id/complete", authenticateToken, completeProject);
+
+export default router;
diff --git a/types/project.ts b/types/project.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4bde7be7444a85238fdb6ac4a82ba2ec54e1c476
--- /dev/null
+++ b/types/project.ts
@@ -0,0 +1,44 @@
+import { Difficulty, DifficultyMode, ProjectStatus } from "@prisma/client";
+
+export interface IProject {
+ id: string;
+ title: string;
+ description: string;
+ difficultyLevel: Difficulty;
+ technologies: string[];
+ categories: string[];
+ estimatedTime?: string;
+ learningObjectives: string[];
+ resourceLinks: any[];
+ starterRepoUrl?: string;
+ difficultyModes: DifficultyMode[];
+ submissionCount: number;
+ completionRate: number;
+ createdById: string;
+ createdAt: Date;
+ updatedAt: Date;
+ milestones?: IProjectMilestone[];
+ progressByMode?: Record;
+ userProgress?: any;
+}
+
+export interface IProjectMilestone {
+ id: string;
+ projectId: string;
+ milestoneNumber: number;
+ title: string;
+ description: string;
+ hints: string[];
+ validationCriteria?: string;
+}
+
+export interface IUserProject {
+ id: string;
+ userId: string;
+ projectId: string;
+ status: ProjectStatus;
+ difficultyModeChosen: DifficultyMode;
+ repoUrl?: string;
+ startedAt: Date;
+ completedAt?: Date;
+}
diff --git a/utils/gemini.ts b/utils/gemini.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5b8d925d81d6d89f028002948834660e4557fbc0
--- /dev/null
+++ b/utils/gemini.ts
@@ -0,0 +1,18 @@
+import { GoogleGenerativeAI } from "@google/generative-ai";
+
+const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || "");
+
+export const getGeminiResponse = async (
+ prompt: string,
+ history: { role: string; parts: { text: string }[] }[] = []
+) => {
+ const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" });
+
+ const chat = model.startChat({
+ history: history,
+ });
+
+ const result = await chat.sendMessage(prompt);
+ const response = await result.response;
+ return response.text();
+};
diff --git a/utils/helper.ts b/utils/helper.ts
new file mode 100644
index 0000000000000000000000000000000000000000..adae3a5bf01d01dc875b8940e314f88d5741a3f8
--- /dev/null
+++ b/utils/helper.ts
@@ -0,0 +1,17 @@
+export const parseIfString = (val: any) => {
+ if (typeof val === "string") {
+ try {
+ return JSON.parse(val);
+ } catch (e) {
+ return val;
+ }
+ }
+ return val;
+};
+
+export function generateSlug(title: string): string {
+ return title
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "-")
+ .replace(/^-+|-+$/g, "");
+}
diff --git a/utils/passport.ts b/utils/passport.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ff91abd0f6250527bbbb31555d569b75e698bf90
--- /dev/null
+++ b/utils/passport.ts
@@ -0,0 +1,119 @@
+import passport from "passport";
+import { Strategy as GoogleStrategy } from "passport-google-oauth20";
+import { Strategy as GitHubStrategy } from "passport-github2";
+import prisma from "../prisma/client";
+import dotenv from "dotenv";
+
+dotenv.config();
+
+passport.use(
+ new GoogleStrategy(
+ {
+ clientID: process.env.GOOGLE_CLIENT_ID!,
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
+ callbackURL: `${process.env.BACKEND_URL}/api/auth/google/callback`,
+ },
+ async (accessToken, refreshToken, profile, done) => {
+ try {
+ const email = profile.emails?.[0]?.value;
+ if (!email) {
+ return done(new Error("No email found in Google profile"));
+ }
+
+ let user = await prisma.user.findUnique({
+ where: { googleId: profile.id },
+ });
+
+ if (!user) {
+ user = await prisma.user.upsert({
+ where: { email },
+ update: {
+ googleId: profile.id,
+ firstName:
+ profile.name?.givenName || profile.displayName || "User",
+ lastName: profile.name?.familyName || "",
+ avatarUrl: profile.photos?.[0]?.value,
+ },
+ create: {
+ googleId: profile.id,
+ email,
+ firstName:
+ profile.name?.givenName || profile.displayName || "User",
+ lastName: profile.name?.familyName || "",
+ avatarUrl: profile.photos?.[0]?.value,
+ },
+ });
+ }
+
+ return done(null, user);
+ } catch (error) {
+ return done(error as Error);
+ }
+ }
+ )
+);
+
+passport.use(
+ new GitHubStrategy(
+ {
+ clientID: process.env.GITHUB_CLIENT_ID!,
+ clientSecret: process.env.GITHUB_CLIENT_SECRET!,
+ callbackURL: `${process.env.BACKEND_URL}/api/auth/github/callback`,
+ },
+ async (
+ accessToken: string,
+ refreshToken: string,
+ profile: any,
+ done: any
+ ) => {
+ try {
+ const email = profile.emails?.[0]?.value;
+ if (!email) {
+ return done(new Error("No email found in GitHub profile"));
+ }
+
+ let user = await prisma.user.findUnique({
+ where: { githubId: profile.id },
+ });
+
+ if (!user) {
+ user = await prisma.user.upsert({
+ where: { email },
+ update: {
+ githubId: profile.id,
+ firstName: profile.displayName || profile.username || "User",
+ lastName: "",
+ avatarUrl: profile.photos?.[0]?.value,
+ },
+ create: {
+ githubId: profile.id,
+ email,
+ firstName: profile.displayName || profile.username || "User",
+ lastName: "",
+ avatarUrl: profile.photos?.[0]?.value,
+ },
+ });
+ }
+
+ return done(null, user);
+ } catch (error) {
+ return done(error as Error);
+ }
+ }
+ )
+);
+
+passport.serializeUser((user: any, done) => {
+ done(null, user.id);
+});
+
+passport.deserializeUser(async (id: string, done) => {
+ try {
+ const user = await prisma.user.findUnique({ where: { id } });
+ done(null, user);
+ } catch (error) {
+ done(error);
+ }
+});
+
+export default passport;
diff --git a/utils/provider/index.ts b/utils/provider/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..58536bc3e79151cab1f79a566ce11f19203f436b
--- /dev/null
+++ b/utils/provider/index.ts
@@ -0,0 +1,35 @@
+import { OpenAI } from "openai";
+
+const openRouter = new OpenAI({
+ baseURL: "https://openrouter.ai/api/v1",
+ apiKey: process.env.OPENROUTER_API_KEY!,
+});
+
+const client = openRouter;
+export const aiProvider = async (
+ messages: Array<{ role: "system" | "user" | "assistant"; content: string }>
+) => {
+ const completion = await client.chat.completions.create({
+ model: "meta-llama/llama-4-maverick:free",
+ messages: messages as Array<{
+ role: "system" | "user" | "assistant";
+ content: string;
+ }>,
+ });
+
+ return completion.choices[0].message.content ?? "";
+};
+
+export const refinePrompt = async (userInput: string) => {
+ const systemPrompt = `You are an expert AI career and project advisor. Refine the user's input to be clear, specific, and optimized for accurate responses about tech career guidance or realistic project ideas. Include relevant keywords (e.g., frontend, backend, React, APIs), clarify user goals or skill level if implied, and remove ambiguity. Return a single, concise prompt in plain text (max 100 words), ready for AI processing.`;
+
+ const response = await client.chat.completions.create({
+ model: "meta-llama/llama-4-maverick:free",
+ messages: [
+ { role: "system", content: systemPrompt },
+ { role: "user", content: userInput },
+ ],
+ });
+
+ return response.choices[0].message.content ?? "";
+};
diff --git a/utils/provider/models.ts b/utils/provider/models.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f2561185ab20094d062865010fe9b4dca1b098d0
--- /dev/null
+++ b/utils/provider/models.ts
@@ -0,0 +1,71 @@
+import { LLM, Provider } from "./types";
+
+export const RENDER_MODELS: Record =
+ {
+ // OpenRouter free models
+ default: {
+ model: "mistralai/mistral-7b-instruct:free",
+ provider: "openrouter",
+ },
+ premium: {
+ model: "cognitivecomputations/dolphin3.0-r1-mistral-24b:free",
+ provider: "openrouter",
+ },
+ mistral: {
+ model: "mistralai/mistral-7b-instruct:free",
+ provider: "openrouter",
+ },
+ grok: {
+ model: "xai/grok-1:free",
+ provider: "openrouter",
+ },
+ cinematika: {
+ model: "openrouter/cinematika-7b:free",
+ provider: "openrouter",
+ },
+ "nous-capybara": {
+ model: "nousresearch/nous-capybara-7b:free",
+ provider: "openrouter",
+ },
+ "command-r": {
+ model: "cohere/command-r:free",
+ provider: "openrouter",
+ },
+ dolphin: {
+ model: "cognitivecomputations/dolphin3.0-r1-mistral-24b:free",
+ provider: "openrouter",
+ },
+ microsoft: {
+ model: "microsoft/phi-2:free",
+ provider: "openrouter",
+ },
+ qwen: {
+ model: "qwen/qwen3-4b:free",
+ provider: "openrouter",
+ },
+ deepseek: {
+ model: "deepseek/deepseek-v3-base:free",
+ provider: "openrouter",
+ },
+ // Note: The following models are not free and have been removed
+ gemini: {
+ model: "google/gemini-2.5-pro-exp-03-25",
+ provider: "openrouter",
+ },
+ amazon: {
+ model: "amazon/nova-micro-v1",
+ provider: "openrouter",
+ },
+ "gpt-4": {
+ model: "openai/gpt-4.1-nano",
+ provider: "openrouter",
+ },
+ llama3: {
+ model: "llama3-70b-8192",
+ provider: "groq",
+ },
+ gemma: {
+ model: "gemma-7b-it",
+ provider: "groq",
+ },
+ };
diff --git a/utils/provider/types.ts b/utils/provider/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9c72d08cb49e8a24a32747e2cd67ac178430de50
--- /dev/null
+++ b/utils/provider/types.ts
@@ -0,0 +1,19 @@
+export type LLM =
+ | "default"
+ | "premium"
+ | "mistral"
+ | "grok"
+ | "cinematika"
+ | "nous-capybara"
+ | "command-r"
+ | "dolphin"
+ | "llama3"
+ | "gemma"
+ | "microsoft"
+ | "qwen"
+ | "deepseek"
+ | "gemini"
+ | "amazon"
+ | "gpt-4";
+
+export type Provider = "openrouter" | "groq";
diff --git a/utils/redis.ts b/utils/redis.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1a47f06af8a806a79f2daa1f6ae8324256eea6a9
--- /dev/null
+++ b/utils/redis.ts
@@ -0,0 +1,13 @@
+import Redis from "ioredis";
+
+const redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379");
+
+redis.on("error", (err) => {
+ console.error("Redis error:", err);
+});
+
+redis.on("connect", () => {
+ console.log("Connected to Redis");
+});
+
+export default redis;
diff --git a/utils/socket.ts b/utils/socket.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d7c8ccb59b82dc93cfcf18f0518bc2fb767eef7f
--- /dev/null
+++ b/utils/socket.ts
@@ -0,0 +1,60 @@
+import { Server } from "socket.io";
+import { Server as HttpServer } from "http";
+
+export const initSocket = (server: HttpServer) => {
+ const io = new Server(server, {
+ cors: {
+ origin: process.env.CLIENT_URL
+ ? process.env.CLIENT_URL.split(",")
+ : ["http://localhost:3002"],
+ methods: ["GET", "POST"],
+ credentials: true,
+ },
+ });
+
+ io.on("connection", (socket) => {
+ console.log("A user connected:", socket.id);
+
+ // Join a collaboration room for a specific project
+ socket.on("join-project", (projectId: string) => {
+ socket.join(`project:${projectId}`);
+ console.log(`User ${socket.id} joined project ${projectId}`);
+ });
+
+ // Leave project room
+ socket.on("leave-project", (projectId: string) => {
+ socket.leave(`project:${projectId}`);
+ console.log(`User ${socket.id} left project ${projectId}`);
+ });
+
+ // Handle code changes (if not using Yjs directly via y-websocket server)
+ socket.on("code-change", (data: { projectId: string; change: any }) => {
+ socket.to(`project:${data.projectId}`).emit("code-update", data.change);
+ });
+
+ // Real-time cursor/presence
+ socket.on(
+ "cursor-move",
+ (data: { projectId: string; position: any; user: any }) => {
+ socket.to(`data.projectId`).emit("cursor-update", {
+ userId: socket.id,
+ ...data,
+ });
+ }
+ );
+
+ // Video Chat Signaling
+ socket.on("video-signal", (data: { target: string; signal: any }) => {
+ io.to(data.target).emit("video-signal", {
+ sender: socket.id,
+ signal: data.signal,
+ });
+ });
+
+ socket.on("disconnect", () => {
+ console.log("User disconnected:", socket.id);
+ });
+ });
+
+ return io;
+};