omni_orchestrator/schemas/v1/models/
user.rs1use 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#[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::info!("Authentication attempt for path: {}", request.uri().path());
88
89 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 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 let user_id = if let Some(token_str) = token {
122 log::info!("Attempting JWT token validation");
124 match validate_token(&token_str, auth_config) {
125 Ok(claims) => {
126 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 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 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 log::info!("No authentication method found");
177 None
178 };
179
180 if let Some(id) = user_id {
182 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 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 log::info!("Authentication failed, no valid credentials");
208 rocket::request::Outcome::Forward(rocket::http::Status::Unauthorized)
209 }
210 }
211}
212
213fn validate_token(token: &str, auth_config: &AuthConfig) -> Result<Claims, jsonwebtoken::errors::Error> {
215 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 Ok(token_data.claims)
224}