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::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 internal_error = |e: &dyn std::fmt::Display| {
92        let msg = format!("{} {}", t!("Database error:"), e);
93        log::error!("{msg}");
94        let error_response = serde_json::json!({ "status": "fail", "message": msg });
95        (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
96    };
97
98    let user_id = uuid::Uuid::new_v4();
99    // Generate the user's signing keypair first (off the request thread); the
100    // private half goes into their per-user DB, the public half into the global
101    // users row.
102    let keypair = server::auth_keys::generate()
103        .await
104        .map_err(|e| internal_error(&e))?;
105    let db_name = server::provision::create_user_database(user_id)
106        .await
107        .map_err(|e| internal_error(&e))?;
108
109    if let Err(e) =
110        server::provision::store_user_private_key(&db_name, &keypair.private_pem_b64).await
111    {
112        // The DB exists but key storage failed; drop the orphan so a retry
113        // starts clean.
114        if let Err(drop_err) = server::provision::drop_user_database(user_id).await {
115            log::error!(
116                "{} {}",
117                t!("Failed to clean up provisioned database:"),
118                drop_err
119            );
120        }
121        return Err(internal_error(&e));
122    }
123
124    let insert = sqlx::query_as!(
125        User,
126        "INSERT INTO users (id,user_name,email,user_password,db_name,jwt_public_key) VALUES ($1, $2, $3, $4, $5, $6) 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",
127        user_id,
128        body.name.clone(),
129        body.email.clone().to_ascii_lowercase(),
130        hashed_password,
131        db_name,
132        keypair.public_pem_b64
133    )
134    .fetch_one(&mut *conn)
135    .await;
136
137    let user = match insert {
138        Ok(user) => user,
139        Err(e) => {
140            // The per-user database was already created; the user row failed
141            // (e.g. a concurrent registration won the UNIQUE(email) race).
142            // Drop the orphan so failed registrations don't leak databases.
143            if let Err(drop_err) = server::provision::drop_user_database(user_id).await {
144                log::error!(
145                    "{} {}",
146                    t!("Failed to clean up provisioned database after registration error:"),
147                    drop_err
148                );
149            }
150            let msg = format!("{} {}", t!("Database error:"), e);
151            log::error!("{msg}");
152            let error_response = serde_json::json!({
153                "status": "fail",
154                "message": msg,
155            });
156            return Err((StatusCode::INTERNAL_SERVER_ERROR, Json(error_response)));
157        }
158    };
159
160    let user_response = serde_json::json!({"status": "success","data": serde_json::json!({
161        "user": filter_user_record(&user)
162    })});
163
164    Ok(Json(user_response))
165}
166
167struct LoginCookies {
168    access: Cookie<'static>,
169    refresh: Cookie<'static>,
170    logged_in: Cookie<'static>,
171    access_token: String,
172}
173
174async fn authenticate_user(
175    data: &Arc<AppState>,
176    email: &str,
177    password: &str,
178) -> Result<LoginCookies, (StatusCode, Json<serde_json::Value>)> {
179    let verified = server::command::user::verify_user_password(email, password)
180        .await
181        .map_err(|e| {
182            let msg = format!("{} {}", t!("Database error:"), e);
183            log::error!("{msg}");
184            let error_response = serde_json::json!({
185                "status": "error",
186                "message": msg,
187            });
188            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
189        })?
190        .ok_or_else(|| {
191            let msg = format!("{}", t!("Invalid email or password"));
192            log::error!("{msg}");
193            let error_response = serde_json::json!({
194                "status": "fail",
195                "message": msg,
196            });
197            (StatusCode::BAD_REQUEST, Json(error_response))
198        })?;
199
200    let access_token_details = mint_token(
201        verified.user_id,
202        data.conf.access_token_max_age,
203        crate::token::TokenType::Access,
204    )
205    .await?;
206    let refresh_token_details = mint_token(
207        verified.user_id,
208        data.conf.refresh_token_max_age,
209        crate::token::TokenType::Refresh,
210    )
211    .await?;
212
213    save_token_data_to_redis(data, &access_token_details, data.conf.access_token_max_age).await?;
214    save_token_data_to_redis(
215        data,
216        &refresh_token_details,
217        data.conf.refresh_token_max_age,
218    )
219    .await?;
220
221    let access_cookie = Cookie::build((
222        "access_token",
223        access_token_details.token.clone().unwrap_or_default(),
224    ))
225    .path("/")
226    .max_age(time::Duration::minutes(data.conf.access_token_max_age * 60))
227    .same_site(SameSite::Lax)
228    .secure(crate::auth_keys::secure_cookies())
229    .http_only(true)
230    .build();
231
232    let refresh_cookie = Cookie::build((
233        "refresh_token",
234        refresh_token_details.token.unwrap_or_default(),
235    ))
236    .path("/")
237    .max_age(time::Duration::minutes(
238        data.conf.refresh_token_max_age * 60,
239    ))
240    .same_site(SameSite::Lax)
241    .secure(crate::auth_keys::secure_cookies())
242    .http_only(true)
243    .build();
244
245    let logged_in_cookie = Cookie::build(("logged_in", "true"))
246        .path("/")
247        .max_age(time::Duration::minutes(data.conf.access_token_max_age * 60))
248        .same_site(SameSite::Lax)
249        .http_only(false)
250        .build();
251
252    Ok(LoginCookies {
253        access: access_cookie,
254        refresh: refresh_cookie,
255        logged_in: logged_in_cookie,
256        access_token: access_token_details.token.unwrap_or_default(),
257    })
258}
259
260fn build_cookie_headers(cookies: &LoginCookies) -> HeaderMap {
261    let mut headers = HeaderMap::new();
262    headers.append(
263        header::SET_COOKIE,
264        cookies.access.to_string().parse().unwrap(),
265    );
266    headers.append(
267        header::SET_COOKIE,
268        cookies.refresh.to_string().parse().unwrap(),
269    );
270    headers.append(
271        header::SET_COOKIE,
272        cookies.logged_in.to_string().parse().unwrap(),
273    );
274    headers
275}
276
277pub async fn login_user_handler(
278    State(data): State<Arc<AppState>>,
279    Json(body): Json<LoginUserSchema>,
280) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
281    let cookies = authenticate_user(&data, &body.email, &body.password).await?;
282
283    let mut response = Response::new(
284        json!({"status": "success", "access_token": cookies.access_token}).to_string(),
285    );
286    let mut headers = build_cookie_headers(&cookies);
287    headers.append("HX-Redirect", "/".to_string().parse().unwrap());
288    response.headers_mut().extend(headers);
289    Ok(response)
290}
291
292pub async fn login_form_handler(
293    State(data): State<Arc<AppState>>,
294    axum::Form(body): axum::Form<LoginUserSchema>,
295) -> impl IntoResponse {
296    match authenticate_user(&data, &body.email, &body.password).await {
297        Ok(cookies) => {
298            let mut headers = build_cookie_headers(&cookies);
299            headers.insert(header::LOCATION, "/".parse().unwrap());
300            (StatusCode::SEE_OTHER, headers, "").into_response()
301        }
302        Err((status, json_err)) => {
303            let msg = json_err
304                .get("message")
305                .and_then(|v| v.as_str())
306                .unwrap_or("Login failed");
307            (status, msg.to_string()).into_response()
308        }
309    }
310}
311
312pub async fn refresh_access_token_handler(
313    cookie_jar: CookieJar,
314    State(data): State<Arc<AppState>>,
315) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
316    let message = t!("could not refresh access token");
317
318    let refresh_token = cookie_jar
319        .get("refresh_token")
320        .map(|cookie| cookie.value().to_string())
321        .ok_or_else(|| {
322            let error_response = serde_json::json!({
323                "status": "fail",
324                "message": message
325            });
326            (StatusCode::FORBIDDEN, Json(error_response))
327        })?;
328
329    let refresh_token_details =
330        match crate::auth_keys::verify(&refresh_token, crate::token::TokenType::Refresh).await {
331            Some(token_details) => token_details,
332            None => {
333                let error_response = serde_json::json!({
334                    "status": "fail",
335                    "message": t!("Token is invalid or session has expired")
336                });
337                return Err((StatusCode::UNAUTHORIZED, Json(error_response)));
338            }
339        };
340
341    let mut redis_client = data
342        .redis_client
343        .get_multiplexed_async_connection()
344        .await
345        .map_err(|e| {
346            let msg = format!("{} {}", t!("Redis error:"), e);
347            log::error!("{msg}");
348            let error_response = serde_json::json!({
349                "status": "error",
350                "message": msg,
351            });
352            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
353        })?;
354
355    // Atomically consume the presented refresh session (GETDEL) so this refresh
356    // token is one-time-use: a concurrent replay finds it gone. The session is
357    // re-registered under a fresh uuid below.
358    let redis_token_user_id = redis_client
359        .get_del::<_, Option<String>>(refresh_token_details.token_uuid.to_string())
360        .await
361        .map_err(|e| {
362            let msg = format!("{} {}", t!("Redis error:"), e);
363            log::error!("{msg}");
364            let error_response = serde_json::json!({
365                "status": "error",
366                "message": msg,
367            });
368            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
369        })?
370        .ok_or_else(|| {
371            let msg = format!("{}", t!("Token is invalid or session has expired"));
372            log::error!("{msg}");
373            let error_response = serde_json::json!({
374                "status": "error",
375                "message": msg,
376            });
377            (StatusCode::UNAUTHORIZED, Json(error_response))
378        })?;
379
380    let user_id_uuid = uuid::Uuid::parse_str(&redis_token_user_id).map_err(|_| {
381        let msg = format!("{}", t!("Token is invalid or session has expired"));
382        log::error!("{msg}");
383        let error_response = serde_json::json!({
384            "status": "error",
385            "message": msg,
386        });
387        (StatusCode::UNAUTHORIZED, Json(error_response))
388    })?;
389
390    // Bind the verified JWT subject to the Redis session owner (see jwt_auth).
391    if user_id_uuid != refresh_token_details.user_id {
392        let error_response = serde_json::json!({
393            "status": "fail",
394            "message": t!("Token is invalid or session has expired")
395        });
396        return Err((StatusCode::UNAUTHORIZED, Json(error_response)));
397    }
398
399    let mut conn = get_connection().await.map_err(|e| {
400        let msg = format!("{} {}", t!("Database error:"), e);
401        log::error!("{msg}");
402        let error_response = serde_json::json!({
403            "status": "fail",
404            "message": msg,
405        });
406        (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
407    })?;
408
409    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)
410        .fetch_optional(&mut *conn)
411        .await
412        .map_err(|e| {
413            let msg = format!("{} {}", t!("Error fetching user from database:"), e);
414            log::error!("{msg}");
415            let error_response = serde_json::json!({
416                "status": "fail",
417                "message": msg,
418            });
419            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
420        })?;
421
422    let user = user.ok_or_else(|| {
423        let msg = format!(
424            "{}",
425            t!("The user belonging to this token no longer exists")
426        );
427        log::error!("{msg}");
428        let error_response = serde_json::json!({
429            "status": "fail",
430            "message": msg,
431        });
432        (StatusCode::UNAUTHORIZED, Json(error_response))
433    })?;
434
435    let access_token_details = mint_token(
436        user.id,
437        data.conf.access_token_max_age,
438        crate::token::TokenType::Access,
439    )
440    .await?;
441
442    save_token_data_to_redis(&data, &access_token_details, data.conf.access_token_max_age).await?;
443
444    // Rotate the refresh token too: the old one was consumed (GETDEL) above, so
445    // mint a fresh refresh token and register it. This keeps each refresh token
446    // one-time-use on this endpoint as well.
447    let refresh_token_details = mint_token(
448        user.id,
449        data.conf.refresh_token_max_age,
450        crate::token::TokenType::Refresh,
451    )
452    .await?;
453
454    save_token_data_to_redis(
455        &data,
456        &refresh_token_details,
457        data.conf.refresh_token_max_age,
458    )
459    .await?;
460
461    let access_cookie = Cookie::build((
462        "access_token",
463        access_token_details.token.clone().unwrap_or_default(),
464    ))
465    .path("/")
466    .max_age(time::Duration::minutes(data.conf.access_token_max_age * 60))
467    .same_site(SameSite::Lax)
468    .secure(crate::auth_keys::secure_cookies())
469    .http_only(true);
470
471    let refresh_cookie = Cookie::build((
472        "refresh_token",
473        refresh_token_details.token.unwrap_or_default(),
474    ))
475    .path("/")
476    .max_age(time::Duration::minutes(
477        data.conf.refresh_token_max_age * 60,
478    ))
479    .same_site(SameSite::Lax)
480    .secure(crate::auth_keys::secure_cookies())
481    .http_only(true);
482
483    let logged_in_cookie = Cookie::build(("logged_in", "true"))
484        .path("/")
485        .max_age(time::Duration::minutes(data.conf.access_token_max_age * 60))
486        .same_site(SameSite::Lax)
487        .http_only(false);
488
489    let mut response = Response::new(
490        json!({"status": "success", "access_token": access_token_details.token.unwrap()})
491            .to_string(),
492    );
493    let mut headers = HeaderMap::new();
494    headers.append(
495        header::SET_COOKIE,
496        access_cookie.to_string().parse().unwrap(),
497    );
498    headers.append(
499        header::SET_COOKIE,
500        refresh_cookie.to_string().parse().unwrap(),
501    );
502    headers.append(
503        header::SET_COOKIE,
504        logged_in_cookie.to_string().parse().unwrap(),
505    );
506
507    response.headers_mut().extend(headers);
508    Ok(response)
509}
510
511pub async fn logout_handler(
512    cookie_jar: CookieJar,
513    Extension(auth_guard): Extension<JWTAuthMiddleware>,
514    State(data): State<Arc<AppState>>,
515) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
516    let message = t!("Token is invalid or session has expired");
517
518    let refresh_token = cookie_jar
519        .get("refresh_token")
520        .map(|cookie| cookie.value().to_string())
521        .ok_or_else(|| {
522            let error_response = serde_json::json!({
523                "status": "fail",
524                "message": message
525            });
526            (StatusCode::FORBIDDEN, Json(error_response))
527        })?;
528
529    let refresh_token_details =
530        match crate::auth_keys::verify(&refresh_token, crate::token::TokenType::Refresh).await {
531            Some(token_details) => token_details,
532            None => {
533                let error_response = serde_json::json!({
534                    "status": "fail",
535                    "message": t!("Token is invalid or session has expired")
536                });
537                return Err((StatusCode::UNAUTHORIZED, Json(error_response)));
538            }
539        };
540
541    let mut redis_client = data
542        .redis_client
543        .get_multiplexed_async_connection()
544        .await
545        .map_err(|e| {
546            let msg = format!("{} {}", t!("Redis error:"), e);
547            log::error!("{msg}");
548            let error_response = serde_json::json!({
549                "status": "error",
550                "message": msg,
551            });
552            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
553        })?;
554
555    // Bind the refresh token to the authenticated principal: the access token
556    // (auth_guard) and the refresh token must belong to the same user, so a
557    // client can't present user A's access token with user B's refresh token to
558    // invalidate B's session.
559    if refresh_token_details.user_id != auth_guard.user.id {
560        let error_response = serde_json::json!({
561            "status": "fail",
562            "message": t!("Token is invalid or session has expired")
563        });
564        return Err((StatusCode::UNAUTHORIZED, Json(error_response)));
565    }
566
567    let _: bool = redis_client
568        .del(&[
569            refresh_token_details.token_uuid.to_string(),
570            auth_guard.access_token_uuid.to_string(),
571        ])
572        .await
573        .map_err(|e| {
574            let error_response = serde_json::json!({
575                "status": "error",
576                "message": format_args!("{:?}", e)
577            });
578            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
579        })?;
580
581    let access_cookie = Cookie::build(("access_token", ""))
582        .path("/")
583        .max_age(time::Duration::minutes(-1))
584        .same_site(SameSite::Lax)
585        .secure(crate::auth_keys::secure_cookies())
586        .http_only(true);
587    let refresh_cookie = Cookie::build(("refresh_token", ""))
588        .path("/")
589        .max_age(time::Duration::minutes(-1))
590        .same_site(SameSite::Lax)
591        .secure(crate::auth_keys::secure_cookies())
592        .http_only(true);
593
594    let logged_in_cookie = Cookie::build(("logged_in", "true"))
595        .path("/")
596        .max_age(time::Duration::minutes(-1))
597        .same_site(SameSite::Lax)
598        .http_only(false);
599
600    let mut headers = HeaderMap::new();
601    headers.append(
602        header::SET_COOKIE,
603        access_cookie.to_string().parse().unwrap(),
604    );
605    headers.append(
606        header::SET_COOKIE,
607        refresh_cookie.to_string().parse().unwrap(),
608    );
609    headers.append(
610        header::SET_COOKIE,
611        logged_in_cookie.to_string().parse().unwrap(),
612    );
613    headers.append("HX-Redirect", "/".to_string().parse().unwrap());
614
615    let mut response = Response::new(json!({"status": "success"}).to_string());
616    response.headers_mut().extend(headers);
617    Ok(response)
618}
619
620pub async fn get_me_handler(
621    Extension(jwtauth): Extension<JWTAuthMiddleware>,
622) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
623    let json_response = serde_json::json!({
624        "status":  "success",
625        "data": serde_json::json!({
626            "user": filter_user_record(&jwtauth.user)
627        })
628    });
629
630    Ok(Json(json_response))
631}
632
633pub async fn get_version() -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
634    if let Some(CmdResult::String(version)) = server::command::config::GetVersion::new()
635        .run()
636        .await
637        .map_err(|_| {
638            (
639                StatusCode::INTERNAL_SERVER_ERROR,
640                Json(json!("Can't get version")),
641            )
642        })?
643        && let Some(CmdResult::String(build_date)) = server::command::config::GetBuildDate::new()
644            .run()
645            .await
646            .map_err(|_| {
647                (
648                    StatusCode::INTERNAL_SERVER_ERROR,
649                    Json(json!("Can't get version")),
650                )
651            })?
652    {
653        Ok(Response::new(format!(
654            "<span class=\"version\">{version}</span><span class=\"build_date\" data-iso=\"{build_date}\"><script>
655    const el = document.currentScript.parentElement;
656    el.textContent = new Date(el.dataset.iso.trim()).toLocaleString();
657  </script></span>"
658        )))
659    } else {
660        Ok(Response::new("Unversioned".to_string()))
661    }
662}
663
664pub async fn get_logout_link() -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
665    Ok(Response::new(
666        "<a hx-get=\"/api/auth/logout\">Logout</a>".to_string(),
667    ))
668}
669
670pub async fn get_home_link() -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
671    Ok(Response::new("<a href='/'>Back to main</a>".to_string()))
672}
673
674fn filter_user_record(user: &User) -> FilteredUser {
675    FilteredUser {
676        id: user.id.to_string(),
677        email: user.email.clone(),
678        name: user.name.clone(),
679        photo: user.photo.clone(),
680        role: user.role.clone(),
681        verified: user.verified,
682        createdAt: user.created_at.unwrap(),
683        updatedAt: user.updated_at.unwrap(),
684    }
685}
686
687async fn mint_token(
688    user_id: uuid::Uuid,
689    max_age: i64,
690    token_type: crate::token::TokenType,
691) -> Result<TokenDetails, (StatusCode, Json<serde_json::Value>)> {
692    crate::auth_keys::mint(user_id, max_age, token_type)
693        .await
694        .map_err(|e| {
695            let msg = format!("{} {}", t!("error generating token:"), e);
696            log::error!("{msg}");
697            let error_response = serde_json::json!({
698                "status": "error",
699                "message": msg,
700            });
701            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
702        })
703}
704
705async fn save_token_data_to_redis(
706    data: &Arc<AppState>,
707    token_details: &TokenDetails,
708    max_age: i64,
709) -> Result<(), (StatusCode, Json<serde_json::Value>)> {
710    let mut redis_client = data
711        .redis_client
712        .get_multiplexed_async_connection()
713        .await
714        .map_err(|e| {
715            let msg = format!("{} {}", t!("Redis error:"), e);
716            log::error!("{msg}");
717            let error_response = serde_json::json!({
718                "status": "error",
719                "message": msg,
720            });
721            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
722        })?;
723    let _: bool = redis_client
724        .set_ex(
725            token_details.token_uuid.to_string(),
726            token_details.user_id.to_string(),
727            (max_age * 60) as u64,
728        )
729        .await
730        .map_err(|e| {
731            let error_response = serde_json::json!({
732                "status": "error",
733                "message": format_args!("{}", e),
734            });
735            (StatusCode::UNPROCESSABLE_ENTITY, Json(error_response))
736        })?;
737    Ok(())
738}