omni_orchestrator/schemas/v1/models/
user.rs

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