posthog_cli/utils/
auth.rs1use 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
62pub 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 env_id_validator(env_id: &str) -> Result<Validation, CustomUserError> {
109 if env_id.is_empty() {
111 return Ok(Validation::Invalid("Environment ID cannot be empty".into()));
112 }
113
114 if env_id.parse::<u32>().is_err() {
116 return Ok(Validation::Invalid(
117 "Environment ID must be a number".into(),
118 ));
119 }
120
121 Ok(Validation::Valid)
122}
123
124pub fn get_token() -> Result<Token, Error> {
125 let env = EnvVarProvider;
126 let env_err = match env.get_credentials() {
127 Ok(token) => {
128 info!("Using token from env var, for environment {}", token.env_id);
129 return Ok(token);
130 }
131 Err(e) => e,
132 };
133 let provider = HomeDirProvider;
134 let dir_err = match provider.get_credentials() {
135 Ok(token) => {
136 info!(
137 "Using token from: {}, for environment {}",
138 provider.report_location(),
139 token.env_id
140 );
141 return Ok(token);
142 }
143 Err(e) => e,
144 };
145
146 Err(
147 anyhow::anyhow!("Couldn't load credentials... Have you logged in recently?")
148 .context(env_err)
149 .context(dir_err),
150 )
151}