use crate::sql::Pool;
use crate::totp::TotpSecret;
use crate::Model;
#[derive(Model, Debug, Clone)]
#[rustango(table = "rustango_admin_totp", managed = false)]
#[allow(dead_code)]
pub struct AdminTotp {
#[rustango(primary_key)]
pub user_id: i64,
#[rustango(max_length = 64)]
pub secret_base32: String,
#[rustango(default = "false")]
pub confirmed: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
}
const CREATE_TABLE_PG: &str = r#"
CREATE TABLE IF NOT EXISTS "rustango_admin_totp" (
"user_id" BIGINT PRIMARY KEY,
"secret_base32" VARCHAR(64) NOT NULL,
"confirmed" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT now()
);
"#;
const CREATE_TABLE_MYSQL: &str = r#"
CREATE TABLE IF NOT EXISTS `rustango_admin_totp` (
`user_id` BIGINT PRIMARY KEY,
`secret_base32` VARCHAR(64) NOT NULL,
`confirmed` TINYINT(1) NOT NULL DEFAULT 0,
`created_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)
);
"#;
const CREATE_TABLE_SQLITE: &str = r#"
CREATE TABLE IF NOT EXISTS "rustango_admin_totp" (
"user_id" INTEGER PRIMARY KEY,
"secret_base32" TEXT NOT NULL,
"confirmed" INTEGER NOT NULL DEFAULT 0,
"created_at" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
"#;
pub async fn ensure_table(pool: &Pool) -> Result<(), sqlx::Error> {
let ddl = match pool.dialect().name() {
"mysql" => CREATE_TABLE_MYSQL,
"sqlite" => CREATE_TABLE_SQLITE,
_ => CREATE_TABLE_PG,
};
crate::sql::run_ddl_idempotent(pool, ddl).await
}
pub async fn device(pool: &Pool, user_id: i64) -> Option<AdminTotp> {
use crate::sql::FetcherPool as _;
AdminTotp::objects()
.filter("user_id", user_id)
.fetch(pool)
.await
.ok()
.and_then(|rows| rows.into_iter().next())
}
pub async fn confirmed_secret(pool: &Pool, user_id: i64) -> Option<TotpSecret> {
let d = device(pool, user_id).await?;
if !d.confirmed {
return None;
}
TotpSecret::from_base32(&d.secret_base32)
}
pub async fn start_enrollment(
pool: &Pool,
user_id: i64,
secret: &TotpSecret,
) -> Result<(), crate::sql::ExecError> {
let del = AdminTotp::objects()
.filter("user_id", user_id)
.compile_delete()?;
crate::sql::delete_pool(pool, &del).await?;
let row = AdminTotp {
user_id,
secret_base32: secret.to_base32(),
confirmed: false,
created_at: chrono::Utc::now(),
};
row.insert_pool(pool).await?;
Ok(())
}
pub async fn confirm(pool: &Pool, user_id: i64) -> Result<(), crate::sql::ExecError> {
use crate::sql::UpdaterPool as _;
AdminTotp::objects()
.filter("user_id", user_id)
.update()
.set("confirmed", true)
.execute_pool(pool)
.await?;
Ok(())
}