mCaptcha/src/api/v1/auth.rs

191 lines
5.2 KiB
Rust

// Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use actix_identity::Identity;
use actix_web::http::header;
use actix_web::{web, HttpResponse, Responder};
use db_core::errors::DBError;
use serde::{Deserialize, Serialize};
use super::mcaptcha::get_random;
use crate::errors::*;
use crate::AppData;
pub mod routes {
use actix_auth_middleware::GetLoginRoute;
pub struct Auth {
pub logout: &'static str,
pub login: &'static str,
pub register: &'static str,
}
impl Auth {
pub const fn new() -> Auth {
let login = "/api/v1/signin";
let logout = "/logout";
let register = "/api/v1/signup";
Auth {
logout,
login,
register,
}
}
}
impl GetLoginRoute for Auth {
fn get_login_route(&self, src: Option<&str>) -> String {
if let Some(redirect_to) = src {
format!(
"{}?redirect_to={}",
self.login,
urlencoding::encode(redirect_to)
)
} else {
self.login.to_string()
}
}
}
}
pub mod runners {
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. Errors otherwise
pub async fn login_runner(payload: Login, data: &AppData) -> ServiceResult<String> {
use argon2_creds::Config;
let verify = |stored: &str, received: &str| {
if Config::verify(stored, received)? {
Ok(())
} else {
Err(ServiceError::WrongPassword)
}
};
let s = if payload.login.contains('@') {
data.db
.get_password(&db_core::Login::Email(&payload.login))
.await?
} else {
let username = data.creds.username(&payload.login)?;
data.db
.get_password(&db_core::Login::Username(&username))
.await?
};
verify(&s.hash, &payload.password)?;
Ok(s.username)
}
pub async fn register_runner(
payload: &Register,
data: &AppData,
) -> ServiceResult<()> {
if !data.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 p = db_core::Register {
username: &username,
hash: &hash,
email: payload.email.as_deref(),
secret: &secret,
};
match data.db.register(&p).await {
Ok(_) => break,
Err(DBError::SecretTaken) => continue,
Err(e) => return Err(e.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.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.auth.login")]
async fn login(
id: Identity,
payload: web::Json<runners::Login>,
query: web::Query<super::RedirectQuery>,
data: AppData,
) -> ServiceResult<impl Responder> {
let username = runners::login_runner(payload.into_inner(), &data).await?;
id.remember(username);
// Ok(HttpResponse::Ok())
let query = query.into_inner();
if let Some(redirect_to) = query.redirect_to {
Ok(HttpResponse::Found()
.append_header((header::LOCATION, redirect_to))
.finish())
} else {
Ok(HttpResponse::Ok().finish())
}
}
#[my_codegen::get(
path = "crate::V1_API_ROUTES.auth.logout",
wrap = "crate::api::v1::get_middleware()"
)]
async fn signout(id: Identity) -> impl Responder {
if id.identity().is_some() {
id.forget();
}
HttpResponse::Found()
.append_header((header::LOCATION, crate::PAGES.auth.login))
.finish()
}