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
47 changes: 47 additions & 0 deletions backend/config/passport.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const passport = require("passport");
const GoogleStrategy = require("passport-google-oauth20").Strategy;
const GitHubStrategy = require("passport-github2").Strategy;
const User = require("../models/User");

console.log('Initializing Google OAuth strategy...');
Expand Down Expand Up @@ -35,6 +36,52 @@ passport.use(
)
);

// GitHub OAuth
if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) {
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/auth/github/callback",
scope: ["read:user", "user:email"],
},
async (accessToken, refreshToken, profile, done) => {
try {
let email = null;
if (Array.isArray(profile.emails) && profile.emails.length > 0) {
email = profile.emails.find(e => e.verified)?.value || profile.emails[0].value;
}

let user = await User.findOne({ githubId: profile.id });
if (!user && email) {
// Optional linking by email if previously registered
user = await User.findOne({ email });
if (user && !user.githubId) {
user.githubId = profile.id;
if (!user.avatar) user.avatar = profile.photos?.[0]?.value;
await user.save();
}
}

if (!user) {
user = new User({
githubId: profile.id,
name: profile.displayName || profile.username,
email: email,
avatar: profile.photos?.[0]?.value,
});
await user.save();
}
return done(null, user);
} catch (err) {
return done(err, null);
}
}
)
);
}

// serialize + deserialize
passport.serializeUser((user, done) => {
done(null, user.id);
Expand Down
5 changes: 5 additions & 0 deletions backend/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const UserSchema = new Schema({
githubId: {
type: String,
unique: true,
sparse: true,
},
googleId: {
type: String,
unique: true,
Expand Down
12 changes: 12 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.17.1",
"multer": "^2.0.2",
"node-fetch": "^3.3.2",
"node-cron": "^4.2.1",
"node-fetch": "^3.3.2",
"nodemailer": "^7.0.6",
"passport": "^0.7.0",
"passport-github2": "^0.1.12",
"passport-google-oauth20": "^2.0.0",
"rate-limiter-flexible": "^7.3.0",
"resend": "^6.0.1"
Expand Down
25 changes: 25 additions & 0 deletions backend/routes/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,31 @@ router.get(
}
);

// GitHub OAuth
router.get(
"/github",
passport.authenticate("github", { scope: ["read:user", "user:email"] })
);

router.get(
"/github/callback",
passport.authenticate("github", {
failureRedirect: `${process.env.CLIENT_URL}/login`,
failureMessage: true,
session: true,
}),
async (req, res) => {
try {
const token = await generateJWT(req.user.id);
const redirectUrl = `${process.env.CLIENT_URL}/dashboard?token=${encodeURIComponent(token)}`;
return res.redirect(redirectUrl);
} catch (err) {
console.error('JWT generation failed after GitHub OAuth:', err);
return res.redirect(`${process.env.CLIENT_URL}/login?error=github_oauth_token_failed`);
}
}
);

// @route POST api/auth/register
// @desc Register user
// @access Public
Expand Down
9 changes: 7 additions & 2 deletions frontend/src/Components/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ export default function Dashboard() {
const [goals, setGoals] = useState([]);
const navigate = useNavigate();

const token = localStorage.getItem("token");
const getAuthToken = () => localStorage.getItem("token");

const fetchProfile = async () => {
const token = getAuthToken();
if (!token) {
navigate("/login");
setLoading(false);
Expand Down Expand Up @@ -78,6 +79,7 @@ export default function Dashboard() {
const handleGoalsChange = async (updatedGoals) => {
setGoals(updatedGoals);
try {
const token = getAuthToken();
await fetch(`${import.meta.env.VITE_API_URL}/api/profile/goals`, {
method: "PUT",
headers: { "Content-Type": "application/json", "x-auth-token": token },
Expand All @@ -91,6 +93,7 @@ export default function Dashboard() {
const handleNotesChange = async (updatedNotes) => {
setProfile((prev) => ({ ...prev, notes: updatedNotes }));
try {
const token = getAuthToken();
await fetch(`${import.meta.env.VITE_API_URL}/api/profile/notes`, {
method: "PUT",
headers: { "Content-Type": "application/json", "x-auth-token": token },
Expand All @@ -104,6 +107,7 @@ export default function Dashboard() {
const handleActivityAdd = async (date) => {
setProfile((prev) => ({ ...prev, activity: [...prev.activity, date] }));
try {
const token = getAuthToken();
await fetch(`${import.meta.env.VITE_API_URL}/api/profile/activity`, {
method: "PUT",
headers: { "Content-Type": "application/json", "x-auth-token": token },
Expand All @@ -117,6 +121,7 @@ export default function Dashboard() {
const handleTimeUpdate = async (newTime) => {
setProfile((prev) => ({ ...prev, timeSpent: newTime }));
try {
const token = getAuthToken();
await fetch(`${import.meta.env.VITE_API_URL}/api/profile/time`, {
method: "PUT",
headers: { "Content-Type": "application/json", "x-auth-token": token },
Expand Down Expand Up @@ -193,7 +198,7 @@ export default function Dashboard() {
activityData={activity}
onAddActivity={async (day) => {
try {
const token = localStorage.getItem("token");
const token = getAuthToken();
const res = await fetch(
`${import.meta.env.VITE_API_URL}/api/profile/activity`,
{
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/Components/auth/Login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ const Login = () => {
window.location.href = `${import.meta.env.VITE_API_URL}/auth/google`;
};

const handleGithubLogin = () => {
window.location.href = `${import.meta.env.VITE_API_URL}/auth/github`;
};

// Show verification component if user needs to verify email
if (showVerification) {
return (
Expand Down Expand Up @@ -243,6 +247,7 @@ const Login = () => {
<div className="grid grid-cols-2 gap-3">
<button
type="button"
onClick={handleGithubLogin}
className="border border-[var(--input)] text-[var(--primary)] hover:bg-[var(--accent)] py-3 rounded-lg flex justify-center items-center"
>
<Github className="h-4 w-4 mr-2" /> GitHub
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/Components/auth/Register.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ const Register = () => {
window.location.href = `${import.meta.env.VITE_API_URL}/auth/google`;
};

const handleGithubRegister = () => {
window.location.href = `${import.meta.env.VITE_API_URL}/auth/github`;
};

if (showVerification) {
return (
<EmailVerification
Expand Down Expand Up @@ -314,7 +318,7 @@ const Register = () => {

{/* Social Login */}
<div className="grid grid-cols-2 gap-3">
<button type="button" className="flex items-center cursor-pointer justify-center py-3 border border-[var(--input)] rounded-lg text-[var(--primary)] hover:bg-[var(--accent)]">
<button type="button" onClick={handleGithubRegister} className="flex items-center cursor-pointer justify-center py-3 border border-[var(--input)] rounded-lg text-[var(--primary)] hover:bg-[var(--accent)]">
<Github className="h-4 w-4 mr-2" />
GitHub
</button>
Expand Down
Loading