Skip to main content

systemprompt_cloud/
credentials.rs

1//! On-disk representation of authenticated cloud credentials.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use std::fs;
7use std::path::Path;
8
9use chrono::{DateTime, Duration, Utc};
10use serde::{Deserialize, Serialize};
11use systemprompt_identifiers::{CloudAuthToken, Email};
12use systemprompt_logging::CliService;
13use systemprompt_models::net::{HTTP_AUTH_VERIFY_TIMEOUT, HTTP_CONNECT_TIMEOUT};
14use validator::Validate;
15
16use crate::auth;
17use crate::error::{CloudError, CloudResult};
18use crate::private_dir::write_private_json;
19
20#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
21pub struct CloudCredentials {
22    pub api_token: CloudAuthToken,
23
24    #[validate(url(message = "API URL must be a valid URL"))]
25    pub api_url: String,
26
27    pub authenticated_at: DateTime<Utc>,
28
29    pub user_email: Email,
30
31    #[serde(default)]
32    pub last_validated_at: Option<DateTime<Utc>>,
33}
34
35impl CloudCredentials {
36    #[must_use]
37    pub fn new(api_token: CloudAuthToken, api_url: String, user_email: Email) -> Self {
38        let now = Utc::now();
39        Self {
40            api_token,
41            api_url,
42            authenticated_at: now,
43            user_email,
44            last_validated_at: Some(now),
45        }
46    }
47
48    #[must_use]
49    pub const fn token(&self) -> &CloudAuthToken {
50        &self.api_token
51    }
52
53    #[must_use]
54    pub fn is_token_expired(&self) -> bool {
55        auth::is_expired(self.token())
56    }
57
58    #[must_use]
59    pub fn expires_within(&self, duration: Duration) -> bool {
60        auth::expires_within(self.token(), duration)
61    }
62
63    pub fn load_and_validate_from_path(path: &Path) -> CloudResult<Self> {
64        let creds = Self::load_from_path(path)?;
65
66        creds
67            .validate()
68            .map_err(|e| CloudError::CredentialsCorrupted {
69                source: Box::new(e),
70            })?;
71
72        if creds.is_token_expired() {
73            return Err(CloudError::TokenExpired);
74        }
75
76        if creds.expires_within(Duration::hours(1)) {
77            CliService::warning(
78                "Cloud token will expire soon. Consider running 'systemprompt cloud auth login' to \
79                 refresh.",
80            );
81        }
82
83        Ok(creds)
84    }
85
86    pub async fn validate_with_api(&self) -> CloudResult<bool> {
87        let client = reqwest::Client::builder()
88            .connect_timeout(HTTP_CONNECT_TIMEOUT)
89            .timeout(HTTP_AUTH_VERIFY_TIMEOUT)
90            .build()?;
91
92        let response = client
93            .get(format!("{}/api/v1/auth/me", self.api_url))
94            .header(
95                "Authorization",
96                format!("Bearer {}", self.api_token.as_str()),
97            )
98            .send()
99            .await?;
100
101        Ok(response.status().is_success())
102    }
103
104    pub fn load_from_path(path: &Path) -> CloudResult<Self> {
105        if !path.exists() {
106            return Err(CloudError::NotAuthenticated);
107        }
108
109        let content = fs::read_to_string(path)?;
110
111        let creds: Self =
112            serde_json::from_str(&content).map_err(|e| CloudError::CredentialsCorrupted {
113                source: Box::new(e),
114            })?;
115
116        creds
117            .validate()
118            .map_err(|e| CloudError::CredentialsCorrupted {
119                source: Box::new(e),
120            })?;
121
122        Ok(creds)
123    }
124
125    pub fn save_to_path(&self, path: &Path) -> CloudResult<()> {
126        self.validate()
127            .map_err(|e| CloudError::CredentialsCorrupted {
128                source: Box::new(e),
129            })?;
130
131        write_private_json(path, self)
132    }
133
134    pub fn delete_from_path(path: &Path) -> CloudResult<()> {
135        if path.exists() {
136            fs::remove_file(path)?;
137        }
138        Ok(())
139    }
140}