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}