@@ -8,6 +8,7 @@ import { ContextualHelpIcon } from './ContextualHelpIcon';
88import { useTheme } from './ThemeProvider' ;
99import { useRegisterModal } from './modal/ModalStackProvider' ;
1010import { api } from '~/trpc/react' ;
11+ import { useAuth } from './AuthProvider' ;
1112
1213interface GeneralSettingsModalProps {
1314 isOpen : boolean ;
@@ -17,7 +18,9 @@ interface GeneralSettingsModalProps {
1718export function GeneralSettingsModal ( { isOpen, onClose } : GeneralSettingsModalProps ) {
1819 useRegisterModal ( isOpen , { id : 'general-settings-modal' , allowEscape : true , onClose } ) ;
1920 const { theme, setTheme } = useTheme ( ) ;
21+ const { isAuthenticated, expirationTime, checkAuth } = useAuth ( ) ;
2022 const [ activeTab , setActiveTab ] = useState < 'general' | 'github' | 'auth' | 'auto-sync' > ( 'general' ) ;
23+ const [ sessionExpirationDisplay , setSessionExpirationDisplay ] = useState < string > ( '' ) ;
2124 const [ githubToken , setGithubToken ] = useState ( '' ) ;
2225 const [ saveFilter , setSaveFilter ] = useState ( false ) ;
2326 const [ savedFilters , setSavedFilters ] = useState < any > ( null ) ;
@@ -34,6 +37,7 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
3437 const [ authHasCredentials , setAuthHasCredentials ] = useState ( false ) ;
3538 const [ authSetupCompleted , setAuthSetupCompleted ] = useState ( false ) ;
3639 const [ authLoading , setAuthLoading ] = useState ( false ) ;
40+ const [ sessionDurationDays , setSessionDurationDays ] = useState ( 7 ) ;
3741
3842 // Auto-sync state
3943 const [ autoSyncEnabled , setAutoSyncEnabled ] = useState ( false ) ;
@@ -214,11 +218,12 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
214218 try {
215219 const response = await fetch ( '/api/settings/auth-credentials' ) ;
216220 if ( response . ok ) {
217- const data = await response . json ( ) as { username : string ; enabled : boolean ; hasCredentials : boolean ; setupCompleted : boolean } ;
221+ const data = await response . json ( ) as { username : string ; enabled : boolean ; hasCredentials : boolean ; setupCompleted : boolean ; sessionDurationDays ?: number } ;
218222 setAuthUsername ( data . username ?? '' ) ;
219223 setAuthEnabled ( data . enabled ?? false ) ;
220224 setAuthHasCredentials ( data . hasCredentials ?? false ) ;
221225 setAuthSetupCompleted ( data . setupCompleted ?? false ) ;
226+ setSessionDurationDays ( data . sessionDurationDays ?? 7 ) ;
222227 }
223228 } catch ( error ) {
224229 console . error ( 'Error loading auth credentials:' , error ) ;
@@ -227,6 +232,64 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
227232 }
228233 } ;
229234
235+ // Format expiration time display
236+ const formatExpirationTime = ( expTime : number | null ) : string => {
237+ if ( ! expTime ) return 'No active session' ;
238+
239+ const now = Date . now ( ) ;
240+ const timeUntilExpiration = expTime - now ;
241+
242+ if ( timeUntilExpiration <= 0 ) {
243+ return 'Session expired' ;
244+ }
245+
246+ const days = Math . floor ( timeUntilExpiration / ( 1000 * 60 * 60 * 24 ) ) ;
247+ const hours = Math . floor ( ( timeUntilExpiration % ( 1000 * 60 * 60 * 24 ) ) / ( 1000 * 60 * 60 ) ) ;
248+ const minutes = Math . floor ( ( timeUntilExpiration % ( 1000 * 60 * 60 ) ) / ( 1000 * 60 ) ) ;
249+
250+ const parts : string [ ] = [ ] ;
251+ if ( days > 0 ) {
252+ parts . push ( `${ days } ${ days === 1 ? 'day' : 'days' } ` ) ;
253+ }
254+ if ( hours > 0 ) {
255+ parts . push ( `${ hours } ${ hours === 1 ? 'hour' : 'hours' } ` ) ;
256+ }
257+ if ( minutes > 0 && days === 0 ) {
258+ parts . push ( `${ minutes } ${ minutes === 1 ? 'minute' : 'minutes' } ` ) ;
259+ }
260+
261+ if ( parts . length === 0 ) {
262+ return 'Less than a minute' ;
263+ }
264+
265+ return parts . join ( ', ' ) ;
266+ } ;
267+
268+ // Update expiration display periodically
269+ useEffect ( ( ) => {
270+ const updateExpirationDisplay = ( ) => {
271+ if ( expirationTime ) {
272+ setSessionExpirationDisplay ( formatExpirationTime ( expirationTime ) ) ;
273+ } else {
274+ setSessionExpirationDisplay ( '' ) ;
275+ }
276+ } ;
277+
278+ updateExpirationDisplay ( ) ;
279+
280+ // Update every minute
281+ const interval = setInterval ( updateExpirationDisplay , 60000 ) ;
282+
283+ return ( ) => clearInterval ( interval ) ;
284+ } , [ expirationTime ] ) ;
285+
286+ // Refresh auth when tab changes to auth tab
287+ useEffect ( ( ) => {
288+ if ( activeTab === 'auth' && isOpen ) {
289+ void checkAuth ( ) ;
290+ }
291+ } , [ activeTab , isOpen , checkAuth ] ) ;
292+
230293 const saveAuthCredentials = async ( ) => {
231294 if ( authPassword !== authConfirmPassword ) {
232295 setMessage ( { type : 'error' , text : 'Passwords do not match' } ) ;
@@ -265,6 +328,41 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
265328 }
266329 } ;
267330
331+ const saveSessionDuration = async ( days : number ) => {
332+ if ( days < 1 || days > 365 ) {
333+ setMessage ( { type : 'error' , text : 'Session duration must be between 1 and 365 days' } ) ;
334+ return ;
335+ }
336+
337+ setAuthLoading ( true ) ;
338+ setMessage ( null ) ;
339+
340+ try {
341+ const response = await fetch ( '/api/settings/auth-credentials' , {
342+ method : 'PATCH' ,
343+ headers : {
344+ 'Content-Type' : 'application/json' ,
345+ } ,
346+ body : JSON . stringify ( { sessionDurationDays : days } ) ,
347+ } ) ;
348+
349+ if ( response . ok ) {
350+ setMessage ( { type : 'success' , text : `Session duration updated to ${ days } days` } ) ;
351+ setSessionDurationDays ( days ) ;
352+ setTimeout ( ( ) => setMessage ( null ) , 3000 ) ;
353+ } else {
354+ const errorData = await response . json ( ) ;
355+ setMessage ( { type : 'error' , text : errorData . error ?? 'Failed to update session duration' } ) ;
356+ setTimeout ( ( ) => setMessage ( null ) , 3000 ) ;
357+ }
358+ } catch {
359+ setMessage ( { type : 'error' , text : 'Failed to update session duration' } ) ;
360+ setTimeout ( ( ) => setMessage ( null ) , 3000 ) ;
361+ } finally {
362+ setAuthLoading ( false ) ;
363+ }
364+ } ;
365+
268366 const toggleAuthEnabled = async ( enabled : boolean ) => {
269367 setAuthLoading ( true ) ;
270368 setMessage ( null ) ;
@@ -662,7 +760,10 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
662760 { activeTab === 'auth' && (
663761 < div className = "space-y-4 sm:space-y-6" >
664762 < div >
665- < h3 className = "text-base sm:text-lg font-medium text-foreground mb-3 sm:mb-4" > Authentication Settings</ h3 >
763+ < div className = "flex items-center gap-2 mb-3 sm:mb-4" >
764+ < h3 className = "text-base sm:text-lg font-medium text-foreground" > Authentication Settings</ h3 >
765+ < ContextualHelpIcon section = "auth-settings" tooltip = "Help with Authentication Settings" />
766+ </ div >
666767 < p className = "text-sm sm:text-base text-muted-foreground mb-4" >
667768 Configure authentication to secure access to your application.
668769 </ p >
@@ -699,6 +800,68 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
699800 </ div >
700801 </ div >
701802
803+ { isAuthenticated && expirationTime && (
804+ < div className = "p-4 border border-border rounded-lg" >
805+ < h4 className = "font-medium text-foreground mb-2" > Session Information</ h4 >
806+ < div className = "space-y-2" >
807+ < div >
808+ < p className = "text-sm text-muted-foreground" > Session expires in:</ p >
809+ < p className = "text-sm font-medium text-foreground" > { sessionExpirationDisplay } </ p >
810+ </ div >
811+ < div >
812+ < p className = "text-sm text-muted-foreground" > Expiration date:</ p >
813+ < p className = "text-sm font-medium text-foreground" >
814+ { new Date ( expirationTime ) . toLocaleString ( ) }
815+ </ p >
816+ </ div >
817+ </ div >
818+ </ div >
819+ ) }
820+
821+ < div className = "p-4 border border-border rounded-lg" >
822+ < h4 className = "font-medium text-foreground mb-2" > Session Duration</ h4 >
823+ < p className = "text-sm text-muted-foreground mb-4" >
824+ Configure how long user sessions should last before requiring re-authentication.
825+ </ p >
826+
827+ < div className = "space-y-3" >
828+ < div >
829+ < label htmlFor = "session-duration" className = "block text-sm font-medium text-foreground mb-1" >
830+ Session Duration (days)
831+ </ label >
832+ < div className = "flex items-center gap-3" >
833+ < Input
834+ id = "session-duration"
835+ type = "number"
836+ min = "1"
837+ max = "365"
838+ placeholder = "Enter days"
839+ value = { sessionDurationDays }
840+ onChange = { ( e : React . ChangeEvent < HTMLInputElement > ) => {
841+ const value = parseInt ( e . target . value , 10 ) ;
842+ if ( ! isNaN ( value ) ) {
843+ setSessionDurationDays ( value ) ;
844+ }
845+ } }
846+ disabled = { authLoading || ! authSetupCompleted }
847+ className = "w-32"
848+ />
849+ < span className = "text-sm text-muted-foreground" > days (1-365)</ span >
850+ < Button
851+ onClick = { ( ) => saveSessionDuration ( sessionDurationDays ) }
852+ disabled = { authLoading || ! authSetupCompleted }
853+ size = "sm"
854+ >
855+ Save
856+ </ Button >
857+ </ div >
858+ < p className = "text-xs text-muted-foreground mt-2" >
859+ Note: This setting applies to new logins. Current sessions will not be affected.
860+ </ p >
861+ </ div >
862+ </ div >
863+ </ div >
864+
702865 < div className = "p-4 border border-border rounded-lg" >
703866 < h4 className = "font-medium text-foreground mb-2" > Update Credentials</ h4 >
704867 < p className = "text-sm text-muted-foreground mb-4" >
0 commit comments