litecloud/api/src/auth.rs

90 lines
2.9 KiB
Rust

use rocket::request::{FromRequest, Outcome, Request};
use rocket::http::Status;
use serde::{Deserialize, Serialize};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation, Algorithm};
use chrono::{Utc, Duration};
use std::env;
use argon2::{Argon2, PasswordHash, PasswordVerifier, password_hash::SaltString, PasswordHasher};
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub sub: i32, // user id
pub role: String,
pub exp: usize,
}
pub fn hash_password(password: &str) -> Result<String, argon2::password_hash::Error> {
let salt = SaltString::generate(&mut rand::thread_rng());
Argon2::default().hash_password(password.as_bytes(), &salt).map(|h| h.to_string())
}
pub fn verify_password(password: &str, hash: &str) -> bool {
if let Ok(parsed_hash) = PasswordHash::new(hash) {
Argon2::default().verify_password(password.as_bytes(), &parsed_hash).is_ok()
} else {
false
}
}
pub fn generate_token(user_id: i32, role: &str) -> String {
let expiration = Utc::now() + Duration::days(7);
let claims = Claims {
sub: user_id,
role: role.to_string(),
exp: expiration.timestamp() as usize,
};
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes()))
.expect("JWT encoding failed")
}
pub fn decode_token(token: &str) -> Option<Claims> {
let secret = env::var("JWT_SECRET").ok()?;
decode::<Claims>(
token,
&DecodingKey::from_secret(secret.as_bytes()),
&Validation::new(Algorithm::HS256),
).ok().map(|data| data.claims)
}
#[derive(Serialize)]
pub struct AuthenticatedUser {
pub user_id: Option<i32>,
pub role: String,
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for AuthenticatedUser {
type Error = ();
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
if let Some(auth_header) = req.headers().get_one("Authorization") {
if let Some(token) = auth_header.strip_prefix("Bearer ") {
if let Some(claims) = decode_token(token) {
return Outcome::Success(Self {
user_id: Some(claims.sub),
role: claims.role,
});
}
}
}
Outcome::Error((Status::Unauthorized, ()))
}
}
pub struct AdminOnly(pub i32);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for AdminOnly {
type Error = ();
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
match AuthenticatedUser::from_request(req).await {
Outcome::Success(user) if user.role == "admin" && user.user_id.is_some() => Outcome::Success(AdminOnly(user.user_id.unwrap())),
_ => Outcome::Error((Status::Unauthorized, ()))
}
}
}