1use anyhow::Result;
6use chrono::Utc;
7use rocket::http::Status;
8use rocket::{State, get, post};
9use serde::{Deserialize, Serialize};
10use sqlx::PgPool;
11use uuid::Uuid;
12
13use super::Token;
14
15#[derive(sqlx::FromRow, Serialize)]
17pub struct SentInviteInfo {
18 id: i32,
20 device_id: uuid::Uuid,
22 sender_id: String,
24 receiver_id: String,
26 receiver_name: String,
28 receiver_email: String,
30 status: i32,
32 expiry_timestamp: i64,
34 created_at: chrono::DateTime<chrono::Utc>,
36}
37
38#[derive(sqlx::FromRow, Serialize)]
40pub struct ReceivedInviteInfo {
41 id: i32,
43 device_id: uuid::Uuid,
45 sender_id: String,
47 receiver_id: String,
49 sender_name: String,
51 sender_email: String,
53 status: i32,
55 expiry_timestamp: i64,
57 created_at: chrono::DateTime<chrono::Utc>,
59}
60
61#[derive(Deserialize)]
63pub struct CreateInviteRequest {
64 receiver_email: String,
66 device_id: String,
68 expiry_duration: String,
70}
71
72#[derive(Deserialize)]
74pub struct UpdateInviteRequest {
75 expiry_duration: String,
77}
78
79#[post("/create_invite", data = "<request>")]
81pub async fn create_invite(
82 token: Token,
83 request: rocket::serde::json::Json<CreateInviteRequest>,
84 db_pool: &State<PgPool>,
85) -> Result<String, Status> {
86 let device_uuid = match Uuid::parse_str(&request.device_id) {
88 Ok(uuid) => uuid,
89 Err(_) => {
90 return Err(Status::BadRequest);
91 }
92 };
93
94 let user_row: Option<(String,)> =
96 sqlx::query_as("SELECT firebase_uid FROM users WHERE current_token = $1")
97 .bind(&token.0)
98 .fetch_optional(&**db_pool)
99 .await
100 .map_err(|_| Status::InternalServerError)?;
101 let sender_id = match user_row {
102 Some((uid,)) => uid,
103 None => {
104 return Err(Status::Unauthorized);
105 }
106 };
107
108 let device_row: Option<(Option<String>,)> =
110 sqlx::query_as("SELECT user_id FROM devices WHERE uuid = $1")
111 .bind(device_uuid)
112 .fetch_optional(&**db_pool)
113 .await
114 .map_err(|_| Status::InternalServerError)?;
115 let owner_id = if let Some((Some(owner),)) = device_row {
116 owner
117 } else {
118 return Err(Status::NotFound);
119 };
120 if owner_id != sender_id {
121 println!(
122 "DEBUG: User {} does not own device {} (owned by {})",
123 sender_id, request.device_id, owner_id
124 );
125 return Err(Status::Forbidden);
126 }
127
128 let receiver_row: Option<(String,)> =
130 sqlx::query_as("SELECT firebase_uid FROM users WHERE email = $1")
131 .bind(&request.receiver_email)
132 .fetch_optional(&**db_pool)
133 .await
134 .map_err(|_| Status::InternalServerError)?;
135 let receiver_id = if let Some((uid,)) = receiver_row {
136 uid
137 } else {
138 println!(
139 "DEBUG: Receiver not found for email: {}",
140 request.receiver_email
141 );
142 return Err(Status::NotFound);
143 };
144
145 let existing_invite: Option<(i32,)> = sqlx::query_as(
147 "SELECT id FROM invites WHERE device_id = $1 AND receiver_id = $2 AND status = 0",
148 )
149 .bind(device_uuid) .bind(&receiver_id)
151 .fetch_optional(&**db_pool)
152 .await
153 .map_err(|_| Status::InternalServerError)?;
154 if existing_invite.is_some() {
155 return Err(Status::Conflict);
156 }
157
158 let now = Utc::now();
160 let expiry_timestamp = calculate_expiry_timestamp(now, &request.expiry_duration);
161
162 let invite_id: i32 = sqlx::query_scalar(
164 "INSERT INTO invites (device_id, sender_id, receiver_id, expiry_timestamp) VALUES ($1, $2, $3, $4) RETURNING id"
165 )
166 .bind(device_uuid) .bind(&sender_id)
168 .bind(&receiver_id)
169 .bind(expiry_timestamp)
170 .fetch_one(&**db_pool)
171 .await
172 .map_err(|_| {
173 Status::InternalServerError
174 })?;
175
176 Ok(serde_json::json!({
177 "invite_id": invite_id,
178 "message": "Invite created successfully"
179 })
180 .to_string())
181}
182
183#[get("/invites")]
185pub async fn get_invites(token: Token, db_pool: &State<PgPool>) -> Result<String, Status> {
186 let user_row: Option<(String,)> =
188 sqlx::query_as("SELECT firebase_uid FROM users WHERE current_token = $1")
189 .bind(&token.0)
190 .fetch_optional(&**db_pool)
191 .await
192 .map_err(|_| Status::InternalServerError)?;
193 let user_id = match user_row {
194 Some((uid,)) => uid,
195 None => {
196 return Err(Status::Unauthorized);
197 }
198 };
199
200 let sent_invites: Vec<SentInviteInfo> =
202 sqlx::query_as("SELECT i.id, i.device_id, i.sender_id, i.receiver_id, ru.name as receiver_name, ru.email as receiver_email, i.status, i.expiry_timestamp, i.created_at FROM invites i LEFT JOIN users ru ON i.receiver_id = ru.firebase_uid WHERE i.sender_id = $1")
203 .bind(&user_id)
204 .fetch_all(&**db_pool)
205 .await
206 .map_err(|_| {
207 Status::InternalServerError
208 })?;
209
210 let received_invites: Vec<ReceivedInviteInfo> =
212 sqlx::query_as("SELECT i.id, i.device_id, i.sender_id, i.receiver_id, su.name as sender_name, su.email as sender_email, i.status, i.expiry_timestamp, i.created_at FROM invites i LEFT JOIN users su ON i.sender_id = su.firebase_uid WHERE i.receiver_id = $1")
213 .bind(&user_id)
214 .fetch_all(&**db_pool)
215 .await
216 .map_err(|_| {
217 Status::InternalServerError
218 })?;
219
220 let sent: Vec<serde_json::Value> = sent_invites
221 .into_iter()
222 .map(|invite| serde_json::to_value(invite).unwrap())
223 .collect();
224
225 let received: Vec<serde_json::Value> = received_invites
226 .into_iter()
227 .map(|invite| serde_json::to_value(invite).unwrap())
228 .collect();
229
230 Ok(serde_json::json!({
231 "sent": sent,
232 "received": received
233 })
234 .to_string())
235}
236
237#[post("/accept_invite/<invite_id>")]
239pub async fn accept_invite(
240 token: Token,
241 invite_id: i32,
242 db_pool: &State<PgPool>,
243) -> Result<(), Status> {
244 let user_row: Option<(String,)> =
246 sqlx::query_as("SELECT firebase_uid FROM users WHERE current_token = $1")
247 .bind(&token.0)
248 .fetch_optional(&**db_pool)
249 .await
250 .map_err(|_| Status::InternalServerError)?;
251 let user_id = match user_row {
252 Some((uid,)) => uid,
253 None => return Err(Status::Unauthorized),
254 };
255
256 let rows_affected = sqlx::query(
258 "UPDATE invites SET status = 1 WHERE id = $1 AND receiver_id = $2 AND status = 0",
259 )
260 .bind(invite_id)
261 .bind(&user_id)
262 .execute(&**db_pool)
263 .await
264 .map_err(|_| Status::InternalServerError)?
265 .rows_affected();
266
267 if rows_affected == 0 {
268 Err(Status::NotFound)
269 } else {
270 Ok(())
271 }
272}
273
274#[post("/reject_invite/<invite_id>")]
276pub async fn reject_invite(
277 token: Token,
278 invite_id: i32,
279 db_pool: &State<PgPool>,
280) -> Result<(), Status> {
281 let user_row: Option<(String,)> =
283 sqlx::query_as("SELECT firebase_uid FROM users WHERE current_token = $1")
284 .bind(&token.0)
285 .fetch_optional(&**db_pool)
286 .await
287 .map_err(|_| Status::InternalServerError)?;
288 let user_id = match user_row {
289 Some((uid,)) => uid,
290 None => {
291 return Err(Status::Unauthorized);
292 }
293 };
294
295 let rows_affected = sqlx::query("DELETE FROM invites WHERE id = $1 AND receiver_id = $2")
297 .bind(invite_id)
298 .bind(&user_id)
299 .execute(&**db_pool)
300 .await
301 .map_err(|_| Status::InternalServerError)?
302 .rows_affected();
303
304 if rows_affected == 0 {
305 return Err(Status::NotFound);
306 }
307
308 Ok(())
309}
310
311#[post("/cancel_invite/<invite_id>")]
313pub async fn cancel_invite(
314 token: Token,
315 invite_id: i32,
316 db_pool: &State<PgPool>,
317) -> Result<(), Status> {
318 let user_row: Option<(String,)> =
320 sqlx::query_as("SELECT firebase_uid FROM users WHERE current_token = $1")
321 .bind(&token.0)
322 .fetch_optional(&**db_pool)
323 .await
324 .map_err(|_| Status::InternalServerError)?;
325 let user_id = match user_row {
326 Some((uid,)) => uid,
327 None => return Err(Status::Unauthorized),
328 };
329
330 let rows_affected = sqlx::query("DELETE FROM invites WHERE id = $1 AND sender_id = $2")
332 .bind(invite_id)
333 .bind(&user_id)
334 .execute(&**db_pool)
335 .await
336 .map_err(|_| Status::InternalServerError)?;
337
338 if rows_affected.rows_affected() == 0 {
339 return Err(Status::NotFound);
340 }
341
342 Ok(())
343}
344
345#[post("/update_invite/<invite_id>", data = "<request>")]
347pub async fn update_invite(
348 token: Token,
349 invite_id: i32,
350 request: rocket::serde::json::Json<UpdateInviteRequest>,
351 db_pool: &State<PgPool>,
352) -> Result<(), Status> {
353 let user_row: Option<(String,)> =
355 sqlx::query_as("SELECT firebase_uid FROM users WHERE current_token = $1")
356 .bind(&token.0)
357 .fetch_optional(&**db_pool)
358 .await
359 .map_err(|_| Status::InternalServerError)?;
360 let user_id = match user_row {
361 Some((uid,)) => uid,
362 None => return Err(Status::Unauthorized),
363 };
364
365 let now = Utc::now();
367 let new_expiry_timestamp = calculate_expiry_timestamp(now, &request.expiry_duration);
368
369 let rows_affected =
371 sqlx::query("UPDATE invites SET expiry_timestamp = $1 WHERE id = $2 AND sender_id = $3")
372 .bind(new_expiry_timestamp)
373 .bind(invite_id)
374 .bind(&user_id)
375 .execute(&**db_pool)
376 .await
377 .map_err(|_| Status::InternalServerError)?;
378
379 if rows_affected.rows_affected() == 0 {
380 return Err(Status::NotFound);
381 }
382
383 Ok(())
384}
385
386fn calculate_expiry_timestamp(base_time: chrono::DateTime<chrono::Utc>, duration: &str) -> i64 {
388 let duration = match duration {
389 "2_dias" => chrono::Duration::days(2),
390 "1_semana" => chrono::Duration::days(7),
391 "2_semanas" => chrono::Duration::days(14),
392 "1_mes" => chrono::Duration::days(30),
393 "permanente" => chrono::Duration::days(36500), _ => chrono::Duration::days(7),
395 };
396 (base_time + duration).timestamp_millis()
397}