diff --git a/Refresh.Database/GameDatabaseContext.Registration.cs b/Refresh.Database/GameDatabaseContext.Registration.cs index 58e2a531..b2ee5086 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 44470215..d925ac54 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 00000000..e288c57b --- /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 00000000..9ea6ba4b --- /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 f500de34..d53067eb 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,