posthog_cli/utils/
auth.rs

1use super::homedir::{ensure_homedir_exists, posthog_home_dir};
2use anyhow::{Context, Error};
3use inquire::{validator::Validation, CustomUserError};
4use reqwest::Url;
5use tracing::info;
6
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Serialize, Deserialize, Clone)]
10pub struct Token {
11    pub host: Option<String>,
12    pub token: String,
13    pub env_id: String,
14}
15
16impl Token {
17    pub fn get_host(&self) -> String {
18        self.host
19            .clone()
20            .unwrap_or("https://us.posthog.com".to_string())
21    }
22}
23
24pub trait CredentialProvider {
25    fn get_credentials(&self) -> Result<Token, Error>;
26    fn store_credentials(&self, token: Token) -> Result<(), Error>;
27    fn report_location(&self) -> String;
28}
29
30pub struct HomeDirProvider;
31
32impl CredentialProvider for HomeDirProvider {
33    fn get_credentials(&self) -> Result<Token, Error> {
34        let home = posthog_home_dir();
35        let file = home.join("credentials.json");
36        let token = std::fs::read_to_string(file.clone()).context(format!(
37            "While trying to read credentials from file {file:?}"
38        ))?;
39        let token = serde_json::from_str(&token).context("While trying to parse token")?;
40        Ok(token)
41    }
42
43    fn store_credentials(&self, token: Token) -> Result<(), Error> {
44        let home = posthog_home_dir();
45        ensure_homedir_exists()?;
46        let file = home.join("credentials.json");
47        let token = serde_json::to_string(&token).context("While trying to serialize token")?;
48        std::fs::write(file.clone(), token).context(format!(
49            "While trying to write credentials to file {file:?}",
50        ))?;
51        Ok(())
52    }
53
54    fn report_location(&self) -> String {
55        posthog_home_dir()
56            .join("credentials.json")
57            .to_string_lossy()
58            .to_string()
59    }
60}
61
62/// Tries to read the token from the env var `POSTHOG_CLI_TOKEN`
63pub struct EnvVarProvider;
64
65impl CredentialProvider for EnvVarProvider {
66    fn get_credentials(&self) -> Result<Token, Error> {
67        let host = std::env::var("POSTHOG_CLI_HOST").ok();
68        let token = std::env::var("POSTHOG_CLI_TOKEN").context("While trying to read env var")?;
69        let env_id = std::env::var("POSTHOG_CLI_ENV_ID").context("While trying to read env var")?;
70        Ok(Token {
71            host,
72            token,
73            env_id,
74        })
75    }
76
77    fn store_credentials(&self, _token: Token) -> Result<(), Error> {
78        Ok(())
79    }
80
81    fn report_location(&self) -> String {
82        unimplemented!("We should never try to save a credential to the env");
83    }
84}
85
86pub fn host_validator(host: &str) -> Result<Validation, CustomUserError> {
87    if host.is_empty() || Url::parse(host).is_err() {
88        return Ok(Validation::Invalid("Host must be a valid URL".into()));
89    }
90
91    Ok(Validation::Valid)
92}
93
94pub fn token_validator(token: &str) -> Result<Validation, CustomUserError> {
95    if token.is_empty() {
96        return Ok(Validation::Invalid("Token cannot be empty".into()));
97    };
98
99    if !token.starts_with("phx_") {
100        return Ok(Validation::Invalid(
101            "Token looks wrong, must start with 'phx_'".into(),
102        ));
103    }
104
105    Ok(Validation::Valid)
106}
107
108pub fn get_token() -> Result<Token, Error> {
109    let env = EnvVarProvider;
110    let env_err = match env.get_credentials() {
111        Ok(token) => {
112            info!("Using token from env var, for environment {}", token.env_id);
113            return Ok(token);
114        }
115        Err(e) => e,
116    };
117    let provider = HomeDirProvider;
118    let dir_err = match provider.get_credentials() {
119        Ok(token) => {
120            info!(
121                "Using token from: {}, for environment {}",
122                provider.report_location(),
123                token.env_id
124            );
125            return Ok(token);
126        }
127        Err(e) => e,
128    };
129
130    Err(
131        anyhow::anyhow!("Couldn't load credentials... Have you logged in recently?")
132            .context(env_err)
133            .context(dir_err),
134    )
135}