From bf51f623e47cb2877630ab63838b1e0847e0a803 Mon Sep 17 00:00:00 2001 From: Radical Date: Fri, 23 May 2025 12:55:27 +0200 Subject: [PATCH] feat: migrate to diesel and new error type in auth --- src/api/v1/auth/login.rs | 152 +++++++++++------------------------- src/api/v1/auth/mod.rs | 53 ++++++------- src/api/v1/auth/refresh.rs | 75 ++++++++---------- src/api/v1/auth/register.rs | 123 ++++++++++------------------- src/api/v1/auth/revoke.rs | 105 +++++-------------------- 5 files changed, 162 insertions(+), 346 deletions(-) diff --git a/src/api/v1/auth/login.rs b/src/api/v1/auth/login.rs index 38d5449..8ad345e 100644 --- a/src/api/v1/auth/login.rs +++ b/src/api/v1/auth/login.rs @@ -1,14 +1,14 @@ use std::time::{SystemTime, UNIX_EPOCH}; -use actix_web::{Error, HttpResponse, post, web}; +use actix_web::{HttpResponse, post, web}; use argon2::{PasswordHash, PasswordVerifier}; -use log::error; +use diesel::{dsl::insert_into, ExpressionMethods, QueryDsl}; +use diesel_async::RunQueryDsl; use serde::Deserialize; +use uuid::Uuid; use crate::{ - Data, - api::v1::auth::{EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX}, - utils::{generate_access_token, generate_refresh_token, refresh_token_cookie}, + error::Error, api::v1::auth::{EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX}, schema::*, utils::{generate_access_token, generate_refresh_token, refresh_token_cookie}, Data }; use super::Response; @@ -29,66 +29,42 @@ pub async fn response( return Ok(HttpResponse::Forbidden().json(r#"{ "password_hashed": false }"#)); } + use users::dsl; + + let mut conn = data.pool.get().await?; + if EMAIL_REGEX.is_match(&login_information.username) { - let row = - sqlx::query_as("SELECT CAST(uuid as VARCHAR), password FROM users WHERE email = $1") - .bind(&login_information.username) - .fetch_one(&data.pool) - .await; + // FIXME: error handling, right now i just want this to work + let (uuid, password): (Uuid, String) = dsl::users + .filter(dsl::email.eq(&login_information.username)) + .select((dsl::uuid, dsl::password)) + .get_result(&mut conn) + .await?; - if let Err(error) = row { - if error.to_string() - == "no rows returned by a query that expected to return at least one row" - { - return Ok(HttpResponse::Unauthorized().finish()); - } - - error!("{}", error); - return Ok(HttpResponse::InternalServerError().json( - r#"{ "error": "Unhandled exception occured, contact the server administrator" }"#, - )); - } - - let (uuid, password): (String, String) = row.unwrap(); - - return Ok(login( + return login( data.clone(), uuid, login_information.password.clone(), password, login_information.device_name.clone(), ) - .await); + .await; } else if USERNAME_REGEX.is_match(&login_information.username) { - let row = - sqlx::query_as("SELECT CAST(uuid as VARCHAR), password FROM users WHERE username = $1") - .bind(&login_information.username) - .fetch_one(&data.pool) - .await; + // FIXME: error handling, right now i just want this to work + let (uuid, password): (Uuid, String) = dsl::users + .filter(dsl::username.eq(&login_information.username)) + .select((dsl::uuid, dsl::password)) + .get_result(&mut conn) + .await?; - if let Err(error) = row { - if error.to_string() - == "no rows returned by a query that expected to return at least one row" - { - return Ok(HttpResponse::Unauthorized().finish()); - } - - error!("{}", error); - return Ok(HttpResponse::InternalServerError().json( - r#"{ "error": "Unhandled exception occured, contact the server administrator" }"#, - )); - } - - let (uuid, password): (String, String) = row.unwrap(); - - return Ok(login( + return login( data.clone(), uuid, login_information.password.clone(), password, login_information.device_name.clone(), ) - .await); + .await; } Ok(HttpResponse::Unauthorized().finish()) @@ -96,79 +72,45 @@ pub async fn response( async fn login( data: actix_web::web::Data, - uuid: String, + uuid: Uuid, request_password: String, database_password: String, device_name: String, -) -> HttpResponse { - let parsed_hash_raw = PasswordHash::new(&database_password); +) -> Result { + let mut conn = data.pool.get().await?; - if let Err(error) = parsed_hash_raw { - error!("{}", error); - return HttpResponse::InternalServerError().finish(); - } - - let parsed_hash = parsed_hash_raw.unwrap(); + let parsed_hash = PasswordHash::new(&database_password).map_err(|e| Error::PasswordHashError(e.to_string()))?; if data .argon2 .verify_password(request_password.as_bytes(), &parsed_hash) .is_err() { - return HttpResponse::Unauthorized().finish(); + return Err(Error::Unauthorized("Wrong username or password".to_string())); } - let refresh_token_raw = generate_refresh_token(); - let access_token_raw = generate_access_token(); - - if let Err(error) = refresh_token_raw { - error!("{}", error); - return HttpResponse::InternalServerError().finish(); - } - - let refresh_token = refresh_token_raw.unwrap(); - - if let Err(error) = access_token_raw { - error!("{}", error); - return HttpResponse::InternalServerError().finish(); - } - - let access_token = access_token_raw.unwrap(); + let refresh_token = generate_refresh_token()?; + let access_token = generate_access_token()?; let current_time = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() + .duration_since(UNIX_EPOCH)? .as_secs() as i64; - if let Err(error) = sqlx::query(&format!( - "INSERT INTO refresh_tokens (token, uuid, created_at, device_name) VALUES ($1, '{}', $2, $3 )", - uuid - )) - .bind(&refresh_token) - .bind(current_time) - .bind(device_name) - .execute(&data.pool) - .await - { - error!("{}", error); - return HttpResponse::InternalServerError().finish(); - } + use refresh_tokens::dsl as rdsl; - if let Err(error) = sqlx::query(&format!( - "INSERT INTO access_tokens (token, refresh_token, uuid, created_at) VALUES ($1, $2, '{}', $3 )", - uuid - )) - .bind(&access_token) - .bind(&refresh_token) - .bind(current_time) - .execute(&data.pool) - .await - { - error!("{}", error); - return HttpResponse::InternalServerError().finish() - } + insert_into(refresh_tokens::table) + .values((rdsl::token.eq(&refresh_token), rdsl::uuid.eq(uuid), rdsl::created_at.eq(current_time), rdsl::device_name.eq(device_name))) + .execute(&mut conn) + .await?; - HttpResponse::Ok() + use access_tokens::dsl as adsl; + + insert_into(access_tokens::table) + .values((adsl::token.eq(&access_token), adsl::refresh_token.eq(&refresh_token), adsl::uuid.eq(uuid), adsl::created_at.eq(current_time))) + .execute(&mut conn) + .await?; + + Ok(HttpResponse::Ok() .cookie(refresh_token_cookie(refresh_token)) - .json(Response { access_token }) + .json(Response { access_token })) } diff --git a/src/api/v1/auth/mod.rs b/src/api/v1/auth/mod.rs index 326b2ef..249ec4b 100644 --- a/src/api/v1/auth/mod.rs +++ b/src/api/v1/auth/mod.rs @@ -1,16 +1,17 @@ use std::{ - str::FromStr, sync::LazyLock, time::{SystemTime, UNIX_EPOCH}, }; -use actix_web::{HttpResponse, Scope, web}; -use log::error; +use actix_web::{Scope, web}; +use diesel::{ExpressionMethods, QueryDsl}; +use diesel_async::RunQueryDsl; use regex::Regex; use serde::Serialize; -use sqlx::Postgres; use uuid::Uuid; +use crate::{error::Error, Conn, schema::access_tokens::dsl}; + mod login; mod refresh; mod register; @@ -40,40 +41,30 @@ pub fn web() -> Scope { pub async fn check_access_token( access_token: &str, - pool: &sqlx::Pool, -) -> Result { - let row = sqlx::query_as( - "SELECT CAST(uuid as VARCHAR), created_at FROM access_tokens WHERE token = $1", - ) - .bind(access_token) - .fetch_one(pool) - .await; - - if let Err(error) = row { - if error.to_string() - == "no rows returned by a query that expected to return at least one row" - { - return Err(HttpResponse::Unauthorized().finish()); - } - - error!("{}", error); - return Err(HttpResponse::InternalServerError().json( - r#"{ "error": "Unhandled exception occured, contact the server administrator" }"#, - )); - } - - let (uuid, created_at): (String, i64) = row.unwrap(); + conn: &mut Conn, +) -> Result { + let (uuid, created_at): (Uuid, i64) = dsl::access_tokens + .filter(dsl::token.eq(access_token)) + .select((dsl::uuid, dsl::created_at)) + .get_result(conn) + .await + .map_err(|error| { + if error == diesel::result::Error::NotFound { + Error::Unauthorized("Invalid access token".to_string()) + } else { + Error::from(error) + } + })?; let current_time = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() + .duration_since(UNIX_EPOCH)? .as_secs() as i64; let lifetime = current_time - created_at; if lifetime > 3600 { - return Err(HttpResponse::Unauthorized().finish()); + return Err(Error::Unauthorized("Invalid access token".to_string())); } - Ok(Uuid::from_str(&uuid).unwrap()) + Ok(uuid) } diff --git a/src/api/v1/auth/refresh.rs b/src/api/v1/auth/refresh.rs index cf1c4bb..468945d 100644 --- a/src/api/v1/auth/refresh.rs +++ b/src/api/v1/auth/refresh.rs @@ -1,10 +1,11 @@ -use actix_web::{Error, HttpRequest, HttpResponse, post, web}; +use actix_web::{HttpRequest, HttpResponse, post, web}; +use diesel::{delete, update, ExpressionMethods, QueryDsl}; +use diesel_async::RunQueryDsl; use log::error; use std::time::{SystemTime, UNIX_EPOCH}; use crate::{ - Data, - utils::{generate_access_token, generate_refresh_token, refresh_token_cookie}, + error::Error, schema::{access_tokens::{self, dsl}, refresh_tokens::{self, dsl as rdsl}}, utils::{generate_access_token, generate_refresh_token, refresh_token_cookie}, Data }; use super::Response; @@ -20,23 +21,23 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result(&mut conn) .await { - let created_at: i64 = row; - let lifetime = current_time - created_at; if lifetime > 2592000 { - if let Err(error) = sqlx::query("DELETE FROM refresh_tokens WHERE token = $1") - .bind(&refresh_token) - .execute(&data.pool) + if let Err(error) = delete(refresh_tokens::table) + .filter(rdsl::token.eq(&refresh_token)) + .execute(&mut conn) .await { error!("{}", error); @@ -52,8 +53,7 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result 1987200 { @@ -66,14 +66,14 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result { refresh_token = new_refresh_token; @@ -84,27 +84,16 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result { - let refresh_token = generate_refresh_token(); - let access_token = generate_access_token(); + .execute(&mut conn) + .await?; - if refresh_token.is_err() { - error!("{}", refresh_token.unwrap_err()); - return Ok(HttpResponse::InternalServerError().finish()); - } + let refresh_token = generate_refresh_token()?; + let access_token = generate_access_token()?; - let refresh_token = refresh_token.unwrap(); + let current_time = SystemTime::now() + .duration_since(UNIX_EPOCH)? + .as_secs() as i64; - if access_token.is_err() { - error!("{}", access_token.unwrap_err()); - return Ok(HttpResponse::InternalServerError().finish()); - } + insert_into(refresh_tokens::table) + .values(( + rdsl::token.eq(&refresh_token), + rdsl::uuid.eq(uuid), + rdsl::created_at.eq(current_time), + rdsl::device_name.eq(&account_information.device_name), + )) + .execute(&mut conn) + .await?; - let access_token = access_token.unwrap(); + insert_into(access_tokens::table) + .values(( + adsl::token.eq(&access_token), + adsl::refresh_token.eq(&refresh_token), + adsl::uuid.eq(uuid), + adsl::created_at.eq(current_time), + )) + .execute(&mut conn) + .await?; - let current_time = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() as i64; - - if let Err(error) = sqlx::query(&format!("INSERT INTO refresh_tokens (token, uuid, created_at, device_name) VALUES ($1, '{}', $2, $3 )", uuid)) - .bind(&refresh_token) - .bind(current_time) - .bind(&account_information.device_name) - .execute(&data.pool) - .await { - error!("{}", error); - return Ok(HttpResponse::InternalServerError().finish()) - } - - if let Err(error) = sqlx::query(&format!("INSERT INTO access_tokens (token, refresh_token, uuid, created_at) VALUES ($1, $2, '{}', $3 )", uuid)) - .bind(&access_token) - .bind(&refresh_token) - .bind(current_time) - .execute(&data.pool) - .await { - error!("{}", error); - return Ok(HttpResponse::InternalServerError().finish()) - } - - HttpResponse::Ok() - .cookie(refresh_token_cookie(refresh_token)) - .json(Response { access_token }) - } - Err(error) => { - let err_msg = error.as_database_error().unwrap().message(); - - match err_msg { - err_msg - if err_msg.contains("unique") && err_msg.contains("username_key") => - { - HttpResponse::Forbidden().json(ResponseError { - gorb_id_available: false, - ..Default::default() - }) - } - err_msg if err_msg.contains("unique") && err_msg.contains("email_key") => { - HttpResponse::Forbidden().json(ResponseError { - email_available: false, - ..Default::default() - }) - } - _ => { - error!("{}", err_msg); - HttpResponse::InternalServerError().finish() - } - } - } - }, - ); + return Ok(HttpResponse::Ok() + .cookie(refresh_token_cookie(refresh_token)) + .json(Response { access_token })) } Ok(HttpResponse::InternalServerError().finish()) diff --git a/src/api/v1/auth/revoke.rs b/src/api/v1/auth/revoke.rs index a4f9196..116ed5c 100644 --- a/src/api/v1/auth/revoke.rs +++ b/src/api/v1/auth/revoke.rs @@ -1,10 +1,10 @@ -use actix_web::{Error, HttpRequest, HttpResponse, post, web}; +use actix_web::{HttpRequest, HttpResponse, post, web}; use argon2::{PasswordHash, PasswordVerifier}; -use futures::future; -use log::error; -use serde::{Deserialize, Serialize}; +use diesel::{delete, ExpressionMethods, QueryDsl}; +use diesel_async::RunQueryDsl; +use serde::Deserialize; -use crate::{Data, api::v1::auth::check_access_token, utils::get_auth_header}; +use crate::{api::v1::auth::check_access_token, error::Error, schema::users::dsl as udsl, schema::refresh_tokens::{self, dsl as rdsl}, utils::get_auth_header, Data}; #[derive(Deserialize)] struct RevokeRequest { @@ -12,17 +12,6 @@ struct RevokeRequest { device_name: String, } -#[derive(Serialize)] -struct Response { - deleted: bool, -} - -impl Response { - fn new(deleted: bool) -> Self { - Self { deleted } - } -} - // TODO: Should maybe be a delete request? #[post("/revoke")] pub async fn res( @@ -32,85 +21,33 @@ pub async fn res( ) -> Result { let headers = req.headers(); - let auth_header = get_auth_header(headers); + let auth_header = get_auth_header(headers)?; - if let Err(error) = auth_header { - return Ok(error); - } + let mut conn = data.pool.get().await?; - let authorized = check_access_token(auth_header.unwrap(), &data.pool).await; + let uuid = check_access_token(auth_header, &mut conn).await?; - if let Err(error) = authorized { - return Ok(error); - } + let database_password: String = udsl::users + .filter(udsl::uuid.eq(uuid)) + .select(udsl::password) + .get_result(&mut conn) + .await?; - let uuid = authorized.unwrap(); - - let database_password_raw = sqlx::query_scalar(&format!( - "SELECT password FROM users WHERE uuid = '{}'", - uuid - )) - .fetch_one(&data.pool) - .await; - - if let Err(error) = database_password_raw { - error!("{}", error); - return Ok(HttpResponse::InternalServerError().json(Response::new(false))); - } - - let database_password: String = database_password_raw.unwrap(); - - let hashed_password_raw = PasswordHash::new(&database_password); - - if let Err(error) = hashed_password_raw { - error!("{}", error); - return Ok(HttpResponse::InternalServerError().json(Response::new(false))); - } - - let hashed_password = hashed_password_raw.unwrap(); + let hashed_password = PasswordHash::new(&database_password).map_err(|e| Error::PasswordHashError(e.to_string()))?; if data .argon2 .verify_password(revoke_request.password.as_bytes(), &hashed_password) .is_err() { - return Ok(HttpResponse::Unauthorized().finish()); + return Err(Error::Unauthorized("Wrong username or password".to_string())); } - let tokens_raw = sqlx::query_scalar(&format!( - "SELECT token FROM refresh_tokens WHERE uuid = '{}' AND device_name = $1", - uuid - )) - .bind(&revoke_request.device_name) - .fetch_all(&data.pool) - .await; + delete(refresh_tokens::table) + .filter(rdsl::uuid.eq(uuid)) + .filter(rdsl::device_name.eq(&revoke_request.device_name)) + .execute(&mut conn) + .await?; - if tokens_raw.is_err() { - error!("{:?}", tokens_raw); - return Ok(HttpResponse::InternalServerError().json(Response::new(false))); - } - - let tokens: Vec = tokens_raw.unwrap(); - - let mut refresh_tokens_delete = vec![]; - - for token in tokens { - refresh_tokens_delete.push( - sqlx::query("DELETE FROM refresh_tokens WHERE token = $1") - .bind(token.clone()) - .execute(&data.pool), - ); - } - - let results = future::join_all(refresh_tokens_delete).await; - - let errors: Vec<&Result> = - results.iter().filter(|r| r.is_err()).collect(); - - if !errors.is_empty() { - error!("{:?}", errors); - return Ok(HttpResponse::InternalServerError().finish()); - } - - Ok(HttpResponse::Ok().json(Response::new(true))) + Ok(HttpResponse::Ok().finish()) }