mecha10_cli/types/
credentials.rs

1//! Credentials type definitions for CLI authentication
2//!
3//! Types for storing and managing user credentials and device code flow.
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8/// Stored credentials for authenticated user
9///
10/// Saved to ~/.mecha10/credentials.json after successful login.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Credentials {
13    /// User's API key for authenticating with control plane
14    pub api_key: String,
15
16    /// User ID from the auth system
17    pub user_id: String,
18
19    /// User's email address
20    pub email: String,
21
22    /// User's display name
23    #[serde(default)]
24    pub name: Option<String>,
25
26    /// When the credentials were obtained
27    pub authenticated_at: DateTime<Utc>,
28
29    /// The auth server URL used for authentication
30    pub auth_url: String,
31}
32
33impl Credentials {
34    /// Check if credentials appear valid (non-empty api_key)
35    pub fn is_valid(&self) -> bool {
36        !self.api_key.is_empty() && self.api_key.starts_with("mecha_")
37    }
38
39    /// Get a masked version of the API key for display
40    #[allow(dead_code)]
41    pub fn masked_api_key(&self) -> String {
42        if self.api_key.len() > 12 {
43            format!("{}...{}", &self.api_key[..12], &self.api_key[self.api_key.len() - 4..])
44        } else {
45            "***".to_string()
46        }
47    }
48}
49
50/// Response from device code request
51///
52/// Returned by POST /auth/device/code
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct DeviceCodeResponse {
55    /// The device code for backend polling
56    pub device_code: String,
57
58    /// User-facing code to enter in browser
59    pub user_code: String,
60
61    /// URL for user to visit
62    pub verification_uri: String,
63
64    /// Seconds until the code expires
65    pub expires_in: u32,
66
67    /// Seconds to wait between poll attempts
68    pub interval: u32,
69}
70
71/// Status of a device code authorization
72#[derive(Debug, Clone, Serialize, Deserialize)]
73#[serde(tag = "status", rename_all = "snake_case")]
74pub enum DeviceCodeStatus {
75    /// User hasn't completed authorization yet
76    Pending,
77
78    /// User authorized - includes credentials
79    Authorized {
80        api_key: String,
81        user_id: String,
82        email: String,
83        name: Option<String>,
84    },
85
86    /// User denied the authorization
87    Denied,
88
89    /// Device code has expired
90    Expired,
91}
92
93impl DeviceCodeStatus {
94    /// Check if this is a terminal state (no more polling needed)
95    #[allow(dead_code)]
96    pub fn is_terminal(&self) -> bool {
97        !matches!(self, DeviceCodeStatus::Pending)
98    }
99
100    /// Check if authorization was successful
101    #[allow(dead_code)]
102    pub fn is_authorized(&self) -> bool {
103        matches!(self, DeviceCodeStatus::Authorized { .. })
104    }
105}
106
107/// Error types for authentication operations
108#[derive(Debug, Clone, Serialize, Deserialize)]
109#[serde(tag = "error", rename_all = "snake_case")]
110pub enum AuthError {
111    /// Network or connection error
112    NetworkError { message: String },
113
114    /// Server returned an error
115    ServerError { message: String, status_code: Option<u16> },
116
117    /// Device code expired before user completed auth
118    ExpiredCode,
119
120    /// User denied the authorization request
121    AccessDenied,
122
123    /// Credentials file corrupted or invalid
124    InvalidCredentials { message: String },
125
126    /// Rate limited by server
127    RateLimited { retry_after: Option<u32> },
128}
129
130impl std::fmt::Display for AuthError {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        match self {
133            AuthError::NetworkError { message } => write!(f, "Network error: {}", message),
134            AuthError::ServerError { message, status_code } => {
135                if let Some(code) = status_code {
136                    write!(f, "Server error ({}): {}", code, message)
137                } else {
138                    write!(f, "Server error: {}", message)
139                }
140            }
141            AuthError::ExpiredCode => write!(f, "Device code expired"),
142            AuthError::AccessDenied => write!(f, "Access denied by user"),
143            AuthError::InvalidCredentials { message } => write!(f, "Invalid credentials: {}", message),
144            AuthError::RateLimited { retry_after } => {
145                if let Some(secs) = retry_after {
146                    write!(f, "Rate limited, retry after {} seconds", secs)
147                } else {
148                    write!(f, "Rate limited")
149                }
150            }
151        }
152    }
153}
154
155impl std::error::Error for AuthError {}