pub mod email_verification {
use rand::Rng;
use oxidite_db::sqlx::Row;
pub fn generate_token() -> String {
let mut rng = rand::rng();
let random_bytes: Vec<u8> = (0..32).map(|_| rng.random::<u8>()).collect();
hex::encode(random_bytes)
}
pub async fn create_token<D: oxidite_db::Database + ?Sized>(
db: &D,
user_id: i64,
) -> oxidite_db::Result<String> {
let token = generate_token();
let query = oxidite_db::sqlx::query(
"UPDATE users SET verification_token = ? WHERE id = ?"
)
.bind(&token)
.bind(user_id);
db.execute_query(query).await?;
Ok(token)
}
pub async fn verify_email<D: oxidite_db::Database + ?Sized>(
db: &D,
token: &str,
) -> oxidite_db::Result<bool> {
let query = oxidite_db::sqlx::query(
"UPDATE users SET email_verified = 1, verification_token = NULL
WHERE verification_token = ?"
)
.bind(token);
let rows = db.execute_query(query).await?;
Ok(rows > 0)
}
pub async fn is_verified<D: oxidite_db::Database + ?Sized>(
db: &D,
user_id: i64,
) -> oxidite_db::Result<bool> {
let query = oxidite_db::sqlx::query(
"SELECT email_verified FROM users WHERE id = ?"
)
.bind(user_id);
let row = db.fetch_one(query).await?;
if let Some(row) = row {
let verified: i64 = row.try_get("email_verified").unwrap_or(0);
Ok(verified == 1)
} else {
Ok(false)
}
}
}
pub mod password_reset {
use rand::Rng;
use oxidite_db::sqlx::Row;
pub fn generate_token() -> String {
let mut rng = rand::rng();
let random_bytes: Vec<u8> = (0..32).map(|_| rng.random::<u8>()).collect();
hex::encode(random_bytes)
}
pub async fn create_token<D: oxidite_db::Database + ?Sized>(
db: &D,
user_id: i64,
) -> oxidite_db::Result<String> {
let token = generate_token();
let now = chrono::Utc::now().timestamp();
let expires_at = now + 3600;
let query = oxidite_db::sqlx::query(
"INSERT INTO password_reset_tokens (user_id, token, expires_at, created_at)
VALUES (?, ?, ?, ?)"
)
.bind(user_id)
.bind(&token)
.bind(expires_at)
.bind(now);
db.execute_query(query).await?;
Ok(token)
}
pub async fn verify_token<D: oxidite_db::Database + ?Sized>(
db: &D,
token: &str,
) -> oxidite_db::Result<Option<i64>> {
let now = chrono::Utc::now().timestamp();
let query = oxidite_db::sqlx::query(
"SELECT user_id FROM password_reset_tokens
WHERE token = ? AND expires_at > ?"
)
.bind(token)
.bind(now);
let row = db.fetch_one(query).await?;
if let Some(row) = row {
let user_id: i64 = row.try_get("user_id").unwrap_or(0);
Ok(Some(user_id))
} else {
Ok(None)
}
}
pub async fn consume_token<D: oxidite_db::Database + ?Sized>(
db: &D,
token: &str,
) -> oxidite_db::Result<()> {
let query = oxidite_db::sqlx::query(
"DELETE FROM password_reset_tokens WHERE token = ?"
)
.bind(token);
db.execute_query(query).await?;
Ok(())
}
pub async fn cleanup_expired<D: oxidite_db::Database + ?Sized>(
db: &D,
) -> oxidite_db::Result<()> {
let now = chrono::Utc::now().timestamp();
let query = oxidite_db::sqlx::query(
"DELETE FROM password_reset_tokens WHERE expires_at < ?"
)
.bind(now);
db.execute_query(query).await?;
Ok(())
}
}
pub mod two_factor {
use totp_rs::{TOTP, Algorithm};
use oxidite_db::sqlx::Row; use rand::Rng;
pub fn generate_secret() -> String {
use base64::Engine;
let mut rng = rand::rng();
let random_bytes: Vec<u8> = (0..20).map(|_| rng.random::<u8>()).collect();
base64::engine::general_purpose::STANDARD.encode(random_bytes)
}
pub async fn enable<D: oxidite_db::Database + ?Sized>(
db: &D,
user_id: i64,
secret: &str,
) -> oxidite_db::Result<()> {
let query = oxidite_db::sqlx::query(
"UPDATE users SET two_factor_secret = ?, two_factor_enabled = 1
WHERE id = ?"
)
.bind(secret)
.bind(user_id);
db.execute_query(query).await?;
Ok(())
}
pub async fn disable<D: oxidite_db::Database + ?Sized>(
db: &D,
user_id: i64,
) -> oxidite_db::Result<()> {
let query = oxidite_db::sqlx::query(
"UPDATE users SET two_factor_secret = NULL, two_factor_enabled = 0
WHERE id = ?"
)
.bind(user_id);
db.execute_query(query).await?;
Ok(())
}
pub fn verify_code(secret: &str, code: &str) -> bool {
use base64::Engine;
let secret_bytes = match base64::engine::general_purpose::STANDARD.decode(secret) {
Ok(bytes) => bytes,
Err(_) => return false,
};
let totp = match TOTP::new(
Algorithm::SHA1,
6,
1,
30,
secret_bytes,
) {
Ok(t) => t,
Err(_) => return false,
};
totp.check_current(code).unwrap_or(false)
}
pub async fn get_secret<D: oxidite_db::Database + ?Sized>(
db: &D,
user_id: i64,
) -> oxidite_db::Result<Option<String>> {
let query = oxidite_db::sqlx::query(
"SELECT two_factor_secret, two_factor_enabled FROM users WHERE id = ?"
)
.bind(user_id);
let row = db.fetch_one(query).await?;
if let Some(row) = row {
let enabled: i64 = row.try_get("two_factor_enabled").unwrap_or(0);
if enabled == 1 {
let secret: String = row.try_get("two_factor_secret").unwrap_or_default();
if !secret.is_empty() {
return Ok(Some(secret));
}
}
}
Ok(None)
}
pub fn generate_provisioning_uri(secret: &str, account: &str, issuer: &str) -> String {
format!(
"otpauth://totp/{}:{}?secret={}&issuer={}",
urlencoding::encode(issuer),
urlencoding::encode(account),
secret,
urlencoding::encode(issuer)
)
}
}