lockwise_backend/
invite.rs

1//! Módulo para gerenciamento de convites.
2//!
3//! Este módulo implementa a funcionalidade de convites temporários para compartilhamento
4//! de acesso a dispositivos LockWise entre usuários.
5use 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/// Informações sobre convites enviados.
16#[derive(sqlx::FromRow, Serialize)]
17pub struct SentInviteInfo {
18    /// ID do convite.
19    id: i32,
20    /// UUID do dispositivo.
21    device_id: uuid::Uuid,
22    /// ID do usuário remetente.
23    sender_id: String,
24    /// ID do usuário destinatário.
25    receiver_id: String,
26    /// Nome do destinatário.
27    receiver_name: String,
28    /// Email do destinatário.
29    receiver_email: String,
30    /// Status do convite (0: pendente, 1: aceito).
31    status: i32,
32    /// Timestamp de expiração.
33    expiry_timestamp: i64,
34    /// Timestamp de criação.
35    created_at: chrono::DateTime<chrono::Utc>,
36}
37
38/// Informações sobre convites recebidos.
39#[derive(sqlx::FromRow, Serialize)]
40pub struct ReceivedInviteInfo {
41    /// ID do convite.
42    id: i32,
43    /// UUID do dispositivo.
44    device_id: uuid::Uuid,
45    /// ID do usuário remetente.
46    sender_id: String,
47    /// ID do usuário destinatário.
48    receiver_id: String,
49    /// Nome do remetente.
50    sender_name: String,
51    /// Email do remetente.
52    sender_email: String,
53    /// Status do convite.
54    status: i32,
55    /// Timestamp de expiração.
56    expiry_timestamp: i64,
57    /// Timestamp de criação.
58    created_at: chrono::DateTime<chrono::Utc>,
59}
60
61/// Estrutura de requisição para criar um convite.
62#[derive(Deserialize)]
63pub struct CreateInviteRequest {
64    /// Email do destinatário do convite.
65    receiver_email: String,
66    /// UUID do dispositivo a convidar.
67    device_id: String,
68    /// String de duração de expiração (ex.: "2_dias", "1_semana").
69    expiry_duration: String,
70}
71
72/// Estrutura de requisição para atualizar um convite.
73#[derive(Deserialize)]
74pub struct UpdateInviteRequest {
75    /// Nova string de duração de expiração.
76    expiry_duration: String,
77}
78
79/// Cria convite para acesso temporário ao dispositivo.
80#[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    // Parse device_id to UUID
87    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    // Validate token and get sender
95    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    // Check if sender owns the device
109    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    // Find receiver by email
129    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    // Check if invite already exists and is pending
146    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) // UUID for invites table
150    .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    // Calculate expiry timestamp
159    let now = Utc::now();
160    let expiry_timestamp = calculate_expiry_timestamp(now, &request.expiry_duration);
161
162    // Create invite
163    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) // UUID for invites table
167    .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/// Recupera convites enviados e recebidos do usuário.
184#[get("/invites")]
185pub async fn get_invites(token: Token, db_pool: &State<PgPool>) -> Result<String, Status> {
186    // Validate token
187    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    // Get sent invites with receiver info
201    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    // Get received invites with sender info
211    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/// Aceita convite para acesso ao dispositivo.
238#[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    // Validate token
245    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    // Update invite status to accepted (1)
257    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/// Rejeita convite.
275#[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    // Validate token
282    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    // Delete the invite
296    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/// Cancela convite enviado.
312#[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    // Validate token
319    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    // Delete the invite
331    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/// Atualiza um convite (ex.: estender expiração).
346#[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    // Validate token
354    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    // Calculate new expiry timestamp
366    let now = Utc::now();
367    let new_expiry_timestamp = calculate_expiry_timestamp(now, &request.expiry_duration);
368
369    // Update invite expiry
370    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
386/// Calcula timestamp de expiração a partir de string de duração (ex.: "1h", "30m").
387fn 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), // 100 years
394        _ => chrono::Duration::days(7),
395    };
396    (base_time + duration).timestamp_millis()
397}