started integrating the flow

This commit is contained in:
Artemis 2025-03-18 12:26:13 +01:00
parent e01df6266c
commit 77a681c02a
10 changed files with 220 additions and 157 deletions

View file

@ -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;

11
src/db/otp.rs Normal file
View file

@ -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<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)
}

View file

@ -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(

View file

@ -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/<id>")]
pub fn qr_profile(id: &str, _user: User, config: &State<Config>) -> 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<DollProfile>,
reserved_tags: Vec<i32>,
}
#[get("/data_dump")]
pub async fn export_data(
mut db: DollTagsDb,
user: User,
client_ip: IpAddr,
) -> RawResult<Json<DataDump>> {
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/<id>")]
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/<id>")]
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())
}

View file

@ -0,0 +1,3 @@
pub mod common;
pub mod otp;
pub mod settings;

View file

@ -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! {})
}

View file

@ -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/<id>")]
pub fn qr_profile(id: &str, _user: User, config: &State<Config>) -> 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<DollProfile>,
reserved_tags: Vec<i32>,
}
#[get("/data_dump")]
pub async fn export_data(
mut db: DollTagsDb,
user: User,
client_ip: IpAddr,
) -> RawResult<Json<DataDump>> {
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/<id>")]
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/<id>")]
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())
}

View file

@ -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 {

View file

View file

@ -66,6 +66,21 @@
</form>
</div>
<section id="otp">
{% if otp %}
<p>Wow, you already have OTP enabled even though it's not yet implemented.</p>
{% else %}
<h3>Two-factor authentication</h3>
<p>
You don't have two-factor auth enabled.<br />
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 %}
</section>
<section id="data-export">
<h3>Exporting your data</h3>