Skip to content
Open
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
13 changes: 12 additions & 1 deletion backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
PORT=5000
MONGO_URI=mongodb://localhost:27017/os-compass
JWT_SECRET=your_jwt_secret_here
SESSION_SECRET=your_session_secret_here
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
GITHUB_CALLBACK_URL=http://localhost:5000/api/auth/github/callback
FRONTEND_URL=http://localhost:5500
EMAIL_HOST=smtp.mailtrap.io
EMAIL_PORT=2525
EMAIL_USER=your_user
EMAIL_PASS=your_pass
EMAIL_FROM=noreply@opensource-compass.org
GEMINI_API_KEY=
GEMINI_MODEL=gemini-2.5-flash
GEMINI_MODEL=gemini-2.0-flash
63 changes: 63 additions & 0 deletions backend/config/passport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import passport from "passport";
import { Strategy as GitHubStrategy } from "passport-github2";
import User from "../models/User.js";
import dotenv from "dotenv";

dotenv.config();

passport.use(
new GitHubStrategy(
{
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: process.env.GITHUB_CALLBACK_URL || "http://localhost:5000/api/auth/github/callback",
},
async (accessToken, refreshToken, profile, done) => {
try {
let user = await User.findOne({ githubId: profile.id });

if (!user) {
// If user doesn't exist by githubId, check if one exists by email
const email = profile.emails && profile.emails[0] ? profile.emails[0].value : null;

if (email) {
user = await User.findOne({ email });
}

if (user) {
// Update existing user with githubId
user.githubId = profile.id;
await user.save();
} else {
// Create new user
user = await User.create({
name: profile.displayName || profile.username,
email: email || `${profile.username}@github.com`,
githubId: profile.id,
password: Math.random().toString(36).slice(-8), // Dummy password
});
}
}

return done(null, user);
} catch (error) {
return done(error, null);
}
}
)
);

passport.serializeUser((user, done) => {
done(null, user.id);
});

passport.deserializeUser(async (id, done) => {
try {
const user = await User.findById(id);
done(null, user);
} catch (error) {
done(error, null);
}
});

export default passport;
75 changes: 61 additions & 14 deletions backend/controllers/authController.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,22 @@ export const registerUser = async (req, res) => {
});

// Send Welcome Email
await sendEmail({
to: email,
subject: "Welcome to AlgoAI 🚀",
html: `
<h2>Hello ${name} 👋</h2>
<p>Welcome to <b>AlgoAI</b>.</p>
<p>You can now start using AI tools.</p>
<br/>
<p>— Team AlgoAI</p>
`,
});
try {
await sendEmail({
to: email,
subject: "Welcome to OpenSource Compass 🚀",
html: `
<h2>Hello ${name} 👋</h2>
<p>Welcome to <b>OpenSource Compass</b>.</p>
<p>You can now track your progress and personalize your learning journey.</p>
<br/>
<p>— The OpenSource Compass Team</p>
`,
});
} catch (error) {
console.error("Welcome email failed to send:", error);
// Continue even if email fails - don't block registration
}

res.status(201).json({
success: true,
Expand Down Expand Up @@ -74,7 +79,7 @@ export const loginUser = async (req, res) => {
{ expiresIn: "1d" }
);


res.cookie("token", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
Expand Down Expand Up @@ -108,6 +113,48 @@ export const logoutUser = (req, res) => {


export const getUserProfile = async (req, res) => {
const user = await User.findById(req.userId).select("-password");
res.json({ success: true, user });
// User is already attached to req.user by protect middleware
res.json({ success: true, user: req.user });
};

export const trackGuideCompletion = async (req, res) => {
const { guideId } = req.body;
if (!guideId) return res.status(400).json({ message: "Guide ID required" });

try {
const user = await User.findById(req.user._id);
if (!user) return res.status(404).json({ message: "User not found" });

// Check if already completed
const isAlreadyCompleted = user.completedGuides.some((g) => g.guideId === guideId);
if (isAlreadyCompleted) {
return res.status(200).json({ success: true, message: "Guide already completed", user });
}

user.completedGuides.push({ guideId, completedAt: new Date() });
await user.save();

res.json({ success: true, message: "Guide progress tracked", user });
} catch (error) {
res.status(500).json({ message: "Server error" });
}
};

export const updateUserProfile = async (req, res) => {
const { name, password } = req.body;
try {
const user = await User.findById(req.user._id);
if (!user) return res.status(404).json({ message: "User not found" });

if (name) user.name = name;
if (password) {
const hashedPassword = await bcrypt.hash(password, 10);
user.password = hashedPassword;
}

await user.save();
res.json({ success: true, message: "Profile updated", user: { id: user._id, name: user.name, email: user.email } });
} catch (error) {
res.status(500).json({ message: "Server error" });
}
};
15 changes: 15 additions & 0 deletions backend/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ const UserSchema = new mongoose.Schema(
minlength: [6, "Password must be at least 6 characters"],
},

// GITHUB AUTH
githubId: {
type: String,
unique: true,
sparse: true,
},

// NEW FIELDS FOR CONTRIBUTOR PROGRESS
progress: {
issuesSelected: {
Expand All @@ -42,6 +49,14 @@ const UserSchema = new mongoose.Schema(
},
},

// TRACKING GUIDES AND MODULES
completedGuides: [
{
guideId: { type: String, required: true },
completedAt: { type: Date, default: Date.now },
}
],

// ONBOARDING LEVEL
level: {
type: String,
Expand Down
Loading