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    println!(
87        "DEBUG: create_invite called with receiver_email: {}, device_id: {}",
88        request.receiver_email, request.device_id
89    );
90
91    // Parse device_id to UUID
92    let device_uuid = match Uuid::parse_str(&request.device_id) {
93        Ok(uuid) => uuid,
94        Err(_) => {
95            return Err(Status::BadRequest);
96        }
97    };
98
99    // Validate token and get sender
100    let user_row: Option<(String,)> =
101        sqlx::query_as("SELECT firebase_uid FROM users WHERE current_token = $1")
102            .bind(&token.0)
103            .fetch_optional(&**db_pool)
104            .await
105            .map_err(|_| Status::InternalServerError)?;
106    let sender_id = match user_row {
107        Some((uid,)) => uid,
108        None => {
109            return Err(Status::Unauthorized);
110        }
111    };
112
113    // Check if sender owns the device
114    let device_row: Option<(Option<String>,)> =
115        sqlx::query_as("SELECT user_id FROM devices WHERE uuid = $1")
116            .bind(device_uuid)
117            .fetch_optional(&**db_pool)
118            .await
119            .map_err(|_| Status::InternalServerError)?;
120    let owner_id = if let Some((Some(owner),)) = device_row {
121        owner
122    } else {
123        return Err(Status::NotFound);
124    };
125    if owner_id != sender_id {
126        println!(
127            "DEBUG: User {} does not own device {} (owned by {})",
128            sender_id, request.device_id, owner_id
129        );
130        return Err(Status::Forbidden);
131    }
132
133    // Find receiver by email
134    let receiver_row: Option<(String,)> =
135        sqlx::query_as("SELECT firebase_uid FROM users WHERE email = $1")
136            .bind(&request.receiver_email)
137            .fetch_optional(&**db_pool)
138            .await
139            .map_err(|_| Status::InternalServerError)?;
140    let receiver_id = if let Some((uid,)) = receiver_row {
141        uid
142    } else {
143        println!(
144            "DEBUG: Receiver not found for email: {}",
145            request.receiver_email
146        );
147        return Err(Status::NotFound);
148    };
149
150    // Check if invite already exists and is pending
151    let existing_invite: Option<(i32,)> = sqlx::query_as(
152        "SELECT id FROM invites WHERE device_id = $1 AND receiver_id = $2 AND status = 0",
153    )
154    .bind(device_uuid) // UUID for invites table
155    .bind(&receiver_id)
156    .fetch_optional(&**db_pool)
157    .await
158    .map_err(|_| Status::InternalServerError)?;
159    if existing_invite.is_some() {
160        return Err(Status::Conflict);
161    }
162
163    // Calculate expiry timestamp
164    let now = Utc::now();
165    let expiry_timestamp = calculate_expiry_timestamp(now, &request.expiry_duration);
166
167    // Create invite
168    let invite_id: i32 = sqlx::query_scalar(
169        "INSERT INTO invites (device_id, sender_id, receiver_id, expiry_timestamp) VALUES ($1, $2, $3, $4) RETURNING id"
170    )
171    .bind(device_uuid) // UUID for invites table
172    .bind(&sender_id)
173    .bind(&receiver_id)
174    .bind(expiry_timestamp)
175    .fetch_one(&**db_pool)
176    .await
177    .map_err(|_| {
178        Status::InternalServerError
179    })?;
180
181    Ok(serde_json::json!({
182        "invite_id": invite_id,
183        "message": "Invite created successfully"
184    })
185    .to_string())
186}
187
188/// Recupera convites enviados e recebidos do usuário.
189#[get("/invites")]
190pub async fn get_invites(token: Token, db_pool: &State<PgPool>) -> Result<String, Status> {
191    // Validate token
192    let user_row: Option<(String,)> =
193        sqlx::query_as("SELECT firebase_uid FROM users WHERE current_token = $1")
194            .bind(&token.0)
195            .fetch_optional(&**db_pool)
196            .await
197            .map_err(|_| Status::InternalServerError)?;
198    let user_id = match user_row {
199        Some((uid,)) => uid,
200        None => {
201            return Err(Status::Unauthorized);
202        }
203    };
204
205    // Get sent invites with receiver info
206    let sent_invites: Vec<SentInviteInfo> =
207        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")
208            .bind(&user_id)
209            .fetch_all(&**db_pool)
210            .await
211            .map_err(|_| {
212                Status::InternalServerError
213            })?;
214
215    // Get received invites with sender info
216    let received_invites: Vec<ReceivedInviteInfo> =
217        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")
218            .bind(&user_id)
219            .fetch_all(&**db_pool)
220            .await
221            .map_err(|_| {
222                Status::InternalServerError
223            })?;
224
225    let sent: Vec<serde_json::Value> = sent_invites
226        .into_iter()
227        .map(|invite| serde_json::to_value(invite).unwrap())
228        .collect();
229
230    let received: Vec<serde_json::Value> = received_invites
231        .into_iter()
232        .map(|invite| serde_json::to_value(invite).unwrap())
233        .collect();
234
235    Ok(serde_json::json!({
236        "sent": sent,
237        "received": received
238    })
239    .to_string())
240}
241
242/// Aceita convite para acesso ao dispositivo.
243#[post("/accept_invite/<invite_id>")]
244pub async fn accept_invite(
245    token: Token,
246    invite_id: i32,
247    db_pool: &State<PgPool>,
248) -> Result<(), Status> {
249    // Validate token
250    let user_row: Option<(String,)> =
251        sqlx::query_as("SELECT firebase_uid FROM users WHERE current_token = $1")
252            .bind(&token.0)
253            .fetch_optional(&**db_pool)
254            .await
255            .map_err(|_| Status::InternalServerError)?;
256    let user_id = match user_row {
257        Some((uid,)) => uid,
258        None => return Err(Status::Unauthorized),
259    };
260
261    // Update invite status to accepted (1)
262    let rows_affected = sqlx::query(
263        "UPDATE invites SET status = 1 WHERE id = $1 AND receiver_id = $2 AND status = 0",
264    )
265    .bind(invite_id)
266    .bind(&user_id)
267    .execute(&**db_pool)
268    .await
269    .map_err(|_| Status::InternalServerError)?
270    .rows_affected();
271
272    if rows_affected == 0 {
273        Err(Status::NotFound)
274    } else {
275        Ok(())
276    }
277}
278
279/// Rejeita convite.
280#[post("/reject_invite/<invite_id>")]
281pub async fn reject_invite(
282    token: Token,
283    invite_id: i32,
284    db_pool: &State<PgPool>,
285) -> Result<(), Status> {
286    // Validate token
287    let user_row: Option<(String,)> =
288        sqlx::query_as("SELECT firebase_uid FROM users WHERE current_token = $1")
289            .bind(&token.0)
290            .fetch_optional(&**db_pool)
291            .await
292            .map_err(|_| Status::InternalServerError)?;
293    let user_id = match user_row {
294        Some((uid,)) => uid,
295        None => {
296            return Err(Status::Unauthorized);
297        }
298    };
299
300    // Delete the invite
301    println!(
302        "DEBUG: Deleting invite with id: {}, receiver_id: {}",
303        invite_id, user_id
304    );
305    let rows_affected = sqlx::query("DELETE FROM invites WHERE id = $1 AND receiver_id = $2")
306        .bind(invite_id)
307        .bind(&user_id)
308        .execute(&**db_pool)
309        .await
310        .map_err(|_| Status::InternalServerError)?
311        .rows_affected();
312
313    if rows_affected == 0 {
314        return Err(Status::NotFound);
315    }
316
317    println!(
318        "DEBUG: Invite deleted successfully, rows affected: {}",
319        rows_affected
320    );
321    Ok(())
322}
323
324/// Cancela convite enviado.
325#[post("/cancel_invite/<invite_id>")]
326pub async fn cancel_invite(
327    token: Token,
328    invite_id: i32,
329    db_pool: &State<PgPool>,
330) -> Result<(), Status> {
331    // Validate token
332    let user_row: Option<(String,)> =
333        sqlx::query_as("SELECT firebase_uid FROM users WHERE current_token = $1")
334            .bind(&token.0)
335            .fetch_optional(&**db_pool)
336            .await
337            .map_err(|_| Status::InternalServerError)?;
338    let user_id = match user_row {
339        Some((uid,)) => uid,
340        None => return Err(Status::Unauthorized),
341    };
342
343    // Delete the invite
344    let rows_affected = sqlx::query("DELETE FROM invites WHERE id = $1 AND sender_id = $2")
345        .bind(invite_id)
346        .bind(&user_id)
347        .execute(&**db_pool)
348        .await
349        .map_err(|_| Status::InternalServerError)?;
350
351    if rows_affected.rows_affected() == 0 {
352        return Err(Status::NotFound);
353    }
354
355    Ok(())
356}
357
358/// Atualiza um convite (ex.: estender expiração).
359#[post("/update_invite/<invite_id>", data = "<request>")]
360pub async fn update_invite(
361    token: Token,
362    invite_id: i32,
363    request: rocket::serde::json::Json<UpdateInviteRequest>,
364    db_pool: &State<PgPool>,
365) -> Result<(), Status> {
366    // Validate token
367    let user_row: Option<(String,)> =
368        sqlx::query_as("SELECT firebase_uid FROM users WHERE current_token = $1")
369            .bind(&token.0)
370            .fetch_optional(&**db_pool)
371            .await
372            .map_err(|_| Status::InternalServerError)?;
373    let user_id = match user_row {
374        Some((uid,)) => uid,
375        None => return Err(Status::Unauthorized),
376    };
377
378    // Calculate new expiry timestamp
379    let now = Utc::now();
380    let new_expiry_timestamp = calculate_expiry_timestamp(now, &request.expiry_duration);
381
382    // Update invite expiry
383    let rows_affected =
384        sqlx::query("UPDATE invites SET expiry_timestamp = $1 WHERE id = $2 AND sender_id = $3")
385            .bind(new_expiry_timestamp)
386            .bind(invite_id)
387            .bind(&user_id)
388            .execute(&**db_pool)
389            .await
390            .map_err(|_| Status::InternalServerError)?;
391
392    if rows_affected.rows_affected() == 0 {
393        return Err(Status::NotFound);
394    }
395
396    Ok(())
397}
398
399/// Calcula timestamp de expiração a partir de string de duração (ex.: "1h", "30m").
400fn calculate_expiry_timestamp(base_time: chrono::DateTime<chrono::Utc>, duration: &str) -> i64 {
401    let duration = match duration {
402        "2_dias" => chrono::Duration::days(2),
403        "1_semana" => chrono::Duration::days(7),
404        "2_semanas" => chrono::Duration::days(14),
405        "1_mes" => chrono::Duration::days(30),
406        "permanente" => chrono::Duration::days(36500), // 100 years
407        _ => chrono::Duration::days(7),
408    };
409    (base_time + duration).timestamp_millis()
410}