refactoring, proper User implementation, more settings endpoints. backend probably needs a full refactor to an API/Service/Repository architecture for maintainability
This commit is contained in:
@@ -6,7 +6,7 @@ pub mod two_factor;
|
|||||||
pub use session::Session;
|
pub use session::Session;
|
||||||
|
|
||||||
pub use account::{generate_invite, invite_page, login, login_page, signup, signup_page};
|
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::{
|
pub use two_factor::{
|
||||||
confirm_totp, disable_totp, get_totp, get_totp_status, mfa_page, verify_totp,
|
confirm_totp, disable_totp, get_totp, get_totp_status, mfa_page, verify_totp,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,13 +30,7 @@ pub async fn change_password(
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let parsed_hash = PasswordHash::new(&user.pass_hash)
|
user.verify_password(&form.old_password)?;
|
||||||
.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)?;
|
|
||||||
|
|
||||||
// old password is correct, so new one can be set.
|
// old password is correct, so new one can be set.
|
||||||
let salt = SaltString::generate(&mut OsRng);
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
@@ -59,7 +53,43 @@ pub struct DisplayNameForm {
|
|||||||
pub display_name: Option<String>,
|
pub display_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/settings/display_name", data = "<new>")]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub struct PasswordAnd2fa {
|
||||||
|
pub password: String,
|
||||||
|
pub totp_code: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[delete("/settings", data = "<data>")]
|
||||||
|
pub async fn delete_account(
|
||||||
|
session: Session,
|
||||||
|
mut db: Connection<Postgres>,
|
||||||
|
data: Json<PasswordAnd2fa>,
|
||||||
|
) -> 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 = "<new>")]
|
||||||
pub async fn change_display_name(
|
pub async fn change_display_name(
|
||||||
session: Session,
|
session: Session,
|
||||||
mut db: Connection<Postgres>,
|
mut db: Connection<Postgres>,
|
||||||
@@ -82,3 +112,32 @@ pub async fn change_display_name(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct UsernameForm {
|
||||||
|
username: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[patch("/settings/username", data = "<new>")]
|
||||||
|
pub async fn change_username(
|
||||||
|
session: Session,
|
||||||
|
mut db: Connection<Postgres>,
|
||||||
|
new: Json<UsernameForm>,
|
||||||
|
) -> 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(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use futures_util::TryFutureExt;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
Request,
|
Request,
|
||||||
http::Status,
|
http::Status,
|
||||||
@@ -14,9 +15,11 @@ use totp_rs::{Algorithm, Secret, TOTP};
|
|||||||
use crate::{
|
use crate::{
|
||||||
auth::{
|
auth::{
|
||||||
account::AuthResponse,
|
account::AuthResponse,
|
||||||
|
profile::PasswordAnd2fa,
|
||||||
session::{Claims, Session, TokenScope},
|
session::{Claims, Session, TokenScope},
|
||||||
},
|
},
|
||||||
db::Postgres,
|
db::Postgres,
|
||||||
|
user::User,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Utility methods
|
// Utility methods
|
||||||
@@ -79,8 +82,16 @@ pub async fn confirm_totp(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/totp.jpg")]
|
#[derive(Deserialize)]
|
||||||
pub async fn get_totp(mfa: TOTPSecret) -> Option<Json<QrResponse>> {
|
pub struct PasswordConfirmation {
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/totp.jpg", data = "<form>")]
|
||||||
|
pub async fn get_totp(
|
||||||
|
mfa: TOTPSecret,
|
||||||
|
form: Json<PasswordConfirmation>,
|
||||||
|
) -> Option<Json<QrResponse>> {
|
||||||
let qr_b64 = totp_gen(mfa.user_id, mfa.secret.as_bytes())
|
let qr_b64 = totp_gen(mfa.user_id, mfa.secret.as_bytes())
|
||||||
.expect("Invalid TOTP")
|
.expect("Invalid TOTP")
|
||||||
.get_qr_base64()
|
.get_qr_base64()
|
||||||
@@ -216,35 +227,39 @@ pub async fn get_totp_status(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/totp")]
|
#[delete("/totp", data = "<form>")]
|
||||||
pub async fn disable_totp(
|
pub async fn disable_totp(
|
||||||
user: Session,
|
user: Session,
|
||||||
mut db: Connection<Postgres>,
|
mut db: Connection<Postgres>,
|
||||||
|
form: Json<PasswordAnd2fa>,
|
||||||
) -> Result<Json<AuthResponse>, Status> {
|
) -> Result<Json<AuthResponse>, Status> {
|
||||||
sqlx::query!(
|
let totp_code = form.totp_code.clone().ok_or(Status::BadRequest)?;
|
||||||
"UPDATE users SET twofa_enabled = false, totp_secret = NULL WHERE id = $1",
|
let mut user = User::get_by_id(user.user_id, &mut db)
|
||||||
user.user_id as i32,
|
.await
|
||||||
)
|
.ok_or(Status::NotFound)?;
|
||||||
.execute(&mut **db)
|
|
||||||
.await
|
user.verify_password(&form.password)?;
|
||||||
.map_err(|_| Status::NotFound)?;
|
user.verify_2fa(&totp_code)?;
|
||||||
|
user.set_twofa_enabled(false, &mut db)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Status::InternalServerError)?;
|
||||||
|
|
||||||
Ok(Json(AuthResponse {
|
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,
|
totp_required: false,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/totp/verify", data = "<body>")]
|
#[post("/totp/verify", data = "<body>")]
|
||||||
pub async fn verify_totp(
|
pub async fn verify_totp(
|
||||||
user: Claims, // request guard checks token validity
|
claims: Claims, // request guard checks token validity
|
||||||
mut db: Connection<Postgres>,
|
mut db: Connection<Postgres>,
|
||||||
body: Json<TotpVerifyRequest>,
|
body: Json<TotpVerifyRequest>,
|
||||||
) -> Result<Json<AuthResponse>, Status> {
|
) -> Result<Json<AuthResponse>, Status> {
|
||||||
println!("reached 1");
|
println!("reached 1");
|
||||||
|
|
||||||
// reject if they somehow got here with a full token
|
// reject if they somehow got here with a full token
|
||||||
if user.scope != TokenScope::TotpPending {
|
if claims.scope != TokenScope::TotpPending {
|
||||||
return Err(Status::Forbidden);
|
return Err(Status::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,7 +267,7 @@ pub async fn verify_totp(
|
|||||||
|
|
||||||
let row = sqlx::query!(
|
let row = sqlx::query!(
|
||||||
"SELECT totp_secret FROM users WHERE id = $1 AND twofa_enabled = TRUE",
|
"SELECT totp_secret FROM users WHERE id = $1 AND twofa_enabled = TRUE",
|
||||||
user.sub
|
claims.sub
|
||||||
)
|
)
|
||||||
.fetch_one(&mut **db)
|
.fetch_one(&mut **db)
|
||||||
.await
|
.await
|
||||||
@@ -261,7 +276,7 @@ pub async fn verify_totp(
|
|||||||
println!("reached 3");
|
println!("reached 3");
|
||||||
|
|
||||||
let totp = totp_gen(
|
let totp = totp_gen(
|
||||||
user.sub as usize,
|
claims.sub as usize,
|
||||||
row.totp_secret
|
row.totp_secret
|
||||||
.expect("user with 2fa enabled has no totp secret")
|
.expect("user with 2fa enabled has no totp secret")
|
||||||
.as_bytes(),
|
.as_bytes(),
|
||||||
@@ -277,7 +292,7 @@ pub async fn verify_totp(
|
|||||||
|
|
||||||
println!("reached 5");
|
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 {
|
Ok(Json(AuthResponse {
|
||||||
token: claims.encode(),
|
token: claims.encode(),
|
||||||
|
|||||||
+3
-1
@@ -77,7 +77,9 @@ fn rocket() -> Rocket<Build> {
|
|||||||
auth::disable_totp,
|
auth::disable_totp,
|
||||||
auth::get_totp_status,
|
auth::get_totp_status,
|
||||||
auth::change_password,
|
auth::change_password,
|
||||||
auth::change_display_name
|
auth::change_display_name,
|
||||||
|
auth::change_username,
|
||||||
|
auth::delete_account,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.register(
|
.register(
|
||||||
|
|||||||
+72
-2
@@ -1,9 +1,10 @@
|
|||||||
|
use argon2::{Argon2, PasswordHash, PasswordVerifier};
|
||||||
use redis::AsyncCommands;
|
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 rocket_db_pools::Connection;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::Session,
|
auth::{Session, two_factor::totp_gen},
|
||||||
db::{Postgres, Redis},
|
db::{Postgres, Redis},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,6 +32,43 @@ impl User {
|
|||||||
.unwrap_or(None)
|
.unwrap_or(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn delete(&mut self, db: &mut Connection<Postgres>) -> 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(
|
pub async fn set_display_name(
|
||||||
&mut self,
|
&mut self,
|
||||||
display_name: Option<String>,
|
display_name: Option<String>,
|
||||||
@@ -47,6 +85,38 @@ impl User {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn set_username(
|
||||||
|
&mut self,
|
||||||
|
username: String,
|
||||||
|
db: &mut Connection<Postgres>,
|
||||||
|
) -> 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<Postgres>,
|
||||||
|
) -> 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(
|
pub async fn set_pass_hash(
|
||||||
&mut self,
|
&mut self,
|
||||||
pass_hash: String,
|
pass_hash: String,
|
||||||
|
|||||||
Reference in New Issue
Block a user