Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions Refresh.Database/GameDatabaseContext.Users.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,20 @@ public partial class GameDatabaseContext // Users
return this.GameUsersIncluded.FirstOrDefault(u => u.UserId == objectId);
}

public GameUser? GetUserByIdAndType(string idType, string id)
{
switch (idType.ToLower())
{
case "username":
case "name":
return this.GetUserByUsername(id, false);
case "uuid":
return this.GetUserByUuid(id);
default:
return null;
}
}

public DatabaseList<GameUser> GetUsers(int count, int skip)
=> new(this.GameUsersIncluded.OrderByDescending(u => u.JoinDate), skip, count);

Expand Down Expand Up @@ -232,6 +246,27 @@ public void UpdateUserData(GameUser user, IApiEditUserRequest data)
});
}

public void UpdateUserData(GameUser user, IApiAdminEditUserRequest data)
{
if (data.IconHash != null)
user.IconHash = data.IconHash;

if (data.VitaIconHash != null)
user.VitaIconHash = data.VitaIconHash;

if (data.BetaIconHash != null)
user.BetaIconHash = data.BetaIconHash;

if (data.Description != null)
user.Description = data.Description;

if (data.Role != null)
user.Role = data.Role.Value;

this.GameUsers.Update(user);
this.SaveChanges();
}

public void UpdatePlanetModdedStatus(GameUser user)
{
user.AreLbp2PlanetsModded = this.GetPlanetModdedStatus(user.Lbp2PlanetsHash);
Expand Down
12 changes: 12 additions & 0 deletions Refresh.Database/Query/IApiAdminEditUserRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Refresh.Database.Models.Users;

namespace Refresh.Database.Query;

public interface IApiAdminEditUserRequest
{
string? IconHash { get; set; }
string? VitaIconHash { get; set; }
string? BetaIconHash { get; set; }
string? Description { get; set; }
GameUserRole? Role { get; set; }
}
81 changes: 81 additions & 0 deletions Refresh.Interfaces.APIv3/Endpoints/Admin/AdminUserApiEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
using Bunkum.Core.Endpoints;
using Bunkum.Core.Storage;
using Bunkum.Protocols.Http;
using Refresh.Common.Constants;
using Refresh.Common.Verification;
using Refresh.Core.Authentication.Permission;
using Refresh.Core.Types.Data;
using Refresh.Database;
using Refresh.Database.Models.Moderation;
using Refresh.Database.Models.Users;
using Refresh.Interfaces.APIv3.Documentation.Attributes;
using Refresh.Interfaces.APIv3.Endpoints.ApiTypes;
Expand Down Expand Up @@ -159,6 +161,85 @@ public ApiOkResponse ResetUserPlanetsByUsername(RequestContext context, GameData
database.ResetUserPlanets(user);
return new ApiOkResponse();
}

[ApiV3Endpoint("admin/users/{idType}/{id}", HttpMethods.Patch), MinimumRole(GameUserRole.Moderator)]
[DocSummary("Updates the specified user's profile with the given data")]
[DocError(typeof(ApiNotFoundError), ApiNotFoundError.UserMissingErrorWhen)]
[DocError(typeof(ApiValidationError), ApiValidationError.MayNotOverwriteRoleErrorWhen)]
[DocError(typeof(ApiValidationError), ApiValidationError.RoleMissingErrorWhen)]
[DocError(typeof(ApiValidationError), ApiValidationError.WrongRoleUpdateMethodErrorWhen)]
[DocError(typeof(ApiNotFoundError), ApiNotFoundError.IconMissingErrorWhen)]
[DocError(typeof(ApiValidationError), ApiValidationError.InvalidUsernameErrorWhen)]
public ApiResponse<ApiExtendedGameUserResponse> UpdateUser(RequestContext context, GameDatabaseContext database,
GameUser user, ApiAdminUpdateUserRequest body, DataContext dataContext,
[DocSummary("The type of identifier used to look up the user. Can be either 'uuid' or 'username'.")] string idType,
[DocSummary("The UUID or username of the user, depending on the specified ID type.")] string id)
{
GameUser? targetUser = database.GetUserByIdAndType(idType, id);

if (targetUser == null)
return ApiNotFoundError.UserMissingError;

// Only admins may edit anyone's role.
// TODO: Maybe moderators should also be able to set roles, but only for users below them, and to roles below them?
if (body.Role != null)
{
if (user.Role < GameUserRole.Admin)
return ApiValidationError.MayNotOverwriteRoleError;

if (!Enum.IsDefined(typeof(GameUserRole), body.Role))
return ApiValidationError.RoleMissingError;

// All roles below regular user are special and must be given using different endpoints because they require extra information.
// Incase the implementation of #286 requires a guest role, that one will very likely be below User aswell, and it should also not
// be assignable with this endpoint (when should a user ever be demoted to a temporary guest?)
if (body.Role < GameUserRole.User)
return ApiValidationError.WrongRoleUpdateMethodError;
}

if (body.IconHash != null)
{
if (body.IconHash.IsBlankHash())
body.IconHash = "0";
else if (database.GetAssetFromHash(body.IconHash) == null)
return ApiNotFoundError.IconMissingError;
}

if (body.VitaIconHash != null)
{
if (body.VitaIconHash.IsBlankHash())
body.VitaIconHash = "0";
else if (database.GetAssetFromHash(body.VitaIconHash) == null)
return ApiNotFoundError.IconMissingError;
}

if (body.BetaIconHash != null)
{
if (body.BetaIconHash.IsBlankHash())
body.BetaIconHash = "0";
else if (database.GetAssetFromHash(body.BetaIconHash) == null)
return ApiNotFoundError.IconMissingError;
}

if (body.Username != null)
{
if (!database.IsUsernameValid(body.Username))
return new ApiValidationError(ApiValidationError.InvalidUsernameErrorWhen
+ " Are you sure you used a PSN/RPCN username?");

database.RenameUser(targetUser, body.Username);
}

// Trim description
if (body.Description != null && body.Description.Length > UgcLimits.DescriptionLimit)
body.Description = body.Description[..UgcLimits.DescriptionLimit];

database.UpdateUserData(targetUser, body);
// TODO: In ApiV4, moderation actions should also provide reasons
database.CreateModerationAction(targetUser, ModerationActionType.UserModification, user, "");

return ApiExtendedGameUserResponse.FromOld(targetUser, dataContext);
}

[ApiV3Endpoint("admin/users/uuid/{uuid}", HttpMethods.Delete), MinimumRole(GameUserRole.Moderator)]
[DocSummary("Deletes a user user by their UUID.")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public class ApiNotFoundError : ApiError

public const string PhotoMissingErrorWhen = "The photo could not be found";
public static readonly ApiNotFoundError PhotoMissingError = new(PhotoMissingErrorWhen);

public const string IconMissingErrorWhen = "The icon could not be found";
public static readonly ApiNotFoundError IconMissingError = new(IconMissingErrorWhen);

public const string ContestMissingErrorWhen = "The contest could not be found";
public static readonly ApiNotFoundError ContestMissingError = new(ContestMissingErrorWhen);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,24 @@ public class ApiValidationError : ApiError
public const string EmailDoesNotActuallyExistErrorWhen = "The email address given does not exist. Are you sure you typed it in correctly?";
public static readonly ApiValidationError EmailDoesNotActuallyExistError = new(EmailDoesNotActuallyExistErrorWhen);

public const string BadUserLookupIdTypeWhen = "The ID type used to specify the user is not supported";
public static readonly ApiValidationError BadUserLookupIdType = new(BadUserLookupIdTypeWhen);
public const string MayNotOverwriteRoleErrorWhen = "You may not overwrite user roles because you are not an admin";
public static readonly ApiValidationError MayNotOverwriteRoleError = new(MayNotOverwriteRoleErrorWhen);

public const string WrongRoleUpdateMethodErrorWhen = "The specified role cannot be assigned to the user using this endpoint.";
public static readonly ApiValidationError WrongRoleUpdateMethodError = new(WrongRoleUpdateMethodErrorWhen);

public const string RoleMissingErrorWhen = "The specified role does not exist.";
public static readonly ApiValidationError RoleMissingError = new(RoleMissingErrorWhen);

public const string ContestOrganizerIdParseErrorWhen = "The organizer's user ID could not be parsed by the server";
public static readonly ApiValidationError ContestOrganizerIdParseError = new(ContestOrganizerIdParseErrorWhen);

public const string ContestDataMissingErrorWhen = "The contest must at least have a title, aswell as a start and end date specified";
public static readonly ApiValidationError ContestDataMissingError = new(ContestDataMissingErrorWhen);

public const string InvalidUsernameErrorWhen = "The username must be valid. The requirements are 3 to 16 alphanumeric characters, plus hyphens and underscores.";
public static readonly ApiValidationError InvalidUsernameError = new(InvalidUsernameErrorWhen);

// TODO: Split off error messages which are actually 401 or anything else that isn't 400
public ApiValidationError(string message) : base(message) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ public ApiOkResponse DenyVerificationRequest(RequestContext context, GameDatabas

[ApiV3Endpoint("register", HttpMethods.Post), Authentication(false)]
[DocSummary("Registers a new user.")]
[DocError(typeof(ApiValidationError), ApiValidationError.InvalidUsernameErrorWhen)]
[DocRequestBody(typeof(ApiRegisterRequest))]
#if !DEBUG
[RateLimitSettings(3600, 10, 3600 / 2, "register")]
Expand Down Expand Up @@ -315,10 +316,8 @@ public ApiResponse<IApiAuthenticationResponse> Register(RequestContext context,
return new ApiAuthenticationError("You aren't allowed to play on this instance.");

if (!database.IsUsernameValid(body.Username))
return new ApiValidationError(
"The username must be valid. " +
"The requirements are 3 to 16 alphanumeric characters, plus hyphens and underscores. " +
"Are you sure you used your PSN/RPCN username?");
return new ApiValidationError(ApiValidationError.InvalidUsernameErrorWhen
+ " Are you sure you used your PSN/RPCN username?");

if (database.IsUsernameQueued(body.Username) || database.IsEmailQueued(body.EmailAddress))
return UserInQueueError();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Refresh.Database.Models.Users;
using Refresh.Database.Query;

namespace Refresh.Interfaces.APIv3.Endpoints.DataTypes.Request;

[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))]
public class ApiAdminUpdateUserRequest : IApiAdminEditUserRequest
{
public string? Username { get; set; }
public string? IconHash { get; set; }
public string? VitaIconHash { get; set; }
public string? BetaIconHash { get; set; }
public string? Description { get; set; }
public GameUserRole? Role { get; set; }
}
15 changes: 1 addition & 14 deletions Refresh.Interfaces.APIv3/Endpoints/ReviewApiEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,26 +40,13 @@ public ApiListResponse<ApiGameReviewResponse> GetReviewsForLevel(RequestContext

[ApiV3Endpoint("users/{userIdType}/{userId}/reviews"), Authentication(false)]
[DocUsesPageData, DocSummary("Gets a list of the reviews posted by a user.")]
[DocError(typeof(ApiValidationError), ApiValidationError.BadUserLookupIdTypeWhen)]
[DocError(typeof(ApiNotFoundError), ApiNotFoundError.UserMissingErrorWhen)]
public ApiListResponse<ApiGameReviewResponse> GetReviewsByUser(RequestContext context,
GameDatabaseContext database, IDataStore dataStore, DataContext dataContext,
[DocSummary("The type of identifier used to look up the user. Can be either 'uuid' or 'username'.")] string userIdType,
[DocSummary("The UUID or username of the user, depending on the specified ID type.")] string userId)
{
GameUser? user;
switch (userIdType)
{
case "uuid":
user = database.GetUserByUuid(userId);
break;
case "username":
user = database.GetUserByUsername(userId);
break;
default:
return ApiValidationError.BadUserLookupIdType;
};

GameUser? user = database.GetUserByIdAndType(userIdType, userId);
if (user == null) return ApiNotFoundError.UserMissingError;

(int skip, int count) = context.GetPageData();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ protected override void SetupServices()
this.Server.AddService<CommandService>();
this.Server.AddService<GuidCheckerService>();
this.Server.AddService<SmtpService>();
this.Server.AddService<RoleService>();

// Must always be last, see comment in RefreshGameServer
this.Server.AddService<DataContextService>();
Expand Down
3 changes: 2 additions & 1 deletion RefreshTests.GameServer/TestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,13 @@ public HttpClient GetAuthenticatedClient(TokenType type, TokenGame game, TokenPl
return client;
}

public GameUser CreateUser(string? username = null, GameUserRole role = GameUserRole.User)
public GameUser CreateUser(string? username = null, GameUserRole role = GameUserRole.User, bool verifyEmail = true)
{
username ??= this.UserIncrement.ToString();

GameUser user = this.Database.CreateUser(username, $"{username}@{username}.local");
if (role != GameUserRole.User) this.Database.SetUserRole(user, role);
if (verifyEmail) this.Database.VerifyUserEmail(user);

return user;
}
Expand Down
Loading