1
+ import { useState } from "react" ;
2
+ import { Flex , Text , Button , Skeleton , toast } from "@raystack/apsara/v1" ;
3
+ import { useUser } from "../../user-context" ;
4
+ import { RevokeSessionConfirm } from "./revoke-session-confirm" ;
5
+ import { useQuery , useMutation , createConnectQueryKey , useTransport } from "@connectrpc/connect-query" ;
6
+ import { useQueryClient } from "@tanstack/react-query" ;
7
+ import { AdminServiceQueries , Session } from "@raystack/proton/frontier" ;
8
+ import dayjs from "dayjs" ;
9
+ import relativeTime from "dayjs/plugin/relativeTime" ;
10
+ import { timestampToDate } from "~/utils/connect-timestamp" ;
11
+ import styles from "./sessions.module.css" ;
12
+
13
+ dayjs . extend ( relativeTime ) ;
14
+
15
+ export const formatDeviceDisplay = ( browser ?: string , operatingSystem ?: string ) : string => {
16
+ const browserName = browser || "Unknown" ;
17
+ const osName = operatingSystem || "Unknown" ;
18
+ return browserName === "Unknown" && osName === "Unknown" ? "Unknown browser and OS" : `${ browserName } on ${ osName } ` ;
19
+ } ;
20
+
21
+ export const UserSessions = ( ) => {
22
+ const { user } = useUser ( ) ;
23
+ const queryClient = useQueryClient ( ) ;
24
+ const transport = useTransport ( ) ;
25
+ const [ isRevokeDialogOpen , setIsRevokeDialogOpen ] = useState ( false ) ;
26
+ const [ selectedSession , setSelectedSession ] = useState < {
27
+ browser : string ;
28
+ operatingSystem : string ;
29
+ ipAddress : string ;
30
+ location : string ;
31
+ lastActive : string ;
32
+ sessionId : string ;
33
+ } | null > ( null ) ;
34
+
35
+ const {
36
+ data : sessionsData ,
37
+ isLoading,
38
+ error
39
+ } = useQuery (
40
+ AdminServiceQueries . listUserSessions ,
41
+ { userId : user ?. id || "" } ,
42
+ {
43
+ enabled : ! ! user ?. id ,
44
+ }
45
+ ) ;
46
+
47
+ const {
48
+ mutate : revokeUserSession ,
49
+ isPending : isRevokingSession ,
50
+ } = useMutation ( AdminServiceQueries . revokeUserSession , {
51
+ onSuccess : ( ) => {
52
+ queryClient . invalidateQueries ( {
53
+ queryKey : createConnectQueryKey ( {
54
+ schema : AdminServiceQueries . listUserSessions ,
55
+ transport,
56
+ input : { userId : user ?. id || "" } ,
57
+ cardinality : "finite" ,
58
+ } ) ,
59
+ } ) ;
60
+ toast . success ( 'Session revoked successfully' ) ;
61
+ } ,
62
+ onError : ( error : any ) => {
63
+ toast . error ( 'Failed to revoke session' , {
64
+ description : error . message || 'Something went wrong'
65
+ } ) ;
66
+ } ,
67
+ } ) ;
68
+
69
+ const handleRevoke = ( session : Session ) => {
70
+ setSelectedSession ( {
71
+ browser : session . metadata ?. browser || "Unknown" ,
72
+ operatingSystem : session . metadata ?. operatingSystem || "Unknown" ,
73
+ ipAddress : session . metadata ?. ipAddress || "Unknown" ,
74
+ location : session . metadata ?. location || "Unknown" ,
75
+ lastActive : formatLastActive ( session . updatedAt ) ,
76
+ sessionId : session . id || ""
77
+ } ) ;
78
+ setIsRevokeDialogOpen ( true ) ;
79
+ } ;
80
+
81
+ const handleRevokeConfirm = ( ) => {
82
+ if ( selectedSession ?. sessionId ) {
83
+ revokeUserSession ( { sessionId : selectedSession . sessionId } ) ;
84
+ }
85
+ } ;
86
+
87
+
88
+ const formatLastActive = ( updatedAt ?: any ) => {
89
+ if ( ! updatedAt ) return "Unknown" ;
90
+
91
+ const date = timestampToDate ( updatedAt ) ;
92
+ if ( ! date ) return "Unknown" ;
93
+
94
+ return dayjs ( date ) . fromNow ( ) ;
95
+ } ;
96
+
97
+ const renderSessionsHeader = ( ) => (
98
+ < Flex direction = "column" gap = { 3 } >
99
+ < Text size = 'large' weight = 'medium' > Sessions</ Text >
100
+ < Text size = 'regular' variant = "secondary" >
101
+ Devices logged into this account.
102
+ </ Text >
103
+ </ Flex >
104
+ ) ;
105
+
106
+ if ( isLoading ) {
107
+ return (
108
+ < Flex direction = "column" gap = { 9 } >
109
+ { renderSessionsHeader ( ) }
110
+ < Flex direction = "column" className = { styles . sessionsContainer } >
111
+ < Skeleton
112
+ height = "32px"
113
+ containerStyle = { { padding : '1rem 0' } }
114
+ count = { 3 }
115
+ />
116
+ </ Flex >
117
+ </ Flex >
118
+ ) ;
119
+ }
120
+
121
+ if ( error ) {
122
+ return (
123
+ < Flex direction = "column" gap = { 9 } >
124
+ { renderSessionsHeader ( ) }
125
+ < Flex justify = "center" align = "center" style = { { padding : "2rem" } } >
126
+ < Text color = "danger" > Failed to load sessions</ Text >
127
+ </ Flex >
128
+ </ Flex >
129
+ ) ;
130
+ }
131
+
132
+ const sessions = sessionsData ?. sessions || [ ] ;
133
+
134
+ return (
135
+ < Flex direction = "column" gap = { 9 } >
136
+ { renderSessionsHeader ( ) }
137
+
138
+ < Flex direction = "column" className = { styles . sessionsContainer } >
139
+ { sessions . length === 0 ? (
140
+ < Flex justify = "center" align = "center" style = { { padding : "2rem" } } >
141
+ < Text variant = "secondary" > No active sessions found</ Text >
142
+ </ Flex >
143
+ ) : (
144
+ sessions . map ( ( session , index ) => (
145
+ < Flex key = { session . id } justify = "between" align = "center" className = { styles . sessionItem } >
146
+ < Flex direction = "column" gap = { 3 } >
147
+ < Text size = "regular" >
148
+ { formatDeviceDisplay ( session . metadata ?. browser , session . metadata ?. operatingSystem ) }
149
+ </ Text >
150
+ < Flex gap = { 2 } align = "center" >
151
+ < Text variant = "tertiary" size = "small" >
152
+ { session . metadata ?. location || "Unknown location" }
153
+ </ Text >
154
+ < Text variant = "tertiary" size = "small" > •</ Text >
155
+ < Text variant = "tertiary" size = "small" >
156
+ Last active { formatLastActive ( session . updatedAt ) }
157
+ </ Text >
158
+ </ Flex >
159
+ </ Flex >
160
+ < Button
161
+ variant = "text"
162
+ color = "neutral"
163
+ data-test-id = { `frontier-ui-revoke-session-${ index + 1 } ` }
164
+ onClick = { ( ) => handleRevoke ( session ) }
165
+ >
166
+ Revoke
167
+ </ Button >
168
+ </ Flex >
169
+ ) )
170
+ ) }
171
+ </ Flex >
172
+
173
+ < RevokeSessionConfirm
174
+ isOpen = { isRevokeDialogOpen }
175
+ onOpenChange = { setIsRevokeDialogOpen }
176
+ sessionInfo = { selectedSession || undefined }
177
+ onRevokeConfirm = { handleRevokeConfirm }
178
+ isLoading = { isRevokingSession }
179
+ />
180
+ </ Flex >
181
+ ) ;
182
+ } ;
0 commit comments