errorable and seperated runner methods for auth

This commit is contained in:
realaravinth 2021-06-28 19:16:59 +05:30
parent 2162d32455
commit cc17f2048f
No known key found for this signature in database
GPG key ID: AD9F0F08E855ED88
9 changed files with 222 additions and 135 deletions

View file

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

View file

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

View file

@ -14,12 +14,10 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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<String>,
}
#[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<String>,
}
#[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<Register>,
async fn register(
payload: web::Json<runners::Register>,
data: AppData,
) -> ServiceResult<impl Responder> {
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<Login>,
payload: web::Json<runners::Login>,
data: AppData,
) -> ServiceResult<impl Responder> {
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")]

View file

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

View file

@ -30,6 +30,7 @@ mod data;
mod docs;
mod errors;
mod middleware;
#[macro_use]
mod pages;
#[macro_use]
mod routes;

View file

@ -15,21 +15,36 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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<String>,
password: Option<String>,
error: Option<String>,
}
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<runners::Login>,
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)
}
}
}

View file

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

View file

@ -13,12 +13,37 @@
*
* You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. */
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<E: ResponseError>(self, e: E) -> HttpResponse;
}
#[macro_export]
macro_rules! ImplErrorable {
($struct:ident) => {
impl crate::pages::errors::Errorable for $struct {
fn get_error_resp<E>(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> {

View file

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