1+ use crate :: models:: app:: AppMessage ;
2+ use crate :: models:: auth:: AuthUser ;
13use crate :: models:: error:: ChaosError ;
2- use sqlx:: Postgres ;
3- use chrono:: Utc ;
44use crate :: models:: invite:: Invite ;
5+ use crate :: models:: organisation:: Organisation ;
56use crate :: models:: transaction:: DBTransaction ;
7+ use axum:: extract:: Path ;
68use axum:: response:: IntoResponse ;
9+ use chrono:: { DateTime , Utc } ;
10+ use serde:: Serialize ;
11+ use sqlx:: query;
12+ use std:: ops:: DerefMut ;
713
814
915/// Handler for invite-related HTTP requests.
1016pub struct InviteHandler ;
1117
1218impl InviteHandler {
13- /// Validates whether an invite code is still valid (i.e. not expired) .
19+ /// Gets invite details for a given invite code .
1420 ///
1521 /// # Arguments
1622 ///
17- /// * `code ` - The invite code to validate
18- /// * `transaction ` - Database transaction to use
23+ /// * `transaction ` - Database transaction
24+ /// * `code ` - Invite code
1925 ///
2026 /// # Returns
2127 ///
22- /// * `Result<Invite, ChaosError>` - `Ok(invite)` if valid, `Err` if lookup fails
23- pub async fn validate_invite_is_valid (
24- code : & str ,
25- transaction : & mut Transaction < ' _ , Postgres > ,
26- ) -> Result < Invite , ChaosError > {
27- let invite = Invite :: get ( code, transaction) . await ?;
28- // check if the invite has already been used
29- if invite. used_at . is_some ( ) {
30- return Err ( ChaosError :: BadRequestWithMessage (
31- "Invite already used" . to_string ( ) ,
32- ) ) ;
33- }
34- // check if the invite has expired
35- if invite. expires_at <= Utc :: now ( ) {
36- return Err ( ChaosError :: BadRequestWithMessage (
37- "Invite expired" . to_string ( ) ,
38- ) ) ;
39- }
40- Ok ( invite)
28+ /// * `Result<impl IntoResponse, ChaosError>` - Invite details or error
29+ pub async fn get (
30+ mut transaction : DBTransaction < ' _ > ,
31+ Path ( code) : Path < String > ,
32+ ) -> Result < impl IntoResponse , ChaosError > {
33+ let invite = Invite :: get_by_code ( & code, & mut transaction. tx ) . await ?;
34+ let org = query ! (
35+ "SELECT name FROM organisations WHERE id = $1" ,
36+ invite. organisation_id
37+ )
38+ . fetch_one ( transaction. tx . deref_mut ( ) )
39+ . await ?;
40+
41+ // Include derived booleans expected by the frontend.
42+ let details = InviteDetails {
43+ organisation_id : invite. organisation_id ,
44+ organisation_name : org. name ,
45+ email : invite. email ,
46+ expires_at : invite. expires_at ,
47+ used : invite. used_at . is_some ( ) ,
48+ expired : invite. expires_at <= Utc :: now ( ) ,
49+ invited_by_organisation_id : invite. invited_by_organisation_id ,
50+ } ;
51+
52+ transaction. tx . commit ( ) . await ?;
53+ Ok ( AppMessage :: OkMessage ( details) )
4154 }
42-
4355
44- /// Uses an invite code for a user.
56+ /// Accepts an invite for the current authenticated user.
4557 ///
46- /// Validates the invite, marks it as used by the given user
58+ /// Validates the invite is not expired/used, then marks it as used.
4759 ///
4860 /// # Arguments
4961 ///
50- /// * `code ` - The invite code being redeemed
51- /// * `user_id ` - The ID of the user redeeming the invite
52- /// * `transaction ` - Database transaction to use
62+ /// * `transaction ` - Database transaction
63+ /// * `code ` - Invite code
64+ /// * `user ` - Authenticated user
5365 ///
5466 /// # Returns
5567 ///
56- /// * `Result<impl IntoResponse, ChaosError>` - `Ok(())` if the invite was used; `Err` if the invite is invalid
68+ /// * `Result<impl IntoResponse, ChaosError>` - Success message or error
5769 pub async fn use_invite (
58- code : & str ,
59- user_id : i64 ,
60- transaction : & mut Transaction < ' _ , Postgres > ,
70+ mut transaction : DBTransaction < ' _ > ,
71+ Path ( code ) : Path < String > ,
72+ user : AuthUser ,
6173 ) -> Result < impl IntoResponse , ChaosError > {
74+ let invite = Invite :: get_by_code ( & code, & mut transaction. tx ) . await ?;
6275
63- let invite = Self :: validate_invite_is_valid ( code, transaction) . await ?;
76+ // Validate the invite is not already used or expired.
77+ if invite. used_at . is_some ( ) {
78+ return Err ( ChaosError :: BadRequestWithMessage ( "Invite already used" . to_string ( ) ) ) ;
79+ }
80+ if invite. expires_at <= Utc :: now ( ) {
81+ return Err ( ChaosError :: BadRequestWithMessage ( "Invite expired" . to_string ( ) ) ) ;
82+ }
6483
65- Invite :: save_used_by_person ( user_id, & invite, transaction) . await ?;
84+ // Add the user to the organisation.
85+ Organisation :: add_user ( invite. organisation_id , user. user_id , & mut transaction. tx ) . await ?;
6686
67- transaction. tx . commit ( ) . await ?;
87+ // Mark the invite as used.
88+ Invite :: mark_used ( & code, user. user_id , invite. invited_by_organisation_id , & mut transaction. tx ) . await ?;
6889
69- Ok ( ( StatusCode :: OK , AppMessage :: OkMessage ( "Invite used successfully" ) ) )
90+ transaction. tx . commit ( ) . await ?;
91+ Ok ( AppMessage :: OkMessage ( "Invite accepted successfully" ) )
7092 }
7193
72- pub async fn get_code_by_id (
73- invite_id : i64 ,
74- transaction : & mut DBTransaction < ' _ > ,
75- ) -> Result < String , ChaosError > {
76- let invite = Invite :: get ( invite_id, & mut transaction. tx ) . await ?;
77- Ok ( invite. code )
78- }
94+
95+ }
96+
97+ /// Response payload for invite details expected by the frontend.
98+ #[ derive( Serialize ) ]
99+ pub struct InviteDetails {
100+ #[ serde( serialize_with = "crate::models::serde_string::serialize" ) ]
101+ pub organisation_id : i64 ,
102+ pub organisation_name : String ,
103+ pub email : String ,
104+ pub expires_at : DateTime < Utc > ,
105+ pub used : bool ,
106+ pub expired : bool ,
107+ /// ID of the organisation that invited the user
108+ #[ serde( serialize_with = "crate::models::serde_string::serialize_option" ) ]
109+ #[ serde( deserialize_with = "crate::models::serde_string::deserialize_option" ) ]
110+ pub invited_by_organisation_id : Option < i64 >
79111}
0 commit comments