#
mcaptcha/api/v1/
auth.rs

    1// Copyright (C) 2022  Aravinth Manivannan <realaravinth@batsense.net>
    2// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
    3//
    4// SPDX-License-Identifier: AGPL-3.0-or-later
    5
    6use actix_identity::Identity;
    7use actix_web::http::header;
    8use actix_web::{web, HttpResponse, Responder};
    9use db_core::errors::DBError;
    10use serde::{Deserialize, Serialize};
    11
    12use super::mcaptcha::get_random;
    13use crate::errors::*;
    14use crate::AppData;
    15
    16pub mod routes {
    17    use actix_auth_middleware::GetLoginRoute;
    18
    19    pub struct Auth {
    20        pub logout: &'static str,
    21        pub login: &'static str,
    22        pub register: &'static str,
    23    }
    24
    25    impl Auth {
    26        pub const fn new() -> Auth {
    27            let login = "/api/v1/signin";
    28            let logout = "/logout";
    29            let register = "/api/v1/signup";
    30            Auth {
    31                logout,
    32                login,
    33                register,
    34            }
    35        }
    36    }
    37
    38    impl GetLoginRoute for Auth {
    39        fn get_login_route(&self, src: Option<&str>) -> String {
    40            if let Some(redirect_to) = src {
    41                format!(
    42                    "{}?redirect_to={}",
    43                    self.login,
    44                    urlencoding::encode(redirect_to)
    45                )
    46            } else {
    47                self.login.to_string()
    48            }
    49        }
    50    }
    51}
    52
    53pub mod runners {
    54    use super::*;
    55
    56    #[derive(Clone, Debug, Deserialize, Serialize)]
    57    pub struct Register {
    58        pub username: String,
    59        pub password: String,
    60        pub confirm_password: String,
    61        pub email: Option<String>,
    62    }
    63
    64    #[derive(Clone, Debug, Deserialize, Serialize)]
    65    pub struct Login {
    66        // login accepts both username and email under "username field"
    67        // TODO update all instances where login is used
    68        pub login: String,
    69        pub password: String,
    70    }
    71
    72    #[derive(Clone, Debug, Deserialize, Serialize)]
    73    pub struct Password {
    74        pub password: String,
    75    }
    76
    77    /// returns Ok(()) when everything checks out and the user is authenticated. Errors otherwise
    78    pub async fn login_runner(payload: Login, data: &AppData) -> ServiceResult<String> {
    79        use argon2_creds::Config;
    80
    81        let verify = |stored: &str, received: &str| {
    82            if Config::verify(stored, received)? {
    83                Ok(())
    84            } else {
    85                Err(ServiceError::WrongPassword)
    86            }
    87        };
    88
    89        let s = if payload.login.contains('@') {
    90            data.db
    91                .get_password(&db_core::Login::Email(&payload.login))
    92                .await?
    93        } else {
    94            let username = data.creds.username(&payload.login)?;
    95            data.db
    96                .get_password(&db_core::Login::Username(&username))
    97                .await?
    98        };
    99
    100        verify(&s.hash, &payload.password)?;
    101        Ok(s.username)
    102    }
    103    pub async fn register_runner(
    104        payload: &Register,
    105        data: &AppData,
    106    ) -> ServiceResult<()> {
    107        if !data.settings.allow_registration {
    108            return Err(ServiceError::ClosedForRegistration);
    109        }
    110
    111        if payload.password != payload.confirm_password {
    112            return Err(ServiceError::PasswordsDontMatch);
    113        }
    114        let username = data.creds.username(&payload.username)?;
    115        let hash = data.creds.password(&payload.password)?;
    116
    117        if let Some(email) = &payload.email {
    118            data.creds.email(email)?;
    119        }
    120
    121        let mut secret;
    122
    123        loop {
    124            secret = get_random(32);
    125
    126            let p = db_core::Register {
    127                username: &username,
    128                hash: &hash,
    129                email: payload.email.as_deref(),
    130                secret: &secret,
    131            };
    132
    133            match data.db.register(&p).await {
    134                Ok(_) => break,
    135                Err(DBError::SecretTaken) => continue,
    136                Err(e) => return Err(e.into()),
    137            }
    138        }
    139
    140        Ok(())
    141    }
    142}
    143
    144pub fn services(cfg: &mut web::ServiceConfig) {
    145    cfg.service(register);
    146    cfg.service(login);
    147    cfg.service(signout);
    148}
    149#[my_codegen::post(path = "crate::V1_API_ROUTES.auth.register")]
    150async fn register(
    151    payload: web::Json<runners::Register>,
    152    data: AppData,
    153) -> ServiceResult<impl Responder> {
    154    runners::register_runner(&payload, &data).await?;
    155    Ok(HttpResponse::Ok())
    156}
    157
    158#[my_codegen::post(path = "crate::V1_API_ROUTES.auth.login")]
    159async fn login(
    160    id: Identity,
    161    payload: web::Json<runners::Login>,
    162    query: web::Query<super::RedirectQuery>,
    163    data: AppData,
    164) -> ServiceResult<impl Responder> {
    165    let username = runners::login_runner(payload.into_inner(), &data).await?;
    166    id.remember(username);
    167    //    Ok(HttpResponse::Ok())
    168
    169    let query = query.into_inner();
    170    if let Some(redirect_to) = query.redirect_to {
    171        Ok(HttpResponse::Found()
    172            .append_header((header::LOCATION, redirect_to))
    173            .finish())
    174    } else {
    175        Ok(HttpResponse::Ok().finish())
    176    }
    177}
    178
    179#[my_codegen::get(
    180    path = "crate::V1_API_ROUTES.auth.logout",
    181    wrap = "crate::api::v1::get_middleware()"
    182)]
    183async fn signout(id: Identity) -> impl Responder {
    184    if id.identity().is_some() {
    185        id.forget();
    186    }
    187    HttpResponse::Found()
    188        .append_header((header::LOCATION, crate::PAGES.auth.login))
    189        .finish()
    190}