Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
name: Deploy to DigitalOcean

on:
push:
branches:
- main
pull_request:
branches:
- main

env:
DEPLOY_SERVER: ${{ secrets.DEPLOY_SERVER }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_PATH: /root/CrackCode-Main/crackcode

jobs:
test:
runs-on: ubuntu-latest
name: Test Build

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies (Server)
working-directory: crackcode/server
run: npm ci --omit=dev

- name: Install dependencies (Client)
working-directory: crackcode/client
run: npm ci

- name: Build client
working-directory: crackcode/client
run: npm run build

- name: Lint check (Server)
working-directory: crackcode/server
run: npm run lint 2>/dev/null || echo "No lint script configured"

deploy:
needs: test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
name: Deploy to Production

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install SSH key
uses: shimataro/ssh-key-action@v2
with:
key: ${{ secrets.SSH_PRIVATE_KEY }}
known_hosts: ${{ secrets.KNOWN_HOSTS }}

- name: Pull latest code
run: |
ssh -p 22 ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_SERVER }} \
"cd ${{ env.DEPLOY_PATH }} && git pull origin main"

- name: Rebuild and deploy containers
run: |
ssh -p 22 ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_SERVER }} \
"cd ${{ env.DEPLOY_PATH }} && \
docker compose down && \
docker system prune -f && \
docker compose up -d --build"

- name: Verify deployment
run: |
ssh -p 22 ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_SERVER }} \
"cd ${{ env.DEPLOY_PATH }} && \
docker compose ps && \
echo '--- Server Logs ---' && \
docker compose logs server | head -20"

- name: Health check
run: |
for i in {1..30}; do
if curl -f http://${{ env.DEPLOY_SERVER }}:4173 > /dev/null 2>&1; then
echo "✅ Application is UP"
exit 0
fi
echo "Checking... ($i/30)"
sleep 2
done
echo "❌ Application failed to start"
exit 1

- name: Deployment Success Notification
if: success()
run: |
echo "🚀 Deployment successful!"
echo "Application: http://${{ env.DEPLOY_SERVER }}:4173"

- name: Deployment Failure Notification
if: failure()
run: |
echo "❌ Deployment failed - Check logs above"
exit 1
17 changes: 17 additions & 0 deletions crackcode/server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ import shopRoutes from "./src/routes/Shop.routes.js";
// AI Routes (ERROR DIAGNOSIS + ASSISTANT)
import aiRoutes from "./src/services/aiRoutes.js";
import { logAIConfig } from "./src/services/aiConfig.js";
import { verifyBrevoApiKey } from "./src/modules/notifications/brevo.client.js";

import authRoutes from "./src/modules/auth/routes.js";
import userRoutes from "./src/modules/user/routes.js";
Expand All @@ -200,6 +201,7 @@ import sessionRoutes from "./src/modules/session/routes.js";
import rewardsRoutes from "./src/modules/rewards/routes.js";
import codeEditorRoutes from "./src/modules/codeEditor/routes.js";
import badgeRoutes from "./src/modules/badges/routes.js";
import brevoRoutes from "./src/modules/notifications/brevo.routes.js";

// Session cleanup utility
import { cleanupExpiredSessions } from "./src/modules/session/session.service.js";
Expand Down Expand Up @@ -300,6 +302,7 @@ app.use("/api/session", sessionRoutes);
app.use("/api/rewards", rewardsRoutes);
app.use("/api/codeEditor", codeEditorRoutes);
app.use("/api/badges", badgeRoutes);
app.use("/api/admin/brevo", brevoRoutes);

// Career Map APIs
app.use("/api/questions", questionRoutes);
Expand Down Expand Up @@ -377,6 +380,20 @@ const startServer = async () => {
console.error('❌ AI enabled but GEMINI_API_KEY missing in .env');
process.exit(1);
}

// Check Brevo connectivity and log status
try {
const brevoOk = await verifyBrevoApiKey();
if (brevoOk) {
console.log('✅ Brevo API reachable');
} else if (process.env.BREVO_API_KEY) {
console.warn('⚠️ BREVO_API_KEY set but Brevo API did not respond (check key/network)');
} else {
console.warn('⚠️ BREVO_API_KEY not configured. Email sending will be disabled.');
}
} catch (err) {
console.warn('⚠️ Could not verify Brevo API:', err?.message || err);
}

await connectRedisOrExit();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import User from "../../auth/User.model.js";
import { checkAndUnlockMultipleBadges } from "../../badges/badge.service.js";
import UserProgress from "../../learn/UserProgress.model.js";
import Question, { findQuestionByIdentifier } from "../../learn/Question.model.js";

// Valid career paths
const VALID_CAREERS = ["MLEngineer", "DataScientist", "SoftwareEngineer"];
Expand Down Expand Up @@ -123,13 +124,18 @@ export const submitAnswer = async (req, res) => {
// If answer is correct, update progress and check for badge unlocks
if (isCorrect) {
try {
// Normalize question identifier (support problemId strings)
let qid = questionId;
const qdoc = await findQuestionByIdentifier(questionId);
if (qdoc) qid = qdoc._id;

// Check if already completed to avoid duplicate rewards
const existingProgress = await UserProgress.findOne({ userId, questionId });
const existingProgress = await UserProgress.findOne({ userId, questionId: qid });
const isFirstCompletion = !existingProgress || existingProgress.status !== 'completed';

// Update progress
await UserProgress.findOneAndUpdate(
{ userId, questionId },
{ userId, questionId: qid },
{
status: "completed",
completedAt: new Date(),
Expand Down
69 changes: 44 additions & 25 deletions crackcode/server/src/modules/auth/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import User from "./User.model.js";
import PendingRegistration from "./PendingRegistration.model.js";
import transporter from "./nodemailer.config.js";
import { sendTransactionalEmail } from "../notifications/brevo.client.js";
import {
createSession,
invalidateSession,
Expand Down Expand Up @@ -107,20 +107,27 @@ export const register = async (req, res) =>{
});
await pending.save();

// Try to send OTP email. In development we don't want SMTP problems to block signup,
// so swallow sendMail failures and expose the tempId (and OTP in logs) so devs can verify.
// Send OTP email via Brevo API only. Fail fast if API key not configured.
try {
await transporter.sendMail({
from: process.env.SENDER_EMAIL,
to: email,
subject: "Account Verification OTP",
text: `Your OTP is ${otp}. Verify your account using this OTP.`,
});
const apiKey = process.env.BREVO_API_KEY || process.env.SENDINBLUE_API_KEY || process.env.SIB_API_KEY;
if (!apiKey) {
// In non-production, log OTP to help local testing. In production, return error.
if (process.env.NODE_ENV !== "production") {
console.log(`DEV OTP for ${email}: ${otp} (tempId=${pending._id})`);
} else {
console.error("BREVO_API_KEY missing - cannot send verification email");
return res.status(500).json({ success: false, message: "Email provider not configured (BREVO_API_KEY missing)" });
}
} else {
const html = `<p>Your OTP is <strong>${otp}</strong>. Verify your account using this OTP.</p>`;
await sendTransactionalEmail({ to: email, subject: "Account Verification OTP", html, senderEmail: process.env.SENDER_EMAIL });
}
} catch (mailErr) {
console.warn("⚠️ Failed to send verification email:", mailErr?.message || mailErr);
// In non-production, log the OTP so local testing can proceed without SMTP
console.error("⚠️ Failed to send verification email:", mailErr?.message || mailErr);
if (process.env.NODE_ENV !== "production") {
console.log(`DEV OTP for ${email}: ${otp} (tempId=${pending._id})`);
} else {
return res.status(500).json({ success: false, message: "Failed to send verification email" });
}
}

Expand Down Expand Up @@ -311,14 +318,20 @@ export const sendVerifyOtp = async (req, res) => {
await pending.save();

try {
await transporter.sendMail({
from: process.env.SENDER_EMAIL,
to: pending.email,
subject: "Account Verification OTP",
text: `Your OTP is ${otp}. Verify your account using this OTP.`,
});
const apiKey = process.env.BREVO_API_KEY || process.env.SENDINBLUE_API_KEY || process.env.SIB_API_KEY;
if (!apiKey) {
if (process.env.NODE_ENV !== "production") {
console.log(`DEV OTP for ${pending.email}: ${otp}`);
} else {
console.error("BREVO_API_KEY missing - cannot send verification OTP");
return res.status(500).json({ success: false, message: "Email provider not configured (BREVO_API_KEY missing)" });
}
} else {
const html = `<p>Your OTP is <strong>${otp}</strong>. Verify your account using this OTP.</p>`;
await sendTransactionalEmail({ to: pending.email, subject: "Account Verification OTP", html, senderEmail: process.env.SENDER_EMAIL });
}
} catch (mailErr) {
console.error("[Email] Failed to send verification OTP:", mailErr?.message);
console.error("[Email] Failed to send verification OTP:", mailErr?.message || mailErr);
return res.status(500).json({ success: false, message: "Failed to send OTP email. Please try again." });
}

Expand Down Expand Up @@ -455,14 +468,20 @@ export const sendResetOtp = async (req, res) => {
await user.save();

try {
await transporter.sendMail({
from: process.env.SENDER_EMAIL,
to: user.email,
subject: "Password Reset OTP",
text: `Your OTP for password reset is ${otp}. It will expire in 15 minutes.`,
});
const apiKey = process.env.BREVO_API_KEY || process.env.SENDINBLUE_API_KEY || process.env.SIB_API_KEY;
if (!apiKey) {
if (process.env.NODE_ENV !== "production") {
console.log(`DEV OTP for ${user.email}: ${otp}`);
} else {
console.error("BREVO_API_KEY missing - cannot send password reset OTP");
return res.status(500).json({ success: false, message: "Email provider not configured (BREVO_API_KEY missing)" });
}
} else {
const html = `<p>Your OTP for password reset is <strong>${otp}</strong>. It will expire in 15 minutes.</p>`;
await sendTransactionalEmail({ to: user.email, subject: "Password Reset OTP", html, senderEmail: process.env.SENDER_EMAIL });
}
} catch (mailErr) {
console.error("[Email] Failed to send password reset OTP:", mailErr?.message);
console.error("[Email] Failed to send password reset OTP:", mailErr?.message || mailErr);
return res.status(500).json({ success: false, message: "Failed to send OTP email. Please try again." });
}

Expand Down
41 changes: 0 additions & 41 deletions crackcode/server/src/modules/auth/nodemailer.config.js

This file was deleted.

14 changes: 14 additions & 0 deletions crackcode/server/src/modules/learn/Question.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,17 @@ questionSchema.index({ difficulty: 1 });
questionSchema.index({ language: 1 });

export default mongoose.model("Question", questionSchema);

// Helper: find question by either ObjectId or problemId string
export async function findQuestionByIdentifier(idOrProblemId) {
if (!idOrProblemId) return null;

// If it's a valid ObjectId, try by _id first
if (mongoose.Types.ObjectId.isValid(idOrProblemId)) {
const byId = await mongoose.model("Question").findById(idOrProblemId);
if (byId) return byId;
}

// Otherwise, try problemId
return await mongoose.model("Question").findOne({ problemId: idOrProblemId });
}
11 changes: 9 additions & 2 deletions crackcode/server/src/modules/learn/progress.controller.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import UserProgress from "./UserProgress.model.js";
import Question from "./Question.model.js";
import Question, { findQuestionByIdentifier } from "./Question.model.js";
import User from "../auth/User.model.js";
import { checkAndUnlockMultipleBadges } from "../badges/badge.service.js";

Expand All @@ -19,7 +19,14 @@ export const getUserProgress = async (req, res) => {
export const updateProgress = async (req, res) => {
try {
const userId = req.userId;
const { questionId, status } = req.body;
const { questionId: rawQuestionId, status } = req.body;

// Normalize question identifier to ObjectId when possible
let questionId = rawQuestionId;
if (rawQuestionId) {
const q = await findQuestionByIdentifier(rawQuestionId);
if (q) questionId = q._id;
}

// detect previous state to know if this is a new completion
const existing = await UserProgress.findOne({ userId, questionId });
Expand Down
Loading
Loading