11use super :: frontend_prelude:: * ;
22
3- use crate :: models:: { CrateOwnerInvitation , User } ;
3+ use crate :: controllers:: helpers:: pagination:: { Page , PaginationOptions } ;
4+ use crate :: controllers:: util:: AuthenticatedUser ;
5+ use crate :: models:: { Crate , CrateOwnerInvitation , Rights , User } ;
46use crate :: schema:: { crate_owner_invitations, crates, users} ;
5- use crate :: views:: { EncodableCrateOwnerInvitation , EncodablePublicUser , InvitationResponse } ;
6- use diesel:: dsl:: any;
7- use std:: collections:: HashMap ;
7+ use crate :: util:: errors:: { forbidden, internal} ;
8+ use crate :: views:: {
9+ EncodableCrateOwnerInvitation , EncodableCrateOwnerInvitationV1 , EncodablePublicUser ,
10+ InvitationResponse ,
11+ } ;
12+ use chrono:: { Duration , Utc } ;
13+ use diesel:: { pg:: Pg , sql_types:: Bool } ;
14+ use indexmap:: IndexMap ;
15+ use std:: collections:: { HashMap , HashSet } ;
816
9- /// Handles the `GET /me/crate_owner_invitations` route.
17+ /// Handles the `GET /api/v1/ me/crate_owner_invitations` route.
1018pub fn list ( req : & mut dyn RequestExt ) -> EndpointResult {
11- // Ensure that the user is authenticated
12- let user = req . authenticate ( ) ? . forbid_api_token_auth ( ) ? . user ( ) ;
19+ let auth = req . authenticate ( ) ? . forbid_api_token_auth ( ) ? ;
20+ let user_id = auth . user_id ( ) ;
1321
14- // Load all pending invitations for the user
15- let conn = & * req. db_read_only ( ) ?;
16- let crate_owner_invitations: Vec < CrateOwnerInvitation > = crate_owner_invitations:: table
17- . filter ( crate_owner_invitations:: invited_user_id. eq ( user. id ) )
18- . load ( & * conn) ?;
22+ let PrivateListResponse {
23+ invitations, users, ..
24+ } = prepare_list ( req, auth, ListFilter :: InviteeId ( user_id) ) ?;
1925
20- // Make a list of all related users
21- let user_ids: Vec < _ > = crate_owner_invitations
22- . iter ( )
23- . map ( |invitation| invitation. invited_by_user_id )
24- . collect ( ) ;
25-
26- // Load all related users
27- let users: Vec < User > = users:: table
28- . filter ( users:: id. eq ( any ( user_ids) ) )
29- . load ( conn) ?;
30-
31- let users: HashMap < i32 , User > = users. into_iter ( ) . map ( |user| ( user. id , user) ) . collect ( ) ;
32-
33- // Make a list of all related crates
34- let crate_ids: Vec < _ > = crate_owner_invitations
35- . iter ( )
36- . map ( |invitation| invitation. crate_id )
37- . collect ( ) ;
38-
39- // Load all related crates
40- let crates: Vec < _ > = crates:: table
41- . select ( ( crates:: id, crates:: name) )
42- . filter ( crates:: id. eq ( any ( crate_ids) ) )
43- . load ( conn) ?;
44-
45- let crates: HashMap < i32 , String > = crates. into_iter ( ) . collect ( ) ;
46-
47- // Turn `CrateOwnerInvitation` list into `EncodableCrateOwnerInvitation` list
48- let config = & req. app ( ) . config ;
49- let crate_owner_invitations = crate_owner_invitations
26+ // The schema for the private endpoints is converted to the schema used by v1 endpoints.
27+ let crate_owner_invitations = invitations
5028 . into_iter ( )
51- . filter ( |i| !i . is_expired ( config ) )
52- . map ( |invitation| {
53- let inviter_id = invitation . invited_by_user_id ;
54- let inviter_name = users
55- . get ( & inviter_id)
56- . map ( |user| user. gh_login . clone ( ) )
57- . unwrap_or_default ( ) ;
58-
59- let crate_name = crates
60- . get ( & invitation . crate_id )
61- . cloned ( )
62- . unwrap_or_else ( || String :: from ( "(unknown crate name)" ) ) ;
63-
64- let expires_at = invitation . expires_at ( config ) ;
65- EncodableCrateOwnerInvitation :: from ( invitation , inviter_name , crate_name , expires_at )
29+ . map ( |private| {
30+ Ok ( EncodableCrateOwnerInvitationV1 {
31+ invited_by_username : users
32+ . iter ( )
33+ . find ( |u| u . id == private . inviter_id )
34+ . ok_or_else ( || internal ( & format ! ( "missing user {}" , private . inviter_id ) ) ) ?
35+ . login
36+ . clone ( ) ,
37+ invitee_id : private . invitee_id ,
38+ inviter_id : private . inviter_id ,
39+ crate_name : private . crate_name ,
40+ crate_id : private . crate_id ,
41+ created_at : private . created_at ,
42+ expires_at : private . expires_at ,
43+ } )
6644 } )
67- . collect ( ) ;
68-
69- // Turn `User` list into `EncodablePublicUser` list
70- let users = users
71- . into_iter ( )
72- . map ( |( _, user) | EncodablePublicUser :: from ( user) )
73- . collect ( ) ;
45+ . collect :: < AppResult < Vec < EncodableCrateOwnerInvitationV1 > > > ( ) ?;
7446
7547 #[ derive( Serialize ) ]
7648 struct R {
77- crate_owner_invitations : Vec < EncodableCrateOwnerInvitation > ,
49+ crate_owner_invitations : Vec < EncodableCrateOwnerInvitationV1 > ,
7850 users : Vec < EncodablePublicUser > ,
7951 }
8052 Ok ( req. json ( & R {
@@ -83,12 +55,206 @@ pub fn list(req: &mut dyn RequestExt) -> EndpointResult {
8355 } ) )
8456}
8557
58+ /// Handles the `GET /api/private/crate-owner-invitations` route.
59+ pub fn private_list ( req : & mut dyn RequestExt ) -> EndpointResult {
60+ let auth = req. authenticate ( ) ?. forbid_api_token_auth ( ) ?;
61+
62+ let filter = if let Some ( crate_name) = req. query ( ) . get ( "crate_name" ) {
63+ ListFilter :: CrateName ( crate_name. clone ( ) )
64+ } else if let Some ( id) = req. query ( ) . get ( "invitee_id" ) . and_then ( |i| i. parse ( ) . ok ( ) ) {
65+ ListFilter :: InviteeId ( id)
66+ } else {
67+ return Err ( bad_request ( "missing or invalid filter" ) ) ;
68+ } ;
69+
70+ let list = prepare_list ( req, auth, filter) ?;
71+ Ok ( req. json ( & list) )
72+ }
73+
74+ enum ListFilter {
75+ CrateName ( String ) ,
76+ InviteeId ( i32 ) ,
77+ }
78+
79+ fn prepare_list (
80+ req : & mut dyn RequestExt ,
81+ auth : AuthenticatedUser ,
82+ filter : ListFilter ,
83+ ) -> AppResult < PrivateListResponse > {
84+ let pagination: PaginationOptions = PaginationOptions :: builder ( )
85+ . enable_pages ( false )
86+ . enable_seek ( true )
87+ . gather ( req) ?;
88+
89+ let user = auth. user ( ) ;
90+ let conn = req. db_read_only ( ) ?;
91+ let config = & req. app ( ) . config ;
92+
93+ let mut crate_names = HashMap :: new ( ) ;
94+ let mut users = IndexMap :: new ( ) ;
95+ users. insert ( user. id , user. clone ( ) ) ;
96+
97+ let sql_filter: Box < dyn BoxableExpression < crate_owner_invitations:: table , Pg , SqlType = Bool > > =
98+ match filter {
99+ ListFilter :: CrateName ( crate_name) => {
100+ // Only allow crate owners to query pending invitations for their crate.
101+ let krate: Crate = Crate :: by_name ( & crate_name) . first ( & * conn) ?;
102+ let owners = krate. owners ( & * conn) ?;
103+ if user. rights ( req. app ( ) , & owners) ? != Rights :: Full {
104+ return Err ( forbidden ( ) ) ;
105+ }
106+
107+ // Cache the crate name to avoid querying it from the database again
108+ crate_names. insert ( krate. id , krate. name . clone ( ) ) ;
109+
110+ Box :: new ( crate_owner_invitations:: crate_id. eq ( krate. id ) )
111+ }
112+ ListFilter :: InviteeId ( invitee_id) => {
113+ if invitee_id != user. id {
114+ return Err ( forbidden ( ) ) ;
115+ }
116+ Box :: new ( crate_owner_invitations:: invited_user_id. eq ( invitee_id) )
117+ }
118+ } ;
119+
120+ // Load all the non-expired invitations matching the filter.
121+ let expire_cutoff = Duration :: days ( config. ownership_invitations_expiration_days as i64 ) ;
122+ let query = crate_owner_invitations:: table
123+ . filter ( sql_filter)
124+ . filter ( crate_owner_invitations:: created_at. gt ( ( Utc :: now ( ) - expire_cutoff) . naive_utc ( ) ) )
125+ . order_by ( (
126+ crate_owner_invitations:: crate_id,
127+ crate_owner_invitations:: invited_user_id,
128+ ) )
129+ // We fetch one element over the page limit to then detect whether there is a next page.
130+ . limit ( pagination. per_page as i64 + 1 ) ;
131+
132+ // Load and paginate the results.
133+ let mut raw_invitations: Vec < CrateOwnerInvitation > = match pagination. page {
134+ Page :: Unspecified => query. load ( & * conn) ?,
135+ Page :: Seek ( s) => {
136+ let seek_key: ( i32 , i32 ) = s. decode ( ) ?;
137+ query
138+ . filter (
139+ crate_owner_invitations:: crate_id. gt ( seek_key. 0 ) . or (
140+ crate_owner_invitations:: crate_id
141+ . eq ( seek_key. 0 )
142+ . and ( crate_owner_invitations:: invited_user_id. gt ( seek_key. 1 ) ) ,
143+ ) ,
144+ )
145+ . load ( & * conn) ?
146+ }
147+ Page :: Numeric ( _) => unreachable ! ( "page-based pagination is disabled" ) ,
148+ } ;
149+ let next_page = if raw_invitations. len ( ) > pagination. per_page as usize {
150+ // We fetch `per_page + 1` to check if there are records for the next page. Since the last
151+ // element is not what the user wanted it's discarded.
152+ raw_invitations. pop ( ) ;
153+
154+ if let Some ( last) = raw_invitations. last ( ) {
155+ let mut params = IndexMap :: new ( ) ;
156+ params. insert (
157+ "seek" . into ( ) ,
158+ crate :: controllers:: helpers:: pagination:: encode_seek ( (
159+ last. crate_id ,
160+ last. invited_user_id ,
161+ ) ) ?,
162+ ) ;
163+ Some ( req. query_with_params ( params) )
164+ } else {
165+ None
166+ }
167+ } else {
168+ None
169+ } ;
170+
171+ // Load all the related crates.
172+ let missing_crate_names = raw_invitations
173+ . iter ( )
174+ . map ( |i| i. crate_id )
175+ . filter ( |id| !crate_names. contains_key ( id) )
176+ . collect :: < Vec < _ > > ( ) ;
177+ if !missing_crate_names. is_empty ( ) {
178+ let new_names: Vec < ( i32 , String ) > = crates:: table
179+ . select ( ( crates:: id, crates:: name) )
180+ . filter ( crates:: id. eq_any ( missing_crate_names) )
181+ . load ( & * conn) ?;
182+ for ( id, name) in new_names. into_iter ( ) {
183+ crate_names. insert ( id, name) ;
184+ }
185+ }
186+
187+ // Load all the related users.
188+ let missing_users = raw_invitations
189+ . iter ( )
190+ . flat_map ( |invite| {
191+ std:: iter:: once ( invite. invited_user_id )
192+ . chain ( std:: iter:: once ( invite. invited_by_user_id ) )
193+ } )
194+ . filter ( |id| !users. contains_key ( id) )
195+ . collect :: < Vec < _ > > ( ) ;
196+ if !missing_users. is_empty ( ) {
197+ let new_users: Vec < User > = users:: table
198+ . filter ( users:: id. eq_any ( missing_users) )
199+ . load ( & * conn) ?;
200+ for user in new_users. into_iter ( ) {
201+ users. insert ( user. id , user) ;
202+ }
203+ }
204+
205+ // Turn `CrateOwnerInvitation`s into `EncodablePrivateCrateOwnerInvitation`.
206+ let config = & req. app ( ) . config ;
207+ let mut invitations = Vec :: new ( ) ;
208+ let mut users_in_response = HashSet :: new ( ) ;
209+ for invitation in raw_invitations. into_iter ( ) {
210+ invitations. push ( EncodableCrateOwnerInvitation {
211+ invitee_id : invitation. invited_user_id ,
212+ inviter_id : invitation. invited_by_user_id ,
213+ crate_id : invitation. crate_id ,
214+ crate_name : crate_names
215+ . get ( & invitation. crate_id )
216+ . ok_or_else ( || internal ( & format ! ( "missing crate with id {}" , invitation. crate_id) ) ) ?
217+ . clone ( ) ,
218+ created_at : invitation. created_at ,
219+ expires_at : invitation. expires_at ( config) ,
220+ } ) ;
221+ users_in_response. insert ( invitation. invited_user_id ) ;
222+ users_in_response. insert ( invitation. invited_by_user_id ) ;
223+ }
224+
225+ // Provide a stable response for the users list, only including the referenced users with
226+ // stable sorting.
227+ users. retain ( |k, _| users_in_response. contains ( k) ) ;
228+ users. sort_keys ( ) ;
229+
230+ Ok ( PrivateListResponse {
231+ invitations,
232+ users : users
233+ . into_iter ( )
234+ . map ( |( _, user) | EncodablePublicUser :: from ( user) )
235+ . collect ( ) ,
236+ meta : ResponseMeta { next_page } ,
237+ } )
238+ }
239+
240+ #[ derive( Serialize ) ]
241+ struct PrivateListResponse {
242+ invitations : Vec < EncodableCrateOwnerInvitation > ,
243+ users : Vec < EncodablePublicUser > ,
244+ meta : ResponseMeta ,
245+ }
246+
247+ #[ derive( Serialize ) ]
248+ struct ResponseMeta {
249+ next_page : Option < String > ,
250+ }
251+
86252#[ derive( Deserialize ) ]
87253struct OwnerInvitation {
88254 crate_owner_invite : InvitationResponse ,
89255}
90256
91- /// Handles the `PUT /me/crate_owner_invitations/:crate_id` route.
257+ /// Handles the `PUT /api/v1/ me/crate_owner_invitations/:crate_id` route.
92258pub fn handle_invite ( req : & mut dyn RequestExt ) -> EndpointResult {
93259 let mut body = String :: new ( ) ;
94260 req. body ( ) . read_to_string ( & mut body) ?;
@@ -117,7 +283,7 @@ pub fn handle_invite(req: &mut dyn RequestExt) -> EndpointResult {
117283 } ) )
118284}
119285
120- /// Handles the `PUT /me/crate_owner_invitations/accept/:token` route.
286+ /// Handles the `PUT /api/v1/ me/crate_owner_invitations/accept/:token` route.
121287pub fn handle_invite_with_token ( req : & mut dyn RequestExt ) -> EndpointResult {
122288 let config = & req. app ( ) . config ;
123289 let conn = req. db_conn ( ) ?;
0 commit comments