systemprompt_cloud/
credentials.rs1use std::fs;
4use std::path::Path;
5
6use chrono::{DateTime, Duration, Utc};
7use serde::{Deserialize, Serialize};
8use systemprompt_identifiers::CloudAuthToken;
9use systemprompt_logging::CliService;
10use systemprompt_models::net::{HTTP_AUTH_VERIFY_TIMEOUT, HTTP_CONNECT_TIMEOUT};
11use validator::Validate;
12
13use crate::auth;
14use crate::error::{CloudError, CloudResult};
15
16#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
17pub struct CloudCredentials {
18 #[validate(length(min = 1, message = "API token cannot be empty"))]
19 pub api_token: String,
20
21 #[validate(url(message = "API URL must be a valid URL"))]
22 pub api_url: String,
23
24 pub authenticated_at: DateTime<Utc>,
25
26 #[validate(email(message = "User email must be a valid email address"))]
27 pub user_email: String,
28}
29
30impl CloudCredentials {
31 #[must_use]
32 pub fn new(api_token: String, api_url: String, user_email: String) -> Self {
33 Self {
34 api_token,
35 api_url,
36 authenticated_at: Utc::now(),
37 user_email,
38 }
39 }
40
41 #[must_use]
42 pub fn token(&self) -> CloudAuthToken {
43 CloudAuthToken::new(&self.api_token)
44 }
45
46 #[must_use]
47 pub fn is_token_expired(&self) -> bool {
48 auth::is_expired(&self.token())
49 }
50
51 #[must_use]
52 pub fn expires_within(&self, duration: Duration) -> bool {
53 auth::expires_within(&self.token(), duration)
54 }
55
56 pub fn load_and_validate_from_path(path: &Path) -> CloudResult<Self> {
57 let creds = Self::load_from_path(path)?;
58
59 creds
60 .validate()
61 .map_err(|e| CloudError::CredentialsCorrupted {
62 source: serde_json::Error::io(std::io::Error::new(
63 std::io::ErrorKind::InvalidData,
64 e.to_string(),
65 )),
66 })?;
67
68 if creds.is_token_expired() {
69 return Err(CloudError::TokenExpired);
70 }
71
72 if creds.expires_within(Duration::hours(1)) {
73 CliService::warning(
74 "Cloud token will expire soon. Consider running 'systemprompt cloud login' to \
75 refresh.",
76 );
77 }
78
79 Ok(creds)
80 }
81
82 pub async fn validate_with_api(&self) -> CloudResult<bool> {
83 let client = reqwest::Client::builder()
84 .connect_timeout(HTTP_CONNECT_TIMEOUT)
85 .timeout(HTTP_AUTH_VERIFY_TIMEOUT)
86 .build()?;
87
88 let response = client
89 .get(format!("{}/api/v1/auth/me", self.api_url))
90 .header("Authorization", format!("Bearer {}", self.api_token))
91 .send()
92 .await?;
93
94 Ok(response.status().is_success())
95 }
96
97 pub fn load_from_path(path: &Path) -> CloudResult<Self> {
98 if !path.exists() {
99 return Err(CloudError::NotAuthenticated);
100 }
101
102 let content = fs::read_to_string(path)?;
103
104 let creds: Self = serde_json::from_str(&content)
105 .map_err(|e| CloudError::CredentialsCorrupted { source: e })?;
106
107 creds
108 .validate()
109 .map_err(|e| CloudError::CredentialsCorrupted {
110 source: serde_json::Error::io(std::io::Error::new(
111 std::io::ErrorKind::InvalidData,
112 e.to_string(),
113 )),
114 })?;
115
116 Ok(creds)
117 }
118
119 pub fn save_to_path(&self, path: &Path) -> CloudResult<()> {
120 self.validate()
121 .map_err(|e| CloudError::CredentialsCorrupted {
122 source: serde_json::Error::io(std::io::Error::new(
123 std::io::ErrorKind::InvalidData,
124 e.to_string(),
125 )),
126 })?;
127
128 if let Some(dir) = path.parent() {
129 fs::create_dir_all(dir)?;
130
131 let gitignore_path = dir.join(".gitignore");
132 if !gitignore_path.exists() {
133 fs::write(&gitignore_path, "*\n")?;
134 }
135 }
136
137 let content = serde_json::to_string_pretty(self)?;
138 fs::write(path, content)?;
139
140 #[cfg(unix)]
141 {
142 use std::os::unix::fs::PermissionsExt;
143 let mut perms = fs::metadata(path)?.permissions();
144 perms.set_mode(0o600);
145 fs::set_permissions(path, perms)?;
146 }
147
148 Ok(())
149 }
150
151 pub fn delete_from_path(path: &Path) -> CloudResult<()> {
152 if path.exists() {
153 fs::remove_file(path)?;
154 }
155 Ok(())
156 }
157}