survey/src/api/v1/admin/auth.rs

261 lines
8.1 KiB
Rust

/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* 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_identity::Identity;
use actix_web::http::header;
use actix_web::{web, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use super::{get_random, RedirectQuery};
use crate::errors::*;
use crate::AppData;
pub mod routes {
use crate::middleware::auth::GetLoginRoute;
use url::Url;
pub struct Auth {
pub logout: &'static str,
pub login: &'static str,
pub register: &'static str,
}
impl GetLoginRoute for Auth {
fn get_login_route(&self, src: Option<&str>) -> String {
if let Some(redirect_to) = src {
let mut url = Url::parse("http://x/").unwrap();
url.set_path(self.login);
url.query_pairs_mut()
.append_pair("redirect_to", redirect_to);
let path = format!("{}/?{}", url.path(), url.query().unwrap());
path
} else {
self.login.to_string()
}
}
}
impl Auth {
pub const fn new() -> Auth {
let login = "/admin/api/v1/signin";
let logout = "/admin/logout";
let register = "/admin/api/v1/signup";
Auth {
logout,
login,
register,
}
}
}
}
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 {
// login accepts both username and email under "username field"
// TODO update all instances where login is used
pub login: 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<String> {
use argon2_creds::Config;
use sqlx::Error::RowNotFound;
let verify = |stored: &str, received: &str| {
if Config::verify(stored, received)? {
Ok(())
} else {
Err(ServiceError::WrongPassword)
}
};
if payload.login.contains('@') {
#[derive(Clone, Debug)]
struct EmailLogin {
name: String,
password: String,
}
let email_fut = sqlx::query_as!(
EmailLogin,
r#"SELECT name, password FROM survey_admins WHERE email = ($1)"#,
&payload.login,
)
.fetch_one(&data.db)
.await;
match email_fut {
Ok(s) => {
verify(&s.password, &payload.password)?;
Ok(s.name)
}
Err(RowNotFound) => Err(ServiceError::AccountNotFound),
Err(_) => Err(ServiceError::InternalServerError),
}
} else {
let username_fut = sqlx::query_as!(
Password,
r#"SELECT password FROM survey_admins WHERE name = ($1)"#,
&payload.login,
)
.fetch_one(&data.db)
.await;
match username_fut {
Ok(s) => {
verify(&s.password, &payload.password)?;
Ok(payload.login.clone())
}
Err(RowNotFound) => Err(ServiceError::AccountNotFound),
Err(_) => Err(ServiceError::InternalServerError),
}
}
}
pub async fn register_runner(
payload: &Register,
data: &AppData,
) -> ServiceResult<()> {
if !crate::SETTINGS.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 survey_admins
(name , password, email, secret) values ($1, $2, $3, $4)",
&username,
&hash,
&email,
&secret,
)
.execute(&data.db)
.await;
} else {
res = sqlx::query!(
"INSERT INTO survey_admins
(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("survey_admins_name_key") {
return Err(ServiceError::UsernameTaken);
} else if msg.contains("survey_admins_email_key") {
return Err(ServiceError::EmailTaken);
} else if msg.contains("survey_admins_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(register);
cfg.service(login);
cfg.service(signout);
}
#[my_codegen::post(path = "crate::V1_API_ROUTES.admin.auth.register")]
async fn register(
payload: web::Json<runners::Register>,
data: AppData,
) -> ServiceResult<impl Responder> {
runners::register_runner(&payload, &data).await?;
Ok(HttpResponse::Ok())
}
#[my_codegen::post(path = "crate::V1_API_ROUTES.admin.auth.login")]
async fn login(
id: Identity,
payload: web::Json<runners::Login>,
path: web::Query<RedirectQuery>,
data: AppData,
) -> ServiceResult<impl Responder> {
let payload = payload.into_inner();
let username = runners::login_runner(&payload, &data).await?;
let path = path.into_inner();
id.remember(username);
if let Some(redirect_to) = path.redirect_to {
Ok(HttpResponse::Found()
.insert_header((header::LOCATION, redirect_to))
.finish())
} else {
Ok(HttpResponse::Ok().into())
}
}
#[my_codegen::get(
path = "crate::V1_API_ROUTES.admin.auth.logout",
wrap = "crate::api::v1::admin::get_admin_check_login()"
)]
async fn signout(id: Identity) -> impl Responder {
if id.identity().is_some() {
id.forget();
}
HttpResponse::Found()
.append_header((header::LOCATION, crate::V1_API_ROUTES.admin.auth.register))
.finish()
}