90 lines
2.9 KiB
Rust
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, ()))
|
|
}
|
|
}
|
|
}
|