1- import React , { useState , useEffect , lazy , Suspense } from 'react' ;
1+ import React , { useState , useEffect , useRef , useCallback , lazy , Suspense } from 'react' ;
22import { BrowserRouter , Routes , Route , Navigate } from 'react-router-dom' ;
33import Navbar from './components/Navbar' ;
44import GitMeChat from './components/GitMeChat' ;
55import Footer from './components/Footer' ;
66
7+ // Idle-timeout for the in-memory GitHub PAT. After this many minutes of
8+ // inactivity, we wipe state so /profile stops working and an attacker who
9+ // obtains a foothold later (open laptop, XSS from a compromised dep) can't
10+ // find the token in memory. 15 minutes matches common enterprise policy.
11+ const IDLE_WIPE_MINUTES = 15 ;
12+
713const LoginPage = lazy ( ( ) => import ( './pages/LoginPage' ) ) ;
814const HomePage = lazy ( ( ) => import ( './pages/HomePage' ) ) ;
915const ProfilePage = lazy ( ( ) => import ( './pages/ProfilePage' ) ) ;
@@ -21,21 +27,24 @@ const App = () => {
2127 const [ contributionData , setContributionData ] = useState ( null ) ;
2228 const [ isAutoLoggingIn , setIsAutoLoggingIn ] = useState ( false ) ;
2329
24- // --- Automatic Login ---
30+ // --- Automatic Login (LOCAL DEV ONLY) ---
31+ // SECURITY: production builds must NOT bundle VITE_GITHUB_TOKEN. The CI
32+ // workflow deliberately omits it. This block gates the whole auto-login
33+ // path on `import.meta.env.DEV` so even if the vars leak through some
34+ // other build, the token never gets read at runtime in production.
2535 useEffect ( ( ) => {
36+ if ( ! import . meta. env . DEV ) return ;
2637 const autoUsername = import . meta. env . VITE_GITHUB_USERNAME ;
2738 const autoToken = import . meta. env . VITE_GITHUB_TOKEN ;
39+ if ( ! autoUsername || ! autoToken || data || isAutoLoggingIn ) return ;
2840
29- if ( autoUsername && autoToken && ! data && ! isAutoLoggingIn ) {
30- setIsAutoLoggingIn ( true ) ;
31- handleLogin ( autoUsername , autoToken )
32- . catch ( ( err ) => {
33- console . error ( "Auto-login failed:" , err ) ;
34- } )
35- . finally ( ( ) => {
36- setIsAutoLoggingIn ( false ) ;
37- } ) ;
38- }
41+ setIsAutoLoggingIn ( true ) ;
42+ handleLogin ( autoUsername , autoToken )
43+ . catch ( ( ) => {
44+ // Silent — invalid or expired dev token. User can fall back to
45+ // the manual login form.
46+ } )
47+ . finally ( ( ) => setIsAutoLoggingIn ( false ) ) ;
3948 } , [ ] ) ;
4049
4150
@@ -87,8 +96,8 @@ const App = () => {
8796 if ( result . data ?. user ?. contributionsCollection ?. contributionCalendar ) {
8897 return { id : period . id , calendar : result . data . user . contributionsCollection . contributionCalendar } ;
8998 }
90- } catch ( err ) {
91- console . error ( `Error fetching calendar for ${ period . id } :` , err ) ;
99+ } catch ( _err ) {
100+ // Silent — a missing calendar year is a soft failure, not fatal.
92101 }
93102 return null ;
94103 } ;
@@ -175,17 +184,57 @@ const App = () => {
175184 const calendars = await fetchContributionCalendar ( tok , user , yearsToFetch ) ;
176185 setContributionData ( { years : yearsToFetch , calendar : calendars } ) ;
177186 } catch ( err ) {
178- console . error ( "Login error:" , err ) ;
179- throw err ; // Re-throw so LoginPage can catch it
187+ // Do NOT log err here — some GitHub error responses echo request
188+ // metadata that includes the token. Rethrow with a scrubbed message.
189+ throw new Error ( err ?. message || 'Sign-in failed. Check your credentials and try again.' ) ;
180190 }
181191 } ;
182192
183- const handleLogout = ( ) => {
193+ const handleLogout = useCallback ( ( ) => {
184194 setData ( null ) ;
185195 setToken ( '' ) ;
186196 setUsername ( '' ) ;
187197 setContributionData ( null ) ;
188- } ;
198+ } , [ ] ) ;
199+
200+ // --- Idle-timeout token wipe -------------------------------------------
201+ // Reset a timer on any user interaction. If IDLE_WIPE_MINUTES elapse
202+ // with no interaction, clear all auth state. Keeps the token from
203+ // living in memory on an unattended tab.
204+ const idleTimerRef = useRef ( null ) ;
205+ useEffect ( ( ) => {
206+ if ( ! token ) return ;
207+
208+ const reset = ( ) => {
209+ if ( idleTimerRef . current ) clearTimeout ( idleTimerRef . current ) ;
210+ idleTimerRef . current = setTimeout (
211+ handleLogout ,
212+ IDLE_WIPE_MINUTES * 60 * 1000
213+ ) ;
214+ } ;
215+
216+ const events = [ 'mousedown' , 'keydown' , 'scroll' , 'touchstart' , 'visibilitychange' ] ;
217+ events . forEach ( ( e ) => window . addEventListener ( e , reset , { passive : true } ) ) ;
218+ reset ( ) ;
219+
220+ return ( ) => {
221+ events . forEach ( ( e ) => window . removeEventListener ( e , reset ) ) ;
222+ if ( idleTimerRef . current ) clearTimeout ( idleTimerRef . current ) ;
223+ } ;
224+ } , [ token , handleLogout ] ) ;
225+
226+ // Also wipe on `beforeunload` — belt-and-braces since React state dies
227+ // with the tab anyway, but this covers same-origin navigations.
228+ useEffect ( ( ) => {
229+ const wipe = ( ) => {
230+ setToken ( '' ) ;
231+ setUsername ( '' ) ;
232+ setData ( null ) ;
233+ setContributionData ( null ) ;
234+ } ;
235+ window . addEventListener ( 'beforeunload' , wipe ) ;
236+ return ( ) => window . removeEventListener ( 'beforeunload' , wipe ) ;
237+ } , [ ] ) ;
189238
190239 return (
191240 < BrowserRouter basename = "/gitme" >
0 commit comments