@@ -498,44 +498,49 @@ export default function Settings() {
498498 ) }
499499
500500 { activeTab === 'general' && (
501- < Card >
502- < CardHeader >
503- < CardTitle > General Settings</ CardTitle >
504- < CardDescription > Application preferences and security.</ CardDescription >
505- </ CardHeader >
506- < CardContent className = "space-y-6" >
507- < div className = "space-y-4" >
508- < h3 className = "text-lg font-medium" > Change Password</ h3 >
509- < form onSubmit = { handleChangePassword } className = "space-y-4 max-w-sm" >
510- < div className = "space-y-2" >
511- < label htmlFor = "old-password" > Current Password</ label >
512- < Input
513- id = "old-password"
514- type = "password"
515- value = { oldPassword }
516- onChange = { ( e ) => setOldPassword ( e . target . value ) }
517- placeholder = "Your current password"
518- autoComplete = "current-password"
519- />
520- </ div >
521- < div className = "space-y-2" >
522- < label htmlFor = "new-password" > New Password</ label >
523- < Input
524- id = "new-password"
525- type = "password"
526- value = { newPassword }
527- onChange = { ( e ) => setNewPassword ( e . target . value ) }
528- placeholder = "Min. 8 characters"
529- autoComplete = "new-password"
530- />
531- </ div >
532- < Button type = "submit" disabled = { ! oldPassword || ! newPassword || passwordMutation . isPending } >
533- { passwordMutation . isPending ? 'Updating...' : 'Update Password' }
534- </ Button >
535- </ form >
536- </ div >
537- </ CardContent >
538- </ Card >
501+ < div className = "space-y-6" >
502+ < Card >
503+ < CardHeader >
504+ < CardTitle > General Settings</ CardTitle >
505+ < CardDescription > Application preferences and security.</ CardDescription >
506+ </ CardHeader >
507+ < CardContent className = "space-y-6" >
508+ < div className = "space-y-4" >
509+ < h3 className = "text-lg font-medium" > Change Password</ h3 >
510+ < form onSubmit = { handleChangePassword } className = "space-y-4 max-w-sm" >
511+ < div className = "space-y-2" >
512+ < label htmlFor = "old-password" > Current Password</ label >
513+ < Input
514+ id = "old-password"
515+ type = "password"
516+ value = { oldPassword }
517+ onChange = { ( e ) => setOldPassword ( e . target . value ) }
518+ placeholder = "Your current password"
519+ autoComplete = "current-password"
520+ />
521+ </ div >
522+ < div className = "space-y-2" >
523+ < label htmlFor = "new-password" > New Password</ label >
524+ < Input
525+ id = "new-password"
526+ type = "password"
527+ value = { newPassword }
528+ onChange = { ( e ) => setNewPassword ( e . target . value ) }
529+ placeholder = "Min. 8 characters"
530+ autoComplete = "new-password"
531+ />
532+ </ div >
533+ < Button type = "submit" disabled = { ! oldPassword || ! newPassword || passwordMutation . isPending } >
534+ { passwordMutation . isPending ? 'Updating...' : 'Update Password' }
535+ </ Button >
536+ </ form >
537+ </ div >
538+ </ CardContent >
539+ </ Card >
540+
541+ < SessionsCard />
542+ < DangerZoneCard />
543+ </ div >
539544 ) }
540545
541546 { /* Create Key Modal (Custom) */ }
@@ -598,3 +603,124 @@ export default function Settings() {
598603 </ div >
599604 ) ;
600605}
606+
607+ // ── Active Sessions ──────────────────────────────────────────────────────────
608+
609+ function SessionsCard ( ) {
610+ const queryClient = useQueryClient ( ) ;
611+ const { data : sessions = [ ] , isLoading } = useQuery ( {
612+ queryKey : [ 'auth-sessions' ] ,
613+ queryFn : authService . getSessions ,
614+ } ) ;
615+
616+ const revokeMutation = useMutation ( {
617+ mutationFn : authService . revokeSession ,
618+ onSuccess : ( ) => queryClient . invalidateQueries ( { queryKey : [ 'auth-sessions' ] } ) ,
619+ onError : ( err : any ) =>
620+ alert ( err ?. response ?. data ?. error || 'Failed to revoke session' ) ,
621+ } ) ;
622+
623+ return (
624+ < Card >
625+ < CardHeader >
626+ < CardTitle > Active Sessions</ CardTitle >
627+ < CardDescription >
628+ Devices and browsers signed into your account. Revoke any you don't recognize.
629+ </ CardDescription >
630+ </ CardHeader >
631+ < CardContent >
632+ { isLoading ? (
633+ < p className = "text-sm text-muted-foreground" > Loading…</ p >
634+ ) : sessions . length === 0 ? (
635+ < p className = "text-sm text-muted-foreground" > No active sessions.</ p >
636+ ) : (
637+ < Table >
638+ < TableHeader >
639+ < TableRow >
640+ < TableHead > Device</ TableHead >
641+ < TableHead > IP</ TableHead >
642+ < TableHead > Created</ TableHead >
643+ < TableHead > Expires</ TableHead >
644+ < TableHead className = "text-right" > Action</ TableHead >
645+ </ TableRow >
646+ </ TableHeader >
647+ < TableBody >
648+ { sessions . map ( ( s ) => (
649+ < TableRow key = { s . jti } >
650+ < TableCell className = "max-w-xs truncate" title = { s . user_agent } >
651+ { s . user_agent || '—' }
652+ </ TableCell >
653+ < TableCell > { s . ip || '—' } </ TableCell >
654+ < TableCell >
655+ { s . created_at ? format ( new Date ( s . created_at ) , 'PP p' ) : '—' }
656+ </ TableCell >
657+ < TableCell >
658+ { s . expires_at ? format ( new Date ( s . expires_at ) , 'PP p' ) : '—' }
659+ </ TableCell >
660+ < TableCell className = "text-right" >
661+ < Button
662+ size = "sm"
663+ variant = "ghost"
664+ onClick = { ( ) => {
665+ if ( window . confirm ( 'Revoke this session?' ) ) {
666+ revokeMutation . mutate ( s . jti ) ;
667+ }
668+ } }
669+ disabled = { revokeMutation . isPending }
670+ >
671+ < Trash2 className = "h-4 w-4" />
672+ </ Button >
673+ </ TableCell >
674+ </ TableRow >
675+ ) ) }
676+ </ TableBody >
677+ </ Table >
678+ ) }
679+ </ CardContent >
680+ </ Card >
681+ ) ;
682+ }
683+
684+ // ── Danger Zone (delete account) ─────────────────────────────────────────────
685+
686+ function DangerZoneCard ( ) {
687+ const { logout } = useAuth ( ) ;
688+ const deleteMutation = useMutation ( {
689+ mutationFn : authService . deleteAccount ,
690+ onSuccess : ( ) => {
691+ alert ( 'Your account has been deleted. Goodbye.' ) ;
692+ logout ( ) ;
693+ } ,
694+ onError : ( err : any ) =>
695+ alert ( err ?. response ?. data ?. error || 'Failed to delete account' ) ,
696+ } ) ;
697+
698+ const handleDelete = ( ) => {
699+ const confirm1 = window . prompt (
700+ 'This permanently deletes your account, sessions, and personal data.\n\nType DELETE to confirm:' ,
701+ ) ;
702+ if ( confirm1 !== 'DELETE' ) return ;
703+ if ( ! window . confirm ( 'Last chance — delete account permanently?' ) ) return ;
704+ deleteMutation . mutate ( ) ;
705+ } ;
706+
707+ return (
708+ < Card className = "border-destructive/50" >
709+ < CardHeader >
710+ < CardTitle className = "text-destructive" > Danger Zone</ CardTitle >
711+ < CardDescription >
712+ Permanently delete your account and all associated data. This cannot be undone.
713+ </ CardDescription >
714+ </ CardHeader >
715+ < CardContent >
716+ < Button
717+ variant = "destructive"
718+ onClick = { handleDelete }
719+ disabled = { deleteMutation . isPending }
720+ >
721+ { deleteMutation . isPending ? 'Deleting…' : 'Delete My Account' }
722+ </ Button >
723+ </ CardContent >
724+ </ Card >
725+ ) ;
726+ }
0 commit comments