use crate::{HandlerError, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthConfig {
pub jwt_secret: String,
pub jwt_expiration_hours: u64,
pub bcrypt_cost: u32,
pub session_timeout_minutes: u64,
pub allowed_origins: Vec<String>,
pub enable_cors: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserCredentials {
pub username: String,
pub password: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JwtClaims {
pub sub: String, pub exp: u64, pub iat: u64, pub iss: String, pub aud: String, pub role: Option<String>, pub permissions: Vec<String>, }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum UserRole {
Admin,
User,
Guest,
Custom(String),
}
pub struct AuthMiddleware {
config: AuthConfig,
}
impl AuthMiddleware {
pub fn new(config: AuthConfig) -> Self {
Self { config }
}
#[cfg(feature = "jsonwebtoken")]
pub fn generate_jwt(&self, user_id: &str, role: Option<&str>) -> Result<String> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| HandlerError::Jsonnet(format!("Time error: {}", e)))?
.as_secs();
let claims = JwtClaims {
sub: user_id.to_string(),
exp: now + (self.config.jwt_expiration_hours * 3600),
iat: now,
iss: "kotoba-auth".to_string(),
aud: "kotoba-app".to_string(),
role: role.map(|s| s.to_string()),
permissions: vec![], };
jsonwebtoken::encode(
&jsonwebtoken::Header::default(),
&claims,
&jsonwebtoken::EncodingKey::from_secret(self.config.jwt_secret.as_bytes()),
)
.map_err(|e| HandlerError::Jsonnet(format!("JWT encoding error: {}", e)))
}
#[cfg(feature = "jsonwebtoken")]
pub fn verify_jwt(&self, token: &str) -> Result<JwtClaims> {
let validation = jsonwebtoken::Validation::default();
let token_data = jsonwebtoken::decode::<JwtClaims>(
token,
&jsonwebtoken::DecodingKey::from_secret(self.config.jwt_secret.as_bytes()),
&validation,
)
.map_err(|e| HandlerError::Jsonnet(format!("JWT decoding error: {}", e)))?;
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| HandlerError::Jsonnet(format!("Time error: {}", e)))?
.as_secs();
if token_data.claims.exp < now {
return Err(HandlerError::Jsonnet("JWT token has expired".to_string()));
}
Ok(token_data.claims)
}
#[cfg(feature = "bcrypt")]
pub fn hash_password(&self, password: &str) -> Result<String> {
bcrypt::hash(password, self.config.bcrypt_cost)
.map_err(|e| HandlerError::Jsonnet(format!("Password hashing error: {}", e)))
}
#[cfg(feature = "bcrypt")]
pub fn verify_password(&self, password: &str, hash: &str) -> Result<bool> {
bcrypt::verify(password, hash)
.map_err(|e| HandlerError::Jsonnet(format!("Password verification error: {}", e)))
}
pub fn add_cors_headers(&self, response_headers: &mut HashMap<String, String>, origin: Option<&str>) {
if !self.config.enable_cors {
return;
}
response_headers.insert("Access-Control-Allow-Origin".to_string(),
origin.unwrap_or("*").to_string());
response_headers.insert("Access-Control-Allow-Methods".to_string(),
"GET, POST, PUT, DELETE, OPTIONS".to_string());
response_headers.insert("Access-Control-Allow-Headers".to_string(),
"Content-Type, Authorization, X-Requested-With".to_string());
response_headers.insert("Access-Control-Max-Age".to_string(), "86400".to_string());
}
pub fn is_origin_allowed(&self, origin: &str) -> bool {
if self.config.allowed_origins.is_empty() {
return true; }
self.config.allowed_origins.iter().any(|allowed| allowed == origin)
}
pub fn validate_session(&self, session_id: &str, user_id: &str) -> Result<bool> {
if session_id.len() < 32 {
return Ok(false);
}
if user_id.is_empty() {
return Ok(false);
}
Ok(true)
}
pub fn check_permission(&self, user_role: &UserRole, required_permission: &str) -> bool {
match user_role {
UserRole::Admin => true, UserRole::User => {
matches!(required_permission,
"read_profile" | "update_profile" | "read_posts" | "create_posts")
}
UserRole::Guest => {
matches!(required_permission, "read_posts" | "read_public")
}
UserRole::Custom(role) => {
match role.as_str() {
"editor" => matches!(required_permission,
"read_posts" | "create_posts" | "update_posts" | "delete_posts"),
"moderator" => matches!(required_permission,
"read_posts" | "update_posts" | "moderate_comments"),
_ => false,
}
}
}
}
pub fn config(&self) -> &AuthConfig {
&self.config
}
}
pub struct SessionManager {
sessions: HashMap<String, SessionData>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionData {
pub user_id: String,
pub created_at: u64,
pub expires_at: u64,
pub data: HashMap<String, serde_json::Value>,
}
impl SessionManager {
pub fn new() -> Self {
Self {
sessions: HashMap::new(),
}
}
pub fn create_session(&mut self, user_id: &str, timeout_minutes: u64) -> Result<String> {
let session_id = uuid::Uuid::new_v4().to_string();
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| HandlerError::Jsonnet(format!("Time error: {}", e)))?
.as_secs();
let session_data = SessionData {
user_id: user_id.to_string(),
created_at: now,
expires_at: now + (timeout_minutes * 60),
data: HashMap::new(),
};
self.sessions.insert(session_id.clone(), session_data);
Ok(session_id)
}
pub fn get_session(&self, session_id: &str) -> Option<&SessionData> {
self.sessions.get(session_id)
}
pub fn validate_session(&self, session_id: &str) -> Result<bool> {
if let Some(session) = self.sessions.get(session_id) {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| HandlerError::Jsonnet(format!("Time error: {}", e)))?
.as_secs();
Ok(session.expires_at > now)
} else {
Ok(false)
}
}
pub fn update_session(&mut self, session_id: &str, data: HashMap<String, serde_json::Value>) -> Result<()> {
if let Some(session) = self.sessions.get_mut(session_id) {
session.data = data;
Ok(())
} else {
Err(HandlerError::Jsonnet("Session not found".to_string()))
}
}
pub fn delete_session(&mut self, session_id: &str) -> Result<()> {
if self.sessions.remove(session_id).is_some() {
Ok(())
} else {
Err(HandlerError::Jsonnet("Session not found".to_string()))
}
}
pub fn cleanup_expired_sessions(&mut self) -> Result<usize> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| HandlerError::Jsonnet(format!("Time error: {}", e)))?
.as_secs();
let expired_count = self.sessions.len();
self.sessions.retain(|_, session| session.expires_at > now);
Ok(expired_count - self.sessions.len())
}
pub fn active_sessions_count(&self) -> usize {
self.sessions.len()
}
}
pub struct PasswordUtils;
impl PasswordUtils {
pub fn check_password_strength(password: &str) -> Result<(bool, Vec<String>)> {
let mut issues = Vec::new();
let mut is_strong = true;
if password.len() < 8 {
issues.push("Password must be at least 8 characters long".to_string());
is_strong = false;
}
if !password.chars().any(|c| c.is_uppercase()) {
issues.push("Password must contain at least one uppercase letter".to_string());
is_strong = false;
}
if !password.chars().any(|c| c.is_lowercase()) {
issues.push("Password must contain at least one lowercase letter".to_string());
is_strong = false;
}
if !password.chars().any(|c| c.is_numeric()) {
issues.push("Password must contain at least one number".to_string());
is_strong = false;
}
if !password.chars().any(|c| !c.is_alphanumeric()) {
issues.push("Password must contain at least one special character".to_string());
is_strong = false;
}
Ok((is_strong, issues))
}
pub fn generate_secure_password(length: usize) -> String {
use rand::Rng;
let charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ\
abcdefghijklmnopqrstuvwxyz\
0123456789\
!@#$%^&*()_+-=[]{}|;:,.<>?";
let mut rng = rand::thread_rng();
let password: String = (0..length)
.map(|_| {
let idx = rng.gen_range(0..charset.len());
charset.chars().nth(idx).unwrap()
})
.collect();
password
}
}
pub struct RateLimiter {
requests: HashMap<String, Vec<u64>>,
max_requests: u32,
window_seconds: u64,
}
impl RateLimiter {
pub fn new(max_requests: u32, window_seconds: u64) -> Self {
Self {
requests: HashMap::new(),
max_requests,
window_seconds,
}
}
pub fn check_rate_limit(&mut self, identifier: &str) -> Result<bool> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| HandlerError::Jsonnet(format!("Time error: {}", e)))?
.as_secs();
let window_start = now.saturating_sub(self.window_seconds);
if let Some(requests) = self.requests.get_mut(identifier) {
requests.retain(|×tamp| timestamp > window_start);
}
let current_requests = self.requests
.get(identifier)
.map(|requests| requests.len())
.unwrap_or(0);
if current_requests >= self.max_requests as usize {
return Ok(false); }
self.requests
.entry(identifier.to_string())
.or_insert_with(Vec::new)
.push(now);
Ok(true)
}
pub fn remaining_requests(&self, identifier: &str) -> u32 {
let current_requests = self.requests
.get(identifier)
.map(|requests| requests.len())
.unwrap_or(0);
self.max_requests.saturating_sub(current_requests as u32)
}
pub fn reset_time(&self, identifier: &str) -> Option<u64> {
self.requests.get(identifier)
.and_then(|requests| requests.first())
.map(|first_request| first_request + self.window_seconds)
}
}
#[cfg(feature = "jsonwebtoken")]
pub struct OAuthHelper {
client_id: String,
client_secret: String,
redirect_uri: String,
}
#[cfg(feature = "jsonwebtoken")]
impl OAuthHelper {
pub fn new(client_id: &str, client_secret: &str, redirect_uri: &str) -> Self {
Self {
client_id: client_id.to_string(),
client_secret: client_secret.to_string(),
redirect_uri: redirect_uri.to_string(),
}
}
pub fn generate_google_oauth_url(&self, state: &str) -> String {
format!(
"https://accounts.google.com/o/oauth2/v2/auth?\
response_type=code&\
client_id={}&\
redirect_uri={}&\
scope=openid%20email%20profile&\
state={}",
self.client_id,
urlencoding::encode(&self.redirect_uri),
urlencoding::encode(state)
)
}
pub fn generate_github_oauth_url(&self, state: &str) -> String {
format!(
"https://github.com/login/oauth/authorize?\
client_id={}&\
redirect_uri={}&\
scope=user:email&\
state={}",
self.client_id,
urlencoding::encode(&self.redirect_uri),
urlencoding::encode(state)
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_password_strength_check() {
let (is_strong, issues) = PasswordUtils::check_password_strength("weak").unwrap();
assert!(!is_strong);
assert!(issues.len() > 0);
let (is_strong, issues) = PasswordUtils::check_password_strength("StrongPass123!").unwrap();
assert!(is_strong);
assert_eq!(issues.len(), 0);
}
#[test]
fn test_generate_secure_password() {
let password = PasswordUtils::generate_secure_password(12);
assert_eq!(password.len(), 12);
let has_upper = password.chars().any(|c| c.is_uppercase());
let has_lower = password.chars().any(|c| c.is_lowercase());
let has_digit = password.chars().any(|c| c.is_numeric());
let has_special = password.chars().any(|c| !c.is_alphanumeric());
assert!(has_upper || has_lower || has_digit || has_special);
}
#[test]
fn test_user_role_permissions() {
let auth = AuthMiddleware::new(AuthConfig {
jwt_secret: "test".to_string(),
jwt_expiration_hours: 24,
bcrypt_cost: 4,
session_timeout_minutes: 60,
allowed_origins: vec![],
enable_cors: false,
});
assert!(auth.check_permission(&UserRole::Admin, "delete_users"));
assert!(auth.check_permission(&UserRole::Admin, "read_system_logs"));
assert!(auth.check_permission(&UserRole::User, "read_profile"));
assert!(auth.check_permission(&UserRole::User, "create_posts"));
assert!(!auth.check_permission(&UserRole::User, "delete_users"));
assert!(auth.check_permission(&UserRole::Guest, "read_posts"));
assert!(auth.check_permission(&UserRole::Guest, "read_public"));
assert!(!auth.check_permission(&UserRole::Guest, "create_posts"));
assert!(auth.check_permission(&UserRole::Custom("editor".to_string()), "update_posts"));
assert!(!auth.check_permission(&UserRole::Custom("editor".to_string()), "delete_users"));
}
#[test]
fn test_session_manager() {
let mut session_manager = SessionManager::new();
let session_id = session_manager.create_session("user123", 60).unwrap();
assert!(!session_id.is_empty());
let session = session_manager.get_session(&session_id).unwrap();
assert_eq!(session.user_id, "user123");
assert!(session_manager.validate_session(&session_id).unwrap());
session_manager.delete_session(&session_id).unwrap();
assert!(!session_manager.validate_session(&session_id).unwrap());
}
#[test]
fn test_rate_limiter() {
let mut limiter = RateLimiter::new(3, 60);
let identifier = "user123";
assert!(limiter.check_rate_limit(identifier).unwrap());
assert!(limiter.check_rate_limit(identifier).unwrap());
assert!(limiter.check_rate_limit(identifier).unwrap());
assert!(!limiter.check_rate_limit(identifier).unwrap());
assert_eq!(limiter.remaining_requests(identifier), 0);
}
#[test]
fn test_auth_config_creation() {
let config = AuthConfig {
jwt_secret: "my-secret-key".to_string(),
jwt_expiration_hours: 24,
bcrypt_cost: 12,
session_timeout_minutes: 60,
allowed_origins: vec!["https://example.com".to_string()],
enable_cors: true,
};
assert_eq!(config.jwt_expiration_hours, 24);
assert_eq!(config.bcrypt_cost, 12);
assert!(config.enable_cors);
}
}