Skip to content

Commit b6eb449

Browse files
committed
Store username in credentials file
1 parent a629a6d commit b6eb449

File tree

3 files changed

+114
-23
lines changed

3 files changed

+114
-23
lines changed

compiler-cli/src/hex.rs

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ mod auth;
33
use crate::{cli, http::HttpClient};
44
use gleam_core::{
55
Error, Result,
6+
error::wrap,
67
hex::{self, RetirementReason},
78
io::HttpClient as _,
89
paths::ProjectPaths,
@@ -99,7 +100,7 @@ pub(crate) fn authenticate() -> Result<()> {
99100
let previous = auth.read_stored_api_key()?;
100101

101102
if previous.is_some() {
102-
let question = "You already have a local Hex API token. Would you like to replace it
103+
let question = "You already have a local Hex API key. Would you like to replace it
103104
with a new one?";
104105
if !cli::confirm(question)? {
105106
return Ok(());
@@ -109,13 +110,36 @@ with a new one?";
109110
let new_key = auth.create_and_store_api_key()?;
110111

111112
if let Some(previous) = previous {
113+
if previous.username != new_key.username {
114+
if let Some(previous_username) = previous.username {
115+
let text = wrap(&format!("
116+
Your previous Hex API key was created with username `{}` which is different from the username
117+
used to create the new Hex API key. You have to delete the key `{}` manually at https://hex.pm",
118+
previous_username, previous.name
119+
));
120+
121+
println!("{text}");
122+
return Ok(());
123+
}
124+
}
125+
112126
println!("Deleting previous key `{}` from Hex", previous.name);
113-
runtime.block_on(hex::remove_api_key(
114-
&previous.name,
115-
&config,
116-
&new_key.unencrypted,
117-
&http,
118-
))?;
127+
if runtime
128+
.block_on(hex::remove_api_key(
129+
&previous.name,
130+
&config,
131+
&new_key.unencrypted,
132+
&http,
133+
))
134+
.is_err()
135+
{
136+
let text = wrap(&format!(
137+
"There was an error deleting key `{}` from Hex. You have to delete the key manually at https://hex.pm",
138+
previous.name
139+
));
140+
141+
println!("{text}");
142+
};
119143
}
120144
Ok(())
121145
}

compiler-cli/src/hex/auth.rs

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use crate::{cli, fs::ConsoleWarningEmitter, http::HttpClient};
2+
use camino::Utf8Path;
23
use gleam_core::{
34
Error, Result, Warning, encryption, hex, paths::global_hexpm_credentials_path,
45
warning::WarningEmitter,
56
};
7+
use serde::{Deserialize, Serialize};
68
use std::{rc::Rc, time::SystemTime};
79

810
pub const USER_PROMPT: &str = "https://hex.pm username";
@@ -12,15 +14,17 @@ pub const LOCAL_PASS_PROMPT: &str = "Local password";
1214
pub const PASS_ENV_NAME: &str = "HEXPM_PASS";
1315
pub const API_ENV_NAME: &str = "HEXPM_API_KEY";
1416

15-
#[derive(Debug)]
17+
#[derive(Debug, Serialize, Deserialize)]
1618
pub struct EncryptedApiKey {
1719
pub name: String,
1820
pub encrypted: String,
21+
pub username: Option<String>,
1922
}
2023

2124
#[derive(Debug)]
2225
pub struct UnencryptedApiKey {
2326
pub unencrypted: String,
27+
pub username: Option<String>,
2428
}
2529

2630
pub struct HexAuthentication<'runtime> {
@@ -74,11 +78,18 @@ encrypt your Hex API key.
7478
detail: e.to_string(),
7579
})?;
7680

77-
crate::fs::write(&path, &format!("{name}\n{encrypted}"))?;
81+
let encrypted = EncryptedApiKey {
82+
name,
83+
encrypted,
84+
username: Some(username.clone()),
85+
};
86+
87+
encrypted.save(&path)?;
7888
println!("Encrypted Hex API key written to {path}");
7989

8090
Ok(UnencryptedApiKey {
8191
unencrypted: api_key,
92+
username: Some(username),
8293
})
8394
}
8495

@@ -117,7 +128,12 @@ encrypt your Hex API key.
117128
}
118129

119130
fn read_and_decrypt_stored_api_key(&mut self) -> Result<Option<UnencryptedApiKey>> {
120-
let Some(EncryptedApiKey { encrypted, .. }) = self.read_stored_api_key()? else {
131+
let Some(EncryptedApiKey {
132+
encrypted,
133+
username,
134+
..
135+
}) = self.read_stored_api_key()?
136+
else {
121137
return Ok(None);
122138
};
123139

@@ -127,26 +143,19 @@ encrypt your Hex API key.
127143
detail: e.to_string(),
128144
})?;
129145

130-
Ok(Some(UnencryptedApiKey { unencrypted }))
146+
Ok(Some(UnencryptedApiKey {
147+
unencrypted,
148+
username,
149+
}))
131150
}
132151

133152
pub fn read_stored_api_key(&self) -> Result<Option<EncryptedApiKey>> {
134153
let path = global_hexpm_credentials_path();
135154
if !path.exists() {
136155
return Ok(None);
137156
}
138-
let text = crate::fs::read(&path)?;
139-
let mut chunks = text.splitn(2, '\n');
140-
let Some(name) = chunks.next() else {
141-
return Ok(None);
142-
};
143-
let Some(encrypted) = chunks.next() else {
144-
return Ok(None);
145-
};
146-
Ok(Some(EncryptedApiKey {
147-
name: name.to_string(),
148-
encrypted: encrypted.to_string(),
149-
}))
157+
158+
EncryptedApiKey::load(&path)
150159
}
151160
}
152161

@@ -158,6 +167,49 @@ impl Drop for HexAuthentication<'_> {
158167
}
159168
}
160169

170+
impl EncryptedApiKey {
171+
pub fn save(&self, path: &Utf8Path) -> Result<()> {
172+
let text = toml::to_string(self).map_err(|_| Error::InvalidCredentialsFile {
173+
path: path.to_string(),
174+
})?;
175+
176+
crate::fs::write(path, &text)
177+
}
178+
179+
pub fn load(path: &Utf8Path) -> Result<Option<Self>> {
180+
let text = crate::fs::read(path)?;
181+
182+
toml::from_str(&text).or_else(|_| {
183+
// fallback from old format
184+
let mut chunks = text.splitn(2, '\n');
185+
186+
let Some(name) = chunks.next() else {
187+
return Err(Error::InvalidCredentialsFile {
188+
path: path.to_string(),
189+
});
190+
};
191+
192+
let Some(encrypted) = chunks.next() else {
193+
return Err(Error::InvalidCredentialsFile {
194+
path: path.to_string(),
195+
});
196+
};
197+
198+
let key = Self {
199+
name: name.to_string(),
200+
encrypted: encrypted.to_string(),
201+
username: None,
202+
};
203+
204+
// try to save the file in the new format, but let if fail silently,
205+
// we do not want the load operation to fail because of a write.
206+
let _ = key.save(path);
207+
208+
Ok(Some(key))
209+
})
210+
}
211+
}
212+
161213
fn ask_local_password(warnings: &mut Vec<Warning>) -> std::result::Result<String, Error> {
162214
std::env::var(PASS_ENV_NAME)
163215
.inspect(|_| {

compiler-core/src/error.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,9 @@ file_names.iter().map(|x| x.as_str()).join(", "))]
321321

322322
#[error("Failed to decrypt local Hex API key")]
323323
FailedToDecryptLocalHexApiKey { detail: String },
324+
325+
#[error("Invalid Credentials file")]
326+
InvalidCredentialsFile { path: String },
324327
}
325328

326329
/// 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:
14871490
}]
14881491
}
14891492

1493+
Error::InvalidCredentialsFile {path}=> {
1494+
let text = wrap_format!("Your credentials file at {path} is in the wrong format. Try deleting the file and authenticate again.");
1495+
1496+
vec![Diagnostic {
1497+
title: "Invalid credentials file".into(),
1498+
text,
1499+
level: Level::Error,
1500+
location: None,
1501+
hint: None
1502+
}]
1503+
}
1504+
14901505
Error::NonUtf8Path { path } => {
14911506
let text = format!(
14921507
"Encountered a non UTF-8 path '{}', but only UTF-8 paths are supported.",

0 commit comments

Comments
 (0)