1
1
use super :: frontend_prelude:: * ;
2
2
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 } ;
4
6
use 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 } ;
8
16
9
- /// Handles the `GET /me/crate_owner_invitations` route.
17
+ /// Handles the `GET /api/v1/ me/crate_owner_invitations` route.
10
18
pub 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 ( ) ;
13
21
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) ) ?;
19
25
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
50
28
. 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
+ } )
66
44
} )
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 > > > ( ) ?;
74
46
75
47
#[ derive( Serialize ) ]
76
48
struct R {
77
- crate_owner_invitations : Vec < EncodableCrateOwnerInvitation > ,
49
+ crate_owner_invitations : Vec < EncodableCrateOwnerInvitationV1 > ,
78
50
users : Vec < EncodablePublicUser > ,
79
51
}
80
52
Ok ( req. json ( & R {
@@ -83,12 +55,206 @@ pub fn list(req: &mut dyn RequestExt) -> EndpointResult {
83
55
} ) )
84
56
}
85
57
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
+
86
252
#[ derive( Deserialize ) ]
87
253
struct OwnerInvitation {
88
254
crate_owner_invite : InvitationResponse ,
89
255
}
90
256
91
- /// Handles the `PUT /me/crate_owner_invitations/:crate_id` route.
257
+ /// Handles the `PUT /api/v1/ me/crate_owner_invitations/:crate_id` route.
92
258
pub fn handle_invite ( req : & mut dyn RequestExt ) -> EndpointResult {
93
259
let mut body = String :: new ( ) ;
94
260
req. body ( ) . read_to_string ( & mut body) ?;
@@ -117,7 +283,7 @@ pub fn handle_invite(req: &mut dyn RequestExt) -> EndpointResult {
117
283
} ) )
118
284
}
119
285
120
- /// Handles the `PUT /me/crate_owner_invitations/accept/:token` route.
286
+ /// Handles the `PUT /api/v1/ me/crate_owner_invitations/accept/:token` route.
121
287
pub fn handle_invite_with_token ( req : & mut dyn RequestExt ) -> EndpointResult {
122
288
let config = & req. app ( ) . config ;
123
289
let conn = req. db_conn ( ) ?;
0 commit comments