use axum::{
extract::{Json, Path},
http::StatusCode,
response::IntoResponse,
};
use chrono::{Duration, Utc};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Claims {
pub sub: String,
pub email: String,
pub iat: i64,
pub exp: i64,
pub token_type: TokenType,
pub scopes: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum TokenType {
Access,
Refresh,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenPair {
pub access_token: String,
pub refresh_token: String,
pub token_type: String,
pub expires_in: u64,
}
#[derive(Debug, Clone)]
pub struct AuthConfig {
pub jwt_secret: String,
pub access_token_expiry: Duration,
pub refresh_token_expiry: Duration,
pub issuer: String,
pub audience: String,
}
impl Default for AuthConfig {
fn default() -> Self {
Self {
jwt_secret: std::env::var("JWT_SECRET")
.unwrap_or_else(|_| "change-me-in-production".to_string()),
access_token_expiry: Duration::hours(1),
refresh_token_expiry: Duration::days(7),
issuer: "reasonkit.sh".to_string(),
audience: "reasonkit-api".to_string(),
}
}
}
pub struct AuthService {
config: AuthConfig,
encoding_key: EncodingKey,
decoding_key: DecodingKey,
}
impl AuthService {
pub fn new(config: AuthConfig) -> Self {
let encoding_key = EncodingKey::from_secret(config.jwt_secret.as_bytes());
let decoding_key = DecodingKey::from_secret(config.jwt_secret.as_bytes());
Self {
config,
encoding_key,
decoding_key,
}
}
pub fn generate_token_pair(
&self,
user_id: &str,
email: &str,
scopes: Vec<String>,
) -> TokenPair {
let now = Utc::now();
let access_claims = Claims {
sub: user_id.to_string(),
email: email.to_string(),
iat: now.timestamp(),
exp: (now + self.config.access_token_expiry).timestamp(),
token_type: TokenType::Access,
scopes: scopes.clone(),
};
let access_token = encode(&Header::default(), &access_claims, &self.encoding_key)
.expect("Failed to encode access token");
let refresh_claims = Claims {
sub: user_id.to_string(),
email: email.to_string(),
iat: now.timestamp(),
exp: (now + self.config.refresh_token_expiry).timestamp(),
token_type: TokenType::Refresh,
scopes,
};
let refresh_token = encode(&Header::default(), &refresh_claims, &self.encoding_key)
.expect("Failed to encode refresh token");
TokenPair {
access_token,
refresh_token,
token_type: "Bearer".to_string(),
expires_in: self.config.access_token_expiry.num_seconds() as u64,
}
}
pub fn validate_token(&self, token: &str) -> Result<Claims, AuthError> {
let validation = Validation::default();
decode::<Claims>(token, &self.decoding_key, &validation)
.map(|data| data.claims)
.map_err(|e| AuthError::InvalidToken(e.to_string()))
}
pub fn hash_password(password: &str) -> Result<String, AuthError> {
use argon2::Argon2;
use password_hash::{rand_core::OsRng, PasswordHasher, SaltString};
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
argon2
.hash_password(password.as_bytes(), &salt)
.map(|hash| hash.to_string())
.map_err(|e| AuthError::PasswordHashError(e.to_string()))
}
pub fn verify_password(password: &str, hash: &str) -> Result<bool, AuthError> {
use argon2::Argon2;
use password_hash::{PasswordHash, PasswordVerifier};
let parsed_hash =
PasswordHash::new(hash).map_err(|e| AuthError::PasswordHashError(e.to_string()))?;
Ok(Argon2::default()
.verify_password(password.as_bytes(), &parsed_hash)
.is_ok())
}
}
#[derive(Debug, thiserror::Error)]
pub enum AuthError {
#[error("Invalid credentials")]
InvalidCredentials,
#[error("Invalid token: {0}")]
InvalidToken(String),
#[error("Token expired")]
TokenExpired,
#[error("User not found")]
UserNotFound,
#[error("Email already registered")]
EmailAlreadyExists,
#[error("Password hash error: {0}")]
PasswordHashError(String),
#[error("2FA required")]
TwoFactorRequired,
#[error("Invalid 2FA code")]
Invalid2FACode,
#[error("Database error: {0}")]
DatabaseError(String),
}
pub mod handlers {
use super::*;
#[derive(Debug, Deserialize)]
pub struct RegisterRequest {
pub email: String,
pub password: String,
pub name: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct LoginRequest {
pub email: String,
pub password: String,
pub totp_code: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct AuthResponse {
pub success: bool,
pub tokens: Option<TokenPair>,
pub user_id: Option<String>,
pub requires_2fa: bool,
pub message: String,
}
pub async fn register(Json(_req): Json<RegisterRequest>) -> impl IntoResponse {
let response = AuthResponse {
success: true,
tokens: None,
user_id: Some("user_placeholder".to_string()),
requires_2fa: false,
message: "Registration successful. Please verify your email.".to_string(),
};
(StatusCode::CREATED, Json(response))
}
pub async fn login(Json(_req): Json<LoginRequest>) -> impl IntoResponse {
let response = AuthResponse {
success: true,
tokens: Some(TokenPair {
access_token: "placeholder".to_string(),
refresh_token: "placeholder".to_string(),
token_type: "Bearer".to_string(),
expires_in: 3600,
}),
user_id: Some("user_placeholder".to_string()),
requires_2fa: false,
message: "Login successful".to_string(),
};
(StatusCode::OK, Json(response))
}
pub async fn logout() -> impl IntoResponse {
(StatusCode::OK, Json(serde_json::json!({"success": true})))
}
pub async fn refresh_token() -> impl IntoResponse {
StatusCode::NOT_IMPLEMENTED
}
pub async fn request_password_reset() -> impl IntoResponse {
StatusCode::NOT_IMPLEMENTED
}
pub async fn reset_password(Path(_token): Path<String>) -> impl IntoResponse {
StatusCode::NOT_IMPLEMENTED
}
pub async fn setup_2fa() -> impl IntoResponse {
StatusCode::NOT_IMPLEMENTED
}
pub async fn verify_2fa() -> impl IntoResponse {
StatusCode::NOT_IMPLEMENTED
}
}