Implement YML configuration

- Sorts config better
- Stored in a robust format
- Migrates from old JSON format automatically on startup
- Retains version migration feature
- Renames ServerSettings to ServerConfiguration
This commit is contained in:
jvyden 2022-05-14 15:50:57 -04:00
parent 9d80f1e178
commit 9d74a4104b
No known key found for this signature in database
GPG key ID: 18BCF2BE0262B278
53 changed files with 630 additions and 302 deletions

1
.gitignore vendored
View file

@ -23,6 +23,7 @@ png/
/ProjectLighthouse/r/*
/ProjectLighthouse/logs/*
lighthouse.config.json
lighthouse.yml
gitBranch.txt
gitVersion.txt
gitRemotes.txt

View file

@ -1,10 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SwUserDefinedSpecifications">
<option name="specTypeByUrl">
<map />
</option>
</component>
<component name="com.jetbrains.rider.android.RiderAndroidMiscFileCreationComponent">
<option name="ENSURE_MISC_FILE_EXISTS" value="true" />
</component>

View file

@ -10,8 +10,8 @@ public sealed class DatabaseFactAttribute : FactAttribute
public DatabaseFactAttribute()
{
ServerSettings.Instance = new ServerSettings();
ServerSettings.Instance.DbConnectionString = "server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse";
ServerConfiguration.Instance = new ServerConfiguration();
ServerConfiguration.Instance.DbConnectionString = "server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse";
if (!ServerStatics.DbConnected) this.Skip = "Database not available";
else
lock(migrateLock)

View file

@ -76,6 +76,7 @@
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForOtherTypes/@EntryValue">UseExplicitType</s:String>
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForSimpleTypes/@EntryValue">UseExplicitType</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=BE/@EntryIndexedValue">BE</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=DB/@EntryIndexedValue">DB</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=DDS/@EntryIndexedValue">DDS</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=DLC/@EntryIndexedValue">DLC</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IP/@EntryIndexedValue">IP</s:String>
@ -89,6 +90,7 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=PSP/@EntryIndexedValue">PSP</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RPCS/@EntryIndexedValue">RPCS</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SMTP/@EntryIndexedValue">SMTP</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SSL/@EntryIndexedValue">SSL</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=TLS/@EntryIndexedValue">TLS</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=Method/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"&gt;&lt;ExtraRule Prefix="" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateConstants/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>

View file

@ -33,7 +33,7 @@ public class ClientConfigurationController : ControllerBase
"ProbabilityOfPacketDelay 0.0\nMinPacketDelayFrames 0\nMaxPacketDelayFrames 3\nProbabilityOfPacketDrop 0.0\nEnableFakeConditionsForLoopback true\nNumberOfFramesPredictionAllowedForNonLocalPlayer 1000\nEnablePrediction true\nMinPredictedFrames 0\nMaxPredictedFrames 10\nAllowGameRendCameraSplit true\nFramesBeforeAgressiveCatchup 30\nPredictionPadSides 200\nPredictionPadTop 200\nPredictionPadBottom 200\nShowErrorNumbers true\nAllowModeratedLevels false\nAllowModeratedPoppetItems false\nTIMEOUT_WAIT_FOR_JOIN_RESPONSE_FROM_PREV_PARTY_HOST 50.0\nTIMEOUT_WAIT_FOR_CHANGE_LEVEL_PARTY_HOST 30.0\nTIMEOUT_WAIT_FOR_CHANGE_LEVEL_PARTY_MEMBER 45.0\nTIMEOUT_WAIT_FOR_REQUEST_JOIN_FRIEND 15.0\nTIMEOUT_WAIT_FOR_CONNECTION_FROM_HOST 30.0\nTIMEOUT_WAIT_FOR_ROOM_ID_TO_JOIN 60.0\nTIMEOUT_WAIT_FOR_GET_NUM_PLAYERS_ONLINE 60.0\nTIMEOUT_WAIT_FOR_SIGNALLING_CONNECTIONS 120.0\nTIMEOUT_WAIT_FOR_PARTY_DATA 60.0\nTIME_TO_WAIT_FOR_LEAVE_MESSAGE_TO_COME_BACK 20.0\nTIME_TO_WAIT_FOR_FOLLOWING_REQUESTS_TO_ARRIVE 30.0\nTIMEOUT_WAIT_FOR_FINISHED_MIGRATING_HOST 30.0\nTIMEOUT_WAIT_FOR_PARTY_LEADER_FINISH_JOINING 45.0\nTIMEOUT_WAIT_FOR_QUICKPLAY_LEVEL 60.0\nTIMEOUT_WAIT_FOR_PLAYERS_TO_JOIN 30.0\nTIMEOUT_WAIT_FOR_DIVE_IN_PLAYERS 240.0\nTIMEOUT_WAIT_FOR_FIND_BEST_ROOM 60.0\nTIMEOUT_DIVE_IN_TOTAL 300.0\nTIMEOUT_WAIT_FOR_SOCKET_CONNECTION 120.0\nTIMEOUT_WAIT_FOR_REQUEST_RESOURCE_MESSAGE 120.0\nTIMEOUT_WAIT_FOR_LOCAL_CLIENT_TO_GET_RESOURCE_LIST 120.0\nTIMEOUT_WAIT_FOR_CLIENT_TO_LOAD_RESOURCES 120.0\nTIMEOUT_WAIT_FOR_LOCAL_CLIENT_TO_SAVE_GAME_STATE 30.0\nTIMEOUT_WAIT_FOR_ADD_PLAYERS_TO_TAKE 30.0\nTIMEOUT_WAIT_FOR_UPDATE_FROM_CLIENT 90.0\nTIMEOUT_WAIT_FOR_HOST_TO_GET_RESOURCE_LIST 60.0\nTIMEOUT_WAIT_FOR_HOST_TO_SAVE_GAME_STATE 60.0\nTIMEOUT_WAIT_FOR_HOST_TO_ADD_US 30.0\nTIMEOUT_WAIT_FOR_UPDATE 60.0\nTIMEOUT_WAIT_FOR_REQUEST_JOIN 50.0\nTIMEOUT_WAIT_FOR_AUTOJOIN_PRESENCE 60.0\nTIMEOUT_WAIT_FOR_AUTOJOIN_CONNECTION 120.0\nSECONDS_BETWEEN_PINS_AWARDED_UPLOADS 300.0\nEnableKeepAlive true\nAllowVoIPRecordingPlayback true\nOverheatingThresholdDisallowMidgameJoin 0.95\nMaxCatchupFrames 3\nMaxLagBeforeShowLoading 23\nMinLagBeforeHideLoading 30\nLagImprovementInflectionPoint -1.0\nFlickerThreshold 2.0\nClosedDemo2014Version 1\nClosedDemo2014Expired false\nEnablePlayedFilter true\nEnableCommunityDecorations true\nGameStateUpdateRate 10.0\nGameStateUpdateRateWithConsumers 1.0\nDisableDLCPublishCheck false\nEnableDiveIn true\nEnableHackChecks false\nAllowOnlineCreate true" +
$"TelemetryServer {hostname}\n" +
$"CDNHostName {hostname}\n" +
$"ShowLevelBoos {ServerSettings.Instance.BooingEnabled.ToString().ToLower()}\n"
$"ShowLevelBoos {ServerConfiguration.Instance.UserGeneratedContentLimits.BooingEnabled.ToString().ToLower()}\n"
);
}

View file

@ -81,9 +81,9 @@ public class LoginController : ControllerBase
return this.StatusCode(403, "");
}
if (ServerSettings.Instance.UseExternalAuth)
if (ServerConfiguration.Instance.Authentication.UseExternalAuth)
{
if (ServerSettings.Instance.BlockDeniedUsers)
if (ServerConfiguration.Instance.Authentication.BlockDeniedUsers)
{
string ipAddressAndName = $"{token.UserLocation}|{user.Username}";
if (DeniedAuthenticationHelper.RecentlyDenied(ipAddressAndName) || DeniedAuthenticationHelper.GetAttempts(ipAddressAndName) > 3)

View file

@ -41,7 +41,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.";
User? user = await this.database.UserFromGameRequest(this.Request);
if (user == null) return this.StatusCode(403, "");
return this.Ok($"{license}\n{ServerSettings.Instance.EulaText}");
return this.Ok($"{license}\n{ServerConfiguration.Instance.EulaText}");
}
[HttpGet("announce")]
@ -60,7 +60,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.";
GameToken gameToken = userAndToken.Value.Item2;
#endif
string announceText = ServerSettings.Instance.AnnounceText;
string announceText = ServerConfiguration.Instance.AnnounceText;
announceText = announceText.Replace("%user", user.Username);
announceText = announceText.Replace("%id", user.UserId.ToString());

View file

@ -34,7 +34,7 @@ public class PhotosController : ControllerBase
User? user = await this.database.UserFromGameRequest(this.Request);
if (user == null) return this.StatusCode(403, "");
if (user.PhotosByMe >= ServerSettings.Instance.PhotosQuota) return this.BadRequest();
if (user.PhotosByMe >= ServerConfiguration.Instance.UserGeneratedContentLimits.PhotosQuota) return this.BadRequest();
this.Request.Body.Position = 0;
string bodyString = await new StreamReader(this.Request.Body).ReadToEndAsync();
@ -99,7 +99,7 @@ public class PhotosController : ControllerBase
{
Title = "New photo uploaded!",
Description = $"{user.Username} uploaded a new photo.",
ImageUrl = $"{ServerSettings.Instance.ExternalUrl}/gameAssets/{photo.LargeHash}",
ImageUrl = $"{ServerConfiguration.Instance.ExternalUrl}/gameAssets/{photo.LargeHash}",
Color = WebhookHelper.UnionColor,
}
);

View file

@ -56,7 +56,7 @@ public class PublishController : ControllerBase
if (oldSlot == null) return this.NotFound();
if (oldSlot.CreatorId != user.UserId) return this.BadRequest();
}
else if (user.GetUsedSlotsForGame(gameToken.GameVersion) > ServerSettings.Instance.EntitledSlots)
else if (user.GetUsedSlotsForGame(gameToken.GameVersion) > ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots)
{
return this.StatusCode(403, "");
}
@ -167,7 +167,7 @@ public class PublishController : ControllerBase
return this.Ok(oldSlot.Serialize(gameToken.GameVersion));
}
if (user.GetUsedSlotsForGame(gameToken.GameVersion) > ServerSettings.Instance.EntitledSlots)
if (user.GetUsedSlotsForGame(gameToken.GameVersion) > ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots)
{
return this.StatusCode(403, "");
}
@ -198,7 +198,7 @@ public class PublishController : ControllerBase
await WebhookHelper.SendWebhook
(
"New level published!",
$"**{user.Username}** just published a new level: [**{slot.Name}**]({ServerSettings.Instance.ExternalUrl}/slot/{slot.SlotId})\n{slot.Description}"
$"**{user.Username}** just published a new level: [**{slot.Name}**]({ServerConfiguration.Instance.ExternalUrl}/slot/{slot.SlotId})\n{slot.Description}"
);
return this.Ok(slot.Serialize(gameToken.GameVersion));
@ -232,7 +232,7 @@ public class PublishController : ControllerBase
XmlSerializer serializer = new(typeof(Slot));
Slot? slot = (Slot?)serializer.Deserialize(new StringReader(bodyString));
SanitizationHelper.SanitizeStringsInClass(slot);
return slot;

View file

@ -42,7 +42,7 @@ public class SlotsController : ControllerBase
this.database.Slots.ByGameVersion(gameVersion, token.UserId == user.UserId, true)
.Where(s => s.Creator!.Username == user.Username)
.Skip(pageStart - 1)
.Take(Math.Min(pageSize, ServerSettings.Instance.EntitledSlots)),
.Take(Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots)),
string.Empty,
(current, slot) => current + slot.Serialize(token.GameVersion)
);
@ -56,7 +56,7 @@ public class SlotsController : ControllerBase
new Dictionary<string, object>
{
{
"hint_start", pageStart + Math.Min(pageSize, ServerSettings.Instance.EntitledSlots)
"hint_start", pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots)
},
{
"total", user.UsedSlots
@ -135,7 +135,7 @@ public class SlotsController : ControllerBase
new Dictionary<string, object>
{
{
"hint_start", pageStart + Math.Min(pageSize, ServerSettings.Instance.EntitledSlots)
"hint_start", pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots)
},
{
"total", await StatisticsHelper.SlotCount()
@ -169,7 +169,7 @@ public class SlotsController : ControllerBase
new Dictionary<string, object>
{
{
"hint_start", pageStart + Math.Min(pageSize, ServerSettings.Instance.EntitledSlots)
"hint_start", pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots)
},
{
"total", await StatisticsHelper.TeamPickCount()
@ -200,7 +200,7 @@ public class SlotsController : ControllerBase
new Dictionary<string, object>
{
{
"hint_start", pageStart + Math.Min(pageSize, ServerSettings.Instance.EntitledSlots)
"hint_start", pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots)
},
{
"total", await StatisticsHelper.SlotCount()
@ -244,7 +244,7 @@ public class SlotsController : ControllerBase
new Dictionary<string, object>
{
{
"hint_start", pageStart + Math.Min(pageSize, ServerSettings.Instance.EntitledSlots)
"hint_start", pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots)
},
{
"total", await StatisticsHelper.SlotCount()
@ -302,7 +302,7 @@ public class SlotsController : ControllerBase
new Dictionary<string, object>
{
{
"hint_start", pageStart + Math.Min(pageSize, ServerSettings.Instance.EntitledSlots)
"hint_start", pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots)
},
{
"total", await StatisticsHelper.SlotCount()
@ -346,7 +346,7 @@ public class SlotsController : ControllerBase
new Dictionary<string, object>
{
{
"hint_start", pageStart + Math.Min(pageSize, ServerSettings.Instance.EntitledSlots)
"hint_start", pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots)
},
{
"total", await StatisticsHelper.SlotCount()
@ -403,4 +403,4 @@ public class SlotsController : ControllerBase
return whereSlots.Include(s => s.Creator).Include(s => s.Location);
}
}
}

View file

@ -46,7 +46,7 @@ public class Database : DbContext
public DbSet<EmailSetToken> EmailSetTokens { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseMySql(ServerSettings.Instance.DbConnectionString, MySqlServerVersion.LatestSupportedServerVersion);
=> options.UseMySql(ServerConfiguration.Instance.DbConnectionString, MySqlServerVersion.LatestSupportedServerVersion);
#nullable enable
public async Task<User> CreateUser(string username, string password, string? emailAddress = null)
@ -82,10 +82,10 @@ public class Database : DbContext
await this.SaveChangesAsync();
if (emailAddress != null && ServerSettings.Instance.SMTPEnabled)
if (emailAddress != null && ServerConfiguration.Instance.Mail.MailEnabled)
{
string body = "An account for Project Lighthouse has been registered with this email address.\n\n" +
$"You can login at {ServerSettings.Instance.ExternalUrl}.";
$"You can login at {ServerConfiguration.Instance.ExternalUrl}.";
SMTPHelper.SendEmail(emailAddress, "Project Lighthouse Account Created: " + username, body);
}

View file

@ -18,11 +18,11 @@ public static class CaptchaHelper
[SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")]
public static async Task<bool> Verify(string token)
{
if (!ServerSettings.Instance.HCaptchaEnabled) return true;
if (!ServerConfiguration.Instance.Captcha.CaptchaEnabled) return true;
List<KeyValuePair<string, string>> payload = new()
{
new("secret", ServerSettings.Instance.HCaptchaSecret),
new("secret", ServerConfiguration.Instance.Captcha.Secret),
new("response", token),
};

View file

@ -21,7 +21,7 @@ public static class CensorHelper
public static string ScanMessage(string message)
{
if (ServerSettings.Instance.UserInputFilterMode == FilterMode.None) return message;
if (ServerConfiguration.Instance.UserInputFilterMode == FilterMode.None) return message;
int profaneIndex = -1;
@ -44,7 +44,7 @@ public static class CensorHelper
sb.Append(message.AsSpan(0, profanityIndex));
switch (ServerSettings.Instance.UserInputFilterMode)
switch (ServerConfiguration.Instance.UserInputFilterMode)
{
case FilterMode.Random:
for(int i = 0; i < profanityLength; i++)

View file

@ -26,7 +26,7 @@ public static class RequestExtensions
public static async Task<bool> CheckCaptchaValidity(this HttpRequest request)
{
if (ServerSettings.Instance.HCaptchaEnabled)
if (ServerConfiguration.Instance.Captcha.CaptchaEnabled)
{
bool gotCaptcha = request.Form.TryGetValue("h-captcha-response", out StringValues values);
if (!gotCaptcha) return false;

View file

@ -20,7 +20,7 @@ public static class FileHelper
public static bool IsFileSafe(LbpFile file)
{
if (!ServerSettings.Instance.CheckForUnsafeFiles) return true;
if (!ServerConfiguration.Instance.CheckForUnsafeFiles) return true;
if (file.FileType == LbpFileType.Unknown) return false;

View file

@ -14,7 +14,8 @@ namespace LBPUnion.ProjectLighthouse.Helpers;
public static class InfluxHelper
{
public static readonly InfluxDBClient Client = InfluxDBClientFactory.Create(ServerSettings.Instance.InfluxUrl, ServerSettings.Instance.InfluxToken);
public static readonly InfluxDBClient Client = InfluxDBClientFactory.Create
(url: ServerConfiguration.Instance.InfluxDB.Url, token: ServerConfiguration.Instance.InfluxDB.Token);
private static readonly List<GameVersion> gameVersions = new()
{
@ -40,10 +41,10 @@ public static class InfluxHelper
.Tag("game", gameVersion.ToString())
.Field("playerCountGame", await StatisticsHelper.RecentMatchesForGame(gameVersion));
writeApi.WritePoint(gamePoint, ServerSettings.Instance.InfluxBucket, ServerSettings.Instance.InfluxOrg);
writeApi.WritePoint(gamePoint, ServerConfiguration.Instance.InfluxDB.Bucket, ServerConfiguration.Instance.InfluxDB.Organization);
}
writeApi.WritePoint(point, ServerSettings.Instance.InfluxBucket, ServerSettings.Instance.InfluxOrg);
writeApi.WritePoint(point, ServerConfiguration.Instance.InfluxDB.Bucket, ServerConfiguration.Instance.InfluxDB.Organization);
writeApi.Flush();
}

View file

@ -12,20 +12,20 @@ public static class SMTPHelper
static SMTPHelper()
{
if (!ServerSettings.Instance.SMTPEnabled) return;
if (!ServerConfiguration.Instance.Mail.MailEnabled) return;
client = new SmtpClient(ServerSettings.Instance.SMTPHost, ServerSettings.Instance.SMTPPort)
client = new SmtpClient(ServerConfiguration.Instance.Mail.Host, ServerConfiguration.Instance.Mail.Port)
{
EnableSsl = ServerSettings.Instance.SMTPSsl,
Credentials = new NetworkCredential(ServerSettings.Instance.SMTPFromAddress, ServerSettings.Instance.SMTPPassword),
EnableSsl = ServerConfiguration.Instance.Mail.UseSSL,
Credentials = new NetworkCredential(ServerConfiguration.Instance.Mail.FromAddress, ServerConfiguration.Instance.Mail.Password),
};
fromAddress = new MailAddress(ServerSettings.Instance.SMTPFromAddress, ServerSettings.Instance.SMTPFromName);
fromAddress = new MailAddress(ServerConfiguration.Instance.Mail.FromAddress, ServerConfiguration.Instance.Mail.FromName);
}
public static bool SendEmail(string recipientAddress, string subject, string body)
{
if (!ServerSettings.Instance.SMTPEnabled) return false;
if (!ServerConfiguration.Instance.Mail.MailEnabled) return false;
MailMessage message = new(fromAddress, new MailAddress(recipientAddress))
{

View file

@ -7,14 +7,17 @@ namespace LBPUnion.ProjectLighthouse.Helpers;
public static class WebhookHelper
{
private static readonly DiscordWebhookClient client = (ServerSettings.Instance.DiscordWebhookEnabled ? new DiscordWebhookClient(ServerSettings.Instance.DiscordWebhookUrl) : null);
private static readonly DiscordWebhookClient client = (ServerConfiguration.Instance.DiscordIntegration.DiscordIntegrationEnabled
? new DiscordWebhookClient(ServerConfiguration.Instance.DiscordIntegration.Url)
: null);
public static readonly Color UnionColor = new(0, 140, 255);
public static Task SendWebhook(EmbedBuilder builder) => SendWebhook(builder.Build());
public static async Task SendWebhook(Embed embed)
{
if (!ServerSettings.Instance.DiscordWebhookEnabled) return;
if (!ServerConfiguration.Instance.DiscordIntegration.DiscordIntegrationEnabled) return;
await client.SendMessageAsync
(

View file

@ -18,6 +18,6 @@ public class InfluxLogger : ILogger
PointData point = PointData.Measurement("lighthouseLog").Field("level", level).Field("content", content);
writeApi.WritePoint(point, ServerSettings.Instance.InfluxBucket, ServerSettings.Instance.InfluxOrg);
writeApi.WritePoint(point, ServerConfiguration.Instance.InfluxDB.Bucket, ServerConfiguration.Instance.InfluxDB.Organization);
}
}

View file

@ -18,7 +18,7 @@ public class CompleteEmailVerificationPage : BaseLayout
public async Task<IActionResult> OnGet(string token)
{
if (!ServerSettings.Instance.SMTPEnabled) return this.NotFound();
if (!ServerConfiguration.Instance.Mail.MailEnabled) return this.NotFound();
User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("~/login");

View file

@ -21,7 +21,7 @@ public class AuthenticationPage : BaseLayout
public IActionResult OnGet()
{
if (!ServerSettings.Instance.UseExternalAuth) return this.NotFound();
if (!ServerConfiguration.Instance.Authentication.UseExternalAuth) return this.NotFound();
if (this.User == null) return this.StatusCode(403, "");
this.IpAddress = this.HttpContext.Connection.RemoteIpAddress;

View file

@ -12,7 +12,7 @@
@if (Model.User != null)
{
<p>You are currently logged in as <b>@Model.User.Username</b>.</p>
if (ServerSettings.Instance.UseExternalAuth && Model.AuthenticationAttemptsCount > 0)
if (ServerConfiguration.Instance.Authentication.UseExternalAuth && Model.AuthenticationAttemptsCount > 0)
{
<p>
<b>You have @Model.AuthenticationAttemptsCount authentication attempts pending. Click <a href="/authentication">here</a> to view them.</b>

View file

@ -11,7 +11,7 @@
}
else
{
if (ServerSettings.Instance.UseExternalAuth)
if (ServerConfiguration.Instance.Authentication.UseExternalAuth)
{
Model.NavigationItems.Add(new PageNavigationItem("Authentication", "/authentication", "key"));
}
@ -62,16 +62,16 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@* Google Analytics *@
@if (ServerSettings.Instance.GoogleAnalyticsEnabled)
@if (ServerConfiguration.Instance.GoogleAnalytics.AnalyticsEnabled)
{
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=@ServerSettings.Instance.GoogleAnalyticsId"></script>
<script async src="https://www.googletagmanager.com/gtag/js?id=@ServerConfiguration.Instance.GoogleAnalytics.Id"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '@ServerSettings.Instance.GoogleAnalyticsId');
gtag('config', '@ServerConfiguration.Instance.GoogleAnalytics.Id');
</script>
}
</head>

View file

@ -50,13 +50,13 @@
</div>
</div>
@if (ServerSettings.Instance.HCaptchaEnabled)
@if (ServerConfiguration.Instance.Captcha.CaptchaEnabled)
{
@await Html.PartialAsync("Partials/CaptchaPartial")
}
<input type="submit" value="Log in" id="submit" class="ui blue button">
@if (ServerSettings.Instance.RegistrationEnabled)
@if (ServerConfiguration.Instance.Authentication.RegistrationEnabled)
{
<a href="/register">
<div class="ui button">

View file

@ -65,7 +65,7 @@ public class LoginForm : BaseLayout
return this.Page();
}
if (user.EmailAddress == null && ServerSettings.Instance.SMTPEnabled)
if (user.EmailAddress == null && ServerConfiguration.Instance.Mail.MailEnabled)
{
Logger.LogWarn($"User {user.Username} (id: {user.UserId}) failed to login; email not set", LogArea.Login);
@ -104,7 +104,7 @@ public class LoginForm : BaseLayout
Logger.LogSuccess($"User {user.Username} (id: {user.UserId}) successfully logged in on web", LogArea.Login);
if (user.PasswordResetRequired) return this.Redirect("~/passwordResetRequired");
if (ServerSettings.Instance.SMTPEnabled && !user.EmailAddressVerified) return this.Redirect("~/login/sendVerificationEmail");
if (ServerConfiguration.Instance.Mail.MailEnabled && !user.EmailAddressVerified) return this.Redirect("~/login/sendVerificationEmail");
return this.RedirectToPage(nameof(LandingPage));
}

View file

@ -1,6 +1,6 @@
@using LBPUnion.ProjectLighthouse.Types.Settings
@if (ServerSettings.Instance.HCaptchaEnabled)
@if (ServerConfiguration.Instance.Captcha.CaptchaEnabled)
{
<div class="h-captcha" data-sitekey="@ServerSettings.Instance.HCaptchaSiteKey"></div>
<div class="h-captcha" data-sitekey="@ServerConfiguration.Instance.Captcha.SiteKey"></div>
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
}

View file

@ -26,7 +26,7 @@
bool showLink = (bool?)ViewData["ShowLink"] ?? false;
string iconHash = Model.IconHash;
if (string.IsNullOrWhiteSpace(iconHash) || iconHash.StartsWith('g')) iconHash = ServerSettings.Instance.MissingIconHash;
if (string.IsNullOrWhiteSpace(iconHash) || iconHash.StartsWith('g')) iconHash = ServerConfiguration.Instance.WebsiteConfiguration.MissingIconHash;
}
<div class="card">
@{

View file

@ -43,7 +43,7 @@
</div>
</div>
@if (ServerSettings.Instance.SMTPEnabled)
@if (ServerConfiguration.Instance.Mail.MailEnabled)
{
<div class="field">
<label>Email address</label>
@ -72,7 +72,7 @@
</div>
</div>
@if (ServerSettings.Instance.HCaptchaEnabled)
@if (ServerConfiguration.Instance.Captcha.CaptchaEnabled)
{
@await Html.PartialAsync("Partials/CaptchaPartial")
}

View file

@ -22,7 +22,7 @@ public class RegisterForm : BaseLayout
[SuppressMessage("ReSharper", "SpecifyStringComparison")]
public async Task<IActionResult> OnPost(string username, string password, string confirmPassword, string emailAddress)
{
if (!ServerSettings.Instance.RegistrationEnabled) return this.NotFound();
if (!ServerConfiguration.Instance.Authentication.RegistrationEnabled) return this.NotFound();
if (string.IsNullOrWhiteSpace(username))
{
@ -36,7 +36,7 @@ public class RegisterForm : BaseLayout
return this.Page();
}
if (string.IsNullOrWhiteSpace(emailAddress) && ServerSettings.Instance.SMTPEnabled)
if (string.IsNullOrWhiteSpace(emailAddress) && ServerConfiguration.Instance.Mail.MailEnabled)
{
this.Error = "Email address field is required.";
return this.Page();
@ -54,7 +54,7 @@ public class RegisterForm : BaseLayout
return this.Page();
}
if (ServerSettings.Instance.SMTPEnabled &&
if (ServerConfiguration.Instance.Mail.MailEnabled &&
await this.Database.Users.FirstOrDefaultAsync(u => u.EmailAddress.ToLower() == emailAddress.ToLower()) != null)
{
this.Error = "The email address you've chosen is already taken.";
@ -80,7 +80,7 @@ public class RegisterForm : BaseLayout
this.Response.Cookies.Append("LighthouseToken", webToken.UserToken);
if (ServerSettings.Instance.SMTPEnabled) return this.Redirect("~/login/sendVerificationEmail");
if (ServerConfiguration.Instance.Mail.MailEnabled) return this.Redirect("~/login/sendVerificationEmail");
return this.RedirectToPage(nameof(LandingPage));
}
@ -90,7 +90,7 @@ public class RegisterForm : BaseLayout
public IActionResult OnGet()
{
this.Error = string.Empty;
if (!ServerSettings.Instance.RegistrationEnabled) return this.NotFound();
if (!ServerConfiguration.Instance.Authentication.RegistrationEnabled) return this.NotFound();
return this.Page();
}

View file

@ -17,7 +17,7 @@ public class SendVerificationEmailPage : BaseLayout
public async Task<IActionResult> OnGet()
{
if (!ServerSettings.Instance.SMTPEnabled) return this.NotFound();
if (!ServerConfiguration.Instance.Mail.MailEnabled) return this.NotFound();
User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("/login");
@ -47,7 +47,7 @@ public class SendVerificationEmailPage : BaseLayout
string body = "Hello,\n\n" +
$"This email is a request to verify this email for your (likely new!) Project Lighthouse account ({user.Username}).\n\n" +
$"To verify your account, click the following link: {ServerSettings.Instance.ExternalUrl}/verifyEmail?token={verifyToken.EmailToken}\n\n\n" +
$"To verify your account, click the following link: {ServerConfiguration.Instance.ExternalUrl}/verifyEmail?token={verifyToken.EmailToken}\n\n\n" +
"If this wasn't you, feel free to ignore this email.";
if (SMTPHelper.SendEmail(user.EmailAddress, "Project Lighthouse Email Verification", body))

View file

@ -12,7 +12,7 @@
<form class="ui form" onsubmit="return onSubmit(this)" method="post">
@Html.AntiForgeryToken()
@if (ServerSettings.Instance.SMTPEnabled)
@if (ServerConfiguration.Instance.Mail.MailEnabled)
{
<div class="field">
<label>Please type a valid email address and verify it:</label>

View file

@ -22,7 +22,7 @@ public class SetEmailForm : BaseLayout
public async Task<IActionResult> OnGet(string? token = null)
{
if (!ServerSettings.Instance.SMTPEnabled) return this.NotFound();
if (!ServerConfiguration.Instance.Mail.MailEnabled) return this.NotFound();
if (token == null) return this.Redirect("/login");
EmailSetToken? emailToken = await this.Database.EmailSetTokens.FirstOrDefaultAsync(t => t.EmailToken == token);
@ -35,7 +35,7 @@ public class SetEmailForm : BaseLayout
public async Task<IActionResult> OnPost(string emailAddress, string token)
{
if (!ServerSettings.Instance.SMTPEnabled) return this.NotFound();
if (!ServerConfiguration.Instance.Mail.MailEnabled) return this.NotFound();
EmailSetToken? emailToken = await this.Database.EmailSetTokens.Include(t => t.User).FirstOrDefaultAsync(t => t.EmailToken == token);
if (emailToken == null) return this.Redirect("/login");

View file

@ -82,7 +82,7 @@
}
<div class="ui divider"></div>
@for (int i = 0; i < Model.Reviews.Count; i++)
@for(int i = 0; i < Model.Reviews.Count; i++)
{
Review review = Model.Reviews[i];
string faceHash = review.Thumb switch {
@ -95,10 +95,9 @@
if (string.IsNullOrWhiteSpace(faceHash))
{
faceHash = ServerSettings.Instance.MissingIconHash;
faceHash = ServerConfiguration.Instance.WebsiteConfiguration.MissingIconHash;
}
string faceAlt = review.Thumb switch {
-1 => "Boo!",
0 => "Meh.",
@ -111,7 +110,7 @@
<div class="card">
<div>
<img class="cardIcon slotCardIcon" src="@ServerSettings.Instance.ExternalUrl/gameAssets/@faceHash" alt="@faceAlt" title="@faceAlt" style="min-width: @(size)px; width: @(size)px; height: @(size)px">
<img class="cardIcon slotCardIcon" src="@ServerConfiguration.Instance.ExternalUrl/gameAssets/@faceHash" alt="@faceAlt" title="@faceAlt" style="min-width: @(size)px; width: @(size)px; height: @(size)px">
</div>
<div class="cardStats">
<h3 style="margin-bottom: 5px;">@review.Reviewer?.Username</h3>

View file

@ -18,8 +18,8 @@ public class SlotPage : BaseLayout
public List<Comment> Comments = new();
public List<Review> Reviews = new();
public readonly bool CommentsEnabled = ServerSettings.Instance.LevelCommentsEnabled;
public readonly bool ReviewsEnabled = ServerSettings.Instance.LevelReviewsEnabled;
public readonly bool CommentsEnabled = ServerConfiguration.Instance.UserGeneratedContentLimits.LevelCommentsEnabled;
public readonly bool ReviewsEnabled = ServerConfiguration.Instance.UserGeneratedContentLimits.LevelReviewsEnabled;
public Slot? Slot;
public SlotPage(Database database) : base(database)

View file

@ -15,7 +15,7 @@ public class UserPage : BaseLayout
{
public List<Comment>? Comments;
public bool CommentsEnabled = ServerSettings.Instance.ProfileCommentsEnabled;
public bool CommentsEnabled = ServerConfiguration.Instance.UserGeneratedContentLimits.ProfileCommentsEnabled;
public bool IsProfileUserHearted;
@ -45,15 +45,16 @@ public class UserPage : BaseLayout
}
if (this.User == null) return this.Page();
foreach (Comment c in this.Comments)
{
Reaction? reaction = await this.Database.Reactions.FirstOrDefaultAsync(r => r.UserId == this.User.UserId && r.TargetId == c.CommentId);
if (reaction != null) c.YourThumb = reaction.Rating;
Reaction? reaction = await this.Database.Reactions.FirstOrDefaultAsync(r => r.UserId == this.User.UserId && r.TargetId == c.CommentId);
if (reaction != null) c.YourThumb = reaction.Rating;
}
this.IsProfileUserHearted = await this.Database.HeartedProfiles.FirstOrDefaultAsync
(u => u.UserId == this.User.UserId && u.HeartedUserId == this.ProfileUser.UserId) != null;
(u => u.UserId == this.User.UserId && u.HeartedUserId == this.ProfileUser.UserId) !=
null;
return this.Page();
}
}
}

View file

@ -31,7 +31,7 @@ public static class Program
Logger.LogInfo($"You are running version {VersionHelper.FullVersion}", LogArea.Startup);
// Referencing ServerSettings.Instance here loads the config, see ServerSettings.cs for more information
Logger.LogSuccess("Loaded config file version " + ServerSettings.Instance.ConfigVersion, LogArea.Startup);
Logger.LogSuccess("Loaded config file version " + ServerConfiguration.Instance.ConfigVersion, LogArea.Startup);
Logger.LogInfo("Determining if the database is available...", LogArea.Startup);
bool dbConnected = ServerStatics.DbConnected;
@ -50,11 +50,11 @@ public static class Program
Logger.LogInfo("Migrating database...", LogArea.Database);
MigrateDatabase(database);
if (ServerSettings.Instance.InfluxEnabled)
if (ServerConfiguration.Instance.InfluxDB.InfluxEnabled)
{
Logger.LogInfo("Influx logging is enabled. Starting influx logging...", LogArea.Startup);
InfluxHelper.StartLogging().Wait();
if (ServerSettings.Instance.InfluxLoggingEnabled) Logger.AddLogger(new InfluxLogger());
if (ServerConfiguration.Instance.InfluxDB.LoggingEnabled) Logger.AddLogger(new InfluxLogger());
}
Logger.LogDebug
@ -72,7 +72,7 @@ public static class Program
return;
}
if (ServerSettings.Instance.ConvertAssetsOnStartup) FileHelper.ConvertAllTexturesToPng();
if (ServerConfiguration.Instance.WebsiteConfiguration.ConvertAssetsOnStartup) FileHelper.ConvertAllTexturesToPng();
Logger.LogInfo("Starting room cleanup thread...", LogArea.Startup);
RoomHelper.StartCleanupThread();
@ -102,7 +102,7 @@ public static class Program
{
webBuilder.UseStartup<Startup.Startup>();
webBuilder.UseWebRoot("StaticFiles");
webBuilder.UseUrls(ServerSettings.Instance.ServerListenUrl);
webBuilder.UseUrls(ServerConfiguration.Instance.ListenUrl);
}
)
.ConfigureLogging

View file

@ -27,6 +27,7 @@
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.1"/>
<PackageReference Include="SharpZipLib" Version="1.3.3"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.1"/>
<PackageReference Include="YamlDotNet" Version="11.2.1"/>
</ItemGroup>
<ItemGroup>
@ -52,6 +53,7 @@
</EmbeddedResource>
<None Remove="recent-activity.xml" />
<None Remove="r.tar.gz" />
<None Remove="lighthouse.yml"/>
</ItemGroup>
<ItemGroup>

View file

@ -37,7 +37,7 @@ public class DebugWarmupLifetime : IHostLifetime
{
using HttpClient client = new();
string url = ServerSettings.Instance.ServerListenUrl;
string url = ServerConfiguration.Instance.ListenUrl;
url = url.Replace("0.0.0.0", "127.0.0.1");
Logger.LogDebug("Warming up Hot Reload...", LogArea.Startup);

View file

@ -96,7 +96,7 @@ public class Startup
{
bool computeDigests = true;
if (string.IsNullOrEmpty(ServerSettings.Instance.ServerDigestKey))
if (string.IsNullOrEmpty(ServerConfiguration.Instance.DigestKey.PrimaryDigestKey))
{
Logger.LogWarn
(
@ -172,7 +172,8 @@ public class Startup
if (computeDigests && digestPath.StartsWith("/LITTLEBIGPLANETPS3_XML"))
{
string clientRequestDigest = await CryptoHelper.ComputeDigest(digestPath, authCookie, body, ServerSettings.Instance.ServerDigestKey);
string clientRequestDigest = await CryptoHelper.ComputeDigest
(digestPath, authCookie, body, ServerConfiguration.Instance.DigestKey.PrimaryDigestKey);
// Check the digest we've just calculated against the X-Digest-A header if the game set the header. They should match.
if (context.Request.Headers.TryGetValue("X-Digest-A", out StringValues sentDigest))
@ -185,13 +186,14 @@ public class Startup
// Reset the body stream
body.Position = 0;
clientRequestDigest = await CryptoHelper.ComputeDigest(digestPath, authCookie, body, ServerSettings.Instance.AlternateDigestKey);
clientRequestDigest = await CryptoHelper.ComputeDigest
(digestPath, authCookie, body, ServerConfiguration.Instance.DigestKey.AlternateDigestKey);
if (clientRequestDigest != sentDigest)
{
#if DEBUG
Console.WriteLine("Digest failed");
Console.WriteLine("digestKey: " + ServerSettings.Instance.ServerDigestKey);
Console.WriteLine("altDigestKey: " + ServerSettings.Instance.AlternateDigestKey);
Console.WriteLine("digestKey: " + ServerConfiguration.Instance.DigestKey.PrimaryDigestKey);
Console.WriteLine("altDigestKey: " + ServerConfiguration.Instance.DigestKey.AlternateDigestKey);
Console.WriteLine("computed digest: " + clientRequestDigest);
#endif
// We still failed to validate. Abort the request.
@ -218,7 +220,9 @@ public class Startup
{
responseBuffer.Position = 0;
string digestKey = usedAlternateDigestKey ? ServerSettings.Instance.AlternateDigestKey : ServerSettings.Instance.ServerDigestKey;
string digestKey = usedAlternateDigestKey
? ServerConfiguration.Instance.DigestKey.AlternateDigestKey
: ServerConfiguration.Instance.DigestKey.PrimaryDigestKey;
// Compute the digest for the response.
string serverDigest = await CryptoHelper.ComputeDigest(context.Request.Path, authCookie, responseBuffer, digestKey);

View file

@ -289,8 +289,8 @@ public class Slot
LbpSerializer.StringElement("yourlbpPlayCount", yourVisitedStats?.PlaysLBP1) +
LbpSerializer.StringElement("yourlbp3PlayCount", yourVisitedStats?.PlaysLBP3) +
yourReview?.Serialize("yourReview") +
LbpSerializer.StringElement("reviewsEnabled", ServerSettings.Instance.LevelReviewsEnabled) +
LbpSerializer.StringElement("commentsEnabled", ServerSettings.Instance.LevelCommentsEnabled) +
LbpSerializer.StringElement("reviewsEnabled", ServerConfiguration.Instance.UserGeneratedContentLimits.LevelReviewsEnabled) +
LbpSerializer.StringElement("commentsEnabled", ServerConfiguration.Instance.UserGeneratedContentLimits.LevelCommentsEnabled) +
LbpSerializer.StringElement("playerCount", playerCount) +
LbpSerializer.StringElement("reviewCount", this.ReviewCount);

View file

@ -0,0 +1,8 @@
namespace LBPUnion.ProjectLighthouse.Types.Settings.ConfigurationCategories;
public class AuthenticationConfiguration
{
public bool BlockDeniedUsers { get; set; } = true;
public bool RegistrationEnabled { get; set; } = true;
public bool UseExternalAuth { get; set; }
}

View file

@ -0,0 +1,13 @@
namespace LBPUnion.ProjectLighthouse.Types.Settings.ConfigurationCategories;
public class CaptchaConfiguration
{
// TODO: support recaptcha, not just hcaptcha
// use an enum to define which captcha services can be used?
// LBPUnion.ProjectLighthouse.Types.Settings.CaptchaService
public bool CaptchaEnabled { get; set; }
public string SiteKey { get; set; } = "";
public string Secret { get; set; } = "";
}

View file

@ -0,0 +1,8 @@
namespace LBPUnion.ProjectLighthouse.Types.Settings.ConfigurationCategories;
public class DigestKeyConfiguration
{
// todo: move to list?
public string PrimaryDigestKey { get; set; } = "";
public string AlternateDigestKey { get; set; } = "";
}

View file

@ -0,0 +1,11 @@
#nullable enable
namespace LBPUnion.ProjectLighthouse.Types.Settings.ConfigurationCategories;
public class DiscordIntegrationConfiguration
{
//TODO: integrations should be modular/abstracted away
public bool DiscordIntegrationEnabled { get; set; }
public string Url { get; set; } = "";
}

View file

@ -0,0 +1,8 @@
namespace LBPUnion.ProjectLighthouse.Types.Settings.ConfigurationCategories;
public class GoogleAnalyticsConfiguration
{
public bool AnalyticsEnabled { get; set; }
public string Id { get; set; } = "";
}

View file

@ -0,0 +1,16 @@
namespace LBPUnion.ProjectLighthouse.Types.Settings.ConfigurationCategories;
public class InfluxDBConfiguration
{
public bool InfluxEnabled { get; set; }
/// <summary>
/// Whether or not to log to InfluxDB.
/// </summary>
public bool LoggingEnabled { get; set; }
public string Organization { get; set; } = "lighthouse";
public string Bucket { get; set; } = "lighthouse";
public string Token { get; set; } = "";
public string Url { get; set; } = "http://localhost:8086";
}

View file

@ -0,0 +1,18 @@
namespace LBPUnion.ProjectLighthouse.Types.Settings.ConfigurationCategories;
public class MailConfiguration
{
public bool MailEnabled { get; set; }
public string Host { get; set; } = "";
public int Port { get; set; } = 587;
public string FromAddress { get; set; } = "lighthouse@example.com";
public string FromName { get; set; } = "Project Lighthouse";
public string Password { get; set; } = "";
public bool UseSSL { get; set; } = true;
}

View file

@ -0,0 +1,21 @@
namespace LBPUnion.ProjectLighthouse.Types.Settings.ConfigurationCategories;
public class UserGeneratedContentLimitConfiguration
{
/// <summary>
/// The maximum amount of slots allowed on users' earth
/// </summary>
public int EntitledSlots { get; set; } = 50;
public int ListsQuota { get; set; } = 50;
public int PhotosQuota { get; set; } = 500;
public bool ProfileCommentsEnabled { get; set; } = true;
public bool LevelCommentsEnabled { get; set; } = true;
public bool LevelReviewsEnabled { get; set; } = true;
public bool BooingEnabled { get; set; } = true;
}

View file

@ -0,0 +1,8 @@
namespace LBPUnion.ProjectLighthouse.Types.Settings.ConfigurationCategories;
public class WebsiteConfiguration
{
public string MissingIconHash { get; set; } = "";
public bool ConvertAssetsOnStartup { get; set; } = true;
}

View file

@ -0,0 +1,214 @@
using System.IO;
using System.Text.Json;
using LBPUnion.ProjectLighthouse.Types.Settings.ConfigurationCategories;
namespace LBPUnion.ProjectLighthouse.Types.Settings.Legacy;
#nullable enable
internal class LegacyServerSettings
{
#region Meta
public const string ConfigFileName = "lighthouse.config.json";
#endregion
#region InfluxDB
public bool InfluxEnabled { get; set; }
public bool InfluxLoggingEnabled { get; set; }
public string InfluxOrg { get; set; } = "lighthouse";
public string InfluxBucket { get; set; } = "lighthouse";
public string InfluxToken { get; set; } = "";
public string InfluxUrl { get; set; } = "http://localhost:8086";
#endregion
public string EulaText { get; set; } = "";
#if !DEBUG
public string AnnounceText { get; set; } = "You are now logged in as %user.";
#else
public string AnnounceText { get; set; } = "You are now logged in as %user (id: %id).";
#endif
public string DbConnectionString { get; set; } = "server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse";
public string ExternalUrl { get; set; } = "http://localhost:10060";
public string ServerDigestKey { get; set; } = "";
public string AlternateDigestKey { get; set; } = "";
public bool UseExternalAuth { get; set; }
public bool CheckForUnsafeFiles { get; set; } = true;
public bool RegistrationEnabled { get; set; } = true;
#region UGC Limits
/// <summary>
/// The maximum amount of slots allowed on users' earth
/// </summary>
public int EntitledSlots { get; set; } = 50;
public int ListsQuota { get; set; } = 50;
public int PhotosQuota { get; set; } = 500;
public bool ProfileCommentsEnabled { get; set; } = true;
public bool LevelCommentsEnabled { get; set; } = true;
public bool LevelReviewsEnabled { get; set; } = true;
#endregion
#region Google Analytics
public bool GoogleAnalyticsEnabled { get; set; }
public string GoogleAnalyticsId { get; set; } = "";
#endregion
public bool BlockDeniedUsers { get; set; } = true;
public bool BooingEnabled { get; set; } = true;
public FilterMode UserInputFilterMode { get; set; } = FilterMode.None;
#region Discord Webhook
public bool DiscordWebhookEnabled { get; set; }
public string DiscordWebhookUrl { get; set; } = "";
#endregion
public bool ConfigReloading { get; set; } = true;
public string MissingIconHash { get; set; } = "";
#region HCaptcha
public bool HCaptchaEnabled { get; set; }
public string HCaptchaSiteKey { get; set; } = "";
public string HCaptchaSecret { get; set; } = "";
#endregion
public string ServerListenUrl { get; set; } = "http://localhost:10060";
public bool ConvertAssetsOnStartup { get; set; } = true;
#region SMTP
public bool SMTPEnabled { get; set; }
public string SMTPHost { get; set; } = "";
public int SMTPPort { get; set; } = 587;
public string SMTPFromAddress { get; set; } = "lighthouse@example.com";
public string SMTPFromName { get; set; } = "Project Lighthouse";
public string SMTPPassword { get; set; } = "";
public bool SMTPSsl { get; set; } = true;
#endregion
internal static LegacyServerSettings? FromFile(string path)
{
string data = File.ReadAllText(path);
return JsonSerializer.Deserialize<LegacyServerSettings>(data);
}
internal ServerConfiguration ToNewConfiguration()
{
ServerConfiguration configuration = new();
configuration.ConfigReloading = this.ConfigReloading;
configuration.AnnounceText = this.AnnounceText;
configuration.EulaText = this.EulaText;
configuration.ExternalUrl = this.ExternalUrl;
configuration.DbConnectionString = this.DbConnectionString;
configuration.CheckForUnsafeFiles = this.CheckForUnsafeFiles;
configuration.UserInputFilterMode = this.UserInputFilterMode;
// configuration categories
configuration.InfluxDB = new InfluxDBConfiguration
{
InfluxEnabled = this.InfluxEnabled,
LoggingEnabled = this.InfluxLoggingEnabled,
Bucket = this.InfluxBucket,
Organization = this.InfluxOrg,
Token = this.InfluxToken,
Url = InfluxUrl,
};
configuration.Authentication = new AuthenticationConfiguration
{
RegistrationEnabled = this.RegistrationEnabled,
BlockDeniedUsers = this.BlockDeniedUsers,
UseExternalAuth = this.UseExternalAuth,
};
configuration.Captcha = new CaptchaConfiguration
{
CaptchaEnabled = this.HCaptchaEnabled,
SiteKey = this.HCaptchaSiteKey,
Secret = this.HCaptchaSecret,
};
configuration.Mail = new MailConfiguration
{
MailEnabled = this.SMTPEnabled,
Host = this.SMTPHost,
Password = this.SMTPPassword,
Port = this.SMTPPort,
FromAddress = this.SMTPFromAddress,
FromName = this.SMTPFromName,
UseSSL = this.SMTPSsl,
};
configuration.DigestKey = new DigestKeyConfiguration
{
PrimaryDigestKey = this.ServerDigestKey,
AlternateDigestKey = this.AlternateDigestKey,
};
configuration.DiscordIntegration = new DiscordIntegrationConfiguration
{
DiscordIntegrationEnabled = this.DiscordWebhookEnabled,
Url = this.DiscordWebhookUrl,
};
configuration.GoogleAnalytics = new GoogleAnalyticsConfiguration
{
AnalyticsEnabled = this.GoogleAnalyticsEnabled,
Id = this.GoogleAnalyticsId,
};
configuration.UserGeneratedContentLimits = new UserGeneratedContentLimitConfiguration
{
BooingEnabled = this.BooingEnabled,
EntitledSlots = this.EntitledSlots,
ListsQuota = this.ListsQuota,
PhotosQuota = this.PhotosQuota,
LevelCommentsEnabled = this.LevelCommentsEnabled,
LevelReviewsEnabled = this.LevelReviewsEnabled,
ProfileCommentsEnabled = this.ProfileCommentsEnabled,
};
configuration.WebsiteConfiguration = new WebsiteConfiguration
{
MissingIconHash = this.MissingIconHash,
ConvertAssetsOnStartup = this.ConvertAssetsOnStartup,
};
return configuration;
}
}

View file

@ -0,0 +1,189 @@
#nullable enable
using System;
using System.Diagnostics;
using System.IO;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.Types.Settings.ConfigurationCategories;
using LBPUnion.ProjectLighthouse.Types.Settings.Legacy;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace LBPUnion.ProjectLighthouse.Types.Settings;
[Serializable]
public class ServerConfiguration
{
// HEY, YOU!
// THIS VALUE MUST BE INCREMENTED FOR EVERY CONFIG CHANGE!
//
// This is so Lighthouse can properly identify outdated configurations and update them with newer settings accordingly.
// If you are modifying anything here that isn't outside of a method, this value MUST be incremented.
// It is also strongly recommended to not remove any items, else it will cause deserialization errors.
// You can use an ObsoleteAttribute instead. Make sure you set it to error, though.
//
// Thanks for listening~
public const int CurrentConfigVersion = 1;
#region Meta
public static ServerConfiguration Instance;
[YamlMember(Alias = "configVersionDoNotModifyOrYouWillBeSlapped")]
public int ConfigVersion { get; set; } = CurrentConfigVersion;
public const string ConfigFileName = "lighthouse.yml";
public const string LegacyConfigFileName = LegacyServerSettings.ConfigFileName;
#endregion Meta
#region Setup
private static FileSystemWatcher fileWatcher;
// ReSharper disable once NotNullMemberIsNotInitialized
#pragma warning disable CS8618
static ServerConfiguration()
{
if (ServerStatics.IsUnitTesting) return; // Unit testing, we don't want to read configurations here since the tests will provide their own
Logger.LogInfo("Loading config...", LogArea.Config);
ServerConfiguration? tempConfig;
// If a valid YML configuration is available!
if (File.Exists(ConfigFileName) && (tempConfig = fromFile(ConfigFileName)) != null)
{
// Instance = JsonSerializer.Deserialize<ServerConfiguration>(configFile) ?? throw new ArgumentNullException(nameof(ConfigFileName));
Instance = tempConfig;
if (Instance.ConfigVersion < CurrentConfigVersion)
{
Logger.LogInfo($"Upgrading config file from version {Instance.ConfigVersion} to version {CurrentConfigVersion}", LogArea.Config);
Instance.ConfigVersion = CurrentConfigVersion;
Instance.writeConfig(ConfigFileName);
}
}
// If we have a valid legacy configuration we can migrate, let's do it now.
else if (File.Exists(LegacyConfigFileName))
{
Logger.LogWarn("This version of Project Lighthouse now uses YML instead of JSON to store configuration.", LogArea.Config);
Logger.LogWarn
("As such, the config will now be migrated to use YML. Do not modify the original JSON file; changes will not be kept.", LogArea.Config);
Logger.LogInfo($"The new configuration is stored at {ConfigFileName}.", LogArea.Config);
LegacyServerSettings? legacyConfig = LegacyServerSettings.FromFile(LegacyConfigFileName);
Debug.Assert(legacyConfig != null);
Instance = legacyConfig.ToNewConfiguration();
Instance.writeConfig(ConfigFileName);
Logger.LogSuccess("The configuration migration completed successfully.", LogArea.Config);
}
// If there is no valid YML configuration available,
// generate a blank one and ask the server operator to configure it, then exit.
else
{
new ServerConfiguration().writeConfig(ConfigFileName + ".configme");
Logger.LogWarn
(
"The configuration file was not found. " +
"A blank configuration file has been created for you at " +
$"{Path.Combine(Environment.CurrentDirectory, ConfigFileName + ".configme")}",
LogArea.Config
);
Environment.Exit(1);
}
// Set up reloading
if (Instance.ConfigReloading)
{
Logger.LogInfo("Setting up config reloading...", LogArea.Config);
fileWatcher = new FileSystemWatcher
{
Path = Environment.CurrentDirectory,
Filter = ConfigFileName,
NotifyFilter = NotifyFilters.LastWrite, // only watch for writes to config file
};
fileWatcher.Changed += onConfigChanged; // add event handler
fileWatcher.EnableRaisingEvents = true; // begin watching
}
}
#pragma warning restore CS8618
private static void onConfigChanged(object sender, FileSystemEventArgs e)
{
Debug.Assert(e.Name == ConfigFileName);
Logger.LogInfo("Configuration file modified, reloading config...", LogArea.Config);
Logger.LogWarn("Some changes may not apply; they will require a restart of Lighthouse.", LogArea.Config);
ServerConfiguration? configuration = fromFile(ConfigFileName);
if (configuration == null)
{
Logger.LogWarn("The new configuration was unable to be loaded for some reason. The old config has been kept.", LogArea.Config);
return;
}
Instance = configuration;
Logger.LogSuccess("Successfully reloaded the configuration!", LogArea.Config);
}
private static INamingConvention namingConvention = CamelCaseNamingConvention.Instance;
private static ServerConfiguration? fromFile(string path)
{
IDeserializer deserializer = new DeserializerBuilder().WithNamingConvention(namingConvention).Build();
string text;
try
{
text = File.ReadAllText(path);
}
catch
{
return null;
}
return deserializer.Deserialize<ServerConfiguration>(text);
}
private void writeConfig(string path)
{
ISerializer serializer = new SerializerBuilder().WithNamingConvention(namingConvention).Build();
File.WriteAllText(path, serializer.Serialize(this));
}
#endregion
public string ListenUrl { get; set; } = "http://localhost:10060";
public string DbConnectionString { get; set; } = "server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse";
public string ExternalUrl { get; set; } = "http://localhost:10060";
public bool ConfigReloading { get; set; }
public string EulaText { get; set; } = "";
#if !DEBUG
public string AnnounceText { get; set; } = "You are now logged in as %user.";
#else
public string AnnounceText { get; set; } = "You are now logged in as %user (id: %id).";
#endif
public bool CheckForUnsafeFiles { get; set; } = true;
public FilterMode UserInputFilterMode { get; set; } = FilterMode.None;
public AuthenticationConfiguration Authentication { get; set; } = new();
public CaptchaConfiguration Captcha { get; set; } = new();
public DigestKeyConfiguration DigestKey { get; set; } = new();
public DiscordIntegrationConfiguration DiscordIntegration { get; set; } = new();
public GoogleAnalyticsConfiguration GoogleAnalytics { get; set; } = new();
public InfluxDBConfiguration InfluxDB { get; set; } = new();
public MailConfiguration Mail { get; set; } = new();
public UserGeneratedContentLimitConfiguration UserGeneratedContentLimits { get; set; } = new();
public WebsiteConfiguration WebsiteConfiguration { get; set; } = new();
}

View file

@ -1,198 +0,0 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using JetBrains.Annotations;
using LBPUnion.ProjectLighthouse.Logging;
namespace LBPUnion.ProjectLighthouse.Types.Settings;
[Serializable]
public class ServerSettings
{
public const int CurrentConfigVersion = 26; // MUST BE INCREMENTED FOR EVERY CONFIG CHANGE!
private static FileSystemWatcher fileWatcher;
// ReSharper disable once NotNullMemberIsNotInitialized
static ServerSettings()
{
if (ServerStatics.IsUnitTesting) return; // Unit testing, we don't want to read configurations here since the tests will provide their own
Logger.LogInfo("Loading config...", LogArea.Config);
if (File.Exists(ConfigFileName))
{
string configFile = File.ReadAllText(ConfigFileName);
Instance = JsonSerializer.Deserialize<ServerSettings>(configFile) ?? throw new ArgumentNullException(nameof(ConfigFileName));
if (Instance.ConfigVersion < CurrentConfigVersion)
{
Logger.LogInfo($"Upgrading config file from version {Instance.ConfigVersion} to version {CurrentConfigVersion}", LogArea.Config);
Instance.ConfigVersion = CurrentConfigVersion;
configFile = JsonSerializer.Serialize
(
Instance,
typeof(ServerSettings),
new JsonSerializerOptions
{
WriteIndented = true,
}
);
File.WriteAllText(ConfigFileName, configFile);
}
}
else
{
string configFile = JsonSerializer.Serialize
(
new ServerSettings(),
typeof(ServerSettings),
new JsonSerializerOptions
{
WriteIndented = true,
}
);
File.WriteAllText(ConfigFileName, configFile);
Logger.LogWarn
(
"The configuration file was not found. " +
"A blank configuration file has been created for you at " +
$"{Path.Combine(Environment.CurrentDirectory, ConfigFileName)}",
LogArea.Config
);
Environment.Exit(1);
}
// Set up reloading
if (Instance.ConfigReloading)
{
Logger.LogInfo("Setting up config reloading...", LogArea.Config);
fileWatcher = new FileSystemWatcher
{
Path = Environment.CurrentDirectory,
Filter = ConfigFileName,
NotifyFilter = NotifyFilters.LastWrite, // only watch for writes to config file
};
fileWatcher.Changed += onConfigChanged; // add event handler
fileWatcher.EnableRaisingEvents = true; // begin watching
}
}
private static void onConfigChanged(object sender, FileSystemEventArgs e)
{
Debug.Assert(e.Name == ConfigFileName);
Logger.LogInfo("Configuration file modified, reloading config.", LogArea.Config);
Logger.LogWarn("Some changes may not apply, in which case may require a restart of Project Lighthouse.", LogArea.Config);
string configFile = File.ReadAllText(ConfigFileName);
Instance = JsonSerializer.Deserialize<ServerSettings>(configFile) ?? throw new ArgumentNullException(nameof(ConfigFileName));
}
public bool InfluxEnabled { get; set; }
public bool InfluxLoggingEnabled { get; set; }
public string InfluxOrg { get; set; } = "lighthouse";
public string InfluxBucket { get; set; } = "lighthouse";
public string InfluxToken { get; set; } = "";
public string InfluxUrl { get; set; } = "http://localhost:8086";
public string EulaText { get; set; } = "";
#if !DEBUG
public string AnnounceText { get; set; } = "You are now logged in as %user.";
#else
public string AnnounceText { get; set; } = "You are now logged in as %user (id: %id).";
#endif
public string DbConnectionString { get; set; } = "server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse";
public string ExternalUrl { get; set; } = "http://localhost:10060";
public string ServerDigestKey { get; set; } = "";
public string AlternateDigestKey { get; set; } = "";
public bool UseExternalAuth { get; set; }
public bool CheckForUnsafeFiles { get; set; } = true;
public bool RegistrationEnabled { get; set; } = true;
/// <summary>
/// The maximum amount of slots allowed on users' earth
/// </summary>
public int EntitledSlots { get; set; } = 50;
public int ListsQuota { get; set; } = 50;
public int PhotosQuota { get; set; } = 500;
public bool ProfileCommentsEnabled { get; set; } = true;
public bool LevelCommentsEnabled { get; set; } = true;
public bool LevelReviewsEnabled { get; set; } = true;
public bool GoogleAnalyticsEnabled { get; set; }
public string GoogleAnalyticsId { get; set; } = "";
public bool BlockDeniedUsers { get; set; } = true;
public bool BooingEnabled { get; set; } = true;
public FilterMode UserInputFilterMode { get; set; } = FilterMode.None;
public bool DiscordWebhookEnabled { get; set; }
public string DiscordWebhookUrl { get; set; } = "";
public bool ConfigReloading { get; set; } = true;
public string MissingIconHash { get; set; } = "";
public bool HCaptchaEnabled { get; set; }
public string HCaptchaSiteKey { get; set; } = "";
public string HCaptchaSecret { get; set; } = "";
public string ServerListenUrl { get; set; } = "http://localhost:10060";
public bool ConvertAssetsOnStartup { get; set; } = true;
#region SMTP
public bool SMTPEnabled { get; set; }
public string SMTPHost { get; set; } = "";
public int SMTPPort { get; set; } = 587;
public string SMTPFromAddress { get; set; } = "lighthouse@example.com";
public string SMTPFromName { get; set; } = "Project Lighthouse";
public string SMTPPassword { get; set; } = "";
public bool SMTPSsl { get; set; } = true;
#endregion
#region Meta
[NotNull]
public static ServerSettings Instance;
[JsonPropertyName("ConfigVersionDoNotModifyOrYouWillBeSlapped")]
public int ConfigVersion { get; set; } = CurrentConfigVersion;
public const string ConfigFileName = "lighthouse.config.json";
#endregion Meta
}

View file

@ -64,7 +64,7 @@ public class User
if (string.IsNullOrWhiteSpace(avatarHash) || this.IconHash.StartsWith('g')) avatarHash = this.YayHash;
if (string.IsNullOrWhiteSpace(avatarHash)) avatarHash = this.MehHash;
if (string.IsNullOrWhiteSpace(avatarHash)) avatarHash = this.BooHash;
if (string.IsNullOrWhiteSpace(avatarHash)) avatarHash = ServerSettings.Instance.MissingIconHash;
if (string.IsNullOrWhiteSpace(avatarHash)) avatarHash = ServerConfiguration.Instance.WebsiteConfiguration.MissingIconHash;
return avatarHash;
}
@ -149,13 +149,17 @@ public class User
LbpSerializer.StringElement("game", (int)gameVersion) +
this.serializeSlots(gameVersion) +
LbpSerializer.StringElement("lists", this.Lists) +
LbpSerializer.StringElement("lists_quota", ServerSettings.Instance.ListsQuota) + // technically not a part of the user but LBP expects it
LbpSerializer.StringElement
(
"lists_quota",
ServerConfiguration.Instance.UserGeneratedContentLimits.ListsQuota
) + // technically not a part of the user but LBP expects it
LbpSerializer.StringElement("biography", this.Biography) +
LbpSerializer.StringElement("reviewCount", this.Reviews) +
LbpSerializer.StringElement("commentCount", this.Comments) +
LbpSerializer.StringElement("photosByMeCount", this.PhotosByMe) +
LbpSerializer.StringElement("photosWithMeCount", this.PhotosWithMe) +
LbpSerializer.StringElement("commentsEnabled", ServerSettings.Instance.ProfileCommentsEnabled) +
LbpSerializer.StringElement("commentsEnabled", ServerConfiguration.Instance.UserGeneratedContentLimits.ProfileCommentsEnabled) +
LbpSerializer.StringElement("location", this.Location.Serialize()) +
LbpSerializer.StringElement("favouriteSlotCount", this.HeartedLevels) +
LbpSerializer.StringElement("favouriteUserCount", this.HeartedUsers) +
@ -204,7 +208,7 @@ public class User
[JsonIgnore]
[XmlIgnore]
public int EntitledSlots => ServerSettings.Instance.EntitledSlots + this.AdminGrantedSlots;
public int EntitledSlots => ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots + this.AdminGrantedSlots;
/// <summary>
/// The number of slots remaining on the earth