diff --git a/compiler-cli/src/hex.rs b/compiler-cli/src/hex.rs index a5ceaa142b6..8164a19d66c 100644 --- a/compiler-cli/src/hex.rs +++ b/compiler-cli/src/hex.rs @@ -3,6 +3,7 @@ mod auth; use crate::{cli, http::HttpClient}; use gleam_core::{ Error, Result, + error::wrap, hex::{self, RetirementReason}, io::HttpClient as _, paths::ProjectPaths, @@ -99,7 +100,7 @@ pub(crate) fn authenticate() -> Result<()> { let previous = auth.read_stored_api_key()?; if previous.is_some() { - let question = "You already have a local Hex API token. Would you like to replace it + let question = "You already have a local Hex API key. Would you like to replace it with a new one?"; if !cli::confirm(question)? { return Ok(()); @@ -109,13 +110,37 @@ with a new one?"; let new_key = auth.create_and_store_api_key()?; if let Some(previous) = previous { + if previous.username != new_key.username { + if let Some(previous_username) = previous.username { + let text = wrap(&format!( + " +Your previous Hex API key was created with username `{}` which is different from the username +used to create the new Hex API key. You have to delete the key `{}` manually at https://hex.pm", + previous_username, previous.name + )); + + println!("{text}"); + return Ok(()); + } + } + println!("Deleting previous key `{}` from Hex", previous.name); - runtime.block_on(hex::remove_api_key( - &previous.name, - &config, - &new_key.unencrypted, - &http, - ))?; + if runtime + .block_on(hex::remove_api_key( + &previous.name, + &config, + &new_key.unencrypted, + &http, + )) + .is_err() + { + let text = wrap(&format!( + "There was an error deleting key `{}` from Hex. You have to delete the key manually at https://hex.pm", + previous.name + )); + + println!("{text}"); + }; } Ok(()) } diff --git a/compiler-cli/src/hex/auth.rs b/compiler-cli/src/hex/auth.rs index 7d03f00a1ec..fe11e1f643a 100644 --- a/compiler-cli/src/hex/auth.rs +++ b/compiler-cli/src/hex/auth.rs @@ -1,8 +1,10 @@ use crate::{cli, fs::ConsoleWarningEmitter, http::HttpClient}; +use camino::Utf8Path; use gleam_core::{ Error, Result, Warning, encryption, hex, paths::global_hexpm_credentials_path, warning::WarningEmitter, }; +use serde::{Deserialize, Serialize}; use std::{rc::Rc, time::SystemTime}; pub const USER_PROMPT: &str = "https://hex.pm username"; @@ -12,15 +14,17 @@ pub const LOCAL_PASS_PROMPT: &str = "Local password"; pub const PASS_ENV_NAME: &str = "HEXPM_PASS"; pub const API_ENV_NAME: &str = "HEXPM_API_KEY"; -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] pub struct EncryptedApiKey { pub name: String, pub encrypted: String, + pub username: Option, } #[derive(Debug)] pub struct UnencryptedApiKey { pub unencrypted: String, + pub username: Option, } pub struct HexAuthentication<'runtime> { @@ -74,11 +78,18 @@ encrypt your Hex API key. detail: e.to_string(), })?; - crate::fs::write(&path, &format!("{name}\n{encrypted}"))?; + let encrypted = EncryptedApiKey { + name, + encrypted, + username: Some(username.clone()), + }; + + encrypted.save(&path)?; println!("Encrypted Hex API key written to {path}"); Ok(UnencryptedApiKey { unencrypted: api_key, + username: Some(username), }) } @@ -117,7 +128,12 @@ encrypt your Hex API key. } fn read_and_decrypt_stored_api_key(&mut self) -> Result> { - let Some(EncryptedApiKey { encrypted, .. }) = self.read_stored_api_key()? else { + let Some(EncryptedApiKey { + encrypted, + username, + .. + }) = self.read_stored_api_key()? + else { return Ok(None); }; @@ -127,7 +143,10 @@ encrypt your Hex API key. detail: e.to_string(), })?; - Ok(Some(UnencryptedApiKey { unencrypted })) + Ok(Some(UnencryptedApiKey { + unencrypted, + username, + })) } pub fn read_stored_api_key(&self) -> Result> { @@ -135,18 +154,8 @@ encrypt your Hex API key. if !path.exists() { return Ok(None); } - let text = crate::fs::read(&path)?; - let mut chunks = text.splitn(2, '\n'); - let Some(name) = chunks.next() else { - return Ok(None); - }; - let Some(encrypted) = chunks.next() else { - return Ok(None); - }; - Ok(Some(EncryptedApiKey { - name: name.to_string(), - encrypted: encrypted.to_string(), - })) + + EncryptedApiKey::load(&path) } } @@ -158,6 +167,49 @@ impl Drop for HexAuthentication<'_> { } } +impl EncryptedApiKey { + pub fn save(&self, path: &Utf8Path) -> Result<()> { + let text = toml::to_string(self).map_err(|_| Error::InvalidCredentialsFile { + path: path.to_string(), + })?; + + crate::fs::write(path, &text) + } + + pub fn load(path: &Utf8Path) -> Result> { + let text = crate::fs::read(path)?; + + toml::from_str(&text).or_else(|_| { + // fallback from old format + let mut chunks = text.splitn(2, '\n'); + + let Some(name) = chunks.next() else { + return Err(Error::InvalidCredentialsFile { + path: path.to_string(), + }); + }; + + let Some(encrypted) = chunks.next() else { + return Err(Error::InvalidCredentialsFile { + path: path.to_string(), + }); + }; + + let key = Self { + name: name.to_string(), + encrypted: encrypted.to_string(), + username: None, + }; + + // try to save the file in the new format, but let if fail silently, + // we do not want the load operation to fail because of a write. + let _ = key.save(path); + + Ok(Some(key)) + }) + } +} + fn ask_local_password(warnings: &mut Vec) -> std::result::Result { std::env::var(PASS_ENV_NAME) .inspect(|_| { diff --git a/compiler-core/src/error.rs b/compiler-core/src/error.rs index eceaa0c3c21..b3647c283ae 100644 --- a/compiler-core/src/error.rs +++ b/compiler-core/src/error.rs @@ -321,6 +321,9 @@ file_names.iter().map(|x| x.as_str()).join(", "))] #[error("Failed to decrypt local Hex API key")] FailedToDecryptLocalHexApiKey { detail: String }, + + #[error("Invalid Credentials file")] + InvalidCredentialsFile { path: String }, } /// This is to make clippy happy and not make the error variant too big by @@ -1487,6 +1490,18 @@ The error from the encryption library was: }] } + Error::InvalidCredentialsFile {path}=> { + let text = wrap_format!("Your credentials file at {path} is in the wrong format. Try deleting the file and authenticate again."); + + vec![Diagnostic { + title: "Invalid credentials file".into(), + text, + level: Level::Error, + location: None, + hint: None + }] + } + Error::NonUtf8Path { path } => { let text = format!( "Encountered a non UTF-8 path '{}', but only UTF-8 paths are supported.",