From cc17f2048f7d7ce4a12359e12aa8b84b8509c561 Mon Sep 17 00:00:00 2001 From: realaravinth Date: Mon, 28 Jun 2021 19:16:59 +0530 Subject: [PATCH] errorable and seperated runner methods for auth --- src/api/v1/account/delete.rs | 2 +- src/api/v1/account/test.rs | 2 +- src/api/v1/auth.rs | 236 +++++++++++++++++++---------------- src/api/v1/tests/auth.rs | 38 +++--- src/main.rs | 1 + src/pages/auth/login.rs | 48 ++++++- src/pages/auth/mod.rs | 1 + src/pages/errors.rs | 27 +++- src/tests/mod.rs | 2 +- 9 files changed, 222 insertions(+), 135 deletions(-) diff --git a/src/api/v1/account/delete.rs b/src/api/v1/account/delete.rs index 9539df06..d897d7c4 100644 --- a/src/api/v1/account/delete.rs +++ b/src/api/v1/account/delete.rs @@ -18,7 +18,7 @@ use actix_identity::Identity; use actix_web::{web, HttpResponse, Responder}; -use super::auth::Password; +use super::auth::runners::Password; use crate::errors::*; use crate::AppData; diff --git a/src/api/v1/account/test.rs b/src/api/v1/account/test.rs index a949f9f4..adffa590 100644 --- a/src/api/v1/account/test.rs +++ b/src/api/v1/account/test.rs @@ -20,7 +20,7 @@ use actix_web::test; use super::email::*; use super::*; -use crate::api::v1::auth::*; +use crate::api::v1::auth::runners::Password; use crate::api::v1::ROUTES; use crate::data::Data; use crate::*; diff --git a/src/api/v1/auth.rs b/src/api/v1/auth.rs index 69f8c022..b5869190 100644 --- a/src/api/v1/auth.rs +++ b/src/api/v1/auth.rs @@ -14,12 +14,10 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -use std::borrow::Cow; use actix_identity::Identity; use actix_web::http::header; use actix_web::{web, HttpResponse, Responder}; -use log::debug; use serde::{Deserialize, Serialize}; use super::mcaptcha::get_random; @@ -47,127 +45,145 @@ pub mod routes { } } +pub mod runners { + use std::borrow::Cow; + + use super::*; + + #[derive(Clone, Debug, Deserialize, Serialize)] + pub struct Register { + pub username: String, + pub password: String, + pub confirm_password: String, + pub email: Option, + } + + #[derive(Clone, Debug, Deserialize, Serialize)] + pub struct Login { + pub username: String, + pub password: String, + } + + #[derive(Clone, Debug, Deserialize, Serialize)] + pub struct Password { + pub password: String, + } + + /// returns Ok(()) when everything checks out and the user is authenticated. Erros otherwise + pub async fn login_runner(payload: &Login, data: &AppData) -> ServiceResult<()> { + use argon2_creds::Config; + use sqlx::Error::RowNotFound; + + let rec = sqlx::query_as!( + Password, + r#"SELECT password FROM mcaptcha_users WHERE name = ($1)"#, + &payload.username, + ) + .fetch_one(&data.db) + .await; + + match rec { + Ok(s) => { + if Config::verify(&s.password, &payload.password)? { + Ok(()) + } else { + Err(ServiceError::WrongPassword) + } + } + Err(RowNotFound) => Err(ServiceError::UsernameNotFound), + Err(_) => Err(ServiceError::InternalServerError), + } + } + + pub async fn register_runner( + payload: &Register, + data: &AppData, + ) -> ServiceResult<()> { + if !crate::SETTINGS.server.allow_registration { + return Err(ServiceError::ClosedForRegistration); + } + + if payload.password != payload.confirm_password { + return Err(ServiceError::PasswordsDontMatch); + } + let username = data.creds.username(&payload.username)?; + let hash = data.creds.password(&payload.password)?; + + if let Some(email) = &payload.email { + data.creds.email(&email)?; + } + + let mut secret; + + loop { + secret = get_random(32); + let res; + if let Some(email) = &payload.email { + res = sqlx::query!( + "INSERT INTO mcaptcha_users + (name , password, email, secret) VALUES ($1, $2, $3, $4)", + &username, + &hash, + &email, + &secret, + ) + .execute(&data.db) + .await; + } else { + res = sqlx::query!( + "INSERT INTO mcaptcha_users + (name , password, secret) VALUES ($1, $2, $3)", + &username, + &hash, + &secret, + ) + .execute(&data.db) + .await; + } + if res.is_ok() { + break; + } else if let Err(sqlx::Error::Database(err)) = res { + if err.code() == Some(Cow::from("23505")) { + let msg = err.message(); + if msg.contains("mcaptcha_users_name_key") { + return Err(ServiceError::UsernameTaken); + } else if msg.contains("mcaptcha_users_secret_key") { + continue; + } else { + return Err(ServiceError::InternalServerError); + } + } else { + return Err(sqlx::Error::Database(err).into()); + } + }; + } + Ok(()) + } +} + pub fn services(cfg: &mut web::ServiceConfig) { - cfg.service(signup); - cfg.service(signin); + cfg.service(register); + cfg.service(login); cfg.service(signout); } - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Register { - pub username: String, - pub password: String, - pub confirm_password: String, - pub email: Option, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Login { - pub username: String, - pub password: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Password { - pub password: String, -} - #[my_codegen::post(path = "crate::V1_API_ROUTES.auth.register")] -async fn signup( - payload: web::Json, +async fn register( + payload: web::Json, data: AppData, ) -> ServiceResult { - if !crate::SETTINGS.server.allow_registration { - return Err(ServiceError::ClosedForRegistration); - } - - if payload.password != payload.confirm_password { - return Err(ServiceError::PasswordsDontMatch); - } - let username = data.creds.username(&payload.username)?; - let hash = data.creds.password(&payload.password)?; - - if let Some(email) = &payload.email { - data.creds.email(&email)?; - } - - let mut secret; - - loop { - secret = get_random(32); - let res; - if let Some(email) = &payload.email { - res = sqlx::query!( - "INSERT INTO mcaptcha_users - (name , password, email, secret) VALUES ($1, $2, $3, $4)", - &username, - &hash, - &email, - &secret, - ) - .execute(&data.db) - .await; - } else { - res = sqlx::query!( - "INSERT INTO mcaptcha_users - (name , password, secret) VALUES ($1, $2, $3)", - &username, - &hash, - &secret, - ) - .execute(&data.db) - .await; - } - if res.is_ok() { - break; - } else if let Err(sqlx::Error::Database(err)) = res { - if err.code() == Some(Cow::from("23505")) { - let msg = err.message(); - if msg.contains("mcaptcha_users_name_key") { - return Err(ServiceError::UsernameTaken); - } else if msg.contains("mcaptcha_users_secret_key") { - continue; - } else { - return Err(ServiceError::InternalServerError); - } - } else { - return Err(sqlx::Error::Database(err).into()); - } - }; - } + runners::register_runner(&payload, &data).await?; Ok(HttpResponse::Ok()) } #[my_codegen::post(path = "crate::V1_API_ROUTES.auth.login")] -async fn signin( +async fn login( id: Identity, - payload: web::Json, + payload: web::Json, data: AppData, ) -> ServiceResult { - use argon2_creds::Config; - use sqlx::Error::RowNotFound; - - let rec = sqlx::query_as!( - Password, - r#"SELECT password FROM mcaptcha_users WHERE name = ($1)"#, - &payload.username, - ) - .fetch_one(&data.db) - .await; - - match rec { - Ok(s) => { - if Config::verify(&s.password, &payload.password)? { - debug!("remembered {}", payload.username); - id.remember(payload.into_inner().username); - Ok(HttpResponse::Ok()) - } else { - Err(ServiceError::WrongPassword) - } - } - Err(RowNotFound) => Err(ServiceError::UsernameNotFound), - Err(_) => Err(ServiceError::InternalServerError), - } + runners::login_runner(&payload, &data).await?; + id.remember(payload.into_inner().username); + Ok(HttpResponse::Ok()) } #[my_codegen::get(path = "crate::V1_API_ROUTES.auth.logout", wrap = "crate::CheckLogin")] diff --git a/src/api/v1/tests/auth.rs b/src/api/v1/tests/auth.rs index 9ec79a1a..03014184 100644 --- a/src/api/v1/tests/auth.rs +++ b/src/api/v1/tests/auth.rs @@ -18,7 +18,7 @@ use actix_web::http::{header, StatusCode}; use actix_web::test; -use crate::api::v1::auth::*; +use crate::api::v1::auth::runners::{Login, Register}; use crate::api::v1::ROUTES; use crate::data::Data; use crate::errors::*; @@ -57,17 +57,6 @@ async fn auth_works() { let (_, _, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await; let cookies = get_cookie!(signin_resp); - // // check if update user secret works - // let resp = test::call_service( - // &mut app, - // test::TestRequest::post() - // .cookie(cookies.clone()) - // .uri(GET_SECRET) - // .to_request(), - // ) - // .await; - // assert_eq!(resp.status(), StatusCode::OK); - // 2. check if duplicate username is allowed let msg = Register { username: NAME.into(), @@ -86,7 +75,7 @@ async fn auth_works() { .await; // 3. sigining in with non-existent user - let mut login = Login { + let mut creds = Login { username: "nonexistantuser".into(), password: msg.password.clone(), }; @@ -94,21 +83,36 @@ async fn auth_works() { NAME, PASSWORD, ROUTES.auth.login, - &login, + &creds, ServiceError::UsernameNotFound, StatusCode::NOT_FOUND, ) .await; + let resp = test::call_service( + &mut app, + post_request!(&creds, PAGES.auth.login).to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + creds.username = NAME.into(); + let resp = test::call_service( + &mut app, + post_request!(&creds, PAGES.auth.login).to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + // 4. trying to signin with wrong password - login.username = NAME.into(); - login.password = NAME.into(); + creds.username = NAME.into(); + creds.password = NAME.into(); bad_post_req_test( NAME, PASSWORD, ROUTES.auth.login, - &login, + &creds, ServiceError::WrongPassword, StatusCode::UNAUTHORIZED, ) diff --git a/src/main.rs b/src/main.rs index 72be2977..af9da47a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,6 +30,7 @@ mod data; mod docs; mod errors; mod middleware; +#[macro_use] mod pages; #[macro_use] mod routes; diff --git a/src/pages/auth/login.rs b/src/pages/auth/login.rs index 4c49c8f5..02da32a9 100644 --- a/src/pages/auth/login.rs +++ b/src/pages/auth/login.rs @@ -15,21 +15,36 @@ * along with this program. If not, see . */ -use crate::PAGES; -use actix_web::{HttpResponse, Responder}; +use actix_identity::Identity; +use actix_web::{web, HttpResponse, Responder}; use lazy_static::lazy_static; use my_codegen::get; use sailfish::TemplateOnce; +use crate::api::v1::auth::runners; +use crate::pages::errors::Errorable; +use crate::AppData; +use crate::PAGES; + #[derive(Clone, TemplateOnce)] #[template(path = "auth/login/index.html")] -struct IndexPage; +struct IndexPage { + username: Option, + password: Option, + error: Option, +} + +crate::ImplErrorable!(IndexPage); const PAGE: &str = "Login"; impl Default for IndexPage { fn default() -> Self { - IndexPage + IndexPage { + username: None, + password: None, + error: None, + } } } @@ -43,3 +58,28 @@ pub async fn login() -> impl Responder { .content_type("text/html; charset=utf-8") .body(&*INDEX) } +#[my_codegen::post(path = "PAGES.auth.login")] +async fn login_post( + id: Identity, + payload: web::Json, + data: AppData, +) -> impl Responder { + match runners::login_runner(&payload, &data).await { + Ok(_) => { + id.remember(payload.into_inner().username); + HttpResponse::Ok().into() + } + Err(e) => { + let payload = payload.into_inner(); + let username = Some(payload.username); + let password = Some(payload.password); + + let page = IndexPage { + username, + password, + ..Default::default() + }; + page.get_error_resp(e) + } + } +} diff --git a/src/pages/auth/mod.rs b/src/pages/auth/mod.rs index fa7bb602..a758ac0b 100644 --- a/src/pages/auth/mod.rs +++ b/src/pages/auth/mod.rs @@ -20,6 +20,7 @@ pub mod register; pub fn services(cfg: &mut actix_web::web::ServiceConfig) { cfg.service(login::login); + cfg.service(login::login_post); cfg.service(register::join); } diff --git a/src/pages/errors.rs b/src/pages/errors.rs index 9495c473..df72995a 100644 --- a/src/pages/errors.rs +++ b/src/pages/errors.rs @@ -13,12 +13,37 @@ * * You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -use actix_web::{web, HttpResponse, Responder}; +use actix_web::{error::ResponseError, web, HttpResponse, Responder}; use lazy_static::lazy_static; use sailfish::TemplateOnce; use crate::errors::PageError; +pub trait Errorable: TemplateOnce { + fn get_error_resp(self, e: E) -> HttpResponse; +} + +#[macro_export] +macro_rules! ImplErrorable { + ($struct:ident) => { + impl crate::pages::errors::Errorable for $struct { + fn get_error_resp(mut self, e: E) -> actix_web::HttpResponse + where + E: actix_web::error::ResponseError + std::fmt::Display, + //R: actix_web::Responder + { + self.error = Some(e.to_string()); + let page = self.render_once().unwrap(); + println!("status code: {}", e.status_code()); + actix_web::dev::HttpResponseBuilder::new(e.status_code()) + .content_type("text/html; charset=utf-8") + .body(&page) + .into() + } + } + }; +} + #[derive(Clone, TemplateOnce)] #[template(path = "errors/index.html")] struct ErrorPage<'a> { diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 4049d2fb..81d90437 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -10,7 +10,7 @@ use libmcaptcha::defense::Level; use serde::Serialize; use super::*; -use crate::api::v1::auth::{Login, Register}; +use crate::api::v1::auth::runners::{Login, Register}; use crate::api::v1::mcaptcha::captcha::MCaptchaDetails; use crate::api::v1::mcaptcha::levels::AddLevels; use crate::api::v1::ROUTES;