pub mod methods; use chrono::{Datelike, SecondsFormat, Utc}; use dotenvy::dotenv; use ldap3::{LdapConn, Scope, SearchEntry}; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use sqlx::{ sqlite::{SqliteConnectOptions, SqlitePoolOptions}, Error, Pool, Sqlite, }; use std::{ env, str::FromStr, time::{SystemTime, UNIX_EPOCH}, }; use tide::prelude::*; #[derive(Debug, Clone, Deserialize, Serialize, sqlx::FromRow)] pub struct AccountsNew { pub mail: String, pub auth_code: String, pub date_iso: String, pub date_expiry: String, pub name_first: String, pub name_surname: String, pub id_student: String, } #[derive(Debug, Clone, Deserialize, Serialize, sqlx::FromRow)] pub struct AccountsReset { pub user: String, pub auth_code: String, pub date_expiry: String, } #[derive(Debug, Clone, Deserialize, Serialize, sqlx::FromRow)] pub struct Accounts { pub user: String, pub uid: i64, pub discord: Option, pub mail: String, pub student_id: String, pub enabled: bool, pub secure: bool, } pub async fn db_init(config: &Config) -> Result, Error> { let database = format!("{}/{}", &config.home, &config.database); let pool = SqlitePoolOptions::new() .max_connections(5) .connect_with(SqliteConnectOptions::from_str(&format!("sqlite://{}", database))?.create_if_missing(true)) .await?; sqlx::query( "CREATE TABLE IF NOT EXISTS accounts_new ( mail text primary key, auth_code text not null, date_iso text not null, date_expiry text not null, name_first text not null, name_surname integer not null, id_student text not null )", ) .execute(&pool) .await?; sqlx::query("CREATE INDEX IF NOT EXISTS index_auth_code ON accounts_new (auth_code)") .execute(&pool) .await?; sqlx::query("CREATE INDEX IF NOT EXISTS index_date_expiry ON accounts_new (date_expiry)") .execute(&pool) .await?; sqlx::query( "CREATE TABLE IF NOT EXISTS accounts_reset ( user text primary key, auth_code text not null, date_expiry text not null )", ) .execute(&pool) .await?; sqlx::query("CREATE INDEX IF NOT EXISTS index_auth_code ON accounts_reset (auth_code)") .execute(&pool) .await?; sqlx::query("CREATE INDEX IF NOT EXISTS index_date_expiry ON accounts_reset (date_expiry)") .execute(&pool) .await?; // this is for active use sqlx::query( "CREATE TABLE IF NOT EXISTS accounts ( user text primary key, uid integer not null, discord text, mail text not null, student_id text not null, enabled integer not null, secure integer not null )", ) .execute(&pool) .await?; sqlx::query("CREATE INDEX IF NOT EXISTS index_uid_number ON accounts (uid)").execute(&pool).await?; sqlx::query("CREATE INDEX IF NOT EXISTS index_mail ON accounts (mail)").execute(&pool).await?; sqlx::query("CREATE INDEX IF NOT EXISTS index_student_id ON accounts (student_id)") .execute(&pool) .await?; update_accounts(&pool, config).await; Ok(pool) } pub fn get_now() -> i64 { if let Ok(x) = SystemTime::now().duration_since(UNIX_EPOCH) { x.as_secs() as i64 } else { 0 } } pub fn get_now_iso(short: bool) -> String { let now = Utc::now(); if short { format!("{}-{:02}-{:02}", now.year(), now.month(), now.day()) } else { now.to_rfc3339_opts(SecondsFormat::Millis, true) } } #[derive(Clone)] pub struct State { pub db: Pool, pub config: Config, } #[derive(Debug, Clone)] pub struct Config { pub ldap_host: String, pub ldap_admin: String, pub ldap_admin_pw: String, pub home: String, pub database: String, pub csv: String, pub host_port: String, pub mail_smtp: String, pub mail_user: String, pub mail_pass: String, } pub fn get_config() -> Config { dotenv().ok(); // reasonable defaults let mut config = Config { ldap_host: "".to_string(), ldap_admin: "".to_string(), ldap_admin_pw: "".to_string(), home: ".".to_string(), database: "database.db".to_string(), csv: "wolves.csv".to_string(), host_port: "127.0.0.1:8087".to_string(), mail_smtp: "".to_string(), mail_user: "".to_string(), mail_pass: "".to_string(), }; if let Ok(x) = env::var("LDAP_HOST") { config.ldap_host = x.trim().to_string(); } if let Ok(x) = env::var("LDAP_ADMIN") { config.ldap_admin = x.trim().to_string(); } if let Ok(x) = env::var("LDAP_ADMIN_PW") { config.ldap_admin_pw = x.trim().to_string(); } if let Ok(x) = env::var("HOME") { config.home = x.trim().to_string(); } if let Ok(x) = env::var("DATABASE") { config.database = x.trim().to_string(); } if let Ok(x) = env::var("CSV") { config.csv = x.trim().to_string(); } if let Ok(x) = env::var("HOST_PORT") { config.host_port = x.trim().to_string(); } if let Ok(x) = env::var("EMAIL_SMTP") { config.mail_smtp = x.trim().to_string(); } if let Ok(x) = env::var("EMAIL_USER") { config.mail_user = x.trim().to_string(); } if let Ok(x) = env::var("EMAIL_PASS") { config.mail_pass = x.trim().to_string(); } config } async fn update_accounts(pool: &Pool, config: &Config) { let mut ldap = LdapConn::new(&config.ldap_host).unwrap(); ldap.simple_bind(&config.ldap_admin, &config.ldap_admin_pw).unwrap().success().unwrap(); // use this to pre load a large chunk of data if let Ok(x) = ldap.search("ou=users,dc=skynet,dc=ie", Scope::OneLevel, "(objectClass=*)", vec!["uid", "uidNumber", "skDiscord", "skMemberOf", "mail", "skID", "skSecure"]) { if let Ok((rs, _res)) = x.success() { for entry in rs { let tmp = SearchEntry::construct(entry); let mut tmp_account = Accounts { user: "".to_string(), uid: 0, discord: None, mail: "".to_string(), student_id: "".to_string(), enabled: false, secure: false, }; // pull out the required info if tmp.attrs.contains_key("uid") && !tmp.attrs["uid"].is_empty() { tmp_account.user = tmp.attrs["uid"][0].clone(); } if tmp.attrs.contains_key("uidNumber") && !tmp.attrs["uidNumber"].is_empty() { tmp_account.uid = tmp.attrs["uidNumber"][0].clone().parse().unwrap_or(0); } if tmp.attrs.contains_key("skDiscord") && !tmp.attrs["skDiscord"].is_empty() { tmp_account.discord = Option::from(tmp.attrs["skDiscord"][0].clone()); } if tmp.attrs.contains_key("mail") && !tmp.attrs["mail"].is_empty() { tmp_account.mail = tmp.attrs["mail"][0].clone(); } if tmp.attrs.contains_key("skID") && !tmp.attrs["skID"].is_empty() { tmp_account.student_id = tmp.attrs["skID"][0].clone(); } if tmp.attrs.contains_key("skMemberOf") && !tmp.attrs["skMemberOf"].is_empty() && tmp.attrs["skMemberOf"].contains(&String::from("cn=skynet-users-linux,ou=groups,dc=skynet,dc=ie")) { tmp_account.enabled = true; } if tmp.attrs.contains_key("skSecure") && !tmp.attrs["skSecure"].is_empty() { tmp_account.secure = true; } if !tmp_account.user.is_empty() { sqlx::query_as::<_, Accounts>( " INSERT OR REPLACE INTO accounts (user, uid, discord, mail, student_id, enabled, secure) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) ", ) .bind(&tmp_account.user) .bind(tmp_account.uid) .bind(&tmp_account.discord) .bind(&tmp_account.mail) .bind(&tmp_account.student_id) .bind(tmp_account.enabled) .bind(tmp_account.secure) .fetch_optional(pool) .await .ok(); } } } } // done with ldap ldap.unbind().unwrap(); } #[derive(Debug, serde::Deserialize)] pub struct Record { #[serde(rename = "MemID")] pub mem_id: String, #[serde(rename = "Student Num")] pub id_student: String, #[serde(rename = "Contact Email")] pub email: String, #[serde(rename = "Expiry")] pub expiry: String, #[serde(rename = "First Name")] pub name_first: String, #[serde(rename = "Last Name")] pub name_second: String, } pub fn read_csv(config: &Config) -> Result, Box> { let mut records: Vec = vec![]; if let Ok(mut rdr) = csv::Reader::from_path(format!("{}/{}", &config.home, &config.csv)) { for result in rdr.deserialize() { // Notice that we need to provide a type hint for automatic // deserialization. let record: Record = result?; if record.mem_id.is_empty() { continue; } records.push(record); } } Ok(records) } // from https://rust-lang-nursery.github.io/rust-cookbook/algorithms/randomness.html#create-random-passwords-from-a-set-of-alphanumeric-characters pub fn random_string(len: usize) -> String { thread_rng().sample_iter(&Alphanumeric).take(len).map(char::from).collect() }