From 41f7555128ef9c3fbff92754949d786ca1eb1305 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Wed, 8 Jul 2026 16:53:23 +0200 Subject: [PATCH 1/8] Ability to see previous usernames via API --- .../GameDatabaseContext.Registration.cs | 18 ++++++++++++ .../Endpoints/Admin/AdminUserApiEndpoints.cs | 25 +++++++++++++++++ .../ApiExtendedPreviousUsernameResponse.cs | 23 +++++++++++++++ .../Users/ApiPreviousUsernameResponse.cs | 28 +++++++++++++++++++ .../Endpoints/UserApiEndpoints.cs | 13 +++++++++ 5 files changed, 107 insertions(+) create mode 100644 Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/ApiExtendedPreviousUsernameResponse.cs create mode 100644 Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/ApiPreviousUsernameResponse.cs diff --git a/Refresh.Database/GameDatabaseContext.Registration.cs b/Refresh.Database/GameDatabaseContext.Registration.cs index 58e2a5318..b2ee50867 100644 --- a/Refresh.Database/GameDatabaseContext.Registration.cs +++ b/Refresh.Database/GameDatabaseContext.Registration.cs @@ -8,6 +8,9 @@ namespace Refresh.Database; public partial class GameDatabaseContext // Registration { + private IQueryable PreviousUsernamesIncluded => this.PreviousUsernames + .Include(p => p.User); + public GameUser CreateUser(string username, string emailAddress, bool skipChecks = false) { if (!skipChecks) @@ -110,6 +113,21 @@ public bool IsUsernameTaken(string username, GameUser? userToName = null) return false; } + + public bool WasUsernamePreviouslyTaken(string username) + { + return this.PreviousUsernames.Any(u => u.Username == username); + } + + public DatabaseList GetPreviousUsernameRecordsForUsername(string username, int skip, int count) + { + return new(this.PreviousUsernamesIncluded.Where(u => u.Username == username), skip, count); + } + + public DatabaseList GetPreviousUsernameRecordsByUser(GameUser user, int skip, int count) + { + return new(this.PreviousUsernamesIncluded.Where(u => u.UserId == user.UserId), skip, count); + } public bool IsEmailTaken(string emailAddress) { diff --git a/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminUserApiEndpoints.cs b/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminUserApiEndpoints.cs index 444702150..d925ac548 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminUserApiEndpoints.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminUserApiEndpoints.cs @@ -50,6 +50,31 @@ public ApiListResponse GetExtendedUsers(RequestCont return list; } + [ApiV3Endpoint("admin/previousUsernames/byName/{username}"), MinimumRole(GameUserRole.Moderator)] + [DocSummary("Gets all users (with extended information) who have once used the specified username.")] + [DocUsesPageData] + public ApiListResponse GetExtendedPreviousUsernamesByUsername(RequestContext context, + GameDatabaseContext database, IDataStore dataStore, DataContext dataContext, string username) + { + (int skip, int count) = context.GetPageData(); + DatabaseList previousNames = database.GetPreviousUsernameRecordsForUsername(username, skip, count); + return DatabaseListExtensions.FromOldList(previousNames, dataContext); + } + + [ApiV3Endpoint("admin/previousUsernames/byUserUuid/{uuid}"), MinimumRole(GameUserRole.Moderator)] + [DocSummary("Gets all previous usernames which have once been used by the specified user.")] + [DocUsesPageData] + public ApiListResponse GetExtendedPreviousUsernamesByUser(RequestContext context, + GameDatabaseContext database, IDataStore dataStore, DataContext dataContext, string uuid) + { + GameUser? user = database.GetUserByUuid(uuid); + if (user == null) return ApiNotFoundError.UserMissingError; + + (int skip, int count) = context.GetPageData(); + DatabaseList previousNames = database.GetPreviousUsernameRecordsByUser(user, skip, count); + return DatabaseListExtensions.FromOldList(previousNames, dataContext); + } + [ApiV3Endpoint("admin/users/{idType}/{id}/resetPassword", HttpMethods.Put), MinimumRole(GameUserRole.Moderator)] [DocSummary("Resets a user's password by their UUID or username.")] [DocError(typeof(ApiNotFoundError), ApiNotFoundError.UserMissingErrorWhen)] diff --git a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/ApiExtendedPreviousUsernameResponse.cs b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/ApiExtendedPreviousUsernameResponse.cs new file mode 100644 index 000000000..e288c57b9 --- /dev/null +++ b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/ApiExtendedPreviousUsernameResponse.cs @@ -0,0 +1,23 @@ +using Refresh.Core.Types.Data; +using Refresh.Database.Models.Users; + +namespace Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Users; + +[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] +public class ApiExtendedPreviousUsernameResponse : ApiPreviousUsernameResponse, IDataConvertableFrom +{ + public new static ApiExtendedPreviousUsernameResponse? FromOld(PreviousUsername? old, DataContext dataContext) + { + if (old == null) return null; + + return new ApiExtendedPreviousUsernameResponse + { + Username = old.Username, + User = ApiExtendedGameUserResponse.FromOld(old.User, dataContext)!, + ReplacedAt = old.ReplacedAt, + }; + } + + public new static IEnumerable FromOldList(IEnumerable oldList, DataContext dataContext) + => oldList.Select(old => FromOld(old, dataContext)).ToList()!; +} \ No newline at end of file diff --git a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/ApiPreviousUsernameResponse.cs b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/ApiPreviousUsernameResponse.cs new file mode 100644 index 000000000..9ea6ba4b5 --- /dev/null +++ b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/ApiPreviousUsernameResponse.cs @@ -0,0 +1,28 @@ +using Refresh.Core.Types.Data; +using Refresh.Database.Models.Users; + +namespace Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Users; + +[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] +public class ApiPreviousUsernameResponse : IApiResponse, IDataConvertableFrom +{ + public required string Username { get; set; } + public required ApiGameUserResponse User { get; set; } + public required DateTimeOffset ReplacedAt { get; set; } + + public static ApiPreviousUsernameResponse? FromOld(PreviousUsername? old, DataContext dataContext) + { + if (old == null) return null; + + return new ApiPreviousUsernameResponse + { + Username = old.Username, + User = ApiGameUserResponse.FromOld(old.User, dataContext)!, + ReplacedAt = old.ReplacedAt, + }; + } + + public static IEnumerable FromOldList(IEnumerable oldList, DataContext dataContext) + => oldList.Select(old => FromOld(old, dataContext)).ToList()!; +} + diff --git a/Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs b/Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs index f500de340..d53067eb3 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs @@ -15,6 +15,7 @@ using Refresh.Database.Models.Authentication; using Refresh.Database.Models.Pins; using Refresh.Database.Models.Users; +using Refresh.Interfaces.APIv3.Documentation.Attributes; using Refresh.Interfaces.APIv3.Documentation.Descriptions; using Refresh.Interfaces.APIv3.Endpoints.ApiTypes; using Refresh.Interfaces.APIv3.Endpoints.ApiTypes.Errors; @@ -99,6 +100,18 @@ public ApiResponse GetMyUser(RequestContext context return ApiExtendedGameUserResponse.FromOld(user, dataContext); } + [ApiV3Endpoint("users/me/previousUsernames"), MinimumRole(GameUserRole.Moderator)] + [DocSummary("Gets all previous usernames which you have used.")] + [DocUsesPageData] + [RateLimitSettings(120, 35, 80, "me-api")] // TODO remove when we clean up rate-limit stats + public ApiListResponse GetMyPreviousUsernames(RequestContext context, + GameDatabaseContext database, IDataStore dataStore, DataContext dataContext, GameUser user) + { + (int skip, int count) = context.GetPageData(); + DatabaseList previousNames = database.GetPreviousUsernameRecordsByUser(user, skip, count); + return DatabaseListExtensions.FromOldList(previousNames, dataContext); + } + [ApiV3Endpoint("users/me", HttpMethods.Patch)] [DocSummary("Updates your profile with the given data")] [RateLimitSettings(UserModificationEndpointLimits.TimeoutDuration, UserModificationEndpointLimits.ApiRequestAmount, From 4c7edbe3396ad388351dc6a11869092266507c7a Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Mon, 24 Aug 2026 16:06:36 +0200 Subject: [PATCH 2/8] Allow searching previous usernames by user's current name --- .../Endpoints/Admin/AdminUserApiEndpoints.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminUserApiEndpoints.cs b/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminUserApiEndpoints.cs index d925ac548..396c090ee 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminUserApiEndpoints.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminUserApiEndpoints.cs @@ -61,13 +61,13 @@ public ApiListResponse GetExtendedPreviousU return DatabaseListExtensions.FromOldList(previousNames, dataContext); } - [ApiV3Endpoint("admin/previousUsernames/byUserUuid/{uuid}"), MinimumRole(GameUserRole.Moderator)] + [ApiV3Endpoint("admin/previousUsernames/byUser/{idType}/{id}"), MinimumRole(GameUserRole.Moderator)] [DocSummary("Gets all previous usernames which have once been used by the specified user.")] [DocUsesPageData] public ApiListResponse GetExtendedPreviousUsernamesByUser(RequestContext context, - GameDatabaseContext database, IDataStore dataStore, DataContext dataContext, string uuid) + GameDatabaseContext database, IDataStore dataStore, DataContext dataContext, string idType, string id) { - GameUser? user = database.GetUserByUuid(uuid); + GameUser? user = database.GetUserByIdAndType(idType, id); if (user == null) return ApiNotFoundError.UserMissingError; (int skip, int count) = context.GetPageData(); From 8143e1dcca7fa1271737f97d02bef6f07626905a Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Mon, 24 Aug 2026 16:18:07 +0200 Subject: [PATCH 3/8] Sort previous username lists --- Refresh.Database/GameDatabaseContext.Registration.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Refresh.Database/GameDatabaseContext.Registration.cs b/Refresh.Database/GameDatabaseContext.Registration.cs index b2ee50867..5714b8233 100644 --- a/Refresh.Database/GameDatabaseContext.Registration.cs +++ b/Refresh.Database/GameDatabaseContext.Registration.cs @@ -121,12 +121,16 @@ public bool WasUsernamePreviouslyTaken(string username) public DatabaseList GetPreviousUsernameRecordsForUsername(string username, int skip, int count) { - return new(this.PreviousUsernamesIncluded.Where(u => u.Username == username), skip, count); + return new(this.PreviousUsernamesIncluded + .Where(u => u.Username == username) + .OrderByDescending(u => u.ReplacedAt), skip, count); } public DatabaseList GetPreviousUsernameRecordsByUser(GameUser user, int skip, int count) { - return new(this.PreviousUsernamesIncluded.Where(u => u.UserId == user.UserId), skip, count); + return new(this.PreviousUsernamesIncluded + .Where(u => u.UserId == user.UserId) + .OrderByDescending(u => u.ReplacedAt), skip, count); } public bool IsEmailTaken(string emailAddress) From 8a935b20703aedba5a0489fc7c9d859612c52faf Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Mon, 24 Aug 2026 18:05:43 +0200 Subject: [PATCH 4/8] Fix routing to /users/me/previousUsernames, don't include extended users there --- .../Endpoints/UserApiEndpoints.cs | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs b/Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs index d53067eb3..691fad204 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs @@ -2,7 +2,9 @@ using Bunkum.Core; using Bunkum.Core.Endpoints; using Bunkum.Core.RateLimit; +using Bunkum.Core.Responses; using Bunkum.Core.Storage; +using Bunkum.Listener.Protocol; using Bunkum.Protocols.Http; using Refresh.Common.Constants; using Refresh.Core.Authentication.Permission; @@ -32,17 +34,24 @@ public class UserApiEndpoints : EndpointGroup [DocError(typeof(ApiNotFoundError), "The user cannot be found")] [RateLimitSettings(SingleUserEndpointLimits.TimeoutDuration, SingleUserEndpointLimits.ApiRequestAmount, SingleUserEndpointLimits.BlockDuration, SingleUserEndpointLimits.ApiRequestBucket)] - public ApiResponse GetUser(RequestContext context, GameDatabaseContext database, + public Response GetUser(RequestContext context, GameDatabaseContext database, GameUser? user, [DocSummary(SharedParamDescriptions.UserIdParam)] string id, [DocSummary(SharedParamDescriptions.UserIdTypeParam)] string idType, DataContext dataContext) { - GameUser? user = database.GetUserByIdAndType(idType, id); - if(user == null) return ApiNotFoundError.UserMissingError; + // Hack to prevent Bunkum from routing other endpoints' requests to here until we finally fix Bunkum's routing + // to prioritize methods with less route params + if (id == "previousUsernames") + { + if (user == null) return ApiAuthenticationError.NotAuthenticated; + return new(this.GetMyPreviousUsernames(context, database, dataContext, user), ContentType.Json); + } - return ApiGameUserResponse.FromOld(user, dataContext); + GameUser? targetUser = database.GetUserByIdAndType(idType, id); + if (targetUser == null) return ApiNotFoundError.UserMissingError; + + return new(ApiGameUserResponse.FromOld(targetUser, dataContext), ContentType.Json); } - // TODO: Also allow specifying user by username [ApiV3Endpoint("users/{idType}/{id}/heart", HttpMethods.Post)] [DocSummary("Hearts a user by their name or UUID")] [DocError(typeof(ApiNotFoundError), ApiNotFoundError.UserMissingErrorWhen)] @@ -104,12 +113,12 @@ public ApiResponse GetMyUser(RequestContext context [DocSummary("Gets all previous usernames which you have used.")] [DocUsesPageData] [RateLimitSettings(120, 35, 80, "me-api")] // TODO remove when we clean up rate-limit stats - public ApiListResponse GetMyPreviousUsernames(RequestContext context, - GameDatabaseContext database, IDataStore dataStore, DataContext dataContext, GameUser user) + public ApiListResponse GetMyPreviousUsernames(RequestContext context, + GameDatabaseContext database, DataContext dataContext, GameUser user) { (int skip, int count) = context.GetPageData(); DatabaseList previousNames = database.GetPreviousUsernameRecordsByUser(user, skip, count); - return DatabaseListExtensions.FromOldList(previousNames, dataContext); + return DatabaseListExtensions.FromOldList(previousNames, dataContext); } [ApiV3Endpoint("users/me", HttpMethods.Patch)] From 4d758053b59781395614534ef52b601be9668b97 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Mon, 24 Aug 2026 18:13:38 +0200 Subject: [PATCH 5/8] Move/deduplicate methods and properties in DB context --- .../GameDatabaseContext.Registration.cs | 17 ----------------- Refresh.Database/GameDatabaseContext.Users.cs | 10 +++++++++- .../Endpoints/Admin/AdminUserApiEndpoints.cs | 2 +- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/Refresh.Database/GameDatabaseContext.Registration.cs b/Refresh.Database/GameDatabaseContext.Registration.cs index 3137fa1c5..0df6fd181 100644 --- a/Refresh.Database/GameDatabaseContext.Registration.cs +++ b/Refresh.Database/GameDatabaseContext.Registration.cs @@ -8,9 +8,6 @@ namespace Refresh.Database; public partial class GameDatabaseContext // Registration { - private IQueryable PreviousUsernamesIncluded => this.PreviousUsernames - .Include(p => p.User); - public GameUser CreateUser(string username, string emailAddress, bool skipChecks = false) { if (!skipChecks) @@ -111,20 +108,6 @@ public bool WasUsernamePreviouslyTaken(string username) { return this.PreviousUsernames.Any(u => u.Username == username); } - - public DatabaseList GetPreviousUsernameRecordsForUsername(string username, int skip, int count) - { - return new(this.PreviousUsernamesIncluded - .Where(u => u.Username == username) - .OrderByDescending(u => u.ReplacedAt), skip, count); - } - - public DatabaseList GetPreviousUsernameRecordsByUser(GameUser user, int skip, int count) - { - return new(this.PreviousUsernamesIncluded - .Where(u => u.UserId == user.UserId) - .OrderByDescending(u => u.ReplacedAt), skip, count); - } public bool IsEmailTaken(string emailAddress) { diff --git a/Refresh.Database/GameDatabaseContext.Users.cs b/Refresh.Database/GameDatabaseContext.Users.cs index 002ff3be0..021cc7862 100644 --- a/Refresh.Database/GameDatabaseContext.Users.cs +++ b/Refresh.Database/GameDatabaseContext.Users.cs @@ -128,7 +128,15 @@ public DatabaseList GetMostFavouritedUsers(int skip, int count) public DatabaseList GetPreviousUsernameRecordsByName(string username, int skip, int count) { return new(this.PreviousUsernamesIncluded - .Where(u => u.Username == username), skip, count); + .Where(u => u.Username == username) + .OrderByDescending(u => u.ReplacedAt), skip, count); + } + + public DatabaseList GetPreviousUsernameRecordsByUser(GameUser user, int skip, int count) + { + return new(this.PreviousUsernamesIncluded + .Where(u => u.UserId == user.UserId) + .OrderByDescending(u => u.ReplacedAt), skip, count); } public void UpdateUserData(GameUser user, ISerializedEditUser data, TokenGame game) diff --git a/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminUserApiEndpoints.cs b/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminUserApiEndpoints.cs index c4084dae4..886e9b24c 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminUserApiEndpoints.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminUserApiEndpoints.cs @@ -57,7 +57,7 @@ public ApiListResponse GetExtendedPreviousU GameDatabaseContext database, IDataStore dataStore, DataContext dataContext, string username) { (int skip, int count) = context.GetPageData(); - DatabaseList previousNames = database.GetPreviousUsernameRecordsForUsername(username, skip, count); + DatabaseList previousNames = database.GetPreviousUsernameRecordsByName(username, skip, count); return DatabaseListExtensions.FromOldList(previousNames, dataContext); } From 8c32069b4d71bcfe3415eace1b3d6c60319dd813 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Mon, 24 Aug 2026 18:58:02 +0200 Subject: [PATCH 6/8] Disable endpoint to get own previous usernames for now --- .../Endpoints/UserApiEndpoints.cs | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs b/Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs index 691fad204..9266233d4 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs @@ -34,22 +34,14 @@ public class UserApiEndpoints : EndpointGroup [DocError(typeof(ApiNotFoundError), "The user cannot be found")] [RateLimitSettings(SingleUserEndpointLimits.TimeoutDuration, SingleUserEndpointLimits.ApiRequestAmount, SingleUserEndpointLimits.BlockDuration, SingleUserEndpointLimits.ApiRequestBucket)] - public Response GetUser(RequestContext context, GameDatabaseContext database, GameUser? user, + public ApiResponse GetUser(RequestContext context, GameDatabaseContext database, [DocSummary(SharedParamDescriptions.UserIdParam)] string id, [DocSummary(SharedParamDescriptions.UserIdTypeParam)] string idType, DataContext dataContext) { - // Hack to prevent Bunkum from routing other endpoints' requests to here until we finally fix Bunkum's routing - // to prioritize methods with less route params - if (id == "previousUsernames") - { - if (user == null) return ApiAuthenticationError.NotAuthenticated; - return new(this.GetMyPreviousUsernames(context, database, dataContext, user), ContentType.Json); - } + GameUser? user = database.GetUserByIdAndType(idType, id); + if(user == null) return ApiNotFoundError.UserMissingError; - GameUser? targetUser = database.GetUserByIdAndType(idType, id); - if (targetUser == null) return ApiNotFoundError.UserMissingError; - - return new(ApiGameUserResponse.FromOld(targetUser, dataContext), ContentType.Json); + return ApiGameUserResponse.FromOld(user, dataContext); } [ApiV3Endpoint("users/{idType}/{id}/heart", HttpMethods.Post)] @@ -109,10 +101,19 @@ public ApiResponse GetMyUser(RequestContext context return ApiExtendedGameUserResponse.FromOld(user, dataContext); } + /* + * TODO enable this function/endpoint once Bunkum stops routing requests addressed at this endpoint to GetUser() instead. + * Such a fix could be making Bunkum's router prefer endpoints with the least route parameters, + * since the issue here is that requests meant for this endpoint have their route match with /users/{idType}/{id}, + * where "me" is used for the idType param and "previousUsernames" is used for the id param. + * + * Wanted to do a hack for this first, just like with /users/me vs /users/{route}, but this time it got too complicated. + * Should just fix the underlying problem at this point. + */ + /* [ApiV3Endpoint("users/me/previousUsernames"), MinimumRole(GameUserRole.Moderator)] [DocSummary("Gets all previous usernames which you have used.")] [DocUsesPageData] - [RateLimitSettings(120, 35, 80, "me-api")] // TODO remove when we clean up rate-limit stats public ApiListResponse GetMyPreviousUsernames(RequestContext context, GameDatabaseContext database, DataContext dataContext, GameUser user) { @@ -120,6 +121,7 @@ public ApiListResponse GetMyPreviousUsernames(Reque DatabaseList previousNames = database.GetPreviousUsernameRecordsByUser(user, skip, count); return DatabaseListExtensions.FromOldList(previousNames, dataContext); } + */ [ApiV3Endpoint("users/me", HttpMethods.Patch)] [DocSummary("Updates your profile with the given data")] From 3aec5805cce0085663d9ada1915e12315f201296 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Mon, 24 Aug 2026 19:26:58 +0200 Subject: [PATCH 7/8] Just remove the endpoint completely for now --- .../Endpoints/UserApiEndpoints.cs | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs b/Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs index 9266233d4..e6f9d6509 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs @@ -101,28 +101,6 @@ public ApiResponse GetMyUser(RequestContext context return ApiExtendedGameUserResponse.FromOld(user, dataContext); } - /* - * TODO enable this function/endpoint once Bunkum stops routing requests addressed at this endpoint to GetUser() instead. - * Such a fix could be making Bunkum's router prefer endpoints with the least route parameters, - * since the issue here is that requests meant for this endpoint have their route match with /users/{idType}/{id}, - * where "me" is used for the idType param and "previousUsernames" is used for the id param. - * - * Wanted to do a hack for this first, just like with /users/me vs /users/{route}, but this time it got too complicated. - * Should just fix the underlying problem at this point. - */ - /* - [ApiV3Endpoint("users/me/previousUsernames"), MinimumRole(GameUserRole.Moderator)] - [DocSummary("Gets all previous usernames which you have used.")] - [DocUsesPageData] - public ApiListResponse GetMyPreviousUsernames(RequestContext context, - GameDatabaseContext database, DataContext dataContext, GameUser user) - { - (int skip, int count) = context.GetPageData(); - DatabaseList previousNames = database.GetPreviousUsernameRecordsByUser(user, skip, count); - return DatabaseListExtensions.FromOldList(previousNames, dataContext); - } - */ - [ApiV3Endpoint("users/me", HttpMethods.Patch)] [DocSummary("Updates your profile with the given data")] [RateLimitSettings(UserModificationEndpointLimits.TimeoutDuration, UserModificationEndpointLimits.ApiRequestAmount, From 93e23b1c61cc7d3af2b32f66722ff86b03ffe51a Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Tue, 25 Aug 2026 17:31:30 +0200 Subject: [PATCH 8/8] Test guarding of admin previous username list endpoints --- .../Tests/ApiV3/AdminUserEditApiTests.cs | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/RefreshTests.GameServer/Tests/ApiV3/AdminUserEditApiTests.cs b/RefreshTests.GameServer/Tests/ApiV3/AdminUserEditApiTests.cs index 7c1f63e37..82033faf2 100644 --- a/RefreshTests.GameServer/Tests/ApiV3/AdminUserEditApiTests.cs +++ b/RefreshTests.GameServer/Tests/ApiV3/AdminUserEditApiTests.cs @@ -354,6 +354,97 @@ public void CanRenameUserBackToTheirOwnPreviousName() Assert.That(originalHistory.Items.Count, Is.EqualTo(1)); Assert.That(originalHistory.Items.First().UserId.ToString(), Is.EqualTo(owner.UserId.ToString())); } + + [Test] + public void PreviousUsernameAdminEndpointsRequireAuth() + { + using TestContext context = this.GetServer(); + GameUser target = context.CreateUser("theName"); + + // test with at least one actual rename + context.Database.RenameUser(target, "theCoolerName"); + + // cannot access + HttpResponseMessage response = context.Http.GetAsync($"/api/v3/admin/previousUsernames/byUser/uuid/{target.UserId}").Result; + Assert.That(response.StatusCode, Is.EqualTo(Forbidden)); + + response = context.Http.GetAsync($"/api/v3/admin/previousUsernames/byUser/name/{target.Username}").Result; + Assert.That(response.StatusCode, Is.EqualTo(Forbidden)); + + response = context.Http.GetAsync($"/api/v3/admin/previousUsernames/byName/theName").Result; + Assert.That(response.StatusCode, Is.EqualTo(Forbidden)); + } + + [Test] + [TestCase(GameUserRole.Restricted, false)] + [TestCase(GameUserRole.User, false)] + [TestCase(GameUserRole.Trusted, false)] + [TestCase(GameUserRole.Curator, false)] + [TestCase(GameUserRole.Moderator, true)] + [TestCase(GameUserRole.Admin, true)] + public void PreviousUsernameAdminEndpointsAreGuardedByRole(GameUserRole accessorRole, bool mayAccess) + { + using TestContext context = this.GetServer(); + GameUser accessor = context.CreateUser("accessor", accessorRole); + HttpClient client = context.GetAuthenticatedClient(TokenType.Api, accessor); + + // Prepare + GameUser target1 = context.CreateUser("coolName1"); + GameUser target2 = context.CreateUser("coolName2"); + context.Database.RenameUser(target1, "rename1"); + context.Database.RenameUser(target2, "rename2"); + + Action?, ApiListResponse?> assertionCB + = delegate(ApiListResponse? response1, ApiListResponse? response2) + { + if (mayAccess) + { + Assert.That(response1?.Data, Is.Not.Null); + Assert.That(response1?.ListInfo, Is.Not.Null); + + Assert.That(response1!.Data!.Count, Is.EqualTo(1)); + Assert.That(response1!.ListInfo!.TotalItems, Is.EqualTo(1)); + + Assert.That(response1.Data!.First().Username, Is.EqualTo("coolName1")); + Assert.That(response1.Data!.First().User.Username, Is.EqualTo("rename1")); + Assert.That(response1.Data!.First().User.UserId.ToString(), Is.EqualTo(target1.UserId.ToString())); + + Assert.That(response2?.Data, Is.Not.Null); + Assert.That(response2?.ListInfo, Is.Not.Null); + + Assert.That(response2!.Data!.Count, Is.EqualTo(1)); + Assert.That(response2!.ListInfo!.TotalItems, Is.EqualTo(1)); + + Assert.That(response2.Data!.First().Username, Is.EqualTo("coolName2")); + Assert.That(response2.Data!.First().User.Username, Is.EqualTo("rename2")); + Assert.That(response2.Data!.First().User.UserId.ToString(), Is.EqualTo(target2.UserId.ToString())); + } + else + { + Assert.That(response1, Is.Null); + Assert.That(response2, Is.Null); + } + }; + + // Now test + ApiListResponse? response1 = client.GetList( + $"/api/v3/admin/previousUsernames/byUser/uuid/{target1.UserId}", mayAccess, !mayAccess); + ApiListResponse? response2 = client.GetList( + $"/api/v3/admin/previousUsernames/byUser/uuid/{target2.UserId}", mayAccess, !mayAccess); + assertionCB(response1, response2); + + ApiListResponse? response3 = client.GetList( + $"/api/v3/admin/previousUsernames/byUser/name/{target1.Username}", mayAccess, !mayAccess); + ApiListResponse? response4 = client.GetList( + $"/api/v3/admin/previousUsernames/byUser/name/{target2.Username}", mayAccess, !mayAccess); + assertionCB(response3, response4); + + ApiListResponse? response5 = client.GetList( + $"/api/v3/admin/previousUsernames/byName/coolName1", mayAccess, !mayAccess); + ApiListResponse? response6 = client.GetList( + $"/api/v3/admin/previousUsernames/byName/coolName2", mayAccess, !mayAccess); + assertionCB(response5, response6); + } [Test] public void CanRenameUserBackAndForth()