// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
use std::error::Error;
use log::info;
use sqlx::PgPool;
use uuid::Uuid;
use crate::config::TFConfig;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use totp_rs::{Secret, TOTP};
use crate::util::{TOTP_ALGORITHM, TOTP_DIGITS, TOTP_ISSUER, TOTP_SKEW, TOTP_STEP};
// https://admin.defined.net/auth/magic-link?email=coredoescode%40gmail.com&token=ml-ckBsgw_5IdK5VYgseBYcoV_v_cQjtdq1re_RhDu_MKg
pub async fn send_magic_link(id: i64, email: String, db: &PgPool, config: &TFConfig) -> Result<(), Box> {
let otp = format!("ml-{}", Uuid::new_v4());
let otp_url = config.web_root.join(&format!("/auth/magic-link?email={}&token={}", urlencoding::encode(&email.clone()), otp.clone())).unwrap();
sqlx::query!("INSERT INTO magic_links (id, user_id, expires_on) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;", otp, id as i32, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32 + config.magic_links_valid_for as i32).execute(db).await?;
// TODO: send email
info!("sent magic link {} to {}, valid for {} seconds", otp_url, email.clone(), config.magic_links_valid_for);
Ok(())
}
pub async fn generate_session_token(user_id: i64, db: &PgPool, config: &TFConfig) -> Result> {
let token = format!("st-{}", Uuid::new_v4());
sqlx::query!("INSERT INTO session_tokens (id, user_id, expires_on) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;", token, user_id as i32, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32 + config.session_tokens_valid_for as i32).execute(db).await?;
Ok(token)
}
pub async fn validate_session_token(token: String, db: &PgPool) -> Result> {
Ok(sqlx::query!("SELECT user_id FROM session_tokens WHERE id = $1 AND expires_on > $2", token, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32).fetch_one(db).await?.user_id as i64)
}
pub async fn generate_auth_token(user_id: i64, session_id: String, db: &PgPool) -> Result> {
let token = format!("at-{}", Uuid::new_v4());
sqlx::query!("INSERT INTO auth_tokens (id, session_token, user_id) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;", token, session_id, user_id as i32).execute(db).await?;
Ok(token)
}
pub async fn validate_auth_token(token: String, session_id: String, db: &PgPool) -> Result<(), Box> {
validate_session_token(session_id.clone(), db).await?;
sqlx::query!("SELECT * FROM auth_tokens WHERE id = $1 AND session_token = $2", token, session_id).fetch_one(db).await?;
Ok(())
}
/*
CREATE TABLE totp_create_tokens (
id VARCHAR(39) NOT NULL PRIMARY KEY,
expires_on INTEGER NOT NULL,
totp_otpurl VARCHAR(3000) NOT NULL,
totp_secret VARCHAR(128) NOT NULL
);
*/
pub async fn create_totp_token(email: String, db: &PgPool, config: &TFConfig) -> Result<(String, TOTP), Box> {
// create the TOTP parameters
let secret = Secret::generate_secret();
let totpmachine = TOTP::new(TOTP_ALGORITHM, TOTP_DIGITS, TOTP_SKEW, TOTP_STEP, secret.to_bytes().unwrap(), Some(TOTP_ISSUER.to_string()), email).unwrap();
let otpurl = totpmachine.get_url();
let otpsecret = totpmachine.get_secret_base32();
let otpid = format!("totp-{}", Uuid::new_v4());
sqlx::query!("INSERT INTO totp_create_tokens (id, expires_on, totp_otpurl, totp_secret) VALUES ($1, $2, $3, $4);", otpid.clone(), (SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + config.totp_verification_valid_for) as i32, otpurl, otpsecret).execute(db).await?;
Ok((otpid, totpmachine))
}
pub async fn verify_totp_token(otpid: String, email: String, db: &PgPool) -> Result> {
let totprow = sqlx::query!("SELECT * FROM totp_create_tokens WHERE id = $1 AND expires_on > $2", otpid, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32).fetch_one(db).await?;
let secret = Secret::Encoded(totprow.totp_secret);
let totpmachine = TOTP::new(TOTP_ALGORITHM, TOTP_DIGITS, TOTP_SKEW, TOTP_STEP, secret.to_bytes().unwrap(), Some(TOTP_ISSUER.to_string()), email).unwrap();
if totpmachine.get_url() != totprow.totp_otpurl {
return Err("OTPURLs do not match (email does not match?)".into())
}
Ok(totpmachine)
}
pub async fn use_totp_token(otpid: String, email: String, db: &PgPool) -> Result> {
let totpmachine = verify_totp_token(otpid.clone(), email.clone(), db).await?;
sqlx::query!("DELETE FROM totp_create_tokens WHERE id = $1", otpid).execute(db).await?;
sqlx::query!("UPDATE users SET totp_otpurl = $1, totp_secret = $2, totp_verified = 1 WHERE email = $3", totpmachine.get_url(), totpmachine.get_secret_base32(), email).execute(db).await?;
Ok(totpmachine)
}
pub async fn get_totpmachine(user: i32, db: &PgPool) -> Result> {
let user = sqlx::query!("SELECT totp_secret, totp_otpurl, email FROM users WHERE id = $1", user).fetch_one(db).await?;
let secret = Secret::Encoded(user.totp_secret);
Ok(TOTP::new(TOTP_ALGORITHM, TOTP_DIGITS, TOTP_SKEW, TOTP_STEP, secret.to_bytes().unwrap(), Some(TOTP_ISSUER.to_string()), user.email).unwrap())
}
pub async fn user_has_totp(user: i32, db: &PgPool) -> Result> {
Ok(sqlx::query!("SELECT totp_verified FROM users WHERE id = $1", user).fetch_one(db).await?.totp_verified == 1)
}