Skip to main content

web/
handler.rs

1use std::sync::Arc;
2
3use argon2::{Argon2, PasswordHasher};
4use axum::{
5    Extension, Json,
6    extract::State,
7    http::{HeaderMap, Response, StatusCode, header},
8    response::IntoResponse,
9};
10use axum_extra::extract::{
11    CookieJar,
12    cookie::{Cookie, SameSite},
13};
14use serde_json::json;
15
16use crate::{
17    AppState,
18    jwt_auth::JWTAuthMiddleware,
19    model::{LoginUserSchema, RegisterUserSchema, User},
20    response::FilteredUser,
21    token::{self, TokenDetails},
22};
23use rust_i18n::t;
24
25use redis::AsyncCommands;
26use server::{command::CmdResult, db::get_connection};
27
28pub async fn register_user_handler(
29    State(_data): State<Arc<AppState>>,
30    Json(body): Json<RegisterUserSchema>,
31) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
32    let mut conn = get_connection().await.map_err(|e| {
33        let msg = format!("{} {}", t!("Database error:"), e);
34        log::error!("{msg}");
35        let error_response = serde_json::json!({
36            "status": "fail",
37            "message": msg,
38        });
39        (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
40    })?;
41
42    let user_exists: Option<bool> =
43        sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM users WHERE email = $1)")
44            .bind(body.email.clone().to_ascii_lowercase())
45            .fetch_one(&mut *conn)
46            .await
47            .map_err(|e| {
48                let msg = format!("{} {}", t!("Database error:"), e);
49                log::error!("{msg}");
50                let error_response = serde_json::json!({
51                    "status": "fail",
52                    "message": msg,
53                });
54                (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
55            })?;
56
57    if let Some(exists) = user_exists
58        && exists
59    {
60        let msg = format!("{}", t!("User with that email already exists"));
61        log::error!("{msg}");
62        let error_response = serde_json::json!({
63            "status": "fail",
64            "message": msg,
65        });
66        return Err((StatusCode::CONFLICT, Json(error_response)));
67    }
68
69    let hashed_password = Argon2::default()
70        .hash_password(body.password.as_bytes())
71        .map_err(|e| {
72            let msg = format!("{} {}", t!("Error while hashing password:"), e);
73            log::error!("{msg}");
74            let error_response = serde_json::json!({
75                "status": "fail",
76                "message": msg,
77            });
78            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
79        })
80        .map(|hash| hash.to_string())?;
81    let mut conn = get_connection().await.map_err(|e| {
82        let msg = format!("{} {}", t!("Database error:"), e);
83        log::error!("{msg}");
84        let error_response = serde_json::json!({
85            "status": "fail",
86            "message": msg,
87        });
88        (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
89    })?;
90
91    let user = sqlx::query_as!(
92        User,
93        "INSERT INTO users (user_name,email,user_password) VALUES ($1, $2, $3) RETURNING id, user_name as name, email, user_password as password, user_role as role, photo, verified, db_name as database, created_at, updated_at",
94        body.name.clone(),
95        body.email.clone().to_ascii_lowercase(),
96        hashed_password
97    )
98    .fetch_one(&mut *conn)
99    .await
100    .map_err(|e| {
101        let msg = format!("{} {}", t!("Database error:"), e);
102        log::error!("{msg}");
103        let error_response = serde_json::json!({
104            "status": "fail",
105            "message": msg,
106        });
107        (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
108    })?;
109
110    let user_response = serde_json::json!({"status": "success","data": serde_json::json!({
111        "user": filter_user_record(&user)
112    })});
113
114    Ok(Json(user_response))
115}
116
117struct LoginCookies {
118    access: Cookie<'static>,
119    refresh: Cookie<'static>,
120    logged_in: Cookie<'static>,
121    access_token: String,
122}
123
124async fn authenticate_user(
125    data: &Arc<AppState>,
126    email: &str,
127    password: &str,
128) -> Result<LoginCookies, (StatusCode, Json<serde_json::Value>)> {
129    let verified = server::command::user::verify_user_password(email, password)
130        .await
131        .map_err(|e| {
132            let msg = format!("{} {}", t!("Database error:"), e);
133            log::error!("{msg}");
134            let error_response = serde_json::json!({
135                "status": "error",
136                "message": msg,
137            });
138            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
139        })?
140        .ok_or_else(|| {
141            let msg = format!("{}", t!("Invalid email or password"));
142            log::error!("{msg}");
143            let error_response = serde_json::json!({
144                "status": "fail",
145                "message": msg,
146            });
147            (StatusCode::BAD_REQUEST, Json(error_response))
148        })?;
149
150    let access_token_details = generate_token(
151        verified.user_id,
152        data.conf.access_token_max_age,
153        &data.conf.access_token_private_key,
154    )?;
155    let refresh_token_details = generate_token(
156        verified.user_id,
157        data.conf.refresh_token_max_age,
158        &data.conf.refresh_token_private_key,
159    )?;
160
161    save_token_data_to_redis(data, &access_token_details, data.conf.access_token_max_age).await?;
162    save_token_data_to_redis(
163        data,
164        &refresh_token_details,
165        data.conf.refresh_token_max_age,
166    )
167    .await?;
168
169    let access_cookie = Cookie::build((
170        "access_token",
171        access_token_details.token.clone().unwrap_or_default(),
172    ))
173    .path("/")
174    .max_age(time::Duration::minutes(data.conf.access_token_max_age * 60))
175    .same_site(SameSite::Lax)
176    .http_only(true)
177    .build();
178
179    let refresh_cookie = Cookie::build((
180        "refresh_token",
181        refresh_token_details.token.unwrap_or_default(),
182    ))
183    .path("/")
184    .max_age(time::Duration::minutes(
185        data.conf.refresh_token_max_age * 60,
186    ))
187    .same_site(SameSite::Lax)
188    .http_only(true)
189    .build();
190
191    let logged_in_cookie = Cookie::build(("logged_in", "true"))
192        .path("/")
193        .max_age(time::Duration::minutes(data.conf.access_token_max_age * 60))
194        .same_site(SameSite::Lax)
195        .http_only(false)
196        .build();
197
198    Ok(LoginCookies {
199        access: access_cookie,
200        refresh: refresh_cookie,
201        logged_in: logged_in_cookie,
202        access_token: access_token_details.token.unwrap_or_default(),
203    })
204}
205
206fn build_cookie_headers(cookies: &LoginCookies) -> HeaderMap {
207    let mut headers = HeaderMap::new();
208    headers.append(
209        header::SET_COOKIE,
210        cookies.access.to_string().parse().unwrap(),
211    );
212    headers.append(
213        header::SET_COOKIE,
214        cookies.refresh.to_string().parse().unwrap(),
215    );
216    headers.append(
217        header::SET_COOKIE,
218        cookies.logged_in.to_string().parse().unwrap(),
219    );
220    headers
221}
222
223pub async fn login_user_handler(
224    State(data): State<Arc<AppState>>,
225    Json(body): Json<LoginUserSchema>,
226) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
227    let cookies = authenticate_user(&data, &body.email, &body.password).await?;
228
229    let mut response = Response::new(
230        json!({"status": "success", "access_token": cookies.access_token}).to_string(),
231    );
232    let mut headers = build_cookie_headers(&cookies);
233    headers.append("HX-Redirect", "/".to_string().parse().unwrap());
234    response.headers_mut().extend(headers);
235    Ok(response)
236}
237
238pub async fn login_form_handler(
239    State(data): State<Arc<AppState>>,
240    axum::Form(body): axum::Form<LoginUserSchema>,
241) -> impl IntoResponse {
242    match authenticate_user(&data, &body.email, &body.password).await {
243        Ok(cookies) => {
244            let mut headers = build_cookie_headers(&cookies);
245            headers.insert(header::LOCATION, "/".parse().unwrap());
246            (StatusCode::SEE_OTHER, headers, "").into_response()
247        }
248        Err((status, json_err)) => {
249            let msg = json_err
250                .get("message")
251                .and_then(|v| v.as_str())
252                .unwrap_or("Login failed");
253            (status, msg.to_string()).into_response()
254        }
255    }
256}
257
258pub async fn refresh_access_token_handler(
259    cookie_jar: CookieJar,
260    State(data): State<Arc<AppState>>,
261) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
262    let message = t!("could not refresh access token");
263
264    let refresh_token = cookie_jar
265        .get("refresh_token")
266        .map(|cookie| cookie.value().to_string())
267        .ok_or_else(|| {
268            let error_response = serde_json::json!({
269                "status": "fail",
270                "message": message
271            });
272            (StatusCode::FORBIDDEN, Json(error_response))
273        })?;
274
275    let refresh_token_details =
276        match token::verify_jwt_token(&data.conf.refresh_token_public_key, &refresh_token) {
277            Ok(token_details) => token_details,
278            Err(e) => {
279                let error_response = serde_json::json!({
280                    "status": "fail",
281                    "message": format_args!("{:?}", e)
282                });
283                return Err((StatusCode::UNAUTHORIZED, Json(error_response)));
284            }
285        };
286
287    let mut redis_client = data
288        .redis_client
289        .get_multiplexed_async_connection()
290        .await
291        .map_err(|e| {
292            let msg = format!("{} {}", t!("Redis error:"), e);
293            log::error!("{msg}");
294            let error_response = serde_json::json!({
295                "status": "error",
296                "message": msg,
297            });
298            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
299        })?;
300
301    let redis_token_user_id = redis_client
302        .get::<_, String>(refresh_token_details.token_uuid.to_string())
303        .await
304        .map_err(|_| {
305            let msg = format!("{}", t!("Token is invalid or session has expired"));
306            log::error!("{msg}");
307            let error_response = serde_json::json!({
308                "status": "error",
309                "message": msg,
310            });
311            (StatusCode::UNAUTHORIZED, Json(error_response))
312        })?;
313
314    let user_id_uuid = uuid::Uuid::parse_str(&redis_token_user_id).map_err(|_| {
315        let msg = format!("{}", t!("Token is invalid or session has expired"));
316        log::error!("{msg}");
317        let error_response = serde_json::json!({
318            "status": "error",
319            "message": msg,
320        });
321        (StatusCode::UNAUTHORIZED, Json(error_response))
322    })?;
323
324    let mut conn = get_connection().await.map_err(|e| {
325        let msg = format!("{} {}", t!("Database error:"), e);
326        log::error!("{msg}");
327        let error_response = serde_json::json!({
328            "status": "fail",
329            "message": msg,
330        });
331        (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
332    })?;
333
334    let user = sqlx::query_as!(User, "SELECT id, user_name as name, email, user_password as password, user_role as role, photo, verified, db_name as database, created_at, updated_at FROM users WHERE id = $1", user_id_uuid)
335        .fetch_optional(&mut *conn)
336        .await
337        .map_err(|e| {
338            let msg = format!("{} {}", t!("Error fetching user from database:"), e);
339            log::error!("{msg}");
340            let error_response = serde_json::json!({
341                "status": "fail",
342                "message": msg,
343            });
344            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
345        })?;
346
347    let user = user.ok_or_else(|| {
348        let msg = format!(
349            "{}",
350            t!("The user belonging to this token no longer exists")
351        );
352        log::error!("{msg}");
353        let error_response = serde_json::json!({
354            "status": "fail",
355            "message": msg,
356        });
357        (StatusCode::UNAUTHORIZED, Json(error_response))
358    })?;
359
360    let access_token_details = generate_token(
361        user.id,
362        data.conf.access_token_max_age,
363        &data.conf.access_token_private_key,
364    )?;
365
366    save_token_data_to_redis(&data, &access_token_details, data.conf.access_token_max_age).await?;
367
368    let access_cookie = Cookie::build((
369        "access_token",
370        access_token_details.token.clone().unwrap_or_default(),
371    ))
372    .path("/")
373    .max_age(time::Duration::minutes(data.conf.access_token_max_age * 60))
374    .same_site(SameSite::Lax)
375    .http_only(true);
376
377    let logged_in_cookie = Cookie::build(("logged_in", "true"))
378        .path("/")
379        .max_age(time::Duration::minutes(data.conf.access_token_max_age * 60))
380        .same_site(SameSite::Lax)
381        .http_only(false);
382
383    let mut response = Response::new(
384        json!({"status": "success", "access_token": access_token_details.token.unwrap()})
385            .to_string(),
386    );
387    let mut headers = HeaderMap::new();
388    headers.append(
389        header::SET_COOKIE,
390        access_cookie.to_string().parse().unwrap(),
391    );
392    headers.append(
393        header::SET_COOKIE,
394        logged_in_cookie.to_string().parse().unwrap(),
395    );
396
397    response.headers_mut().extend(headers);
398    Ok(response)
399}
400
401pub async fn logout_handler(
402    cookie_jar: CookieJar,
403    Extension(auth_guard): Extension<JWTAuthMiddleware>,
404    State(data): State<Arc<AppState>>,
405) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
406    let message = t!("Token is invalid or session has expired");
407
408    let refresh_token = cookie_jar
409        .get("refresh_token")
410        .map(|cookie| cookie.value().to_string())
411        .ok_or_else(|| {
412            let error_response = serde_json::json!({
413                "status": "fail",
414                "message": message
415            });
416            (StatusCode::FORBIDDEN, Json(error_response))
417        })?;
418
419    let refresh_token_details =
420        match token::verify_jwt_token(&data.conf.refresh_token_public_key, &refresh_token) {
421            Ok(token_details) => token_details,
422            Err(e) => {
423                let error_response = serde_json::json!({
424                    "status": "fail",
425                    "message": format_args!("{:?}", e)
426                });
427                return Err((StatusCode::UNAUTHORIZED, Json(error_response)));
428            }
429        };
430
431    let mut redis_client = data
432        .redis_client
433        .get_multiplexed_async_connection()
434        .await
435        .map_err(|e| {
436            let msg = format!("{} {}", t!("Redis error:"), e);
437            log::error!("{msg}");
438            let error_response = serde_json::json!({
439                "status": "error",
440                "message": msg,
441            });
442            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
443        })?;
444
445    let _: bool = redis_client
446        .del(&[
447            refresh_token_details.token_uuid.to_string(),
448            auth_guard.access_token_uuid.to_string(),
449        ])
450        .await
451        .map_err(|e| {
452            let error_response = serde_json::json!({
453                "status": "error",
454                "message": format_args!("{:?}", e)
455            });
456            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
457        })?;
458
459    let access_cookie = Cookie::build(("access_token", ""))
460        .path("/")
461        .max_age(time::Duration::minutes(-1))
462        .same_site(SameSite::Lax)
463        .http_only(true);
464    let refresh_cookie = Cookie::build(("refresh_token", ""))
465        .path("/")
466        .max_age(time::Duration::minutes(-1))
467        .same_site(SameSite::Lax)
468        .http_only(true);
469
470    let logged_in_cookie = Cookie::build(("logged_in", "true"))
471        .path("/")
472        .max_age(time::Duration::minutes(-1))
473        .same_site(SameSite::Lax)
474        .http_only(false);
475
476    let mut headers = HeaderMap::new();
477    headers.append(
478        header::SET_COOKIE,
479        access_cookie.to_string().parse().unwrap(),
480    );
481    headers.append(
482        header::SET_COOKIE,
483        refresh_cookie.to_string().parse().unwrap(),
484    );
485    headers.append(
486        header::SET_COOKIE,
487        logged_in_cookie.to_string().parse().unwrap(),
488    );
489    headers.append("HX-Redirect", "/".to_string().parse().unwrap());
490
491    let mut response = Response::new(json!({"status": "success"}).to_string());
492    response.headers_mut().extend(headers);
493    Ok(response)
494}
495
496pub async fn get_me_handler(
497    Extension(jwtauth): Extension<JWTAuthMiddleware>,
498) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
499    let json_response = serde_json::json!({
500        "status":  "success",
501        "data": serde_json::json!({
502            "user": filter_user_record(&jwtauth.user)
503        })
504    });
505
506    Ok(Json(json_response))
507}
508
509pub async fn get_version() -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
510    if let Some(CmdResult::String(version)) = server::command::config::GetVersion::new()
511        .run()
512        .await
513        .map_err(|_| {
514            (
515                StatusCode::INTERNAL_SERVER_ERROR,
516                Json(json!("Can't get version")),
517            )
518        })?
519        && let Some(CmdResult::String(build_date)) = server::command::config::GetBuildDate::new()
520            .run()
521            .await
522            .map_err(|_| {
523                (
524                    StatusCode::INTERNAL_SERVER_ERROR,
525                    Json(json!("Can't get version")),
526                )
527            })?
528    {
529        Ok(Response::new(format!(
530            "<span class=\"version\">{version}</span><span class=\"build_date\" data-iso=\"{build_date}\"><script>
531    const el = document.currentScript.parentElement;
532    el.textContent = new Date(el.dataset.iso.trim()).toLocaleString();
533  </script></span>"
534        )))
535    } else {
536        Ok(Response::new("Unversioned".to_string()))
537    }
538}
539
540pub async fn get_logout_link() -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
541    Ok(Response::new(
542        "<a hx-get=\"/api/auth/logout\">Logout</a>".to_string(),
543    ))
544}
545
546pub async fn get_home_link() -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
547    Ok(Response::new("<a href='/'>Back to main</a>".to_string()))
548}
549
550fn filter_user_record(user: &User) -> FilteredUser {
551    FilteredUser {
552        id: user.id.to_string(),
553        email: user.email.clone(),
554        name: user.name.clone(),
555        photo: user.photo.clone(),
556        role: user.role.clone(),
557        verified: user.verified,
558        createdAt: user.created_at.unwrap(),
559        updatedAt: user.updated_at.unwrap(),
560    }
561}
562
563fn generate_token(
564    user_id: uuid::Uuid,
565    max_age: i64,
566    private_key: &str,
567) -> Result<TokenDetails, (StatusCode, Json<serde_json::Value>)> {
568    token::generate_jwt_token(user_id, max_age, private_key).map_err(|e| {
569        let msg = format!("{} {}", t!("error generating token:"), e);
570        log::error!("{msg}");
571        let error_response = serde_json::json!({
572            "status": "error",
573            "message": msg,
574        });
575        (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
576    })
577}
578
579async fn save_token_data_to_redis(
580    data: &Arc<AppState>,
581    token_details: &TokenDetails,
582    max_age: i64,
583) -> Result<(), (StatusCode, Json<serde_json::Value>)> {
584    let mut redis_client = data
585        .redis_client
586        .get_multiplexed_async_connection()
587        .await
588        .map_err(|e| {
589            let msg = format!("{} {}", t!("Redis error:"), e);
590            log::error!("{msg}");
591            let error_response = serde_json::json!({
592                "status": "error",
593                "message": msg,
594            });
595            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
596        })?;
597    let _: bool = redis_client
598        .set_ex(
599            token_details.token_uuid.to_string(),
600            token_details.user_id.to_string(),
601            (max_age * 60) as u64,
602        )
603        .await
604        .map_err(|e| {
605            let error_response = serde_json::json!({
606                "status": "error",
607                "message": format_args!("{}", e),
608            });
609            (StatusCode::UNPROCESSABLE_ENTITY, Json(error_response))
610        })?;
611    Ok(())
612}