Spaces:
Runtime error
Runtime error
Upload 94 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- controllers/ai/index.ts +219 -0
- controllers/analytics/index.ts +72 -0
- controllers/auth/index.ts +448 -0
- controllers/code-review/index.ts +71 -0
- controllers/community/index.ts +152 -0
- controllers/events/index.ts +221 -0
- controllers/gamification/index.ts +69 -0
- controllers/github/index.ts +109 -0
- controllers/notifications/index.ts +75 -0
- controllers/paths/index.ts +82 -0
- controllers/projects/index.ts +706 -0
- controllers/social/index.ts +187 -0
- controllers/teams/index.ts +80 -0
- dist/controllers/ai/index.js +146 -0
- dist/controllers/analytics/index.js +74 -0
- dist/controllers/auth/index.js +397 -0
- dist/controllers/code-review/index.js +70 -0
- dist/controllers/community/index.js +156 -0
- dist/controllers/events/index.js +202 -0
- dist/controllers/gamification/index.js +71 -0
- dist/controllers/notifications/index.js +78 -0
- dist/controllers/paths/index.js +87 -0
- dist/controllers/projects/index.js +416 -0
- dist/controllers/social/index.js +181 -0
- dist/controllers/teams/index.js +71 -0
- dist/index.js +70 -0
- dist/middlewares/auth.js +32 -0
- dist/middlewares/email.js +36 -0
- dist/middlewares/errorHandler.js +51 -0
- dist/middlewares/file.js +32 -0
- dist/middlewares/generateOTP.js +13 -0
- dist/models/auth/index.js +26 -0
- dist/models/project/index.js +21 -0
- dist/prisma.config.js +16 -0
- dist/prisma/client.js +5 -0
- dist/prisma/migrations/generate-slugs.js +34 -0
- dist/prisma/seed.js +303 -0
- dist/routes/ai.js +11 -0
- dist/routes/analytics.js +9 -0
- dist/routes/auth.js +30 -0
- dist/routes/code-review.js +9 -0
- dist/routes/community.js +12 -0
- dist/routes/events.js +17 -0
- dist/routes/gamification.js +9 -0
- dist/routes/notifications.js +14 -0
- dist/routes/paths.js +10 -0
- dist/routes/project.js +22 -0
- dist/routes/social.js +16 -0
- dist/routes/teams.js +10 -0
- dist/routes/user.js +11 -0
controllers/ai/index.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Request, Response } from "express";
|
| 2 |
+
import prisma from "../../prisma/client";
|
| 3 |
+
import { getGeminiResponse } from "../../utils/gemini";
|
| 4 |
+
|
| 5 |
+
export const chatWithAI = async (req: Request, res: Response) => {
|
| 6 |
+
const userId = (req as any).user?.id;
|
| 7 |
+
const { message, projectId } = req.body;
|
| 8 |
+
|
| 9 |
+
try {
|
| 10 |
+
let projectContext = "";
|
| 11 |
+
let difficultyMode: string | null = null;
|
| 12 |
+
|
| 13 |
+
// 1. Fetch project context if projectId is provided
|
| 14 |
+
if (projectId) {
|
| 15 |
+
const project = await prisma.project.findUnique({
|
| 16 |
+
where: { id: projectId },
|
| 17 |
+
include: {
|
| 18 |
+
milestones: true,
|
| 19 |
+
userProjects: { where: { userId } },
|
| 20 |
+
},
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
if (project) {
|
| 24 |
+
difficultyMode =
|
| 25 |
+
project.userProjects?.[0]?.difficultyModeChosen || "STANDARD";
|
| 26 |
+
projectContext = `
|
| 27 |
+
You are helping a student with the following project:
|
| 28 |
+
Project Title: ${project.title}
|
| 29 |
+
Description: ${project.description}
|
| 30 |
+
Difficulty Level: ${project.difficultyLevel}
|
| 31 |
+
Technologies: ${project.technologies.join(", ")}
|
| 32 |
+
Estimated Time: ${project.estimatedTime}
|
| 33 |
+
Chosen Difficulty Mode: ${difficultyMode}
|
| 34 |
+
|
| 35 |
+
Milestones:
|
| 36 |
+
${project.milestones
|
| 37 |
+
.sort((a, b) => a.milestoneNumber - b.milestoneNumber)
|
| 38 |
+
.map((m) => `${m.milestoneNumber}. ${m.title}: ${m.description}`)
|
| 39 |
+
.join("\n")}
|
| 40 |
+
|
| 41 |
+
Learning Objectives:
|
| 42 |
+
${project.learningObjectives.map((obj, i) => `${i + 1}. ${obj}`).join("\n")}
|
| 43 |
+
`;
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
// 2. Define mode-specific instructions
|
| 48 |
+
let modeInstruction = "";
|
| 49 |
+
if (difficultyMode === "GUIDED") {
|
| 50 |
+
modeInstruction =
|
| 51 |
+
"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.";
|
| 52 |
+
} else if (difficultyMode === "HARDCORE") {
|
| 53 |
+
modeInstruction =
|
| 54 |
+
"The user is in HARDCORE mode. Be very brief, provide only high-level conceptual hints, and never provide code solutions.";
|
| 55 |
+
} else {
|
| 56 |
+
modeInstruction =
|
| 57 |
+
"The user is in STANDARD mode. Provide balanced guidance, focus on concepts, and only provide code as a last resort.";
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// 3. Build System Prompt
|
| 61 |
+
const systemPrompt = `You are an AI Guide for a project-based learning platform.
|
| 62 |
+
Your goal is to help students learn by providing guidance, not just giving away answers.
|
| 63 |
+
${projectContext}
|
| 64 |
+
${modeInstruction}
|
| 65 |
+
Keep your responses concise and educational. Always respond in markdown format.`;
|
| 66 |
+
|
| 67 |
+
// 4. Fetch chat history
|
| 68 |
+
const history = await prisma.chatMessage.findMany({
|
| 69 |
+
where: { userId, projectId: projectId || null },
|
| 70 |
+
orderBy: { createdAt: "asc" },
|
| 71 |
+
take: 20,
|
| 72 |
+
});
|
| 73 |
+
|
| 74 |
+
const geminiHistory = history.map((msg) => ({
|
| 75 |
+
role: msg.role === "user" ? "user" : "model",
|
| 76 |
+
parts: [{ text: msg.content }],
|
| 77 |
+
}));
|
| 78 |
+
|
| 79 |
+
// 5. Get AI Response
|
| 80 |
+
const aiResponse = await getGeminiResponse(
|
| 81 |
+
`${systemPrompt}\n\nUser Question: ${message}`,
|
| 82 |
+
geminiHistory
|
| 83 |
+
);
|
| 84 |
+
|
| 85 |
+
// 6. Persist Conversation
|
| 86 |
+
await prisma.chatMessage.createMany({
|
| 87 |
+
data: [
|
| 88 |
+
{
|
| 89 |
+
userId,
|
| 90 |
+
projectId: projectId || null,
|
| 91 |
+
role: "user",
|
| 92 |
+
content: message,
|
| 93 |
+
},
|
| 94 |
+
{
|
| 95 |
+
userId,
|
| 96 |
+
projectId: projectId || null,
|
| 97 |
+
role: "assistant",
|
| 98 |
+
content: aiResponse,
|
| 99 |
+
},
|
| 100 |
+
],
|
| 101 |
+
});
|
| 102 |
+
|
| 103 |
+
// 7. Stream response (simulated streaming)
|
| 104 |
+
res.setHeader("Content-Type", "text/plain");
|
| 105 |
+
res.setHeader("Transfer-Encoding", "chunked");
|
| 106 |
+
res.setHeader("Cache-Control", "no-cache");
|
| 107 |
+
res.setHeader("Connection", "keep-alive");
|
| 108 |
+
|
| 109 |
+
const words = aiResponse.split(" ");
|
| 110 |
+
for (let i = 0; i < words.length; i++) {
|
| 111 |
+
res.write(words[i] + " ");
|
| 112 |
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
res.end();
|
| 116 |
+
} catch (error: any) {
|
| 117 |
+
console.error("Chat error:", error);
|
| 118 |
+
if (!res.headersSent) {
|
| 119 |
+
res.status(500).json({ success: false, message: error.message });
|
| 120 |
+
} else {
|
| 121 |
+
res.end();
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
};
|
| 125 |
+
|
| 126 |
+
export const getAllChatMessages = async (req: Request, res: Response) => {
|
| 127 |
+
const userId = (req as any).user?.id;
|
| 128 |
+
|
| 129 |
+
try {
|
| 130 |
+
const messages = await prisma.chatMessage.findMany({
|
| 131 |
+
where: { userId },
|
| 132 |
+
orderBy: { createdAt: "asc" },
|
| 133 |
+
});
|
| 134 |
+
|
| 135 |
+
res.json({ success: true, data: messages });
|
| 136 |
+
} catch (error: any) {
|
| 137 |
+
res.status(500).json({ success: false, message: error.message });
|
| 138 |
+
}
|
| 139 |
+
};
|
| 140 |
+
|
| 141 |
+
export const requestHint = async (req: Request, res: Response) => {
|
| 142 |
+
const userId = (req as any).user?.id;
|
| 143 |
+
const { projectId, milestoneNumber, difficultyMode } = req.body;
|
| 144 |
+
|
| 145 |
+
try {
|
| 146 |
+
const milestone = await prisma.projectMilestone.findUnique({
|
| 147 |
+
where: { projectId_milestoneNumber: { projectId, milestoneNumber } },
|
| 148 |
+
include: { project: true },
|
| 149 |
+
});
|
| 150 |
+
|
| 151 |
+
if (!milestone)
|
| 152 |
+
return res
|
| 153 |
+
.status(404)
|
| 154 |
+
.json({ success: false, message: "Milestone not found" });
|
| 155 |
+
|
| 156 |
+
let modeInstruction = "";
|
| 157 |
+
if (difficultyMode === "GUIDED") {
|
| 158 |
+
modeInstruction =
|
| 159 |
+
"The user is in GUIDED mode. Be very helpful and detailed.";
|
| 160 |
+
} else {
|
| 161 |
+
modeInstruction =
|
| 162 |
+
"The user is in STANDARD mode. Provide a balanced, conceptual hint.";
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
const modeHintsForPrompt = Array.isArray(milestone.hints)
|
| 166 |
+
? milestone.hints
|
| 167 |
+
: (milestone.hints as any)?.[difficultyMode] || [];
|
| 168 |
+
|
| 169 |
+
const prompt = `The user is stuck on Milestone ${milestoneNumber} ("${
|
| 170 |
+
milestone.title
|
| 171 |
+
}") of the project "${milestone.project.title}".
|
| 172 |
+
Milestone description: ${milestone.description}.
|
| 173 |
+
Difficulty Mode: ${difficultyMode}.
|
| 174 |
+
${modeInstruction}
|
| 175 |
+
Existing hints: ${modeHintsForPrompt.join(", ")}.
|
| 176 |
+
Please provide a new, progressive hint that helps them move forward without revealing the full solution.
|
| 177 |
+
Format the response as a single, clear hint string.`;
|
| 178 |
+
|
| 179 |
+
const hint = await getGeminiResponse(prompt);
|
| 180 |
+
|
| 181 |
+
// Persist the hint by mode in the JSON object
|
| 182 |
+
const existingHints = (milestone.hints as any) || {};
|
| 183 |
+
const modeHints = existingHints[difficultyMode] || [];
|
| 184 |
+
modeHints.push(hint);
|
| 185 |
+
|
| 186 |
+
await prisma.projectMilestone.update({
|
| 187 |
+
where: { id: milestone.id },
|
| 188 |
+
data: {
|
| 189 |
+
hints: {
|
| 190 |
+
...existingHints,
|
| 191 |
+
[difficultyMode]: modeHints,
|
| 192 |
+
},
|
| 193 |
+
},
|
| 194 |
+
});
|
| 195 |
+
|
| 196 |
+
res.json({ success: true, data: hint });
|
| 197 |
+
} catch (error: any) {
|
| 198 |
+
res.status(500).json({ success: false, message: error.message });
|
| 199 |
+
}
|
| 200 |
+
};
|
| 201 |
+
|
| 202 |
+
export const getChatHistory = async (req: Request, res: Response) => {
|
| 203 |
+
const { projectId } = req.params;
|
| 204 |
+
const userId = (req as any).user?.id;
|
| 205 |
+
|
| 206 |
+
try {
|
| 207 |
+
const messages = await prisma.chatMessage.findMany({
|
| 208 |
+
where: {
|
| 209 |
+
userId,
|
| 210 |
+
projectId: projectId || null,
|
| 211 |
+
},
|
| 212 |
+
orderBy: { createdAt: "asc" },
|
| 213 |
+
});
|
| 214 |
+
|
| 215 |
+
res.json({ success: true, data: messages });
|
| 216 |
+
} catch (error: any) {
|
| 217 |
+
res.status(500).json({ success: false, message: error.message });
|
| 218 |
+
}
|
| 219 |
+
};
|
controllers/analytics/index.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Request, Response, NextFunction } from "express";
|
| 2 |
+
import prisma from "../../prisma/client";
|
| 3 |
+
|
| 4 |
+
export const getUserAnalytics = async (req: Request, res: Response, next: NextFunction) => {
|
| 5 |
+
const userId = (req as any).user?.id;
|
| 6 |
+
|
| 7 |
+
try {
|
| 8 |
+
const user = await prisma.user.findUnique({
|
| 9 |
+
where: { id: userId },
|
| 10 |
+
select: {
|
| 11 |
+
xp: true,
|
| 12 |
+
streak: true,
|
| 13 |
+
_count: {
|
| 14 |
+
select: {
|
| 15 |
+
projects: true,
|
| 16 |
+
submissions: true,
|
| 17 |
+
achievements: true
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
const projectCompletion = await prisma.userProject.groupBy({
|
| 24 |
+
by: ["status"],
|
| 25 |
+
where: { userId },
|
| 26 |
+
_count: true
|
| 27 |
+
});
|
| 28 |
+
|
| 29 |
+
res.json({
|
| 30 |
+
success: true,
|
| 31 |
+
data: {
|
| 32 |
+
stats: user,
|
| 33 |
+
projectCompletion
|
| 34 |
+
}
|
| 35 |
+
});
|
| 36 |
+
} catch (error: any) {
|
| 37 |
+
next(error);
|
| 38 |
+
}
|
| 39 |
+
};
|
| 40 |
+
|
| 41 |
+
export const getAdminAnalytics = async (req: Request, res: Response, next: NextFunction) => {
|
| 42 |
+
const userId = (req as any).user?.id;
|
| 43 |
+
|
| 44 |
+
try {
|
| 45 |
+
const user = await prisma.user.findUnique({ where: { id: userId } });
|
| 46 |
+
if (user?.role !== "ADMIN") {
|
| 47 |
+
return res.status(403).json({ success: false, message: "Forbidden" });
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
const totalUsers = await prisma.user.count();
|
| 51 |
+
const totalProjects = await prisma.project.count();
|
| 52 |
+
const totalSubmissions = await prisma.submission.count();
|
| 53 |
+
|
| 54 |
+
const popularProjects = await prisma.project.findMany({
|
| 55 |
+
orderBy: { submissionCount: "desc" },
|
| 56 |
+
take: 5,
|
| 57 |
+
select: { title: true, submissionCount: true }
|
| 58 |
+
});
|
| 59 |
+
|
| 60 |
+
res.json({
|
| 61 |
+
success: true,
|
| 62 |
+
data: {
|
| 63 |
+
totalUsers,
|
| 64 |
+
totalProjects,
|
| 65 |
+
totalSubmissions,
|
| 66 |
+
popularProjects
|
| 67 |
+
}
|
| 68 |
+
});
|
| 69 |
+
} catch (error: any) {
|
| 70 |
+
next(error);
|
| 71 |
+
}
|
| 72 |
+
};
|
controllers/auth/index.ts
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Request, Response } from "express";
|
| 2 |
+
import prisma from "../../prisma/client";
|
| 3 |
+
import * as argon2 from "argon2";
|
| 4 |
+
import { sendEmail } from "../../middlewares/email";
|
| 5 |
+
import { generateOTP } from "../../middlewares/generateOTP";
|
| 6 |
+
import jwt from "jsonwebtoken";
|
| 7 |
+
import dotenv from "dotenv";
|
| 8 |
+
import redis from "../../utils/redis";
|
| 9 |
+
import { Difficulty, Role } from "@prisma/client";
|
| 10 |
+
|
| 11 |
+
dotenv.config();
|
| 12 |
+
const JWT_SECRET: string = process.env.JWT_SECRET || "default_secret";
|
| 13 |
+
|
| 14 |
+
export const signUp = async (req: Request, res: Response) => {
|
| 15 |
+
const { firstName, lastName, skillLevel, email, password, role } = req.body;
|
| 16 |
+
try {
|
| 17 |
+
const existingUser = await prisma.user.findUnique({ where: { email } });
|
| 18 |
+
if (existingUser) {
|
| 19 |
+
return res
|
| 20 |
+
.status(400)
|
| 21 |
+
.json({ success: false, message: "Email already registered" });
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
const hashedPassword = await argon2.hash(password);
|
| 25 |
+
const user = await prisma.user.create({
|
| 26 |
+
data: {
|
| 27 |
+
firstName,
|
| 28 |
+
lastName,
|
| 29 |
+
email,
|
| 30 |
+
skillLevel: (skillLevel as Difficulty) || "BEGINNER",
|
| 31 |
+
password: hashedPassword,
|
| 32 |
+
role: (role as Role) || "STUDENT",
|
| 33 |
+
},
|
| 34 |
+
});
|
| 35 |
+
|
| 36 |
+
res.json({
|
| 37 |
+
success: true,
|
| 38 |
+
message: "User registered successfully",
|
| 39 |
+
data: { id: user.id },
|
| 40 |
+
});
|
| 41 |
+
} catch (error: any) {
|
| 42 |
+
res.status(500).json({ success: false, message: error.message });
|
| 43 |
+
}
|
| 44 |
+
};
|
| 45 |
+
|
| 46 |
+
export const login = async (req: Request, res: Response) => {
|
| 47 |
+
const { email, password } = req.body;
|
| 48 |
+
try {
|
| 49 |
+
const user = await prisma.user.findUnique({ where: { email } });
|
| 50 |
+
if (!user || !user.password) {
|
| 51 |
+
return res
|
| 52 |
+
.status(401)
|
| 53 |
+
.json({ success: false, message: "Invalid Email or Password" });
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
const verifyPassword = await argon2.verify(user.password, password);
|
| 57 |
+
if (!verifyPassword) {
|
| 58 |
+
return res
|
| 59 |
+
.status(401)
|
| 60 |
+
.json({ success: false, message: "Invalid Email or Password" });
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
const token = jwt.sign({ id: user.id, role: user.role }, JWT_SECRET, {
|
| 64 |
+
expiresIn: "7d",
|
| 65 |
+
});
|
| 66 |
+
|
| 67 |
+
res.cookie("token", token, {
|
| 68 |
+
httpOnly: true,
|
| 69 |
+
secure: process.env.NODE_ENV === "production",
|
| 70 |
+
sameSite: "strict",
|
| 71 |
+
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
| 72 |
+
});
|
| 73 |
+
|
| 74 |
+
res.json({
|
| 75 |
+
success: true,
|
| 76 |
+
message: "Login successful",
|
| 77 |
+
data: {
|
| 78 |
+
id: user.id,
|
| 79 |
+
firstName: user.firstName,
|
| 80 |
+
lastName: user.lastName,
|
| 81 |
+
email: user.email,
|
| 82 |
+
role: user.role,
|
| 83 |
+
token,
|
| 84 |
+
},
|
| 85 |
+
});
|
| 86 |
+
} catch (error: any) {
|
| 87 |
+
res.status(500).json({ success: false, message: error.message });
|
| 88 |
+
}
|
| 89 |
+
};
|
| 90 |
+
|
| 91 |
+
export const logout = async (req: Request, res: Response) => {
|
| 92 |
+
res.clearCookie("token");
|
| 93 |
+
res.json({ success: true, message: "Logged out successfully" });
|
| 94 |
+
};
|
| 95 |
+
|
| 96 |
+
export const me = async (req: Request, res: Response) => {
|
| 97 |
+
const userId = (req as any).user?.id;
|
| 98 |
+
if (!userId) {
|
| 99 |
+
return res.status(401).json({ success: false, message: "Unauthorized" });
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
try {
|
| 103 |
+
const user = await prisma.user.findUnique({
|
| 104 |
+
where: { id: userId },
|
| 105 |
+
select: {
|
| 106 |
+
id: true,
|
| 107 |
+
email: true,
|
| 108 |
+
firstName: true,
|
| 109 |
+
lastName: true,
|
| 110 |
+
skillLevel: true,
|
| 111 |
+
bio: true,
|
| 112 |
+
portfolioLinks: true,
|
| 113 |
+
xp: true,
|
| 114 |
+
streak: true,
|
| 115 |
+
role: true,
|
| 116 |
+
avatarUrl: true,
|
| 117 |
+
createdAt: true,
|
| 118 |
+
projects: {
|
| 119 |
+
include: {
|
| 120 |
+
project: true,
|
| 121 |
+
},
|
| 122 |
+
},
|
| 123 |
+
achievements: {
|
| 124 |
+
include: {
|
| 125 |
+
achievement: true,
|
| 126 |
+
},
|
| 127 |
+
},
|
| 128 |
+
},
|
| 129 |
+
});
|
| 130 |
+
|
| 131 |
+
if (!user) {
|
| 132 |
+
return res
|
| 133 |
+
.status(404)
|
| 134 |
+
.json({ success: false, message: "User not found" });
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
res.json({ success: true, data: user });
|
| 138 |
+
} catch (error: any) {
|
| 139 |
+
res.status(500).json({ success: false, message: error.message });
|
| 140 |
+
}
|
| 141 |
+
};
|
| 142 |
+
|
| 143 |
+
export const forgetPassword = async (req: Request, res: Response) => {
|
| 144 |
+
const { email } = req.body;
|
| 145 |
+
const appName = "Dev Drill";
|
| 146 |
+
|
| 147 |
+
try {
|
| 148 |
+
const user = await prisma.user.findUnique({ where: { email } });
|
| 149 |
+
if (!user) {
|
| 150 |
+
return res
|
| 151 |
+
.status(404)
|
| 152 |
+
.json({ success: false, message: "Account is not registered" });
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
const { otp, otpDate } = generateOTP();
|
| 156 |
+
// Store OTP in Redis with 1 hour expiry
|
| 157 |
+
await redis.setex(`otp:${email}`, 3600, otp.toString());
|
| 158 |
+
|
| 159 |
+
const htmlMessage = `
|
| 160 |
+
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
| 161 |
+
<div style="background: #f0f7ff; padding: 30px; border-radius: 10px;">
|
| 162 |
+
<h1 style="color: #1a237e; margin-bottom: 25px;">${appName}</h1>
|
| 163 |
+
<p style="font-size: 16px; color: #333;">
|
| 164 |
+
Hello ${user.lastName},<br><br>
|
| 165 |
+
We received a request to reset your password for your ${appName} account.
|
| 166 |
+
</p>
|
| 167 |
+
<div style="background: #fff; padding: 20px; border-radius: 5px; margin: 20px 0; text-align: center;">
|
| 168 |
+
<p style="margin: 0; font-weight: bold; font-size: 24px; letter-spacing: 2px;">${otp}</p>
|
| 169 |
+
</div>
|
| 170 |
+
<p style="font-size: 14px; color: #666;">
|
| 171 |
+
This verification code will expire in 1 hour.<br>
|
| 172 |
+
If you didn't request this password reset, please ignore this email.
|
| 173 |
+
</p>
|
| 174 |
+
</div>
|
| 175 |
+
<p style="font-size: 12px; color: #999; text-align: center; margin-top: 20px;">
|
| 176 |
+
© ${new Date().getFullYear()} ${appName}. All rights reserved.
|
| 177 |
+
</p>
|
| 178 |
+
</div>
|
| 179 |
+
`;
|
| 180 |
+
|
| 181 |
+
const textMessage =
|
| 182 |
+
`${appName} Password Reset\n\n` +
|
| 183 |
+
`Hello ${user.lastName},\n\n` +
|
| 184 |
+
`Use this OTP to reset your password: ${otp}\n` +
|
| 185 |
+
`This code expires in 1 hour.`;
|
| 186 |
+
|
| 187 |
+
sendEmail(
|
| 188 |
+
user.email,
|
| 189 |
+
`Password Reset Request - ${appName}`,
|
| 190 |
+
htmlMessage,
|
| 191 |
+
textMessage
|
| 192 |
+
);
|
| 193 |
+
|
| 194 |
+
res.json({
|
| 195 |
+
success: true,
|
| 196 |
+
message: `An OTP has been sent to your mail. Check your mail for your verification code`,
|
| 197 |
+
});
|
| 198 |
+
} catch (error: any) {
|
| 199 |
+
res.status(500).json({ success: false, message: error.message });
|
| 200 |
+
}
|
| 201 |
+
};
|
| 202 |
+
|
| 203 |
+
export const verifyOTP = async (req: Request, res: Response) => {
|
| 204 |
+
const { email, otp: userOtp } = req.body;
|
| 205 |
+
|
| 206 |
+
try {
|
| 207 |
+
const storedOtp = await redis.get(`otp:${email}`);
|
| 208 |
+
if (!storedOtp) {
|
| 209 |
+
return res
|
| 210 |
+
.status(400)
|
| 211 |
+
.json({ success: false, message: "OTP expired or not found" });
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
if (storedOtp !== userOtp.toString()) {
|
| 215 |
+
return res
|
| 216 |
+
.status(400)
|
| 217 |
+
.json({ success: false, message: "Invalid Verification code!" });
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
// OTP verified, can remove it now
|
| 221 |
+
await redis.del(`otp:${email}`);
|
| 222 |
+
// Optionally set a flag in Redis that OTP was verified for this email to allow password reset
|
| 223 |
+
await redis.setex(`otp_verified:${email}`, 600, "true");
|
| 224 |
+
|
| 225 |
+
res.json({ success: true, message: "Verification successful" });
|
| 226 |
+
} catch (error: any) {
|
| 227 |
+
res.status(500).json({ success: false, message: error.message });
|
| 228 |
+
}
|
| 229 |
+
};
|
| 230 |
+
|
| 231 |
+
export const resetPassword = async (req: Request, res: Response) => {
|
| 232 |
+
const { email, newPassword } = req.body;
|
| 233 |
+
|
| 234 |
+
try {
|
| 235 |
+
const isVerified = await redis.get(`otp_verified:${email}`);
|
| 236 |
+
if (!isVerified) {
|
| 237 |
+
return res
|
| 238 |
+
.status(400)
|
| 239 |
+
.json({ success: false, message: "OTP not verified" });
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
const user = await prisma.user.findUnique({ where: { email } });
|
| 243 |
+
if (!user)
|
| 244 |
+
return res
|
| 245 |
+
.status(404)
|
| 246 |
+
.json({ success: false, message: "User not found" });
|
| 247 |
+
|
| 248 |
+
if (user.password) {
|
| 249 |
+
const verifyPassword = await argon2.verify(user.password, newPassword);
|
| 250 |
+
if (verifyPassword) {
|
| 251 |
+
return res
|
| 252 |
+
.status(400)
|
| 253 |
+
.json({ success: false, message: "You entered your old password" });
|
| 254 |
+
}
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
const newPass = await argon2.hash(newPassword);
|
| 258 |
+
await prisma.user.update({
|
| 259 |
+
where: { email },
|
| 260 |
+
data: { password: newPass },
|
| 261 |
+
});
|
| 262 |
+
|
| 263 |
+
await redis.del(`otp_verified:${email}`);
|
| 264 |
+
|
| 265 |
+
res.json({ success: true, message: "Password successfully reset" });
|
| 266 |
+
} catch (error: any) {
|
| 267 |
+
res.status(500).json({ success: false, message: error.message });
|
| 268 |
+
}
|
| 269 |
+
};
|
| 270 |
+
|
| 271 |
+
export const getUser = async (req: Request, res: Response) => {
|
| 272 |
+
const { id } = req.params;
|
| 273 |
+
try {
|
| 274 |
+
const user = await prisma.user.findUnique({
|
| 275 |
+
where: { id },
|
| 276 |
+
select: {
|
| 277 |
+
id: true,
|
| 278 |
+
email: true,
|
| 279 |
+
firstName: true,
|
| 280 |
+
lastName: true,
|
| 281 |
+
skillLevel: true,
|
| 282 |
+
bio: true,
|
| 283 |
+
portfolioLinks: true,
|
| 284 |
+
xp: true,
|
| 285 |
+
streak: true,
|
| 286 |
+
role: true,
|
| 287 |
+
avatarUrl: true,
|
| 288 |
+
createdAt: true,
|
| 289 |
+
projects: {
|
| 290 |
+
include: {
|
| 291 |
+
project: true,
|
| 292 |
+
},
|
| 293 |
+
},
|
| 294 |
+
achievements: {
|
| 295 |
+
include: {
|
| 296 |
+
achievement: true,
|
| 297 |
+
},
|
| 298 |
+
},
|
| 299 |
+
},
|
| 300 |
+
});
|
| 301 |
+
if (!user)
|
| 302 |
+
return res
|
| 303 |
+
.status(404)
|
| 304 |
+
.json({ success: false, message: "User not found" });
|
| 305 |
+
res.json({ success: true, data: user });
|
| 306 |
+
} catch (error: any) {
|
| 307 |
+
res.status(500).json({ success: false, message: error.message });
|
| 308 |
+
}
|
| 309 |
+
};
|
| 310 |
+
|
| 311 |
+
export const updateUser = async (req: Request, res: Response) => {
|
| 312 |
+
const { id } = req.params;
|
| 313 |
+
const authenticatedUserId = (req as any).user?.id;
|
| 314 |
+
|
| 315 |
+
if (authenticatedUserId !== id) {
|
| 316 |
+
return res.status(403).json({ success: false, message: "Forbidden" });
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
let {
|
| 320 |
+
firstName,
|
| 321 |
+
lastName,
|
| 322 |
+
email,
|
| 323 |
+
password,
|
| 324 |
+
bio,
|
| 325 |
+
skillLevel,
|
| 326 |
+
portfolioLinks,
|
| 327 |
+
avatarUrl,
|
| 328 |
+
} = req.body;
|
| 329 |
+
try {
|
| 330 |
+
const user = await prisma.user.findUnique({ where: { id } });
|
| 331 |
+
if (!user) {
|
| 332 |
+
return res
|
| 333 |
+
.status(404)
|
| 334 |
+
.json({ success: false, message: "User not found" });
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
const data: any = {
|
| 338 |
+
firstName,
|
| 339 |
+
lastName,
|
| 340 |
+
bio,
|
| 341 |
+
skillLevel,
|
| 342 |
+
portfolioLinks,
|
| 343 |
+
avatarUrl,
|
| 344 |
+
};
|
| 345 |
+
|
| 346 |
+
if (email && email !== user.email) {
|
| 347 |
+
const existingUser = await prisma.user.findUnique({ where: { email } });
|
| 348 |
+
if (existingUser) {
|
| 349 |
+
return res
|
| 350 |
+
.status(400)
|
| 351 |
+
.json({ success: false, message: "Email already in use" });
|
| 352 |
+
}
|
| 353 |
+
data.email = email;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
if (password) {
|
| 357 |
+
data.password = await argon2.hash(password);
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
const updatedUser = await prisma.user.update({
|
| 361 |
+
where: { id },
|
| 362 |
+
data,
|
| 363 |
+
select: {
|
| 364 |
+
id: true,
|
| 365 |
+
email: true,
|
| 366 |
+
firstName: true,
|
| 367 |
+
lastName: true,
|
| 368 |
+
skillLevel: true,
|
| 369 |
+
bio: true,
|
| 370 |
+
portfolioLinks: true,
|
| 371 |
+
xp: true,
|
| 372 |
+
streak: true,
|
| 373 |
+
role: true,
|
| 374 |
+
avatarUrl: true,
|
| 375 |
+
createdAt: true,
|
| 376 |
+
projects: {
|
| 377 |
+
include: {
|
| 378 |
+
project: true,
|
| 379 |
+
},
|
| 380 |
+
},
|
| 381 |
+
achievements: {
|
| 382 |
+
include: {
|
| 383 |
+
achievement: true,
|
| 384 |
+
},
|
| 385 |
+
},
|
| 386 |
+
},
|
| 387 |
+
});
|
| 388 |
+
|
| 389 |
+
res.json({
|
| 390 |
+
success: true,
|
| 391 |
+
data: updatedUser,
|
| 392 |
+
});
|
| 393 |
+
} catch (error: any) {
|
| 394 |
+
res.status(500).json({ success: false, message: error.message });
|
| 395 |
+
}
|
| 396 |
+
};
|
| 397 |
+
|
| 398 |
+
export const googleAuthCallback = async (req: Request, res: Response) => {
|
| 399 |
+
try {
|
| 400 |
+
const user = req.user as any;
|
| 401 |
+
if (!user) {
|
| 402 |
+
return res.redirect(
|
| 403 |
+
`${process.env.CLIENT_URL}/auth/signin?error=auth_failed`
|
| 404 |
+
);
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
const token = jwt.sign({ id: user.id, role: user.role }, JWT_SECRET, {
|
| 408 |
+
expiresIn: "7d",
|
| 409 |
+
});
|
| 410 |
+
|
| 411 |
+
res.cookie("token", token, {
|
| 412 |
+
httpOnly: true,
|
| 413 |
+
secure: process.env.NODE_ENV === "production",
|
| 414 |
+
sameSite: "lax",
|
| 415 |
+
maxAge: 7 * 24 * 60 * 60 * 1000,
|
| 416 |
+
});
|
| 417 |
+
|
| 418 |
+
res.redirect(`${process.env.CLIENT_URL}/dashboard`);
|
| 419 |
+
} catch (error: any) {
|
| 420 |
+
res.redirect(`${process.env.CLIENT_URL}/auth/signin?error=server_error`);
|
| 421 |
+
}
|
| 422 |
+
};
|
| 423 |
+
|
| 424 |
+
export const githubAuthCallback = async (req: Request, res: Response) => {
|
| 425 |
+
try {
|
| 426 |
+
const user = req.user as any;
|
| 427 |
+
if (!user) {
|
| 428 |
+
return res.redirect(
|
| 429 |
+
`${process.env.CLIENT_URL}/auth/signin?error=auth_failed`
|
| 430 |
+
);
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
const token = jwt.sign({ id: user.id, role: user.role }, JWT_SECRET, {
|
| 434 |
+
expiresIn: "7d",
|
| 435 |
+
});
|
| 436 |
+
|
| 437 |
+
res.cookie("token", token, {
|
| 438 |
+
httpOnly: true,
|
| 439 |
+
secure: process.env.NODE_ENV === "production",
|
| 440 |
+
sameSite: "lax",
|
| 441 |
+
maxAge: 7 * 24 * 60 * 60 * 1000,
|
| 442 |
+
});
|
| 443 |
+
|
| 444 |
+
res.redirect(`${process.env.CLIENT_URL}/dashboard`);
|
| 445 |
+
} catch (error: any) {
|
| 446 |
+
res.redirect(`${process.env.CLIENT_URL}/auth/signin?error=server_error`);
|
| 447 |
+
}
|
| 448 |
+
};
|
controllers/code-review/index.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Request, Response } from "express";
|
| 2 |
+
import prisma from "../../prisma/client";
|
| 3 |
+
import { getGeminiResponse } from "../../utils/gemini";
|
| 4 |
+
import axios from "axios";
|
| 5 |
+
|
| 6 |
+
export const analyzeCode = async (req: Request, res: Response) => {
|
| 7 |
+
const userId = (req as any).user?.id;
|
| 8 |
+
const { repoUrl, projectId } = req.body;
|
| 9 |
+
|
| 10 |
+
try {
|
| 11 |
+
// 1. Fetch code from GitHub (simplified: fetch README or a few files for demo)
|
| 12 |
+
// In a real app, you'd use a GitHub App or User Token to clone or fetch via API
|
| 13 |
+
const repoPath = repoUrl.replace("https://github.com/", "");
|
| 14 |
+
const [owner, repo] = repoPath.split("/");
|
| 15 |
+
|
| 16 |
+
// Fetch repo info as a placeholder for analysis
|
| 17 |
+
const githubApiUrl = `https://api.github.com/repos/${owner}/${repo}`;
|
| 18 |
+
const repoInfo = await axios.get(githubApiUrl);
|
| 19 |
+
|
| 20 |
+
// 2. Automated analysis (simulated)
|
| 21 |
+
const metrics = {
|
| 22 |
+
complexity: "Medium",
|
| 23 |
+
securityIssues: 0,
|
| 24 |
+
lintErrors: 0
|
| 25 |
+
};
|
| 26 |
+
|
| 27 |
+
// 3. AI-powered feedback
|
| 28 |
+
const prompt = `Please review this GitHub repository: ${repoUrl}.
|
| 29 |
+
Owner: ${owner}
|
| 30 |
+
Repo: ${repo}
|
| 31 |
+
Description: ${repoInfo.data.description}
|
| 32 |
+
Provide constructive feedback on the project structure and suggest improvements for a student.`;
|
| 33 |
+
|
| 34 |
+
const feedback = await getGeminiResponse(prompt);
|
| 35 |
+
|
| 36 |
+
// 4. Store review results
|
| 37 |
+
const submission = await prisma.submission.create({
|
| 38 |
+
data: {
|
| 39 |
+
userId,
|
| 40 |
+
projectId,
|
| 41 |
+
repoUrl,
|
| 42 |
+
feedback,
|
| 43 |
+
score: 85 // Simulated score
|
| 44 |
+
}
|
| 45 |
+
});
|
| 46 |
+
|
| 47 |
+
res.json({ success: true, data: submission });
|
| 48 |
+
} catch (error: any) {
|
| 49 |
+
res.status(500).json({ success: false, message: error.message });
|
| 50 |
+
}
|
| 51 |
+
};
|
| 52 |
+
|
| 53 |
+
export const getSubmissionReview = async (req: Request, res: Response) => {
|
| 54 |
+
const { id } = req.params;
|
| 55 |
+
|
| 56 |
+
try {
|
| 57 |
+
const submission = await prisma.submission.findUnique({
|
| 58 |
+
where: { id },
|
| 59 |
+
include: {
|
| 60 |
+
project: { select: { title: true } },
|
| 61 |
+
user: { select: { firstName: true, lastName: true } }
|
| 62 |
+
}
|
| 63 |
+
});
|
| 64 |
+
|
| 65 |
+
if (!submission) return res.status(404).json({ success: false, message: "Submission not found" });
|
| 66 |
+
|
| 67 |
+
res.json({ success: true, data: submission });
|
| 68 |
+
} catch (error: any) {
|
| 69 |
+
res.status(500).json({ success: false, message: error.message });
|
| 70 |
+
}
|
| 71 |
+
};
|
controllers/community/index.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Request, Response, NextFunction } from "express";
|
| 2 |
+
import prisma from "../../prisma/client";
|
| 3 |
+
|
| 4 |
+
export const getPublicSubmissions = async (req: Request, res: Response, next: NextFunction) => {
|
| 5 |
+
try {
|
| 6 |
+
const submissions = await prisma.submission.findMany({
|
| 7 |
+
where: { isPublic: true },
|
| 8 |
+
include: {
|
| 9 |
+
user: {
|
| 10 |
+
select: {
|
| 11 |
+
id: true,
|
| 12 |
+
firstName: true,
|
| 13 |
+
lastName: true,
|
| 14 |
+
avatarUrl: true,
|
| 15 |
+
},
|
| 16 |
+
},
|
| 17 |
+
project: {
|
| 18 |
+
select: {
|
| 19 |
+
id: true,
|
| 20 |
+
title: true,
|
| 21 |
+
},
|
| 22 |
+
},
|
| 23 |
+
_count: {
|
| 24 |
+
select: {
|
| 25 |
+
comments: true,
|
| 26 |
+
votes: true,
|
| 27 |
+
},
|
| 28 |
+
},
|
| 29 |
+
},
|
| 30 |
+
orderBy: { createdAt: "desc" },
|
| 31 |
+
});
|
| 32 |
+
|
| 33 |
+
res.json({ success: true, data: submissions });
|
| 34 |
+
} catch (error: any) {
|
| 35 |
+
next(error);
|
| 36 |
+
}
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
export const commentOnSubmission = async (req: Request, res: Response, next: NextFunction) => {
|
| 40 |
+
const { id: submissionId } = req.params;
|
| 41 |
+
const { content } = req.body;
|
| 42 |
+
const userId = (req as any).user?.id;
|
| 43 |
+
|
| 44 |
+
try {
|
| 45 |
+
const comment = await prisma.comment.create({
|
| 46 |
+
data: {
|
| 47 |
+
content,
|
| 48 |
+
userId,
|
| 49 |
+
submissionId,
|
| 50 |
+
},
|
| 51 |
+
include: {
|
| 52 |
+
user: {
|
| 53 |
+
select: {
|
| 54 |
+
id: true,
|
| 55 |
+
firstName: true,
|
| 56 |
+
lastName: true,
|
| 57 |
+
avatarUrl: true,
|
| 58 |
+
},
|
| 59 |
+
},
|
| 60 |
+
},
|
| 61 |
+
});
|
| 62 |
+
|
| 63 |
+
res.status(201).json({ success: true, data: comment });
|
| 64 |
+
} catch (error: any) {
|
| 65 |
+
next(error);
|
| 66 |
+
}
|
| 67 |
+
};
|
| 68 |
+
|
| 69 |
+
export const voteOnSubmission = async (req: Request, res: Response, next: NextFunction) => {
|
| 70 |
+
const { id: submissionId } = req.params;
|
| 71 |
+
const { value } = req.body; // 1 for upvote, -1 for downvote
|
| 72 |
+
const userId = (req as any).user?.id;
|
| 73 |
+
|
| 74 |
+
try {
|
| 75 |
+
const vote = await prisma.vote.upsert({
|
| 76 |
+
where: {
|
| 77 |
+
userId_submissionId: {
|
| 78 |
+
userId,
|
| 79 |
+
submissionId,
|
| 80 |
+
},
|
| 81 |
+
},
|
| 82 |
+
update: { value },
|
| 83 |
+
create: {
|
| 84 |
+
value,
|
| 85 |
+
userId,
|
| 86 |
+
submissionId,
|
| 87 |
+
},
|
| 88 |
+
});
|
| 89 |
+
|
| 90 |
+
res.json({ success: true, data: vote });
|
| 91 |
+
} catch (error: any) {
|
| 92 |
+
next(error);
|
| 93 |
+
}
|
| 94 |
+
};
|
| 95 |
+
|
| 96 |
+
export const getForumThreads = async (req: Request, res: Response, next: NextFunction) => {
|
| 97 |
+
const { projectId } = req.query;
|
| 98 |
+
|
| 99 |
+
try {
|
| 100 |
+
const threads = await prisma.forumThread.findMany({
|
| 101 |
+
where: projectId ? { projectId: String(projectId) } : {},
|
| 102 |
+
include: {
|
| 103 |
+
user: {
|
| 104 |
+
select: {
|
| 105 |
+
id: true,
|
| 106 |
+
firstName: true,
|
| 107 |
+
lastName: true,
|
| 108 |
+
avatarUrl: true,
|
| 109 |
+
},
|
| 110 |
+
},
|
| 111 |
+
_count: {
|
| 112 |
+
select: { posts: true },
|
| 113 |
+
},
|
| 114 |
+
},
|
| 115 |
+
orderBy: { createdAt: "desc" },
|
| 116 |
+
});
|
| 117 |
+
|
| 118 |
+
res.json({ success: true, data: threads });
|
| 119 |
+
} catch (error: any) {
|
| 120 |
+
next(error);
|
| 121 |
+
}
|
| 122 |
+
};
|
| 123 |
+
|
| 124 |
+
export const createForumThread = async (req: Request, res: Response, next: NextFunction) => {
|
| 125 |
+
const { title, content, projectId } = req.body;
|
| 126 |
+
const userId = (req as any).user?.id;
|
| 127 |
+
|
| 128 |
+
try {
|
| 129 |
+
const thread = await prisma.forumThread.create({
|
| 130 |
+
data: {
|
| 131 |
+
title,
|
| 132 |
+
content,
|
| 133 |
+
userId,
|
| 134 |
+
projectId: projectId || null,
|
| 135 |
+
},
|
| 136 |
+
include: {
|
| 137 |
+
user: {
|
| 138 |
+
select: {
|
| 139 |
+
id: true,
|
| 140 |
+
firstName: true,
|
| 141 |
+
lastName: true,
|
| 142 |
+
avatarUrl: true,
|
| 143 |
+
},
|
| 144 |
+
},
|
| 145 |
+
},
|
| 146 |
+
});
|
| 147 |
+
|
| 148 |
+
res.status(201).json({ success: true, data: thread });
|
| 149 |
+
} catch (error: any) {
|
| 150 |
+
next(error);
|
| 151 |
+
}
|
| 152 |
+
};
|
controllers/events/index.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Request, Response } from "express";
|
| 2 |
+
import prisma from "../../prisma/client";
|
| 3 |
+
import { EventStatus } from "@prisma/client";
|
| 4 |
+
|
| 5 |
+
export const createEvent = async (req: Request, res: Response) => {
|
| 6 |
+
const {
|
| 7 |
+
title,
|
| 8 |
+
description,
|
| 9 |
+
type,
|
| 10 |
+
startDate,
|
| 11 |
+
endDate,
|
| 12 |
+
prizePool,
|
| 13 |
+
maxParticipants,
|
| 14 |
+
bannerUrl,
|
| 15 |
+
} = req.body;
|
| 16 |
+
|
| 17 |
+
try {
|
| 18 |
+
const event = await prisma.event.create({
|
| 19 |
+
data: {
|
| 20 |
+
title,
|
| 21 |
+
description,
|
| 22 |
+
type,
|
| 23 |
+
startDate: new Date(startDate),
|
| 24 |
+
endDate: new Date(endDate),
|
| 25 |
+
prizePool,
|
| 26 |
+
maxParticipants,
|
| 27 |
+
bannerUrl,
|
| 28 |
+
},
|
| 29 |
+
});
|
| 30 |
+
|
| 31 |
+
res.json({ success: true, data: event });
|
| 32 |
+
} catch (error: any) {
|
| 33 |
+
res.status(500).json({ success: false, message: error.message });
|
| 34 |
+
}
|
| 35 |
+
};
|
| 36 |
+
|
| 37 |
+
export const getEvents = async (req: Request, res: Response) => {
|
| 38 |
+
const { status, type, page = 1, limit = 10 } = req.query;
|
| 39 |
+
|
| 40 |
+
try {
|
| 41 |
+
const where: any = {};
|
| 42 |
+
if (status) where.status = status as EventStatus;
|
| 43 |
+
if (type) where.type = type;
|
| 44 |
+
|
| 45 |
+
const events = await prisma.event.findMany({
|
| 46 |
+
where,
|
| 47 |
+
include: {
|
| 48 |
+
_count: {
|
| 49 |
+
select: { participants: true },
|
| 50 |
+
},
|
| 51 |
+
},
|
| 52 |
+
orderBy: { startDate: "desc" },
|
| 53 |
+
take: Number(limit),
|
| 54 |
+
skip: (Number(page) - 1) * Number(limit),
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
+
res.json({ success: true, data: events });
|
| 58 |
+
} catch (error: any) {
|
| 59 |
+
res.status(500).json({ success: false, message: error.message });
|
| 60 |
+
}
|
| 61 |
+
};
|
| 62 |
+
|
| 63 |
+
export const getEvent = async (req: Request, res: Response) => {
|
| 64 |
+
const { id } = req.params;
|
| 65 |
+
|
| 66 |
+
try {
|
| 67 |
+
const event = await prisma.event.findUnique({
|
| 68 |
+
where: { id },
|
| 69 |
+
include: {
|
| 70 |
+
participants: {
|
| 71 |
+
include: {
|
| 72 |
+
user: {
|
| 73 |
+
select: {
|
| 74 |
+
id: true,
|
| 75 |
+
firstName: true,
|
| 76 |
+
lastName: true,
|
| 77 |
+
avatarUrl: true,
|
| 78 |
+
},
|
| 79 |
+
},
|
| 80 |
+
},
|
| 81 |
+
},
|
| 82 |
+
leaderboard: {
|
| 83 |
+
include: {
|
| 84 |
+
user: {
|
| 85 |
+
select: {
|
| 86 |
+
id: true,
|
| 87 |
+
firstName: true,
|
| 88 |
+
lastName: true,
|
| 89 |
+
avatarUrl: true,
|
| 90 |
+
},
|
| 91 |
+
},
|
| 92 |
+
},
|
| 93 |
+
orderBy: { score: "desc" },
|
| 94 |
+
take: 100,
|
| 95 |
+
},
|
| 96 |
+
},
|
| 97 |
+
});
|
| 98 |
+
|
| 99 |
+
if (!event) {
|
| 100 |
+
return res
|
| 101 |
+
.status(404)
|
| 102 |
+
.json({ success: false, message: "Event not found" });
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
res.json({ success: true, data: event });
|
| 106 |
+
} catch (error: any) {
|
| 107 |
+
res.status(500).json({ success: false, message: error.message });
|
| 108 |
+
}
|
| 109 |
+
};
|
| 110 |
+
|
| 111 |
+
export const joinEvent = async (req: Request, res: Response) => {
|
| 112 |
+
const userId = (req as any).user?.id;
|
| 113 |
+
const { eventId } = req.params;
|
| 114 |
+
|
| 115 |
+
try {
|
| 116 |
+
const event = await prisma.event.findUnique({
|
| 117 |
+
where: { id: eventId },
|
| 118 |
+
include: { _count: { select: { participants: true } } },
|
| 119 |
+
});
|
| 120 |
+
|
| 121 |
+
if (!event) {
|
| 122 |
+
return res
|
| 123 |
+
.status(404)
|
| 124 |
+
.json({ success: false, message: "Event not found" });
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
if (
|
| 128 |
+
event.maxParticipants &&
|
| 129 |
+
event._count.participants >= event.maxParticipants
|
| 130 |
+
) {
|
| 131 |
+
return res.status(400).json({ success: false, message: "Event is full" });
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
const participant = await prisma.eventParticipant.create({
|
| 135 |
+
data: { eventId, userId },
|
| 136 |
+
});
|
| 137 |
+
|
| 138 |
+
await prisma.eventLeaderboard.create({
|
| 139 |
+
data: { eventId, userId, score: 0 },
|
| 140 |
+
});
|
| 141 |
+
|
| 142 |
+
res.json({ success: true, data: participant });
|
| 143 |
+
} catch (error: any) {
|
| 144 |
+
res.status(500).json({ success: false, message: error.message });
|
| 145 |
+
}
|
| 146 |
+
};
|
| 147 |
+
|
| 148 |
+
export const leaveEvent = async (req: Request, res: Response) => {
|
| 149 |
+
const userId = (req as any).user?.id;
|
| 150 |
+
const { eventId } = req.params;
|
| 151 |
+
|
| 152 |
+
try {
|
| 153 |
+
await prisma.eventParticipant.delete({
|
| 154 |
+
where: { eventId_userId: { eventId, userId } },
|
| 155 |
+
});
|
| 156 |
+
|
| 157 |
+
await prisma.eventLeaderboard.delete({
|
| 158 |
+
where: { eventId_userId: { eventId, userId } },
|
| 159 |
+
});
|
| 160 |
+
|
| 161 |
+
res.json({ success: true, message: "Left event successfully" });
|
| 162 |
+
} catch (error: any) {
|
| 163 |
+
res.status(500).json({ success: false, message: error.message });
|
| 164 |
+
}
|
| 165 |
+
};
|
| 166 |
+
|
| 167 |
+
export const updateEventScore = async (req: Request, res: Response) => {
|
| 168 |
+
const { eventId, userId } = req.params;
|
| 169 |
+
const { score } = req.body;
|
| 170 |
+
|
| 171 |
+
try {
|
| 172 |
+
const leaderboard = await prisma.eventLeaderboard.update({
|
| 173 |
+
where: { eventId_userId: { eventId, userId } },
|
| 174 |
+
data: { score },
|
| 175 |
+
});
|
| 176 |
+
|
| 177 |
+
const allScores = await prisma.eventLeaderboard.findMany({
|
| 178 |
+
where: { eventId },
|
| 179 |
+
orderBy: { score: "desc" },
|
| 180 |
+
});
|
| 181 |
+
|
| 182 |
+
for (let i = 0; i < allScores.length; i++) {
|
| 183 |
+
await prisma.eventLeaderboard.update({
|
| 184 |
+
where: { id: allScores[i].id },
|
| 185 |
+
data: { rank: i + 1 },
|
| 186 |
+
});
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
res.json({ success: true, data: leaderboard });
|
| 190 |
+
} catch (error: any) {
|
| 191 |
+
res.status(500).json({ success: false, message: error.message });
|
| 192 |
+
}
|
| 193 |
+
};
|
| 194 |
+
|
| 195 |
+
export const getLeaderboard = async (req: Request, res: Response) => {
|
| 196 |
+
const { eventId } = req.params;
|
| 197 |
+
const { limit = 100 } = req.query;
|
| 198 |
+
|
| 199 |
+
try {
|
| 200 |
+
const leaderboard = await prisma.eventLeaderboard.findMany({
|
| 201 |
+
where: { eventId },
|
| 202 |
+
include: {
|
| 203 |
+
user: {
|
| 204 |
+
select: {
|
| 205 |
+
id: true,
|
| 206 |
+
firstName: true,
|
| 207 |
+
lastName: true,
|
| 208 |
+
avatarUrl: true,
|
| 209 |
+
xp: true,
|
| 210 |
+
},
|
| 211 |
+
},
|
| 212 |
+
},
|
| 213 |
+
orderBy: { score: "desc" },
|
| 214 |
+
take: Number(limit),
|
| 215 |
+
});
|
| 216 |
+
|
| 217 |
+
res.json({ success: true, data: leaderboard });
|
| 218 |
+
} catch (error: any) {
|
| 219 |
+
res.status(500).json({ success: false, message: error.message });
|
| 220 |
+
}
|
| 221 |
+
};
|
controllers/gamification/index.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Request, Response, NextFunction } from "express";
|
| 2 |
+
import prisma from "../../prisma/client";
|
| 3 |
+
import redis from "../../utils/redis";
|
| 4 |
+
|
| 5 |
+
const LEADERBOARD_CACHE_KEY = "leaderboard:top20";
|
| 6 |
+
const CACHE_TTL = 3600; // 1 hour
|
| 7 |
+
|
| 8 |
+
export const getLeaderboard = async (req: Request, res: Response, next: NextFunction) => {
|
| 9 |
+
try {
|
| 10 |
+
// Try to get from cache
|
| 11 |
+
const cachedLeaderboard = await redis.get(LEADERBOARD_CACHE_KEY);
|
| 12 |
+
if (cachedLeaderboard) {
|
| 13 |
+
return res.json({ success: true, data: JSON.parse(cachedLeaderboard), cached: true });
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
const leaderboard = await prisma.user.findMany({
|
| 17 |
+
select: {
|
| 18 |
+
id: true,
|
| 19 |
+
firstName: true,
|
| 20 |
+
lastName: true,
|
| 21 |
+
avatarUrl: true,
|
| 22 |
+
xp: true,
|
| 23 |
+
streak: true
|
| 24 |
+
},
|
| 25 |
+
orderBy: { xp: "desc" },
|
| 26 |
+
take: 20
|
| 27 |
+
});
|
| 28 |
+
|
| 29 |
+
// Save to cache
|
| 30 |
+
await redis.setex(LEADERBOARD_CACHE_KEY, CACHE_TTL, JSON.stringify(leaderboard));
|
| 31 |
+
|
| 32 |
+
res.json({ success: true, data: leaderboard });
|
| 33 |
+
} catch (error: any) {
|
| 34 |
+
next(error);
|
| 35 |
+
}
|
| 36 |
+
};
|
| 37 |
+
|
| 38 |
+
export const getUserAchievements = async (req: Request, res: Response, next: NextFunction) => {
|
| 39 |
+
const userId = (req as any).user?.id;
|
| 40 |
+
|
| 41 |
+
try {
|
| 42 |
+
const achievements = await prisma.userAchievement.findMany({
|
| 43 |
+
where: { userId },
|
| 44 |
+
include: {
|
| 45 |
+
achievement: true
|
| 46 |
+
}
|
| 47 |
+
});
|
| 48 |
+
|
| 49 |
+
res.json({ success: true, data: achievements });
|
| 50 |
+
} catch (error: any) {
|
| 51 |
+
next(error);
|
| 52 |
+
}
|
| 53 |
+
};
|
| 54 |
+
|
| 55 |
+
export const awardXP = async (userId: string, amount: number) => {
|
| 56 |
+
try {
|
| 57 |
+
await prisma.user.update({
|
| 58 |
+
where: { id: userId },
|
| 59 |
+
data: {
|
| 60 |
+
xp: { increment: amount }
|
| 61 |
+
}
|
| 62 |
+
});
|
| 63 |
+
|
| 64 |
+
// Invalidate leaderboard cache when XP changes
|
| 65 |
+
await redis.del(LEADERBOARD_CACHE_KEY);
|
| 66 |
+
} catch (error) {
|
| 67 |
+
console.error("Error awarding XP:", error);
|
| 68 |
+
}
|
| 69 |
+
};
|
controllers/github/index.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Request, Response } from "express";
|
| 2 |
+
import prisma from "../../prisma/client";
|
| 3 |
+
import axios from "axios";
|
| 4 |
+
|
| 5 |
+
export const linkRepository = async (req: Request, res: Response) => {
|
| 6 |
+
const userId = (req as any).user?.id;
|
| 7 |
+
const { projectId, repoUrl } = req.body;
|
| 8 |
+
|
| 9 |
+
try {
|
| 10 |
+
if (!repoUrl.includes("github.com")) {
|
| 11 |
+
return res.status(400).json({
|
| 12 |
+
success: false,
|
| 13 |
+
message: "Invalid GitHub repository URL",
|
| 14 |
+
});
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
// Extract owner and repo from URL
|
| 18 |
+
const repoPath = repoUrl
|
| 19 |
+
.replace("https://github.com/", "")
|
| 20 |
+
.replace(".git", "");
|
| 21 |
+
const [owner, repo] = repoPath.split("/");
|
| 22 |
+
|
| 23 |
+
// Verify repository exists (optional - requires GitHub token)
|
| 24 |
+
try {
|
| 25 |
+
const githubApiUrl = `https://api.github.com/repos/${owner}/${repo}`;
|
| 26 |
+
await axios.get(githubApiUrl);
|
| 27 |
+
} catch (error) {
|
| 28 |
+
return res.status(404).json({
|
| 29 |
+
success: false,
|
| 30 |
+
message: "Repository not found or not accessible",
|
| 31 |
+
});
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// Update user project with repo URL
|
| 35 |
+
const userProject = await prisma.userProject.findFirst({
|
| 36 |
+
where: {
|
| 37 |
+
userId,
|
| 38 |
+
projectId,
|
| 39 |
+
},
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
if (!userProject) {
|
| 43 |
+
return res.status(404).json({
|
| 44 |
+
success: false,
|
| 45 |
+
message: "Project not started by user",
|
| 46 |
+
});
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
const updated = await prisma.userProject.update({
|
| 50 |
+
where: { id: userProject.id },
|
| 51 |
+
data: { repoUrl },
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
res.json({ success: true, data: updated });
|
| 55 |
+
} catch (error: any) {
|
| 56 |
+
console.error("Link repository error:", error);
|
| 57 |
+
res.status(500).json({ success: false, message: error.message });
|
| 58 |
+
}
|
| 59 |
+
};
|
| 60 |
+
|
| 61 |
+
export const getRepositoryCommits = async (req: Request, res: Response) => {
|
| 62 |
+
const userId = (req as any).user?.id;
|
| 63 |
+
const { projectId } = req.params;
|
| 64 |
+
|
| 65 |
+
try {
|
| 66 |
+
const userProject = await prisma.userProject.findFirst({
|
| 67 |
+
where: {
|
| 68 |
+
userId,
|
| 69 |
+
projectId,
|
| 70 |
+
},
|
| 71 |
+
});
|
| 72 |
+
|
| 73 |
+
if (!userProject || !userProject.repoUrl) {
|
| 74 |
+
return res.status(404).json({
|
| 75 |
+
success: false,
|
| 76 |
+
message: "No repository linked",
|
| 77 |
+
});
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// Extract owner and repo
|
| 81 |
+
const repoPath = userProject.repoUrl
|
| 82 |
+
.replace("https://github.com/", "")
|
| 83 |
+
.replace(".git", "");
|
| 84 |
+
const [owner, repo] = repoPath.split("/");
|
| 85 |
+
|
| 86 |
+
// Fetch commits from GitHub API
|
| 87 |
+
const githubApiUrl = `https://api.github.com/repos/${owner}/${repo}/commits`;
|
| 88 |
+
const response = await axios.get(githubApiUrl, {
|
| 89 |
+
params: { per_page: 10 },
|
| 90 |
+
headers: {
|
| 91 |
+
// Add GitHub token if available for higher rate limits
|
| 92 |
+
Authorization: `token ${process.env.GITHUB_TOKEN}`,
|
| 93 |
+
},
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
const commits = response.data.map((commit: any) => ({
|
| 97 |
+
id: commit.sha,
|
| 98 |
+
message: commit.commit.message,
|
| 99 |
+
author: commit.commit.author.name,
|
| 100 |
+
date: commit.commit.author.date,
|
| 101 |
+
url: commit.html_url,
|
| 102 |
+
}));
|
| 103 |
+
|
| 104 |
+
res.json({ success: true, data: commits });
|
| 105 |
+
} catch (error: any) {
|
| 106 |
+
console.error("Get commits error:", error);
|
| 107 |
+
res.status(500).json({ success: false, message: error.message });
|
| 108 |
+
}
|
| 109 |
+
};
|
controllers/notifications/index.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Request, Response } from "express";
|
| 2 |
+
import prisma from "../../prisma/client";
|
| 3 |
+
|
| 4 |
+
export const getNotifications = async (req: Request, res: Response) => {
|
| 5 |
+
const userId = (req as any).user?.id;
|
| 6 |
+
const { page = 1, limit = 20, unreadOnly = false } = req.query;
|
| 7 |
+
|
| 8 |
+
try {
|
| 9 |
+
const where: any = { userId };
|
| 10 |
+
if (unreadOnly === "true") {
|
| 11 |
+
where.read = false;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
const notifications = await prisma.notification.findMany({
|
| 15 |
+
where,
|
| 16 |
+
orderBy: { createdAt: "desc" },
|
| 17 |
+
take: Number(limit),
|
| 18 |
+
skip: (Number(page) - 1) * Number(limit),
|
| 19 |
+
});
|
| 20 |
+
|
| 21 |
+
const unreadCount = await prisma.notification.count({
|
| 22 |
+
where: { userId, read: false },
|
| 23 |
+
});
|
| 24 |
+
|
| 25 |
+
res.json({ success: true, data: { notifications, unreadCount } });
|
| 26 |
+
} catch (error: any) {
|
| 27 |
+
res.status(500).json({ success: false, message: error.message });
|
| 28 |
+
}
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
export const markAsRead = async (req: Request, res: Response) => {
|
| 32 |
+
const userId = (req as any).user?.id;
|
| 33 |
+
const { notificationId } = req.params;
|
| 34 |
+
|
| 35 |
+
try {
|
| 36 |
+
await prisma.notification.updateMany({
|
| 37 |
+
where: { id: notificationId, userId },
|
| 38 |
+
data: { read: true },
|
| 39 |
+
});
|
| 40 |
+
|
| 41 |
+
res.json({ success: true, message: "Notification marked as read" });
|
| 42 |
+
} catch (error: any) {
|
| 43 |
+
res.status(500).json({ success: false, message: error.message });
|
| 44 |
+
}
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
export const markAllAsRead = async (req: Request, res: Response) => {
|
| 48 |
+
const userId = (req as any).user?.id;
|
| 49 |
+
|
| 50 |
+
try {
|
| 51 |
+
await prisma.notification.updateMany({
|
| 52 |
+
where: { userId, read: false },
|
| 53 |
+
data: { read: true },
|
| 54 |
+
});
|
| 55 |
+
|
| 56 |
+
res.json({ success: true, message: "All notifications marked as read" });
|
| 57 |
+
} catch (error: any) {
|
| 58 |
+
res.status(500).json({ success: false, message: error.message });
|
| 59 |
+
}
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
export const deleteNotification = async (req: Request, res: Response) => {
|
| 63 |
+
const userId = (req as any).user?.id;
|
| 64 |
+
const { notificationId } = req.params;
|
| 65 |
+
|
| 66 |
+
try {
|
| 67 |
+
await prisma.notification.deleteMany({
|
| 68 |
+
where: { id: notificationId, userId },
|
| 69 |
+
});
|
| 70 |
+
|
| 71 |
+
res.json({ success: true, message: "Notification deleted" });
|
| 72 |
+
} catch (error: any) {
|
| 73 |
+
res.status(500).json({ success: false, message: error.message });
|
| 74 |
+
}
|
| 75 |
+
};
|
controllers/paths/index.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Request, Response, NextFunction } from "express";
|
| 2 |
+
import prisma from "../../prisma/client";
|
| 3 |
+
|
| 4 |
+
export const getLearningPaths = async (req: Request, res: Response, next: NextFunction) => {
|
| 5 |
+
try {
|
| 6 |
+
const paths = await prisma.learningPath.findMany({
|
| 7 |
+
include: {
|
| 8 |
+
projects: {
|
| 9 |
+
include: {
|
| 10 |
+
project: {
|
| 11 |
+
select: {
|
| 12 |
+
title: true,
|
| 13 |
+
difficultyLevel: true,
|
| 14 |
+
},
|
| 15 |
+
},
|
| 16 |
+
},
|
| 17 |
+
orderBy: { orderIndex: "asc" },
|
| 18 |
+
},
|
| 19 |
+
},
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
res.json({ success: true, data: paths });
|
| 23 |
+
} catch (error: any) {
|
| 24 |
+
next(error);
|
| 25 |
+
}
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
export const getPathProgress = async (req: Request, res: Response, next: NextFunction) => {
|
| 29 |
+
const { id: pathId } = req.params;
|
| 30 |
+
const userId = (req as any).user?.id;
|
| 31 |
+
|
| 32 |
+
try {
|
| 33 |
+
const progress = await prisma.userPathProgress.findUnique({
|
| 34 |
+
where: {
|
| 35 |
+
userId_pathId: {
|
| 36 |
+
userId,
|
| 37 |
+
pathId,
|
| 38 |
+
},
|
| 39 |
+
},
|
| 40 |
+
include: {
|
| 41 |
+
path: {
|
| 42 |
+
include: {
|
| 43 |
+
projects: {
|
| 44 |
+
include: { project: true },
|
| 45 |
+
orderBy: { orderIndex: "asc" },
|
| 46 |
+
},
|
| 47 |
+
},
|
| 48 |
+
},
|
| 49 |
+
},
|
| 50 |
+
});
|
| 51 |
+
|
| 52 |
+
res.json({ success: true, data: progress });
|
| 53 |
+
} catch (error: any) {
|
| 54 |
+
next(error);
|
| 55 |
+
}
|
| 56 |
+
};
|
| 57 |
+
|
| 58 |
+
export const startPath = async (req: Request, res: Response, next: NextFunction) => {
|
| 59 |
+
const { id: pathId } = req.params;
|
| 60 |
+
const userId = (req as any).user?.id;
|
| 61 |
+
|
| 62 |
+
try {
|
| 63 |
+
const progress = await prisma.userPathProgress.upsert({
|
| 64 |
+
where: {
|
| 65 |
+
userId_pathId: {
|
| 66 |
+
userId,
|
| 67 |
+
pathId
|
| 68 |
+
}
|
| 69 |
+
},
|
| 70 |
+
update: {},
|
| 71 |
+
create: {
|
| 72 |
+
userId,
|
| 73 |
+
pathId,
|
| 74 |
+
currentProjectIndex: 0,
|
| 75 |
+
},
|
| 76 |
+
});
|
| 77 |
+
|
| 78 |
+
res.status(201).json({ success: true, data: progress });
|
| 79 |
+
} catch (error: any) {
|
| 80 |
+
next(error);
|
| 81 |
+
}
|
| 82 |
+
};
|
controllers/projects/index.ts
ADDED
|
@@ -0,0 +1,706 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Request, Response } from "express";
|
| 2 |
+
import prisma from "../../prisma/client";
|
| 3 |
+
import { Difficulty, DifficultyMode, ProjectStatus } from "@prisma/client";
|
| 4 |
+
import { checkAuth } from "../../middlewares/auth";
|
| 5 |
+
import { upLoadFiles, uploadBuffer } from "../../middlewares/file";
|
| 6 |
+
import { generateSlug, parseIfString } from "../../utils/helper";
|
| 7 |
+
import { getGeminiResponse } from "../../utils/gemini";
|
| 8 |
+
|
| 9 |
+
export const createProject = async (req: Request, res: Response) => {
|
| 10 |
+
const userId = (req as any).user?.id;
|
| 11 |
+
let {
|
| 12 |
+
title,
|
| 13 |
+
description,
|
| 14 |
+
difficultyLevel,
|
| 15 |
+
technologies,
|
| 16 |
+
categories,
|
| 17 |
+
estimatedTime,
|
| 18 |
+
learningObjectives,
|
| 19 |
+
resourceLinks,
|
| 20 |
+
starterRepoUrl,
|
| 21 |
+
difficultyModes,
|
| 22 |
+
coverImage,
|
| 23 |
+
milestones,
|
| 24 |
+
} = req.body;
|
| 25 |
+
|
| 26 |
+
// Parse JSON strings from FormData
|
| 27 |
+
technologies = parseIfString(technologies);
|
| 28 |
+
categories = parseIfString(categories);
|
| 29 |
+
learningObjectives = parseIfString(learningObjectives);
|
| 30 |
+
resourceLinks = parseIfString(resourceLinks);
|
| 31 |
+
difficultyModes = parseIfString(difficultyModes);
|
| 32 |
+
milestones = parseIfString(milestones);
|
| 33 |
+
|
| 34 |
+
try {
|
| 35 |
+
if (!userId)
|
| 36 |
+
return res.status(401).json({ success: false, message: "Unauthorized" });
|
| 37 |
+
|
| 38 |
+
const user = await prisma.user.findUnique({ where: { id: userId } });
|
| 39 |
+
if (!user || (user.role !== "ADMIN" && user.role !== "CONTRIBUTOR")) {
|
| 40 |
+
return res.status(403).json({
|
| 41 |
+
success: false,
|
| 42 |
+
message: "Only Admins or Contributors can create projects",
|
| 43 |
+
});
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
let imageUrl = coverImage;
|
| 47 |
+
if (req.file) {
|
| 48 |
+
imageUrl = await uploadBuffer(req.file.buffer, req.file.originalname);
|
| 49 |
+
} else if (coverImage && !coverImage.startsWith("http")) {
|
| 50 |
+
imageUrl = await upLoadFiles(coverImage);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
const project = await prisma.project.create({
|
| 54 |
+
data: {
|
| 55 |
+
title,
|
| 56 |
+
slug: generateSlug(title),
|
| 57 |
+
description,
|
| 58 |
+
difficultyLevel: difficultyLevel as Difficulty,
|
| 59 |
+
technologies,
|
| 60 |
+
categories,
|
| 61 |
+
estimatedTime,
|
| 62 |
+
learningObjectives,
|
| 63 |
+
resourceLinks: resourceLinks || [],
|
| 64 |
+
starterRepoUrl,
|
| 65 |
+
difficultyModes: difficultyModes || ["GUIDED", "STANDARD", "HARDCORE"],
|
| 66 |
+
createdBy: { connect: { id: userId } },
|
| 67 |
+
coverImage: imageUrl,
|
| 68 |
+
milestones: {
|
| 69 |
+
create: milestones?.map((m: any, index: number) => ({
|
| 70 |
+
milestoneNumber: index + 1,
|
| 71 |
+
title: m.title,
|
| 72 |
+
description: m.description,
|
| 73 |
+
hints: Array.isArray(m.hints) ? { GUIDED: m.hints } : m.hints || {},
|
| 74 |
+
validationCriteria: m.validationCriteria,
|
| 75 |
+
})),
|
| 76 |
+
},
|
| 77 |
+
},
|
| 78 |
+
include: { milestones: true },
|
| 79 |
+
});
|
| 80 |
+
|
| 81 |
+
res.status(201).json({ success: true, data: project });
|
| 82 |
+
} catch (error: any) {
|
| 83 |
+
res.status(500).json({ success: false, message: error.message });
|
| 84 |
+
}
|
| 85 |
+
};
|
| 86 |
+
|
| 87 |
+
export const updateProject = async (req: Request, res: Response) => {
|
| 88 |
+
const { id } = req.params;
|
| 89 |
+
const userId = (req as any).user?.id;
|
| 90 |
+
|
| 91 |
+
let updateData = req.body;
|
| 92 |
+
|
| 93 |
+
updateData.technologies = parseIfString(updateData.technologies);
|
| 94 |
+
updateData.categories = parseIfString(updateData.categories);
|
| 95 |
+
updateData.learningObjectives = parseIfString(updateData.learningObjectives);
|
| 96 |
+
updateData.resourceLinks = parseIfString(updateData.resourceLinks);
|
| 97 |
+
updateData.difficultyModes = parseIfString(updateData.difficultyModes);
|
| 98 |
+
|
| 99 |
+
const milestonesData = parseIfString(updateData.milestones);
|
| 100 |
+
delete updateData.milestones;
|
| 101 |
+
|
| 102 |
+
try {
|
| 103 |
+
const project = await prisma.project.findUnique({
|
| 104 |
+
where: { id },
|
| 105 |
+
include: { milestones: true, userProjects: true },
|
| 106 |
+
});
|
| 107 |
+
|
| 108 |
+
if (!project) {
|
| 109 |
+
return res.status(404).json({
|
| 110 |
+
success: false,
|
| 111 |
+
message: "Project not found",
|
| 112 |
+
});
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
const user = await prisma.user.findUnique({ where: { id: userId } });
|
| 116 |
+
|
| 117 |
+
if (!user || (user.role !== "ADMIN" && project.createdById !== userId)) {
|
| 118 |
+
return res.status(403).json({
|
| 119 |
+
success: false,
|
| 120 |
+
message: "Unauthorized to update this project",
|
| 121 |
+
});
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
if (req.file) {
|
| 125 |
+
updateData.coverImage = await uploadBuffer(
|
| 126 |
+
req.file.buffer,
|
| 127 |
+
req.file.originalname
|
| 128 |
+
);
|
| 129 |
+
} else if (
|
| 130 |
+
updateData.coverImage &&
|
| 131 |
+
!updateData.coverImage.startsWith("http")
|
| 132 |
+
) {
|
| 133 |
+
updateData.coverImage = await upLoadFiles(updateData.coverImage);
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
// Update slug if title changed
|
| 137 |
+
if (updateData.title && updateData.title !== project.title) {
|
| 138 |
+
updateData.slug = generateSlug(updateData.title);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
// Clean up any undefined or empty string values
|
| 142 |
+
Object.keys(updateData).forEach((key) => {
|
| 143 |
+
if (updateData[key] === undefined || updateData[key] === "") {
|
| 144 |
+
delete updateData[key];
|
| 145 |
+
}
|
| 146 |
+
});
|
| 147 |
+
|
| 148 |
+
// Update project with milestones handled separately
|
| 149 |
+
const updatedProject = await prisma.project.update({
|
| 150 |
+
where: { id },
|
| 151 |
+
data: {
|
| 152 |
+
...updateData,
|
| 153 |
+
...(milestonesData && {
|
| 154 |
+
milestones: {
|
| 155 |
+
deleteMany: {},
|
| 156 |
+
create: milestonesData
|
| 157 |
+
.filter((m: any) => m.title && m.description)
|
| 158 |
+
.map((m: any, index: number) => ({
|
| 159 |
+
milestoneNumber: index + 1,
|
| 160 |
+
title: m.title,
|
| 161 |
+
description: m.description,
|
| 162 |
+
hints: typeof m.hints === "object" ? m.hints : {},
|
| 163 |
+
validationCriteria:
|
| 164 |
+
typeof m.validationCriteria === "string"
|
| 165 |
+
? m.validationCriteria
|
| 166 |
+
: JSON.stringify(m.validationCriteria || {}),
|
| 167 |
+
})),
|
| 168 |
+
},
|
| 169 |
+
}),
|
| 170 |
+
},
|
| 171 |
+
include: {
|
| 172 |
+
milestones: {
|
| 173 |
+
orderBy: { milestoneNumber: "asc" },
|
| 174 |
+
},
|
| 175 |
+
createdBy: {
|
| 176 |
+
select: {
|
| 177 |
+
id: true,
|
| 178 |
+
firstName: true,
|
| 179 |
+
lastName: true,
|
| 180 |
+
email: true,
|
| 181 |
+
avatarUrl: true,
|
| 182 |
+
},
|
| 183 |
+
},
|
| 184 |
+
userProjects: { where: { userId: user.id } },
|
| 185 |
+
},
|
| 186 |
+
});
|
| 187 |
+
|
| 188 |
+
// Build progressByMode
|
| 189 |
+
let progressByMode: any = {};
|
| 190 |
+
let userProgress = null;
|
| 191 |
+
|
| 192 |
+
if (updatedProject.userProjects && updatedProject.userProjects.length > 0) {
|
| 193 |
+
for (const up of updatedProject.userProjects) {
|
| 194 |
+
const completedMilestones = await prisma.userMilestone.findMany({
|
| 195 |
+
where: {
|
| 196 |
+
userId: user.id,
|
| 197 |
+
userProjectId: up.id,
|
| 198 |
+
},
|
| 199 |
+
select: { milestoneId: true },
|
| 200 |
+
});
|
| 201 |
+
|
| 202 |
+
progressByMode[up.difficultyModeChosen] = {
|
| 203 |
+
status: up.status,
|
| 204 |
+
completedMilestones: completedMilestones.map((m) => m.milestoneId),
|
| 205 |
+
userProjectId: up.id,
|
| 206 |
+
startedAt: up.startedAt,
|
| 207 |
+
completedAt: up.completedAt,
|
| 208 |
+
};
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
userProgress =
|
| 212 |
+
updatedProject.userProjects[updatedProject.userProjects.length - 1];
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
res.json({
|
| 216 |
+
success: true,
|
| 217 |
+
data: {
|
| 218 |
+
...updatedProject,
|
| 219 |
+
progressByMode,
|
| 220 |
+
userProgress,
|
| 221 |
+
},
|
| 222 |
+
});
|
| 223 |
+
} catch (error: any) {
|
| 224 |
+
console.error("Update project error:", error);
|
| 225 |
+
res.status(500).json({
|
| 226 |
+
success: false,
|
| 227 |
+
message: error.message,
|
| 228 |
+
});
|
| 229 |
+
}
|
| 230 |
+
};
|
| 231 |
+
|
| 232 |
+
export const deleteProject = async (req: Request, res: Response) => {
|
| 233 |
+
const { id } = req.params;
|
| 234 |
+
const userId = (req as any).user?.id;
|
| 235 |
+
|
| 236 |
+
try {
|
| 237 |
+
const project = await prisma.project.findUnique({ where: { id } });
|
| 238 |
+
if (!project)
|
| 239 |
+
return res
|
| 240 |
+
.status(404)
|
| 241 |
+
.json({ success: false, message: "Project not found" });
|
| 242 |
+
|
| 243 |
+
const user = await prisma.user.findUnique({ where: { id: userId } });
|
| 244 |
+
if (!user || user.role !== "ADMIN") {
|
| 245 |
+
return res
|
| 246 |
+
.status(403)
|
| 247 |
+
.json({ success: false, message: "Only Admins can delete projects" });
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
await prisma.project.delete({ where: { id } });
|
| 251 |
+
res.json({ success: true, message: "Project deleted successfully" });
|
| 252 |
+
} catch (error: any) {
|
| 253 |
+
res.status(500).json({ success: false, message: error.message });
|
| 254 |
+
}
|
| 255 |
+
};
|
| 256 |
+
|
| 257 |
+
export const getProjects = async (req: Request, res: Response) => {
|
| 258 |
+
const { difficulty, tech, category, title } = req.query;
|
| 259 |
+
|
| 260 |
+
try {
|
| 261 |
+
const where: any = {};
|
| 262 |
+
if (difficulty) where.difficultyLevel = difficulty as Difficulty;
|
| 263 |
+
if (tech) where.technologies = { has: tech as string };
|
| 264 |
+
if (category) where.categories = { has: category as string };
|
| 265 |
+
if (title) where.title = { contains: title as string, mode: "insensitive" };
|
| 266 |
+
|
| 267 |
+
const projects = await prisma.project.findMany({
|
| 268 |
+
where,
|
| 269 |
+
include: {
|
| 270 |
+
createdBy: {
|
| 271 |
+
select: { firstName: true, lastName: true },
|
| 272 |
+
},
|
| 273 |
+
},
|
| 274 |
+
orderBy: { createdAt: "desc" },
|
| 275 |
+
});
|
| 276 |
+
|
| 277 |
+
res.json({ success: true, data: projects });
|
| 278 |
+
} catch (error: any) {
|
| 279 |
+
res.status(500).json({ success: false, message: error.message });
|
| 280 |
+
}
|
| 281 |
+
};
|
| 282 |
+
|
| 283 |
+
export const getProjectById = async (req: Request, res: Response) => {
|
| 284 |
+
const { id } = req.params;
|
| 285 |
+
|
| 286 |
+
try {
|
| 287 |
+
const user = await checkAuth(req);
|
| 288 |
+
const project = await prisma.project.findUnique({
|
| 289 |
+
where: { id },
|
| 290 |
+
include: {
|
| 291 |
+
milestones: { orderBy: { milestoneNumber: "asc" } },
|
| 292 |
+
createdBy: {
|
| 293 |
+
select: {
|
| 294 |
+
id: true,
|
| 295 |
+
firstName: true,
|
| 296 |
+
lastName: true,
|
| 297 |
+
avatarUrl: true,
|
| 298 |
+
},
|
| 299 |
+
},
|
| 300 |
+
userProjects: user ? { where: { userId: user.id } } : false,
|
| 301 |
+
},
|
| 302 |
+
});
|
| 303 |
+
|
| 304 |
+
if (!project)
|
| 305 |
+
return res
|
| 306 |
+
.status(404)
|
| 307 |
+
.json({ success: false, message: "Project not found" });
|
| 308 |
+
|
| 309 |
+
// If user is authenticated, fetch their progress and completed milestones for each mode
|
| 310 |
+
let progressByMode: any = {};
|
| 311 |
+
let userProgress = null;
|
| 312 |
+
|
| 313 |
+
if (user && project.userProjects && project.userProjects.length > 0) {
|
| 314 |
+
for (const up of project.userProjects) {
|
| 315 |
+
const completedMilestones = await prisma.userMilestone.findMany({
|
| 316 |
+
where: {
|
| 317 |
+
userId: user.id,
|
| 318 |
+
userProjectId: up.id,
|
| 319 |
+
},
|
| 320 |
+
select: { milestoneId: true },
|
| 321 |
+
});
|
| 322 |
+
|
| 323 |
+
progressByMode[up.difficultyModeChosen] = {
|
| 324 |
+
status: up.status,
|
| 325 |
+
completedMilestones: completedMilestones.map((m) => m.milestoneId),
|
| 326 |
+
userProjectId: up.id,
|
| 327 |
+
startedAt: up.startedAt,
|
| 328 |
+
completedAt: up.completedAt,
|
| 329 |
+
};
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
// Set the most recent userProject as the default userProgress
|
| 333 |
+
userProgress = project.userProjects[project.userProjects.length - 1];
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
res.json({
|
| 337 |
+
success: true,
|
| 338 |
+
data: {
|
| 339 |
+
...project,
|
| 340 |
+
progressByMode,
|
| 341 |
+
userProgress,
|
| 342 |
+
},
|
| 343 |
+
});
|
| 344 |
+
} catch (error: any) {
|
| 345 |
+
console.error("Get project error:", error);
|
| 346 |
+
res.status(500).json({ success: false, message: error.message });
|
| 347 |
+
}
|
| 348 |
+
};
|
| 349 |
+
|
| 350 |
+
// User Progress Endpoints
|
| 351 |
+
export const startProject = async (req: Request, res: Response) => {
|
| 352 |
+
const { id: projectId } = req.params;
|
| 353 |
+
const userId = (req as any).user?.id;
|
| 354 |
+
const { difficultyModeChosen } = req.body;
|
| 355 |
+
|
| 356 |
+
try {
|
| 357 |
+
const mode = (difficultyModeChosen as DifficultyMode) || "STANDARD";
|
| 358 |
+
|
| 359 |
+
const existingProgress = await prisma.userProject.findUnique({
|
| 360 |
+
where: {
|
| 361 |
+
userId_projectId_difficultyModeChosen: {
|
| 362 |
+
userId,
|
| 363 |
+
projectId,
|
| 364 |
+
difficultyModeChosen: mode,
|
| 365 |
+
},
|
| 366 |
+
},
|
| 367 |
+
});
|
| 368 |
+
|
| 369 |
+
if (existingProgress) {
|
| 370 |
+
return res.status(400).json({
|
| 371 |
+
success: false,
|
| 372 |
+
message: "Project already started in this mode",
|
| 373 |
+
});
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
const progress = await prisma.userProject.create({
|
| 377 |
+
data: {
|
| 378 |
+
userId,
|
| 379 |
+
projectId,
|
| 380 |
+
difficultyModeChosen: mode,
|
| 381 |
+
status: "IN_PROGRESS",
|
| 382 |
+
},
|
| 383 |
+
});
|
| 384 |
+
|
| 385 |
+
res.status(201).json({ success: true, data: progress });
|
| 386 |
+
} catch (error: any) {
|
| 387 |
+
res.status(500).json({ success: false, message: error.message });
|
| 388 |
+
}
|
| 389 |
+
};
|
| 390 |
+
|
| 391 |
+
export const updateProgress = async (req: Request, res: Response) => {
|
| 392 |
+
const { id: projectId } = req.params;
|
| 393 |
+
const userId = (req as any).user?.id;
|
| 394 |
+
const { status, repoUrl, difficultyMode } = req.body;
|
| 395 |
+
|
| 396 |
+
try {
|
| 397 |
+
const progress = await prisma.userProject.update({
|
| 398 |
+
where: {
|
| 399 |
+
userId_projectId_difficultyModeChosen: {
|
| 400 |
+
userId,
|
| 401 |
+
projectId,
|
| 402 |
+
difficultyModeChosen: difficultyMode as DifficultyMode,
|
| 403 |
+
},
|
| 404 |
+
},
|
| 405 |
+
data: { status: status as ProjectStatus, repoUrl },
|
| 406 |
+
});
|
| 407 |
+
|
| 408 |
+
res.json({ success: true, data: progress });
|
| 409 |
+
} catch (error: any) {
|
| 410 |
+
res.status(500).json({ success: false, message: error.message });
|
| 411 |
+
}
|
| 412 |
+
};
|
| 413 |
+
|
| 414 |
+
export const completeProject = async (req: Request, res: Response) => {
|
| 415 |
+
const { id: projectId } = req.params;
|
| 416 |
+
const userId = (req as any).user?.id;
|
| 417 |
+
const { repoUrl, difficultyMode } = req.body;
|
| 418 |
+
|
| 419 |
+
try {
|
| 420 |
+
const progress = await prisma.userProject.update({
|
| 421 |
+
where: {
|
| 422 |
+
userId_projectId_difficultyModeChosen: {
|
| 423 |
+
userId,
|
| 424 |
+
projectId,
|
| 425 |
+
difficultyModeChosen: difficultyMode as DifficultyMode,
|
| 426 |
+
},
|
| 427 |
+
},
|
| 428 |
+
data: {
|
| 429 |
+
status: "COMPLETED",
|
| 430 |
+
completedAt: new Date(),
|
| 431 |
+
repoUrl,
|
| 432 |
+
},
|
| 433 |
+
});
|
| 434 |
+
|
| 435 |
+
// Award XP (simple example)
|
| 436 |
+
await prisma.user.update({
|
| 437 |
+
where: { id: userId },
|
| 438 |
+
data: { xp: { increment: 100 } },
|
| 439 |
+
});
|
| 440 |
+
|
| 441 |
+
// Update project completion rate
|
| 442 |
+
const totalStarted = await prisma.userProject.count({
|
| 443 |
+
where: { projectId },
|
| 444 |
+
});
|
| 445 |
+
const totalCompleted = await prisma.userProject.count({
|
| 446 |
+
where: { projectId, status: "COMPLETED" },
|
| 447 |
+
});
|
| 448 |
+
|
| 449 |
+
await prisma.project.update({
|
| 450 |
+
where: { id: projectId },
|
| 451 |
+
data: {
|
| 452 |
+
completionRate: (totalCompleted / totalStarted) * 100,
|
| 453 |
+
submissionCount: { increment: 1 },
|
| 454 |
+
},
|
| 455 |
+
});
|
| 456 |
+
|
| 457 |
+
res.json({ success: true, data: progress });
|
| 458 |
+
} catch (error: any) {
|
| 459 |
+
res.status(500).json({ success: false, message: error.message });
|
| 460 |
+
}
|
| 461 |
+
};
|
| 462 |
+
|
| 463 |
+
export const getUserProgress = async (req: Request, res: Response) => {
|
| 464 |
+
const userId = (req as any).user?.id;
|
| 465 |
+
|
| 466 |
+
try {
|
| 467 |
+
const progress = await prisma.userProject.findMany({
|
| 468 |
+
where: { userId },
|
| 469 |
+
include: {
|
| 470 |
+
project: {
|
| 471 |
+
include: {
|
| 472 |
+
milestones: {
|
| 473 |
+
orderBy: { milestoneNumber: "asc" },
|
| 474 |
+
},
|
| 475 |
+
createdBy: {
|
| 476 |
+
select: { firstName: true, lastName: true, avatarUrl: true },
|
| 477 |
+
},
|
| 478 |
+
},
|
| 479 |
+
},
|
| 480 |
+
// Correct relation name from schema.prisma
|
| 481 |
+
milestoneProgress: {
|
| 482 |
+
select: { milestoneId: true, completedAt: true },
|
| 483 |
+
},
|
| 484 |
+
},
|
| 485 |
+
});
|
| 486 |
+
|
| 487 |
+
res.json({ success: true, data: progress });
|
| 488 |
+
} catch (error: any) {
|
| 489 |
+
res.status(500).json({ success: false, message: error.message });
|
| 490 |
+
}
|
| 491 |
+
};
|
| 492 |
+
|
| 493 |
+
// Submissions
|
| 494 |
+
export const submitProject = async (req: Request, res: Response) => {
|
| 495 |
+
const { id: projectId } = req.params;
|
| 496 |
+
const userId = (req as any).user?.id;
|
| 497 |
+
const { repoUrl } = req.body;
|
| 498 |
+
|
| 499 |
+
try {
|
| 500 |
+
const submission = await prisma.submission.create({
|
| 501 |
+
data: {
|
| 502 |
+
userId,
|
| 503 |
+
projectId,
|
| 504 |
+
repoUrl,
|
| 505 |
+
},
|
| 506 |
+
});
|
| 507 |
+
|
| 508 |
+
res.status(201).json({ success: true, data: submission });
|
| 509 |
+
} catch (error: any) {
|
| 510 |
+
res.status(500).json({ success: false, message: error.message });
|
| 511 |
+
}
|
| 512 |
+
};
|
| 513 |
+
|
| 514 |
+
export const completeMilestone = async (req: Request, res: Response) => {
|
| 515 |
+
const { id: projectId, milestoneId } = req.params;
|
| 516 |
+
const userId = (req as any).user?.id;
|
| 517 |
+
const { difficultyMode } = req.body;
|
| 518 |
+
|
| 519 |
+
try {
|
| 520 |
+
const userProject = await prisma.userProject.findUnique({
|
| 521 |
+
where: {
|
| 522 |
+
userId_projectId_difficultyModeChosen: {
|
| 523 |
+
userId,
|
| 524 |
+
projectId,
|
| 525 |
+
difficultyModeChosen: difficultyMode as DifficultyMode,
|
| 526 |
+
},
|
| 527 |
+
},
|
| 528 |
+
});
|
| 529 |
+
|
| 530 |
+
if (!userProject) {
|
| 531 |
+
return res.status(400).json({
|
| 532 |
+
success: false,
|
| 533 |
+
message: "Project not started by user in this mode",
|
| 534 |
+
});
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
const existingMilestone = await prisma.userMilestone.findUnique({
|
| 538 |
+
where: {
|
| 539 |
+
userId_milestoneId_userProjectId: {
|
| 540 |
+
userId,
|
| 541 |
+
milestoneId,
|
| 542 |
+
userProjectId: userProject.id,
|
| 543 |
+
},
|
| 544 |
+
},
|
| 545 |
+
});
|
| 546 |
+
|
| 547 |
+
if (existingMilestone) {
|
| 548 |
+
return res.status(400).json({
|
| 549 |
+
success: false,
|
| 550 |
+
message: "Milestone already completed in this mode",
|
| 551 |
+
});
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
// 1. Create the milestone completion record
|
| 555 |
+
const milestone = await prisma.userMilestone.create({
|
| 556 |
+
data: {
|
| 557 |
+
userId,
|
| 558 |
+
milestoneId,
|
| 559 |
+
userProjectId: userProject.id,
|
| 560 |
+
},
|
| 561 |
+
});
|
| 562 |
+
|
| 563 |
+
// 2. CHECK FOR PROJECT COMPLETION
|
| 564 |
+
const totalMilestones = await prisma.projectMilestone.count({
|
| 565 |
+
where: { projectId },
|
| 566 |
+
});
|
| 567 |
+
|
| 568 |
+
const completedCount = await prisma.userMilestone.count({
|
| 569 |
+
where: {
|
| 570 |
+
userId,
|
| 571 |
+
userProjectId: userProject.id,
|
| 572 |
+
},
|
| 573 |
+
});
|
| 574 |
+
|
| 575 |
+
// 3. AUTO-COMPLETE PROJECT if all milestones are done
|
| 576 |
+
if (completedCount === totalMilestones) {
|
| 577 |
+
await prisma.userProject.update({
|
| 578 |
+
where: { id: userProject.id },
|
| 579 |
+
data: {
|
| 580 |
+
status: "COMPLETED",
|
| 581 |
+
completedAt: new Date(),
|
| 582 |
+
},
|
| 583 |
+
});
|
| 584 |
+
|
| 585 |
+
await prisma.user.update({
|
| 586 |
+
where: { id: userId },
|
| 587 |
+
data: { xp: { increment: 50 } },
|
| 588 |
+
});
|
| 589 |
+
}
|
| 590 |
+
|
| 591 |
+
res.status(201).json({ success: true, data: milestone });
|
| 592 |
+
} catch (error: any) {
|
| 593 |
+
res.status(500).json({ success: false, message: error.message });
|
| 594 |
+
}
|
| 595 |
+
};
|
| 596 |
+
|
| 597 |
+
export const getFeaturedProjects = async (req: Request, res: Response) => {
|
| 598 |
+
try {
|
| 599 |
+
const featuredProjects = await prisma.project.findMany({
|
| 600 |
+
take: 4,
|
| 601 |
+
orderBy: { submissionCount: "desc" },
|
| 602 |
+
include: {
|
| 603 |
+
createdBy: {
|
| 604 |
+
select: { firstName: true, lastName: true },
|
| 605 |
+
},
|
| 606 |
+
},
|
| 607 |
+
});
|
| 608 |
+
|
| 609 |
+
res.json({ success: true, data: featuredProjects });
|
| 610 |
+
} catch (error: any) {
|
| 611 |
+
res.status(500).json({ success: false, message: error.message });
|
| 612 |
+
}
|
| 613 |
+
};
|
| 614 |
+
|
| 615 |
+
export const submitCode = async (req: Request, res: Response) => {
|
| 616 |
+
const userId = (req as any).user?.id;
|
| 617 |
+
const { projectId, milestoneId, code, language } = req.body;
|
| 618 |
+
|
| 619 |
+
try {
|
| 620 |
+
// Fetch milestone details for validation criteria
|
| 621 |
+
const milestone = await prisma.projectMilestone.findUnique({
|
| 622 |
+
where: { id: milestoneId },
|
| 623 |
+
include: { project: true },
|
| 624 |
+
});
|
| 625 |
+
|
| 626 |
+
if (!milestone) {
|
| 627 |
+
return res.status(404).json({
|
| 628 |
+
success: false,
|
| 629 |
+
message: "Milestone not found",
|
| 630 |
+
});
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
// AI-powered code review
|
| 634 |
+
const reviewPrompt = `
|
| 635 |
+
You are a code reviewer for a learning platform. Review this code submission:
|
| 636 |
+
|
| 637 |
+
Project: ${milestone.project.title}
|
| 638 |
+
Milestone: ${milestone.title}
|
| 639 |
+
Description: ${milestone.description}
|
| 640 |
+
Validation Criteria: ${milestone.validationCriteria || "General best practices"}
|
| 641 |
+
|
| 642 |
+
Code:
|
| 643 |
+
\`\`\`${language}
|
| 644 |
+
${code}
|
| 645 |
+
\`\`\`
|
| 646 |
+
|
| 647 |
+
Provide:
|
| 648 |
+
1. Does this code meet the milestone requirements? (YES/NO)
|
| 649 |
+
2. Code quality score (0-100)
|
| 650 |
+
3. Specific feedback on what's good
|
| 651 |
+
4. Areas for improvement
|
| 652 |
+
5. Security concerns (if any)
|
| 653 |
+
|
| 654 |
+
Format as JSON:
|
| 655 |
+
{
|
| 656 |
+
"meetsRequirements": boolean,
|
| 657 |
+
"score": number,
|
| 658 |
+
"strengths": string[],
|
| 659 |
+
"improvements": string[],
|
| 660 |
+
"securityIssues": string[]
|
| 661 |
+
}
|
| 662 |
+
`;
|
| 663 |
+
|
| 664 |
+
const reviewResult = await getGeminiResponse(reviewPrompt);
|
| 665 |
+
|
| 666 |
+
// Parse AI response
|
| 667 |
+
let parsedReview;
|
| 668 |
+
try {
|
| 669 |
+
// Extract JSON from potential markdown code blocks
|
| 670 |
+
const jsonMatch = reviewResult.match(/```json\n?([\s\S]*?)\n?```/);
|
| 671 |
+
const jsonStr = jsonMatch ? jsonMatch[1] : reviewResult;
|
| 672 |
+
parsedReview = JSON.parse(jsonStr);
|
| 673 |
+
} catch (e) {
|
| 674 |
+
parsedReview = {
|
| 675 |
+
meetsRequirements: false,
|
| 676 |
+
score: 0,
|
| 677 |
+
strengths: [],
|
| 678 |
+
improvements: ["Unable to parse AI review"],
|
| 679 |
+
securityIssues: [],
|
| 680 |
+
};
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
// Save code submission
|
| 684 |
+
const submission = await prisma.submission.create({
|
| 685 |
+
data: {
|
| 686 |
+
userId,
|
| 687 |
+
projectId,
|
| 688 |
+
repoUrl: `code-submission-${Date.now()}`, // Placeholder
|
| 689 |
+
feedback: JSON.stringify(parsedReview),
|
| 690 |
+
score: parsedReview.score,
|
| 691 |
+
aiReviewData: parsedReview,
|
| 692 |
+
},
|
| 693 |
+
});
|
| 694 |
+
|
| 695 |
+
res.json({
|
| 696 |
+
success: true,
|
| 697 |
+
data: {
|
| 698 |
+
submission,
|
| 699 |
+
review: parsedReview,
|
| 700 |
+
},
|
| 701 |
+
});
|
| 702 |
+
} catch (error: any) {
|
| 703 |
+
console.error("Code submission error:", error);
|
| 704 |
+
res.status(500).json({ success: false, message: error.message });
|
| 705 |
+
}
|
| 706 |
+
};
|
controllers/social/index.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Request, Response } from "express";
|
| 2 |
+
import prisma from "../../prisma/client";
|
| 3 |
+
|
| 4 |
+
export const followUser = async (req: Request, res: Response) => {
|
| 5 |
+
const userId = (req as any).user?.id;
|
| 6 |
+
const { followingId } = req.body;
|
| 7 |
+
|
| 8 |
+
try {
|
| 9 |
+
if (userId === followingId) {
|
| 10 |
+
return res
|
| 11 |
+
.status(400)
|
| 12 |
+
.json({ success: false, message: "Cannot follow yourself" });
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
const existingFollow = await prisma.follow.findUnique({
|
| 16 |
+
where: { followerId_followingId: { followerId: userId, followingId } },
|
| 17 |
+
});
|
| 18 |
+
|
| 19 |
+
if (existingFollow) {
|
| 20 |
+
return res
|
| 21 |
+
.status(400)
|
| 22 |
+
.json({ success: false, message: "Already following this user" });
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
const follow = await prisma.follow.create({
|
| 26 |
+
data: { followerId: userId, followingId },
|
| 27 |
+
});
|
| 28 |
+
|
| 29 |
+
await prisma.notification.create({
|
| 30 |
+
data: {
|
| 31 |
+
userId: followingId,
|
| 32 |
+
type: "FOLLOW",
|
| 33 |
+
title: "New Follower",
|
| 34 |
+
message: "Someone started following you",
|
| 35 |
+
link: `/profile/${userId}`,
|
| 36 |
+
},
|
| 37 |
+
});
|
| 38 |
+
|
| 39 |
+
res.json({ success: true, data: follow });
|
| 40 |
+
} catch (error: any) {
|
| 41 |
+
res.status(500).json({ success: false, message: error.message });
|
| 42 |
+
}
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
export const unfollowUser = async (req: Request, res: Response) => {
|
| 46 |
+
const userId = (req as any).user?.id;
|
| 47 |
+
const { followingId } = req.params;
|
| 48 |
+
|
| 49 |
+
try {
|
| 50 |
+
await prisma.follow.delete({
|
| 51 |
+
where: { followerId_followingId: { followerId: userId, followingId } },
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
res.json({ success: true, message: "Unfollowed successfully" });
|
| 55 |
+
} catch (error: any) {
|
| 56 |
+
res.status(500).json({ success: false, message: error.message });
|
| 57 |
+
}
|
| 58 |
+
};
|
| 59 |
+
|
| 60 |
+
export const getFollowers = async (req: Request, res: Response) => {
|
| 61 |
+
const { userId } = req.params;
|
| 62 |
+
|
| 63 |
+
try {
|
| 64 |
+
const followers = await prisma.follow.findMany({
|
| 65 |
+
where: { followingId: userId },
|
| 66 |
+
include: {
|
| 67 |
+
follower: {
|
| 68 |
+
select: {
|
| 69 |
+
id: true,
|
| 70 |
+
firstName: true,
|
| 71 |
+
lastName: true,
|
| 72 |
+
avatarUrl: true,
|
| 73 |
+
xp: true,
|
| 74 |
+
},
|
| 75 |
+
},
|
| 76 |
+
},
|
| 77 |
+
});
|
| 78 |
+
|
| 79 |
+
res.json({ success: true, data: followers });
|
| 80 |
+
} catch (error: any) {
|
| 81 |
+
res.status(500).json({ success: false, message: error.message });
|
| 82 |
+
}
|
| 83 |
+
};
|
| 84 |
+
|
| 85 |
+
export const getFollowing = async (req: Request, res: Response) => {
|
| 86 |
+
const { userId } = req.params;
|
| 87 |
+
|
| 88 |
+
try {
|
| 89 |
+
const following = await prisma.follow.findMany({
|
| 90 |
+
where: { followerId: userId },
|
| 91 |
+
include: {
|
| 92 |
+
following: {
|
| 93 |
+
select: {
|
| 94 |
+
id: true,
|
| 95 |
+
firstName: true,
|
| 96 |
+
lastName: true,
|
| 97 |
+
avatarUrl: true,
|
| 98 |
+
xp: true,
|
| 99 |
+
},
|
| 100 |
+
},
|
| 101 |
+
},
|
| 102 |
+
});
|
| 103 |
+
|
| 104 |
+
res.json({ success: true, data: following });
|
| 105 |
+
} catch (error: any) {
|
| 106 |
+
res.status(500).json({ success: false, message: error.message });
|
| 107 |
+
}
|
| 108 |
+
};
|
| 109 |
+
|
| 110 |
+
export const getActivityFeed = async (req: Request, res: Response) => {
|
| 111 |
+
const userId = (req as any).user?.id;
|
| 112 |
+
const { page = 1, limit = 20 } = req.query;
|
| 113 |
+
|
| 114 |
+
try {
|
| 115 |
+
const following = await prisma.follow.findMany({
|
| 116 |
+
where: { followerId: userId },
|
| 117 |
+
select: { followingId: true },
|
| 118 |
+
});
|
| 119 |
+
|
| 120 |
+
const followingIds = following.map((f) => f.followingId);
|
| 121 |
+
|
| 122 |
+
const activities = await prisma.userProject.findMany({
|
| 123 |
+
where: {
|
| 124 |
+
userId: { in: followingIds },
|
| 125 |
+
status: "COMPLETED",
|
| 126 |
+
},
|
| 127 |
+
include: {
|
| 128 |
+
user: {
|
| 129 |
+
select: {
|
| 130 |
+
id: true,
|
| 131 |
+
firstName: true,
|
| 132 |
+
lastName: true,
|
| 133 |
+
avatarUrl: true,
|
| 134 |
+
},
|
| 135 |
+
},
|
| 136 |
+
project: {
|
| 137 |
+
select: {
|
| 138 |
+
id: true,
|
| 139 |
+
title: true,
|
| 140 |
+
slug: true,
|
| 141 |
+
},
|
| 142 |
+
},
|
| 143 |
+
},
|
| 144 |
+
orderBy: { completedAt: "desc" },
|
| 145 |
+
take: Number(limit),
|
| 146 |
+
skip: (Number(page) - 1) * Number(limit),
|
| 147 |
+
});
|
| 148 |
+
|
| 149 |
+
res.json({ success: true, data: activities });
|
| 150 |
+
} catch (error: any) {
|
| 151 |
+
res.status(500).json({ success: false, message: error.message });
|
| 152 |
+
}
|
| 153 |
+
};
|
| 154 |
+
|
| 155 |
+
export const shareProject = async (req: Request, res: Response) => {
|
| 156 |
+
const userId = (req as any).user?.id;
|
| 157 |
+
const { projectId, platform } = req.body;
|
| 158 |
+
|
| 159 |
+
try {
|
| 160 |
+
const project = await prisma.project.findUnique({
|
| 161 |
+
where: { id: projectId },
|
| 162 |
+
});
|
| 163 |
+
|
| 164 |
+
if (!project) {
|
| 165 |
+
return res
|
| 166 |
+
.status(404)
|
| 167 |
+
.json({ success: false, message: "Project not found" });
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
const shareUrl = `${process.env.CLIENT_URL}/projects/${project.slug}`;
|
| 171 |
+
const ogImageUrl = `${process.env.CLIENT_URL}/api/og-image/${project.slug}`;
|
| 172 |
+
|
| 173 |
+
const share = await prisma.socialShare.create({
|
| 174 |
+
data: {
|
| 175 |
+
userId,
|
| 176 |
+
projectId,
|
| 177 |
+
platform,
|
| 178 |
+
shareUrl,
|
| 179 |
+
ogImageUrl,
|
| 180 |
+
},
|
| 181 |
+
});
|
| 182 |
+
|
| 183 |
+
res.json({ success: true, data: { shareUrl, ogImageUrl, share } });
|
| 184 |
+
} catch (error: any) {
|
| 185 |
+
res.status(500).json({ success: false, message: error.message });
|
| 186 |
+
}
|
| 187 |
+
};
|
controllers/teams/index.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Request, Response, NextFunction } from "express";
|
| 2 |
+
import prisma from "../../prisma/client";
|
| 3 |
+
|
| 4 |
+
export const createTeam = async (
|
| 5 |
+
req: Request,
|
| 6 |
+
res: Response,
|
| 7 |
+
next: NextFunction
|
| 8 |
+
) => {
|
| 9 |
+
const { name, projectId } = req.body;
|
| 10 |
+
const userId = (req as any).user?.id;
|
| 11 |
+
|
| 12 |
+
try {
|
| 13 |
+
const team = await prisma.team.create({
|
| 14 |
+
data: {
|
| 15 |
+
name,
|
| 16 |
+
projectId,
|
| 17 |
+
members: {
|
| 18 |
+
create: {
|
| 19 |
+
userId,
|
| 20 |
+
},
|
| 21 |
+
},
|
| 22 |
+
},
|
| 23 |
+
include: {
|
| 24 |
+
members: true,
|
| 25 |
+
},
|
| 26 |
+
});
|
| 27 |
+
|
| 28 |
+
res.status(201).json({ success: true, data: team });
|
| 29 |
+
} catch (error: any) {
|
| 30 |
+
next(error);
|
| 31 |
+
}
|
| 32 |
+
};
|
| 33 |
+
|
| 34 |
+
export const joinTeam = async (
|
| 35 |
+
req: Request,
|
| 36 |
+
res: Response,
|
| 37 |
+
next: NextFunction
|
| 38 |
+
) => {
|
| 39 |
+
const { id: teamId } = req.params;
|
| 40 |
+
const userId = (req as any).user?.id;
|
| 41 |
+
|
| 42 |
+
try {
|
| 43 |
+
const teamMember = await prisma.teamMember.create({
|
| 44 |
+
data: {
|
| 45 |
+
teamId,
|
| 46 |
+
userId,
|
| 47 |
+
},
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
res.status(201).json({ success: true, data: teamMember });
|
| 51 |
+
} catch (error: any) {
|
| 52 |
+
next(error);
|
| 53 |
+
}
|
| 54 |
+
};
|
| 55 |
+
|
| 56 |
+
export const getTeamProjects = async (
|
| 57 |
+
req: Request,
|
| 58 |
+
res: Response,
|
| 59 |
+
next: NextFunction
|
| 60 |
+
) => {
|
| 61 |
+
const { id: teamId } = req.params;
|
| 62 |
+
|
| 63 |
+
try {
|
| 64 |
+
const team = await prisma.team.findUnique({
|
| 65 |
+
where: { id: teamId },
|
| 66 |
+
include: {
|
| 67 |
+
project: true,
|
| 68 |
+
},
|
| 69 |
+
});
|
| 70 |
+
|
| 71 |
+
if (!team)
|
| 72 |
+
return res
|
| 73 |
+
.status(404)
|
| 74 |
+
.json({ success: false, message: "Team not found" });
|
| 75 |
+
|
| 76 |
+
res.json({ success: true, data: team.project });
|
| 77 |
+
} catch (error: any) {
|
| 78 |
+
next(error);
|
| 79 |
+
}
|
| 80 |
+
};
|
dist/controllers/ai/index.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
| 3 |
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
| 4 |
+
};
|
| 5 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 6 |
+
exports.requestHint = exports.getChatHistory = exports.getAllChatMessages = exports.chatWithAI = void 0;
|
| 7 |
+
const client_1 = __importDefault(require("../../prisma/client"));
|
| 8 |
+
const gemini_1 = require("../../utils/gemini");
|
| 9 |
+
const chatWithAI = async (req, res) => {
|
| 10 |
+
var _a, _b, _c;
|
| 11 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 12 |
+
const { message, projectId } = req.body;
|
| 13 |
+
try {
|
| 14 |
+
// Fetch project context if projectId is provided
|
| 15 |
+
let projectContext = "";
|
| 16 |
+
let difficultyMode = null;
|
| 17 |
+
if (projectId) {
|
| 18 |
+
const project = await client_1.default.project.findUnique({
|
| 19 |
+
where: { id: projectId },
|
| 20 |
+
include: {
|
| 21 |
+
milestones: true,
|
| 22 |
+
userProjects: { where: { userId } },
|
| 23 |
+
},
|
| 24 |
+
});
|
| 25 |
+
if (project) {
|
| 26 |
+
difficultyMode =
|
| 27 |
+
((_c = (_b = project.userProjects) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.difficultyModeChosen) || "STANDARD";
|
| 28 |
+
projectContext = `The user is working on the project: "${project.title}".
|
| 29 |
+
Description: ${project.description}.
|
| 30 |
+
Difficulty Level: ${project.difficultyLevel}.
|
| 31 |
+
User's chosen mode: ${difficultyMode}.
|
| 32 |
+
Milestones: ${project.milestones
|
| 33 |
+
.map((m) => `${m.milestoneNumber}. ${m.title}`)
|
| 34 |
+
.join(", ")}.`;
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
// Fetch chat history from DB
|
| 38 |
+
const history = await client_1.default.chatMessage.findMany({
|
| 39 |
+
where: { userId, projectId: projectId || null },
|
| 40 |
+
orderBy: { createdAt: "asc" },
|
| 41 |
+
take: 20,
|
| 42 |
+
});
|
| 43 |
+
const geminiHistory = history.map((msg) => ({
|
| 44 |
+
role: msg.role === "user" ? "user" : "model",
|
| 45 |
+
parts: [{ text: msg.content }],
|
| 46 |
+
}));
|
| 47 |
+
let modeInstruction = "";
|
| 48 |
+
if (difficultyMode === "GUIDED") {
|
| 49 |
+
modeInstruction =
|
| 50 |
+
"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.";
|
| 51 |
+
}
|
| 52 |
+
else if (difficultyMode === "HARDCORE") {
|
| 53 |
+
modeInstruction =
|
| 54 |
+
"The user is in HARDCORE mode. Be very brief, provide only high-level conceptual hints, and never provide code solutions.";
|
| 55 |
+
}
|
| 56 |
+
else {
|
| 57 |
+
modeInstruction =
|
| 58 |
+
"The user is in STANDARD mode. Provide balanced guidance, focus on concepts, and only provide code as a last resort.";
|
| 59 |
+
}
|
| 60 |
+
const systemPrompt = `You are an AI Guide for a project-based learning platform.
|
| 61 |
+
Your goal is to help students learn by providing guidance, not just giving away answers.
|
| 62 |
+
${projectContext}
|
| 63 |
+
${modeInstruction}
|
| 64 |
+
Keep your responses concise and educational.`;
|
| 65 |
+
const fullPrompt = `${systemPrompt}\n\nUser: ${message}`;
|
| 66 |
+
const aiResponse = await (0, gemini_1.getGeminiResponse)(fullPrompt, geminiHistory);
|
| 67 |
+
// Save messages to DB
|
| 68 |
+
await client_1.default.chatMessage.createMany({
|
| 69 |
+
data: [
|
| 70 |
+
{
|
| 71 |
+
userId,
|
| 72 |
+
projectId: projectId || null,
|
| 73 |
+
role: "user",
|
| 74 |
+
content: message,
|
| 75 |
+
},
|
| 76 |
+
{
|
| 77 |
+
userId,
|
| 78 |
+
projectId: projectId || null,
|
| 79 |
+
role: "assistant",
|
| 80 |
+
content: aiResponse,
|
| 81 |
+
},
|
| 82 |
+
],
|
| 83 |
+
});
|
| 84 |
+
res.json({ success: true, data: aiResponse });
|
| 85 |
+
}
|
| 86 |
+
catch (error) {
|
| 87 |
+
res.status(500).json({ success: false, message: error.message });
|
| 88 |
+
}
|
| 89 |
+
};
|
| 90 |
+
exports.chatWithAI = chatWithAI;
|
| 91 |
+
const getAllChatMessages = async (req, res) => {
|
| 92 |
+
var _a;
|
| 93 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 94 |
+
try {
|
| 95 |
+
const messages = await client_1.default.chatMessage.findMany({
|
| 96 |
+
where: { userId },
|
| 97 |
+
orderBy: { createdAt: "asc" },
|
| 98 |
+
});
|
| 99 |
+
res.json({ success: true, data: messages });
|
| 100 |
+
}
|
| 101 |
+
catch (error) {
|
| 102 |
+
res.status(500).json({ success: false, message: error.message });
|
| 103 |
+
}
|
| 104 |
+
};
|
| 105 |
+
exports.getAllChatMessages = getAllChatMessages;
|
| 106 |
+
const getChatHistory = async (req, res) => {
|
| 107 |
+
var _a;
|
| 108 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 109 |
+
const { projectId } = req.params;
|
| 110 |
+
try {
|
| 111 |
+
const history = await client_1.default.chatMessage.findMany({
|
| 112 |
+
where: { userId, projectId: projectId === "null" ? null : projectId },
|
| 113 |
+
orderBy: { createdAt: "asc" },
|
| 114 |
+
});
|
| 115 |
+
res.json({ success: true, data: history });
|
| 116 |
+
}
|
| 117 |
+
catch (error) {
|
| 118 |
+
res.status(500).json({ success: false, message: error.message });
|
| 119 |
+
}
|
| 120 |
+
};
|
| 121 |
+
exports.getChatHistory = getChatHistory;
|
| 122 |
+
const requestHint = async (req, res) => {
|
| 123 |
+
var _a;
|
| 124 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 125 |
+
const { projectId, milestoneNumber } = req.body;
|
| 126 |
+
try {
|
| 127 |
+
const milestone = await client_1.default.projectMilestone.findUnique({
|
| 128 |
+
where: { projectId_milestoneNumber: { projectId, milestoneNumber } },
|
| 129 |
+
include: { project: true },
|
| 130 |
+
});
|
| 131 |
+
if (!milestone)
|
| 132 |
+
return res
|
| 133 |
+
.status(404)
|
| 134 |
+
.json({ success: false, message: "Milestone not found" });
|
| 135 |
+
const prompt = `The user is stuck on Milestone ${milestoneNumber} ("${milestone.title}") of the project "${milestone.project.title}".
|
| 136 |
+
Milestone description: ${milestone.description}.
|
| 137 |
+
Existing hints: ${milestone.hints.join(", ")}.
|
| 138 |
+
Please provide a new, progressive hint that helps them move forward without revealing the full solution.`;
|
| 139 |
+
const hint = await (0, gemini_1.getGeminiResponse)(prompt);
|
| 140 |
+
res.json({ success: true, data: hint });
|
| 141 |
+
}
|
| 142 |
+
catch (error) {
|
| 143 |
+
res.status(500).json({ success: false, message: error.message });
|
| 144 |
+
}
|
| 145 |
+
};
|
| 146 |
+
exports.requestHint = requestHint;
|
dist/controllers/analytics/index.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
| 3 |
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
| 4 |
+
};
|
| 5 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 6 |
+
exports.getAdminAnalytics = exports.getUserAnalytics = void 0;
|
| 7 |
+
const client_1 = __importDefault(require("../../prisma/client"));
|
| 8 |
+
const getUserAnalytics = async (req, res, next) => {
|
| 9 |
+
var _a;
|
| 10 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 11 |
+
try {
|
| 12 |
+
const user = await client_1.default.user.findUnique({
|
| 13 |
+
where: { id: userId },
|
| 14 |
+
select: {
|
| 15 |
+
xp: true,
|
| 16 |
+
streak: true,
|
| 17 |
+
_count: {
|
| 18 |
+
select: {
|
| 19 |
+
projects: true,
|
| 20 |
+
submissions: true,
|
| 21 |
+
achievements: true
|
| 22 |
+
}
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
});
|
| 26 |
+
const projectCompletion = await client_1.default.userProject.groupBy({
|
| 27 |
+
by: ["status"],
|
| 28 |
+
where: { userId },
|
| 29 |
+
_count: true
|
| 30 |
+
});
|
| 31 |
+
res.json({
|
| 32 |
+
success: true,
|
| 33 |
+
data: {
|
| 34 |
+
stats: user,
|
| 35 |
+
projectCompletion
|
| 36 |
+
}
|
| 37 |
+
});
|
| 38 |
+
}
|
| 39 |
+
catch (error) {
|
| 40 |
+
next(error);
|
| 41 |
+
}
|
| 42 |
+
};
|
| 43 |
+
exports.getUserAnalytics = getUserAnalytics;
|
| 44 |
+
const getAdminAnalytics = async (req, res, next) => {
|
| 45 |
+
var _a;
|
| 46 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 47 |
+
try {
|
| 48 |
+
const user = await client_1.default.user.findUnique({ where: { id: userId } });
|
| 49 |
+
if ((user === null || user === void 0 ? void 0 : user.role) !== "ADMIN") {
|
| 50 |
+
return res.status(403).json({ success: false, message: "Forbidden" });
|
| 51 |
+
}
|
| 52 |
+
const totalUsers = await client_1.default.user.count();
|
| 53 |
+
const totalProjects = await client_1.default.project.count();
|
| 54 |
+
const totalSubmissions = await client_1.default.submission.count();
|
| 55 |
+
const popularProjects = await client_1.default.project.findMany({
|
| 56 |
+
orderBy: { submissionCount: "desc" },
|
| 57 |
+
take: 5,
|
| 58 |
+
select: { title: true, submissionCount: true }
|
| 59 |
+
});
|
| 60 |
+
res.json({
|
| 61 |
+
success: true,
|
| 62 |
+
data: {
|
| 63 |
+
totalUsers,
|
| 64 |
+
totalProjects,
|
| 65 |
+
totalSubmissions,
|
| 66 |
+
popularProjects
|
| 67 |
+
}
|
| 68 |
+
});
|
| 69 |
+
}
|
| 70 |
+
catch (error) {
|
| 71 |
+
next(error);
|
| 72 |
+
}
|
| 73 |
+
};
|
| 74 |
+
exports.getAdminAnalytics = getAdminAnalytics;
|
dist/controllers/auth/index.js
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
| 3 |
+
if (k2 === undefined) k2 = k;
|
| 4 |
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
| 5 |
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
| 6 |
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
| 7 |
+
}
|
| 8 |
+
Object.defineProperty(o, k2, desc);
|
| 9 |
+
}) : (function(o, m, k, k2) {
|
| 10 |
+
if (k2 === undefined) k2 = k;
|
| 11 |
+
o[k2] = m[k];
|
| 12 |
+
}));
|
| 13 |
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
| 14 |
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
| 15 |
+
}) : function(o, v) {
|
| 16 |
+
o["default"] = v;
|
| 17 |
+
});
|
| 18 |
+
var __importStar = (this && this.__importStar) || (function () {
|
| 19 |
+
var ownKeys = function(o) {
|
| 20 |
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
| 21 |
+
var ar = [];
|
| 22 |
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
| 23 |
+
return ar;
|
| 24 |
+
};
|
| 25 |
+
return ownKeys(o);
|
| 26 |
+
};
|
| 27 |
+
return function (mod) {
|
| 28 |
+
if (mod && mod.__esModule) return mod;
|
| 29 |
+
var result = {};
|
| 30 |
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
| 31 |
+
__setModuleDefault(result, mod);
|
| 32 |
+
return result;
|
| 33 |
+
};
|
| 34 |
+
})();
|
| 35 |
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
| 36 |
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
| 37 |
+
};
|
| 38 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 39 |
+
exports.githubAuthCallback = exports.googleAuthCallback = exports.updateUser = exports.getUser = exports.resetPassword = exports.verifyOTP = exports.forgetPassword = exports.me = exports.logout = exports.login = exports.signUp = void 0;
|
| 40 |
+
const client_1 = __importDefault(require("../../prisma/client"));
|
| 41 |
+
const argon2 = __importStar(require("argon2"));
|
| 42 |
+
const email_1 = require("../../middlewares/email");
|
| 43 |
+
const generateOTP_1 = require("../../middlewares/generateOTP");
|
| 44 |
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
| 45 |
+
const dotenv_1 = __importDefault(require("dotenv"));
|
| 46 |
+
const redis_1 = __importDefault(require("../../utils/redis"));
|
| 47 |
+
dotenv_1.default.config();
|
| 48 |
+
const JWT_SECRET = process.env.JWT_SECRET || "default_secret";
|
| 49 |
+
const signUp = async (req, res) => {
|
| 50 |
+
const { firstName, lastName, skillLevel, email, password, role } = req.body;
|
| 51 |
+
try {
|
| 52 |
+
const existingUser = await client_1.default.user.findUnique({ where: { email } });
|
| 53 |
+
if (existingUser) {
|
| 54 |
+
return res
|
| 55 |
+
.status(400)
|
| 56 |
+
.json({ success: false, message: "Email already registered" });
|
| 57 |
+
}
|
| 58 |
+
const hashedPassword = await argon2.hash(password);
|
| 59 |
+
const user = await client_1.default.user.create({
|
| 60 |
+
data: {
|
| 61 |
+
firstName,
|
| 62 |
+
lastName,
|
| 63 |
+
email,
|
| 64 |
+
skillLevel: skillLevel || "BEGINNER",
|
| 65 |
+
password: hashedPassword,
|
| 66 |
+
role: role || "STUDENT",
|
| 67 |
+
},
|
| 68 |
+
});
|
| 69 |
+
res.json({
|
| 70 |
+
success: true,
|
| 71 |
+
message: "User registered successfully",
|
| 72 |
+
data: { id: user.id },
|
| 73 |
+
});
|
| 74 |
+
}
|
| 75 |
+
catch (error) {
|
| 76 |
+
res.status(500).json({ success: false, message: error.message });
|
| 77 |
+
}
|
| 78 |
+
};
|
| 79 |
+
exports.signUp = signUp;
|
| 80 |
+
const login = async (req, res) => {
|
| 81 |
+
const { email, password } = req.body;
|
| 82 |
+
try {
|
| 83 |
+
const user = await client_1.default.user.findUnique({ where: { email } });
|
| 84 |
+
if (!user || !user.password) {
|
| 85 |
+
return res
|
| 86 |
+
.status(401)
|
| 87 |
+
.json({ success: false, message: "Invalid Email or Password" });
|
| 88 |
+
}
|
| 89 |
+
const verifyPassword = await argon2.verify(user.password, password);
|
| 90 |
+
if (!verifyPassword) {
|
| 91 |
+
return res
|
| 92 |
+
.status(401)
|
| 93 |
+
.json({ success: false, message: "Invalid Email or Password" });
|
| 94 |
+
}
|
| 95 |
+
const token = jsonwebtoken_1.default.sign({ id: user.id, role: user.role }, JWT_SECRET, {
|
| 96 |
+
expiresIn: "7d",
|
| 97 |
+
});
|
| 98 |
+
res.cookie("token", token, {
|
| 99 |
+
httpOnly: true,
|
| 100 |
+
secure: process.env.NODE_ENV === "production",
|
| 101 |
+
sameSite: "strict",
|
| 102 |
+
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
| 103 |
+
});
|
| 104 |
+
res.json({
|
| 105 |
+
success: true,
|
| 106 |
+
message: "Login successful",
|
| 107 |
+
data: {
|
| 108 |
+
id: user.id,
|
| 109 |
+
firstName: user.firstName,
|
| 110 |
+
lastName: user.lastName,
|
| 111 |
+
email: user.email,
|
| 112 |
+
role: user.role,
|
| 113 |
+
token,
|
| 114 |
+
},
|
| 115 |
+
});
|
| 116 |
+
}
|
| 117 |
+
catch (error) {
|
| 118 |
+
res.status(500).json({ success: false, message: error.message });
|
| 119 |
+
}
|
| 120 |
+
};
|
| 121 |
+
exports.login = login;
|
| 122 |
+
const logout = async (req, res) => {
|
| 123 |
+
res.clearCookie("token");
|
| 124 |
+
res.json({ success: true, message: "Logged out successfully" });
|
| 125 |
+
};
|
| 126 |
+
exports.logout = logout;
|
| 127 |
+
const me = async (req, res) => {
|
| 128 |
+
var _a;
|
| 129 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 130 |
+
if (!userId) {
|
| 131 |
+
return res.status(401).json({ success: false, message: "Unauthorized" });
|
| 132 |
+
}
|
| 133 |
+
try {
|
| 134 |
+
const user = await client_1.default.user.findUnique({
|
| 135 |
+
where: { id: userId },
|
| 136 |
+
select: {
|
| 137 |
+
id: true,
|
| 138 |
+
email: true,
|
| 139 |
+
firstName: true,
|
| 140 |
+
lastName: true,
|
| 141 |
+
skillLevel: true,
|
| 142 |
+
bio: true,
|
| 143 |
+
portfolioLinks: true,
|
| 144 |
+
xp: true,
|
| 145 |
+
streak: true,
|
| 146 |
+
role: true,
|
| 147 |
+
avatarUrl: true,
|
| 148 |
+
createdAt: true,
|
| 149 |
+
},
|
| 150 |
+
});
|
| 151 |
+
if (!user) {
|
| 152 |
+
return res
|
| 153 |
+
.status(404)
|
| 154 |
+
.json({ success: false, message: "User not found" });
|
| 155 |
+
}
|
| 156 |
+
res.json({ success: true, data: user });
|
| 157 |
+
}
|
| 158 |
+
catch (error) {
|
| 159 |
+
res.status(500).json({ success: false, message: error.message });
|
| 160 |
+
}
|
| 161 |
+
};
|
| 162 |
+
exports.me = me;
|
| 163 |
+
const forgetPassword = async (req, res) => {
|
| 164 |
+
const { email } = req.body;
|
| 165 |
+
const appName = "Dev Drill";
|
| 166 |
+
try {
|
| 167 |
+
const user = await client_1.default.user.findUnique({ where: { email } });
|
| 168 |
+
if (!user) {
|
| 169 |
+
return res
|
| 170 |
+
.status(404)
|
| 171 |
+
.json({ success: false, message: "Account is not registered" });
|
| 172 |
+
}
|
| 173 |
+
const { otp, otpDate } = (0, generateOTP_1.generateOTP)();
|
| 174 |
+
// Store OTP in Redis with 1 hour expiry
|
| 175 |
+
await redis_1.default.setex(`otp:${email}`, 3600, otp.toString());
|
| 176 |
+
const htmlMessage = `
|
| 177 |
+
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
| 178 |
+
<div style="background: #f0f7ff; padding: 30px; border-radius: 10px;">
|
| 179 |
+
<h1 style="color: #1a237e; margin-bottom: 25px;">${appName}</h1>
|
| 180 |
+
<p style="font-size: 16px; color: #333;">
|
| 181 |
+
Hello ${user.lastName},<br><br>
|
| 182 |
+
We received a request to reset your password for your ${appName} account.
|
| 183 |
+
</p>
|
| 184 |
+
<div style="background: #fff; padding: 20px; border-radius: 5px; margin: 20px 0; text-align: center;">
|
| 185 |
+
<p style="margin: 0; font-weight: bold; font-size: 24px; letter-spacing: 2px;">${otp}</p>
|
| 186 |
+
</div>
|
| 187 |
+
<p style="font-size: 14px; color: #666;">
|
| 188 |
+
This verification code will expire in 1 hour.<br>
|
| 189 |
+
If you didn't request this password reset, please ignore this email.
|
| 190 |
+
</p>
|
| 191 |
+
</div>
|
| 192 |
+
<p style="font-size: 12px; color: #999; text-align: center; margin-top: 20px;">
|
| 193 |
+
© ${new Date().getFullYear()} ${appName}. All rights reserved.
|
| 194 |
+
</p>
|
| 195 |
+
</div>
|
| 196 |
+
`;
|
| 197 |
+
const textMessage = `${appName} Password Reset\n\n` +
|
| 198 |
+
`Hello ${user.lastName},\n\n` +
|
| 199 |
+
`Use this OTP to reset your password: ${otp}\n` +
|
| 200 |
+
`This code expires in 1 hour.`;
|
| 201 |
+
(0, email_1.sendEmail)(user.email, `Password Reset Request - ${appName}`, htmlMessage, textMessage);
|
| 202 |
+
res.json({
|
| 203 |
+
success: true,
|
| 204 |
+
message: `An OTP has been sent to your mail. Check your mail for your verification code`,
|
| 205 |
+
});
|
| 206 |
+
}
|
| 207 |
+
catch (error) {
|
| 208 |
+
res.status(500).json({ success: false, message: error.message });
|
| 209 |
+
}
|
| 210 |
+
};
|
| 211 |
+
exports.forgetPassword = forgetPassword;
|
| 212 |
+
const verifyOTP = async (req, res) => {
|
| 213 |
+
const { email, otp: userOtp } = req.body;
|
| 214 |
+
try {
|
| 215 |
+
const storedOtp = await redis_1.default.get(`otp:${email}`);
|
| 216 |
+
if (!storedOtp) {
|
| 217 |
+
return res
|
| 218 |
+
.status(400)
|
| 219 |
+
.json({ success: false, message: "OTP expired or not found" });
|
| 220 |
+
}
|
| 221 |
+
if (storedOtp !== userOtp.toString()) {
|
| 222 |
+
return res
|
| 223 |
+
.status(400)
|
| 224 |
+
.json({ success: false, message: "Invalid Verification code!" });
|
| 225 |
+
}
|
| 226 |
+
// OTP verified, can remove it now
|
| 227 |
+
await redis_1.default.del(`otp:${email}`);
|
| 228 |
+
// Optionally set a flag in Redis that OTP was verified for this email to allow password reset
|
| 229 |
+
await redis_1.default.setex(`otp_verified:${email}`, 600, "true");
|
| 230 |
+
res.json({ success: true, message: "Verification successful" });
|
| 231 |
+
}
|
| 232 |
+
catch (error) {
|
| 233 |
+
res.status(500).json({ success: false, message: error.message });
|
| 234 |
+
}
|
| 235 |
+
};
|
| 236 |
+
exports.verifyOTP = verifyOTP;
|
| 237 |
+
const resetPassword = async (req, res) => {
|
| 238 |
+
const { email, newPassword } = req.body;
|
| 239 |
+
try {
|
| 240 |
+
const isVerified = await redis_1.default.get(`otp_verified:${email}`);
|
| 241 |
+
if (!isVerified) {
|
| 242 |
+
return res
|
| 243 |
+
.status(400)
|
| 244 |
+
.json({ success: false, message: "OTP not verified" });
|
| 245 |
+
}
|
| 246 |
+
const user = await client_1.default.user.findUnique({ where: { email } });
|
| 247 |
+
if (!user)
|
| 248 |
+
return res
|
| 249 |
+
.status(404)
|
| 250 |
+
.json({ success: false, message: "User not found" });
|
| 251 |
+
if (user.password) {
|
| 252 |
+
const verifyPassword = await argon2.verify(user.password, newPassword);
|
| 253 |
+
if (verifyPassword) {
|
| 254 |
+
return res
|
| 255 |
+
.status(400)
|
| 256 |
+
.json({ success: false, message: "You entered your old password" });
|
| 257 |
+
}
|
| 258 |
+
}
|
| 259 |
+
const newPass = await argon2.hash(newPassword);
|
| 260 |
+
await client_1.default.user.update({
|
| 261 |
+
where: { email },
|
| 262 |
+
data: { password: newPass },
|
| 263 |
+
});
|
| 264 |
+
await redis_1.default.del(`otp_verified:${email}`);
|
| 265 |
+
res.json({ success: true, message: "Password successfully reset" });
|
| 266 |
+
}
|
| 267 |
+
catch (error) {
|
| 268 |
+
res.status(500).json({ success: false, message: error.message });
|
| 269 |
+
}
|
| 270 |
+
};
|
| 271 |
+
exports.resetPassword = resetPassword;
|
| 272 |
+
const getUser = async (req, res) => {
|
| 273 |
+
const { id } = req.params;
|
| 274 |
+
try {
|
| 275 |
+
const user = await client_1.default.user.findUnique({
|
| 276 |
+
where: { id },
|
| 277 |
+
select: {
|
| 278 |
+
id: true,
|
| 279 |
+
email: true,
|
| 280 |
+
firstName: true,
|
| 281 |
+
lastName: true,
|
| 282 |
+
skillLevel: true,
|
| 283 |
+
bio: true,
|
| 284 |
+
portfolioLinks: true,
|
| 285 |
+
xp: true,
|
| 286 |
+
streak: true,
|
| 287 |
+
role: true,
|
| 288 |
+
avatarUrl: true,
|
| 289 |
+
createdAt: true,
|
| 290 |
+
},
|
| 291 |
+
});
|
| 292 |
+
if (!user)
|
| 293 |
+
return res
|
| 294 |
+
.status(404)
|
| 295 |
+
.json({ success: false, message: "User not found" });
|
| 296 |
+
res.json({ success: true, data: user });
|
| 297 |
+
}
|
| 298 |
+
catch (error) {
|
| 299 |
+
res.status(500).json({ success: false, message: error.message });
|
| 300 |
+
}
|
| 301 |
+
};
|
| 302 |
+
exports.getUser = getUser;
|
| 303 |
+
const updateUser = async (req, res) => {
|
| 304 |
+
const { id } = req.params;
|
| 305 |
+
let { firstName, lastName, email, password, bio, skillLevel, portfolioLinks, avatarUrl, } = req.body;
|
| 306 |
+
try {
|
| 307 |
+
const user = await client_1.default.user.findUnique({ where: { id } });
|
| 308 |
+
if (!user) {
|
| 309 |
+
return res
|
| 310 |
+
.status(404)
|
| 311 |
+
.json({ success: false, message: "User not found" });
|
| 312 |
+
}
|
| 313 |
+
const data = {
|
| 314 |
+
firstName,
|
| 315 |
+
lastName,
|
| 316 |
+
bio,
|
| 317 |
+
skillLevel,
|
| 318 |
+
portfolioLinks,
|
| 319 |
+
avatarUrl,
|
| 320 |
+
};
|
| 321 |
+
if (email && email !== user.email) {
|
| 322 |
+
const existingUser = await client_1.default.user.findUnique({ where: { email } });
|
| 323 |
+
if (existingUser) {
|
| 324 |
+
return res
|
| 325 |
+
.status(400)
|
| 326 |
+
.json({ success: false, message: "Email already in use" });
|
| 327 |
+
}
|
| 328 |
+
data.email = email;
|
| 329 |
+
}
|
| 330 |
+
if (password) {
|
| 331 |
+
data.password = await argon2.hash(password);
|
| 332 |
+
}
|
| 333 |
+
const updatedUser = await client_1.default.user.update({
|
| 334 |
+
where: { googleId: user.googleId || undefined },
|
| 335 |
+
data,
|
| 336 |
+
});
|
| 337 |
+
res.json({
|
| 338 |
+
success: true,
|
| 339 |
+
data: {
|
| 340 |
+
id: updatedUser.id,
|
| 341 |
+
firstName: updatedUser.firstName,
|
| 342 |
+
lastName: updatedUser.lastName,
|
| 343 |
+
email: updatedUser.email,
|
| 344 |
+
role: updatedUser.role,
|
| 345 |
+
skillLevel: updatedUser.skillLevel,
|
| 346 |
+
},
|
| 347 |
+
});
|
| 348 |
+
}
|
| 349 |
+
catch (error) {
|
| 350 |
+
res.status(500).json({ success: false, message: error.message });
|
| 351 |
+
}
|
| 352 |
+
};
|
| 353 |
+
exports.updateUser = updateUser;
|
| 354 |
+
const googleAuthCallback = async (req, res) => {
|
| 355 |
+
try {
|
| 356 |
+
const user = req.user;
|
| 357 |
+
if (!user) {
|
| 358 |
+
return res.redirect(`${process.env.CLIENT_URL}/auth/signin?error=auth_failed`);
|
| 359 |
+
}
|
| 360 |
+
const token = jsonwebtoken_1.default.sign({ id: user.id, role: user.role }, JWT_SECRET, {
|
| 361 |
+
expiresIn: "7d",
|
| 362 |
+
});
|
| 363 |
+
res.cookie("token", token, {
|
| 364 |
+
httpOnly: true,
|
| 365 |
+
secure: process.env.NODE_ENV === "production",
|
| 366 |
+
sameSite: "lax",
|
| 367 |
+
maxAge: 7 * 24 * 60 * 60 * 1000,
|
| 368 |
+
});
|
| 369 |
+
res.redirect(`${process.env.CLIENT_URL}/dashboard`);
|
| 370 |
+
}
|
| 371 |
+
catch (error) {
|
| 372 |
+
res.redirect(`${process.env.CLIENT_URL}/auth/signin?error=server_error`);
|
| 373 |
+
}
|
| 374 |
+
};
|
| 375 |
+
exports.googleAuthCallback = googleAuthCallback;
|
| 376 |
+
const githubAuthCallback = async (req, res) => {
|
| 377 |
+
try {
|
| 378 |
+
const user = req.user;
|
| 379 |
+
if (!user) {
|
| 380 |
+
return res.redirect(`${process.env.CLIENT_URL}/auth/signin?error=auth_failed`);
|
| 381 |
+
}
|
| 382 |
+
const token = jsonwebtoken_1.default.sign({ id: user.id, role: user.role }, JWT_SECRET, {
|
| 383 |
+
expiresIn: "7d",
|
| 384 |
+
});
|
| 385 |
+
res.cookie("token", token, {
|
| 386 |
+
httpOnly: true,
|
| 387 |
+
secure: process.env.NODE_ENV === "production",
|
| 388 |
+
sameSite: "lax",
|
| 389 |
+
maxAge: 7 * 24 * 60 * 60 * 1000,
|
| 390 |
+
});
|
| 391 |
+
res.redirect(`${process.env.CLIENT_URL}/dashboard`);
|
| 392 |
+
}
|
| 393 |
+
catch (error) {
|
| 394 |
+
res.redirect(`${process.env.CLIENT_URL}/auth/signin?error=server_error`);
|
| 395 |
+
}
|
| 396 |
+
};
|
| 397 |
+
exports.githubAuthCallback = githubAuthCallback;
|
dist/controllers/code-review/index.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
| 3 |
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
| 4 |
+
};
|
| 5 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 6 |
+
exports.getSubmissionReview = exports.analyzeCode = void 0;
|
| 7 |
+
const client_1 = __importDefault(require("../../prisma/client"));
|
| 8 |
+
const gemini_1 = require("../../utils/gemini");
|
| 9 |
+
const axios_1 = __importDefault(require("axios"));
|
| 10 |
+
const analyzeCode = async (req, res) => {
|
| 11 |
+
var _a;
|
| 12 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 13 |
+
const { repoUrl, projectId } = req.body;
|
| 14 |
+
try {
|
| 15 |
+
// 1. Fetch code from GitHub (simplified: fetch README or a few files for demo)
|
| 16 |
+
// In a real app, you'd use a GitHub App or User Token to clone or fetch via API
|
| 17 |
+
const repoPath = repoUrl.replace("https://github.com/", "");
|
| 18 |
+
const [owner, repo] = repoPath.split("/");
|
| 19 |
+
// Fetch repo info as a placeholder for analysis
|
| 20 |
+
const githubApiUrl = `https://api.github.com/repos/${owner}/${repo}`;
|
| 21 |
+
const repoInfo = await axios_1.default.get(githubApiUrl);
|
| 22 |
+
// 2. Automated analysis (simulated)
|
| 23 |
+
const metrics = {
|
| 24 |
+
complexity: "Medium",
|
| 25 |
+
securityIssues: 0,
|
| 26 |
+
lintErrors: 0
|
| 27 |
+
};
|
| 28 |
+
// 3. AI-powered feedback
|
| 29 |
+
const prompt = `Please review this GitHub repository: ${repoUrl}.
|
| 30 |
+
Owner: ${owner}
|
| 31 |
+
Repo: ${repo}
|
| 32 |
+
Description: ${repoInfo.data.description}
|
| 33 |
+
Provide constructive feedback on the project structure and suggest improvements for a student.`;
|
| 34 |
+
const feedback = await (0, gemini_1.getGeminiResponse)(prompt);
|
| 35 |
+
// 4. Store review results
|
| 36 |
+
const submission = await client_1.default.submission.create({
|
| 37 |
+
data: {
|
| 38 |
+
userId,
|
| 39 |
+
projectId,
|
| 40 |
+
repoUrl,
|
| 41 |
+
feedback,
|
| 42 |
+
score: 85 // Simulated score
|
| 43 |
+
}
|
| 44 |
+
});
|
| 45 |
+
res.json({ success: true, data: submission });
|
| 46 |
+
}
|
| 47 |
+
catch (error) {
|
| 48 |
+
res.status(500).json({ success: false, message: error.message });
|
| 49 |
+
}
|
| 50 |
+
};
|
| 51 |
+
exports.analyzeCode = analyzeCode;
|
| 52 |
+
const getSubmissionReview = async (req, res) => {
|
| 53 |
+
const { id } = req.params;
|
| 54 |
+
try {
|
| 55 |
+
const submission = await client_1.default.submission.findUnique({
|
| 56 |
+
where: { id },
|
| 57 |
+
include: {
|
| 58 |
+
project: { select: { title: true } },
|
| 59 |
+
user: { select: { firstName: true, lastName: true } }
|
| 60 |
+
}
|
| 61 |
+
});
|
| 62 |
+
if (!submission)
|
| 63 |
+
return res.status(404).json({ success: false, message: "Submission not found" });
|
| 64 |
+
res.json({ success: true, data: submission });
|
| 65 |
+
}
|
| 66 |
+
catch (error) {
|
| 67 |
+
res.status(500).json({ success: false, message: error.message });
|
| 68 |
+
}
|
| 69 |
+
};
|
| 70 |
+
exports.getSubmissionReview = getSubmissionReview;
|
dist/controllers/community/index.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
| 3 |
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
| 4 |
+
};
|
| 5 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 6 |
+
exports.createForumThread = exports.getForumThreads = exports.voteOnSubmission = exports.commentOnSubmission = exports.getPublicSubmissions = void 0;
|
| 7 |
+
const client_1 = __importDefault(require("../../prisma/client"));
|
| 8 |
+
const getPublicSubmissions = async (req, res, next) => {
|
| 9 |
+
try {
|
| 10 |
+
const submissions = await client_1.default.submission.findMany({
|
| 11 |
+
where: { isPublic: true },
|
| 12 |
+
include: {
|
| 13 |
+
user: {
|
| 14 |
+
select: {
|
| 15 |
+
id: true,
|
| 16 |
+
firstName: true,
|
| 17 |
+
lastName: true,
|
| 18 |
+
avatarUrl: true,
|
| 19 |
+
},
|
| 20 |
+
},
|
| 21 |
+
project: {
|
| 22 |
+
select: {
|
| 23 |
+
id: true,
|
| 24 |
+
title: true,
|
| 25 |
+
},
|
| 26 |
+
},
|
| 27 |
+
_count: {
|
| 28 |
+
select: {
|
| 29 |
+
comments: true,
|
| 30 |
+
votes: true,
|
| 31 |
+
},
|
| 32 |
+
},
|
| 33 |
+
},
|
| 34 |
+
orderBy: { createdAt: "desc" },
|
| 35 |
+
});
|
| 36 |
+
res.json({ success: true, data: submissions });
|
| 37 |
+
}
|
| 38 |
+
catch (error) {
|
| 39 |
+
next(error);
|
| 40 |
+
}
|
| 41 |
+
};
|
| 42 |
+
exports.getPublicSubmissions = getPublicSubmissions;
|
| 43 |
+
const commentOnSubmission = async (req, res, next) => {
|
| 44 |
+
var _a;
|
| 45 |
+
const { id: submissionId } = req.params;
|
| 46 |
+
const { content } = req.body;
|
| 47 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 48 |
+
try {
|
| 49 |
+
const comment = await client_1.default.comment.create({
|
| 50 |
+
data: {
|
| 51 |
+
content,
|
| 52 |
+
userId,
|
| 53 |
+
submissionId,
|
| 54 |
+
},
|
| 55 |
+
include: {
|
| 56 |
+
user: {
|
| 57 |
+
select: {
|
| 58 |
+
id: true,
|
| 59 |
+
firstName: true,
|
| 60 |
+
lastName: true,
|
| 61 |
+
avatarUrl: true,
|
| 62 |
+
},
|
| 63 |
+
},
|
| 64 |
+
},
|
| 65 |
+
});
|
| 66 |
+
res.status(201).json({ success: true, data: comment });
|
| 67 |
+
}
|
| 68 |
+
catch (error) {
|
| 69 |
+
next(error);
|
| 70 |
+
}
|
| 71 |
+
};
|
| 72 |
+
exports.commentOnSubmission = commentOnSubmission;
|
| 73 |
+
const voteOnSubmission = async (req, res, next) => {
|
| 74 |
+
var _a;
|
| 75 |
+
const { id: submissionId } = req.params;
|
| 76 |
+
const { value } = req.body; // 1 for upvote, -1 for downvote
|
| 77 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 78 |
+
try {
|
| 79 |
+
const vote = await client_1.default.vote.upsert({
|
| 80 |
+
where: {
|
| 81 |
+
userId_submissionId: {
|
| 82 |
+
userId,
|
| 83 |
+
submissionId,
|
| 84 |
+
},
|
| 85 |
+
},
|
| 86 |
+
update: { value },
|
| 87 |
+
create: {
|
| 88 |
+
value,
|
| 89 |
+
userId,
|
| 90 |
+
submissionId,
|
| 91 |
+
},
|
| 92 |
+
});
|
| 93 |
+
res.json({ success: true, data: vote });
|
| 94 |
+
}
|
| 95 |
+
catch (error) {
|
| 96 |
+
next(error);
|
| 97 |
+
}
|
| 98 |
+
};
|
| 99 |
+
exports.voteOnSubmission = voteOnSubmission;
|
| 100 |
+
const getForumThreads = async (req, res, next) => {
|
| 101 |
+
const { projectId } = req.query;
|
| 102 |
+
try {
|
| 103 |
+
const threads = await client_1.default.forumThread.findMany({
|
| 104 |
+
where: projectId ? { projectId: String(projectId) } : {},
|
| 105 |
+
include: {
|
| 106 |
+
user: {
|
| 107 |
+
select: {
|
| 108 |
+
id: true,
|
| 109 |
+
firstName: true,
|
| 110 |
+
lastName: true,
|
| 111 |
+
avatarUrl: true,
|
| 112 |
+
},
|
| 113 |
+
},
|
| 114 |
+
_count: {
|
| 115 |
+
select: { posts: true },
|
| 116 |
+
},
|
| 117 |
+
},
|
| 118 |
+
orderBy: { createdAt: "desc" },
|
| 119 |
+
});
|
| 120 |
+
res.json({ success: true, data: threads });
|
| 121 |
+
}
|
| 122 |
+
catch (error) {
|
| 123 |
+
next(error);
|
| 124 |
+
}
|
| 125 |
+
};
|
| 126 |
+
exports.getForumThreads = getForumThreads;
|
| 127 |
+
const createForumThread = async (req, res, next) => {
|
| 128 |
+
var _a;
|
| 129 |
+
const { title, content, projectId } = req.body;
|
| 130 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 131 |
+
try {
|
| 132 |
+
const thread = await client_1.default.forumThread.create({
|
| 133 |
+
data: {
|
| 134 |
+
title,
|
| 135 |
+
content,
|
| 136 |
+
userId,
|
| 137 |
+
projectId: projectId || null,
|
| 138 |
+
},
|
| 139 |
+
include: {
|
| 140 |
+
user: {
|
| 141 |
+
select: {
|
| 142 |
+
id: true,
|
| 143 |
+
firstName: true,
|
| 144 |
+
lastName: true,
|
| 145 |
+
avatarUrl: true,
|
| 146 |
+
},
|
| 147 |
+
},
|
| 148 |
+
},
|
| 149 |
+
});
|
| 150 |
+
res.status(201).json({ success: true, data: thread });
|
| 151 |
+
}
|
| 152 |
+
catch (error) {
|
| 153 |
+
next(error);
|
| 154 |
+
}
|
| 155 |
+
};
|
| 156 |
+
exports.createForumThread = createForumThread;
|
dist/controllers/events/index.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
| 3 |
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
| 4 |
+
};
|
| 5 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 6 |
+
exports.getLeaderboard = exports.updateEventScore = exports.leaveEvent = exports.joinEvent = exports.getEvent = exports.getEvents = exports.createEvent = void 0;
|
| 7 |
+
const client_1 = __importDefault(require("../../prisma/client"));
|
| 8 |
+
const createEvent = async (req, res) => {
|
| 9 |
+
const { title, description, type, startDate, endDate, prizePool, maxParticipants, bannerUrl, } = req.body;
|
| 10 |
+
try {
|
| 11 |
+
const event = await client_1.default.event.create({
|
| 12 |
+
data: {
|
| 13 |
+
title,
|
| 14 |
+
description,
|
| 15 |
+
type,
|
| 16 |
+
startDate: new Date(startDate),
|
| 17 |
+
endDate: new Date(endDate),
|
| 18 |
+
prizePool,
|
| 19 |
+
maxParticipants,
|
| 20 |
+
bannerUrl,
|
| 21 |
+
},
|
| 22 |
+
});
|
| 23 |
+
res.json({ success: true, data: event });
|
| 24 |
+
}
|
| 25 |
+
catch (error) {
|
| 26 |
+
res.status(500).json({ success: false, message: error.message });
|
| 27 |
+
}
|
| 28 |
+
};
|
| 29 |
+
exports.createEvent = createEvent;
|
| 30 |
+
const getEvents = async (req, res) => {
|
| 31 |
+
const { status, type, page = 1, limit = 10 } = req.query;
|
| 32 |
+
try {
|
| 33 |
+
const where = {};
|
| 34 |
+
if (status)
|
| 35 |
+
where.status = status;
|
| 36 |
+
if (type)
|
| 37 |
+
where.type = type;
|
| 38 |
+
const events = await client_1.default.event.findMany({
|
| 39 |
+
where,
|
| 40 |
+
include: {
|
| 41 |
+
_count: {
|
| 42 |
+
select: { participants: true },
|
| 43 |
+
},
|
| 44 |
+
},
|
| 45 |
+
orderBy: { startDate: "desc" },
|
| 46 |
+
take: Number(limit),
|
| 47 |
+
skip: (Number(page) - 1) * Number(limit),
|
| 48 |
+
});
|
| 49 |
+
res.json({ success: true, data: events });
|
| 50 |
+
}
|
| 51 |
+
catch (error) {
|
| 52 |
+
res.status(500).json({ success: false, message: error.message });
|
| 53 |
+
}
|
| 54 |
+
};
|
| 55 |
+
exports.getEvents = getEvents;
|
| 56 |
+
const getEvent = async (req, res) => {
|
| 57 |
+
const { id } = req.params;
|
| 58 |
+
try {
|
| 59 |
+
const event = await client_1.default.event.findUnique({
|
| 60 |
+
where: { id },
|
| 61 |
+
include: {
|
| 62 |
+
participants: {
|
| 63 |
+
include: {
|
| 64 |
+
user: {
|
| 65 |
+
select: {
|
| 66 |
+
id: true,
|
| 67 |
+
firstName: true,
|
| 68 |
+
lastName: true,
|
| 69 |
+
avatarUrl: true,
|
| 70 |
+
},
|
| 71 |
+
},
|
| 72 |
+
},
|
| 73 |
+
},
|
| 74 |
+
leaderboard: {
|
| 75 |
+
include: {
|
| 76 |
+
user: {
|
| 77 |
+
select: {
|
| 78 |
+
id: true,
|
| 79 |
+
firstName: true,
|
| 80 |
+
lastName: true,
|
| 81 |
+
avatarUrl: true,
|
| 82 |
+
},
|
| 83 |
+
},
|
| 84 |
+
},
|
| 85 |
+
orderBy: { score: "desc" },
|
| 86 |
+
take: 100,
|
| 87 |
+
},
|
| 88 |
+
},
|
| 89 |
+
});
|
| 90 |
+
if (!event) {
|
| 91 |
+
return res
|
| 92 |
+
.status(404)
|
| 93 |
+
.json({ success: false, message: "Event not found" });
|
| 94 |
+
}
|
| 95 |
+
res.json({ success: true, data: event });
|
| 96 |
+
}
|
| 97 |
+
catch (error) {
|
| 98 |
+
res.status(500).json({ success: false, message: error.message });
|
| 99 |
+
}
|
| 100 |
+
};
|
| 101 |
+
exports.getEvent = getEvent;
|
| 102 |
+
const joinEvent = async (req, res) => {
|
| 103 |
+
var _a;
|
| 104 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 105 |
+
const { eventId } = req.params;
|
| 106 |
+
try {
|
| 107 |
+
const event = await client_1.default.event.findUnique({
|
| 108 |
+
where: { id: eventId },
|
| 109 |
+
include: { _count: { select: { participants: true } } },
|
| 110 |
+
});
|
| 111 |
+
if (!event) {
|
| 112 |
+
return res
|
| 113 |
+
.status(404)
|
| 114 |
+
.json({ success: false, message: "Event not found" });
|
| 115 |
+
}
|
| 116 |
+
if (event.maxParticipants &&
|
| 117 |
+
event._count.participants >= event.maxParticipants) {
|
| 118 |
+
return res.status(400).json({ success: false, message: "Event is full" });
|
| 119 |
+
}
|
| 120 |
+
const participant = await client_1.default.eventParticipant.create({
|
| 121 |
+
data: { eventId, userId },
|
| 122 |
+
});
|
| 123 |
+
await client_1.default.eventLeaderboard.create({
|
| 124 |
+
data: { eventId, userId, score: 0 },
|
| 125 |
+
});
|
| 126 |
+
res.json({ success: true, data: participant });
|
| 127 |
+
}
|
| 128 |
+
catch (error) {
|
| 129 |
+
res.status(500).json({ success: false, message: error.message });
|
| 130 |
+
}
|
| 131 |
+
};
|
| 132 |
+
exports.joinEvent = joinEvent;
|
| 133 |
+
const leaveEvent = async (req, res) => {
|
| 134 |
+
var _a;
|
| 135 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 136 |
+
const { eventId } = req.params;
|
| 137 |
+
try {
|
| 138 |
+
await client_1.default.eventParticipant.delete({
|
| 139 |
+
where: { eventId_userId: { eventId, userId } },
|
| 140 |
+
});
|
| 141 |
+
await client_1.default.eventLeaderboard.delete({
|
| 142 |
+
where: { eventId_userId: { eventId, userId } },
|
| 143 |
+
});
|
| 144 |
+
res.json({ success: true, message: "Left event successfully" });
|
| 145 |
+
}
|
| 146 |
+
catch (error) {
|
| 147 |
+
res.status(500).json({ success: false, message: error.message });
|
| 148 |
+
}
|
| 149 |
+
};
|
| 150 |
+
exports.leaveEvent = leaveEvent;
|
| 151 |
+
const updateEventScore = async (req, res) => {
|
| 152 |
+
const { eventId, userId } = req.params;
|
| 153 |
+
const { score } = req.body;
|
| 154 |
+
try {
|
| 155 |
+
const leaderboard = await client_1.default.eventLeaderboard.update({
|
| 156 |
+
where: { eventId_userId: { eventId, userId } },
|
| 157 |
+
data: { score },
|
| 158 |
+
});
|
| 159 |
+
const allScores = await client_1.default.eventLeaderboard.findMany({
|
| 160 |
+
where: { eventId },
|
| 161 |
+
orderBy: { score: "desc" },
|
| 162 |
+
});
|
| 163 |
+
for (let i = 0; i < allScores.length; i++) {
|
| 164 |
+
await client_1.default.eventLeaderboard.update({
|
| 165 |
+
where: { id: allScores[i].id },
|
| 166 |
+
data: { rank: i + 1 },
|
| 167 |
+
});
|
| 168 |
+
}
|
| 169 |
+
res.json({ success: true, data: leaderboard });
|
| 170 |
+
}
|
| 171 |
+
catch (error) {
|
| 172 |
+
res.status(500).json({ success: false, message: error.message });
|
| 173 |
+
}
|
| 174 |
+
};
|
| 175 |
+
exports.updateEventScore = updateEventScore;
|
| 176 |
+
const getLeaderboard = async (req, res) => {
|
| 177 |
+
const { eventId } = req.params;
|
| 178 |
+
const { limit = 100 } = req.query;
|
| 179 |
+
try {
|
| 180 |
+
const leaderboard = await client_1.default.eventLeaderboard.findMany({
|
| 181 |
+
where: { eventId },
|
| 182 |
+
include: {
|
| 183 |
+
user: {
|
| 184 |
+
select: {
|
| 185 |
+
id: true,
|
| 186 |
+
firstName: true,
|
| 187 |
+
lastName: true,
|
| 188 |
+
avatarUrl: true,
|
| 189 |
+
xp: true,
|
| 190 |
+
},
|
| 191 |
+
},
|
| 192 |
+
},
|
| 193 |
+
orderBy: { score: "desc" },
|
| 194 |
+
take: Number(limit),
|
| 195 |
+
});
|
| 196 |
+
res.json({ success: true, data: leaderboard });
|
| 197 |
+
}
|
| 198 |
+
catch (error) {
|
| 199 |
+
res.status(500).json({ success: false, message: error.message });
|
| 200 |
+
}
|
| 201 |
+
};
|
| 202 |
+
exports.getLeaderboard = getLeaderboard;
|
dist/controllers/gamification/index.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
| 3 |
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
| 4 |
+
};
|
| 5 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 6 |
+
exports.awardXP = exports.getUserAchievements = exports.getLeaderboard = void 0;
|
| 7 |
+
const client_1 = __importDefault(require("../../prisma/client"));
|
| 8 |
+
const redis_1 = __importDefault(require("../../utils/redis"));
|
| 9 |
+
const LEADERBOARD_CACHE_KEY = "leaderboard:top20";
|
| 10 |
+
const CACHE_TTL = 3600; // 1 hour
|
| 11 |
+
const getLeaderboard = async (req, res, next) => {
|
| 12 |
+
try {
|
| 13 |
+
// Try to get from cache
|
| 14 |
+
const cachedLeaderboard = await redis_1.default.get(LEADERBOARD_CACHE_KEY);
|
| 15 |
+
if (cachedLeaderboard) {
|
| 16 |
+
return res.json({ success: true, data: JSON.parse(cachedLeaderboard), cached: true });
|
| 17 |
+
}
|
| 18 |
+
const leaderboard = await client_1.default.user.findMany({
|
| 19 |
+
select: {
|
| 20 |
+
id: true,
|
| 21 |
+
firstName: true,
|
| 22 |
+
lastName: true,
|
| 23 |
+
avatarUrl: true,
|
| 24 |
+
xp: true,
|
| 25 |
+
streak: true
|
| 26 |
+
},
|
| 27 |
+
orderBy: { xp: "desc" },
|
| 28 |
+
take: 20
|
| 29 |
+
});
|
| 30 |
+
// Save to cache
|
| 31 |
+
await redis_1.default.setex(LEADERBOARD_CACHE_KEY, CACHE_TTL, JSON.stringify(leaderboard));
|
| 32 |
+
res.json({ success: true, data: leaderboard });
|
| 33 |
+
}
|
| 34 |
+
catch (error) {
|
| 35 |
+
next(error);
|
| 36 |
+
}
|
| 37 |
+
};
|
| 38 |
+
exports.getLeaderboard = getLeaderboard;
|
| 39 |
+
const getUserAchievements = async (req, res, next) => {
|
| 40 |
+
var _a;
|
| 41 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 42 |
+
try {
|
| 43 |
+
const achievements = await client_1.default.userAchievement.findMany({
|
| 44 |
+
where: { userId },
|
| 45 |
+
include: {
|
| 46 |
+
achievement: true
|
| 47 |
+
}
|
| 48 |
+
});
|
| 49 |
+
res.json({ success: true, data: achievements });
|
| 50 |
+
}
|
| 51 |
+
catch (error) {
|
| 52 |
+
next(error);
|
| 53 |
+
}
|
| 54 |
+
};
|
| 55 |
+
exports.getUserAchievements = getUserAchievements;
|
| 56 |
+
const awardXP = async (userId, amount) => {
|
| 57 |
+
try {
|
| 58 |
+
await client_1.default.user.update({
|
| 59 |
+
where: { id: userId },
|
| 60 |
+
data: {
|
| 61 |
+
xp: { increment: amount }
|
| 62 |
+
}
|
| 63 |
+
});
|
| 64 |
+
// Invalidate leaderboard cache when XP changes
|
| 65 |
+
await redis_1.default.del(LEADERBOARD_CACHE_KEY);
|
| 66 |
+
}
|
| 67 |
+
catch (error) {
|
| 68 |
+
console.error("Error awarding XP:", error);
|
| 69 |
+
}
|
| 70 |
+
};
|
| 71 |
+
exports.awardXP = awardXP;
|
dist/controllers/notifications/index.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
| 3 |
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
| 4 |
+
};
|
| 5 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 6 |
+
exports.deleteNotification = exports.markAllAsRead = exports.markAsRead = exports.getNotifications = void 0;
|
| 7 |
+
const client_1 = __importDefault(require("../../prisma/client"));
|
| 8 |
+
const getNotifications = async (req, res) => {
|
| 9 |
+
var _a;
|
| 10 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 11 |
+
const { page = 1, limit = 20, unreadOnly = false } = req.query;
|
| 12 |
+
try {
|
| 13 |
+
const where = { userId };
|
| 14 |
+
if (unreadOnly === "true") {
|
| 15 |
+
where.read = false;
|
| 16 |
+
}
|
| 17 |
+
const notifications = await client_1.default.notification.findMany({
|
| 18 |
+
where,
|
| 19 |
+
orderBy: { createdAt: "desc" },
|
| 20 |
+
take: Number(limit),
|
| 21 |
+
skip: (Number(page) - 1) * Number(limit),
|
| 22 |
+
});
|
| 23 |
+
const unreadCount = await client_1.default.notification.count({
|
| 24 |
+
where: { userId, read: false },
|
| 25 |
+
});
|
| 26 |
+
res.json({ success: true, data: { notifications, unreadCount } });
|
| 27 |
+
}
|
| 28 |
+
catch (error) {
|
| 29 |
+
res.status(500).json({ success: false, message: error.message });
|
| 30 |
+
}
|
| 31 |
+
};
|
| 32 |
+
exports.getNotifications = getNotifications;
|
| 33 |
+
const markAsRead = async (req, res) => {
|
| 34 |
+
var _a;
|
| 35 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 36 |
+
const { notificationId } = req.params;
|
| 37 |
+
try {
|
| 38 |
+
await client_1.default.notification.updateMany({
|
| 39 |
+
where: { id: notificationId, userId },
|
| 40 |
+
data: { read: true },
|
| 41 |
+
});
|
| 42 |
+
res.json({ success: true, message: "Notification marked as read" });
|
| 43 |
+
}
|
| 44 |
+
catch (error) {
|
| 45 |
+
res.status(500).json({ success: false, message: error.message });
|
| 46 |
+
}
|
| 47 |
+
};
|
| 48 |
+
exports.markAsRead = markAsRead;
|
| 49 |
+
const markAllAsRead = async (req, res) => {
|
| 50 |
+
var _a;
|
| 51 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 52 |
+
try {
|
| 53 |
+
await client_1.default.notification.updateMany({
|
| 54 |
+
where: { userId, read: false },
|
| 55 |
+
data: { read: true },
|
| 56 |
+
});
|
| 57 |
+
res.json({ success: true, message: "All notifications marked as read" });
|
| 58 |
+
}
|
| 59 |
+
catch (error) {
|
| 60 |
+
res.status(500).json({ success: false, message: error.message });
|
| 61 |
+
}
|
| 62 |
+
};
|
| 63 |
+
exports.markAllAsRead = markAllAsRead;
|
| 64 |
+
const deleteNotification = async (req, res) => {
|
| 65 |
+
var _a;
|
| 66 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 67 |
+
const { notificationId } = req.params;
|
| 68 |
+
try {
|
| 69 |
+
await client_1.default.notification.deleteMany({
|
| 70 |
+
where: { id: notificationId, userId },
|
| 71 |
+
});
|
| 72 |
+
res.json({ success: true, message: "Notification deleted" });
|
| 73 |
+
}
|
| 74 |
+
catch (error) {
|
| 75 |
+
res.status(500).json({ success: false, message: error.message });
|
| 76 |
+
}
|
| 77 |
+
};
|
| 78 |
+
exports.deleteNotification = deleteNotification;
|
dist/controllers/paths/index.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
| 3 |
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
| 4 |
+
};
|
| 5 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 6 |
+
exports.startPath = exports.getPathProgress = exports.getLearningPaths = void 0;
|
| 7 |
+
const client_1 = __importDefault(require("../../prisma/client"));
|
| 8 |
+
const getLearningPaths = async (req, res, next) => {
|
| 9 |
+
try {
|
| 10 |
+
const paths = await client_1.default.learningPath.findMany({
|
| 11 |
+
include: {
|
| 12 |
+
projects: {
|
| 13 |
+
include: {
|
| 14 |
+
project: {
|
| 15 |
+
select: {
|
| 16 |
+
title: true,
|
| 17 |
+
difficultyLevel: true,
|
| 18 |
+
},
|
| 19 |
+
},
|
| 20 |
+
},
|
| 21 |
+
orderBy: { orderIndex: "asc" },
|
| 22 |
+
},
|
| 23 |
+
},
|
| 24 |
+
});
|
| 25 |
+
res.json({ success: true, data: paths });
|
| 26 |
+
}
|
| 27 |
+
catch (error) {
|
| 28 |
+
next(error);
|
| 29 |
+
}
|
| 30 |
+
};
|
| 31 |
+
exports.getLearningPaths = getLearningPaths;
|
| 32 |
+
const getPathProgress = async (req, res, next) => {
|
| 33 |
+
var _a;
|
| 34 |
+
const { id: pathId } = req.params;
|
| 35 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 36 |
+
try {
|
| 37 |
+
const progress = await client_1.default.userPathProgress.findUnique({
|
| 38 |
+
where: {
|
| 39 |
+
userId_pathId: {
|
| 40 |
+
userId,
|
| 41 |
+
pathId,
|
| 42 |
+
},
|
| 43 |
+
},
|
| 44 |
+
include: {
|
| 45 |
+
path: {
|
| 46 |
+
include: {
|
| 47 |
+
projects: {
|
| 48 |
+
include: { project: true },
|
| 49 |
+
orderBy: { orderIndex: "asc" },
|
| 50 |
+
},
|
| 51 |
+
},
|
| 52 |
+
},
|
| 53 |
+
},
|
| 54 |
+
});
|
| 55 |
+
res.json({ success: true, data: progress });
|
| 56 |
+
}
|
| 57 |
+
catch (error) {
|
| 58 |
+
next(error);
|
| 59 |
+
}
|
| 60 |
+
};
|
| 61 |
+
exports.getPathProgress = getPathProgress;
|
| 62 |
+
const startPath = async (req, res, next) => {
|
| 63 |
+
var _a;
|
| 64 |
+
const { id: pathId } = req.params;
|
| 65 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 66 |
+
try {
|
| 67 |
+
const progress = await client_1.default.userPathProgress.upsert({
|
| 68 |
+
where: {
|
| 69 |
+
userId_pathId: {
|
| 70 |
+
userId,
|
| 71 |
+
pathId
|
| 72 |
+
}
|
| 73 |
+
},
|
| 74 |
+
update: {},
|
| 75 |
+
create: {
|
| 76 |
+
userId,
|
| 77 |
+
pathId,
|
| 78 |
+
currentProjectIndex: 0,
|
| 79 |
+
},
|
| 80 |
+
});
|
| 81 |
+
res.status(201).json({ success: true, data: progress });
|
| 82 |
+
}
|
| 83 |
+
catch (error) {
|
| 84 |
+
next(error);
|
| 85 |
+
}
|
| 86 |
+
};
|
| 87 |
+
exports.startPath = startPath;
|
dist/controllers/projects/index.js
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
| 3 |
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
| 4 |
+
};
|
| 5 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 6 |
+
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;
|
| 7 |
+
const client_1 = __importDefault(require("../../prisma/client"));
|
| 8 |
+
const file_1 = require("../../middlewares/file");
|
| 9 |
+
const auth_1 = require("../../middlewares/auth");
|
| 10 |
+
function generateSlug(title) {
|
| 11 |
+
return title
|
| 12 |
+
.toLowerCase()
|
| 13 |
+
.replace(/[^a-z0-9]+/g, "-")
|
| 14 |
+
.replace(/^-+|-+$/g, "");
|
| 15 |
+
}
|
| 16 |
+
const createProject = async (req, res) => {
|
| 17 |
+
var _a;
|
| 18 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 19 |
+
const { title, description, difficultyLevel, technologies, categories, estimatedTime, learningObjectives, resourceLinks, starterRepoUrl, difficultyModes, coverImage, milestones, } = req.body;
|
| 20 |
+
try {
|
| 21 |
+
if (!userId)
|
| 22 |
+
return res.status(401).json({ success: false, message: "Unauthorized" });
|
| 23 |
+
const user = await client_1.default.user.findUnique({ where: { id: userId } });
|
| 24 |
+
if (!user || (user.role !== "ADMIN" && user.role !== "CONTRIBUTOR")) {
|
| 25 |
+
return res.status(403).json({
|
| 26 |
+
success: false,
|
| 27 |
+
message: "Only Admins or Contributors can create projects",
|
| 28 |
+
});
|
| 29 |
+
}
|
| 30 |
+
let imageUrl = coverImage;
|
| 31 |
+
if (coverImage && !coverImage.startsWith("http")) {
|
| 32 |
+
imageUrl = await (0, file_1.upLoadFiles)(coverImage);
|
| 33 |
+
}
|
| 34 |
+
const project = await client_1.default.project.create({
|
| 35 |
+
data: {
|
| 36 |
+
title,
|
| 37 |
+
slug: generateSlug(title),
|
| 38 |
+
description,
|
| 39 |
+
difficultyLevel: difficultyLevel,
|
| 40 |
+
technologies,
|
| 41 |
+
categories,
|
| 42 |
+
estimatedTime,
|
| 43 |
+
learningObjectives,
|
| 44 |
+
resourceLinks: resourceLinks || [],
|
| 45 |
+
starterRepoUrl,
|
| 46 |
+
difficultyModes: difficultyModes || ["GUIDED", "STANDARD", "HARDCORE"],
|
| 47 |
+
createdBy: { connect: { id: userId } },
|
| 48 |
+
milestones: {
|
| 49 |
+
create: milestones === null || milestones === void 0 ? void 0 : milestones.map((m, index) => ({
|
| 50 |
+
milestoneNumber: index + 1,
|
| 51 |
+
title: m.title,
|
| 52 |
+
description: m.description,
|
| 53 |
+
hints: m.hints || [],
|
| 54 |
+
validationCriteria: m.validationCriteria,
|
| 55 |
+
})),
|
| 56 |
+
},
|
| 57 |
+
},
|
| 58 |
+
include: {
|
| 59 |
+
milestones: true,
|
| 60 |
+
},
|
| 61 |
+
});
|
| 62 |
+
res.status(201).json({ success: true, data: project });
|
| 63 |
+
}
|
| 64 |
+
catch (error) {
|
| 65 |
+
res.status(500).json({ success: false, message: error.message });
|
| 66 |
+
}
|
| 67 |
+
};
|
| 68 |
+
exports.createProject = createProject;
|
| 69 |
+
const updateProject = async (req, res) => {
|
| 70 |
+
var _a;
|
| 71 |
+
const { id } = req.params;
|
| 72 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 73 |
+
const updateData = req.body;
|
| 74 |
+
try {
|
| 75 |
+
const project = await client_1.default.project.findUnique({ where: { id } });
|
| 76 |
+
if (!project)
|
| 77 |
+
return res
|
| 78 |
+
.status(404)
|
| 79 |
+
.json({ success: false, message: "Project not found" });
|
| 80 |
+
// Check permissions
|
| 81 |
+
const user = await client_1.default.user.findUnique({ where: { id: userId } });
|
| 82 |
+
if (!user || (user.role !== "ADMIN" && project.createdById !== userId)) {
|
| 83 |
+
return res.status(403).json({
|
| 84 |
+
success: false,
|
| 85 |
+
message: "Unauthorized to update this project",
|
| 86 |
+
});
|
| 87 |
+
}
|
| 88 |
+
const updatedProject = await client_1.default.project.update({
|
| 89 |
+
where: { id },
|
| 90 |
+
data: updateData,
|
| 91 |
+
include: { milestones: true },
|
| 92 |
+
});
|
| 93 |
+
res.json({ success: true, data: updatedProject });
|
| 94 |
+
}
|
| 95 |
+
catch (error) {
|
| 96 |
+
res.status(500).json({ success: false, message: error.message });
|
| 97 |
+
}
|
| 98 |
+
};
|
| 99 |
+
exports.updateProject = updateProject;
|
| 100 |
+
const deleteProject = async (req, res) => {
|
| 101 |
+
var _a;
|
| 102 |
+
const { id } = req.params;
|
| 103 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 104 |
+
try {
|
| 105 |
+
const project = await client_1.default.project.findUnique({ where: { id } });
|
| 106 |
+
if (!project)
|
| 107 |
+
return res
|
| 108 |
+
.status(404)
|
| 109 |
+
.json({ success: false, message: "Project not found" });
|
| 110 |
+
const user = await client_1.default.user.findUnique({ where: { id: userId } });
|
| 111 |
+
if (!user || user.role !== "ADMIN") {
|
| 112 |
+
return res
|
| 113 |
+
.status(403)
|
| 114 |
+
.json({ success: false, message: "Only Admins can delete projects" });
|
| 115 |
+
}
|
| 116 |
+
await client_1.default.project.delete({ where: { id } });
|
| 117 |
+
res.json({ success: true, message: "Project deleted successfully" });
|
| 118 |
+
}
|
| 119 |
+
catch (error) {
|
| 120 |
+
res.status(500).json({ success: false, message: error.message });
|
| 121 |
+
}
|
| 122 |
+
};
|
| 123 |
+
exports.deleteProject = deleteProject;
|
| 124 |
+
const getProjects = async (req, res) => {
|
| 125 |
+
const { difficulty, tech, category, title } = req.query;
|
| 126 |
+
try {
|
| 127 |
+
const where = {};
|
| 128 |
+
if (difficulty)
|
| 129 |
+
where.difficultyLevel = difficulty;
|
| 130 |
+
if (tech)
|
| 131 |
+
where.technologies = { has: tech };
|
| 132 |
+
if (category)
|
| 133 |
+
where.categories = { has: category };
|
| 134 |
+
if (title)
|
| 135 |
+
where.title = { contains: title, mode: "insensitive" };
|
| 136 |
+
const projects = await client_1.default.project.findMany({
|
| 137 |
+
where,
|
| 138 |
+
include: {
|
| 139 |
+
createdBy: {
|
| 140 |
+
select: { firstName: true, lastName: true },
|
| 141 |
+
},
|
| 142 |
+
},
|
| 143 |
+
orderBy: { createdAt: "desc" },
|
| 144 |
+
});
|
| 145 |
+
res.json({ success: true, data: projects });
|
| 146 |
+
}
|
| 147 |
+
catch (error) {
|
| 148 |
+
res.status(500).json({ success: false, message: error.message });
|
| 149 |
+
}
|
| 150 |
+
};
|
| 151 |
+
exports.getProjects = getProjects;
|
| 152 |
+
const getProjectById = async (req, res) => {
|
| 153 |
+
var _a;
|
| 154 |
+
const { id } = req.params;
|
| 155 |
+
try {
|
| 156 |
+
const user = await (0, auth_1.checkAuth)(req);
|
| 157 |
+
const project = (await client_1.default.project.findUnique({
|
| 158 |
+
where: { id },
|
| 159 |
+
include: {
|
| 160 |
+
milestones: { orderBy: { milestoneNumber: "asc" } },
|
| 161 |
+
createdBy: { select: { firstName: true, lastName: true } },
|
| 162 |
+
userProjects: user ? { where: { userId: user.id } } : false,
|
| 163 |
+
},
|
| 164 |
+
}));
|
| 165 |
+
if (!project)
|
| 166 |
+
return res
|
| 167 |
+
.status(404)
|
| 168 |
+
.json({ success: false, message: "Project not found" });
|
| 169 |
+
// If user is authenticated, fetch their progress and completed milestones for each mode
|
| 170 |
+
let progressByMode = {};
|
| 171 |
+
if (user && project.userProjects) {
|
| 172 |
+
for (const up of project.userProjects) {
|
| 173 |
+
const completedMilestones = await client_1.default.userMilestone.findMany({
|
| 174 |
+
where: {
|
| 175 |
+
userId: user.id,
|
| 176 |
+
userProjectId: up.id,
|
| 177 |
+
},
|
| 178 |
+
select: { milestoneId: true },
|
| 179 |
+
});
|
| 180 |
+
progressByMode[up.difficultyModeChosen] = {
|
| 181 |
+
status: up.status,
|
| 182 |
+
completedMilestones: completedMilestones.map((m) => m.milestoneId),
|
| 183 |
+
userProjectId: up.id,
|
| 184 |
+
};
|
| 185 |
+
}
|
| 186 |
+
}
|
| 187 |
+
res.json({
|
| 188 |
+
success: true,
|
| 189 |
+
data: {
|
| 190 |
+
...project,
|
| 191 |
+
progressByMode,
|
| 192 |
+
// For backward compatibility
|
| 193 |
+
userProgress: ((_a = project.userProjects) === null || _a === void 0 ? void 0 : _a[0]) || null,
|
| 194 |
+
},
|
| 195 |
+
});
|
| 196 |
+
}
|
| 197 |
+
catch (error) {
|
| 198 |
+
res.status(500).json({ success: false, message: error.message });
|
| 199 |
+
}
|
| 200 |
+
};
|
| 201 |
+
exports.getProjectById = getProjectById;
|
| 202 |
+
// User Progress Endpoints
|
| 203 |
+
const startProject = async (req, res) => {
|
| 204 |
+
var _a;
|
| 205 |
+
const { id: projectId } = req.params;
|
| 206 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 207 |
+
const { difficultyModeChosen } = req.body;
|
| 208 |
+
try {
|
| 209 |
+
const mode = difficultyModeChosen || "STANDARD";
|
| 210 |
+
const existingProgress = await client_1.default.userProject.findUnique({
|
| 211 |
+
where: {
|
| 212 |
+
userId_projectId_difficultyModeChosen: {
|
| 213 |
+
userId,
|
| 214 |
+
projectId,
|
| 215 |
+
difficultyModeChosen: mode,
|
| 216 |
+
},
|
| 217 |
+
},
|
| 218 |
+
});
|
| 219 |
+
if (existingProgress) {
|
| 220 |
+
return res.status(400).json({
|
| 221 |
+
success: false,
|
| 222 |
+
message: "Project already started in this mode",
|
| 223 |
+
});
|
| 224 |
+
}
|
| 225 |
+
const progress = await client_1.default.userProject.create({
|
| 226 |
+
data: {
|
| 227 |
+
userId,
|
| 228 |
+
projectId,
|
| 229 |
+
difficultyModeChosen: mode,
|
| 230 |
+
status: "IN_PROGRESS",
|
| 231 |
+
},
|
| 232 |
+
});
|
| 233 |
+
res.status(201).json({ success: true, data: progress });
|
| 234 |
+
}
|
| 235 |
+
catch (error) {
|
| 236 |
+
res.status(500).json({ success: false, message: error.message });
|
| 237 |
+
}
|
| 238 |
+
};
|
| 239 |
+
exports.startProject = startProject;
|
| 240 |
+
const updateProgress = async (req, res) => {
|
| 241 |
+
var _a;
|
| 242 |
+
const { id: projectId } = req.params;
|
| 243 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 244 |
+
const { status, repoUrl, difficultyMode } = req.body;
|
| 245 |
+
try {
|
| 246 |
+
const progress = await client_1.default.userProject.update({
|
| 247 |
+
where: {
|
| 248 |
+
userId_projectId_difficultyModeChosen: {
|
| 249 |
+
userId,
|
| 250 |
+
projectId,
|
| 251 |
+
difficultyModeChosen: difficultyMode,
|
| 252 |
+
},
|
| 253 |
+
},
|
| 254 |
+
data: { status: status, repoUrl },
|
| 255 |
+
});
|
| 256 |
+
res.json({ success: true, data: progress });
|
| 257 |
+
}
|
| 258 |
+
catch (error) {
|
| 259 |
+
res.status(500).json({ success: false, message: error.message });
|
| 260 |
+
}
|
| 261 |
+
};
|
| 262 |
+
exports.updateProgress = updateProgress;
|
| 263 |
+
const completeProject = async (req, res) => {
|
| 264 |
+
var _a;
|
| 265 |
+
const { id: projectId } = req.params;
|
| 266 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 267 |
+
const { repoUrl, difficultyMode } = req.body;
|
| 268 |
+
try {
|
| 269 |
+
const progress = await client_1.default.userProject.update({
|
| 270 |
+
where: {
|
| 271 |
+
userId_projectId_difficultyModeChosen: {
|
| 272 |
+
userId,
|
| 273 |
+
projectId,
|
| 274 |
+
difficultyModeChosen: difficultyMode,
|
| 275 |
+
},
|
| 276 |
+
},
|
| 277 |
+
data: {
|
| 278 |
+
status: "COMPLETED",
|
| 279 |
+
completedAt: new Date(),
|
| 280 |
+
repoUrl,
|
| 281 |
+
},
|
| 282 |
+
});
|
| 283 |
+
// Award XP (simple example)
|
| 284 |
+
await client_1.default.user.update({
|
| 285 |
+
where: { id: userId },
|
| 286 |
+
data: { xp: { increment: 100 } },
|
| 287 |
+
});
|
| 288 |
+
// Update project completion rate
|
| 289 |
+
const totalStarted = await client_1.default.userProject.count({
|
| 290 |
+
where: { projectId },
|
| 291 |
+
});
|
| 292 |
+
const totalCompleted = await client_1.default.userProject.count({
|
| 293 |
+
where: { projectId, status: "COMPLETED" },
|
| 294 |
+
});
|
| 295 |
+
await client_1.default.project.update({
|
| 296 |
+
where: { id: projectId },
|
| 297 |
+
data: {
|
| 298 |
+
completionRate: (totalCompleted / totalStarted) * 100,
|
| 299 |
+
submissionCount: { increment: 1 },
|
| 300 |
+
},
|
| 301 |
+
});
|
| 302 |
+
res.json({ success: true, data: progress });
|
| 303 |
+
}
|
| 304 |
+
catch (error) {
|
| 305 |
+
res.status(500).json({ success: false, message: error.message });
|
| 306 |
+
}
|
| 307 |
+
};
|
| 308 |
+
exports.completeProject = completeProject;
|
| 309 |
+
const getUserProgress = async (req, res) => {
|
| 310 |
+
var _a;
|
| 311 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 312 |
+
try {
|
| 313 |
+
const progress = await client_1.default.userProject.findMany({
|
| 314 |
+
where: { userId },
|
| 315 |
+
include: {
|
| 316 |
+
project: {
|
| 317 |
+
select: { title: true, difficultyLevel: true },
|
| 318 |
+
},
|
| 319 |
+
},
|
| 320 |
+
});
|
| 321 |
+
res.json({ success: true, data: progress });
|
| 322 |
+
}
|
| 323 |
+
catch (error) {
|
| 324 |
+
res.status(500).json({ success: false, message: error.message });
|
| 325 |
+
}
|
| 326 |
+
};
|
| 327 |
+
exports.getUserProgress = getUserProgress;
|
| 328 |
+
// Submissions
|
| 329 |
+
const submitProject = async (req, res) => {
|
| 330 |
+
var _a;
|
| 331 |
+
const { id: projectId } = req.params;
|
| 332 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 333 |
+
const { repoUrl } = req.body;
|
| 334 |
+
try {
|
| 335 |
+
const submission = await client_1.default.submission.create({
|
| 336 |
+
data: {
|
| 337 |
+
userId,
|
| 338 |
+
projectId,
|
| 339 |
+
repoUrl,
|
| 340 |
+
},
|
| 341 |
+
});
|
| 342 |
+
res.status(201).json({ success: true, data: submission });
|
| 343 |
+
}
|
| 344 |
+
catch (error) {
|
| 345 |
+
res.status(500).json({ success: false, message: error.message });
|
| 346 |
+
}
|
| 347 |
+
};
|
| 348 |
+
exports.submitProject = submitProject;
|
| 349 |
+
const completeMilestone = async (req, res) => {
|
| 350 |
+
var _a;
|
| 351 |
+
const { id: projectId, milestoneId } = req.params;
|
| 352 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 353 |
+
const { difficultyMode } = req.body;
|
| 354 |
+
try {
|
| 355 |
+
const userProject = await client_1.default.userProject.findUnique({
|
| 356 |
+
where: {
|
| 357 |
+
userId_projectId_difficultyModeChosen: {
|
| 358 |
+
userId,
|
| 359 |
+
projectId,
|
| 360 |
+
difficultyModeChosen: difficultyMode,
|
| 361 |
+
},
|
| 362 |
+
},
|
| 363 |
+
});
|
| 364 |
+
if (!userProject) {
|
| 365 |
+
return res.status(400).json({
|
| 366 |
+
success: false,
|
| 367 |
+
message: "Project not started by user in this mode",
|
| 368 |
+
});
|
| 369 |
+
}
|
| 370 |
+
const existingMilestone = await client_1.default.userMilestone.findUnique({
|
| 371 |
+
where: {
|
| 372 |
+
userId_milestoneId_userProjectId: {
|
| 373 |
+
userId,
|
| 374 |
+
milestoneId,
|
| 375 |
+
userProjectId: userProject.id,
|
| 376 |
+
},
|
| 377 |
+
},
|
| 378 |
+
});
|
| 379 |
+
if (existingMilestone) {
|
| 380 |
+
return res.status(400).json({
|
| 381 |
+
success: false,
|
| 382 |
+
message: "Milestone already completed in this mode",
|
| 383 |
+
});
|
| 384 |
+
}
|
| 385 |
+
const milestone = await client_1.default.userMilestone.create({
|
| 386 |
+
data: {
|
| 387 |
+
userId,
|
| 388 |
+
milestoneId,
|
| 389 |
+
userProjectId: userProject.id,
|
| 390 |
+
},
|
| 391 |
+
});
|
| 392 |
+
res.status(201).json({ success: true, data: milestone });
|
| 393 |
+
}
|
| 394 |
+
catch (error) {
|
| 395 |
+
res.status(500).json({ success: false, message: error.message });
|
| 396 |
+
}
|
| 397 |
+
};
|
| 398 |
+
exports.completeMilestone = completeMilestone;
|
| 399 |
+
const getFeaturedProjects = async (req, res) => {
|
| 400 |
+
try {
|
| 401 |
+
const featuredProjects = await client_1.default.project.findMany({
|
| 402 |
+
take: 4,
|
| 403 |
+
orderBy: { submissionCount: "desc" },
|
| 404 |
+
include: {
|
| 405 |
+
createdBy: {
|
| 406 |
+
select: { firstName: true, lastName: true },
|
| 407 |
+
},
|
| 408 |
+
},
|
| 409 |
+
});
|
| 410 |
+
res.json({ success: true, data: featuredProjects });
|
| 411 |
+
}
|
| 412 |
+
catch (error) {
|
| 413 |
+
res.status(500).json({ success: false, message: error.message });
|
| 414 |
+
}
|
| 415 |
+
};
|
| 416 |
+
exports.getFeaturedProjects = getFeaturedProjects;
|
dist/controllers/social/index.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
| 3 |
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
| 4 |
+
};
|
| 5 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 6 |
+
exports.shareProject = exports.getActivityFeed = exports.getFollowing = exports.getFollowers = exports.unfollowUser = exports.followUser = void 0;
|
| 7 |
+
const client_1 = __importDefault(require("../../prisma/client"));
|
| 8 |
+
const followUser = async (req, res) => {
|
| 9 |
+
var _a;
|
| 10 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 11 |
+
const { followingId } = req.body;
|
| 12 |
+
try {
|
| 13 |
+
if (userId === followingId) {
|
| 14 |
+
return res
|
| 15 |
+
.status(400)
|
| 16 |
+
.json({ success: false, message: "Cannot follow yourself" });
|
| 17 |
+
}
|
| 18 |
+
const existingFollow = await client_1.default.follow.findUnique({
|
| 19 |
+
where: { followerId_followingId: { followerId: userId, followingId } },
|
| 20 |
+
});
|
| 21 |
+
if (existingFollow) {
|
| 22 |
+
return res
|
| 23 |
+
.status(400)
|
| 24 |
+
.json({ success: false, message: "Already following this user" });
|
| 25 |
+
}
|
| 26 |
+
const follow = await client_1.default.follow.create({
|
| 27 |
+
data: { followerId: userId, followingId },
|
| 28 |
+
});
|
| 29 |
+
await client_1.default.notification.create({
|
| 30 |
+
data: {
|
| 31 |
+
userId: followingId,
|
| 32 |
+
type: "FOLLOW",
|
| 33 |
+
title: "New Follower",
|
| 34 |
+
message: "Someone started following you",
|
| 35 |
+
link: `/profile/${userId}`,
|
| 36 |
+
},
|
| 37 |
+
});
|
| 38 |
+
res.json({ success: true, data: follow });
|
| 39 |
+
}
|
| 40 |
+
catch (error) {
|
| 41 |
+
res.status(500).json({ success: false, message: error.message });
|
| 42 |
+
}
|
| 43 |
+
};
|
| 44 |
+
exports.followUser = followUser;
|
| 45 |
+
const unfollowUser = async (req, res) => {
|
| 46 |
+
var _a;
|
| 47 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 48 |
+
const { followingId } = req.params;
|
| 49 |
+
try {
|
| 50 |
+
await client_1.default.follow.delete({
|
| 51 |
+
where: { followerId_followingId: { followerId: userId, followingId } },
|
| 52 |
+
});
|
| 53 |
+
res.json({ success: true, message: "Unfollowed successfully" });
|
| 54 |
+
}
|
| 55 |
+
catch (error) {
|
| 56 |
+
res.status(500).json({ success: false, message: error.message });
|
| 57 |
+
}
|
| 58 |
+
};
|
| 59 |
+
exports.unfollowUser = unfollowUser;
|
| 60 |
+
const getFollowers = async (req, res) => {
|
| 61 |
+
const { userId } = req.params;
|
| 62 |
+
try {
|
| 63 |
+
const followers = await client_1.default.follow.findMany({
|
| 64 |
+
where: { followingId: userId },
|
| 65 |
+
include: {
|
| 66 |
+
follower: {
|
| 67 |
+
select: {
|
| 68 |
+
id: true,
|
| 69 |
+
firstName: true,
|
| 70 |
+
lastName: true,
|
| 71 |
+
avatarUrl: true,
|
| 72 |
+
xp: true,
|
| 73 |
+
},
|
| 74 |
+
},
|
| 75 |
+
},
|
| 76 |
+
});
|
| 77 |
+
res.json({ success: true, data: followers });
|
| 78 |
+
}
|
| 79 |
+
catch (error) {
|
| 80 |
+
res.status(500).json({ success: false, message: error.message });
|
| 81 |
+
}
|
| 82 |
+
};
|
| 83 |
+
exports.getFollowers = getFollowers;
|
| 84 |
+
const getFollowing = async (req, res) => {
|
| 85 |
+
const { userId } = req.params;
|
| 86 |
+
try {
|
| 87 |
+
const following = await client_1.default.follow.findMany({
|
| 88 |
+
where: { followerId: userId },
|
| 89 |
+
include: {
|
| 90 |
+
following: {
|
| 91 |
+
select: {
|
| 92 |
+
id: true,
|
| 93 |
+
firstName: true,
|
| 94 |
+
lastName: true,
|
| 95 |
+
avatarUrl: true,
|
| 96 |
+
xp: true,
|
| 97 |
+
},
|
| 98 |
+
},
|
| 99 |
+
},
|
| 100 |
+
});
|
| 101 |
+
res.json({ success: true, data: following });
|
| 102 |
+
}
|
| 103 |
+
catch (error) {
|
| 104 |
+
res.status(500).json({ success: false, message: error.message });
|
| 105 |
+
}
|
| 106 |
+
};
|
| 107 |
+
exports.getFollowing = getFollowing;
|
| 108 |
+
const getActivityFeed = async (req, res) => {
|
| 109 |
+
var _a;
|
| 110 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 111 |
+
const { page = 1, limit = 20 } = req.query;
|
| 112 |
+
try {
|
| 113 |
+
const following = await client_1.default.follow.findMany({
|
| 114 |
+
where: { followerId: userId },
|
| 115 |
+
select: { followingId: true },
|
| 116 |
+
});
|
| 117 |
+
const followingIds = following.map((f) => f.followingId);
|
| 118 |
+
const activities = await client_1.default.userProject.findMany({
|
| 119 |
+
where: {
|
| 120 |
+
userId: { in: followingIds },
|
| 121 |
+
status: "COMPLETED",
|
| 122 |
+
},
|
| 123 |
+
include: {
|
| 124 |
+
user: {
|
| 125 |
+
select: {
|
| 126 |
+
id: true,
|
| 127 |
+
firstName: true,
|
| 128 |
+
lastName: true,
|
| 129 |
+
avatarUrl: true,
|
| 130 |
+
},
|
| 131 |
+
},
|
| 132 |
+
project: {
|
| 133 |
+
select: {
|
| 134 |
+
id: true,
|
| 135 |
+
title: true,
|
| 136 |
+
slug: true,
|
| 137 |
+
},
|
| 138 |
+
},
|
| 139 |
+
},
|
| 140 |
+
orderBy: { completedAt: "desc" },
|
| 141 |
+
take: Number(limit),
|
| 142 |
+
skip: (Number(page) - 1) * Number(limit),
|
| 143 |
+
});
|
| 144 |
+
res.json({ success: true, data: activities });
|
| 145 |
+
}
|
| 146 |
+
catch (error) {
|
| 147 |
+
res.status(500).json({ success: false, message: error.message });
|
| 148 |
+
}
|
| 149 |
+
};
|
| 150 |
+
exports.getActivityFeed = getActivityFeed;
|
| 151 |
+
const shareProject = async (req, res) => {
|
| 152 |
+
var _a;
|
| 153 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 154 |
+
const { projectId, platform } = req.body;
|
| 155 |
+
try {
|
| 156 |
+
const project = await client_1.default.project.findUnique({
|
| 157 |
+
where: { id: projectId },
|
| 158 |
+
});
|
| 159 |
+
if (!project) {
|
| 160 |
+
return res
|
| 161 |
+
.status(404)
|
| 162 |
+
.json({ success: false, message: "Project not found" });
|
| 163 |
+
}
|
| 164 |
+
const shareUrl = `${process.env.CLIENT_URL}/projects/${project.slug}`;
|
| 165 |
+
const ogImageUrl = `${process.env.CLIENT_URL}/api/og-image/${project.slug}`;
|
| 166 |
+
const share = await client_1.default.socialShare.create({
|
| 167 |
+
data: {
|
| 168 |
+
userId,
|
| 169 |
+
projectId,
|
| 170 |
+
platform,
|
| 171 |
+
shareUrl,
|
| 172 |
+
ogImageUrl,
|
| 173 |
+
},
|
| 174 |
+
});
|
| 175 |
+
res.json({ success: true, data: { shareUrl, ogImageUrl, share } });
|
| 176 |
+
}
|
| 177 |
+
catch (error) {
|
| 178 |
+
res.status(500).json({ success: false, message: error.message });
|
| 179 |
+
}
|
| 180 |
+
};
|
| 181 |
+
exports.shareProject = shareProject;
|
dist/controllers/teams/index.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
| 3 |
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
| 4 |
+
};
|
| 5 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 6 |
+
exports.getTeamProjects = exports.joinTeam = exports.createTeam = void 0;
|
| 7 |
+
const client_1 = __importDefault(require("../../prisma/client"));
|
| 8 |
+
const createTeam = async (req, res, next) => {
|
| 9 |
+
var _a;
|
| 10 |
+
const { name, projectId } = req.body;
|
| 11 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 12 |
+
try {
|
| 13 |
+
const team = await client_1.default.team.create({
|
| 14 |
+
data: {
|
| 15 |
+
name,
|
| 16 |
+
projectId,
|
| 17 |
+
members: {
|
| 18 |
+
create: {
|
| 19 |
+
userId,
|
| 20 |
+
},
|
| 21 |
+
},
|
| 22 |
+
},
|
| 23 |
+
include: {
|
| 24 |
+
members: true,
|
| 25 |
+
},
|
| 26 |
+
});
|
| 27 |
+
res.status(201).json({ success: true, data: team });
|
| 28 |
+
}
|
| 29 |
+
catch (error) {
|
| 30 |
+
next(error);
|
| 31 |
+
}
|
| 32 |
+
};
|
| 33 |
+
exports.createTeam = createTeam;
|
| 34 |
+
const joinTeam = async (req, res, next) => {
|
| 35 |
+
var _a;
|
| 36 |
+
const { id: teamId } = req.params;
|
| 37 |
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
|
| 38 |
+
try {
|
| 39 |
+
const teamMember = await client_1.default.teamMember.create({
|
| 40 |
+
data: {
|
| 41 |
+
teamId,
|
| 42 |
+
userId,
|
| 43 |
+
},
|
| 44 |
+
});
|
| 45 |
+
res.status(201).json({ success: true, data: teamMember });
|
| 46 |
+
}
|
| 47 |
+
catch (error) {
|
| 48 |
+
next(error);
|
| 49 |
+
}
|
| 50 |
+
};
|
| 51 |
+
exports.joinTeam = joinTeam;
|
| 52 |
+
const getTeamProjects = async (req, res, next) => {
|
| 53 |
+
const { id: teamId } = req.params;
|
| 54 |
+
try {
|
| 55 |
+
const team = await client_1.default.team.findUnique({
|
| 56 |
+
where: { id: teamId },
|
| 57 |
+
include: {
|
| 58 |
+
project: true,
|
| 59 |
+
},
|
| 60 |
+
});
|
| 61 |
+
if (!team)
|
| 62 |
+
return res
|
| 63 |
+
.status(404)
|
| 64 |
+
.json({ success: false, message: "Team not found" });
|
| 65 |
+
res.json({ success: true, data: team.project });
|
| 66 |
+
}
|
| 67 |
+
catch (error) {
|
| 68 |
+
next(error);
|
| 69 |
+
}
|
| 70 |
+
};
|
| 71 |
+
exports.getTeamProjects = getTeamProjects;
|
dist/index.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
| 3 |
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
| 4 |
+
};
|
| 5 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 6 |
+
exports.appRoute = void 0;
|
| 7 |
+
const dotenv_1 = __importDefault(require("dotenv"));
|
| 8 |
+
const express_1 = __importDefault(require("express"));
|
| 9 |
+
const cors_1 = __importDefault(require("cors"));
|
| 10 |
+
const cookie_parser_1 = __importDefault(require("cookie-parser"));
|
| 11 |
+
exports.appRoute = (0, express_1.default)();
|
| 12 |
+
const auth_1 = __importDefault(require("./routes/auth"));
|
| 13 |
+
const project_1 = __importDefault(require("./routes/project"));
|
| 14 |
+
const user_1 = __importDefault(require("./routes/user"));
|
| 15 |
+
const ai_1 = __importDefault(require("./routes/ai"));
|
| 16 |
+
const code_review_1 = __importDefault(require("./routes/code-review"));
|
| 17 |
+
const community_1 = __importDefault(require("./routes/community"));
|
| 18 |
+
const teams_1 = __importDefault(require("./routes/teams"));
|
| 19 |
+
const gamification_1 = __importDefault(require("./routes/gamification"));
|
| 20 |
+
const paths_1 = __importDefault(require("./routes/paths"));
|
| 21 |
+
const analytics_1 = __importDefault(require("./routes/analytics"));
|
| 22 |
+
const social_1 = __importDefault(require("./routes/social"));
|
| 23 |
+
const notifications_1 = __importDefault(require("./routes/notifications"));
|
| 24 |
+
const events_1 = __importDefault(require("./routes/events"));
|
| 25 |
+
const errorHandler_1 = require("./middlewares/errorHandler");
|
| 26 |
+
const passport_1 = __importDefault(require("./utils/passport"));
|
| 27 |
+
dotenv_1.default.config();
|
| 28 |
+
const allowedOrigins = process.env.CLIENT_URL
|
| 29 |
+
? process.env.CLIENT_URL.split(",").map((url) => url.trim())
|
| 30 |
+
: ["http://localhost:3002"];
|
| 31 |
+
console.log("CORS Allowed Origins:", allowedOrigins);
|
| 32 |
+
exports.appRoute.use((0, cors_1.default)({
|
| 33 |
+
origin: (origin, callback) => {
|
| 34 |
+
console.log("Incoming origin:", origin);
|
| 35 |
+
if (!origin ||
|
| 36 |
+
allowedOrigins.some((allowed) => origin.startsWith(allowed))) {
|
| 37 |
+
callback(null, true);
|
| 38 |
+
}
|
| 39 |
+
else {
|
| 40 |
+
console.warn("Blocked by CORS:", origin);
|
| 41 |
+
callback(new Error("Not allowed by CORS"));
|
| 42 |
+
}
|
| 43 |
+
},
|
| 44 |
+
credentials: true,
|
| 45 |
+
allowedHeaders: ["Content-Type", "Authorization"],
|
| 46 |
+
methods: ["GET", "POST", "PUT", "DELETE"],
|
| 47 |
+
exposedHeaders: ["Content-Disposition"],
|
| 48 |
+
}));
|
| 49 |
+
console.log("ENV:", process.env.CLIENT_URL);
|
| 50 |
+
exports.appRoute.use(express_1.default.json({ limit: "50mb" }));
|
| 51 |
+
exports.appRoute.use(express_1.default.urlencoded({ limit: "50mb", extended: true }));
|
| 52 |
+
exports.appRoute.use((0, cookie_parser_1.default)());
|
| 53 |
+
exports.appRoute.use(passport_1.default.initialize());
|
| 54 |
+
exports.appRoute.get("/", (req, res) => {
|
| 55 |
+
res.send("Backend is working!");
|
| 56 |
+
});
|
| 57 |
+
exports.appRoute.use("/api/auth", auth_1.default);
|
| 58 |
+
exports.appRoute.use("/api/projects", project_1.default);
|
| 59 |
+
exports.appRoute.use("/api/user", user_1.default);
|
| 60 |
+
exports.appRoute.use("/api/ai", ai_1.default);
|
| 61 |
+
exports.appRoute.use("/api/code-review", code_review_1.default);
|
| 62 |
+
exports.appRoute.use("/api/community", community_1.default);
|
| 63 |
+
exports.appRoute.use("/api/teams", teams_1.default);
|
| 64 |
+
exports.appRoute.use("/api/gamification", gamification_1.default);
|
| 65 |
+
exports.appRoute.use("/api/paths", paths_1.default);
|
| 66 |
+
exports.appRoute.use("/api/analytics", analytics_1.default);
|
| 67 |
+
exports.appRoute.use("/api/social", social_1.default);
|
| 68 |
+
exports.appRoute.use("/api/notifications", notifications_1.default);
|
| 69 |
+
exports.appRoute.use("/api/events", events_1.default);
|
| 70 |
+
exports.appRoute.use(errorHandler_1.errorHandler);
|
dist/middlewares/auth.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
| 3 |
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
| 4 |
+
};
|
| 5 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 6 |
+
exports.checkAuth = exports.authenticateToken = void 0;
|
| 7 |
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
| 8 |
+
const authenticateToken = (req, res, next) => {
|
| 9 |
+
var _a;
|
| 10 |
+
const token = req.cookies.token || ((_a = req.headers["authorization"]) === null || _a === void 0 ? void 0 : _a.split(" ")[1]);
|
| 11 |
+
if (!token)
|
| 12 |
+
res.sendStatus(401);
|
| 13 |
+
jsonwebtoken_1.default.verify(token, process.env.JWT_SECRET, (err, user) => {
|
| 14 |
+
if (err)
|
| 15 |
+
res.sendStatus(403);
|
| 16 |
+
req.user = user;
|
| 17 |
+
next();
|
| 18 |
+
});
|
| 19 |
+
};
|
| 20 |
+
exports.authenticateToken = authenticateToken;
|
| 21 |
+
const checkAuth = async (req) => {
|
| 22 |
+
var _a;
|
| 23 |
+
let user = { id: "" };
|
| 24 |
+
const token = req.cookies.token || ((_a = req.headers["authorization"]) === null || _a === void 0 ? void 0 : _a.split(" ")[1]);
|
| 25 |
+
if (!token)
|
| 26 |
+
return null;
|
| 27 |
+
jsonwebtoken_1.default.verify(token, process.env.JWT_SECRET, (err, decoded) => {
|
| 28 |
+
user = decoded;
|
| 29 |
+
});
|
| 30 |
+
return user;
|
| 31 |
+
};
|
| 32 |
+
exports.checkAuth = checkAuth;
|
dist/middlewares/email.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
| 3 |
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
| 4 |
+
};
|
| 5 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 6 |
+
exports.sendEmail = void 0;
|
| 7 |
+
const dotenv_1 = __importDefault(require("dotenv"));
|
| 8 |
+
const nodemailer_1 = __importDefault(require("nodemailer"));
|
| 9 |
+
dotenv_1.default.config();
|
| 10 |
+
const sendEmail = async (email, subject, html, text) => {
|
| 11 |
+
try {
|
| 12 |
+
const transporter = nodemailer_1.default.createTransport({
|
| 13 |
+
host: "smtp.gmail.com",
|
| 14 |
+
secure: true,
|
| 15 |
+
service: "gmail",
|
| 16 |
+
auth: {
|
| 17 |
+
user: process.env.EMAIL_USERNAME,
|
| 18 |
+
pass: process.env.EMAIL_PASSWORD,
|
| 19 |
+
},
|
| 20 |
+
});
|
| 21 |
+
const options = () => {
|
| 22 |
+
return {
|
| 23 |
+
from: `CV Builder <${process.env.EMAIL_USERNAME}>`,
|
| 24 |
+
to: email,
|
| 25 |
+
subject,
|
| 26 |
+
html: html,
|
| 27 |
+
text: text,
|
| 28 |
+
};
|
| 29 |
+
};
|
| 30 |
+
await transporter.sendMail(options());
|
| 31 |
+
}
|
| 32 |
+
catch (error) {
|
| 33 |
+
return error;
|
| 34 |
+
}
|
| 35 |
+
};
|
| 36 |
+
exports.sendEmail = sendEmail;
|
dist/middlewares/errorHandler.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 3 |
+
exports.errorHandler = void 0;
|
| 4 |
+
const client_1 = require("@prisma/client");
|
| 5 |
+
const errorHandler = (err, req, res, next) => {
|
| 6 |
+
var _a, _b;
|
| 7 |
+
console.error("Error:", err);
|
| 8 |
+
let status = 500;
|
| 9 |
+
let message = "Something went wrong. Please try again.";
|
| 10 |
+
let errors = undefined;
|
| 11 |
+
// Prisma Error Handling
|
| 12 |
+
if (err instanceof client_1.Prisma.PrismaClientKnownRequestError) {
|
| 13 |
+
switch (err.code) {
|
| 14 |
+
case "P2002": // Unique constraint violation
|
| 15 |
+
status = 409;
|
| 16 |
+
const target = ((_a = err.meta) === null || _a === void 0 ? void 0 : _a.target) || [];
|
| 17 |
+
message = `${target.join(", ")} already exists.`;
|
| 18 |
+
break;
|
| 19 |
+
case "P2025": // Record not found
|
| 20 |
+
status = 404;
|
| 21 |
+
message = ((_b = err.meta) === null || _b === void 0 ? void 0 : _b.cause) || "Record not found.";
|
| 22 |
+
break;
|
| 23 |
+
case "P2003": // Foreign key constraint violation
|
| 24 |
+
status = 400;
|
| 25 |
+
message = "Foreign key constraint failed.";
|
| 26 |
+
break;
|
| 27 |
+
default:
|
| 28 |
+
status = 400;
|
| 29 |
+
message = `Database error: ${err.message}`;
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
else if (err instanceof client_1.Prisma.PrismaClientValidationError) {
|
| 33 |
+
status = 400;
|
| 34 |
+
message = "Validation error in database request.";
|
| 35 |
+
}
|
| 36 |
+
else if (err.name === "ValidationError") {
|
| 37 |
+
status = 400;
|
| 38 |
+
message = err.message;
|
| 39 |
+
}
|
| 40 |
+
else if (err.status) {
|
| 41 |
+
status = err.status;
|
| 42 |
+
message = err.message;
|
| 43 |
+
}
|
| 44 |
+
res.status(status).json({
|
| 45 |
+
success: false,
|
| 46 |
+
message,
|
| 47 |
+
errors,
|
| 48 |
+
stack: process.env.NODE_ENV === "development" ? err.stack : undefined,
|
| 49 |
+
});
|
| 50 |
+
};
|
| 51 |
+
exports.errorHandler = errorHandler;
|
dist/middlewares/file.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 3 |
+
exports.upLoadFiles = exports.mapFiles = void 0;
|
| 4 |
+
const dotenv = require("dotenv");
|
| 5 |
+
dotenv.config();
|
| 6 |
+
const cloudinary = require("cloudinary").v2;
|
| 7 |
+
cloudinary.config({
|
| 8 |
+
cloud_name: process.env.CLOUDINARY_NAME,
|
| 9 |
+
api_key: process.env.CLOUDINARY_KEY,
|
| 10 |
+
api_secret: process.env.CLOUDINARY_SECRET,
|
| 11 |
+
});
|
| 12 |
+
const mapFiles = async (files) => {
|
| 13 |
+
let fls = [];
|
| 14 |
+
if (files && (files === null || files === void 0 ? void 0 : files.length) > 0) {
|
| 15 |
+
for await (let file of files) {
|
| 16 |
+
fls.push({
|
| 17 |
+
name: file === null || file === void 0 ? void 0 : file.name,
|
| 18 |
+
type: file === null || file === void 0 ? void 0 : file.type,
|
| 19 |
+
uri: (file === null || file === void 0 ? void 0 : file.uri.includes("res.cloudinary.com"))
|
| 20 |
+
? file === null || file === void 0 ? void 0 : file.uri
|
| 21 |
+
: await (0, exports.upLoadFiles)(file === null || file === void 0 ? void 0 : file.uri, file === null || file === void 0 ? void 0 : file.name),
|
| 22 |
+
});
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
return fls;
|
| 26 |
+
};
|
| 27 |
+
exports.mapFiles = mapFiles;
|
| 28 |
+
const upLoadFiles = async (file, fileName) => {
|
| 29 |
+
const uri = await cloudinary.uploader.upload(file, { public_id: fileName });
|
| 30 |
+
return uri === null || uri === void 0 ? void 0 : uri.secure_url;
|
| 31 |
+
};
|
| 32 |
+
exports.upLoadFiles = upLoadFiles;
|
dist/middlewares/generateOTP.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 3 |
+
exports.generateOTP = void 0;
|
| 4 |
+
const generateOTP = () => {
|
| 5 |
+
let otp;
|
| 6 |
+
const otpDate = Date.now();
|
| 7 |
+
const numbers = 123456;
|
| 8 |
+
// const length: number = Math.floor(Math.random() * 2)
|
| 9 |
+
const randomNumbers = Math.floor(numbers + Math.random() * 100000);
|
| 10 |
+
otp = randomNumbers;
|
| 11 |
+
return { otp, otpDate };
|
| 12 |
+
};
|
| 13 |
+
exports.generateOTP = generateOTP;
|
dist/models/auth/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
| 3 |
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
| 4 |
+
};
|
| 5 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 6 |
+
const mongoose_1 = __importDefault(require("mongoose"));
|
| 7 |
+
const { isEmail } = require("validator");
|
| 8 |
+
const Schema = mongoose_1.default.Schema;
|
| 9 |
+
const auth = new Schema({
|
| 10 |
+
firstName: { type: String, required: true, min: 3 },
|
| 11 |
+
lastName: { type: String, required: true, min: 3 },
|
| 12 |
+
email: {
|
| 13 |
+
type: String,
|
| 14 |
+
required: true,
|
| 15 |
+
unique: true,
|
| 16 |
+
lowercase: true,
|
| 17 |
+
validate: isEmail,
|
| 18 |
+
},
|
| 19 |
+
password: { type: String, required: true, min: 6 },
|
| 20 |
+
manageOTP: {
|
| 21 |
+
otp: { type: Number },
|
| 22 |
+
otpDate: { type: Number },
|
| 23 |
+
},
|
| 24 |
+
}, { timestamps: true });
|
| 25 |
+
const userAuth = mongoose_1.default.model("user", auth);
|
| 26 |
+
exports.default = userAuth;
|
dist/models/project/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 3 |
+
exports.Project = void 0;
|
| 4 |
+
const mongoose_1 = require("mongoose");
|
| 5 |
+
const resourceSchema = new mongoose_1.Schema({
|
| 6 |
+
type: { type: String, enum: ["video", "article", "course"], required: true },
|
| 7 |
+
url: { type: String, required: true },
|
| 8 |
+
title: { type: String, required: true },
|
| 9 |
+
});
|
| 10 |
+
const projectSchema = new mongoose_1.Schema({
|
| 11 |
+
title: { type: String, required: true },
|
| 12 |
+
difficulty: {
|
| 13 |
+
type: String,
|
| 14 |
+
enum: ["beginner", "intermediate", "advanced"],
|
| 15 |
+
required: true,
|
| 16 |
+
},
|
| 17 |
+
description: { type: String, required: true },
|
| 18 |
+
requirements: [{ type: String }],
|
| 19 |
+
resources: [resourceSchema],
|
| 20 |
+
}, { timestamps: true });
|
| 21 |
+
exports.Project = (0, mongoose_1.model)("Project", projectSchema);
|
dist/prisma.config.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 3 |
+
// This file was generated by Prisma and assumes you have installed the following:
|
| 4 |
+
// npm install --save-dev prisma dotenv
|
| 5 |
+
require("dotenv/config");
|
| 6 |
+
const config_1 = require("prisma/config");
|
| 7 |
+
exports.default = (0, config_1.defineConfig)({
|
| 8 |
+
schema: "prisma/schema.prisma",
|
| 9 |
+
migrations: {
|
| 10 |
+
path: "prisma/migrations",
|
| 11 |
+
},
|
| 12 |
+
engine: "classic",
|
| 13 |
+
datasource: {
|
| 14 |
+
url: (0, config_1.env)("DATABASE_URL"),
|
| 15 |
+
},
|
| 16 |
+
});
|
dist/prisma/client.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 3 |
+
const client_1 = require("@prisma/client");
|
| 4 |
+
const prisma = new client_1.PrismaClient();
|
| 5 |
+
exports.default = prisma;
|
dist/prisma/migrations/generate-slugs.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 3 |
+
const client_1 = require("@prisma/client");
|
| 4 |
+
const prisma = new client_1.PrismaClient();
|
| 5 |
+
function generateSlug(title) {
|
| 6 |
+
return title
|
| 7 |
+
.toLowerCase()
|
| 8 |
+
.replace(/[^a-z0-9]+/g, "-")
|
| 9 |
+
.replace(/^-+|-+$/g, "");
|
| 10 |
+
}
|
| 11 |
+
async function generateSlugs() {
|
| 12 |
+
const projects = await prisma.project.findMany({
|
| 13 |
+
where: { slug: null },
|
| 14 |
+
});
|
| 15 |
+
console.log(`Generating slugs for ${projects.length} projects...`);
|
| 16 |
+
for (const project of projects) {
|
| 17 |
+
let slug = generateSlug(project.title);
|
| 18 |
+
let counter = 1;
|
| 19 |
+
// Handle duplicates
|
| 20 |
+
while (await prisma.project.findUnique({ where: { slug } })) {
|
| 21 |
+
slug = `${generateSlug(project.title)}-${counter}`;
|
| 22 |
+
counter++;
|
| 23 |
+
}
|
| 24 |
+
await prisma.project.update({
|
| 25 |
+
where: { id: project.id },
|
| 26 |
+
data: { slug },
|
| 27 |
+
});
|
| 28 |
+
console.log(`✓ ${project.title} -> ${slug}`);
|
| 29 |
+
}
|
| 30 |
+
console.log("Done!");
|
| 31 |
+
}
|
| 32 |
+
generateSlugs()
|
| 33 |
+
.catch(console.error)
|
| 34 |
+
.finally(() => prisma.$disconnect());
|
dist/prisma/seed.js
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 3 |
+
const client_1 = require("@prisma/client");
|
| 4 |
+
const prisma = new client_1.PrismaClient();
|
| 5 |
+
function generateSlug(title) {
|
| 6 |
+
return title
|
| 7 |
+
.toLowerCase()
|
| 8 |
+
.replace(/[^a-z0-9]+/g, "-")
|
| 9 |
+
.replace(/^-+|-+$/g, "");
|
| 10 |
+
}
|
| 11 |
+
const projects = [
|
| 12 |
+
{
|
| 13 |
+
title: "Landing Page Template",
|
| 14 |
+
slug: "landing-page-template",
|
| 15 |
+
difficultyLevel: client_1.Difficulty.BEGINNER,
|
| 16 |
+
description: "Build a responsive landing page with modern design principles",
|
| 17 |
+
technologies: ["HTML", "CSS"],
|
| 18 |
+
categories: ["Frontend", "Web Design"],
|
| 19 |
+
estimatedTime: "4-6 hours",
|
| 20 |
+
learningObjectives: [
|
| 21 |
+
"Responsive design",
|
| 22 |
+
"CSS Flexbox/Grid",
|
| 23 |
+
"Modern UI patterns",
|
| 24 |
+
],
|
| 25 |
+
resourceLinks: [
|
| 26 |
+
{ type: "video", url: "#", title: "HTML & CSS Crash Course" },
|
| 27 |
+
],
|
| 28 |
+
featured: true,
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
title: "E-Commerce Dashboard",
|
| 32 |
+
slug: "ecommerce-dashboard",
|
| 33 |
+
difficultyLevel: client_1.Difficulty.INTERMEDIATE,
|
| 34 |
+
description: "Create dynamic admin dashboard with React and Chart.js",
|
| 35 |
+
technologies: ["React", "Chart.js"],
|
| 36 |
+
categories: ["Frontend", "Data Visualization"],
|
| 37 |
+
estimatedTime: "12-16 hours",
|
| 38 |
+
learningObjectives: [
|
| 39 |
+
"React state management",
|
| 40 |
+
"Data visualization",
|
| 41 |
+
"Dashboard UX",
|
| 42 |
+
],
|
| 43 |
+
resourceLinks: [
|
| 44 |
+
{ type: "article", url: "#", title: "React State Management Guide" },
|
| 45 |
+
],
|
| 46 |
+
featured: true,
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
title: "E-Commerce REST API",
|
| 50 |
+
slug: "ecommerce-rest-api",
|
| 51 |
+
difficultyLevel: client_1.Difficulty.INTERMEDIATE,
|
| 52 |
+
description: "Develop a Node.js API with Express and MongoDB for product management",
|
| 53 |
+
technologies: ["Node.js", "Express", "MongoDB"],
|
| 54 |
+
categories: ["Backend", "API Development"],
|
| 55 |
+
estimatedTime: "16-20 hours",
|
| 56 |
+
learningObjectives: [
|
| 57 |
+
"RESTful API design",
|
| 58 |
+
"Database modeling",
|
| 59 |
+
"Authentication",
|
| 60 |
+
],
|
| 61 |
+
resourceLinks: [
|
| 62 |
+
{ type: "course", url: "#", title: "Node.js Fundamentals" },
|
| 63 |
+
],
|
| 64 |
+
featured: false,
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
title: "Social Media Dashboard",
|
| 68 |
+
slug: "social-media-dashboard",
|
| 69 |
+
difficultyLevel: client_1.Difficulty.ADVANCED,
|
| 70 |
+
description: "Build real-time dashboard with React, GraphQL and WebSockets",
|
| 71 |
+
technologies: ["React", "GraphQL", "WebSockets"],
|
| 72 |
+
categories: ["Full-stack", "Real-time"],
|
| 73 |
+
estimatedTime: "24-32 hours",
|
| 74 |
+
learningObjectives: [
|
| 75 |
+
"GraphQL subscriptions",
|
| 76 |
+
"Real-time data",
|
| 77 |
+
"Advanced state management",
|
| 78 |
+
],
|
| 79 |
+
resourceLinks: [
|
| 80 |
+
{ type: "video", url: "#", title: "GraphQL Subscriptions" },
|
| 81 |
+
],
|
| 82 |
+
featured: true,
|
| 83 |
+
},
|
| 84 |
+
{
|
| 85 |
+
title: "Portfolio Website",
|
| 86 |
+
slug: "portfolio-website",
|
| 87 |
+
difficultyLevel: client_1.Difficulty.BEGINNER,
|
| 88 |
+
description: "Create a personal portfolio with Next.js and Tailwind CSS",
|
| 89 |
+
technologies: ["Next.js", "Tailwind CSS"],
|
| 90 |
+
categories: ["Frontend", "Web Design"],
|
| 91 |
+
estimatedTime: "8-10 hours",
|
| 92 |
+
learningObjectives: [
|
| 93 |
+
"Next.js basics",
|
| 94 |
+
"Tailwind CSS",
|
| 95 |
+
"Portfolio best practices",
|
| 96 |
+
],
|
| 97 |
+
resourceLinks: [
|
| 98 |
+
{ type: "article", url: "#", title: "Next.js Deployment Guide" },
|
| 99 |
+
],
|
| 100 |
+
featured: false,
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
title: "React Todo App with Firebase",
|
| 104 |
+
slug: "react-todo-firebase",
|
| 105 |
+
difficultyLevel: client_1.Difficulty.INTERMEDIATE,
|
| 106 |
+
description: "Create a collaborative todo application with real-time sync",
|
| 107 |
+
technologies: ["React", "Firebase"],
|
| 108 |
+
categories: ["Frontend", "Real-time"],
|
| 109 |
+
estimatedTime: "10-14 hours",
|
| 110 |
+
learningObjectives: [
|
| 111 |
+
"Firebase integration",
|
| 112 |
+
"Real-time sync",
|
| 113 |
+
"CRUD operations",
|
| 114 |
+
],
|
| 115 |
+
resourceLinks: [
|
| 116 |
+
{ type: "article", url: "#", title: "Firebase Realtime Database Guide" },
|
| 117 |
+
],
|
| 118 |
+
featured: false,
|
| 119 |
+
},
|
| 120 |
+
{
|
| 121 |
+
title: "Authentication System",
|
| 122 |
+
slug: "authentication-system",
|
| 123 |
+
difficultyLevel: client_1.Difficulty.ADVANCED,
|
| 124 |
+
description: "Implement OAuth 2.0 flow with Node.js and Passport.js",
|
| 125 |
+
technologies: ["Node.js", "Passport.js"],
|
| 126 |
+
categories: ["Backend", "Security"],
|
| 127 |
+
estimatedTime: "20-24 hours",
|
| 128 |
+
learningObjectives: ["OAuth 2.0", "JWT tokens", "Security best practices"],
|
| 129 |
+
resourceLinks: [
|
| 130 |
+
{ type: "course", url: "#", title: "Web Security Fundamentals" },
|
| 131 |
+
],
|
| 132 |
+
featured: false,
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
title: "Mobile Recipe App",
|
| 136 |
+
slug: "mobile-recipe-app",
|
| 137 |
+
difficultyLevel: client_1.Difficulty.INTERMEDIATE,
|
| 138 |
+
description: "Build cross-platform recipe manager with React Native",
|
| 139 |
+
technologies: ["React Native"],
|
| 140 |
+
categories: ["Mobile", "Frontend"],
|
| 141 |
+
estimatedTime: "18-22 hours",
|
| 142 |
+
learningObjectives: [
|
| 143 |
+
"React Native basics",
|
| 144 |
+
"Mobile navigation",
|
| 145 |
+
"API integration",
|
| 146 |
+
],
|
| 147 |
+
resourceLinks: [
|
| 148 |
+
{ type: "video", url: "#", title: "React Native Navigation Tutorial" },
|
| 149 |
+
],
|
| 150 |
+
featured: false,
|
| 151 |
+
},
|
| 152 |
+
{
|
| 153 |
+
title: "CMS Platform",
|
| 154 |
+
slug: "cms-platform",
|
| 155 |
+
difficultyLevel: client_1.Difficulty.ADVANCED,
|
| 156 |
+
description: "Headless CMS with Next.js and Sanity.io",
|
| 157 |
+
technologies: ["Next.js", "Sanity.io"],
|
| 158 |
+
categories: ["Full-stack", "CMS"],
|
| 159 |
+
estimatedTime: "28-36 hours",
|
| 160 |
+
learningObjectives: ["Headless CMS", "Content modeling", "Next.js ISR"],
|
| 161 |
+
resourceLinks: [
|
| 162 |
+
{ type: "article", url: "#", title: "Sanity Studio Customization" },
|
| 163 |
+
],
|
| 164 |
+
featured: true,
|
| 165 |
+
},
|
| 166 |
+
{
|
| 167 |
+
title: "AI Chat Interface",
|
| 168 |
+
slug: "ai-chat-interface",
|
| 169 |
+
difficultyLevel: client_1.Difficulty.ADVANCED,
|
| 170 |
+
description: "Build GPT-4 chatbot with streaming responses",
|
| 171 |
+
technologies: ["GPT-4", "Streaming"],
|
| 172 |
+
categories: ["AI/ML", "Full-stack"],
|
| 173 |
+
estimatedTime: "24-30 hours",
|
| 174 |
+
learningObjectives: ["LLM integration", "Streaming responses", "Chat UX"],
|
| 175 |
+
resourceLinks: [
|
| 176 |
+
{ type: "course", url: "#", title: "LLM Integration Patterns" },
|
| 177 |
+
],
|
| 178 |
+
featured: true,
|
| 179 |
+
},
|
| 180 |
+
{
|
| 181 |
+
title: "Real-time Chat Application",
|
| 182 |
+
slug: "realtime-chat-app",
|
| 183 |
+
difficultyLevel: client_1.Difficulty.INTERMEDIATE,
|
| 184 |
+
description: "Build chat app with Socket.io and React",
|
| 185 |
+
technologies: ["Socket.io", "React"],
|
| 186 |
+
categories: ["Full-stack", "Real-time"],
|
| 187 |
+
estimatedTime: "14-18 hours",
|
| 188 |
+
learningObjectives: [
|
| 189 |
+
"WebSocket protocol",
|
| 190 |
+
"Real-time messaging",
|
| 191 |
+
"Chat features",
|
| 192 |
+
],
|
| 193 |
+
resourceLinks: [
|
| 194 |
+
{ type: "video", url: "#", title: "WebSocket Fundamentals" },
|
| 195 |
+
],
|
| 196 |
+
featured: false,
|
| 197 |
+
},
|
| 198 |
+
{
|
| 199 |
+
title: "Serverless API Gateway",
|
| 200 |
+
slug: "serverless-api-gateway",
|
| 201 |
+
difficultyLevel: client_1.Difficulty.ADVANCED,
|
| 202 |
+
description: "Create REST API using AWS Lambda & API Gateway",
|
| 203 |
+
technologies: ["AWS Lambda", "API Gateway"],
|
| 204 |
+
categories: ["Backend", "Cloud"],
|
| 205 |
+
estimatedTime: "20-26 hours",
|
| 206 |
+
learningObjectives: [
|
| 207 |
+
"Serverless architecture",
|
| 208 |
+
"AWS services",
|
| 209 |
+
"API design",
|
| 210 |
+
],
|
| 211 |
+
resourceLinks: [
|
| 212 |
+
{ type: "article", url: "#", title: "AWS Serverless Patterns" },
|
| 213 |
+
],
|
| 214 |
+
featured: false,
|
| 215 |
+
},
|
| 216 |
+
{
|
| 217 |
+
title: "E-commerce Product Search",
|
| 218 |
+
slug: "ecommerce-product-search",
|
| 219 |
+
difficultyLevel: client_1.Difficulty.ADVANCED,
|
| 220 |
+
description: "Implement search functionality with Algolia",
|
| 221 |
+
technologies: ["Algolia"],
|
| 222 |
+
categories: ["Frontend", "Search"],
|
| 223 |
+
estimatedTime: "16-20 hours",
|
| 224 |
+
learningObjectives: [
|
| 225 |
+
"Search algorithms",
|
| 226 |
+
"Algolia integration",
|
| 227 |
+
"Search UX",
|
| 228 |
+
],
|
| 229 |
+
resourceLinks: [
|
| 230 |
+
{ type: "course", url: "#", title: "Search Engine Optimization" },
|
| 231 |
+
],
|
| 232 |
+
featured: false,
|
| 233 |
+
},
|
| 234 |
+
{
|
| 235 |
+
title: "Mobile Payment Integration",
|
| 236 |
+
slug: "mobile-payment-integration",
|
| 237 |
+
difficultyLevel: client_1.Difficulty.ADVANCED,
|
| 238 |
+
description: "Add Stripe payments to React Native app",
|
| 239 |
+
technologies: ["React Native", "Stripe"],
|
| 240 |
+
categories: ["Mobile", "Payments"],
|
| 241 |
+
estimatedTime: "18-24 hours",
|
| 242 |
+
learningObjectives: ["Payment processing", "Stripe API", "Mobile security"],
|
| 243 |
+
resourceLinks: [
|
| 244 |
+
{ type: "video", url: "#", title: "Mobile Payment Systems" },
|
| 245 |
+
],
|
| 246 |
+
featured: false,
|
| 247 |
+
},
|
| 248 |
+
{
|
| 249 |
+
title: "Data Visualization Dashboard",
|
| 250 |
+
slug: "data-visualization-dashboard",
|
| 251 |
+
difficultyLevel: client_1.Difficulty.INTERMEDIATE,
|
| 252 |
+
description: "Create analytics dashboard with D3.js",
|
| 253 |
+
technologies: ["D3.js"],
|
| 254 |
+
categories: ["Frontend", "Data Visualization"],
|
| 255 |
+
estimatedTime: "14-18 hours",
|
| 256 |
+
learningObjectives: [
|
| 257 |
+
"D3.js fundamentals",
|
| 258 |
+
"Data visualization",
|
| 259 |
+
"Interactive charts",
|
| 260 |
+
],
|
| 261 |
+
resourceLinks: [{ type: "article", url: "#", title: "D3.js Essentials" }],
|
| 262 |
+
featured: false,
|
| 263 |
+
},
|
| 264 |
+
];
|
| 265 |
+
async function main() {
|
| 266 |
+
console.log("Start seeding...");
|
| 267 |
+
// Create a default user if not exists
|
| 268 |
+
const user = await prisma.user.upsert({
|
| 269 |
+
where: { email: "admin@devresource.com" },
|
| 270 |
+
update: {},
|
| 271 |
+
create: {
|
| 272 |
+
email: "admin@devresource.com",
|
| 273 |
+
firstName: "Admin",
|
| 274 |
+
lastName: "User",
|
| 275 |
+
password: "password123", // In a real app, this should be hashed
|
| 276 |
+
role: "ADMIN",
|
| 277 |
+
},
|
| 278 |
+
});
|
| 279 |
+
console.log(`Created user with id: ${user.id}`);
|
| 280 |
+
for (const project of projects) {
|
| 281 |
+
const createdProject = await prisma.project.upsert({
|
| 282 |
+
where: { slug: project.slug },
|
| 283 |
+
update: {
|
| 284 |
+
...project,
|
| 285 |
+
createdById: user.id,
|
| 286 |
+
},
|
| 287 |
+
create: {
|
| 288 |
+
...project,
|
| 289 |
+
createdById: user.id,
|
| 290 |
+
},
|
| 291 |
+
});
|
| 292 |
+
console.log(`Created project with id: ${createdProject.id}`);
|
| 293 |
+
}
|
| 294 |
+
console.log("Seeding finished.");
|
| 295 |
+
}
|
| 296 |
+
main()
|
| 297 |
+
.catch((e) => {
|
| 298 |
+
console.error(e);
|
| 299 |
+
process.exit(1);
|
| 300 |
+
})
|
| 301 |
+
.finally(async () => {
|
| 302 |
+
await prisma.$disconnect();
|
| 303 |
+
});
|
dist/routes/ai.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 3 |
+
const express_1 = require("express");
|
| 4 |
+
const ai_1 = require("../controllers/ai");
|
| 5 |
+
const auth_1 = require("../middlewares/auth");
|
| 6 |
+
const router = (0, express_1.Router)();
|
| 7 |
+
router.post("/chat", auth_1.authenticateToken, ai_1.chatWithAI);
|
| 8 |
+
router.get("/chat", auth_1.authenticateToken, ai_1.getAllChatMessages);
|
| 9 |
+
router.get("/chat/history/:projectId", auth_1.authenticateToken, ai_1.getChatHistory);
|
| 10 |
+
router.post("/hint-request", auth_1.authenticateToken, ai_1.requestHint);
|
| 11 |
+
exports.default = router;
|
dist/routes/analytics.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 3 |
+
const express_1 = require("express");
|
| 4 |
+
const analytics_1 = require("../controllers/analytics");
|
| 5 |
+
const auth_1 = require("../middlewares/auth");
|
| 6 |
+
const router = (0, express_1.Router)();
|
| 7 |
+
router.get("/user", auth_1.authenticateToken, analytics_1.getUserAnalytics);
|
| 8 |
+
router.get("/admin", auth_1.authenticateToken, analytics_1.getAdminAnalytics);
|
| 9 |
+
exports.default = router;
|
dist/routes/auth.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
| 3 |
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
| 4 |
+
};
|
| 5 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 6 |
+
const express_1 = __importDefault(require("express"));
|
| 7 |
+
const auth_1 = require("../controllers/auth");
|
| 8 |
+
const auth_2 = require("../middlewares/auth");
|
| 9 |
+
const passport_1 = __importDefault(require("../utils/passport"));
|
| 10 |
+
const router = express_1.default.Router();
|
| 11 |
+
router.post("/register", auth_1.signUp);
|
| 12 |
+
router.post("/login", auth_1.login);
|
| 13 |
+
router.post("/logout", auth_1.logout);
|
| 14 |
+
router.get("/me", auth_2.authenticateToken, auth_1.me);
|
| 15 |
+
router.post("/update/:id", auth_2.authenticateToken, auth_1.updateUser);
|
| 16 |
+
router.get("/user/:id", auth_2.authenticateToken, auth_1.getUser);
|
| 17 |
+
router.post("/forgetPassword", auth_1.forgetPassword);
|
| 18 |
+
router.post("/verifyOtp", auth_1.verifyOTP);
|
| 19 |
+
router.post("/resetPassword", auth_1.resetPassword);
|
| 20 |
+
router.get("/google", passport_1.default.authenticate("google", { scope: ["profile", "email"] }));
|
| 21 |
+
router.get("/google/callback", passport_1.default.authenticate("google", {
|
| 22 |
+
session: false,
|
| 23 |
+
failureRedirect: `${process.env.CLIENT_URL}/auth/signin?error=auth_failed`,
|
| 24 |
+
}), auth_1.googleAuthCallback);
|
| 25 |
+
router.get("/github", passport_1.default.authenticate("github", { scope: ["user:email"] }));
|
| 26 |
+
router.get("/github/callback", passport_1.default.authenticate("github", {
|
| 27 |
+
session: false,
|
| 28 |
+
failureRedirect: `${process.env.CLIENT_URL}/auth/signin?error=auth_failed`,
|
| 29 |
+
}), auth_1.githubAuthCallback);
|
| 30 |
+
exports.default = router;
|
dist/routes/code-review.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 3 |
+
const express_1 = require("express");
|
| 4 |
+
const code_review_1 = require("../controllers/code-review");
|
| 5 |
+
const auth_1 = require("../middlewares/auth");
|
| 6 |
+
const router = (0, express_1.Router)();
|
| 7 |
+
router.post("/analyze", auth_1.authenticateToken, code_review_1.analyzeCode);
|
| 8 |
+
router.get("/:id", auth_1.authenticateToken, code_review_1.getSubmissionReview);
|
| 9 |
+
exports.default = router;
|
dist/routes/community.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 3 |
+
const express_1 = require("express");
|
| 4 |
+
const community_1 = require("../controllers/community");
|
| 5 |
+
const auth_1 = require("../middlewares/auth");
|
| 6 |
+
const router = (0, express_1.Router)();
|
| 7 |
+
router.get("/submissions/public", community_1.getPublicSubmissions);
|
| 8 |
+
router.post("/submissions/:id/comment", auth_1.authenticateToken, community_1.commentOnSubmission);
|
| 9 |
+
router.post("/submissions/:id/vote", auth_1.authenticateToken, community_1.voteOnSubmission);
|
| 10 |
+
router.get("/forum/threads", community_1.getForumThreads);
|
| 11 |
+
router.post("/forum/threads", auth_1.authenticateToken, community_1.createForumThread);
|
| 12 |
+
exports.default = router;
|
dist/routes/events.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
| 3 |
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
| 4 |
+
};
|
| 5 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 6 |
+
const express_1 = __importDefault(require("express"));
|
| 7 |
+
const events_1 = require("../controllers/events");
|
| 8 |
+
const auth_1 = require("../middlewares/auth");
|
| 9 |
+
const router = express_1.default.Router();
|
| 10 |
+
router.post("/", auth_1.authenticateToken, events_1.createEvent);
|
| 11 |
+
router.get("/", events_1.getEvents);
|
| 12 |
+
router.get("/:id", events_1.getEvent);
|
| 13 |
+
router.post("/:eventId/join", auth_1.authenticateToken, events_1.joinEvent);
|
| 14 |
+
router.delete("/:eventId/leave", auth_1.authenticateToken, events_1.leaveEvent);
|
| 15 |
+
router.patch("/:eventId/score/:userId", auth_1.authenticateToken, events_1.updateEventScore);
|
| 16 |
+
router.get("/:eventId/leaderboard", events_1.getLeaderboard);
|
| 17 |
+
exports.default = router;
|
dist/routes/gamification.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 3 |
+
const express_1 = require("express");
|
| 4 |
+
const gamification_1 = require("../controllers/gamification");
|
| 5 |
+
const auth_1 = require("../middlewares/auth");
|
| 6 |
+
const router = (0, express_1.Router)();
|
| 7 |
+
router.get("/leaderboard", gamification_1.getLeaderboard);
|
| 8 |
+
router.get("/achievements", auth_1.authenticateToken, gamification_1.getUserAchievements);
|
| 9 |
+
exports.default = router;
|
dist/routes/notifications.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
| 3 |
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
| 4 |
+
};
|
| 5 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 6 |
+
const express_1 = __importDefault(require("express"));
|
| 7 |
+
const notifications_1 = require("../controllers/notifications");
|
| 8 |
+
const auth_1 = require("../middlewares/auth");
|
| 9 |
+
const router = express_1.default.Router();
|
| 10 |
+
router.get("/", auth_1.authenticateToken, notifications_1.getNotifications);
|
| 11 |
+
router.patch("/:notificationId/read", auth_1.authenticateToken, notifications_1.markAsRead);
|
| 12 |
+
router.patch("/read-all", auth_1.authenticateToken, notifications_1.markAllAsRead);
|
| 13 |
+
router.delete("/:notificationId", auth_1.authenticateToken, notifications_1.deleteNotification);
|
| 14 |
+
exports.default = router;
|
dist/routes/paths.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 3 |
+
const express_1 = require("express");
|
| 4 |
+
const paths_1 = require("../controllers/paths");
|
| 5 |
+
const auth_1 = require("../middlewares/auth");
|
| 6 |
+
const router = (0, express_1.Router)();
|
| 7 |
+
router.get("/", paths_1.getLearningPaths);
|
| 8 |
+
router.get("/:id/progress", auth_1.authenticateToken, paths_1.getPathProgress);
|
| 9 |
+
router.post("/:id/start", auth_1.authenticateToken, paths_1.startPath);
|
| 10 |
+
exports.default = router;
|
dist/routes/project.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 3 |
+
const express_1 = require("express");
|
| 4 |
+
const projects_1 = require("../controllers/projects");
|
| 5 |
+
const auth_1 = require("../middlewares/auth");
|
| 6 |
+
const router = (0, express_1.Router)();
|
| 7 |
+
router.get("/", projects_1.getProjects);
|
| 8 |
+
router.get("/progress", auth_1.authenticateToken, projects_1.getUserProgress);
|
| 9 |
+
router.get("/featured", projects_1.getFeaturedProjects);
|
| 10 |
+
router.get("/:id", projects_1.getProjectById);
|
| 11 |
+
router.post("/", auth_1.authenticateToken, projects_1.createProject);
|
| 12 |
+
router.put("/:id", auth_1.authenticateToken, projects_1.updateProject);
|
| 13 |
+
router.delete("/:id", auth_1.authenticateToken, projects_1.deleteProject);
|
| 14 |
+
// User Progress
|
| 15 |
+
router.post("/:id/start", auth_1.authenticateToken, projects_1.startProject);
|
| 16 |
+
router.put("/:id/progress", auth_1.authenticateToken, projects_1.updateProgress);
|
| 17 |
+
router.post("/:id/complete", auth_1.authenticateToken, projects_1.completeProject);
|
| 18 |
+
// Milestones
|
| 19 |
+
router.post("/:id/milestones/:milestoneId/complete", auth_1.authenticateToken, projects_1.completeMilestone);
|
| 20 |
+
// Submissions
|
| 21 |
+
router.post("/:id/submit", auth_1.authenticateToken, projects_1.submitProject);
|
| 22 |
+
exports.default = router;
|
dist/routes/social.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
| 3 |
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
| 4 |
+
};
|
| 5 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 6 |
+
const express_1 = __importDefault(require("express"));
|
| 7 |
+
const social_1 = require("../controllers/social");
|
| 8 |
+
const auth_1 = require("../middlewares/auth");
|
| 9 |
+
const router = express_1.default.Router();
|
| 10 |
+
router.post("/follow", auth_1.authenticateToken, social_1.followUser);
|
| 11 |
+
router.delete("/unfollow/:followingId", auth_1.authenticateToken, social_1.unfollowUser);
|
| 12 |
+
router.get("/followers/:userId", auth_1.authenticateToken, social_1.getFollowers);
|
| 13 |
+
router.get("/following/:userId", auth_1.authenticateToken, social_1.getFollowing);
|
| 14 |
+
router.get("/activity-feed", auth_1.authenticateToken, social_1.getActivityFeed);
|
| 15 |
+
router.post("/share", auth_1.authenticateToken, social_1.shareProject);
|
| 16 |
+
exports.default = router;
|
dist/routes/teams.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 3 |
+
const express_1 = require("express");
|
| 4 |
+
const teams_1 = require("../controllers/teams");
|
| 5 |
+
const auth_1 = require("../middlewares/auth");
|
| 6 |
+
const router = (0, express_1.Router)();
|
| 7 |
+
router.post("/create", auth_1.authenticateToken, teams_1.createTeam);
|
| 8 |
+
router.post("/:id/join", auth_1.authenticateToken, teams_1.joinTeam);
|
| 9 |
+
router.get("/:id/projects", auth_1.authenticateToken, teams_1.getTeamProjects);
|
| 10 |
+
exports.default = router;
|
dist/routes/user.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
Object.defineProperty(exports, "__esModule", { value: true });
|
| 3 |
+
const express_1 = require("express");
|
| 4 |
+
const projects_1 = require("../controllers/projects");
|
| 5 |
+
const auth_1 = require("../middlewares/auth");
|
| 6 |
+
const router = (0, express_1.Router)();
|
| 7 |
+
router.get("/progress", auth_1.authenticateToken, projects_1.getUserProgress);
|
| 8 |
+
router.post("/projects/:id/start", auth_1.authenticateToken, projects_1.startProject);
|
| 9 |
+
router.put("/projects/:id/progress", auth_1.authenticateToken, projects_1.updateProgress);
|
| 10 |
+
router.post("/projects/:id/complete", auth_1.authenticateToken, projects_1.completeProject);
|
| 11 |
+
exports.default = router;
|