Skip to content
10 changes: 9 additions & 1 deletion Refresh.Database/GameDatabaseContext.Users.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,15 @@ public DatabaseList<GameUser> GetMostFavouritedUsers(int skip, int count)
public DatabaseList<PreviousUsername> 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<PreviousUsername> 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)
Expand Down
25 changes: 25 additions & 0 deletions Refresh.Interfaces.APIv3/Endpoints/Admin/AdminUserApiEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,31 @@ public ApiListResponse<ApiExtendedGameUserResponse> 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<ApiExtendedPreviousUsernameResponse> GetExtendedPreviousUsernamesByUsername(RequestContext context,
GameDatabaseContext database, IDataStore dataStore, DataContext dataContext, string username)
{
(int skip, int count) = context.GetPageData();
DatabaseList<PreviousUsername> previousNames = database.GetPreviousUsernameRecordsByName(username, skip, count);
return DatabaseListExtensions.FromOldList<ApiExtendedPreviousUsernameResponse, PreviousUsername>(previousNames, dataContext);
}

[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<ApiExtendedPreviousUsernameResponse> GetExtendedPreviousUsernamesByUser(RequestContext context,
GameDatabaseContext database, IDataStore dataStore, DataContext dataContext, string idType, string id)
{
GameUser? user = database.GetUserByIdAndType(idType, id);
if (user == null) return ApiNotFoundError.UserMissingError;

(int skip, int count) = context.GetPageData();
DatabaseList<PreviousUsername> previousNames = database.GetPreviousUsernameRecordsByUser(user, skip, count);
return DatabaseListExtensions.FromOldList<ApiExtendedPreviousUsernameResponse, PreviousUsername>(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)]
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ApiExtendedPreviousUsernameResponse, PreviousUsername>
{
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<ApiExtendedPreviousUsernameResponse> FromOldList(IEnumerable<PreviousUsername> oldList, DataContext dataContext)
=> oldList.Select(old => FromOld(old, dataContext)).ToList()!;
}
Original file line number Diff line number Diff line change
@@ -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<ApiPreviousUsernameResponse, PreviousUsername>
{
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<ApiPreviousUsernameResponse> FromOldList(IEnumerable<PreviousUsername> oldList, DataContext dataContext)
=> oldList.Select(old => FromOld(old, dataContext)).ToList()!;
}

4 changes: 3 additions & 1 deletion Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,6 +17,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;
Expand All @@ -41,7 +44,6 @@ public ApiResponse<ApiGameUserResponse> GetUser(RequestContext context, GameData
return ApiGameUserResponse.FromOld(user, dataContext);
}

// 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)]
Expand Down
91 changes: 91 additions & 0 deletions RefreshTests.GameServer/Tests/ApiV3/AdminUserEditApiTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ApiExtendedPreviousUsernameResponse>?, ApiListResponse<ApiExtendedPreviousUsernameResponse>?> assertionCB
= delegate(ApiListResponse<ApiExtendedPreviousUsernameResponse>? response1, ApiListResponse<ApiExtendedPreviousUsernameResponse>? 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<ApiExtendedPreviousUsernameResponse>? response1 = client.GetList<ApiExtendedPreviousUsernameResponse>(
$"/api/v3/admin/previousUsernames/byUser/uuid/{target1.UserId}", mayAccess, !mayAccess);
ApiListResponse<ApiExtendedPreviousUsernameResponse>? response2 = client.GetList<ApiExtendedPreviousUsernameResponse>(
$"/api/v3/admin/previousUsernames/byUser/uuid/{target2.UserId}", mayAccess, !mayAccess);
assertionCB(response1, response2);

ApiListResponse<ApiExtendedPreviousUsernameResponse>? response3 = client.GetList<ApiExtendedPreviousUsernameResponse>(
$"/api/v3/admin/previousUsernames/byUser/name/{target1.Username}", mayAccess, !mayAccess);
ApiListResponse<ApiExtendedPreviousUsernameResponse>? response4 = client.GetList<ApiExtendedPreviousUsernameResponse>(
$"/api/v3/admin/previousUsernames/byUser/name/{target2.Username}", mayAccess, !mayAccess);
assertionCB(response3, response4);

ApiListResponse<ApiExtendedPreviousUsernameResponse>? response5 = client.GetList<ApiExtendedPreviousUsernameResponse>(
$"/api/v3/admin/previousUsernames/byName/coolName1", mayAccess, !mayAccess);
ApiListResponse<ApiExtendedPreviousUsernameResponse>? response6 = client.GetList<ApiExtendedPreviousUsernameResponse>(
$"/api/v3/admin/previousUsernames/byName/coolName2", mayAccess, !mayAccess);
assertionCB(response5, response6);
}

[Test]
public void CanRenameUserBackAndForth()
Expand Down
Loading