1use serde::{Deserialize, Serialize};
2use chrono::{DateTime, Utc};
3use sqlx::Pool;
4use sqlx::MySql;
5use sqlx::FromRow;
6use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm};
7
8use super::super::auth::{AuthConfig, Claims};
9
10#[derive(Debug, FromRow, Serialize, Clone, Deserialize)]
11pub struct User {
12 pub id: i64,
13 pub email: String,
14 pub email_verified: i8,
15 pub password: String,
16 pub salt: String,
17 pub login_attempts: i64,
18 pub active: bool,
19 pub status: String,
20 pub created_at: DateTime<Utc>,
21 pub updated_at: DateTime<Utc>,
22 pub last_login_at: Option<DateTime<Utc>>,
23}
24
25#[derive(Debug, FromRow, Serialize, Deserialize)]
26pub struct UserMeta {
27 pub id: i64,
28 pub user_id: i64,
29 pub timezone: Option<String>,
30 pub language: Option<String>,
31 pub theme: Option<String>,
32 pub notification_preferences: Option<serde_json::Value>,
33 pub profile_image: Option<String>,
34 pub dashboard_layout: Option<serde_json::Value>,
35 pub onboarding_completed: i8,
36 pub created_at: DateTime<Utc>,
37 pub updated_at: DateTime<Utc>,
38}
39
40#[derive(Debug, FromRow, Serialize, Deserialize)]
41pub struct UserPii {
42 pub id: i64,
43 pub user_id: i64,
44 pub first_name: Option<String>,
45 pub last_name: Option<String>,
46 pub full_name: Option<String>,
47 pub identity_verified: i8,
48 pub identity_verification_date: Option<DateTime<Utc>>,
49 pub identity_verification_method: Option<String>,
50 pub created_at: DateTime<Utc>,
51 pub updated_at: DateTime<Utc>,
52}
53
54#[derive(Debug, FromRow, Serialize, Deserialize)]
55pub struct UserSession {
56 pub id: i64,
57 pub user_id: i64,
58 pub session_token: String,
59 pub refresh_token: Option<String>,
60 pub ip_address: Option<String>,
61 pub user_agent: Option<String>,
62 pub device_info: Option<serde_json::Value>,
63 pub location_info: Option<serde_json::Value>,
64 pub is_active: i8,
65 pub last_activity: Option<DateTime<Utc>>,
66 pub expires_at: DateTime<Utc>,
67 pub created_at: DateTime<Utc>,
68}
69
70#[derive(Debug, sqlx::FromRow)]
72struct SessionData {
73 user_id: i64,
74}
75
76#[rocket::async_trait]
77impl<'r> rocket::request::FromRequest<'r> for User {
78 type Error = ();
79
80 async fn from_request(request: &'r rocket::Request<'_>) -> rocket::request::Outcome<Self, Self::Error> {
81 log::info!("Authentication attempt for path: {}", request.uri().path());
83
84 let auth_config = match request.rocket().state::<AuthConfig>() {
86 Some(config) => config,
87 None => {
88 log::error!("AuthConfig not found in rocket state");
89 return rocket::request::Outcome::Forward(rocket::http::Status::InternalServerError)
90 }
91 };
92
93 let pool = match request.rocket().state::<Pool<MySql>>() {
94 Some(p) => p,
95 None => {
96 log::error!("Database pool not found in rocket state");
97 return rocket::request::Outcome::Forward(rocket::http::Status::InternalServerError);
98 }
99 };
100
101 let token = if let Some(auth_header) = request.headers().get_one("Authorization") {
103 if auth_header.starts_with("Bearer ") {
104 log::info!("Found Bearer token in Authorization header");
105 Some(auth_header.trim_start_matches("Bearer ").to_string())
106 } else {
107 log::info!("Authorization header present but not a Bearer token");
108 None
109 }
110 } else {
111 log::info!("No Authorization header found");
112 None
113 };
114
115 let user_id = if let Some(token_str) = token {
117 log::info!("Attempting JWT token validation");
119 match validate_token(&token_str, auth_config) {
120 Ok(claims) => {
121 log::info!("JWT token validated successfully");
123 match claims.sub.parse::<i64>() {
124 Ok(id) => {
125 log::info!("User authenticated via JWT token, user_id: {}", id);
126 Some(id)
127 },
128 Err(e) => {
129 log::error!("Failed to parse user ID from token: {}", e);
130 None
131 }
132 }
133 },
134 Err(e) => {
135 log::error!("JWT validation failed: {}", e);
136 None
137 }
138 }
139 } else if let Some(session_cookie) = request.cookies().get("session_id") {
140 log::info!("Attempting session cookie validation");
142 match sqlx::query_as::<_, SessionData>(
143 "SELECT user_id FROM user_sessions WHERE session_token = ? AND expires_at > NOW() AND is_active = 1"
144 )
145 .bind(session_cookie.value())
146 .fetch_optional(pool)
147 .await {
148 Ok(Some(session)) => {
149 log::info!("Session found and valid for user_id: {}", session.user_id);
151 let _ = sqlx::query(
152 "UPDATE user_sessions SET last_activity = NOW() WHERE session_token = ?"
153 )
154 .bind(session_cookie.value())
155 .execute(pool)
156 .await;
157
158 Some(session.user_id)
159 },
160 Ok(None) => {
161 log::warn!("Invalid or expired session: {}", session_cookie.value());
162 None
163 },
164 Err(e) => {
165 log::error!("Database error looking up session: {}", e);
166 None
167 }
168 }
169 } else {
170 log::info!("No authentication method found");
172 None
173 };
174
175 if let Some(id) = user_id {
177 log::info!("Fetching user details for user_id: {}", id);
179 match sqlx::query_as::<_, User>(
180 "SELECT * FROM users WHERE id = ?"
181 )
182 .bind(id)
183 .fetch_one(pool)
184 .await {
185 Ok(user) => {
186 if user.active {
188 log::info!("User {} successfully authenticated and active", id);
189 rocket::request::Outcome::Success(user)
190 } else {
191 log::warn!("Inactive user attempted access: {}", id);
192 rocket::request::Outcome::Error((rocket::http::Status::Forbidden, ()))
193 }
194 },
195 Err(e) => {
196 log::error!("Error fetching user {}: {}", id, e);
197 rocket::request::Outcome::Error((rocket::http::Status::InternalServerError, ()))
198 }
199 }
200 } else {
201 log::info!("Authentication failed, no valid credentials");
203 rocket::request::Outcome::Forward(rocket::http::Status::Unauthorized)
204 }
205 }
206}
207
208fn validate_token(token: &str, auth_config: &AuthConfig) -> Result<Claims, jsonwebtoken::errors::Error> {
210 let token_data = decode::<Claims>(
212 token,
213 &DecodingKey::from_secret(auth_config.jwt_secret.as_bytes()),
214 &Validation::new(Algorithm::HS256)
215 )?;
216
217 Ok(token_data.claims)
219}