diff --git a/backend/src/auth/mod.rs b/backend/src/auth/mod.rs index 41f76e6..6cde613 100644 --- a/backend/src/auth/mod.rs +++ b/backend/src/auth/mod.rs @@ -6,7 +6,7 @@ pub mod two_factor; pub use session::Session; pub use account::{generate_invite, invite_page, login, login_page, signup, signup_page}; -pub use profile::{change_display_name, change_password}; +pub use profile::{change_display_name, change_password, change_username, delete_account}; pub use two_factor::{ confirm_totp, disable_totp, get_totp, get_totp_status, mfa_page, verify_totp, }; diff --git a/backend/src/auth/profile.rs b/backend/src/auth/profile.rs index cef0be5..a3eab8c 100644 --- a/backend/src/auth/profile.rs +++ b/backend/src/auth/profile.rs @@ -30,13 +30,7 @@ pub async fn change_password( ) })?; - let parsed_hash = PasswordHash::new(&user.pass_hash) - .inspect_err(|e| tracing::error!("Failed to parse hash for password! uid:{} {e}", user.id)) - .map_err(|_| Status::InternalServerError)?; - - Argon2::default() - .verify_password(form.old_password.as_bytes(), &parsed_hash) - .map_err(|_| Status::Unauthorized)?; + user.verify_password(&form.old_password)?; // old password is correct, so new one can be set. let salt = SaltString::generate(&mut OsRng); @@ -59,7 +53,43 @@ pub struct DisplayNameForm { pub display_name: Option, } -#[post("/settings/display_name", data = "")] +#[derive(Deserialize, Debug, Clone)] +pub struct PasswordAnd2fa { + pub password: String, + pub totp_code: Option, +} + +#[delete("/settings", data = "")] +pub async fn delete_account( + session: Session, + mut db: Connection, + data: Json, +) -> Result<(), Status> { + let mut user = User::get_by_id(session.user_id, &mut db) + .await + .ok_or(Status::NotFound) + .inspect_err(|_| { + tracing::error!( + "Valid session does not have a valid user. ID: {}", + session.user_id + ) + })?; + + user.verify_password(&data.password)?; + + if user.twofa_enabled { + user.verify_2fa(data.totp_code.as_deref().unwrap_or(""))?; + } + + user.delete(&mut db) + .await + .inspect_err(|e| tracing::error!("{e}")) + .map_err(|_| Status::InternalServerError)?; + + Ok(()) +} + +#[patch("/settings/display_name", data = "")] pub async fn change_display_name( session: Session, mut db: Connection, @@ -82,3 +112,32 @@ pub async fn change_display_name( Ok(()) } + +#[derive(Deserialize)] +pub struct UsernameForm { + username: String, +} + +#[patch("/settings/username", data = "")] +pub async fn change_username( + session: Session, + mut db: Connection, + new: Json, +) -> Result<(), Status> { + let mut user = User::get_by_id(session.user_id, &mut db) + .await + .ok_or(Status::NotFound) + .inspect_err(|_| { + tracing::error!( + "Valid session does not have a valid user. ID: {}", + session.user_id + ) + })?; + + user.set_username(new.username.clone(), &mut db) + .await + .inspect_err(|e| tracing::error!("{e}")) + .map_err(|_| Status::InternalServerError)?; + + Ok(()) +} diff --git a/backend/src/auth/two_factor.rs b/backend/src/auth/two_factor.rs index 6491689..c0fe8aa 100644 --- a/backend/src/auth/two_factor.rs +++ b/backend/src/auth/two_factor.rs @@ -1,3 +1,4 @@ +use futures_util::TryFutureExt; use rocket::{ Request, http::Status, @@ -14,9 +15,11 @@ use totp_rs::{Algorithm, Secret, TOTP}; use crate::{ auth::{ account::AuthResponse, + profile::PasswordAnd2fa, session::{Claims, Session, TokenScope}, }, db::Postgres, + user::User, }; // Utility methods @@ -79,8 +82,16 @@ pub async fn confirm_totp( Ok(()) } -#[get("/totp.jpg")] -pub async fn get_totp(mfa: TOTPSecret) -> Option> { +#[derive(Deserialize)] +pub struct PasswordConfirmation { + password: String, +} + +#[post("/totp.jpg", data = "
")] +pub async fn get_totp( + mfa: TOTPSecret, + form: Json, +) -> Option> { let qr_b64 = totp_gen(mfa.user_id, mfa.secret.as_bytes()) .expect("Invalid TOTP") .get_qr_base64() @@ -216,35 +227,39 @@ pub async fn get_totp_status( )) } -#[delete("/totp")] +#[delete("/totp", data = "")] pub async fn disable_totp( user: Session, mut db: Connection, + form: Json, ) -> Result, Status> { - sqlx::query!( - "UPDATE users SET twofa_enabled = false, totp_secret = NULL WHERE id = $1", - user.user_id as i32, - ) - .execute(&mut **db) - .await - .map_err(|_| Status::NotFound)?; + let totp_code = form.totp_code.clone().ok_or(Status::BadRequest)?; + let mut user = User::get_by_id(user.user_id, &mut db) + .await + .ok_or(Status::NotFound)?; + + user.verify_password(&form.password)?; + user.verify_2fa(&totp_code)?; + user.set_twofa_enabled(false, &mut db) + .await + .map_err(|_| Status::InternalServerError)?; Ok(Json(AuthResponse { - token: Claims::new(user.user_id, TokenScope::Full).encode(), + token: Claims::new(user.id as usize, TokenScope::Full).encode(), totp_required: false, })) } #[post("/totp/verify", data = "")] pub async fn verify_totp( - user: Claims, // request guard checks token validity + claims: Claims, // request guard checks token validity mut db: Connection, body: Json, ) -> Result, Status> { println!("reached 1"); // reject if they somehow got here with a full token - if user.scope != TokenScope::TotpPending { + if claims.scope != TokenScope::TotpPending { return Err(Status::Forbidden); } @@ -252,7 +267,7 @@ pub async fn verify_totp( let row = sqlx::query!( "SELECT totp_secret FROM users WHERE id = $1 AND twofa_enabled = TRUE", - user.sub + claims.sub ) .fetch_one(&mut **db) .await @@ -261,7 +276,7 @@ pub async fn verify_totp( println!("reached 3"); let totp = totp_gen( - user.sub as usize, + claims.sub as usize, row.totp_secret .expect("user with 2fa enabled has no totp secret") .as_bytes(), @@ -277,7 +292,7 @@ pub async fn verify_totp( println!("reached 5"); - let claims = Claims::new(user.sub as usize, TokenScope::Full); + let claims = Claims::new(claims.sub as usize, TokenScope::Full); Ok(Json(AuthResponse { token: claims.encode(), diff --git a/backend/src/main.rs b/backend/src/main.rs index 4d4fa55..22de1c1 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -77,7 +77,9 @@ fn rocket() -> Rocket { auth::disable_totp, auth::get_totp_status, auth::change_password, - auth::change_display_name + auth::change_display_name, + auth::change_username, + auth::delete_account, ], ) .register( diff --git a/backend/src/user.rs b/backend/src/user.rs index 72223ae..7934936 100644 --- a/backend/src/user.rs +++ b/backend/src/user.rs @@ -1,9 +1,10 @@ +use argon2::{Argon2, PasswordHash, PasswordVerifier}; use redis::AsyncCommands; -use rocket::{serde::json::Json, time::OffsetDateTime}; +use rocket::{http::Status, serde::json::Json, time::OffsetDateTime}; use rocket_db_pools::Connection; use crate::{ - auth::Session, + auth::{Session, two_factor::totp_gen}, db::{Postgres, Redis}, }; @@ -31,6 +32,43 @@ impl User { .unwrap_or(None) } + pub async fn delete(&mut self, db: &mut Connection) -> Result<(), sqlx::Error> { + sqlx::query!("DELETE FROM users WHERE id = $1", self.id) + .execute(&mut ***db) + .await?; + Ok(()) + } + + pub fn verify_2fa(&self, code: &str) -> Result<(), Status> { + if totp_gen( + self.id as usize, + self.totp_secret + .clone() + .expect("user with 2fa enabled has no totp secret") + .as_bytes(), + ) + .map_err(|_| Status::InternalServerError)? + .check_current(code) + .map_err(|_| Status::InternalServerError)? + { + Ok(()) + } else { + Err(Status::Unauthorized) + } + } + + pub fn verify_password(&self, password: &str) -> Result<(), Status> { + let parsed_hash = PasswordHash::new(&self.pass_hash) + .inspect_err(|e| { + tracing::error!("Failed to parse hash for password! uid:{} {e}", self.id) + }) + .map_err(|_| Status::InternalServerError)?; + + Argon2::default() + .verify_password(password.as_bytes(), &parsed_hash) + .map_err(|_| Status::Unauthorized) + } + pub async fn set_display_name( &mut self, display_name: Option, @@ -47,6 +85,38 @@ impl User { Ok(()) } + pub async fn set_username( + &mut self, + username: String, + db: &mut Connection, + ) -> Result<(), sqlx::Error> { + self.username = username; + sqlx::query!( + "UPDATE users SET username = $1 WHERE id = $2", + self.username, + self.id + ) + .execute(&mut ***db) + .await?; + Ok(()) + } + + pub async fn set_twofa_enabled( + &mut self, + enabled: bool, + db: &mut Connection, + ) -> Result<(), sqlx::Error> { + self.twofa_enabled = enabled; + sqlx::query!( + "UPDATE users SET twofa_enabled = $1 WHERE id = $2", + self.twofa_enabled, + self.id + ) + .execute(&mut ***db) + .await?; + Ok(()) + } + pub async fn set_pass_hash( &mut self, pass_hash: String,