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;
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 user_exists = server::command::user::email_taken(&body.email)
33 .await
34 .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 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 }
53
54 let hashed_password = Argon2::default()
55 .hash_password(body.password.as_bytes())
56 .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 .map(|hash| hash.to_string())?;
66 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 let user_id = uuid::Uuid::new_v4();
74 let keypair = server::auth_keys::generate()
78 .await
79 .map_err(|e| internal_error(&e))?;
80 let provisioned = server::provision::provision_user_database(user_id)
81 .await
82 .map_err(|e| internal_error(&e))?;
83 let database_name = provisioned.database_name;
84
85 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 server::provision::store_user_private_key(&database_name, &keypair.private_pem_b64).await
103 {
104 drop_orphan(user_id).await;
107 return Err(internal_error(&e));
108 }
109
110 let insert = server::command::user::create_user(server::command::user::NewUser {
119 id: user_id,
120 name: &body.name,
121 email: &body.email,
122 password_hash: &hashed_password,
123 database_name: &database_name,
124 jwt_public_key: &keypair.public_pem_b64,
125 })
126 .await;
127
128 let user = match insert {
129 Ok(account) => User {
130 id: account.id,
131 name: account.name,
132 email: account.email,
133 role: account.role,
134 photo: account.photo,
135 verified: account.verified,
136 created_at: account.created_at,
137 updated_at: account.updated_at,
138 },
139 Err(e) => {
140 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 let user_response = serde_json::json!({"status": "success","data": serde_json::json!({
155 "user": filter_user_record(&user)
156 })});
157
158 Ok(Json(user_response))
159}
160
161struct LoginCookies {
162 access: Cookie<'static>,
163 refresh: Cookie<'static>,
164 logged_in: Cookie<'static>,
165 access_token: String,
166}
167
168async 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
254pub(crate) fn append_set_cookies(
258 headers: &mut HeaderMap,
259 cookies: impl IntoIterator<Item = String>,
260) {
261 for cookie in cookies {
262 if let Ok(value) = cookie.parse() {
263 headers.append(header::SET_COOKIE, value);
264 }
265 }
266}
267
268fn 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
281pub 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
296pub 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
316pub async fn refresh_access_token_handler(
317 cookie_jar: CookieJar,
318 Extension(jwt_auth): Extension<crate::jwt_auth::JWTAuthMiddleware>,
319 State(data): State<Arc<AppState>>,
320) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
321 if jwt_auth.refreshed {
325 return Ok(Response::new(
326 json!({"status": "success", "refreshed_by": "session"}).to_string(),
327 ));
328 }
329
330 let message = t!("could not refresh access token");
331
332 let refresh_token = cookie_jar
333 .get("refresh_token")
334 .map(|cookie| cookie.value().to_string())
335 .ok_or_else(|| {
336 let error_response = serde_json::json!({
337 "status": "fail",
338 "message": message
339 });
340 (StatusCode::FORBIDDEN, Json(error_response))
341 })?;
342
343 let refresh_token_details =
344 match crate::auth_keys::verify(&refresh_token, crate::token::TokenType::Refresh).await {
345 Ok(Some(token_details)) => token_details,
346 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 let error_response = serde_json::json!({
356 "status": "fail",
357 "message": t!("Token is invalid or session has expired")
358 });
359 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 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 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}
482
483fn 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
492pub 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 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 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
571pub 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
584pub fn session_end_headers(target: &'static str) -> HeaderMap {
588 let expire = |name: &'static str, value: &'static str, http_only: bool| {
589 Cookie::build((name, value))
590 .path("/")
591 .max_age(time::Duration::minutes(-1))
592 .same_site(SameSite::Lax)
593 .secure(crate::auth_keys::secure_cookies())
594 .http_only(http_only)
595 .to_string()
596 };
597
598 let mut headers = HeaderMap::new();
599 append_set_cookies(
600 &mut headers,
601 [
602 expire("access_token", "", true),
603 expire("refresh_token", "", true),
604 expire("logged_in", "true", false),
605 ],
606 );
607 headers.append("HX-Redirect", header::HeaderValue::from_static(target));
608 headers
609}
610
611#[derive(serde::Deserialize)]
612pub struct ChangePasswordSchema {
613 pub current_password: String,
614 pub new_password: String,
615 pub confirm_password: String,
616}
617
618pub 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 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 _ => (
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
678pub async fn get_version() -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
679 if let Some(CmdResult::String(version)) = server::command::config::GetVersion::new()
680 .run()
681 .await
682 .map_err(|_| {
683 (
684 StatusCode::INTERNAL_SERVER_ERROR,
685 Json(json!("Can't get version")),
686 )
687 })?
688 && let Some(CmdResult::String(build_date)) = server::command::config::GetBuildDate::new()
689 .run()
690 .await
691 .map_err(|_| {
692 (
693 StatusCode::INTERNAL_SERVER_ERROR,
694 Json(json!("Can't get version")),
695 )
696 })?
697 {
698 Ok(Response::new(format!(
699 "<span class=\"version\">{version}</span><span class=\"build_date\" data-iso=\"{build_date}\"><script>
700 const el = document.currentScript.parentElement;
701 el.textContent = new Date(el.dataset.iso.trim()).toLocaleString();
702 </script></span>"
703 )))
704 } else {
705 Ok(Response::new("Unversioned".to_string()))
706 }
707}
708
709pub async fn get_logout_link(
710 Extension(jwtauth): Extension<JWTAuthMiddleware>,
711) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
712 let admin_link = match jwtauth.user.role.as_str() {
715 "admin" => "<a href=\"/register\">Register user</a> ",
716 _ => "",
717 };
718 Ok(Response::new(format!(
719 "<a href=\"/account/ssh-key\">Account</a> {admin_link}\
720 <a hx-get=\"/api/auth/logout\">Logout</a>"
721 )))
722}
723
724pub 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
728fn filter_user_record(user: &User) -> FilteredUser {
729 FilteredUser {
730 id: user.id.to_string(),
731 email: user.email.clone(),
732 name: user.name.clone(),
733 photo: user.photo.clone(),
734 role: user.role.clone(),
735 verified: user.verified,
736 createdAt: user.created_at.unwrap_or_default(),
737 updatedAt: user.updated_at.unwrap_or_default(),
738 }
739}
740
741async 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
760async 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}