1
use std::sync::Arc;
2

            
3
use argon2::{Argon2, PasswordHasher};
4
use axum::{
5
    Extension, Json,
6
    extract::State,
7
    http::{HeaderMap, Response, StatusCode, header},
8
    response::IntoResponse,
9
};
10
use axum_extra::extract::{
11
    CookieJar,
12
    cookie::{Cookie, SameSite},
13
};
14
use serde_json::json;
15

            
16
use crate::{
17
    AppState,
18
    jwt_auth::JWTAuthMiddleware,
19
    model::{LoginUserSchema, RegisterUserSchema, User},
20
    response::FilteredUser,
21
    token::TokenDetails,
22
};
23
use rust_i18n::t;
24

            
25
use redis::AsyncCommands;
26
use server::command::CmdResult;
27

            
28
1
pub async fn register_user_handler(
29
1
    State(_data): State<Arc<AppState>>,
30
1
    Json(body): Json<RegisterUserSchema>,
31
1
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
32
1
    let user_exists = server::command::user::email_taken(&body.email)
33
1
        .await
34
1
        .map_err(|e| {
35
            let msg = format!("{} {}", t!("Database error:"), e);
36
            log::error!("{msg}");
37
            let error_response = serde_json::json!({
38
                "status": "fail",
39
                "message": msg,
40
            });
41
            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
42
        })?;
43

            
44
1
    if user_exists {
45
        let msg = format!("{}", t!("User with that email already exists"));
46
        log::error!("{msg}");
47
        let error_response = serde_json::json!({
48
            "status": "fail",
49
            "message": msg,
50
        });
51
        return Err((StatusCode::CONFLICT, Json(error_response)));
52
1
    }
53

            
54
1
    let hashed_password = Argon2::default()
55
1
        .hash_password(body.password.as_bytes())
56
1
        .map_err(|e| {
57
            let msg = format!("{} {}", t!("Error while hashing password:"), e);
58
            log::error!("{msg}");
59
            let error_response = serde_json::json!({
60
                "status": "fail",
61
                "message": msg,
62
            });
63
            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
64
        })
65
1
        .map(|hash| hash.to_string())?;
66
1
    let internal_error = |e: &dyn std::fmt::Display| {
67
        let msg = format!("{} {}", t!("Database error:"), e);
68
        log::error!("{msg}");
69
        let error_response = serde_json::json!({ "status": "fail", "message": msg });
70
        (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
71
    };
72

            
73
1
    let user_id = uuid::Uuid::new_v4();
74
    // Generate the user's signing keypair first (off the request thread); the
75
    // private half goes into their per-user DB, the public half into the global
76
    // users row.
77
1
    let keypair = server::auth_keys::generate()
78
1
        .await
79
1
        .map_err(|e| internal_error(&e))?;
80
1
    let provisioned = server::provision::provision_user_database(user_id)
81
1
        .await
82
1
        .map_err(|e| internal_error(&e))?;
83
1
    let database_name = provisioned.database_name;
84

            
85
    // Only what this request created may be cleaned up on failure below: if a
86
    // retry reused an existing database (42P04), its contents are somebody's
87
    // ledger and dropping it would destroy data we did not make.
88
1
    let drop_orphan = |user_id| async move {
89
        if !provisioned.created {
90
            return;
91
        }
92
        if let Err(drop_err) = server::provision::drop_user_database(user_id).await {
93
            log::error!(
94
                "{} {}",
95
                t!("Failed to clean up provisioned database:"),
96
                drop_err
97
            );
98
        }
99
    };
100

            
101
    if let Err(e) =
102
1
        server::provision::store_user_private_key(&database_name, &keypair.private_pem_b64).await
103
    {
104
        // The DB exists but key storage failed; drop the orphan so a retry
105
        // starts clean.
106
        drop_orphan(user_id).await;
107
        return Err(internal_error(&e));
108
1
    }
109

            
110
    // `verified` is set here, not left to the column default. Registration is
111
    // admin-gated (see `create_admin_pages_router`), so the admin creating the
112
    // account has already vouched for it — and nothing in the codebase ever
113
    // sets the flag afterwards: there is no verification endpoint and no email
114
    // flow. Left FALSE, the account authenticates and then fails every
115
    // authenticated request in `jwt_auth`, which is what happened to the first
116
    // real registration on 2026-09-06.
117
    //
118
1
    let insert = server::command::user::create_user(server::command::user::NewUser {
119
1
        id: user_id,
120
1
        name: &body.name,
121
1
        email: &body.email,
122
1
        password_hash: &hashed_password,
123
1
        database_name: &database_name,
124
1
        jwt_public_key: &keypair.public_pem_b64,
125
1
    })
126
1
    .await;
127

            
128
1
    let user = match insert {
129
1
        Ok(account) => User {
130
1
            id: account.id,
131
1
            name: account.name,
132
1
            email: account.email,
133
1
            role: account.role,
134
1
            photo: account.photo,
135
1
            verified: account.verified,
136
1
            created_at: account.created_at,
137
1
            updated_at: account.updated_at,
138
1
        },
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
            drop_orphan(user_id).await;
144
            let msg = format!("{} {}", t!("Database error:"), e);
145
            log::error!("{msg}");
146
            let error_response = serde_json::json!({
147
                "status": "fail",
148
                "message": msg,
149
            });
150
            return Err((StatusCode::INTERNAL_SERVER_ERROR, Json(error_response)));
151
        }
152
    };
153

            
154
1
    let user_response = serde_json::json!({"status": "success","data": serde_json::json!({
155
1
        "user": filter_user_record(&user)
156
    })});
157

            
158
1
    Ok(Json(user_response))
159
1
}
160

            
161
struct LoginCookies {
162
    access: Cookie<'static>,
163
    refresh: Cookie<'static>,
164
    logged_in: Cookie<'static>,
165
    access_token: String,
166
}
167

            
168
async fn authenticate_user(
169
    data: &Arc<AppState>,
170
    email: &str,
171
    password: &str,
172
) -> Result<LoginCookies, (StatusCode, Json<serde_json::Value>)> {
173
    let verified = server::command::user::verify_user_password(email, password)
174
        .await
175
        .map_err(|e| {
176
            let msg = format!("{} {}", t!("Database error:"), e);
177
            log::error!("{msg}");
178
            let error_response = serde_json::json!({
179
                "status": "error",
180
                "message": msg,
181
            });
182
            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
183
        })?
184
        .ok_or_else(|| {
185
            let msg = format!("{}", t!("Invalid email or password"));
186
            log::error!("{msg}");
187
            let error_response = serde_json::json!({
188
                "status": "fail",
189
                "message": msg,
190
            });
191
            (StatusCode::BAD_REQUEST, Json(error_response))
192
        })?;
193

            
194
    let access_token_details = mint_token(
195
        verified.user_id,
196
        data.conf.access_token_max_age,
197
        crate::token::TokenType::Access,
198
        verified.generation,
199
    )
200
    .await?;
201
    let refresh_token_details = mint_token(
202
        verified.user_id,
203
        data.conf.refresh_token_max_age,
204
        crate::token::TokenType::Refresh,
205
        verified.generation,
206
    )
207
    .await?;
208

            
209
    save_token_data_to_redis(data, &access_token_details, data.conf.access_token_max_age).await?;
210
    save_token_data_to_redis(
211
        data,
212
        &refresh_token_details,
213
        data.conf.refresh_token_max_age,
214
    )
215
    .await?;
216

            
217
    let access_cookie = Cookie::build((
218
        "access_token",
219
        access_token_details.token.clone().unwrap_or_default(),
220
    ))
221
    .path("/")
222
    .max_age(time::Duration::minutes(data.conf.access_token_max_age))
223
    .same_site(SameSite::Lax)
224
    .secure(crate::auth_keys::secure_cookies())
225
    .http_only(true)
226
    .build();
227

            
228
    let refresh_cookie = Cookie::build((
229
        "refresh_token",
230
        refresh_token_details.token.unwrap_or_default(),
231
    ))
232
    .path("/")
233
    .max_age(time::Duration::minutes(data.conf.refresh_token_max_age))
234
    .same_site(SameSite::Lax)
235
    .secure(crate::auth_keys::secure_cookies())
236
    .http_only(true)
237
    .build();
238

            
239
    let logged_in_cookie = Cookie::build(("logged_in", "true"))
240
        .path("/")
241
        .max_age(time::Duration::minutes(data.conf.access_token_max_age))
242
        .same_site(SameSite::Lax)
243
        .http_only(false)
244
        .build();
245

            
246
    Ok(LoginCookies {
247
        access: access_cookie,
248
        refresh: refresh_cookie,
249
        logged_in: logged_in_cookie,
250
        access_token: access_token_details.token.unwrap_or_default(),
251
    })
252
}
253

            
254
/// Appends one `Set-Cookie` per rendered cookie. A cookie we built ourselves
255
/// always parses, so a value that somehow does not is dropped rather than
256
/// panicking the request.
257
1
pub(crate) fn append_set_cookies(
258
1
    headers: &mut HeaderMap,
259
1
    cookies: impl IntoIterator<Item = String>,
260
1
) {
261
3
    for cookie in cookies {
262
3
        if let Ok(value) = cookie.parse() {
263
3
            headers.append(header::SET_COOKIE, value);
264
3
        }
265
    }
266
1
}
267

            
268
fn build_cookie_headers(cookies: &LoginCookies) -> HeaderMap {
269
    let mut headers = HeaderMap::new();
270
    append_set_cookies(
271
        &mut headers,
272
        [
273
            cookies.access.to_string(),
274
            cookies.refresh.to_string(),
275
            cookies.logged_in.to_string(),
276
        ],
277
    );
278
    headers
279
}
280

            
281
pub async fn login_user_handler(
282
    State(data): State<Arc<AppState>>,
283
    Json(body): Json<LoginUserSchema>,
284
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
285
    let cookies = authenticate_user(&data, &body.email, &body.password).await?;
286

            
287
    let mut response = Response::new(
288
        json!({"status": "success", "access_token": cookies.access_token}).to_string(),
289
    );
290
    let mut headers = build_cookie_headers(&cookies);
291
    headers.append("HX-Redirect", header::HeaderValue::from_static("/"));
292
    response.headers_mut().extend(headers);
293
    Ok(response)
294
}
295

            
296
pub async fn login_form_handler(
297
    State(data): State<Arc<AppState>>,
298
    axum::Form(body): axum::Form<LoginUserSchema>,
299
) -> impl IntoResponse {
300
    match authenticate_user(&data, &body.email, &body.password).await {
301
        Ok(cookies) => {
302
            let mut headers = build_cookie_headers(&cookies);
303
            headers.insert(header::LOCATION, header::HeaderValue::from_static("/"));
304
            (StatusCode::SEE_OTHER, headers, "").into_response()
305
        }
306
        Err((status, json_err)) => {
307
            let msg = json_err
308
                .get("message")
309
                .and_then(|v| v.as_str())
310
                .unwrap_or("Login failed");
311
            (status, msg.to_string()).into_response()
312
        }
313
    }
314
}
315

            
316
3
pub async fn refresh_access_token_handler(
317
3
    cookie_jar: CookieJar,
318
3
    Extension(jwt_auth): Extension<crate::jwt_auth::JWTAuthMiddleware>,
319
3
    State(data): State<Arc<AppState>>,
320
3
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
321
    // The middleware ahead of this handler refreshes a dead access token on the
322
    // way in, consuming the cookie this request still carries: report that
323
    // refresh instead of redeeming a token that is already gone.
324
3
    if jwt_auth.refreshed {
325
        return Ok(Response::new(
326
            json!({"status": "success", "refreshed_by": "session"}).to_string(),
327
        ));
328
3
    }
329

            
330
3
    let message = t!("could not refresh access token");
331

            
332
3
    let refresh_token = cookie_jar
333
3
        .get("refresh_token")
334
3
        .map(|cookie| cookie.value().to_string())
335
3
        .ok_or_else(|| {
336
2
            let error_response = serde_json::json!({
337
2
                "status": "fail",
338
2
                "message": message
339
            });
340
2
            (StatusCode::FORBIDDEN, Json(error_response))
341
2
        })?;
342

            
343
    let refresh_token_details =
344
1
        match crate::auth_keys::verify(&refresh_token, crate::token::TokenType::Refresh).await {
345
            Ok(Some(token_details)) => token_details,
346
            // An unreadable directory is not an expired session.
347
            Err(e) => {
348
                let error_response = serde_json::json!({
349
                    "status": "error",
350
                    "message": format!("{} {e}", t!("Database error:")),
351
                });
352
                return Err((StatusCode::SERVICE_UNAVAILABLE, Json(error_response)));
353
            }
354
            Ok(None) => {
355
1
                let error_response = serde_json::json!({
356
1
                    "status": "fail",
357
1
                    "message": t!("Token is invalid or session has expired")
358
                });
359
1
                return Err((StatusCode::UNAUTHORIZED, Json(error_response)));
360
            }
361
        };
362

            
363
    let mut redis_client = data
364
        .redis_client
365
        .get_multiplexed_async_connection()
366
        .await
367
        .map_err(|e| {
368
            let msg = format!("{} {}", t!("Redis error:"), e);
369
            log::error!("{msg}");
370
            let error_response = serde_json::json!({
371
                "status": "error",
372
                "message": msg,
373
            });
374
            (StatusCode::SERVICE_UNAVAILABLE, Json(error_response))
375
        })?;
376

            
377
    let user_id_uuid = refresh_token_details.user_id;
378

            
379
    // Bind the refresh token to the authenticated principal, as logout does:
380
    // the rotation deletes the caller's current access session, so a refresh
381
    // token belonging to someone else must not get that far.
382
    if user_id_uuid != jwt_auth.user.id {
383
        let error_response = serde_json::json!({
384
            "status": "fail",
385
            "message": t!("Token is invalid or session has expired")
386
        });
387
        return Err((StatusCode::UNAUTHORIZED, Json(error_response)));
388
    }
389

            
390
    // Minted BEFORE the rotation: minting touches the tenant database, and a
391
    // failure after the old session is consumed would log the user out. A
392
    // token minted and never registered is inert.
393
    let access_token_details = mint_token(
394
        user_id_uuid,
395
        data.conf.access_token_max_age,
396
        crate::token::TokenType::Access,
397
        refresh_token_details.generation,
398
    )
399
    .await?;
400
    let new_refresh_details = mint_token(
401
        user_id_uuid,
402
        data.conf.refresh_token_max_age,
403
        crate::token::TokenType::Refresh,
404
        refresh_token_details.generation,
405
    )
406
    .await?;
407

            
408
    let rotated = crate::jwt_auth::rotate_refresh(
409
        &mut redis_client,
410
        &crate::jwt_auth::Rotation {
411
            token_uuid: refresh_token_details.token_uuid,
412
            owner: user_id_uuid,
413
            successor: new_refresh_details.token_uuid,
414
            access_uuid: access_token_details.token_uuid,
415
            superseded: Some(jwt_auth.access_token_uuid),
416
            refresh_ttl: (data.conf.refresh_token_max_age * 60) as u64,
417
            access_ttl: (data.conf.access_token_max_age * 60) as u64,
418
        },
419
    )
420
    .await
421
    .map_err(|(status, body)| {
422
        (
423
            status,
424
            Json(serde_json::json!({"status": "error", "message": body.0.message})),
425
        )
426
    })?;
427
    if !rotated {
428
        let msg = format!("{}", t!("Token is invalid or session has expired"));
429
        log::error!("{msg}");
430
        return Err((
431
            StatusCode::UNAUTHORIZED,
432
            Json(serde_json::json!({"status": "error", "message": msg})),
433
        ));
434
    }
435

            
436
    let access_cookie = Cookie::build((
437
        "access_token",
438
        access_token_details.token.clone().unwrap_or_default(),
439
    ))
440
    .path("/")
441
    .max_age(time::Duration::minutes(data.conf.access_token_max_age))
442
    .same_site(SameSite::Lax)
443
    .secure(crate::auth_keys::secure_cookies())
444
    .http_only(true);
445

            
446
    let refresh_cookie = Cookie::build((
447
        "refresh_token",
448
        new_refresh_details.token.unwrap_or_default(),
449
    ))
450
    .path("/")
451
    .max_age(time::Duration::minutes(data.conf.refresh_token_max_age))
452
    .same_site(SameSite::Lax)
453
    .secure(crate::auth_keys::secure_cookies())
454
    .http_only(true);
455

            
456
    let logged_in_cookie = Cookie::build(("logged_in", "true"))
457
        .path("/")
458
        .max_age(time::Duration::minutes(data.conf.access_token_max_age))
459
        .same_site(SameSite::Lax)
460
        .http_only(false);
461

            
462
    let mut response = Response::new(
463
        json!({
464
            "status": "success",
465
            "access_token": access_token_details.token.clone().unwrap_or_default(),
466
        })
467
        .to_string(),
468
    );
469
    let mut headers = HeaderMap::new();
470
    append_set_cookies(
471
        &mut headers,
472
        [
473
            access_cookie.to_string(),
474
            refresh_cookie.to_string(),
475
            logged_in_cookie.to_string(),
476
        ],
477
    );
478

            
479
    response.headers_mut().extend(headers);
480
    Ok(response)
481
3
}
482

            
483
/// A session-store failure during logout.
484
fn redis_failed(e: redis::RedisError) -> (StatusCode, Json<serde_json::Value>) {
485
    let error_response = serde_json::json!({
486
        "status": "error",
487
        "message": format!("{e:?}"),
488
    });
489
    (StatusCode::SERVICE_UNAVAILABLE, Json(error_response))
490
}
491

            
492
pub async fn logout_handler(
493
    cookie_jar: CookieJar,
494
    Extension(auth_guard): Extension<JWTAuthMiddleware>,
495
    State(data): State<Arc<AppState>>,
496
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
497
    let message = t!("Token is invalid or session has expired");
498

            
499
    let refresh_token = cookie_jar
500
        .get("refresh_token")
501
        .map(|cookie| cookie.value().to_string())
502
        .ok_or_else(|| {
503
            let error_response = serde_json::json!({
504
                "status": "fail",
505
                "message": message
506
            });
507
            (StatusCode::FORBIDDEN, Json(error_response))
508
        })?;
509

            
510
    let refresh_token_details =
511
        match crate::auth_keys::verify(&refresh_token, crate::token::TokenType::Refresh).await {
512
            Ok(Some(token_details)) => token_details,
513
            // An unreadable directory is not an expired session.
514
            Err(e) => {
515
                let error_response = serde_json::json!({
516
                    "status": "error",
517
                    "message": format!("{} {e}", t!("Database error:")),
518
                });
519
                return Err((StatusCode::SERVICE_UNAVAILABLE, Json(error_response)));
520
            }
521
            Ok(None) => {
522
                let error_response = serde_json::json!({
523
                    "status": "fail",
524
                    "message": t!("Token is invalid or session has expired")
525
                });
526
                return Err((StatusCode::UNAUTHORIZED, Json(error_response)));
527
            }
528
        };
529

            
530
    let mut redis_client = data
531
        .redis_client
532
        .get_multiplexed_async_connection()
533
        .await
534
        .map_err(|e| {
535
            let msg = format!("{} {}", t!("Redis error:"), e);
536
            log::error!("{msg}");
537
            let error_response = serde_json::json!({
538
                "status": "error",
539
                "message": msg,
540
            });
541
            (StatusCode::SERVICE_UNAVAILABLE, Json(error_response))
542
        })?;
543

            
544
    // Bind the refresh token to the authenticated principal: the access token
545
    // (auth_guard) and the refresh token must belong to the same user, so a
546
    // client can't present user A's access token with user B's refresh token to
547
    // invalidate B's session.
548
    if refresh_token_details.user_id != auth_guard.user.id {
549
        let error_response = serde_json::json!({
550
            "status": "fail",
551
            "message": t!("Token is invalid or session has expired")
552
        });
553
        return Err((StatusCode::UNAUTHORIZED, Json(error_response)));
554
    }
555

            
556
    let _: () = redis_client
557
        .del(&[
558
            refresh_token_details.token_uuid.to_string(),
559
            auth_guard.access_token_uuid.to_string(),
560
        ])
561
        .await
562
        .map_err(redis_failed)?;
563

            
564
    let headers = session_end_headers("/");
565

            
566
    let mut response = Response::new(json!({"status": "success"}).to_string());
567
    response.headers_mut().extend(headers);
568
    Ok(response)
569
}
570

            
571
pub async fn get_me_handler(
572
    Extension(jwtauth): Extension<JWTAuthMiddleware>,
573
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
574
    let json_response = serde_json::json!({
575
        "status":  "success",
576
        "data": serde_json::json!({
577
            "user": filter_user_record(&jwtauth.user)
578
        })
579
    });
580

            
581
    Ok(Json(json_response))
582
}
583

            
584
/// Ends the session: expires all three cookies and redirects. Shared by logout
585
/// and the password change, which revokes the very token that made its request
586
/// and so must not leave the caller on a page whose every fetch now 401s.
587
1
pub fn session_end_headers(target: &'static str) -> HeaderMap {
588
3
    let expire = |name: &'static str, value: &'static str, http_only: bool| {
589
3
        Cookie::build((name, value))
590
3
            .path("/")
591
3
            .max_age(time::Duration::minutes(-1))
592
3
            .same_site(SameSite::Lax)
593
3
            .secure(crate::auth_keys::secure_cookies())
594
3
            .http_only(http_only)
595
3
            .to_string()
596
3
    };
597

            
598
1
    let mut headers = HeaderMap::new();
599
1
    append_set_cookies(
600
1
        &mut headers,
601
1
        [
602
1
            expire("access_token", "", true),
603
1
            expire("refresh_token", "", true),
604
1
            expire("logged_in", "true", false),
605
1
        ],
606
    );
607
1
    headers.append("HX-Redirect", header::HeaderValue::from_static(target));
608
1
    headers
609
1
}
610

            
611
#[derive(serde::Deserialize)]
612
pub struct ChangePasswordSchema {
613
    pub current_password: String,
614
    pub new_password: String,
615
    pub confirm_password: String,
616
}
617

            
618
/// Replaces the caller's password and kills every session including this one.
619
///
620
/// The bump invalidates the token that made this request, so the response
621
/// clears the cookies and the client is expected to log in again — leaving the
622
/// caller on a page whose every request has just started failing is worse than
623
/// sending them to `/login`.
624
pub async fn change_password_handler(
625
    Extension(jwtauth): Extension<JWTAuthMiddleware>,
626
    Json(body): Json<ChangePasswordSchema>,
627
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
628
    // Confirmed before anything is written: the change revokes every session
629
    // and there is no self-service recovery, so a single typo would lock the
630
    // account out until an operator resets it by hand.
631
    if body.new_password != body.confirm_password {
632
        return Err((
633
            StatusCode::BAD_REQUEST,
634
            Json(serde_json::json!({
635
                "status": "fail",
636
                "message": t!("The new passwords do not match").to_string(),
637
            })),
638
        ));
639
    }
640

            
641
    server::command::user::change_password(
642
        jwtauth.user.id,
643
        &body.current_password,
644
        &body.new_password,
645
    )
646
    .await
647
    .map_err(|e| {
648
        log::warn!(
649
            "password change refused for user_id={}: {e}",
650
            jwtauth.user.id
651
        );
652
        match e {
653
            server::command::CmdError::Args(_) => (
654
                StatusCode::BAD_REQUEST,
655
                Json(serde_json::json!({
656
                    "status": "fail",
657
                    "message": t!("Invalid email or password").to_string(),
658
                })),
659
            ),
660
            // A database outage is not a wrong password; saying so sends the
661
            // user off retyping a password that was correct.
662
            _ => (
663
                StatusCode::INTERNAL_SERVER_ERROR,
664
                Json(serde_json::json!({
665
                    "status": "error",
666
                    "message": t!("Database error:").to_string(),
667
                })),
668
            ),
669
        }
670
    })?;
671

            
672
    Ok((
673
        session_end_headers("/login"),
674
        Json(serde_json::json!({ "status": "success" })),
675
    ))
676
}
677

            
678
2
pub async fn get_version() -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
679
2
    if let Some(CmdResult::String(version)) = server::command::config::GetVersion::new()
680
2
        .run()
681
2
        .await
682
2
        .map_err(|_| {
683
            (
684
                StatusCode::INTERNAL_SERVER_ERROR,
685
                Json(json!("Can't get version")),
686
            )
687
        })?
688
2
        && let Some(CmdResult::String(build_date)) = server::command::config::GetBuildDate::new()
689
2
            .run()
690
2
            .await
691
2
            .map_err(|_| {
692
                (
693
                    StatusCode::INTERNAL_SERVER_ERROR,
694
                    Json(json!("Can't get version")),
695
                )
696
            })?
697
    {
698
2
        Ok(Response::new(format!(
699
2
            "<span class=\"version\">{version}</span><span class=\"build_date\" data-iso=\"{build_date}\"><script>
700
2
    const el = document.currentScript.parentElement;
701
2
    el.textContent = new Date(el.dataset.iso.trim()).toLocaleString();
702
2
  </script></span>"
703
2
        )))
704
    } else {
705
        Ok(Response::new("Unversioned".to_string()))
706
    }
707
2
}
708

            
709
4
pub async fn get_logout_link(
710
4
    Extension(jwtauth): Extension<JWTAuthMiddleware>,
711
4
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
712
    // `/register` is admin-gated server-side; showing the link to everyone
713
    // would only advertise a 401.
714
4
    let admin_link = match jwtauth.user.role.as_str() {
715
4
        "admin" => "<a href=\"/register\">Register user</a> ",
716
3
        _ => "",
717
    };
718
4
    Ok(Response::new(format!(
719
4
        "<a href=\"/account/ssh-key\">Account</a> {admin_link}\
720
4
         <a hx-get=\"/api/auth/logout\">Logout</a>"
721
4
    )))
722
4
}
723

            
724
pub async fn get_home_link() -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
725
    Ok(Response::new("<a href='/'>Back to main</a>".to_string()))
726
}
727

            
728
1
fn filter_user_record(user: &User) -> FilteredUser {
729
1
    FilteredUser {
730
1
        id: user.id.to_string(),
731
1
        email: user.email.clone(),
732
1
        name: user.name.clone(),
733
1
        photo: user.photo.clone(),
734
1
        role: user.role.clone(),
735
1
        verified: user.verified,
736
1
        createdAt: user.created_at.unwrap_or_default(),
737
1
        updatedAt: user.updated_at.unwrap_or_default(),
738
1
    }
739
1
}
740

            
741
async fn mint_token(
742
    user_id: uuid::Uuid,
743
    max_age: i64,
744
    token_type: crate::token::TokenType,
745
    generation: i32,
746
) -> Result<TokenDetails, (StatusCode, Json<serde_json::Value>)> {
747
    crate::auth_keys::mint(user_id, max_age, token_type, generation)
748
        .await
749
        .map_err(|e| {
750
            let msg = format!("{} {}", t!("error generating token:"), e);
751
            log::error!("{msg}");
752
            let error_response = serde_json::json!({
753
                "status": "error",
754
                "message": msg,
755
            });
756
            (e.status(), Json(error_response))
757
        })
758
}
759

            
760
async fn save_token_data_to_redis(
761
    data: &Arc<AppState>,
762
    token_details: &TokenDetails,
763
    max_age: i64,
764
) -> Result<(), (StatusCode, Json<serde_json::Value>)> {
765
    let mut redis_client = data
766
        .redis_client
767
        .get_multiplexed_async_connection()
768
        .await
769
        .map_err(|e| {
770
            let msg = format!("{} {}", t!("Redis error:"), e);
771
            log::error!("{msg}");
772
            let error_response = serde_json::json!({
773
                "status": "error",
774
                "message": msg,
775
            });
776
            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
777
        })?;
778
    let _: bool = redis_client
779
        .set_ex(
780
            token_details.token_uuid.to_string(),
781
            token_details.user_id.to_string(),
782
            (max_age * 60) as u64,
783
        )
784
        .await
785
        .map_err(|e| {
786
            let error_response = serde_json::json!({
787
                "status": "error",
788
                "message": format_args!("{}", e),
789
            });
790
            (StatusCode::UNPROCESSABLE_ENTITY, Json(error_response))
791
        })?;
792
    Ok(())
793
}