1use axum::{
14 extract::{Json, Path},
15 http::StatusCode,
16 response::IntoResponse,
17};
18use chrono::{Duration, Utc};
19use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
20use serde::{Deserialize, Serialize};
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct Claims {
25 pub sub: String,
27 pub email: String,
29 pub iat: i64,
31 pub exp: i64,
33 pub token_type: TokenType,
35 pub scopes: Vec<String>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
40#[serde(rename_all = "lowercase")]
41pub enum TokenType {
42 Access,
43 Refresh,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct TokenPair {
49 pub access_token: String,
50 pub refresh_token: String,
51 pub token_type: String,
52 pub expires_in: u64,
53}
54
55#[derive(Debug, Clone)]
57pub struct AuthConfig {
58 pub jwt_secret: String,
59 pub access_token_expiry: Duration,
60 pub refresh_token_expiry: Duration,
61 pub issuer: String,
62 pub audience: String,
63}
64
65impl Default for AuthConfig {
66 fn default() -> Self {
67 Self {
68 jwt_secret: std::env::var("JWT_SECRET")
69 .unwrap_or_else(|_| "change-me-in-production".to_string()),
70 access_token_expiry: Duration::hours(1),
71 refresh_token_expiry: Duration::days(7),
72 issuer: "reasonkit.sh".to_string(),
73 audience: "reasonkit-api".to_string(),
74 }
75 }
76}
77
78pub struct AuthService {
80 config: AuthConfig,
81 encoding_key: EncodingKey,
82 decoding_key: DecodingKey,
83}
84
85impl AuthService {
86 pub fn new(config: AuthConfig) -> Self {
87 let encoding_key = EncodingKey::from_secret(config.jwt_secret.as_bytes());
88 let decoding_key = DecodingKey::from_secret(config.jwt_secret.as_bytes());
89 Self {
90 config,
91 encoding_key,
92 decoding_key,
93 }
94 }
95
96 pub fn generate_token_pair(
98 &self,
99 user_id: &str,
100 email: &str,
101 scopes: Vec<String>,
102 ) -> TokenPair {
103 let now = Utc::now();
104
105 let access_claims = Claims {
107 sub: user_id.to_string(),
108 email: email.to_string(),
109 iat: now.timestamp(),
110 exp: (now + self.config.access_token_expiry).timestamp(),
111 token_type: TokenType::Access,
112 scopes: scopes.clone(),
113 };
114
115 let access_token = encode(&Header::default(), &access_claims, &self.encoding_key)
116 .expect("Failed to encode access token");
117
118 let refresh_claims = Claims {
120 sub: user_id.to_string(),
121 email: email.to_string(),
122 iat: now.timestamp(),
123 exp: (now + self.config.refresh_token_expiry).timestamp(),
124 token_type: TokenType::Refresh,
125 scopes,
126 };
127
128 let refresh_token = encode(&Header::default(), &refresh_claims, &self.encoding_key)
129 .expect("Failed to encode refresh token");
130
131 TokenPair {
132 access_token,
133 refresh_token,
134 token_type: "Bearer".to_string(),
135 expires_in: self.config.access_token_expiry.num_seconds() as u64,
136 }
137 }
138
139 pub fn validate_token(&self, token: &str) -> Result<Claims, AuthError> {
141 let validation = Validation::default();
142 decode::<Claims>(token, &self.decoding_key, &validation)
143 .map(|data| data.claims)
144 .map_err(|e| AuthError::InvalidToken(e.to_string()))
145 }
146
147 pub fn hash_password(password: &str) -> Result<String, AuthError> {
149 use argon2::Argon2;
150 use password_hash::{rand_core::OsRng, PasswordHasher, SaltString};
151
152 let salt = SaltString::generate(&mut OsRng);
153 let argon2 = Argon2::default();
154
155 argon2
156 .hash_password(password.as_bytes(), &salt)
157 .map(|hash| hash.to_string())
158 .map_err(|e| AuthError::PasswordHashError(e.to_string()))
159 }
160
161 pub fn verify_password(password: &str, hash: &str) -> Result<bool, AuthError> {
163 use argon2::Argon2;
164 use password_hash::{PasswordHash, PasswordVerifier};
165
166 let parsed_hash =
167 PasswordHash::new(hash).map_err(|e| AuthError::PasswordHashError(e.to_string()))?;
168
169 Ok(Argon2::default()
170 .verify_password(password.as_bytes(), &parsed_hash)
171 .is_ok())
172 }
173}
174
175#[derive(Debug, thiserror::Error)]
177pub enum AuthError {
178 #[error("Invalid credentials")]
179 InvalidCredentials,
180 #[error("Invalid token: {0}")]
181 InvalidToken(String),
182 #[error("Token expired")]
183 TokenExpired,
184 #[error("User not found")]
185 UserNotFound,
186 #[error("Email already registered")]
187 EmailAlreadyExists,
188 #[error("Password hash error: {0}")]
189 PasswordHashError(String),
190 #[error("2FA required")]
191 TwoFactorRequired,
192 #[error("Invalid 2FA code")]
193 Invalid2FACode,
194 #[error("Database error: {0}")]
195 DatabaseError(String),
196}
197
198pub mod handlers {
200 use super::*;
201
202 #[derive(Debug, Deserialize)]
203 pub struct RegisterRequest {
204 pub email: String,
205 pub password: String,
206 pub name: Option<String>,
207 }
208
209 #[derive(Debug, Deserialize)]
210 pub struct LoginRequest {
211 pub email: String,
212 pub password: String,
213 pub totp_code: Option<String>,
214 }
215
216 #[derive(Debug, Serialize)]
217 pub struct AuthResponse {
218 pub success: bool,
219 pub tokens: Option<TokenPair>,
220 pub user_id: Option<String>,
221 pub requires_2fa: bool,
222 pub message: String,
223 }
224
225 pub async fn register(Json(_req): Json<RegisterRequest>) -> impl IntoResponse {
227 let response = AuthResponse {
229 success: true,
230 tokens: None,
231 user_id: Some("user_placeholder".to_string()),
232 requires_2fa: false,
233 message: "Registration successful. Please verify your email.".to_string(),
234 };
235 (StatusCode::CREATED, Json(response))
236 }
237
238 pub async fn login(Json(_req): Json<LoginRequest>) -> impl IntoResponse {
240 let response = AuthResponse {
242 success: true,
243 tokens: Some(TokenPair {
244 access_token: "placeholder".to_string(),
245 refresh_token: "placeholder".to_string(),
246 token_type: "Bearer".to_string(),
247 expires_in: 3600,
248 }),
249 user_id: Some("user_placeholder".to_string()),
250 requires_2fa: false,
251 message: "Login successful".to_string(),
252 };
253 (StatusCode::OK, Json(response))
254 }
255
256 pub async fn logout() -> impl IntoResponse {
258 (StatusCode::OK, Json(serde_json::json!({"success": true})))
259 }
260
261 pub async fn refresh_token() -> impl IntoResponse {
263 StatusCode::NOT_IMPLEMENTED
265 }
266
267 pub async fn request_password_reset() -> impl IntoResponse {
269 StatusCode::NOT_IMPLEMENTED
270 }
271
272 pub async fn reset_password(Path(_token): Path<String>) -> impl IntoResponse {
274 StatusCode::NOT_IMPLEMENTED
275 }
276
277 pub async fn setup_2fa() -> impl IntoResponse {
279 StatusCode::NOT_IMPLEMENTED
280 }
281
282 pub async fn verify_2fa() -> impl IntoResponse {
284 StatusCode::NOT_IMPLEMENTED
285 }
286}