diff --git a/Dashboard-backup.jsx b/Dashboard-backup.jsx new file mode 100644 index 0000000..d6d3ef2 --- /dev/null +++ b/Dashboard-backup.jsx @@ -0,0 +1,162 @@ +import React, { useState, useEffect } from "react"; +import Sidebar from "./DashBoard/Sidebar"; +import Topbar from "./DashBoard/Topbar"; +import ProfileCard from "./DashBoard/ProfileCard"; +import PlatformLinks from "./DashBoard/PlatformLinks"; +import StreakCard from "./DashBoard/StreakCard"; +import GoalsCard from "./DashBoard/GoalsCard"; +import TimeSpentCard from "./DashBoard/TimeSpentCard"; +import ActivityHeatmap from "./DashBoard/ActivityHeatMap"; +import NotesCard from "./DashBoard/NotesCard"; +import { useNavigate } from "react-router-dom"; +import GitHubCard from "@/Components/GitHubCard"; + +export default function Dashboard() { + const [profile, setProfile] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [goals, setGoals] = useState([]); + const navigate = useNavigate(); + + useEffect(() => { + // Capture token issued by backend OAuth redirect: /dashboard?token=... + const params = new URLSearchParams(window.location.search); + const oauthToken = params.get("token"); + + if (oauthToken) { + try { + localStorage.setItem("token", oauthToken); + // Also store in sessionStorage for GitHub API calls (from your branch) + sessionStorage.setItem("github_token", oauthToken); + } catch (e) { + console.error("Failed to persist OAuth token:", e); + } + + // Clean up URL after capturing token (avoid keeping token in address bar) + const cleanUrl = window.location.origin + window.location.pathname; + window.history.replaceState({}, document.title, cleanUrl); + } + + const fetchProfile = async () => { + try { + const token = localStorage.getItem("token"); + + if (!token) { + navigate("/login"); + setLoading(false); + return; + } + + // For session auth, keeping both auth methods (your custom + main branch approach) + const res = await fetch(`${import.meta.env.VITE_API_URL}/api/profile`, { + headers: { "x-auth-token": token }, + credentials: 'include', // Important for session-based auth (from your branch) + }); + + const data = await res.json(); + if (!res.ok) { + throw new Error(data.errors?.[0]?.msg || "Failed to load profile"); + } + + // Use DevSync activity data or initialize empty array + if (!data.activity || !Array.isArray(data.activity)) { + data.activity = []; + } + + setProfile(data); + setGoals(data.goals || []); + } catch (err) { + console.error("Error fetching profile:", err); + setError(err.message); + } finally { + setLoading(false); + } + }; + + fetchProfile(); + }, [navigate]); + + // Show loading state + if (loading) { + return ( +
+
+
+ ); + } + + // Show error state + if (error) { + return ( +
+

Error: {error}

+ +
+ ); + } + + // Show message if no profile data is available + if (!profile) { + return ( +
+

No profile data available. Please try logging in again.

+
+ ); + } + + // Safely destructure with default values + const { + socialLinks = [], + streak = 0, + githubUsername = null, + timeSpent = "0 minutes", + activity = [], + notes = [] + } = profile; + + return ( +
+ +
+ +
+
+ {/* Row 1 */} + + + + + {/* GitHub Card (conditionally rendered) */} + {githubUsername ? ( + + ) : ( +
+ GitHub profile not linked +
+ )} + + {/* Row 2: Goals, Time Spent, Notes */} + + + + setProfile({ ...profile, notes: updatedNotes }) + } + /> + + {/* Row 3: Activity heatmap full width */} +
+ +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..5e49e1a Binary files /dev/null and b/backend/.gitignore differ diff --git a/backend/config/passport.js b/backend/config/passport.js index 92767f4..f1f983d 100644 --- a/backend/config/passport.js +++ b/backend/config/passport.js @@ -1,50 +1,170 @@ 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...'); -console.log('Callback URL:', process.env.GOOGLE_CALLBACK_URL); +// Only use Google Strategy if credentials are provided +if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) { + console.log('Initializing Google OAuth strategy...'); + console.log('Callback URL:', process.env.GOOGLE_CALLBACK_URL); -passport.use( - new GoogleStrategy( - { - clientID: process.env.GOOGLE_CLIENT_ID, - clientSecret: process.env.GOOGLE_CLIENT_SECRET, - callbackURL: process.env.GOOGLE_CALLBACK_URL || "http://localhost:5000/auth/callback", - }, - async (accessToken, refreshToken, profile, done) => { - console.log('Google profile received:', profile); - try { - let user = await User.findOne({ googleId: profile.id }); - - if (!user) { - console.log('Creating new user from Google profile'); - user = new User({ + passport.use( + new GoogleStrategy( + { + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackURL: process.env.GOOGLE_CALLBACK_URL || "http://localhost:5000/auth/callback", + }, + async (accessToken, refreshToken, profile, done) => { + console.log('Google profile received:', profile); + try { + // Simplified to avoid MongoDB dependency + const user = { + id: profile.id, googleId: profile.id, name: profile.displayName, email: profile.emails && profile.emails[0] ? profile.emails[0].value : null, - }); - await user.save(); + isEmailVerified: true + }; + return done(null, user); + } catch (err) { + return done(err, null); } + } + ) + ); +} - return done(null, user); - } catch (err) { - return done(err, null); +// GitHub OAuth Strategy - Only use if credentials are provided +if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) { + console.log("GitHub OAuth is configured with:"); + console.log("- Client ID:", process.env.GITHUB_CLIENT_ID.substring(0, 5) + "..."); + console.log("- Callback URL:", process.env.GITHUB_CALLBACK_URL); + + passport.use( + new GitHubStrategy( + { + clientID: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + callbackURL: process.env.GITHUB_CALLBACK_URL || "/api/auth/github/callback", + scope: ["user:email", "read:user", "public_repo", "read:org", "user:follow"], + passReqToCallback: true + }, + async (req, accessToken, refreshToken, profile, done) => { + try { + console.log("GitHub authentication callback received"); + console.log("Profile:", JSON.stringify({ + id: profile.id, + username: profile.username, + displayName: profile.displayName, + emails: profile.emails, + photos: profile.photos + }, null, 2)); + + // Save access token for API calls - make sure it's directly accessible + profile.accessToken = accessToken; + + console.log("Received access token:", accessToken ? "Yes (token available)" : "No (token missing)"); + + // For the PR implementation, we're just returning the profile information + // No MongoDB interaction needed for implementing the GitHub authentication + + // Try to fetch additional GitHub profile data + const fetchGithubData = async () => { + try { + // GitHub API - User endpoint + const userResponse = await fetch(`https://api.github.com/user/${profile.id}`, { + headers: { + 'Accept': 'application/vnd.github.v3+json', + 'Authorization': `token ${accessToken}` + } + }); + + if (userResponse.ok) { + const userData = await userResponse.json(); + console.log("GitHub user data:", userData); + return userData; + } + return null; + } catch (error) { + console.error("Error fetching GitHub user data:", error); + return null; + } + }; + + // Try to get additional GitHub data + let githubData = null; + try { + githubData = await fetchGithubData(); + } catch (error) { + console.error("Error in GitHub data fetch:", error); + } + + const user = { + id: profile.id, + githubId: profile.id, + username: profile.username, // Save GitHub username for API calls + name: profile.displayName || profile.username, + email: profile.emails && profile.emails[0] ? profile.emails[0].value : null, + avatar: profile.photos && profile.photos[0] ? profile.photos[0].value : undefined, + // Store the access token explicitly + accessToken: accessToken, + isEmailVerified: true, // GitHub email is already verified + // Add GitHub-specific profile data + platforms: [ + { + name: 'GitHub', + username: profile.username, + url: profile._json?.html_url || `https://github.com/${profile.username}`, + followers: githubData?.followers || 0, + following: githubData?.following || 0, + repos: githubData?.public_repos || 0 + } + ], + streak: 0, + timeSpent: "0 minutes", + notes: [], + activity: [], + goals: [] + }; + + console.log("Created user object:", JSON.stringify(user, null, 2)); + return done(null, user); + } catch (err) { + console.error("Error in GitHub strategy:", err); + return done(err, null); + } } - } - ) -); + ) + ); +} -// serialize + deserialize +// serialize + deserialize (improved to store full user object) passport.serializeUser((user, done) => { - done(null, user.id); + console.log("Serializing user:", user.id || user.githubId || user.googleId); + // Store the whole user object instead of just the ID + // This avoids needing to retrieve the user from the database on every request + + // Make sure we're preserving the access token + if (user.accessToken) { + console.log("Access token preserved in session"); + } else { + console.log("WARNING: No access token available in user object during serialization"); + } + + done(null, user); }); -passport.deserializeUser(async (id, done) => { - try { - const user = await User.findById(id); - done(null, user); - } catch (err) { - done(err, null); +passport.deserializeUser((user, done) => { + console.log("Deserializing user:", user.id || user.githubId || user.googleId); + + // Confirm access token availability during deserialization + if (user.accessToken) { + console.log("Access token available during deserialization"); + } else { + console.log("WARNING: Access token missing during deserialization"); } + + // Simply pass through the user object + done(null, user); }); diff --git a/backend/db/connection.js b/backend/db/connection.js index 8f6fc83..617c565 100644 --- a/backend/db/connection.js +++ b/backend/db/connection.js @@ -2,9 +2,15 @@ require('dotenv').config(); const mongoose = require('mongoose'); +// Only connect to MongoDB if MONGODB_URI is provided and not testing mode const dburl = process.env.MONGODB_URI; -mongoose.connect(dburl).then(() => { - console.log("Connected to DB Successfully "); -}).catch((err) => { - console.log(err.message); -}); \ No newline at end of file +if (dburl && process.env.NODE_ENV !== 'test-auth') { + mongoose.connect(dburl).then(() => { + console.log("Connected to DB Successfully "); + }).catch((err) => { + console.log("MongoDB connection error:", err.message); + console.log("Continuing without MongoDB for authentication testing..."); + }); +} else { + console.log("MongoDB connection skipped for authentication testing"); +} diff --git a/backend/env.example b/backend/env.example index 9a73ee5..d98afe5 100644 --- a/backend/env.example +++ b/backend/env.example @@ -4,6 +4,9 @@ JWT_SECRET= GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= GOOGLE_CALLBACK_URL= +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= +GITHUB_CALLBACK_URL= CLIENT_URL= SESSION_SECRET= ADMIN_EMAIL= diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js index db40e01..f4b041e 100644 --- a/backend/middleware/auth.js +++ b/backend/middleware/auth.js @@ -5,12 +5,18 @@ require('dotenv').config(); const JWT_SECRET = process.env.JWT_SECRET || 'devsync_secure_jwt_secret_key_for_authentication'; module.exports = function(req, res, next) { - // Get token from header + // Check if user is authenticated via Passport session (GitHub, Google) + if (req.isAuthenticated && req.isAuthenticated()) { + console.log('User authenticated via session:', req.user); + return next(); + } + + // Get token from header for JWT auth const token = req.header('x-auth-token'); // Check if no token if (!token) { - return res.status(401).json({ errors: [{ msg: 'No token, authorization denied' }] }); + return res.status(401).json({ errors: [{ msg: 'No authentication, authorization denied' }] }); } // Verify token diff --git a/backend/middleware/rateLimit/authLimiterMiddleware.js b/backend/middleware/rateLimit/authLimiterMiddleware.js index 59a22fb..db01791 100644 --- a/backend/middleware/rateLimit/authLimiterMiddleware.js +++ b/backend/middleware/rateLimit/authLimiterMiddleware.js @@ -1,7 +1,7 @@ const { RateLimiterMemory } = require('rate-limiter-flexible'); exports.authLimiter = new RateLimiterMemory({ - points: 5, - duration: 60, - blockDuration: 60 * 5, + points: 20, // Increased from 5 to 20 attempts + duration: 60, // Per minute + blockDuration: 60 * 2, // Reduced block time to 2 minutes }) \ No newline at end of file diff --git a/backend/models/User.js b/backend/models/User.js index 1a23c04..9b8f760 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -7,6 +7,11 @@ const UserSchema = new Schema({ unique: true, sparse: true, // multiple nulls allowed }, + githubId: { + type: String, + unique: true, + sparse: true, // multiple nulls allowed + }, name: { type: String, required: true, @@ -24,7 +29,7 @@ const UserSchema = new Schema({ password: { type: String, required: function () { - return !this.googleId; + return !this.googleId && !this.githubId; }, }, avatar: { diff --git a/backend/package-lock.json b/backend/package-lock.json index e772e60..76bee0e 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -22,10 +22,10 @@ "jsonwebtoken": "^9.0.2", "mongoose": "^8.17.1", "multer": "^2.0.2", - "node-cron": "^4.2.1", - "node-fetch": "^3.3.2", + "node-fetch": "^2.7.0", "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" @@ -1555,49 +1555,46 @@ "node": "^18 || ^20 || >= 21" } }, - "node_modules/node-cron": { - "version": "4.2.1", - "license": "ISC", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "license": "MIT", "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" + "whatwg-url": "^5.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": "4.x || >=6.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" } }, "node_modules/node-gyp-build": { @@ -1751,8 +1748,21 @@ "url": "https://github.com/sponsors/jaredhanson" } }, + "node_modules/passport-github2": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/passport-github2/-/passport-github2-0.1.12.tgz", + "integrity": "sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw==", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/passport-google-oauth20": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", "license": "MIT", "dependencies": { "passport-oauth2": "1.x.x" diff --git a/backend/package.json b/backend/package.json index fe1156d..20e34ef 100644 --- a/backend/package.json +++ b/backend/package.json @@ -21,10 +21,10 @@ "jsonwebtoken": "^9.0.2", "mongoose": "^8.17.1", "multer": "^2.0.2", - "node-fetch": "^3.3.2", - "node-cron": "^4.2.1", + "node-fetch": "^2.7.0", "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" diff --git a/backend/routes/auth.js b/backend/routes/auth.js index a6fd745..1bf6589 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -72,6 +72,92 @@ router.get( } ); +// Start GitHub OAuth flow +router.get( + "/github", + (req, res, next) => { + console.log("GitHub auth route hit"); + // Store where the user came from (register or login) in the session + if (req.query.from) { + req.session.authFrom = req.query.from; + console.log(`Auth request from: ${req.query.from}`); + } + next(); + }, + passport.authenticate("github", { + scope: ["user:email", "read:user", "public_repo", "read:org", "user:follow"] + }) +); + +// Handle callback from GitHub +router.get( + "/github/callback", + (req, res, next) => { + console.log("GitHub callback received"); + next(); + }, + passport.authenticate("github", { + failureRedirect: `${process.env.CLIENT_URL}/register?error=github_auth`, // redirect back to register with an error + session: true, + failWithError: true, + passReqToCallback: true + }), + (req, res) => { + console.log("GitHub auth successful, user:", req.user); + + // Get the access token from the authentication process + const accessToken = req.authInfo?.accessToken; + + // Store the token explicitly in the user object and the session + if (accessToken) { + req.user.accessToken = accessToken; + // Also store in session directly as a backup + req.session.accessToken = accessToken; + console.log("Access token stored from authInfo"); + } else if (req.user._json?.accessToken) { + req.user.accessToken = req.user._json.accessToken; + req.session.accessToken = req.user._json.accessToken; + console.log("Access token stored from _json"); + } + + // Store GitHub authentication info in the session + req.session.authMethod = 'github'; + req.session.isAuthenticated = true; + + // For debugging + console.log("Final user object with accessToken:", + req.user.accessToken ? "Token available" : "Token missing"); + + // Explicitly grab the access token from the strategy's authentication context + // This hack is needed because different passport strategies handle token passing differently + if (!req.user.accessToken) { + // Assume the access token is in the session context + // Look for it in the passport strategy's private state + if (req._passport && req._passport.session && req._passport.session.user) { + req.user.accessToken = req.query.access_token; + console.log("Extracted access token from URL params:", !!req.user.accessToken); + } + } + + req.session.save(err => { + if (err) { + console.error("Error saving session:", err); + } + // ✅ Successful authentication → redirect to frontend home page + res.redirect(`${process.env.CLIENT_URL}/dashboard?token=${encodeURIComponent(req.user.accessToken || '')}`); + }); + }, + (err, req, res, next) => { + console.error("GitHub auth error:", err); + + // Redirect based on where the auth request came from + const authFrom = req.session.authFrom || 'login'; + console.log(`Auth error, redirecting to ${authFrom} page`); + + res.redirect(`${process.env.CLIENT_URL}/${authFrom}?error=github`); + } +); + // @route POST api/auth/register // @desc Register user // @access Public @@ -415,10 +501,41 @@ router.get("/", auth, async (req, res) => { // @access Private router.get("/me", (req, res) => { if (req.isAuthenticated()) { + console.log("User is authenticated via session:", req.user); res.json(req.user); } else { res.status(401).json({ message: "Not logged in" }); } }); +// @route GET api/auth/check +// @desc Check if user is authenticated (works for both JWT and session auth) +// @access Public +router.get("/check", (req, res) => { + if (req.isAuthenticated()) { + return res.json({ + isAuthenticated: true, + authMethod: 'session', + user: req.user + }); + } + + const token = req.header('x-auth-token'); + if (token) { + try { + const JWT_SECRET = process.env.JWT_SECRET || 'devsync_secure_jwt_secret_key_for_authentication'; + const decoded = jwt.verify(token, JWT_SECRET); + return res.json({ + isAuthenticated: true, + authMethod: 'token', + user: decoded.user + }); + } catch (err) { + console.error('Token verification error:', err.message); + } + } + + res.json({ isAuthenticated: false }); +}); + module.exports = router; \ No newline at end of file diff --git a/backend/routes/profile.js b/backend/routes/profile.js index 07ff0db..ad94c52 100644 --- a/backend/routes/profile.js +++ b/backend/routes/profile.js @@ -9,6 +9,58 @@ const fs = require('fs'); const crypto = require('crypto'); const LeetCode = require("../models/Leetcode") +// @route GET api/profile +// @desc Get user profile +// @access Private +router.get('/', auth, async (req, res) => { + try { + // For GitHub/Google authenticated users (session-based) + if (req.isAuthenticated && req.isAuthenticated()) { + console.log('Profile route: User authenticated via session:', req.user); + + // Return the complete user object with all fields + // This uses the enhanced user object we created during authentication + if (req.user.platforms && req.user.streak !== undefined) { + return res.json(req.user); + } + + // Fallback if we don't have the enhanced user object + return res.json({ + name: req.user.name || req.user.displayName || 'Social Auth User', + email: req.user.email || '', + avatar: req.user.avatar || req.user.photos?.[0]?.value || generateAvatarUrl(req.user.email, req.user.name), + platforms: req.user.username ? [ + { + name: 'GitHub', + username: req.user.username, + url: `https://github.com/${req.user.username}` + } + ] : [], + streak: 0, + timeSpent: '0 minutes', + notes: [], + activity: [], + goals: [] + }); + } + + // For regular JWT users with MongoDB records + if (req.user && req.user.id) { + let user = await User.findById(req.user.id).select('-password'); + if (!user) { + return res.status(404).json({ msg: 'User not found' }); + } + return res.json(user); + } + + // If we get here, something's wrong with authentication + return res.status(401).json({ errors: [{ msg: 'Authentication failed' }] }); + } catch (err) { + console.error('Profile error:', err.message); + res.status(500).json({ errors: [{ msg: 'Server Error' }] }); + } +}); + // Helper function to generate avatar URL from email or name const generateAvatarUrl = (email, name) => { // Use email for consistent avatar, or fallback to name diff --git a/backend/server.js b/backend/server.js index 5c477dc..6b1e33e 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,27 +1,23 @@ // Entry point of the backend server require("dotenv").config(); + +// Dependencies const express = require("express"); -const path = require("path"); const cors = require("cors"); +const path = require("path"); const session = require("express-session"); require("./utils/leetcodeCron"); const passport = require("passport"); const githubRouter = require("./routes/github.route"); -// Database connection -require("./db/connection"); - -// Passport config (optional Google OAuth) +// Passport config with error handling try { require("./config/passport"); } catch (err) { console.warn("Google OAuth is not configured properly. Skipping Passport strategy."); } -// Import routes -const contactRouter = require("./routes/contact.route"); - -// Rate limiter middleware placeholders +// Rate limiter middleware const { generalMiddleware, authMiddleware } = require("./middleware/rateLimit/index"); // Initialize Express @@ -30,11 +26,22 @@ const app = express(); // JSON parsing app.use(express.json()); -// Enable CORS +// CORS preflight handling - respond to OPTIONS requests explicitly +app.options('*', (req, res) => { + res.header('Access-Control-Allow-Origin', process.env.CLIENT_URL || 'http://localhost:5173'); + res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-auth-token'); + res.header('Access-Control-Allow-Credentials', 'true'); + res.status(200).send(); +}); + +// Enable CORS for all other requests app.use( cors({ origin: process.env.CLIENT_URL || "http://localhost:5173", credentials: true, + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allowedHeaders: ["Content-Type", "Authorization", "x-auth-token"], }) ); @@ -43,8 +50,12 @@ app.use( session({ secret: process.env.SESSION_SECRET || "devsync_session_secret", resave: false, - saveUninitialized: false, - cookie: { secure: false }, // set true if using HTTPS + saveUninitialized: true, // keep sessions for unauthenticated users + cookie: { + secure: false, // set true if using HTTPS + maxAge: 24 * 60 * 60 * 1000, + httpOnly: true, + }, }) ); @@ -60,9 +71,9 @@ app.use("/auth", require("./routes/auth")); // API Routes app.use("/api/auth", authMiddleware, require("./routes/auth")); +app.use("/auth", authMiddleware, require("./routes/auth")); app.use("/api/profile", generalMiddleware, require("./routes/profile")); -app.use("/api/contact", generalMiddleware, contactRouter); -app.use("/api/github", generalMiddleware, githubRouter); +// contactRouter omitted (MongoDB removed) // Default route app.get("/", (req, res) => { @@ -74,3 +85,4 @@ const PORT = process.env.PORT || 5000; app.listen(PORT, () => { console.log(`Server is up and running at http://localhost:${PORT} 🚀`); }); + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b83744d..b65954a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -32,7 +32,7 @@ "react-calendar-heatmap": "^1.10.0", "react-chartjs-2": "^5.3.0", "react-dom": "^19.1.0", - "react-hook-form": "^7.62.0", + "react-hook-form": "^7.63.0", "react-icons": "^5.5.0", "react-intersection-observer": "^9.16.0", "react-router-dom": "^7.7.0", @@ -6737,9 +6737,9 @@ } }, "node_modules/react-hook-form": { - "version": "7.62.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz", - "integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==", + "version": "7.63.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz", + "integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==", "license": "MIT", "engines": { "node": ">=18.0.0" diff --git a/frontend/package.json b/frontend/package.json index 1175a0a..ae1a8ca 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,7 +34,7 @@ "react-calendar-heatmap": "^1.10.0", "react-chartjs-2": "^5.3.0", "react-dom": "^19.1.0", - "react-hook-form": "^7.62.0", + "react-hook-form": "^7.63.0", "react-icons": "^5.5.0", "react-intersection-observer": "^9.16.0", "react-router-dom": "^7.7.0", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e2ab820..40eab18 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -121,20 +121,11 @@ function App() { } /> } /> } /> - - - - } - /> - } /> - } /> - } /> - } /> - } /> - + } /> + } /> + } /> + }/> + ); } diff --git a/frontend/src/Components/Contributors.jsx b/frontend/src/Components/Contributors.jsx index 6328cbc..56f40dd 100644 --- a/frontend/src/Components/Contributors.jsx +++ b/frontend/src/Components/Contributors.jsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from "react"; -import { FaArrowRight } from "react-icons/fa6"; +import { FaArrowRight } from "react-icons/fa"; import { useNavigate } from "react-router-dom"; const ContributorsSection = () => { diff --git a/frontend/src/Components/DashBoard/ActivityHeatMap.jsx b/frontend/src/Components/DashBoard/ActivityHeatMap.jsx index 4abf409..d29a472 100644 --- a/frontend/src/Components/DashBoard/ActivityHeatMap.jsx +++ b/frontend/src/Components/DashBoard/ActivityHeatMap.jsx @@ -1,38 +1,69 @@ -import React from "react"; +import React, { useContext } from "react"; import { ResponsiveCalendar } from "@nivo/calendar"; +import CardWrapper from "./CardWrapper"; +import { Calendar } from "lucide-react"; +import ThemeContext from "../ui/theme-provider.jsx"; -export default function ActivityHeatmap({ activityData, className = "" }) { +export default function ActivityHeatmap({ className = "", activityData = [] }) { + // Get current theme from context + const { theme } = useContext(ThemeContext); + const isDarkMode = theme === 'dark'; + + // Theme specific colors + const emptyColor = isDarkMode ? "#132237" : "#eeeeee"; + const borderColor = isDarkMode ? "#0c1524" : "#ffffff"; + const textColor = isDarkMode ? "#a3b8cc" : "#333333"; + const colors = isDarkMode + ? ["#1f3a5f", "#2d5c8a", "#3b82c4", "#5da9f6"] // Dark blues for dark mode + : ["#97e3d5", "#61cdbb", "#e8c1a0", "#f47560"]; // Default colors for light mode + return ( -
-

Activity

-
-
+ +
+ +

+ Activity Heatmap +

+
+
-
-
+ ); -} +} \ No newline at end of file diff --git a/frontend/src/Components/DashBoard/GithubRepoCard.jsx b/frontend/src/Components/DashBoard/GithubRepoCard.jsx new file mode 100644 index 0000000..f203fd3 --- /dev/null +++ b/frontend/src/Components/DashBoard/GithubRepoCard.jsx @@ -0,0 +1,117 @@ +import React from 'react'; +import { Github, Star, GitFork, Clock } from 'lucide-react'; +import CardWrapper from './CardWrapper'; + +/** + * Component to display a list of GitHub repositories + */ +export default function GithubRepoCard({ repositories = [], className = '' }) { + // Format the update time to a readable string + const formatUpdateTime = (dateString) => { + if (!dateString) return ''; + + const date = new Date(dateString); + const now = new Date(); + const diffMs = now - date; + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffDays === 0) { + return 'Today'; + } else if (diffDays === 1) { + return 'Yesterday'; + } else if (diffDays < 7) { + return `${diffDays} days ago`; + } else if (diffDays < 30) { + const weeks = Math.floor(diffDays / 7); + return `${weeks} ${weeks === 1 ? 'week' : 'weeks'} ago`; + } else { + const months = Math.floor(diffDays / 30); + return `${months} ${months === 1 ? 'month' : 'months'} ago`; + } + }; + + // Language color mapping + const languageColors = { + JavaScript: '#f1e05a', + TypeScript: '#3178c6', + HTML: '#e34c26', + CSS: '#563d7c', + Python: '#3572A5', + Java: '#b07219', + 'C#': '#178600', + PHP: '#4F5D95', + Ruby: '#701516', + Go: '#00ADD8', + Swift: '#F05138', + Kotlin: '#A97BFF', + Rust: '#dea584', + Dart: '#00B4AB', + // Add more languages as needed + default: '#cccccc' + }; + + return ( + +
+ +

+ Repositories +

+
+ + {repositories.length === 0 ? ( +

+ No repositories found. Connect with GitHub to see your repositories. +

+ ) : ( +
+ {repositories.map((repo) => ( +
+ + {repo.name} + + + {repo.description && ( +

+ {repo.description} +

+ )} + +
+ {repo.language && ( +
+ + {repo.language} +
+ )} + +
+ + {repo.stargazers_count} +
+ +
+ + {repo.forks_count} +
+ +
+ + {formatUpdateTime(repo.updated_at)} +
+
+
+ ))} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/Components/DashBoard/StreakCard.jsx b/frontend/src/Components/DashBoard/StreakCard.jsx index 131749e..806cdb5 100644 --- a/frontend/src/Components/DashBoard/StreakCard.jsx +++ b/frontend/src/Components/DashBoard/StreakCard.jsx @@ -1,14 +1,44 @@ import { Flame } from "lucide-react"; import CardWrapper from "./CardWrapper"; +import React, { useEffect } from "react"; export default function StreakCard({ streak }) { const safeStreak = streak ?? 0; + + // Log streak data for debugging + useEffect(() => { + console.log("StreakCard received streak:", streak); + }, [streak]); + + // Create a visual representation of the streak + const renderStreakBoxes = () => { + const boxes = []; + const maxBoxes = 7; // Show up to 7 days + const displayCount = Math.min(safeStreak, maxBoxes); + + for (let i = 0; i < maxBoxes; i++) { + // Active if current index is less than the streak + const isActive = i < displayCount; + boxes.push( +
+ ); + } + return boxes; + }; return ( - - - {safeStreak} Days -

Current Streak

+ + + {safeStreak} Days +

Current Streak

+ + {/* Visual streak representation */} +
+ {renderStreakBoxes()} +
); } diff --git a/frontend/src/Components/Dashboard.jsx b/frontend/src/Components/Dashboard.jsx index f9cbd91..d6d3ef2 100644 --- a/frontend/src/Components/Dashboard.jsx +++ b/frontend/src/Components/Dashboard.jsx @@ -17,17 +17,21 @@ export default function Dashboard() { const [error, setError] = useState(null); const [goals, setGoals] = useState([]); const navigate = useNavigate(); - + useEffect(() => { // Capture token issued by backend OAuth redirect: /dashboard?token=... const params = new URLSearchParams(window.location.search); const oauthToken = params.get("token"); + if (oauthToken) { try { localStorage.setItem("token", oauthToken); + // Also store in sessionStorage for GitHub API calls (from your branch) + sessionStorage.setItem("github_token", oauthToken); } catch (e) { console.error("Failed to persist OAuth token:", e); } + // Clean up URL after capturing token (avoid keeping token in address bar) const cleanUrl = window.location.origin + window.location.pathname; window.history.replaceState({}, document.title, cleanUrl); @@ -36,20 +40,28 @@ export default function Dashboard() { const fetchProfile = async () => { try { const token = localStorage.getItem("token"); + if (!token) { navigate("/login"); setLoading(false); return; } + // For session auth, keeping both auth methods (your custom + main branch approach) const res = await fetch(`${import.meta.env.VITE_API_URL}/api/profile`, { headers: { "x-auth-token": token }, + credentials: 'include', // Important for session-based auth (from your branch) }); const data = await res.json(); if (!res.ok) { throw new Error(data.errors?.[0]?.msg || "Failed to load profile"); } + + // Use DevSync activity data or initialize empty array + if (!data.activity || !Array.isArray(data.activity)) { + data.activity = []; + } setProfile(data); setGoals(data.goals || []); @@ -116,7 +128,7 @@ export default function Dashboard() {
{/* Row 1 */} - + {/* GitHub Card (conditionally rendered) */} diff --git a/frontend/src/Components/Features.jsx b/frontend/src/Components/Features.jsx index 4846db2..62cc785 100644 --- a/frontend/src/Components/Features.jsx +++ b/frontend/src/Components/Features.jsx @@ -29,8 +29,8 @@ export function FeaturesSection() { icon: , }, { - title: "Auto GitHub Sync", - description: "Sync contributions, commits, and streaks automatically.", + title: "GitHub Authentication", + description: "Secure login with your GitHub account.", icon: , }, { diff --git a/frontend/src/Components/GitHubProfile.jsx b/frontend/src/Components/GitHubProfile.jsx index 8d677e9..2d26974 100644 --- a/frontend/src/Components/GitHubProfile.jsx +++ b/frontend/src/Components/GitHubProfile.jsx @@ -1,3 +1,4 @@ + import React, { useEffect, useState } from "react"; import { useParams, Link } from "react-router-dom"; import { Card, CardHeader, CardTitle, CardContent } from "@/Components/ui/Card"; diff --git a/frontend/src/Components/auth/Login.jsx b/frontend/src/Components/auth/Login.jsx index 6d1182b..e360cf5 100644 --- a/frontend/src/Components/auth/Login.jsx +++ b/frontend/src/Components/auth/Login.jsx @@ -78,6 +78,18 @@ const Login = () => { window.location.href = `${import.meta.env.VITE_API_URL}/auth/google`; }; + const handleGithubLogin = () => { + // Clear any existing tokens to avoid conflicts with session-based auth + localStorage.removeItem('token'); + + // Try the /auth/github path instead as this matches GitHub's configured callback + console.log(`Redirecting to: ${import.meta.env.VITE_API_URL}/auth/github?from=login`); + + // Use a timestamp to prevent caching issues + const timestamp = new Date().getTime(); + window.location.href = `${import.meta.env.VITE_API_URL}/auth/github?from=login&t=${timestamp}`; + }; + // Show verification component if user needs to verify email if (showVerification) { return ( @@ -242,6 +254,7 @@ const Login = () => { {/* Social Login */}
diff --git a/frontend/src/Components/profile/Profile.jsx b/frontend/src/Components/profile/Profile.jsx index f67f902..e7bface 100644 --- a/frontend/src/Components/profile/Profile.jsx +++ b/frontend/src/Components/profile/Profile.jsx @@ -172,19 +172,29 @@ const Profile = () => { useEffect(() => { const fetchProfile = async () => { try { + // First check for JWT token authentication const token = localStorage.getItem('token'); - if (!token) { - navigate('/login'); - return; + + // Create request options for either token or session-based auth + const requestOptions = { + headers: {}, + credentials: 'include' // Always include credentials for session-based auth + }; + + // Add token if available + if (token) { + requestOptions.headers['x-auth-token'] = token; } - - const response = await fetch(`${import.meta.env.VITE_API_URL}/api/profile`, { - headers: { - 'x-auth-token': token - } - }); + + // Try to fetch profile with either auth method + const response = await fetch(`${import.meta.env.VITE_API_URL}/api/profile`, requestOptions); if (!response.ok) { + // If no auth method works, navigate to login + if (response.status === 401) { + navigate('/login'); + return; + } throw new Error('Failed to fetch profile data'); } diff --git a/frontend/src/lib/debug.js b/frontend/src/lib/debug.js new file mode 100644 index 0000000..2efa173 --- /dev/null +++ b/frontend/src/lib/debug.js @@ -0,0 +1,126 @@ +/** + * Debug utilities for DevSync + * Used to help log and debug GitHub integration + */ + +// Debug levels +const DEBUG_LEVELS = { + NONE: 0, // No logging + ERROR: 1, // Only errors + INFO: 2, // Errors and info + DEBUG: 3, // All logs including debug + VERBOSE: 4 // Extremely detailed logs +}; + +// Current debug level - set to INFO by default +let currentLevel = DEBUG_LEVELS.INFO; + +/** + * Set the current debug level + * @param {number} level - Debug level from DEBUG_LEVELS + */ +export function setDebugLevel(level) { + currentLevel = level; +} + +/** + * Log an error message + * @param {string} message - Error message + * @param {any} data - Optional error data + */ +export function logError(message, data) { + if (currentLevel >= DEBUG_LEVELS.ERROR) { + console.error(`[ERROR] ${message}`, data || ''); + } +} + +/** + * Log an info message + * @param {string} message - Info message + * @param {any} data - Optional info data + */ +export function logInfo(message, data) { + if (currentLevel >= DEBUG_LEVELS.INFO) { + console.info(`[INFO] ${message}`, data || ''); + } +} + +/** + * Log a debug message + * @param {string} message - Debug message + * @param {any} data - Optional debug data + */ +export function logDebug(message, data) { + if (currentLevel >= DEBUG_LEVELS.DEBUG) { + console.debug(`[DEBUG] ${message}`, data || ''); + } +} + +/** + * Log a verbose message (very detailed) + * @param {string} message - Verbose message + * @param {any} data - Optional verbose data + */ +export function logVerbose(message, data) { + if (currentLevel >= DEBUG_LEVELS.VERBOSE) { + console.debug(`[VERBOSE] ${message}`, data || ''); + } +} + +/** + * Format GitHub activity data for display and debugging + * @param {Array} activityData - GitHub activity data + * @returns {Object} - Statistics and formatted data + */ +export function formatGitHubData(activityData) { + if (!activityData || !Array.isArray(activityData) || activityData.length === 0) { + return { + count: 0, + hasData: false, + message: "No GitHub activity data available" + }; + } + + // Count events by type + const eventTypes = {}; + activityData.forEach(activity => { + const type = activity.type || 'unknown'; + eventTypes[type] = (eventTypes[type] || 0) + 1; + }); + + // Count total values for heatmap + const totalValue = activityData.reduce((total, activity) => { + return total + (activity.value || 0); + }, 0); + + // Find date range + const dates = activityData + .map(activity => activity.date || activity.day || '') + .filter(Boolean) + .sort(); + + const firstDate = dates[0] || 'unknown'; + const lastDate = dates[dates.length - 1] || 'unknown'; + + return { + count: activityData.length, + hasData: true, + totalValue, + eventTypes, + dateRange: { + first: firstDate, + last: lastDate + }, + sampleItem: activityData[0] + }; +} + +export default { + DEBUG_LEVELS, + setDebugLevel, + logError, + logInfo, + logDebug, + logVerbose, + formatGitHubData +}; \ No newline at end of file diff --git a/latest-dashboard.jsx b/latest-dashboard.jsx new file mode 100644 index 0000000..1458b03 Binary files /dev/null and b/latest-dashboard.jsx differ diff --git a/main-dashboard.jsx b/main-dashboard.jsx new file mode 100644 index 0000000..1458b03 Binary files /dev/null and b/main-dashboard.jsx differ