@@ -3,12 +3,17 @@ use crate::auth::AuthCheck;
33use crate :: controllers:: helpers:: OkResponse ;
44use crate :: email:: EmailMessage ;
55use crate :: models:: NewEmail ;
6- use crate :: schema:: users;
6+ use crate :: schema:: { abandoned_usernames , reserved_usernames , users} ;
77use crate :: util:: errors:: { AppResult , bad_request, server_error} ;
8+
89use axum:: Json ;
910use axum:: extract:: Path ;
11+ use chrono:: { DateTime , SecondsFormat , TimeDelta , Utc } ;
12+ use crates_io_database:: fns:: canon_username;
13+ use crates_io_database:: models:: NewAbandonedUsername ;
14+ use crates_io_validation:: validate_username;
1015use diesel:: prelude:: * ;
11- use diesel_async:: RunQueryDsl ;
16+ use diesel_async:: { AsyncConnection , AsyncPgConnection , RunQueryDsl } ;
1217use http:: request:: Parts ;
1318use lettre:: Address ;
1419use minijinja:: context;
@@ -24,10 +29,14 @@ pub struct UserUpdate {
2429
2530#[ derive( Deserialize , utoipa:: ToSchema ) ]
2631pub struct User {
32+ username : Option < String > ,
2733 email : Option < String > ,
2834 publish_notifications : Option < bool > ,
2935}
3036
37+ /// Amount of time after a username is given up before it can be re-used
38+ const USERNAME_COOLDOWN : TimeDelta = TimeDelta :: days ( 30 ) ;
39+
3140/// Update user settings.
3241///
3342/// This endpoint allows users to update their email address and publish notifications settings.
@@ -97,6 +106,55 @@ pub async fn update_user(
97106 }
98107 }
99108
109+ if let Some ( newname) = & user_update. user . username
110+ && newname != & user. username
111+ {
112+ // stop immediately if username is invalid
113+ validate_username ( newname) . map_err ( bad_request) ?;
114+
115+ conn. transaction ( async |conn|
116+ {
117+ // checks to ensure the new username is available
118+ if is_reserved_username ( newname, conn) . await ? {
119+ return Err ( bad_request ( format ! ( "the username `{newname}` is reserved" ) ) ) ;
120+ }
121+ if username_conflict ( newname, user. id , conn) . await ?
122+ {
123+ return Err ( bad_request ( format ! (
124+ "the username `{newname}` is not available"
125+ ) ) ) ;
126+ }
127+ if let Some ( available_at) = username_has_cooldown ( newname, conn) . await ? {
128+ return Err ( bad_request ( format ! (
129+ "The username `{}` was recently in use. This username will be available after {}." ,
130+ newname,
131+ available_at. to_rfc3339_opts( SecondsFormat :: Secs , true )
132+ ) ) ) ;
133+ }
134+
135+ // build and apply updates to both `users` and `abandoned_usernames` tables
136+ let now = Utc :: now ( ) ;
137+ let available_at = now + USERNAME_COOLDOWN ;
138+ let abandonment_record = NewAbandonedUsername { username : & user. username ,
139+ previous_user_id : Some ( user. id ) ,
140+ adopted_at : user. current_username_adopted_at . as_ref ( ) ,
141+ abandoned_at : & now,
142+ available_at : & available_at
143+ } ;
144+
145+ diesel:: update ( user)
146+ . set ( ( users:: username. eq ( newname) , users:: current_username_adopted_at. eq ( now) ) )
147+ . execute ( conn)
148+ . await ?;
149+ diesel:: insert_into ( abandoned_usernames:: table)
150+ . values ( abandonment_record)
151+ . execute ( conn)
152+ . await ?;
153+
154+ Ok ( ( ) )
155+ } ) . await ?;
156+ }
157+
100158 if let Some ( user_email) = & user_update. user . email {
101159 let user_email = user_email. trim ( ) ;
102160
@@ -141,3 +199,64 @@ pub async fn update_user(
141199
142200 Ok ( OkResponse :: new ( ) )
143201}
202+
203+ /// Returns true if any users *besides the current one*
204+ /// have a username that conflicts with this one
205+ async fn username_conflict (
206+ username : & str ,
207+ user_id : i32 ,
208+ conn : & mut AsyncPgConnection ,
209+ ) -> Result < bool , diesel:: result:: Error > {
210+ let in_use_name_query: Option < String > = users:: table
211+ . filter ( users:: id. ne ( user_id) )
212+ . filter ( canon_username ( users:: username) . eq ( canon_username ( username) ) )
213+ . select ( users:: username)
214+ . first ( conn)
215+ . await
216+ . optional ( ) ?;
217+ if let Some ( _in_use_name) = in_use_name_query {
218+ Ok ( true )
219+ } else {
220+ Ok ( false )
221+ }
222+ }
223+
224+ async fn is_reserved_username (
225+ username : & str ,
226+ mut conn : & mut AsyncPgConnection ,
227+ ) -> Result < bool , diesel:: result:: Error > {
228+ let reserved_name_query: Option < String > = reserved_usernames:: table
229+ . filter ( canon_username ( reserved_usernames:: username) . eq ( canon_username ( username) ) )
230+ . select ( reserved_usernames:: username)
231+ . first ( & mut conn)
232+ . await
233+ . optional ( ) ?;
234+
235+ if let Some ( _reserved_name) = reserved_name_query {
236+ Ok ( true )
237+ } else {
238+ Ok ( false )
239+ }
240+ }
241+
242+ async fn username_has_cooldown (
243+ username : & str ,
244+ conn : & mut AsyncPgConnection ,
245+ ) -> Result < Option < DateTime < Utc > > , diesel:: result:: Error > {
246+ let abandoned_name_query: Option < ( String , DateTime < Utc > ) > = abandoned_usernames:: table
247+ . filter ( canon_username ( abandoned_usernames:: username) . eq ( canon_username ( username) ) )
248+ . filter ( abandoned_usernames:: available_at. gt ( Utc :: now ( ) ) )
249+ . select ( (
250+ abandoned_usernames:: username,
251+ abandoned_usernames:: available_at,
252+ ) )
253+ . first ( conn)
254+ . await
255+ . optional ( ) ?;
256+
257+ if let Some ( ( _abandoned_name, available_at) ) = abandoned_name_query {
258+ Ok ( Some ( available_at) )
259+ } else {
260+ Ok ( None )
261+ }
262+ }
0 commit comments