// Copyright (C) 2022 Aravinth Manivannan // SPDX-FileCopyrightText: 2023 Aravinth Manivannan // // 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, } #[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 { 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, data: AppData, ) -> ServiceResult { 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, query: web::Query, data: AppData, ) -> ServiceResult { 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() }