made most of the sql, now to do the UI

This commit is contained in:
Artemis 2025-03-18 21:11:53 +01:00
parent a8cab92fd5
commit b37867b018
6 changed files with 61 additions and 25 deletions

View file

@ -1,17 +1,41 @@
use uuid::Uuid;
use super::schema::DbHook;
use super::schema::{DbHook, OTP};
const METHOD_TOTP: &'static str = "totp";
pub const METHOD_TOTP: &'static str = "totp";
/// Checks that the provided user has at least one OTP method enabled
pub async fn has_otp(db: &mut DbHook, id: &Uuid) -> sqlx::Result<bool> {
sqlx::query_scalar!("select count(otp_method) from otp where user_id = $1", id)
.fetch_one(&mut **db)
.await
.map(|count| count.unwrap_or(0) > 0)
/// Checks that the provided user has the specified OTP method
pub async fn has_otp(db: &mut DbHook, id: &Uuid, method: &str) -> sqlx::Result<bool> {
Ok(sqlx::query!(
"select otp_method from otp where user_id = $1 and otp_method = $2",
id,
method
)
.fetch_optional(&mut **db)
.await?
.is_some())
}
/// Lists the OTP methods the user has enabled
pub async fn list_enabled_methods(db: &mut DbHook, id: &Uuid) -> sqlx::Result<Vec<String>> {
sqlx::query_scalar!("select otp_method from otp where user_id = $1", id)
.fetch_all(&mut **db)
.await
}
/// Gets the requested OTP method config, if set
pub async fn get_otp_method(db: &mut DbHook, id: &Uuid, method: &str) -> sqlx::Result<Option<OTP>> {
sqlx::query_as!(
OTP,
"select * from otp where user_id = $1 and otp_method = $2",
id,
method
)
.fetch_optional(&mut **db)
.await
}
/// Adds a new otp method with the provided config; will fail without check if one is already set
pub async fn add_otp_method(
db: &mut DbHook,
id: &Uuid,

View file

@ -98,6 +98,18 @@ pub struct User {
pub is_admin: bool,
}
/// A user's OTP config for a given OTP scheme.
/// WAT? why does it work without a serialize impl.?
#[derive(Debug)]
pub struct OTP {
pub user_id: Uuid,
pub created_at: chrono::DateTime<Utc>,
pub otp_method: String,
pub secret_seed: String,
pub recovery_key: String,
}
/// The service status aggregate
#[derive(Debug, Serialize)]
#[serde(crate = "rocket::serde")]

View file

@ -82,8 +82,8 @@ fn rocket() -> _ {
account::common::ask_terminate_account,
account::common::confirm_terminate_account,
account::common::export_data,
account::otp::show_otp_enable_start,
account::otp::handle_otp_enable_start,
account::otp::show_totp_enable_start,
account::otp::handle_totp_enable_start,
],
)
.mount(

View file

@ -6,21 +6,21 @@ use crate::{
auth,
db::{
self,
otp::METHOD_TOTP,
schema::{DollTagsDb, User},
},
pages::CommonTemplateState,
routes,
routes::error_handlers::PageResult,
routes::{self, error_handlers::PageResult},
};
#[get("/settings/otp")]
pub async fn show_otp_enable_start(
#[get("/settings/totp")]
pub async fn show_totp_enable_start(
mut db: DollTagsDb,
user: User,
cookies: &CookieJar<'_>,
meta: CommonTemplateState,
) -> PageResult {
if db::otp::has_otp(&mut *db, &user.id).await? {
if db::otp::has_otp(&mut *db, &user.id, METHOD_TOTP).await? {
return Ok(Redirect::to(uri!("/account", routes::account::settings::show_settings)).into());
}
let totp_secret = Secret::generate_secret();
@ -46,20 +46,20 @@ pub struct OtpEnableForm {
pub otp_code: String,
}
#[post("/settings/otp", data = "<form>")]
pub async fn handle_otp_enable_start(
#[post("/settings/totp", data = "<form>")]
pub async fn handle_totp_enable_start(
mut db: DollTagsDb,
form: Form<OtpEnableForm>,
user: User,
cookies: &CookieJar<'_>,
_meta: CommonTemplateState,
) -> PageResult {
if db::otp::has_otp(&mut *db, &user.id).await? {
if db::otp::has_otp(&mut *db, &user.id, METHOD_TOTP).await? {
return Ok(Redirect::to(uri!("/account", routes::account::settings::show_settings)).into());
}
let secret = match auth::otp::get_secret(cookies) {
Some(v) => v,
None => return Ok(Redirect::to(uri!("/account", show_otp_enable_start)).into()),
None => return Ok(Redirect::to(uri!("/account", show_totp_enable_start)).into()),
};
let totp = auth::otp::make_totp(&user.id.to_string(), secret.to_bytes()?)?;

View file

@ -24,14 +24,14 @@ pub async fn show_settings(
user: User,
meta: CommonTemplateState,
) -> PageResult {
let has_otp = otp::has_otp(&mut *db, &user.id).await?;
let enabled_otp_methods = otp::list_enabled_methods(&mut *db, &user.id).await?;
Ok(Template::render(
"account/settings",
context! {
user,
meta,
otp: has_otp,
enabled_otp_methods,
prev_common: form::Context::default(),
prev_password: form::Context::default(),
},

View file

@ -67,8 +67,8 @@
</div>
<section id="otp">
{% if otp %}
<p>Wow, you already have OTP enabled even though it's not yet implemented.</p>
{% for method in enabled_otp_methods %}
<p>Wow, you already have {{method}} OTP enabled even though it's not yet implemented.</p>
{% else %}
<h3>Two-factor authentication</h3>
@ -77,8 +77,8 @@
You can add one using your authenticator app of choice by clicking below.
</p>
<a href="/account/settings/otp" class="btn">Enable 2FA with an authenticator</a>
{% endif %}
<a href="/account/settings/totp" class="btn">Enable 2FA with an authenticator</a>
{% endfor %}
</section>
<section id="data-export">