systemprompt_cloud/
credentials.rs1use 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}