systemprompt-cloud 0.2.0

systemprompt.io Cloud infrastructure - API client, credentials, OAuth
Documentation
use anyhow::{Context, Result};
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use systemprompt_identifiers::CloudAuthToken;
use systemprompt_logging::CliService;
use validator::Validate;

use crate::auth;
use crate::error::CloudError;

#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
pub struct CloudCredentials {
    #[validate(length(min = 1, message = "API token cannot be empty"))]
    pub api_token: String,

    #[validate(url(message = "API URL must be a valid URL"))]
    pub api_url: String,

    pub authenticated_at: DateTime<Utc>,

    #[validate(email(message = "User email must be a valid email address"))]
    pub user_email: String,
}

impl CloudCredentials {
    #[must_use]
    pub fn new(api_token: String, api_url: String, user_email: String) -> Self {
        Self {
            api_token,
            api_url,
            authenticated_at: Utc::now(),
            user_email,
        }
    }

    pub fn token(&self) -> CloudAuthToken {
        CloudAuthToken::new(&self.api_token)
    }

    #[must_use]
    pub fn is_token_expired(&self) -> bool {
        auth::is_expired(&self.token())
    }

    #[must_use]
    pub fn expires_within(&self, duration: Duration) -> bool {
        auth::expires_within(&self.token(), duration)
    }

    pub fn load_and_validate_from_path(path: &Path) -> Result<Self> {
        let creds = Self::load_from_path(path)?;

        creds
            .validate()
            .map_err(|e| CloudError::CredentialsCorrupted {
                source: serde_json::Error::io(std::io::Error::new(
                    std::io::ErrorKind::InvalidData,
                    e.to_string(),
                )),
            })?;

        if creds.is_token_expired() {
            return Err(CloudError::TokenExpired.into());
        }

        if creds.expires_within(Duration::hours(1)) {
            CliService::warning(
                "Cloud token will expire soon. Consider running 'systemprompt cloud login' to \
                 refresh.",
            );
        }

        Ok(creds)
    }

    pub async fn validate_with_api(&self) -> Result<bool> {
        let client = reqwest::Client::new();

        let response = client
            .get(format!("{}/api/v1/auth/me", self.api_url))
            .header("Authorization", format!("Bearer {}", self.api_token))
            .timeout(std::time::Duration::from_secs(10))
            .send()
            .await?;

        Ok(response.status().is_success())
    }

    pub fn load_from_path(path: &Path) -> Result<Self> {
        if !path.exists() {
            return Err(CloudError::NotAuthenticated.into());
        }

        let content = fs::read_to_string(path)
            .with_context(|| format!("Failed to read {}", path.display()))?;

        let creds: Self = serde_json::from_str(&content)
            .map_err(|e| CloudError::CredentialsCorrupted { source: e })?;

        creds
            .validate()
            .map_err(|e| CloudError::CredentialsCorrupted {
                source: serde_json::Error::io(std::io::Error::new(
                    std::io::ErrorKind::InvalidData,
                    e.to_string(),
                )),
            })?;

        Ok(creds)
    }

    pub fn save_to_path(&self, path: &Path) -> Result<()> {
        self.validate()
            .map_err(|e| CloudError::CredentialsCorrupted {
                source: serde_json::Error::io(std::io::Error::new(
                    std::io::ErrorKind::InvalidData,
                    e.to_string(),
                )),
            })?;

        if let Some(dir) = path.parent() {
            fs::create_dir_all(dir)?;

            let gitignore_path = dir.join(".gitignore");
            if !gitignore_path.exists() {
                fs::write(&gitignore_path, "*\n")?;
            }
        }

        let content = serde_json::to_string_pretty(self)?;
        fs::write(path, content)?;

        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mut perms = fs::metadata(path)?.permissions();
            perms.set_mode(0o600);
            fs::set_permissions(path, perms)?;
        }

        Ok(())
    }

    pub fn delete_from_path(path: &Path) -> Result<()> {
        if path.exists() {
            fs::remove_file(path)?;
        }
        Ok(())
    }
}