From 77a681c02a033c55c3452f59b40f581cb82dbe09 Mon Sep 17 00:00:00 2001 From: Artemis Date: Tue, 18 Mar 2025 12:26:13 +0100 Subject: [PATCH] started integrating the flow --- src/db/mod.rs | 1 + src/db/otp.rs | 11 ++ src/main.rs | 21 +-- src/routes/account/common.rs | 154 +++++++++++++++++ src/routes/account/mod.rs | 3 + src/routes/account/otp.rs | 6 + .../{account.rs => account/settings.rs} | 156 ++---------------- src/routes/form/register_tag.rs | 10 +- templates/account/otp/start.html.tera | 0 templates/account/settings.html.tera | 15 ++ 10 files changed, 220 insertions(+), 157 deletions(-) create mode 100644 src/db/otp.rs create mode 100644 src/routes/account/common.rs create mode 100644 src/routes/account/mod.rs create mode 100644 src/routes/account/otp.rs rename src/routes/{account.rs => account/settings.rs} (56%) create mode 100644 templates/account/otp/start.html.tera diff --git a/src/db/mod.rs b/src/db/mod.rs index d8556c7..0864279 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,6 +1,7 @@ pub mod admin; pub mod doll; pub mod migrate; +pub mod otp; pub mod reseed; pub mod schema; pub mod user; diff --git a/src/db/otp.rs b/src/db/otp.rs new file mode 100644 index 0000000..51ace9b --- /dev/null +++ b/src/db/otp.rs @@ -0,0 +1,11 @@ +use uuid::Uuid; + +use super::schema::DbHook; + +/// Checks that the provided user has at least one OTP method enabled +pub async fn has_otp(db: &mut DbHook, id: &Uuid) -> sqlx::Result { + 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) +} diff --git a/src/main.rs b/src/main.rs index ea1cb18..d7db8d9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,20 +68,21 @@ fn rocket() -> _ { .mount( "/account", routes![ - account::index, - account::qr_profile, - account::show_settings, - account::change_settings, - account::change_password, + account::common::index, + account::common::qr_profile, + account::settings::show_settings, + account::settings::change_settings, + account::settings::change_password, form::register_tag::show_register, form::register_tag::handle_register, form::register_tag::show_edit_tag, form::register_tag::handle_edit_tag, - account::ask_delete, - account::confirm_delete, - account::ask_terminate_account, - account::confirm_terminate_account, - account::export_data, + account::common::ask_delete, + account::common::confirm_delete, + account::common::ask_terminate_account, + account::common::confirm_terminate_account, + account::common::export_data, + account::otp::show_otp_enable_start, ], ) .mount( diff --git a/src/routes/account/common.rs b/src/routes/account/common.rs new file mode 100644 index 0000000..44b350b --- /dev/null +++ b/src/routes/account/common.rs @@ -0,0 +1,154 @@ +use std::net::IpAddr; + +use qrcode_generator::QrCodeEcc; +use rocket::{ + http::{uri::Absolute, CookieJar}, + response::Redirect, + serde::{json::Json, Serialize}, + State, +}; +use rocket_dyn_templates::{context, Template}; +use sqlx::Acquire; + +use crate::{ + auth::session, + config::Config, + db::{ + doll, + schema::{DollProfile, DollTagsDb, User}, + user, + }, + pages::CommonTemplateState, + routes::error_handlers::{PageResult, RawResult}, +}; + +#[get("/")] +pub async fn index(mut db: DollTagsDb, user: User, meta: CommonTemplateState) -> PageResult { + let tags = doll::list(&mut *db, &user.id).await?; + let archived_tags = doll::list_archived(&mut *db, &user.id).await?; + + Ok(Template::render( + "account/index", + context! { + meta, + user, + tags, + archived_tags, + }, + ) + .into()) +} + +#[get("/qr-png/")] +pub fn qr_profile(id: &str, _user: User, config: &State) -> PageResult { + let public_uri = Absolute::parse(&config.public_url)?; + let built_uri = uri!(public_uri, crate::routes::public::short_url(id)); + let image = qrcode_generator::to_png_to_vec(built_uri.to_string(), QrCodeEcc::Low, 400)?; + + Ok(image.into()) +} + +#[derive(Debug, Serialize)] +#[serde(crate = "rocket::serde")] +pub struct DataDump { + account: User, + tags: Vec, + reserved_tags: Vec, +} + +#[get("/data_dump")] +pub async fn export_data( + mut db: DollTagsDb, + user: User, + client_ip: IpAddr, +) -> RawResult> { + let tags = doll::list_all(&mut *db, &user.id).await?; + let reserved_tags = doll::list_archived(&mut *db, &user.id).await?; + + warn!( + "[audit|{}] [{}] exported data", + client_ip, + user.id.to_string() + ); + + Ok(Json(DataDump { + account: user, + tags, + reserved_tags, + })) +} + +#[get("/delete/")] +pub async fn ask_delete( + mut db: DollTagsDb, + id: i32, + user: User, + meta: CommonTemplateState, +) -> PageResult { + let db_tag = doll::get(&mut *db, id, "", false).await?; + + if let Some(tag) = db_tag { + if tag.bound_to_id != user.id { + Ok(Redirect::to(uri!("/account", index)).into()) + } else { + Ok(Template::render( + "tag/delete", + context! { + meta, + tag, + }, + ) + .into()) + } + } else { + Ok(Redirect::to(uri!("/account", index)).into()) + } +} + +#[get("/yes_delete_this/")] +pub async fn confirm_delete( + mut db: DollTagsDb, + id: i32, + user: User, + client_ip: IpAddr, +) -> PageResult { + let mut trx = db.begin().await?; + doll::delete(&mut trx, id, &user.id).await?; + trx.commit().await?; + + warn!( + "[audit|{}] [{}] deleted tag {:0>6}", + client_ip, + user.id.to_string(), + id + ); + + Ok(Redirect::to(uri!("/account", index)).into()) +} + +#[get("/terminate")] +pub fn ask_terminate_account(_user: User, meta: CommonTemplateState) -> Template { + Template::render("account/terminate", context! {meta}) +} + +#[get("/termin@or")] +pub async fn confirm_terminate_account( + mut db: DollTagsDb, + user: User, + cookies: &CookieJar<'_>, + client_ip: IpAddr, +) -> PageResult { + let mut trx = db.begin().await?; + doll::delete_all_from_account(&mut trx, &user.id).await?; + user::delete(&mut trx, &user.id).await?; + session::logout(cookies); + trx.commit().await?; + + warn!( + "[audit|{}] [{}] deleted account", + client_ip, + user.id.to_string() + ); + + Ok(Redirect::to("/").into()) +} diff --git a/src/routes/account/mod.rs b/src/routes/account/mod.rs new file mode 100644 index 0000000..732325d --- /dev/null +++ b/src/routes/account/mod.rs @@ -0,0 +1,3 @@ +pub mod common; +pub mod otp; +pub mod settings; diff --git a/src/routes/account/otp.rs b/src/routes/account/otp.rs new file mode 100644 index 0000000..6ed8274 --- /dev/null +++ b/src/routes/account/otp.rs @@ -0,0 +1,6 @@ +use rocket_dyn_templates::{context, Template}; + +#[get("/settings/otp")] +pub fn show_otp_enable_start() -> Template { + Template::render("account/otp/start", context! {}) +} diff --git a/src/routes/account.rs b/src/routes/account/settings.rs similarity index 56% rename from src/routes/account.rs rename to src/routes/account/settings.rs index 0244ac0..c4805ff 100644 --- a/src/routes/account.rs +++ b/src/routes/account/settings.rs @@ -1,67 +1,42 @@ use std::net::IpAddr; -use qrcode_generator::QrCodeEcc; use rocket::{ form::{self, Contextual, Error, Form}, - http::{uri::Absolute, CookieJar}, response::Redirect, - serde::{json::Json, Serialize}, tokio::task, - State, }; use rocket_dyn_templates::{context, Template}; -use sqlx::Acquire; use crate::{ - auth::{pw, session}, - config::Config, + auth::pw, db::{ - doll, - schema::{DollProfile, DollTagsDb, User}, + otp, + schema::{DollTagsDb, User}, user, }, pages::CommonTemplateState, + routes::error_handlers::PageResult, }; -use super::error_handlers::{PageResult, RawResult}; - -#[get("/")] -pub async fn index(mut db: DollTagsDb, user: User, meta: CommonTemplateState) -> PageResult { - let tags = doll::list(&mut *db, &user.id).await?; - let archived_tags = doll::list_archived(&mut *db, &user.id).await?; +#[get("/settings")] +pub async fn show_settings( + mut db: DollTagsDb, + user: User, + meta: CommonTemplateState, +) -> PageResult { + let has_otp = otp::has_otp(&mut *db, &user.id).await?; Ok(Template::render( - "account/index", - context! { - meta, - user, - tags, - archived_tags, - }, - ) - .into()) -} - -#[get("/qr-png/")] -pub fn qr_profile(id: &str, _user: User, config: &State) -> PageResult { - let public_uri = Absolute::parse(&config.public_url)?; - let built_uri = uri!(public_uri, crate::routes::public::short_url(id)); - let image = qrcode_generator::to_png_to_vec(built_uri.to_string(), QrCodeEcc::Low, 400)?; - - Ok(image.into()) -} - -#[get("/settings")] -pub fn show_settings(user: User, meta: CommonTemplateState) -> Template { - Template::render( "account/settings", context! { user, meta, + otp: has_otp, prev_common: form::Context::default(), prev_password: form::Context::default(), }, ) + .into()) } #[derive(FromForm)] @@ -209,108 +184,3 @@ pub async fn change_password( Ok(Redirect::to(uri!("/account", show_settings)).into()) } - -#[derive(Debug, Serialize)] -#[serde(crate = "rocket::serde")] -pub struct DataDump { - account: User, - tags: Vec, - reserved_tags: Vec, -} - -#[get("/data_dump")] -pub async fn export_data( - mut db: DollTagsDb, - user: User, - client_ip: IpAddr, -) -> RawResult> { - let tags = doll::list_all(&mut *db, &user.id).await?; - let reserved_tags = doll::list_archived(&mut *db, &user.id).await?; - - warn!( - "[audit|{}] [{}] exported data", - client_ip, - user.id.to_string() - ); - - Ok(Json(DataDump { - account: user, - tags, - reserved_tags, - })) -} - -#[get("/delete/")] -pub async fn ask_delete( - mut db: DollTagsDb, - id: i32, - user: User, - meta: CommonTemplateState, -) -> PageResult { - let db_tag = doll::get(&mut *db, id, "", false).await?; - - if let Some(tag) = db_tag { - if tag.bound_to_id != user.id { - Ok(Redirect::to(uri!("/account", index)).into()) - } else { - Ok(Template::render( - "tag/delete", - context! { - meta, - tag, - }, - ) - .into()) - } - } else { - Ok(Redirect::to(uri!("/account", index)).into()) - } -} - -#[get("/yes_delete_this/")] -pub async fn confirm_delete( - mut db: DollTagsDb, - id: i32, - user: User, - client_ip: IpAddr, -) -> PageResult { - let mut trx = db.begin().await?; - doll::delete(&mut trx, id, &user.id).await?; - trx.commit().await?; - - warn!( - "[audit|{}] [{}] deleted tag {:0>6}", - client_ip, - user.id.to_string(), - id - ); - - Ok(Redirect::to(uri!("/account", index)).into()) -} - -#[get("/terminate")] -pub fn ask_terminate_account(_user: User, meta: CommonTemplateState) -> Template { - Template::render("account/terminate", context! {meta}) -} - -#[get("/termin@or")] -pub async fn confirm_terminate_account( - mut db: DollTagsDb, - user: User, - cookies: &CookieJar<'_>, - client_ip: IpAddr, -) -> PageResult { - let mut trx = db.begin().await?; - doll::delete_all_from_account(&mut trx, &user.id).await?; - user::delete(&mut trx, &user.id).await?; - session::logout(cookies); - trx.commit().await?; - - warn!( - "[audit|{}] [{}] deleted account", - client_ip, - user.id.to_string() - ); - - Ok(Redirect::to("/").into()) -} diff --git a/src/routes/form/register_tag.rs b/src/routes/form/register_tag.rs index 8720764..4c73c09 100644 --- a/src/routes/form/register_tag.rs +++ b/src/routes/form/register_tag.rs @@ -85,17 +85,17 @@ pub async fn show_edit_tag( ) -> PageResult { let normalized_id = match id_public_to_db(id) { Some(v) => v, - None => return Ok(Redirect::to(uri!("/account", account::index)).into()), + None => return Ok(Redirect::to(uri!("/account", account::common::index)).into()), }; let tag = match doll::get(&mut *db, normalized_id, "", true).await? { Some(v) => { if v.bound_to_id != user.id { - return Ok(Redirect::to(uri!("/account", account::index)).into()); + return Ok(Redirect::to(uri!("/account", account::common::index)).into()); } v } - None => return Ok(Redirect::to(uri!("/account", account::index)).into()), + None => return Ok(Redirect::to(uri!("/account", account::common::index)).into()), }; Ok(Template::render( @@ -262,7 +262,9 @@ pub async fn handle_edit_tag( meta: CommonTemplateState, ) -> PageResult { let id = match id_public_to_db(id) { - None => return Ok(Redirect::to(uri!("/account", crate::routes::account::index)).into()), + None => { + return Ok(Redirect::to(uri!("/account", crate::routes::account::common::index)).into()) + } Some(v) => v, }; let tag = match tag.value { diff --git a/templates/account/otp/start.html.tera b/templates/account/otp/start.html.tera new file mode 100644 index 0000000..e69de29 diff --git a/templates/account/settings.html.tera b/templates/account/settings.html.tera index ac20efd..828cb9f 100644 --- a/templates/account/settings.html.tera +++ b/templates/account/settings.html.tera @@ -66,6 +66,21 @@ +
+ {% if otp %} +

Wow, you already have OTP enabled even though it's not yet implemented.

+ {% else %} +

Two-factor authentication

+ +

+ You don't have two-factor auth enabled.
+ You can add one using your authenticator app of choice by clicking below. +

+ + Enable 2FA with an authenticator + {% endif %} +
+

Exporting your data