use crate::{
api::{self, CliError, Validation, ValidationSecretCode},
util::{self, error_detail, get_config_profile, Result},
CONFIG_LOGIN_TEMPLATE, CONFIG_SIGNUP_TEMPLATE,
};
use colored::*;
use rand::distributions::{Alphanumeric, DistString};
use secd::{AuthEmailMessenger, AuthStore};
use std::{
fs::{self, File},
io::{self, stdin, stdout, Read, Write},
net::TcpListener,
str::{self, FromStr},
};
use strum::VariantNames;
use tiny_http::Server;
use uuid::Uuid;
const DEFAULT_LOGIN_EMAIL: &str = "
You requested a login link for %secd_email_address%. Please click the following link
http://localhost:5500/myapp/iam/exchange/%secd_link%
or use code: %secd_code%
";
const DEFAULT_SIGNUP_EMAIL: &str = "Welcome to SecD IAM
If you did not request this sign up, you can safely ignore this email. Otherwise, please click the following link to validate your account
http://localhost:5500/myapp/iam/exchange/%secd_link%
or use code: %secd_code%
";
pub async fn admin_init(is_interactive: bool) -> Result<()> {
let config_dir = util::get_config_dir();
let config_profile = get_config_profile();
fs::create_dir_all(config_dir.clone());
if config_profile.try_exists()? {
writeln!(
io::stdout(),
"{} {}",
config_profile.clone().display(),
"already exists and there is nothing to initialize. To create a new IAM store use `iam admin create store` or modify the configuration profile directly"
.yellow()
)?;
} else {
writeln!(stdout(), "{}", "creating default profile".green())?;
let mut login_template = config_dir.clone();
login_template.push(CONFIG_LOGIN_TEMPLATE);
let mut f = File::create(login_template.clone())?;
f.write_all(DEFAULT_LOGIN_EMAIL.as_bytes())?;
let mut signup_template = config_dir.clone();
signup_template.push(CONFIG_SIGNUP_TEMPLATE);
f = File::create(signup_template.clone())?;
f.write_all(DEFAULT_SIGNUP_EMAIL.as_bytes())?;
let mut cfg = api::Config {
profile: vec![api::ConfigProfile {
name: "default".to_string(),
store: AuthStore::Sqlite { conn: "".into() },
store_conn: format!(
"sqlite://{}/{}.sql?mode=rwc",
config_dir.clone().display().to_string(),
Alphanumeric.sample_string(&mut rand::thread_rng(), 5),
),
emailer: secd::AuthEmailMessenger::Local,
email_template_login: Some(login_template.display().to_string()),
email_template_signup: Some(signup_template.display().to_string()),
}],
};
let mut input: String = String::new();
if is_interactive {
writeln!(stdout(), "{}", "For a complete overview of configuration options, cancel the initialization and explore `iam help`")?;
write!(stdout(), "Would you like to create a default local store with local stubs for external services?[(y)es/(n)o]: ")?;
stdout().flush()?;
'outer: loop {
input.clear();
stdin().read_line(&mut input)?;
match input.as_str().trim() {
"Y" | "y" | "Yes" | "yes" => break,
"N" | "n" | "No" | "no" => {
loop {
write!(
stdout(),
"Persistence store {:?}: ",
AuthStore::VARIANTS
.iter()
.map(|s| s.to_lowercase())
.collect::>()
)?;
stdout().flush()?;
input.clear();
stdin().read_line(&mut input)?;
match AuthStore::from_str(&input.trim()) {
Ok(s) => {
cfg.profile[0].store = s;
break;
}
Err(_) => {
writeln!(stdout(), "{}", "Invalid store type".red())?;
}
}
}
write!(stdout(), "Store connection string: ")?;
stdout().flush()?;
input.clear();
stdin().read_line(&mut input)?;
cfg.profile[0].store_conn = input.trim().to_string().clone();
loop {
write!(
stdout(),
"Email provider {:?}: ",
AuthEmailMessenger::VARIANTS
.iter()
.map(|s| s.to_lowercase())
.collect::>()
)?;
stdout().flush()?;
input.clear();
stdin().read_line(&mut input)?;
match AuthEmailMessenger::from_str(&input.trim()) {
Ok(s) => {
cfg.profile[0].emailer = s;
break;
}
Err(_) => {
writeln!(stdout(), "{}", "Invalid email provider".red())?;
}
}
}
write!(
stdout(),
"Email template for login validation:[FilePath or Enter for default]: "
)?;
stdout().flush()?;
input.clear();
stdin().read_line(&mut input)?;
cfg.profile[0].email_template_login =
Some(input.trim().to_string().clone());
write!(
stdout(),
"Email template for signup validation:[FilePath or Enter for default]: "
)?;
stdout().flush()?;
input.clear();
stdin().read_line(&mut input)?;
cfg.profile[0].email_template_login =
Some(input.trim().to_string().clone());
break 'outer;
}
_ => {}
}
}
}
let mut f = File::create(config_profile.clone())?;
f.write_all(toml::to_string(&cfg)?.as_bytes())?;
writeln!(
stdout(),
"{} {} {} {} {}",
"created iam config".green(),
"default",
"at".green(),
config_dir.display().to_string(),
"to hold secD iam configurations".green()
)?;
}
Ok(())
}
pub fn dev_oauth2_listen(port: Option) -> Result {
let server = Server::http(&format!("localhost:{}", port.unwrap_or(1337))).map_err(|_| {
CliError::InternalError(error_detail(
"53abd03d-c426-4bba-969d-f1dbed9af75b",
"Failure while creating a server to listen to oauth responese",
))
})?;
let parser = |s: &str| -> Option {
let maybe_code = s.split("code=").collect::>();
if maybe_code.len() != 2 {
None
} else {
let maybe_code = maybe_code
.last()
.map(|s| s.to_string())
.map(|c| {
c.split("&")
.collect::>()
.first()
.map(|s| s.to_string())
})
.flatten();
maybe_code.map(|s| s.to_string())
}
};
let mut s_code = String::new();
for req in server.incoming_requests() {
match parser(req.url()) {
Some(secret_code) => {
s_code = secret_code;
break;
}
None => continue,
}
}
Ok(urlencoding::decode(&s_code)?.to_string())
}