auth.rs
1 use std::{ 2 sync::{Arc, Mutex}, 3 time::{SystemTime, UNIX_EPOCH}, 4 }; 5 6 use axum::{ 7 Router, 8 extract::{FromRequest, Request, State}, 9 http::StatusCode, 10 middleware::{self, Next}, 11 response::{IntoResponse, Redirect, Response}, 12 }; 13 use bcrypt; 14 15 use crate::config::Config; 16 use axum::extract::Form; 17 use axum::http::header; 18 use jsonwebtoken::{DecodingKey, EncodingKey, Header, decode, encode}; 19 use serde::{Deserialize, Serialize}; 20 use sysinfo::System; 21 22 #[derive(Serialize, Deserialize)] 23 struct Claims { 24 exp: usize, 25 iat: usize, 26 } 27 28 #[derive(Deserialize)] 29 struct LoginForm { 30 password: String, 31 } 32 33 pub async fn auth_handler( 34 State((_, config)): State<(Arc<Mutex<System>>, Config)>, 35 request: Request, 36 ) -> impl IntoResponse { 37 // Extract form data 38 let pass = match Form::<LoginForm>::from_request(request, &()).await { 39 Ok(form) => form.password.clone(), 40 Err(_) => "".to_string(), 41 }; 42 43 // Check if password matches 44 if bcrypt::verify(pass, &config.password_hash.unwrap_or_default()).unwrap_or(false) { 45 // Create JWT token 46 let now = SystemTime::now() 47 .duration_since(UNIX_EPOCH) 48 .unwrap() 49 .as_secs(); 50 51 let claims = Claims { 52 exp: now as usize + 60 * 86400, // 60 days 53 iat: now as usize, 54 }; 55 56 let token = encode( 57 &Header::default(), 58 &claims, 59 &EncodingKey::from_secret(config.jwt_secret.as_bytes()), 60 ) 61 .unwrap_or_default(); 62 63 return Response::builder() 64 .status(StatusCode::OK) 65 .header( 66 header::SET_COOKIE, 67 format!( 68 "prheri_auth_token={}; Path=/; HttpOnly; SameSite=Strict; Max-Age=5184000", 69 token 70 ), 71 ) 72 .body("logged in".to_string()) 73 .unwrap(); 74 } 75 76 return Response::builder() 77 .status(StatusCode::UNAUTHORIZED) 78 .body("Unauthorized".to_string()) 79 .unwrap(); 80 } 81 82 // Middleware function to check authentication 83 async fn auth_middleware( 84 State(config): State<Config>, 85 request: Request, 86 next: Next, 87 ) -> Result<Response, StatusCode> { 88 // Skip authentication for root path to allow login page access 89 if request.uri().path() == "/auth" { 90 return Ok(next.run(request).await); 91 } 92 93 // Extract JWT token from cookie 94 let token = match request.headers().get("cookie") { 95 Some(cookie) => { 96 let cookie_str = cookie.to_str().unwrap_or_default(); 97 let token = cookie_str 98 .split(';') 99 .find(|c| c.contains("prheri_auth_token")) 100 .unwrap_or_default() 101 .split('=') 102 .nth(1) 103 .unwrap_or_default(); 104 105 token.to_string() 106 } 107 None => return Ok(Redirect::temporary("/auth").into_response()), 108 }; 109 110 // Verify JWT token 111 let token_data = match decode::<Claims>( 112 &token, 113 &DecodingKey::from_secret(config.jwt_secret.as_bytes()), 114 &jsonwebtoken::Validation::default(), 115 ) { 116 Ok(data) => data.claims, 117 Err(_) => return Ok(Redirect::temporary("/auth").into_response()), 118 }; 119 120 // Check if token is expired 121 let now = SystemTime::now() 122 .duration_since(UNIX_EPOCH) 123 .unwrap() 124 .as_secs() as usize; 125 if token_data.exp < now { 126 return Ok(Redirect::temporary("/auth").into_response()); 127 } 128 129 return Ok(next.run(request).await); 130 } 131 132 pub fn apply_auth_middleware(app: Router, config: Config) -> Router { 133 app.layer(middleware::from_fn_with_state(config, auth_middleware)) 134 }