authbeam/api/
general.rs

1use crate::database::Database;
2use crate::model::{DatabaseError, ProfileCreate, ProfileLogin, TokenContext};
3use axum::http::{HeaderMap, HeaderValue};
4use hcaptcha_no_wasm::Hcaptcha;
5use databeam::prelude::DefaultReturn;
6
7use axum::response::IntoResponse;
8use axum::{
9    extract::{Query, State},
10    Json,
11};
12use axum_extra::extract::cookie::CookieJar;
13use serde::{Deserialize, Serialize};
14
15/// [`Database::create_profile`]
16pub async fn create_request(
17    headers: HeaderMap,
18    State(database): State<Database>,
19    Json(props): Json<ProfileCreate>,
20) -> impl IntoResponse {
21    if !props.policy_consent {
22        return (
23            HeaderMap::new(),
24            serde_json::to_string(&DatabaseError::NotAllowed.to_json::<()>()).unwrap(),
25        );
26    }
27
28    // get real ip
29    let real_ip = if let Some(ref real_ip_header) = database.config.real_ip_header {
30        headers
31            .get(real_ip_header.to_owned())
32            .unwrap_or(&HeaderValue::from_static(""))
33            .to_str()
34            .unwrap_or("")
35            .to_string()
36    } else {
37        String::new()
38    };
39
40    // check ip
41    if database.get_ipban_by_ip(&real_ip).await.is_ok() {
42        return (
43            HeaderMap::new(),
44            serde_json::to_string(&DatabaseError::NotAllowed.to_json::<()>()).unwrap(),
45        );
46    }
47
48    // create profile
49    let res = match database.create_profile(props, &real_ip).await {
50        Ok(r) => r,
51        Err(e) => {
52            return (
53                HeaderMap::new(),
54                serde_json::to_string(&DefaultReturn {
55                    success: false,
56                    message: e.to_string(),
57                    payload: (),
58                })
59                .unwrap(),
60            );
61        }
62    };
63
64    // return
65    let mut headers = HeaderMap::new();
66
67    headers.insert(
68        "Set-Cookie",
69        format!(
70            "__Secure-Token={}; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age={}",
71            res,
72            60* 60 * 24 * 365
73        )
74        .parse()
75        .unwrap(),
76    );
77
78    (
79        headers,
80        serde_json::to_string(&DefaultReturn {
81            success: true,
82            message: res.clone(),
83            payload: (),
84        })
85        .unwrap(),
86    )
87}
88
89/// [`Database::get_profile_by_username_password`]
90pub async fn login_request(
91    headers: HeaderMap,
92    State(database): State<Database>,
93    Json(props): Json<ProfileLogin>,
94) -> impl IntoResponse {
95    // check hcaptcha
96    if let Err(e) = props
97        .valid_response(&database.config.captcha.secret, None)
98        .await
99    {
100        return (
101            HeaderMap::new(),
102            serde_json::to_string(&DefaultReturn {
103                success: false,
104                message: e.to_string(),
105                payload: (),
106            })
107            .unwrap(),
108        );
109    }
110
111    // ...
112    let mut ua = match database.get_profile_by_username(&props.username).await {
113        Ok(ua) => ua,
114        Err(e) => {
115            return (
116                HeaderMap::new(),
117                serde_json::to_string(&DefaultReturn {
118                    success: false,
119                    message: e.to_string(),
120                    payload: (),
121                })
122                .unwrap(),
123            )
124        }
125    };
126
127    // check password
128    let input_password =
129        rainbeam_shared::hash::hash_salted(props.password.clone(), ua.salt.clone());
130
131    if input_password != ua.password {
132        return (
133            HeaderMap::new(),
134            serde_json::to_string(&DatabaseError::IncorrectPassword.to_json::<()>()).unwrap(),
135        );
136    }
137
138    // get real ip
139    let real_ip = if let Some(ref real_ip_header) = database.config.real_ip_header {
140        headers
141            .get(real_ip_header.to_owned())
142            .unwrap_or(&HeaderValue::from_static(""))
143            .to_str()
144            .unwrap_or("")
145            .to_string()
146    } else {
147        String::new()
148    };
149
150    // check ip
151    if database.get_ipban_by_ip(&real_ip).await.is_ok() {
152        return (
153            HeaderMap::new(),
154            serde_json::to_string(&DatabaseError::NotAllowed.to_json::<()>()).unwrap(),
155        );
156    }
157
158    // check totp
159    if !database.check_totp(&ua, &props.totp) {
160        return (
161            HeaderMap::new(),
162            serde_json::to_string(&DatabaseError::NotAllowed.to_json::<()>()).unwrap(),
163        );
164    }
165
166    // ...
167    let token = databeam::utility::uuid();
168    let token_hashed = databeam::utility::hash(token.clone());
169
170    ua.tokens.push(token_hashed);
171    ua.ips.push(real_ip);
172    ua.token_context.push(TokenContext::default());
173
174    database
175        .update_profile_tokens(&props.username, ua.tokens, ua.ips, ua.token_context)
176        .await
177        .unwrap();
178
179    // return
180    let mut headers = HeaderMap::new();
181
182    headers.insert(
183        "Set-Cookie",
184        format!(
185            "__Secure-Token={}; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age={}",
186            token,
187            60* 60 * 24 * 365
188        )
189        .parse()
190        .unwrap(),
191    );
192
193    (
194        headers,
195        serde_json::to_string(&DefaultReturn {
196            success: true,
197            message: token,
198            payload: (),
199        })
200        .unwrap(),
201    )
202}
203
204#[derive(serde::Deserialize)]
205pub struct CallbackQueryProps {
206    pub token: String, // this uid will need to be sent to the client as a token
207}
208
209pub async fn callback_request(Query(params): Query<CallbackQueryProps>) -> impl IntoResponse {
210    // return
211    (
212        [
213            ("Content-Type".to_string(), "text/html".to_string()),
214            (
215                "Set-Cookie".to_string(),
216                format!(
217                    "__Secure-Token={}; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age={}",
218                    params.token,
219                    60 * 60 * 24 * 365
220                ),
221            ),
222        ],
223        "<head>
224            <meta http-equiv=\"Refresh\" content=\"0; URL=/\" />
225        </head>"
226    )
227}
228
229pub async fn logout_request(jar: CookieJar) -> impl IntoResponse {
230    // check for cookie
231    if let Some(_) = jar.get("__Secure-Token") {
232        return (
233            [
234                ("Content-Type".to_string(), "text/plain".to_string()),
235                (
236                    "Set-Cookie".to_string(),
237                    "__Secure-Token=refresh; SameSite=Strict; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=0".to_string(),
238                )            ],
239            "You have been signed out. You can now close this tab.",
240        );
241    }
242
243    // return
244    (
245        [
246            ("Content-Type".to_string(), "text/plain".to_string()),
247            ("Set-Cookie".to_string(), String::new()),
248        ],
249        "Failed to sign out of account.",
250    )
251}
252
253pub async fn remove_tag(jar: CookieJar) -> impl IntoResponse {
254    // check for cookie
255    // anonymous users cannot remove their own tag
256    if let Some(_) = jar.get("__Secure-Token") {
257        return (
258            [
259                ("Content-Type".to_string(), "text/plain".to_string()),
260                (
261                    "Set-Cookie2".to_string(),
262                    "__Secure-Question-Tag=refresh; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=0".to_string()
263                )
264            ],
265            "You have been signed out. You can now close this tab.",
266        );
267    }
268
269    // return
270    (
271        [
272            ("Content-Type".to_string(), "text/plain".to_string()),
273            ("Set-Cookie".to_string(), String::new()),
274        ],
275        "Failed to remove tag.",
276    )
277}
278
279#[derive(Serialize, Deserialize)]
280pub struct SetTokenQuery {
281    #[serde(default)]
282    pub token: String,
283}
284
285/// Set the current session token
286pub async fn set_token_request(Query(props): Query<SetTokenQuery>) -> impl IntoResponse {
287    (
288        {
289            let mut headers = HeaderMap::new();
290
291            headers.insert(
292                "Set-Cookie",
293                format!(
294                    "__Secure-Token={}; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age={}",
295                    props.token,
296                    60* 60 * 24 * 365
297                )
298                .parse()
299                .unwrap(),
300            );
301
302            headers
303        },
304        "Token changed",
305    )
306}