#![allow(dead_code)]
use base64::engine::{general_purpose::URL_SAFE_NO_PAD, Engine};
use esi_openapi::prelude::*;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct EnvConfig {
pub client_id: String,
pub client_secret: Option<String>,
pub callback_url: String,
pub scopes: String,
pub refresh_token: Option<String>,
pub user_agent: String,
pub structure_id: Option<i64>,
}
pub fn env_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(".env")
}
fn var(name: &str) -> Option<String> {
std::env::var(name)
.ok()
.map(|v| v.trim().to_owned())
.filter(|v| !v.is_empty())
}
pub fn load() -> Result<EnvConfig, String> {
match dotenvy::from_path(env_path()) {
Ok(()) => {}
Err(dotenvy::Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(format!("could not read {}: {e}", env_path().display())),
}
let mut missing = Vec::new();
let client_id = var("ESI_CLIENT_ID");
let callback_url = var("ESI_CALLBACK_URL");
if client_id.is_none() {
missing.push("ESI_CLIENT_ID");
}
if callback_url.is_none() {
missing.push("ESI_CALLBACK_URL");
}
if !missing.is_empty() {
return Err(format!(
"missing {} (copy .env.example to .env and fill it in)",
missing.join(", ")
));
}
let contact = var("ESI_USER_AGENT_CONTACT").unwrap_or_else(|| "unknown".to_owned());
Ok(EnvConfig {
client_id: client_id.unwrap(),
client_secret: var("ESI_CLIENT_SECRET"),
callback_url: callback_url.unwrap(),
scopes: var("ESI_SCOPES").unwrap_or_default(),
refresh_token: var("ESI_REFRESH_TOKEN"),
user_agent: format!(
"esi-openapi/{} ({contact}; +https://github.com/rafaga/esi-openapi)",
env!("CARGO_PKG_VERSION")
),
structure_id: var("ESI_TEST_STRUCTURE_ID").and_then(|v| v.parse().ok()),
})
}
pub fn build_esi(cfg: &EnvConfig) -> EsiResult<Esi> {
let mut builder = EsiBuilder::new()
.user_agent(&cfg.user_agent)
.client_id(&cfg.client_id)
.callback_url(&cfg.callback_url)
.scope(&cfg.scopes);
builder = match &cfg.client_secret {
Some(secret) => builder.client_secret(secret),
None => builder.enable_application_authentication(true),
};
builder.build()
}
pub fn save_refresh_token(token: &str) -> std::io::Result<()> {
let path = env_path();
let content = std::fs::read_to_string(&path).unwrap_or_default();
let newline = if content.contains("\r\n") {
"\r\n"
} else {
"\n"
};
let mut found = false;
let mut lines: Vec<String> = content
.lines()
.map(|line| {
if line.trim_start().starts_with("ESI_REFRESH_TOKEN=") {
found = true;
format!("ESI_REFRESH_TOKEN={token}")
} else {
line.to_owned()
}
})
.collect();
if !found {
lines.push(format!("ESI_REFRESH_TOKEN={token}"));
}
std::fs::write(&path, lines.join(newline) + newline)
}
pub fn character_id_from_token(access_token: &str) -> Option<i32> {
let payload = access_token.split('.').nth(1)?;
let bytes = URL_SAFE_NO_PAD.decode(payload.trim_end_matches('=')).ok()?;
let claims: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
claims["sub"].as_str()?.rsplit(':').next()?.parse().ok()
}
pub async fn authenticated_esi(cfg: &EnvConfig) -> Result<(Esi, i32), String> {
let refresh_token = cfg
.refresh_token
.as_deref()
.ok_or("missing ESI_REFRESH_TOKEN (run `cargo run --example auth_get_refresh_token`)")?;
let mut esi = build_esi(cfg).map_err(|e| e.to_string())?;
esi.use_refresh_token(refresh_token)
.await
.map_err(|e| format!("refreshing the access token failed: {e} ({e:?})"))?;
if let Some(new_token) = esi.refresh_token.as_deref() {
if new_token != refresh_token {
save_refresh_token(new_token).map_err(|e| e.to_string())?;
}
}
let character_id = esi
.access_token
.as_deref()
.and_then(character_id_from_token)
.ok_or("could not read the character ID from the access token")?;
esi.update_spec().await.map_err(|e| e.to_string())?;
Ok((esi, character_id))
}
#[cfg(test)]
mod tests {
use super::character_id_from_token;
use base64::engine::{general_purpose::URL_SAFE_NO_PAD, Engine};
#[test]
fn test_character_id_from_token() {
let payload = URL_SAFE_NO_PAD.encode(r#"{"sub":"CHARACTER:EVE:2112625428"}"#);
let token = format!("header.{payload}.signature");
assert_eq!(character_id_from_token(&token), Some(2112625428));
assert_eq!(character_id_from_token("not-a-jwt"), None);
}
}