-
Notifications
You must be signed in to change notification settings - Fork 200
Feature/bookmarks #558
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Feature/bookmarks #558
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| const express = require('express'); | ||
| const router = express.Router(); | ||
| const User = require('../models/User'); | ||
|
|
||
| // Simple auth check middleware (session-based via passport) | ||
| function ensureAuth(req, res, next) { | ||
| if (req.isAuthenticated && req.isAuthenticated()) return next(); | ||
| return res.status(401).json({ message: 'Authentication required' }); | ||
| } | ||
|
|
||
| // GET /api/bookmarks - returns current user's bookmarks | ||
| router.get('/', ensureAuth, async (req, res) => { | ||
| try { | ||
| console.log('GET /api/bookmarks req.isAuthenticated=', req.isAuthenticated && req.isAuthenticated(), 'user=', req.user && req.user.id); | ||
| const user = await User.findById(req.user._id).select('bookmarks'); | ||
| return res.json({ bookmarks: user?.bookmarks || [] }); | ||
| } catch (err) { | ||
| console.error('Error fetching bookmarks', err); | ||
| return res.status(500).json({ message: 'Failed to fetch bookmarks', error: err.message }); | ||
| } | ||
| }); | ||
|
|
||
| // POST /api/bookmarks - add a bookmark | ||
| router.post('/', ensureAuth, async (req, res) => { | ||
| try { | ||
| console.log('POST /api/bookmarks req.isAuthenticated=', req.isAuthenticated && req.isAuthenticated(), 'user=', req.user && req.user.id, 'body=', req.body); | ||
| const { githubUsername, avatarUrl } = req.body; | ||
| if (!githubUsername || !githubUsername.trim()) { | ||
| return res.status(400).json({ message: 'githubUsername is required' }); | ||
| } | ||
|
|
||
| const user = await User.findById(req.user._id); | ||
| if (!user) return res.status(404).json({ message: 'User not found' }); | ||
|
|
||
| const exists = user.bookmarks?.some( | ||
| (b) => b.githubUsername.toLowerCase() === githubUsername.toLowerCase() | ||
| ); | ||
| if (exists) return res.status(409).json({ message: 'Bookmark already exists' }); | ||
|
|
||
| user.bookmarks = user.bookmarks || []; | ||
| user.bookmarks.unshift({ githubUsername, avatarUrl }); | ||
| await user.save(); | ||
|
|
||
| return res.status(201).json({ message: 'Bookmark saved', bookmark: user.bookmarks[0] }); | ||
| } catch (err) { | ||
| console.error('Error saving bookmark', err); | ||
| return res.status(500).json({ message: 'Failed to save bookmark', error: err.message }); | ||
| } | ||
| }); | ||
|
|
||
| // DELETE /api/bookmarks/:username - remove bookmark | ||
| router.delete('/:username', ensureAuth, async (req, res) => { | ||
| try { | ||
| console.log('DELETE /api/bookmarks/:username req.isAuthenticated=', req.isAuthenticated && req.isAuthenticated(), 'user=', req.user && req.user.id, 'params=', req.params); | ||
| const username = req.params.username; | ||
| const user = await User.findById(req.user._id); | ||
| if (!user) return res.status(404).json({ message: 'User not found' }); | ||
|
|
||
| const before = (user.bookmarks || []).length; | ||
| user.bookmarks = (user.bookmarks || []).filter( | ||
| (b) => b.githubUsername.toLowerCase() !== username.toLowerCase() | ||
| ); | ||
|
|
||
| if (user.bookmarks.length === before) { | ||
| return res.status(404).json({ message: 'Bookmark not found' }); | ||
| } | ||
|
|
||
| await user.save(); | ||
| return res.json({ message: 'Bookmark removed' }); | ||
| } catch (err) { | ||
| return res.status(500).json({ message: 'Failed to remove bookmark', error: err.message }); | ||
| } | ||
| }); | ||
|
|
||
| module.exports = router; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,8 @@ import Signup from "../pages/Signup/Signup.tsx"; | |
| import Login from "../pages/Login/Login.tsx"; | ||
| import ContributorProfile from "../pages/ContributorProfile/ContributorProfile.tsx"; | ||
| import Home from "../pages/Home/Home.tsx"; | ||
| import Activity from "../pages/Activity.tsx"; | ||
| import Activity from "../pages/Activity.tsx"; | ||
| import Bookmarks from "../pages/Bookmarks"; | ||
| import PrivacyPolicy from "../pages/Privacy/PrivacyPolicy.tsx"; // ✅ Updated import path to match your new folder structure | ||
|
|
||
| const Router = () => { | ||
|
|
@@ -22,6 +23,7 @@ const Router = () => { | |
| <Route path="/contributors" element={<Contributors />} /> | ||
| <Route path="/contributor/:username" element={<ContributorProfile />} /> | ||
| <Route path="/activity" element={<Activity />} /> | ||
| <Route path="/bookmarks" element={<Bookmarks />} /> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify whether an existing route guard/auth wrapper already exists to reuse.
rg -n --type=tsx -C3 '\b(ProtectedRoute|PrivateRoute|RequireAuth|AuthGuard)\b|isAuthenticated|currentUser|/login' srcRepository: GitMetricsLab/github_tracker Length of output: 100 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect Router.tsx around the referenced line
echo "---- Router.tsx (context around /bookmarks) ----"
if [ -f "src/Routes/Router.tsx" ]; then
nl -ba src/Routes/Router.tsx | sed -n '1,120p' | sed -n '15,45p'
else
echo "Missing: src/Routes/Router.tsx" >&2
exit 1
fi
echo "---- Search for existing auth/route guards in TSX files ----"
rg -n -g'*.{ts,tsx}' -C3 '\b(ProtectedRoute|PrivateRoute|RequireAuth|AuthGuard)\b|isAuthenticated|currentUser|/login' src || true
echo "---- Search specifically for route protection patterns (auth + redirect) ----"
rg -n -g'*.{ts,tsx}' -C3 '(Navigate\s+to=|redirect|/login|useAuth|useUser|auth|session)' src/Routes src || trueRepository: GitMetricsLab/github_tracker Length of output: 182 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "---- Check file exists ----"
ls -la src/Routes/Router.tsx || true
echo "---- Router.tsx (context around /bookmarks) ----"
if [ -f "src/Routes/Router.tsx" ]; then
cat -n src/Routes/Router.tsx | sed -n '15,45p'
fi
echo "---- Search for existing auth/route guards in TS/TSX files ----"
rg -n -g'*.ts' -g'*.tsx' -C3 '\b(ProtectedRoute|PrivateRoute|RequireAuth|AuthGuard)\b|isAuthenticated|currentUser|/login' src || true
echo "---- Search specifically for route protection patterns (auth + redirect) ----"
rg -n -g'*.ts' -g'*.tsx' -C3 '(Navigate\s+to=|redirect|/login|useAuth|useUser|auth|session)' src/Routes src || trueRepository: GitMetricsLab/github_tracker Length of output: 14988 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "---- Inspect src/App.tsx (routing/auth gating) ----"
cat -n src/App.tsx | sed -n '1,120p'
echo "---- Inspect src/pages/Bookmarks/* (if exists) ----"
ls -la src/pages/Bookmarks || true
fd -t f "Bookmarks.*" src/pages || true
fd -t f "bookmarks" src/pages -i || true
# Show Bookmarks route component if present
if [ -d "src/pages/Bookmarks" ]; then
fd -t f . src/pages/Bookmarks --max-depth 2 -i || true
fi
# fallback: search for "function Bookmarks" or "const Bookmarks"
rg -n -g'*.{ts,tsx}' 'function\s+Bookmarks|const\s+Bookmarks|export\s+default\s+Bookmarks' src/pages src/components src -S || trueRepository: GitMetricsLab/github_tracker Length of output: 2104 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "---- Router.tsx (full) ----"
cat -n src/Routes/Router.tsx
echo "---- Bookmarks.tsx (key sections) ----"
cat -n src/pages/Bookmarks.tsx | sed -n '1,200p'
echo "---- Search for routing-level auth wrappers/components ----"
rg -n -g'*.{ts,tsx}' '\b(ProtectedRoute|PrivateRoute|RequireAuth|AuthGuard)\b' src || true
echo "---- Search for auth checks / redirects used in pages/components ----"
rg -n -g'*.{ts,tsx}' 'Navigate\s+to=|/login|redirect|withCredentials|session|token|useGitHubAuth|useAuth|isAuthenticated|currentUser' src || true
echo "---- Auth usage within Bookmarks.tsx ----"
rg -n -g'*.tsx' 'useGitHubAuth|token|authError|Navigate\s+to=|/login|withCredentials|axios|fetch' src/pages/Bookmarks.tsx || trueRepository: GitMetricsLab/github_tracker Length of output: 9650 Protect the
🤖 Prompt for AI Agents |
||
|
|
||
| {/* Privacy Policy page route */} | ||
| <Route path="/privacy" element={<PrivacyPolicy />} /> | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,8 +12,12 @@ interface EventType { | |||||||||||||||||||||||||||||||||||
| export default function ActivityFeed({ username }: { username: string }) { | ||||||||||||||||||||||||||||||||||||
| const [events, setEvents] = useState<EventType[]>([]); | ||||||||||||||||||||||||||||||||||||
| const [loading, setLoading] = useState(true); | ||||||||||||||||||||||||||||||||||||
| const [error, setError] = useState(""); | ||||||||||||||||||||||||||||||||||||
| const [isBookmarked, setIsBookmarked] = useState(false); | ||||||||||||||||||||||||||||||||||||
| const [bookmarkLoading, setBookmarkLoading] = useState(false); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:5000'; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // 🕒 time ago function | ||||||||||||||||||||||||||||||||||||
| const getTimeAgo = (dateString: string) => { | ||||||||||||||||||||||||||||||||||||
| const diff = Math.floor( | ||||||||||||||||||||||||||||||||||||
| (Date.now() - new Date(dateString).getTime()) / 1000 | ||||||||||||||||||||||||||||||||||||
|
|
@@ -26,19 +30,69 @@ export default function ActivityFeed({ username }: { username: string }) { | |||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||
| // check bookmark state for this username | ||||||||||||||||||||||||||||||||||||
| const checkBookmark = async () => { | ||||||||||||||||||||||||||||||||||||
| if (!username.trim()) return setIsBookmarked(false); | ||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||
| const res = await fetch(`${backendUrl}/api/bookmarks`, { credentials: 'include' }); | ||||||||||||||||||||||||||||||||||||
| if (!res.ok) return setIsBookmarked(false); | ||||||||||||||||||||||||||||||||||||
| const data = await res.json(); | ||||||||||||||||||||||||||||||||||||
| const found = (data.bookmarks || []).some( | ||||||||||||||||||||||||||||||||||||
| (b: any) => b.githubUsername.toLowerCase() === username.toLowerCase() | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
| setIsBookmarked(!!found); | ||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||
| // ignore | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+39
to
+45
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Locate and show the relevant code in ActivityFeed.tsx
FILE="src/components/ActivityFeed.tsx"
if [ ! -f "$FILE" ]; then
echo "Missing file: $FILE" >&2
exit 1
fi
echo "---- $FILE (lines 25-80) ----"
nl -ba "$FILE" | sed -n '25,80p'
echo "---- grep for catch bindings in ActivityFeed.tsx ----"
rg -n "catch\s*\([^)]*\)" "$FILE" || true
echo "---- grep for 'any' in ActivityFeed.tsx ----"
rg -n "\bany\b" "$FILE" || trueRepository: GitMetricsLab/github_tracker Length of output: 190 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE="src/components/ActivityFeed.tsx"
if [ ! -f "$FILE" ]; then
echo "Missing file: $FILE" >&2
exit 1
fi
echo "---- $FILE (lines 25-80) ----"
cat -n "$FILE" | sed -n '25,80p'
echo "---- grep for catch bindings in ActivityFeed.tsx ----"
rg -n "catch\s*\([^)]*\)" "$FILE" || true
echo "---- grep for 'any' in ActivityFeed.tsx ----"
rg -n "\bany\b" "$FILE" || trueRepository: GitMetricsLab/github_tracker Length of output: 2479 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE="src/components/ActivityFeed.tsx"
echo "---- $FILE (lines 80-160) ----"
cat -n "$FILE" | sed -n '80,160p'
echo "---- grep for 'any' in $FILE ----"
rg -n "\bany\b" "$FILE" || trueRepository: GitMetricsLab/github_tracker Length of output: 3425 Type bookmark payload and remove unused catch parameter in the bookmark check
Suggested fix+type BookmarkPayload = { bookmarks?: Array<{ githubUsername: string }> };
+
...
- const data = await res.json();
+ const data: BookmarkPayload = await res.json();
const found = (data.bookmarks || []).some(
- (b: any) => b.githubUsername.toLowerCase() === username.toLowerCase()
+ (b) => b.githubUsername.toLowerCase() === username.toLowerCase()
);
setIsBookmarked(!!found);
- } catch (err) {
+ } catch {
// ignore
}📝 Committable suggestion
Suggested change
🧰 Tools🪛 ESLint[error] 41-41: Unexpected any. Specify a different type. ( [error] 44-44: 'err' is defined but never used. ( 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| checkBookmark(); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const fetchEvents = async () => { | ||||||||||||||||||||||||||||||||||||
| if (!username.trim()) { | ||||||||||||||||||||||||||||||||||||
| setEvents([]); | ||||||||||||||||||||||||||||||||||||
| setError("Please enter a GitHub username to get started."); | ||||||||||||||||||||||||||||||||||||
| setLoading(false); | ||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||
| setLoading(true); | ||||||||||||||||||||||||||||||||||||
| setError(""); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const res = await fetch( | ||||||||||||||||||||||||||||||||||||
| `https://api.github.com/users/${username}/events` | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
63
to
65
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE="src/components/ActivityFeed.tsx"
echo "== Lines 50-90 =="
nl -ba "$FILE" | sed -n '50,90p'
echo
echo "== Search for encodeURIComponent(username) in repo =="
rg -n "encodeURIComponent\s*\(\s*username\s*\)" .
echo
echo "== Search for username usage/sanitization in this file =="
rg -n "\busername\b" "$FILE"Repository: GitMetricsLab/github_tracker Length of output: 151 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE="src/components/ActivityFeed.tsx"
echo "== Lines 50-90 =="
cat -n "$FILE" | sed -n '50,90p'
echo
echo "== Search for encodeURIComponent(username) in repo =="
rg -n "encodeURIComponent\s*\(\s*username\s*\)" .
echo
echo "== Search for username usage/sanitization in this file =="
rg -n "\busername\b" "$FILE"Repository: GitMetricsLab/github_tracker Length of output: 2740 Encode
Suggested fix- const res = await fetch(
- `https://api.github.com/users/${username}/events`
- );
+ const res = await fetch(
+ `https://api.github.com/users/${encodeURIComponent(username)}/events`
+ );📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if (!res.ok) { | ||||||||||||||||||||||||||||||||||||
| let message = "Unable to load activity. Please try again."; | ||||||||||||||||||||||||||||||||||||
| if (res.status === 404) { | ||||||||||||||||||||||||||||||||||||
| message = "GitHub user not found. Please check the username."; | ||||||||||||||||||||||||||||||||||||
| } else if (res.status === 403) { | ||||||||||||||||||||||||||||||||||||
| message = | ||||||||||||||||||||||||||||||||||||
| "GitHub rate limit exceeded. Wait a moment and try again."; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| setEvents([]); | ||||||||||||||||||||||||||||||||||||
| setError(message); | ||||||||||||||||||||||||||||||||||||
| setLoading(false); | ||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const data = await res.json(); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if (!Array.isArray(data)) { | ||||||||||||||||||||||||||||||||||||
| setError("Unexpected response from GitHub. Please try again."); | ||||||||||||||||||||||||||||||||||||
| setEvents([]); | ||||||||||||||||||||||||||||||||||||
| setLoading(false); | ||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| setEvents(data); | ||||||||||||||||||||||||||||||||||||
| setLoading(false); | ||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||
| console.error(err); | ||||||||||||||||||||||||||||||||||||
| setError("Unable to fetch activity. Check your connection and try again."); | ||||||||||||||||||||||||||||||||||||
| setEvents([]); | ||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||
| setLoading(false); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
|
|
@@ -49,40 +103,106 @@ export default function ActivityFeed({ username }: { username: string }) { | |||||||||||||||||||||||||||||||||||
| return () => clearInterval(interval); | ||||||||||||||||||||||||||||||||||||
| }, [username]); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const handleAddBookmark = async () => { | ||||||||||||||||||||||||||||||||||||
| if (!username.trim()) return; | ||||||||||||||||||||||||||||||||||||
| setBookmarkLoading(true); | ||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||
| const res = await fetch(`${backendUrl}/api/bookmarks`, { | ||||||||||||||||||||||||||||||||||||
| method: 'POST', | ||||||||||||||||||||||||||||||||||||
| credentials: 'include', | ||||||||||||||||||||||||||||||||||||
| headers: { 'Content-Type': 'application/json' }, | ||||||||||||||||||||||||||||||||||||
| body: JSON.stringify({ githubUsername: username }), | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| if (!res.ok) throw new Error('Failed to save'); | ||||||||||||||||||||||||||||||||||||
| setIsBookmarked(true); | ||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||
| console.error(err); | ||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||
| setBookmarkLoading(false); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const handleRemoveBookmark = async () => { | ||||||||||||||||||||||||||||||||||||
| if (!username.trim()) return; | ||||||||||||||||||||||||||||||||||||
| setBookmarkLoading(true); | ||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||
| const res = await fetch(`${backendUrl}/api/bookmarks/${encodeURIComponent(username)}`, { | ||||||||||||||||||||||||||||||||||||
| method: 'DELETE', | ||||||||||||||||||||||||||||||||||||
| credentials: 'include', | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| if (!res.ok) throw new Error('Failed to remove'); | ||||||||||||||||||||||||||||||||||||
| setIsBookmarked(false); | ||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||
| console.error(err); | ||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||
| setBookmarkLoading(false); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const currentEvents = events.slice(0, 10); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||
| <div className="p-4"> | ||||||||||||||||||||||||||||||||||||
| <h2 className="text-xl font-bold mb-4 text-center"> | ||||||||||||||||||||||||||||||||||||
| Activity Feed | ||||||||||||||||||||||||||||||||||||
| </h2> | ||||||||||||||||||||||||||||||||||||
| <div className="rounded-[2rem] border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900 p-6 shadow-lg"> | ||||||||||||||||||||||||||||||||||||
| <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between mb-5"> | ||||||||||||||||||||||||||||||||||||
| <div className="flex items-center gap-3"> | ||||||||||||||||||||||||||||||||||||
| <h2 className="text-2xl font-bold">Activity Feed</h2> | ||||||||||||||||||||||||||||||||||||
| <p className="text-sm text-gray-500 dark:text-gray-400"> | ||||||||||||||||||||||||||||||||||||
| Tracking <span className="font-semibold text-gray-900 dark:text-white">{username}</span> | ||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| <div className="flex items-center gap-3"> | ||||||||||||||||||||||||||||||||||||
| <p className="text-xs uppercase tracking-[0.2em] text-gray-400 mr-3">Refreshes every 30s</p> | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||
| onClick={isBookmarked ? handleRemoveBookmark : handleAddBookmark} | ||||||||||||||||||||||||||||||||||||
| disabled={bookmarkLoading} | ||||||||||||||||||||||||||||||||||||
| title={isBookmarked ? 'Remove bookmark' : 'Save bookmark'} | ||||||||||||||||||||||||||||||||||||
| className={`flex items-center gap-2 px-3 py-1.5 rounded-full transition ${isBookmarked ? 'bg-yellow-500 text-white' : 'bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-white'}`} | ||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||
| <span className="text-lg">{isBookmarked ? '★' : '☆'}</span> | ||||||||||||||||||||||||||||||||||||
| <span className="text-sm font-medium">{isBookmarked ? 'Saved' : 'Save'}</span> | ||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| {loading ? ( | ||||||||||||||||||||||||||||||||||||
| <p className="text-center">Loading...</p> | ||||||||||||||||||||||||||||||||||||
| ) : events.length === 0 ? ( | ||||||||||||||||||||||||||||||||||||
| <p className="text-center">No activity found</p> | ||||||||||||||||||||||||||||||||||||
| <div className="rounded-3xl border border-dashed border-indigo-300 bg-indigo-50/70 p-6 text-center text-indigo-700 dark:border-indigo-500/50 dark:bg-indigo-950/40 dark:text-indigo-200"> | ||||||||||||||||||||||||||||||||||||
| Loading GitHub activity... | ||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||
| ) : error ? ( | ||||||||||||||||||||||||||||||||||||
| <div className="rounded-3xl border border-red-200 bg-red-50 p-6 text-center text-red-700 dark:border-red-600/40 dark:bg-red-950/20 dark:text-red-200"> | ||||||||||||||||||||||||||||||||||||
| {error} | ||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||
| ) : currentEvents.length === 0 ? ( | ||||||||||||||||||||||||||||||||||||
| <div className="rounded-3xl border border-gray-200 bg-gray-50 p-6 text-center text-gray-600 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"> | ||||||||||||||||||||||||||||||||||||
| No recent public activity found for this user. | ||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||
| events.slice(0, 10).map((event) => ( | ||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||
| key={event.id} | ||||||||||||||||||||||||||||||||||||
| className="border rounded-lg p-3 mb-3 shadow-sm bg-white dark:bg-gray-700" | ||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||
| <p className="text-sm font-semibold"> | ||||||||||||||||||||||||||||||||||||
| {event.type === "PushEvent" && "🚀 Commit pushed"} | ||||||||||||||||||||||||||||||||||||
| {event.type === "PullRequestEvent" && "🔀 Pull Request"} | ||||||||||||||||||||||||||||||||||||
| {event.type === "IssuesEvent" && "🐛 Issue"} | ||||||||||||||||||||||||||||||||||||
| {event.type === "WatchEvent" && "⭐ Starred repo"} | ||||||||||||||||||||||||||||||||||||
| {![ | ||||||||||||||||||||||||||||||||||||
| "PushEvent", | ||||||||||||||||||||||||||||||||||||
| "PullRequestEvent", | ||||||||||||||||||||||||||||||||||||
| "IssuesEvent", | ||||||||||||||||||||||||||||||||||||
| "WatchEvent", | ||||||||||||||||||||||||||||||||||||
| ].includes(event.type) && event.type} | ||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| <p className="text-xs text-gray-500 mt-1"> | ||||||||||||||||||||||||||||||||||||
| {event.repo?.name} • {getTimeAgo(event.created_at)} | ||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||
| )) | ||||||||||||||||||||||||||||||||||||
| <div className="space-y-3"> | ||||||||||||||||||||||||||||||||||||
| {currentEvents.map((event) => ( | ||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||
| key={event.id} | ||||||||||||||||||||||||||||||||||||
| className="rounded-3xl border border-gray-200 bg-gray-50 p-4 shadow-sm transition hover:border-indigo-300 dark:border-gray-700 dark:bg-gray-800" | ||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||
| <p className="text-sm font-semibold text-gray-900 dark:text-white"> | ||||||||||||||||||||||||||||||||||||
| {event.type === "PushEvent" && "🚀 Commit pushed"} | ||||||||||||||||||||||||||||||||||||
| {event.type === "PullRequestEvent" && "🔀 Pull request event"} | ||||||||||||||||||||||||||||||||||||
| {event.type === "IssuesEvent" && "🐛 Issue event"} | ||||||||||||||||||||||||||||||||||||
| {event.type === "WatchEvent" && "⭐ Starred repository"} | ||||||||||||||||||||||||||||||||||||
| {![ | ||||||||||||||||||||||||||||||||||||
| "PushEvent", | ||||||||||||||||||||||||||||||||||||
| "PullRequestEvent", | ||||||||||||||||||||||||||||||||||||
| "IssuesEvent", | ||||||||||||||||||||||||||||||||||||
| "WatchEvent", | ||||||||||||||||||||||||||||||||||||
| ].includes(event.type) && event.type} | ||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||
| <p className="mt-2 text-sm text-gray-500 dark:text-gray-400"> | ||||||||||||||||||||||||||||||||||||
| {event.repo?.name || "Unknown repository"} • {getTimeAgo(event.created_at)} | ||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prevent hardcoded session secret in production.
The fallback
'dev-secret'is a security risk if theSESSION_SECRETenvironment variable is not set in production. A weak or known secret enables session forgery and hijacking attacks.🔒 Enforce SESSION_SECRET in production
Option 1 (recommended): Fail fast in production if secret is missing:
app.use(session({ - secret: process.env.SESSION_SECRET || 'dev-secret', + secret: process.env.SESSION_SECRET || (() => { + if (process.env.NODE_ENV === 'production') { + throw new Error('SESSION_SECRET must be set in production'); + } + logger.warn('Using dev-secret for session - DO NOT use in production'); + return 'dev-secret'; + })(), resave: false, saveUninitialized: false,Option 2 (simpler): Require SESSION_SECRET always:
And document in README/env.example that developers must set
SESSION_SECRET=dev-secretlocally.📝 Committable suggestion
🤖 Prompt for AI Agents