diff --git a/Refresh.Core/Configuration/GameServerConfig.cs b/Refresh.Core/Configuration/GameServerConfig.cs
index 573d6aa1b..b4074cfc1 100644
--- a/Refresh.Core/Configuration/GameServerConfig.cs
+++ b/Refresh.Core/Configuration/GameServerConfig.cs
@@ -8,7 +8,7 @@ namespace Refresh.Core.Configuration;
[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
public class GameServerConfig : Config
{
- public override int CurrentConfigVersion => 28;
+ public override int CurrentConfigVersion => 29;
public override int Version { get; set; } = 0;
protected override void Migrate(int oldVer, dynamic oldConfig)
@@ -17,14 +17,17 @@ protected override void Migrate(int oldVer, dynamic oldConfig)
// to more cleanly split the perms between certain roles, and to make their enforcement easier.
if (oldVer < 27)
{
+ this.NewUserPermissions = new();
this.NormalUserPermissions = new();
this.TrustedUserPermissions = new();
// filesize quota limit was added during version 11, but the version wasn't bumped, so catch error to be safe
+ // Migrate filesize quota
if (oldVer >= 11)
{
try
{
+ this.NewUserPermissions.UserFilesizeQuota = (int)oldConfig.UserFilesizeQuota;
this.NormalUserPermissions.UserFilesizeQuota = (int)oldConfig.UserFilesizeQuota;
this.TrustedUserPermissions.UserFilesizeQuota = (int)oldConfig.UserFilesizeQuota;
}
@@ -34,8 +37,13 @@ protected override void Migrate(int oldVer, dynamic oldConfig)
}
}
+ // Migrate asset flags/safety level
if (oldVer >= 18)
{
+ this.NewUserPermissions.BlockedAssetFlags.Dangerous = (bool)oldConfig.BlockedAssetFlags.Dangerous;
+ this.NewUserPermissions.BlockedAssetFlags.Media = (bool)oldConfig.BlockedAssetFlags.Media;
+ this.NewUserPermissions.BlockedAssetFlags.Modded = (bool)oldConfig.BlockedAssetFlags.Modded;
+
this.NormalUserPermissions.BlockedAssetFlags.Dangerous = (bool)oldConfig.BlockedAssetFlags.Dangerous;
this.NormalUserPermissions.BlockedAssetFlags.Media = (bool)oldConfig.BlockedAssetFlags.Media;
this.NormalUserPermissions.BlockedAssetFlags.Modded = (bool)oldConfig.BlockedAssetFlags.Modded;
@@ -50,12 +58,14 @@ protected override void Migrate(int oldVer, dynamic oldConfig)
if (oldVer >= 2)
{
int oldSafetyLevel = (int)oldConfig.MaximumAssetSafetyLevel;
- this.NormalUserPermissions.BlockedAssetFlags = new ConfigAssetFlags
+ ConfigAssetFlags fromSafetyLevel = new ConfigAssetFlags
{
Dangerous = oldSafetyLevel < 3,
Modded = oldSafetyLevel < 2,
Media = oldSafetyLevel < 1,
};
+ this.NormalUserPermissions.BlockedAssetFlags = fromSafetyLevel;
+ this.NewUserPermissions.BlockedAssetFlags = fromSafetyLevel;
}
// Asset safety level for trusted users was added in config version 12, so dont try to migrate if we are coming from a version older than that
@@ -80,8 +90,13 @@ protected override void Migrate(int oldVer, dynamic oldConfig)
}
// Timed level upload limits were added in version 19.
+ // Migrate level limits
if (oldVer >= 19)
{
+ this.NewUserPermissions.LevelUploadRateLimit.Enabled = (bool)oldConfig.TimedLevelUploadLimits.Enabled;
+ this.NewUserPermissions.LevelUploadRateLimit.TimeSpanHours = (int)oldConfig.TimedLevelUploadLimits.TimeSpanHours;
+ this.NewUserPermissions.LevelUploadRateLimit.UploadQuota = (int)oldConfig.TimedLevelUploadLimits.LevelQuota;
+
this.NormalUserPermissions.LevelUploadRateLimit.Enabled = (bool)oldConfig.TimedLevelUploadLimits.Enabled;
this.NormalUserPermissions.LevelUploadRateLimit.TimeSpanHours = (int)oldConfig.TimedLevelUploadLimits.TimeSpanHours;
this.NormalUserPermissions.LevelUploadRateLimit.UploadQuota = (int)oldConfig.TimedLevelUploadLimits.LevelQuota;
@@ -94,37 +109,60 @@ protected override void Migrate(int oldVer, dynamic oldConfig)
// Read-only mode was added for both normal and trusted users in version 20.
if (oldVer >= 20)
{
+ this.NewUserPermissions.ReadOnlyMode = (bool)oldConfig.ReadOnlyMode;
this.NormalUserPermissions.ReadOnlyMode = (bool)oldConfig.ReadOnlyMode;
this.TrustedUserPermissions.ReadOnlyMode = (bool)oldConfig.ReadonlyModeForTrustedUsers;
}
}
- // In version 28, PhotoUploadRateLimit and PlaylistUploadRateLimit were added to RolePermissions, and various renamings related to level upload rate-limiting
- // were done to prepare for this: the class TimedLevelUploadLimitProperties was renamed to EntityUploadRateLimitProperties, its attribute LevelQuota was renamed to UploadQuota,
- // and RolePermissions' attribute TimedLevelUploadLimits was renamed to LevelUploadRateLimit
+ // In version 28, PhotoUploadRateLimit and PlaylistUploadRateLimit were added to RolePermissions
+ // and various attributes related to level rate-limiting were renamed
else if (oldVer == 27)
{
this.NormalUserPermissions.LevelUploadRateLimit.Enabled = (bool)oldConfig.NormalUserPermissions.TimedLevelUploadLimits.Enabled;
this.NormalUserPermissions.LevelUploadRateLimit.TimeSpanHours = (int)oldConfig.NormalUserPermissions.TimedLevelUploadLimits.TimeSpanHours;
this.NormalUserPermissions.LevelUploadRateLimit.UploadQuota = (int)oldConfig.NormalUserPermissions.TimedLevelUploadLimits.LevelQuota;
+
+ this.NormalUserPermissions.LevelUploadRateLimit.Enabled = (bool)oldConfig.NormalUserPermissions.TimedLevelUploadLimits.Enabled;
+ this.NormalUserPermissions.LevelUploadRateLimit.TimeSpanHours = (int)oldConfig.NormalUserPermissions.TimedLevelUploadLimits.TimeSpanHours;
+ this.NormalUserPermissions.LevelUploadRateLimit.UploadQuota = (int)oldConfig.NormalUserPermissions.TimedLevelUploadLimits.LevelQuota;
this.TrustedUserPermissions.LevelUploadRateLimit.Enabled = (bool)oldConfig.TrustedUserPermissions.TimedLevelUploadLimits.Enabled;
this.TrustedUserPermissions.LevelUploadRateLimit.TimeSpanHours = (int)oldConfig.TrustedUserPermissions.TimedLevelUploadLimits.TimeSpanHours;
this.TrustedUserPermissions.LevelUploadRateLimit.UploadQuota = (int)oldConfig.TrustedUserPermissions.TimedLevelUploadLimits.LevelQuota;
}
+
+ // In version 29, the NewUser role and its related config options
+ // (new user role perms and HoursUntilNewAccountNoLongerNew) were added
+ else if (oldVer < 29)
+ {
+ this.NewUserPermissions = oldConfig.NormalUserPermissions;
+ }
}
public string LicenseText { get; set; } = "Welcome to Refresh!";
///
- /// Role-specific permissions for normal users and below
+ /// Role-specific permissions for new users.
+ ///
+ public RolePermissions NewUserPermissions = new();
+ ///
+ /// Role-specific permissions for normal, not-new users.
///
public RolePermissions NormalUserPermissions = new();
///
- /// Role-specific permissions for trusted users and above
+ /// Role-specific permissions for trusted users and above.
///
public RolePermissions TrustedUserPermissions = new();
+ ///
+ /// How long we should wait (in hours) until we should use NewUserJob to set a new user's role from NewUser to User,
+ /// effectively marking them as no longer new.
+ /// Once their account hits this age, we will start applying NormalUserPermissions instead of NewUserPermissions
+ /// as their role-perms.
+ ///
+ public int HoursUntilNewAccountNoLongerNew { get; set; } = 24 * 7; // TODO should we think of a better name?
+
public bool AllowUsersToUseIpAuthentication { get; set; } = false;
public bool PermitPsnLogin { get; set; } = true;
public bool PermitRpcnLogin { get; set; } = true;
@@ -181,5 +219,6 @@ protected override void Migrate(int oldVer, dynamic oldConfig)
public string[] HmacDigestKeys = ["CustomServerDigest"];
public bool PermitShowingOnlineUsers { get; set; } = true;
+
public bool EnableDiveIn { get; set; } = true;
}
\ No newline at end of file
diff --git a/Refresh.Core/Configuration/RolePermissions.cs b/Refresh.Core/Configuration/RolePermissions.cs
index f381c60fc..f9e322bd5 100644
--- a/Refresh.Core/Configuration/RolePermissions.cs
+++ b/Refresh.Core/Configuration/RolePermissions.cs
@@ -33,4 +33,25 @@ public RolePermissions() {}
/// The amount of data the user is allowed to upload before all resource uploads get blocked, defaults to 100mb.
///
public int UserFilesizeQuota { get; set; } = 100 * 1_048_576;
+
+ // Not configurable because "Restricted" and "Banned" already imply a user has no uploading perms.
+ public static RolePermissions FromRestrictedUser => new()
+ {
+ ReadOnlyMode = true,
+ BlockedAssetFlags = new(AssetFlags.Dangerous | AssetFlags.Modded | AssetFlags.Media),
+ // Not enabled because restricted may not upload UGC anyway, also we already enable read-only mode for them
+ LevelUploadRateLimit = new()
+ {
+ Enabled = false,
+ },
+ PhotoUploadRateLimit = new()
+ {
+ Enabled = false,
+ },
+ PlaylistUploadRateLimit = new()
+ {
+ Enabled = false,
+ },
+ UserFilesizeQuota = 0,
+ };
}
\ No newline at end of file
diff --git a/Refresh.Core/Extensions/GameUserExtensions.cs b/Refresh.Core/Extensions/GameUserExtensions.cs
index e3f9c3683..257317d1b 100644
--- a/Refresh.Core/Extensions/GameUserExtensions.cs
+++ b/Refresh.Core/Extensions/GameUserExtensions.cs
@@ -7,8 +7,14 @@ public static class GameUserExtensions
{
public static bool IsWriteBlocked(this GameUser user, GameServerConfig config)
{
+ // Admins may always bypass this
if (user.Role == GameUserRole.Admin) return false;
- return GetRolePermissionsForUser(user, config).ReadOnlyMode;
+
+ // Restricted and Banned may not upload/edit any UGC, they also have no role perms because unnecessary
+ else if (user.Role <= GameUserRole.Restricted) return true;
+
+ // Determine based on role perms
+ else return GetRolePermissionsForUser(user, config).ReadOnlyMode;
}
public static bool MayModifyUser(this GameUser user, GameUser targetUser)
@@ -26,9 +32,12 @@ public static bool MayModifyUser(this GameUser user, GameUser targetUser)
public static RolePermissions GetRolePermissionsForUser(this GameUser user, GameServerConfig config)
{
- if (user.Role >= GameUserRole.Trusted)
- return config.TrustedUserPermissions;
-
- return config.NormalUserPermissions;
+ return user.Role switch
+ {
+ >= GameUserRole.Trusted => config.TrustedUserPermissions,
+ GameUserRole.User => config.NormalUserPermissions,
+ GameUserRole.NewUser => config.NewUserPermissions,
+ _ => RolePermissions.FromRestrictedUser,
+ };
}
}
\ No newline at end of file
diff --git a/Refresh.Core/Services/RoleService.cs b/Refresh.Core/Services/RoleService.cs
index 279ea2309..8385aaf43 100644
--- a/Refresh.Core/Services/RoleService.cs
+++ b/Refresh.Core/Services/RoleService.cs
@@ -31,12 +31,13 @@ internal RoleService(GameAuthenticationService authService, GameServerConfig con
if (!(authAttrib?.Required ?? true)) return null;
MinimumRoleAttribute? roleAttrib = method.GetCustomAttribute();
- GameUserRole minimumRole = roleAttrib?.MinimumRole ?? GameUserRole.User;
+ GameUserRole minimumRole = roleAttrib?.MinimumRole ?? GameUserRole.NewUser;
GameUser? user = (GameUser?)this._authService.AuthenticateToken(context, database)?.User;
if (user == null) return null; // Let AuthenticationProvider handle 401
// if the user's role is lower than the minimum role for this endpoint, then return unauthorized
+ // by default, Restricted and Banned will be rejected by this since the default role is NewUser
if (user.Role < minimumRole)
{
return Unauthorized;
diff --git a/Refresh.Database/Models/Users/GameUser.cs b/Refresh.Database/Models/Users/GameUser.cs
index 585a1cea4..b95091a32 100644
--- a/Refresh.Database/Models/Users/GameUser.cs
+++ b/Refresh.Database/Models/Users/GameUser.cs
@@ -105,8 +105,8 @@ public partial class GameUser : IRateLimitUser
/// If `true`, unescape XML tags sent to /filter
///
public bool UnescapeXmlSequences { get; set; }
-
- public GameUserRole Role { get; set; }
+
+ public GameUserRole Role { get; set; } = GameUserRole.NewUser;
///
/// Whether planets containing mods or VoiceRecordings should be shown in-game
diff --git a/Refresh.Database/Models/Users/GameUserRole.cs b/Refresh.Database/Models/Users/GameUserRole.cs
index 62076330c..57b70cc63 100644
--- a/Refresh.Database/Models/Users/GameUserRole.cs
+++ b/Refresh.Database/Models/Users/GameUserRole.cs
@@ -30,6 +30,12 @@ public enum GameUserRole : sbyte
///
User = 0,
///
+ /// A newly registered user. Can have different, usually more restrictive configurable perms than regular users.
+ /// Useful to make spam more difficult, for example. The duration in which an account's age makes it "new" is defined by config.
+ /// Automatically promoted to User by NewUserJob.
+ ///
+ NewUser = -32,
+ ///
/// A user with read-only permissions. May log in and play, but cannot do things such as publish levels or post comments.
///
Restricted = -126,
diff --git a/Refresh.GameServer/RefreshGameServer.cs b/Refresh.GameServer/RefreshGameServer.cs
index 92bb94dcc..13865f9f0 100644
--- a/Refresh.GameServer/RefreshGameServer.cs
+++ b/Refresh.GameServer/RefreshGameServer.cs
@@ -186,12 +186,14 @@ protected override void SetupServices()
protected virtual void SetupWorkers()
{
- this.WorkerManager = RefreshWorkerManager.Create(this.Logger, this._dataStore, this._databaseProvider);
+ this.WorkerManager = RefreshWorkerManager.Create(this.Logger, this._dataStore, this._databaseProvider, this.GetTimeProvider());
if (this._configStore.Integration.DiscordWebhookEnabled && this._configStore.GameServer.PermitShowingOnlineUsers)
{
this.WorkerManager.AddJob(new DiscordIntegrationJob(this._configStore.Integration, this._configStore.GameServer));
}
+
+ this.WorkerManager.AddJob(new NewUserJob(this._configStore.GameServer.HoursUntilNewAccountNoLongerNew));
}
///
diff --git a/Refresh.Interfaces.Game/Endpoints/Levels/PublishEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/Levels/PublishEndpoints.cs
index 9fbfc2765..1497549a6 100644
--- a/Refresh.Interfaces.Game/Endpoints/Levels/PublishEndpoints.cs
+++ b/Refresh.Interfaces.Game/Endpoints/Levels/PublishEndpoints.cs
@@ -110,7 +110,7 @@ public Response StartPublish(RequestContext context,
{
if (dataContext.User!.IsWriteBlocked(config))
{
- dataContext.Database.AddPublishFailNotification("The server is in read-only mode.", body.Title, dataContext.User!);
+ dataContext.Database.AddPublishFailNotification($"The server is in read-only mode.", body.Title, dataContext.User!);
return Unauthorized;
}
diff --git a/Refresh.Interfaces.Workers/RefreshWorkerManager.cs b/Refresh.Interfaces.Workers/RefreshWorkerManager.cs
index 88dfc5d06..25b185d9c 100644
--- a/Refresh.Interfaces.Workers/RefreshWorkerManager.cs
+++ b/Refresh.Interfaces.Workers/RefreshWorkerManager.cs
@@ -1,5 +1,6 @@
using Bunkum.Core.Storage;
using NotEnoughLogs;
+using Refresh.Common.Time;
using Refresh.Database;
using Refresh.Interfaces.Workers.Migrations;
using Refresh.Interfaces.Workers.Repeating;
@@ -9,9 +10,9 @@ namespace Refresh.Interfaces.Workers;
public static class RefreshWorkerManager
{
- public static WorkerManager Create(Logger logger, IDataStore dataStore, GameDatabaseProvider databaseProvider)
+ public static WorkerManager Create(Logger logger, IDataStore dataStore, GameDatabaseProvider databaseProvider, IDateTimeProvider timeProvider)
{
- WorkerManager manager = new(logger, dataStore, databaseProvider);
+ WorkerManager manager = new(logger, dataStore, databaseProvider, timeProvider);
manager.AddJob();
manager.AddJob();
diff --git a/Refresh.Interfaces.Workers/Repeating/NewUserJob.cs b/Refresh.Interfaces.Workers/Repeating/NewUserJob.cs
new file mode 100644
index 000000000..4d5f50c78
--- /dev/null
+++ b/Refresh.Interfaces.Workers/Repeating/NewUserJob.cs
@@ -0,0 +1,40 @@
+using Refresh.Common;
+using Refresh.Database;
+using Refresh.Database.Models.Users;
+using Refresh.Workers;
+
+namespace Refresh.Interfaces.Workers.Repeating;
+
+///
+/// A worker that handles setting new users as regular users, depending on their account age and the server config.
+///
+// TODO also set users back as "new" if duration in config is updated to result in user being "new" again
+public class NewUserJob : RepeatingJob
+{
+ private int _requiredAccountAge;
+ protected override int Interval => 60_000 * 5; // 5 minutes, no need to execute too often
+
+ public NewUserJob(int requiredAccountAge)
+ {
+ this._requiredAccountAge = requiredAccountAge;
+ }
+
+ public override void ExecuteJob(WorkContext context)
+ {
+ DateTimeOffset now = context.TimeProvider.Now;
+ DatabaseList newUsers = context.Database.GetAllUsersWithRole(GameUserRole.NewUser);
+
+ foreach (GameUser user in newUsers.Items.ToList())
+ {
+ // If an account is, e.g., 2 hours and 40 minutes old, and max age for new users is 3 hours, we wouldn't
+ // consider max to be reached yet, so floor the difference.
+ long accountAge = (long)Math.Floor(now.Subtract(user.JoinDate).TotalHours);
+
+ context.Logger.LogDebug(RefreshContext.Worker, $"{nameof(NewUserJob)} - new user: {user}, join date: {user.JoinDate}, current time: {now}, account age: {accountAge}h, configured required age: {this._requiredAccountAge}h.");
+ if (accountAge < this._requiredAccountAge) continue; // Don't promote user if they haven't reached max age yet
+
+ context.Logger.LogInfo(RefreshContext.Worker, $"Promoting {user} to regular user since their account is {accountAge} hours old now (required configured age: {this._requiredAccountAge}h).");
+ context.Database.SetUserRole(user, GameUserRole.User);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Refresh.WorkerManager/Program.cs b/Refresh.WorkerManager/Program.cs
index c458fa38c..2a8932ac8 100644
--- a/Refresh.WorkerManager/Program.cs
+++ b/Refresh.WorkerManager/Program.cs
@@ -2,6 +2,7 @@
using Bunkum.Core.Storage;
using NotEnoughLogs;
using NotEnoughLogs.Behaviour;
+using Refresh.Common.Time;
using Refresh.Core.Configuration;
using Refresh.Database;
using Refresh.Database.Configuration;
@@ -33,7 +34,7 @@
database.Warmup();
logger.LogInfo(BunkumCategory.Startup, "Starting worker manager!");
-WorkerManager manager = RefreshWorkerManager.Create(logger, new FileSystemDataStore(), database);
+WorkerManager manager = RefreshWorkerManager.Create(logger, new FileSystemDataStore(), database, new SystemDateTimeProvider());
manager.Start();
manager.WaitForExit();
\ No newline at end of file
diff --git a/Refresh.Workers/WorkContext.cs b/Refresh.Workers/WorkContext.cs
index 6e4bc2289..47b91c400 100644
--- a/Refresh.Workers/WorkContext.cs
+++ b/Refresh.Workers/WorkContext.cs
@@ -1,5 +1,6 @@
using Bunkum.Core.Storage;
using NotEnoughLogs;
+using Refresh.Common.Time;
using Refresh.Database;
namespace Refresh.Workers;
@@ -9,4 +10,6 @@ public class WorkContext : IDataContext
public required GameDatabaseContext Database { get; init; }
public required Logger Logger { get; init; }
public required IDataStore DataStore { get; init; }
+ // TODO also use this in jobs outside of NewUserJob which also rely on current time
+ public required IDateTimeProvider TimeProvider { get; init; }
}
\ No newline at end of file
diff --git a/Refresh.Workers/WorkerManager.cs b/Refresh.Workers/WorkerManager.cs
index 3c04102ea..f750c8be8 100644
--- a/Refresh.Workers/WorkerManager.cs
+++ b/Refresh.Workers/WorkerManager.cs
@@ -1,6 +1,7 @@
using Bunkum.Core.Storage;
using NotEnoughLogs;
using Refresh.Common;
+using Refresh.Common.Time;
using Refresh.Database;
using Refresh.Database.Models.Workers;
using Refresh.Workers.State;
@@ -12,6 +13,7 @@ public class WorkerManager
private readonly Logger _logger;
private readonly IDataStore _dataStore;
private readonly GameDatabaseProvider _databaseProvider;
+ private readonly IDateTimeProvider _timeProvider;
private readonly int _workerId;
@@ -22,11 +24,12 @@ public class WorkerManager
private readonly List _jobs = [];
- public WorkerManager(Logger logger, IDataStore dataStore, GameDatabaseProvider databaseProvider)
+ public WorkerManager(Logger logger, IDataStore dataStore, GameDatabaseProvider databaseProvider, IDateTimeProvider timeProvider)
{
this._dataStore = dataStore;
this._databaseProvider = databaseProvider;
this._logger = logger;
+ this._timeProvider = timeProvider;
using GameDatabaseContext context = this._databaseProvider.GetContext();
this._workerId = context.CreateWorker();
@@ -49,6 +52,7 @@ public void RunWorkCycle()
Database = this._databaseProvider.GetContext(),
Logger = this._logger,
DataStore = this._dataStore,
+ TimeProvider = this._timeProvider,
};
foreach (WorkerJob job in this._jobs)
diff --git a/RefreshTests.GameServer/TestContext.cs b/RefreshTests.GameServer/TestContext.cs
index a8a0bd96c..ce9b0e294 100644
--- a/RefreshTests.GameServer/TestContext.cs
+++ b/RefreshTests.GameServer/TestContext.cs
@@ -121,12 +121,13 @@ public Token GenerateToken(GameUser? user, TokenType type, TokenGame game, Token
return this.Database.GenerateTokenForUser(user ?? this.CreateUser(), type, game, platform, ipAddress ?? "0.0.0.0", tokenExpirySeconds);
}
+ // Not changing default role to NewUser because I don't want to potentially break over 500 usages.
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 (user.Role != role) this.Database.SetUserRole(user, role);
if (verifyEmail) this.Database.VerifyUserEmail(user);
this.Database.Entry(user).State = EntityState.Unchanged;
@@ -267,6 +268,7 @@ public WorkContext GetWorkContext()
Database = this.Database,
Logger = this.Server.Value.Logger,
DataStore = this.GetDataStore(),
+ TimeProvider = this.Time,
};
}
diff --git a/RefreshTests.GameServer/Tests/Levels/PublishEndpointsTests.cs b/RefreshTests.GameServer/Tests/Levels/PublishEndpointsTests.cs
index 51a9d3cd0..11c19ad55 100644
--- a/RefreshTests.GameServer/Tests/Levels/PublishEndpointsTests.cs
+++ b/RefreshTests.GameServer/Tests/Levels/PublishEndpointsTests.cs
@@ -707,4 +707,54 @@ public void ReuploadStatusPreserved()
Assert.That(dbLevel.OriginalPublisher, Is.EqualTo("glotchmeister69"));
}
}
+
+ [Test]
+ public void CannotPublishLevelIfRestricted()
+ {
+ using TestContext context = this.GetServer();
+ GameUser user = context.CreateUser(role: GameUserRole.User, verifyEmail: true);
+ context.Database.RestrictUser(user, "no", DateTimeOffset.MaxValue);
+
+ using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user);
+
+ GameLevelRequest level = new()
+ {
+ RootResource = TEST_ASSET_HASH,
+ };
+
+ HttpResponseMessage message = client.PostAsync("/lbp/startPublish", new StringContent(level.AsXML())).Result;
+ Assert.That(message.StatusCode, Is.EqualTo(Unauthorized));
+
+ //Upload our """level""" (even though we got an error)
+ message = client.PostAsync($"/lbp/upload/{TEST_ASSET_HASH}", new ReadOnlyMemoryContent("LVLb"u8.ToArray())).Result;
+ Assert.That(message.StatusCode, Is.EqualTo(Unauthorized));
+
+ message = client.PostAsync("/lbp/publish", new StringContent(level.AsXML())).Result;
+ Assert.That(message.StatusCode, Is.EqualTo(Unauthorized));
+ }
+
+ [Test]
+ public void CannotPublishLevelIfBanned()
+ {
+ using TestContext context = this.GetServer();
+ GameUser user = context.CreateUser(role: GameUserRole.User, verifyEmail: true);
+ context.Database.BanUser(user, "no", DateTimeOffset.MaxValue);
+
+ using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user);
+
+ GameLevelRequest level = new()
+ {
+ RootResource = TEST_ASSET_HASH,
+ };
+
+ HttpResponseMessage message = client.PostAsync("/lbp/startPublish", new StringContent(level.AsXML())).Result;
+ Assert.That(message.StatusCode, Is.EqualTo(Unauthorized));
+
+ //Upload our """level""" (even though we got an error)
+ message = client.PostAsync($"/lbp/upload/{TEST_ASSET_HASH}", new ReadOnlyMemoryContent("LVLb"u8.ToArray())).Result;
+ Assert.That(message.StatusCode, Is.EqualTo(Unauthorized));
+
+ message = client.PostAsync("/lbp/publish", new StringContent(level.AsXML())).Result;
+ Assert.That(message.StatusCode, Is.EqualTo(Unauthorized));
+ }
}
\ No newline at end of file
diff --git a/RefreshTests.GameServer/Tests/Users/RegistrationQueueTests.cs b/RefreshTests.GameServer/Tests/Users/RegistrationQueueTests.cs
index f556cd67b..19333bb85 100644
--- a/RefreshTests.GameServer/Tests/Users/RegistrationQueueTests.cs
+++ b/RefreshTests.GameServer/Tests/Users/RegistrationQueueTests.cs
@@ -30,4 +30,19 @@ public void CreateAccountFromQueueDespiteWrongUsernameCasing()
Assert.That(context.Database.GetUserByUsername(wrongUsername, false)?.UserId, Is.EqualTo(context.Database.GetUserByUsername(correctUsername, false)?.UserId));
Assert.That(context.Database.GetUserByUsername(wrongUsername, true)?.Username, Is.Null);
}
+
+ [Test]
+ public void UsersCreatedFromQueueAreNewUsers()
+ {
+ using TestContext context = this.GetServer();
+ context.Database.AddRegistrationToQueue("new", "new@new.new", "some amazing credentials!");
+
+ QueuedRegistration? registration = context.Database.GetQueuedRegistrationByUsername("new");
+ Assert.That(registration, Is.Not.Null);
+
+ // Now use it to create an account
+ GameUser user = context.Database.CreateUserFromQueuedRegistration(registration, TokenPlatform.PS3);
+ Assert.That(user.Username, Is.EqualTo("new"));
+ Assert.That(user.Role, Is.EqualTo(GameUserRole.NewUser));
+ }
}
\ No newline at end of file
diff --git a/RefreshTests.GameServer/Tests/Users/UserRoleTests.cs b/RefreshTests.GameServer/Tests/Users/UserRoleTests.cs
new file mode 100644
index 000000000..8f7c5e6f0
--- /dev/null
+++ b/RefreshTests.GameServer/Tests/Users/UserRoleTests.cs
@@ -0,0 +1,101 @@
+using Refresh.Core.Configuration;
+using Refresh.Core.Extensions;
+using Refresh.Database.Models.Users;
+
+namespace RefreshTests.GameServer.Tests.Users;
+
+public class UserRoleTests : GameServerTest
+{
+ [Test]
+ public void EnsureUsersUseCorrectRolePerms()
+ {
+ using TestContext context = this.GetServer();
+ GameServerConfig config = context.Server.Value.GameServerConfig;
+
+ config.NewUserPermissions.UserFilesizeQuota = 24;
+ config.NewUserPermissions.ReadOnlyMode = false;
+
+ config.NormalUserPermissions.UserFilesizeQuota = 67;
+ config.NormalUserPermissions.ReadOnlyMode = true;
+
+ config.TrustedUserPermissions.UserFilesizeQuota = 23456;
+ config.TrustedUserPermissions.ReadOnlyMode = false;
+
+ GameUser user = context.CreateUser(role: GameUserRole.NewUser);
+ Assert.That(user.Role, Is.EqualTo(GameUserRole.NewUser));
+
+ RolePermissions perms = user.GetRolePermissionsForUser(config);
+ Assert.That(perms.UserFilesizeQuota, Is.EqualTo(24));
+ Assert.That(perms.ReadOnlyMode, Is.False);
+ Assert.That(user.IsWriteBlocked(config), Is.False);
+
+ // Normal user
+ context.Database.SetUserRole(user, GameUserRole.User);
+ context.Database.Refresh();
+ Assert.That(user.Role, Is.EqualTo(GameUserRole.User));
+
+ perms = user.GetRolePermissionsForUser(config);
+ Assert.That(perms.UserFilesizeQuota, Is.EqualTo(67));
+ Assert.That(perms.ReadOnlyMode, Is.True);
+ Assert.That(user.IsWriteBlocked(config), Is.True);
+
+ // Trusted user
+ context.Database.SetUserRole(user, GameUserRole.Trusted);
+ context.Database.Refresh();
+ Assert.That(user.Role, Is.EqualTo(GameUserRole.Trusted));
+
+ perms = user.GetRolePermissionsForUser(config);
+ Assert.That(perms.UserFilesizeQuota, Is.EqualTo(23456));
+ Assert.That(perms.ReadOnlyMode, Is.False);
+ Assert.That(user.IsWriteBlocked(config), Is.False);
+
+ // Curator user
+ context.Database.SetUserRole(user, GameUserRole.Curator);
+ context.Database.Refresh();
+ Assert.That(user.Role, Is.EqualTo(GameUserRole.Curator));
+
+ perms = user.GetRolePermissionsForUser(config);
+ Assert.That(perms.UserFilesizeQuota, Is.EqualTo(23456));
+ Assert.That(perms.ReadOnlyMode, Is.False);
+ Assert.That(user.IsWriteBlocked(config), Is.False);
+
+ // Restricted user
+ context.Database.RestrictUser(user, "lol", DateTimeOffset.MaxValue);
+ context.Database.Refresh();
+ Assert.That(user.Role, Is.EqualTo(GameUserRole.Restricted));
+
+ perms = user.GetRolePermissionsForUser(config);
+ Assert.That(perms.UserFilesizeQuota, Is.EqualTo(0));
+ Assert.That(perms.ReadOnlyMode, Is.True);
+ Assert.That(user.IsWriteBlocked(config), Is.True);
+
+ // Banned user
+ context.Database.BanUser(user, "lel", DateTimeOffset.MaxValue);
+ context.Database.Refresh();
+ Assert.That(user.Role, Is.EqualTo(GameUserRole.Banned));
+
+ perms = user.GetRolePermissionsForUser(config);
+ Assert.That(perms.UserFilesizeQuota, Is.EqualTo(0));
+ Assert.That(perms.ReadOnlyMode, Is.True);
+ Assert.That(user.IsWriteBlocked(config), Is.True);
+ }
+
+ [Test]
+ public void EnsureNewlyCreatedUsersAreNewUsers()
+ {
+ // Ensure using the database method causes users to have the NewUser role
+ using TestContext context = this.GetServer();
+ GameUser user = context.Database.CreateUser("new", "new@new.com");
+ Assert.That(user.Role, Is.EqualTo(GameUserRole.NewUser));
+ }
+
+ [Test]
+ public void EnsureManuallySettingRoleToBannedThrows()
+ {
+ using TestContext context = this.GetServer();
+ GameUser user = context.CreateUser();
+
+ //Assert.That(() => context.Database.SetUserRole(user, GameUserRole.Restricted), Throws.TypeOf()); // TODO consistent behaviour
+ Assert.That(() => context.Database.SetUserRole(user, GameUserRole.Banned), Throws.TypeOf());
+ }
+}
\ No newline at end of file
diff --git a/RefreshTests.GameServer/Tests/Workers/JobStateTests.cs b/RefreshTests.GameServer/Tests/Workers/JobStateTests.cs
index bab10536c..8891b481a 100644
--- a/RefreshTests.GameServer/Tests/Workers/JobStateTests.cs
+++ b/RefreshTests.GameServer/Tests/Workers/JobStateTests.cs
@@ -14,7 +14,7 @@ public void RemovesJobStateIfJobDoesntExist()
{
using TestContext context = this.GetServer();
IDataStore dataStore = context.GetDataStore();
- WorkerManager manager = new(Logger, dataStore, context.DatabaseProvider);
+ WorkerManager manager = new(Logger, dataStore, context.DatabaseProvider, context.Time);
context.Database.UpdateOrCreateJobState(typeof(TestMigrationJob).Name, new MigrationJobState(), WorkerClass.Refresh);
Assert.That(context.Database.GetJobState(typeof(TestMigrationJob).Name, typeof(MigrationJobState), WorkerClass.Refresh), Is.Not.Null);
@@ -34,7 +34,7 @@ public void DoesNotRemoveJobStateIfJobExists(bool uploadLevel)
{
using TestContext context = this.GetServer();
IDataStore dataStore = context.GetDataStore();
- WorkerManager manager = new(Logger, dataStore, context.DatabaseProvider);
+ WorkerManager manager = new(Logger, dataStore, context.DatabaseProvider, context.Time);
TestMigrationJob job = new();
manager.AddJob(job);
@@ -68,7 +68,7 @@ public void DoesNotRemoveJobStateIfNotRefreshClass()
{
using TestContext context = this.GetServer();
IDataStore dataStore = context.GetDataStore();
- WorkerManager manager = new(Logger, dataStore, context.DatabaseProvider);
+ WorkerManager manager = new(Logger, dataStore, context.DatabaseProvider, context.Time);
context.Database.UpdateOrCreateJobState(typeof(TestMigrationJob).Name, new MigrationJobState(), WorkerClass.Craftworld);
Assert.That(context.Database.GetJobState(typeof(TestMigrationJob).Name, typeof(MigrationJobState), WorkerClass.Craftworld), Is.Not.Null);
@@ -89,7 +89,7 @@ public void ReExecutesMigrationJobAfterRollbackAndReupdate()
{
using TestContext context = this.GetServer();
IDataStore dataStore = context.GetDataStore();
- WorkerManager manager = new(Logger, dataStore, context.DatabaseProvider);
+ WorkerManager manager = new(Logger, dataStore, context.DatabaseProvider, context.Time);
TestMigrationJob job = new();
manager.AddJob(job);
@@ -135,7 +135,7 @@ public void ReExecutesMigrationJobAfterRollbackAndReupdate()
// Simulate a roll-back, meaning the job wouldn't be in the WorkerManager anymore, so no migrations will happen, and the job state would be
// auto-removed by WorkerManager.Start() in real cases.
context.Database.Refresh();
- manager = new(Logger, dataStore, context.DatabaseProvider);
+ manager = new(Logger, dataStore, context.DatabaseProvider, context.Time);
GameLevel thirdLevel = context.CreateLevel(user);
manager.RemoveUnusedJobStates();
@@ -155,7 +155,7 @@ public void ReExecutesMigrationJobAfterRollbackAndReupdate()
Assert.That(thirdLevelMigrated!.Title, Does.Not.EndWith(" test"));
// Now simulate a re-update, where the job is in the WorkerManager again
- manager = new(Logger, dataStore, context.DatabaseProvider);
+ manager = new(Logger, dataStore, context.DatabaseProvider, context.Time);
job = new();
manager.AddJob(job);
manager.RemoveUnusedJobStates();
diff --git a/RefreshTests.GameServer/Tests/Workers/NewUserJobTests.cs b/RefreshTests.GameServer/Tests/Workers/NewUserJobTests.cs
new file mode 100644
index 000000000..047ecb7c8
--- /dev/null
+++ b/RefreshTests.GameServer/Tests/Workers/NewUserJobTests.cs
@@ -0,0 +1,41 @@
+using Refresh.Database.Models.Users;
+using Refresh.Interfaces.Workers.Repeating;
+using Refresh.Workers;
+
+namespace RefreshTests.GameServer.Tests.Workers;
+
+public class NewUserJobTests : GameServerTest
+{
+ [Test]
+ [TestCase(120, GameUserRole.User)] // waiting exactly 2 hours is just enough for promotion
+ [TestCase(130, GameUserRole.User)] // waiting 2 hours and 10 minutes is more than enough
+ [TestCase(119, GameUserRole.NewUser)] // waiting 1 hour and 59 minutes is not enough, so stay as NewUser
+ [TestCase(60, GameUserRole.NewUser)] // waiting just 1 hour is totally not enough
+ public void NewUsersGetPromotedIfOldEnough(long fastForwardMinutes, GameUserRole resultingRole)
+ {
+ using TestContext context = this.GetServer();
+ GameUser user = context.CreateUser(role: GameUserRole.NewUser);
+ Assert.That(user.Role, Is.EqualTo(GameUserRole.NewUser));
+
+ // Prepare
+ WorkContext workContext = context.GetWorkContext();
+ NewUserJob job = new(2); // Set required age to 2 hours
+
+ // Ensure job doesn't promote the user immediately
+ job.ExecuteJob(workContext);
+ context.Database.Refresh();
+ GameUser? updatedUser = context.Database.GetUserByObjectId(user.UserId);
+ Assert.That(updatedUser, Is.Not.Null);
+ Assert.That(updatedUser!.Role, Is.EqualTo(GameUserRole.NewUser));
+
+ // skip forward 2 hours and try again
+ context.Time.TimestampMilliseconds += 1000 * 60 * fastForwardMinutes;
+ job.ExecuteJob(workContext);
+ context.Database.Refresh();
+
+ // Ensure job has promoted the user this time (if enough time has passed)
+ updatedUser = context.Database.GetUserByObjectId(user.UserId);
+ Assert.That(updatedUser, Is.Not.Null);
+ Assert.That(updatedUser!.Role, Is.EqualTo(resultingRole));
+ }
+}
\ No newline at end of file