use crate::db::models::{FilteredUser, LoginUser, NewUser, RegisterUser, TokenClaims, User}; use crate::error_handle::internal_error; use crate::state::AppState; use argon2::{password_hash::SaltString, Argon2}; use argon2::{PasswordHash, PasswordHasher, PasswordVerifier}; use axum::extract::Request; use axum::Extension; use axum::{ body::Body, extract::State, http::{header, StatusCode}, middleware::Next, response::{IntoResponse, Response}, Json, }; use axum_extra::extract::cookie::{self, Cookie, SameSite}; use axum_extra::extract::CookieJar; use diesel::{connection, prelude::*}; use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; use rand_core::OsRng; use std::sync::Arc; pub async fn healthcheck() -> impl IntoResponse { Json(serde_json::json!({ "status": "success", "message": "healthy" })) } pub async fn register_user( State(state): State>, Json(body): Json, ) -> Result)> { use crate::db::schema::{users, users::dsl}; let connection = state.database.get().await.unwrap(); let (login, email) = (body.login.clone(), body.email.clone()); let user_exists = connection .interact(move |connection| { dsl::users .filter(dsl::login.eq(login).or(dsl::email.eq(email))) .select(User::as_select()) .first(connection) .optional() }) .await .map_err(internal_error)? .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "status": "fail", "message": format!("Database error: {}", e) })), ) })?; if user_exists.is_some() { return Err(( StatusCode::CONFLICT, Json(serde_json::json!({ "status": "fail", "message": "Login or email already exists" })), )); } let salt = SaltString::generate(&mut OsRng); let hashed_password = Argon2::default() .hash_password(body.password.as_bytes(), &salt) .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "status": "fail", "message": format!("Error while hashing password: {}", e) })), ) }) .map(|hash| hash.to_string())?; let user = NewUser { login: body.login.to_string(), hashed_password: hashed_password, name: body.name, email: body.email, is_admin: body.is_admin, }; let new_user = connection .interact(move |connection| { diesel::insert_into(users::table) .values(&user) .returning(User::as_returning()) .get_result(connection) }) .await .map_err(internal_error)? .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "status": "fail", "message": format!("Database error: {}", e) })), ) })?; let response = serde_json::json!({"status": "success", "data": serde_json::json!({"user": FilteredUser::from(&new_user)})}); Ok(Json(response)) } pub async fn login_user( State(state): State>, Json(body): Json, ) -> Result)> { use crate::db::schema::{users, users::dsl}; let connection = state.database.get().await.unwrap(); let user = connection .interact(move |connection| { dsl::users .filter(dsl::email.eq(body.email)) .select(User::as_select()) .first(connection) }) .await .map_err(internal_error)? .map_err(|e| { ( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "status": "fail", "message": format!("Invalid login or email: {}", e) })), ) })?; let is_valid = match PasswordHash::new(&user.hashed_password) { Ok(parsed_hash) => Argon2::default() .verify_password(body.password.as_bytes(), &parsed_hash) .map_or(false, |_| true), Err(_) => false, }; if !is_valid { return Err(( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "status": "fail", "message": "Invalid login, email or password" })), )); } let now = chrono::Utc::now(); let iat = now.timestamp() as usize; let exp = (now + chrono::Duration::try_minutes(60).unwrap()).timestamp() as usize; let claims = TokenClaims { sub: user.id.to_string(), exp, iat, }; let token = encode( &Header::default(), &claims, &EncodingKey::from_secret(state.config.jwt.secret.as_ref()), ) .unwrap(); let cookie = Cookie::build(("token", token.to_owned())) .path("/") .max_age(time::Duration::hours(1)) .same_site(SameSite::None) .secure(true) .http_only(true); let mut response = Response::new(serde_json::json!({"status": "success", "token": token}).to_string()); response .headers_mut() .insert(header::SET_COOKIE, cookie.to_string().parse().unwrap()); Ok(response) } pub async fn logout_user() -> Result)> { let cookie = Cookie::build(("token", "")) .path("/") .max_age(time::Duration::hours(-1)) .same_site(SameSite::None) .secure(true) .http_only(true); let mut response = Response::new(serde_json::json!({"status": "success"}).to_string()); response .headers_mut() .insert(header::SET_COOKIE, cookie.to_string().parse().unwrap()); Ok(response) } pub async fn jwt_auth( cookie_jar: CookieJar, State(state): State>, mut req: Request, next: Next, ) -> Result)> { let token = cookie_jar .get("token") .map(|cookie| cookie.value().to_string()) .or_else(|| { req.headers() .get(header::AUTHORIZATION) .and_then(|auth_header| auth_header.to_str().ok()) .and_then(|auth_value| { if auth_value.starts_with("Bearer ") { Some(auth_value[7..].to_owned()) } else { None } }) }); let token = token.ok_or_else(|| { ( StatusCode::UNAUTHORIZED, Json(serde_json::json!({ "status": "fail", "message": "Cannot login without token" })), ) })?; let claims = decode::( &token, &DecodingKey::from_secret(state.config.jwt.secret.as_ref()), &Validation::default(), ) .map_err(|_| { ( StatusCode::UNAUTHORIZED, Json(serde_json::json!({"status":"fail","message":"Invalid token"})), ) })? .claims; let user_id = uuid::Uuid::parse_str(&claims.sub).map_err(|_| { ( StatusCode::UNAUTHORIZED, Json(serde_json::json!({"status":"fail","message":"Invalid token"})), ) })?; use crate::db::schema::{users, users::dsl}; let connection = state.database.get().await.unwrap(); let user = connection .interact(move |connection| { dsl::users .filter(dsl::id.eq(user_id)) .select(User::as_select()) .first(connection) .optional() }) .await .map_err(internal_error)? .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "status": "fail", "message": format!("Database error: {}", e) })), ) })?; let user = user.ok_or_else(|| { ( StatusCode::UNAUTHORIZED, Json(serde_json::json!({ "status": "fail", "message": "The user belonging to this token no longer exists" })), ) })?; req.extensions_mut().insert(user); Ok(next.run(req).await) } pub async fn me( Extension(user): Extension, ) -> Result)> { Ok(Json( serde_json::json!({"status":"success","data":serde_json::json!({"user":FilteredUser::from(&user)})}), )) }