1+ use anyhow:: Result ;
2+ use reqwest:: { Client , StatusCode } ;
3+ use colored:: * ;
4+ use chrono:: { DateTime , Utc } ;
5+
6+ use super :: models:: { CliSessionResponse , RevokeSessionRequest , ErrorResponse } ;
7+
8+ pub struct SessionsClient {
9+ client : Client ,
10+ base_url : String ,
11+ }
12+
13+ impl SessionsClient {
14+ pub fn new ( base_url : String ) -> Self {
15+ let client = Client :: builder ( )
16+ . timeout ( std:: time:: Duration :: from_secs ( 30 ) )
17+ . build ( )
18+ . expect ( "Failed to create HTTP client" ) ;
19+
20+ Self { client, base_url }
21+ }
22+
23+ /// List all CLI sessions
24+ pub async fn list_sessions ( & self , access_token : & str ) -> Result < Vec < CliSessionResponse > > {
25+ let url = format ! ( "{}/auth/cli/sessions" , self . base_url) ;
26+
27+ let response = self
28+ . client
29+ . get ( & url)
30+ . header ( "Authorization" , format ! ( "Bearer {}" , access_token) )
31+ . send ( )
32+ . await ?;
33+
34+ match response. status ( ) {
35+ StatusCode :: OK => {
36+ let sessions = response
37+ . json :: < Vec < CliSessionResponse > > ( )
38+ . await ?;
39+ Ok ( sessions)
40+ }
41+ StatusCode :: UNAUTHORIZED => {
42+ anyhow:: bail!( "Authentication token expired. Please login again." )
43+ }
44+ status => {
45+ if let Ok ( error) = response. json :: < ErrorResponse > ( ) . await {
46+ anyhow:: bail!( "{}: {}" , status, error. message)
47+ } else {
48+ anyhow:: bail!( "Failed to list sessions with status: {}" , status)
49+ }
50+ }
51+ }
52+ }
53+
54+ /// Revoke a specific CLI session
55+ pub async fn revoke_session ( & self , access_token : & str , session_id : & str ) -> Result < ( ) > {
56+ let url = format ! ( "{}/auth/cli/sessions/{}" , self . base_url, session_id) ;
57+
58+ let request = RevokeSessionRequest {
59+ reason : Some ( "User revoked from CLI" . to_string ( ) ) ,
60+ } ;
61+
62+ let response = self
63+ . client
64+ . delete ( & url)
65+ . header ( "Authorization" , format ! ( "Bearer {}" , access_token) )
66+ . json ( & request)
67+ . send ( )
68+ . await ?;
69+
70+ match response. status ( ) {
71+ StatusCode :: OK => Ok ( ( ) ) ,
72+ StatusCode :: NOT_FOUND => {
73+ anyhow:: bail!( "Session not found or already revoked" )
74+ }
75+ StatusCode :: UNAUTHORIZED => {
76+ anyhow:: bail!( "Authentication token expired. Please login again." )
77+ }
78+ status => {
79+ if let Ok ( error) = response. json :: < ErrorResponse > ( ) . await {
80+ anyhow:: bail!( "{}: {}" , status, error. message)
81+ } else {
82+ anyhow:: bail!( "Failed to revoke session with status: {}" , status)
83+ }
84+ }
85+ }
86+ }
87+
88+ /// Revoke all CLI sessions
89+ pub async fn revoke_all_sessions ( & self , access_token : & str ) -> Result < ( ) > {
90+ let url = format ! ( "{}/auth/cli/sessions/revoke-all" , self . base_url) ;
91+
92+ let response = self
93+ . client
94+ . post ( & url)
95+ . header ( "Authorization" , format ! ( "Bearer {}" , access_token) )
96+ . send ( )
97+ . await ?;
98+
99+ match response. status ( ) {
100+ StatusCode :: OK => Ok ( ( ) ) ,
101+ StatusCode :: UNAUTHORIZED => {
102+ anyhow:: bail!( "Authentication token expired. Please login again." )
103+ }
104+ status => {
105+ if let Ok ( error) = response. json :: < ErrorResponse > ( ) . await {
106+ anyhow:: bail!( "{}: {}" , status, error. message)
107+ } else {
108+ anyhow:: bail!( "Failed to revoke all sessions with status: {}" , status)
109+ }
110+ }
111+ }
112+ }
113+ }
114+
115+ // Helper functions for formatting sessions
116+
117+ pub fn format_session_display ( sessions : & [ CliSessionResponse ] ) {
118+ if sessions. is_empty ( ) {
119+ println ! ( "{} No active CLI sessions found" , "ℹ" . blue( ) ) ;
120+ return ;
121+ }
122+
123+ println ! ( "\n {} Active CLI Sessions\n " , "🔐" . green( ) ) ;
124+
125+ // Header
126+ println ! (
127+ "{:<12} {:<20} {:<15} {:<20} {:<15} {}" ,
128+ "ID" . bold( ) ,
129+ "Device" . bold( ) ,
130+ "Platform" . bold( ) ,
131+ "Hostname" . bold( ) ,
132+ "Last Active" . bold( ) ,
133+ "Status" . bold( )
134+ ) ;
135+
136+ println ! ( "{}" , "─" . repeat( 100 ) ) ;
137+
138+ for session in sessions {
139+ let id_short = & session. id [ ..8 . min ( session. id . len ( ) ) ] ;
140+ let device_name = session. device_name . as_deref ( ) . unwrap_or ( "Unknown" ) ;
141+ let platform = format_platform ( session. platform . as_deref ( ) ) ;
142+ let hostname = session. hostname . as_deref ( ) . unwrap_or ( "-" ) ;
143+ let last_active = format_relative_time ( & session. last_used_at ) ;
144+
145+ let status = if session. is_current {
146+ "CURRENT" . green ( ) . bold ( )
147+ } else if session. is_active {
148+ "Active" . green ( )
149+ } else {
150+ "Inactive" . red ( )
151+ } ;
152+
153+ println ! (
154+ "{:<12} {:<20} {:<15} {:<20} {:<15} {}" ,
155+ id_short,
156+ truncate_string( device_name, 20 ) ,
157+ platform,
158+ truncate_string( hostname, 20 ) ,
159+ last_active,
160+ status
161+ ) ;
162+ }
163+
164+ println ! ( "\n {} Total: {} active session(s)" , "📊" . blue( ) , sessions. len( ) ) ;
165+ }
166+
167+ fn format_platform ( platform : Option < & str > ) -> String {
168+ match platform {
169+ Some ( p) if p. to_lowercase ( ) . contains ( "darwin" ) => "macOS" ,
170+ Some ( p) if p. to_lowercase ( ) . contains ( "linux" ) => "Linux" ,
171+ Some ( p) if p. to_lowercase ( ) . contains ( "win" ) => "Windows" ,
172+ Some ( p) if p == "cli" => "CLI" ,
173+ Some ( p) => p,
174+ None => "Unknown" ,
175+ } . to_string ( )
176+ }
177+
178+ fn truncate_string ( s : & str , max_len : usize ) -> String {
179+ if s. len ( ) > max_len {
180+ format ! ( "{}..." , & s[ ..max_len - 3 ] )
181+ } else {
182+ s. to_string ( )
183+ }
184+ }
185+
186+ fn format_relative_time ( dt : & DateTime < Utc > ) -> String {
187+ let now = Utc :: now ( ) ;
188+ let duration = now. signed_duration_since ( * dt) ;
189+
190+ if duration. num_days ( ) > 30 {
191+ format ! ( "{} months ago" , duration. num_days( ) / 30 )
192+ } else if duration. num_days ( ) > 0 {
193+ format ! ( "{} days ago" , duration. num_days( ) )
194+ } else if duration. num_hours ( ) > 0 {
195+ format ! ( "{} hours ago" , duration. num_hours( ) )
196+ } else if duration. num_minutes ( ) > 0 {
197+ format ! ( "{} mins ago" , duration. num_minutes( ) )
198+ } else {
199+ "Just now" . to_string ( )
200+ }
201+ }
0 commit comments