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, host: Option<&str>) -> String {
18        self.host
19            .clone()
20            .unwrap_or_else(|| host.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 {:?}",
38            file
39        ))?;
40        let token = serde_json::from_str(&token).context("While trying to parse token")?;
41        Ok(token)
42    }
43
44    fn store_credentials(&self, token: Token) -> Result<(), Error> {
45        let home = posthog_home_dir();
46        ensure_homedir_exists()?;
47        let file = home.join("credentials.json");
48        let token = serde_json::to_string(&token).context("While trying to serialize token")?;
49        std::fs::write(file.clone(), token).context(format!(
50            "While trying to write credentials to file {:?}",
51            file
52        ))?;
53        Ok(())
54    }
55
56    fn report_location(&self) -> String {
57        posthog_home_dir()
58            .join("credentials.json")
59            .to_string_lossy()
60            .to_string()
61    }
62}
63
64/// Tries to read the token from the env var `POSTHOG_CLI_TOKEN`
65pub struct EnvVarProvider;
66
67impl CredentialProvider for EnvVarProvider {
68    fn get_credentials(&self) -> Result<Token, Error> {
69        let host = std::env::var("POSTHOG_CLI_HOST").ok();
70        let token = std::env::var("POSTHOG_CLI_TOKEN").context("While trying to read env var")?;
71        let env_id = std::env::var("POSTHOG_CLI_ENV_ID").context("While trying to read env var")?;
72        Ok(Token {
73            host,
74            token,
75            env_id,
76        })
77    }
78
79    fn store_credentials(&self, _token: Token) -> Result<(), Error> {
80        Ok(())
81    }
82
83    fn report_location(&self) -> String {
84        unimplemented!("We should never try to save a credential to the env");
85    }
86}
87
88pub fn host_validator(host: &str) -> Result<Validation, CustomUserError> {
89    if host.is_empty() || Url::parse(host).is_err() {
90        return Ok(Validation::Invalid("Host must be a valid URL".into()));
91    }
92
93    Ok(Validation::Valid)
94}
95
96pub fn token_validator(token: &str) -> Result<Validation, CustomUserError> {
97    if token.is_empty() {
98        return Ok(Validation::Invalid("Token cannot be empty".into()));
99    };
100
101    if !token.starts_with("phx_") {
102        return Ok(Validation::Invalid(
103            "Token looks wrong, must start with 'phx_'".into(),
104        ));
105    }
106
107    Ok(Validation::Valid)
108}
109
110pub fn load_token() -> Result<Token, Error> {
111    let env = EnvVarProvider;
112    let env_err = match env.get_credentials() {
113        Ok(token) => {
114            info!("Using token from env var, for environment {}", token.env_id);
115            return Ok(token);
116        }
117        Err(e) => e,
118    };
119    let provider = HomeDirProvider;
120    let dir_err = match provider.get_credentials() {
121        Ok(token) => {
122            info!(
123                "Using token from: {}, for environment {}",
124                provider.report_location(),
125                token.env_id
126            );
127            return Ok(token);
128        }
129        Err(e) => e,
130    };
131
132    Err(
133        anyhow::anyhow!("Couldn't load credentials... Have you logged in recently?")
134            .context(env_err)
135            .context(dir_err),
136    )
137}