libomni/types/db/v1/
user.rs

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// Define a struct for session data
71#[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 the request path for context
82        log::info!("Authentication attempt for path: {}", request.uri().path());
83        
84        // Get the authentication config
85        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        // Check for Authorization header first (Bearer token)
102        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        // If no Authorization header, check for session_id cookie
116        let user_id = if let Some(token_str) = token {
117            // Validate the JWT token
118            log::info!("Attempting JWT token validation");
119            match validate_token(&token_str, auth_config) {
120                Ok(claims) => {
121                    // Extract user ID from sub claim
122                    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            // Look up session in database using query_as with the SessionData struct
141            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                    // Update last_activity
150                    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            // No authentication provided
171            log::info!("No authentication method found");
172            None
173        };
174    
175        // Fetch the user if we have an ID
176        if let Some(id) = user_id {
177            // Use query_as to fetch the complete user
178            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                    // Check if user is active
187                    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            // No valid authentication
202            log::info!("Authentication failed, no valid credentials");
203            rocket::request::Outcome::Forward(rocket::http::Status::Unauthorized)
204        }
205    }
206}
207
208// Token validation function
209fn validate_token(token: &str, auth_config: &AuthConfig) -> Result<Claims, jsonwebtoken::errors::Error> {
210    // Decode and validate the token
211    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    // Return the claims
218    Ok(token_data.claims)
219}