mirror of
https://github.com/LBPUnion/ProjectLighthouse.git
synced 2025-05-20 16:22:27 +00:00
Change login UI and improve email setup flow (#619)
* Rework login UI design and change email setup flow * Remove unused middleware * Fix button not lining up with input fields
This commit is contained in:
parent
20b2ef5700
commit
7d187ee982
11 changed files with 207 additions and 179 deletions
|
@ -25,7 +25,7 @@
|
||||||
<value>Password</value>
|
<value>Password</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="register" xml:space="preserve">
|
<data name="register" xml:space="preserve">
|
||||||
<value>Register</value>
|
<value>Create an account</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="forgot_password" xml:space="preserve">
|
<data name="forgot_password" xml:space="preserve">
|
||||||
<value>Forgot Password?</value>
|
<value>Forgot Password?</value>
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
using LBPUnion.ProjectLighthouse.Middlewares;
|
|
||||||
|
|
||||||
namespace LBPUnion.ProjectLighthouse.Servers.Website.Middlewares;
|
|
||||||
|
|
||||||
public class HandlePageErrorMiddleware : Middleware
|
|
||||||
{
|
|
||||||
public HandlePageErrorMiddleware(RequestDelegate next) : base(next)
|
|
||||||
{}
|
|
||||||
|
|
||||||
public override async Task InvokeAsync(HttpContext ctx)
|
|
||||||
{
|
|
||||||
await this.next(ctx);
|
|
||||||
if (ctx.Response.StatusCode == 404 && !ctx.Request.Path.StartsWithSegments("/gameAssets"))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ctx.Request.Path = "/404";
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
// not much we can do to save us, carry on anyways
|
|
||||||
await this.next(ctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,7 @@
|
||||||
#nullable enable
|
#nullable enable
|
||||||
using LBPUnion.ProjectLighthouse.Configuration;
|
using LBPUnion.ProjectLighthouse.Configuration;
|
||||||
|
using LBPUnion.ProjectLighthouse.Helpers;
|
||||||
|
using LBPUnion.ProjectLighthouse.PlayerData;
|
||||||
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
|
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
|
||||||
using LBPUnion.ProjectLighthouse.PlayerData.Profiles.Email;
|
using LBPUnion.ProjectLighthouse.PlayerData.Profiles.Email;
|
||||||
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
|
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
|
||||||
|
@ -19,9 +21,6 @@ public class CompleteEmailVerificationPage : BaseLayout
|
||||||
{
|
{
|
||||||
if (!ServerConfiguration.Instance.Mail.MailEnabled) 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");
|
|
||||||
|
|
||||||
EmailVerificationToken? emailVerifyToken = await this.Database.EmailVerificationTokens.FirstOrDefaultAsync(e => e.EmailToken == token);
|
EmailVerificationToken? emailVerifyToken = await this.Database.EmailVerificationTokens.FirstOrDefaultAsync(e => e.EmailToken == token);
|
||||||
if (emailVerifyToken == null)
|
if (emailVerifyToken == null)
|
||||||
{
|
{
|
||||||
|
@ -29,6 +28,8 @@ public class CompleteEmailVerificationPage : BaseLayout
|
||||||
return this.Page();
|
return this.Page();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
User user = await this.Database.Users.FirstAsync(u => u.UserId == emailVerifyToken.UserId);
|
||||||
|
|
||||||
if (DateTime.Now > emailVerifyToken.ExpiresAt)
|
if (DateTime.Now > emailVerifyToken.ExpiresAt)
|
||||||
{
|
{
|
||||||
this.Error = "This token has expired";
|
this.Error = "This token has expired";
|
||||||
|
@ -44,9 +45,27 @@ public class CompleteEmailVerificationPage : BaseLayout
|
||||||
this.Database.EmailVerificationTokens.Remove(emailVerifyToken);
|
this.Database.EmailVerificationTokens.Remove(emailVerifyToken);
|
||||||
|
|
||||||
user.EmailAddressVerified = true;
|
user.EmailAddressVerified = true;
|
||||||
|
|
||||||
await this.Database.SaveChangesAsync();
|
await this.Database.SaveChangesAsync();
|
||||||
|
|
||||||
return this.Page();
|
if (user.Password != null) return this.Page();
|
||||||
|
|
||||||
|
// if user's account was created automatically
|
||||||
|
WebToken webToken = new()
|
||||||
|
{
|
||||||
|
ExpiresAt = DateTime.Now.AddDays(7),
|
||||||
|
Verified = true,
|
||||||
|
UserId = user.UserId,
|
||||||
|
UserToken = CryptoHelper.GenerateAuthToken(),
|
||||||
|
};
|
||||||
|
user.PasswordResetRequired = true;
|
||||||
|
this.Database.WebTokens.Add(webToken);
|
||||||
|
await this.Database.SaveChangesAsync();
|
||||||
|
this.Response.Cookies.Append("LighthouseToken",
|
||||||
|
webToken.UserToken,
|
||||||
|
new CookieOptions
|
||||||
|
{
|
||||||
|
Expires = DateTimeOffset.Now.AddDays(7),
|
||||||
|
});
|
||||||
|
return this.Redirect("/passwordReset");
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,13 +1,9 @@
|
||||||
#nullable enable
|
#nullable enable
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using LBPUnion.ProjectLighthouse.Configuration;
|
using LBPUnion.ProjectLighthouse.Configuration;
|
||||||
using LBPUnion.ProjectLighthouse.Extensions;
|
|
||||||
using LBPUnion.ProjectLighthouse.Helpers;
|
using LBPUnion.ProjectLighthouse.Helpers;
|
||||||
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
|
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
|
||||||
using LBPUnion.ProjectLighthouse.PlayerData.Profiles.Email;
|
|
||||||
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
|
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Email;
|
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Email;
|
||||||
|
|
||||||
|
@ -16,9 +12,6 @@ public class SendVerificationEmailPage : BaseLayout
|
||||||
public SendVerificationEmailPage(Database database) : base(database)
|
public SendVerificationEmailPage(Database database) : base(database)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
// (User id, timestamp of last request + 30 seconds)
|
|
||||||
private static readonly ConcurrentDictionary<int, long> recentlySentEmail = new();
|
|
||||||
|
|
||||||
public bool Success { get; set; }
|
public bool Success { get; set; }
|
||||||
|
|
||||||
public async Task<IActionResult> OnGet()
|
public async Task<IActionResult> OnGet()
|
||||||
|
@ -28,70 +21,9 @@ public class SendVerificationEmailPage : BaseLayout
|
||||||
User? user = this.Database.UserFromWebRequest(this.Request);
|
User? user = this.Database.UserFromWebRequest(this.Request);
|
||||||
if (user == null) return this.Redirect("/login");
|
if (user == null) return this.Redirect("/login");
|
||||||
|
|
||||||
// `using` weirdness here. I tried to fix it, but I couldn't.
|
if (user.EmailAddressVerified) return this.Redirect("/");
|
||||||
// The user should never see this page once they've been verified, so assert here.
|
|
||||||
System.Diagnostics.Debug.Assert(!user.EmailAddressVerified);
|
|
||||||
|
|
||||||
// Othewise, on a release build, just silently redirect them to the landing page.
|
this.Success = await SMTPHelper.SendVerificationEmail(this.Database, user);
|
||||||
#if !DEBUG
|
|
||||||
if (user.EmailAddressVerified)
|
|
||||||
{
|
|
||||||
return this.Redirect("/");
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Remove expired entries
|
|
||||||
for (int i = recentlySentEmail.Count - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
KeyValuePair<int, long> entry = recentlySentEmail.ElementAt(i);
|
|
||||||
bool valueExists = recentlySentEmail.TryGetValue(entry.Key, out long timestamp);
|
|
||||||
if (!valueExists)
|
|
||||||
{
|
|
||||||
recentlySentEmail.TryRemove(entry.Key, out _);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (TimeHelper.TimestampMillis > timestamp) recentlySentEmail.TryRemove(entry.Key, out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (recentlySentEmail.ContainsKey(user.UserId))
|
|
||||||
{
|
|
||||||
bool valueExists = recentlySentEmail.TryGetValue(user.UserId, out long timestamp);
|
|
||||||
if (!valueExists)
|
|
||||||
{
|
|
||||||
recentlySentEmail.TryRemove(user.UserId, out _);
|
|
||||||
}
|
|
||||||
else if (timestamp > TimeHelper.TimestampMillis)
|
|
||||||
{
|
|
||||||
this.Success = true;
|
|
||||||
return this.Page();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
string? existingToken = await this.Database.EmailVerificationTokens.Where(v => v.UserId == user.UserId).Select(v => v.EmailToken).FirstOrDefaultAsync();
|
|
||||||
if (existingToken != null)
|
|
||||||
this.Database.EmailVerificationTokens.RemoveWhere(t => t.EmailToken == existingToken);
|
|
||||||
|
|
||||||
EmailVerificationToken verifyToken = new()
|
|
||||||
{
|
|
||||||
UserId = user.UserId,
|
|
||||||
User = user,
|
|
||||||
EmailToken = CryptoHelper.GenerateAuthToken(),
|
|
||||||
ExpiresAt = DateTime.Now.AddHours(6),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.Database.EmailVerificationTokens.Add(verifyToken);
|
|
||||||
await this.Database.SaveChangesAsync();
|
|
||||||
|
|
||||||
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: {ServerConfiguration.Instance.ExternalUrl}/verifyEmail?token={verifyToken.EmailToken}\n\n\n" +
|
|
||||||
"If this wasn't you, feel free to ignore this email.";
|
|
||||||
|
|
||||||
this.Success = await SMTPHelper.SendEmailAsync(user.EmailAddress, "Project Lighthouse Email Verification", body);
|
|
||||||
|
|
||||||
// Don't send another email for 30 seconds
|
|
||||||
recentlySentEmail.TryAdd(user.UserId, TimeHelper.TimestampMillis + 30 * 1000);
|
|
||||||
|
|
||||||
return this.Page();
|
return this.Page();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
|
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Errors;
|
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Errors;
|
||||||
|
|
||||||
|
@ -7,9 +7,6 @@ public class NotFoundPage : BaseLayout
|
||||||
{
|
{
|
||||||
public NotFoundPage(Database database) : base(database)
|
public NotFoundPage(Database database) : base(database)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
public void OnGet()
|
public IActionResult OnGet() => this.Page();
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -6,6 +6,7 @@
|
||||||
@{
|
@{
|
||||||
Layout = "Layouts/BaseLayout";
|
Layout = "Layouts/BaseLayout";
|
||||||
Model.Title = Model.Translate(GeneralStrings.LogIn);
|
Model.Title = Model.Translate(GeneralStrings.LogIn);
|
||||||
|
Model.ShowTitleInPage = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
<script src="https://geraintluff.github.io/sha256/sha256.min.js"></script>
|
<script src="https://geraintluff.github.io/sha256/sha256.min.js"></script>
|
||||||
|
@ -26,61 +27,70 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(Model.Error))
|
<div class="ui middle aligned center aligned grid">
|
||||||
{
|
<form class="ui form" onsubmit="return onSubmit(this)" method="post">
|
||||||
<div class="ui negative message">
|
@if (!string.IsNullOrWhiteSpace(Model.Error))
|
||||||
<div class="header">
|
|
||||||
@Model.Translate(GeneralStrings.Error)
|
|
||||||
</div>
|
|
||||||
<p style="white-space: pre-line">@Model.Error</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<form class="ui form" onsubmit="return onSubmit(this)" method="post">
|
|
||||||
@Html.AntiForgeryToken()
|
|
||||||
<input type="hidden" id="redirect" name="redirect">
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
@{
|
|
||||||
string username = ServerConfiguration.Instance.Mail.MailEnabled ? Model.Translate(GeneralStrings.Email) : Model.Translate(GeneralStrings.Username);
|
|
||||||
}
|
|
||||||
<label>@username</label>
|
|
||||||
<div class="ui left icon input">
|
|
||||||
<input type="text" name="username" id="text" placeholder="@username">
|
|
||||||
<i class="user icon"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label>@Model.Translate(GeneralStrings.Password)</label>
|
|
||||||
<div class="ui left icon input">
|
|
||||||
<input type="password" id="password" placeholder="@Model.Translate(GeneralStrings.Password)">
|
|
||||||
<input type="hidden" id="password-submit" name="password">
|
|
||||||
<i class="lock icon"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (ServerConfiguration.Instance.Captcha.CaptchaEnabled)
|
|
||||||
{
|
|
||||||
@await Html.PartialAsync("Partials/CaptchaPartial")
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<input type="submit" value="@Model.Translate(GeneralStrings.LogIn)" id="submit" class="ui blue button">
|
|
||||||
@if (ServerConfiguration.Instance.Authentication.RegistrationEnabled)
|
|
||||||
{
|
{
|
||||||
<a href="/register">
|
<div class="ui negative message">
|
||||||
<div class="ui button">
|
<div class="header">
|
||||||
<i class="user alternate add icon"></i>
|
@Model.Translate(GeneralStrings.Error)
|
||||||
@Model.Translate(GeneralStrings.Register)
|
</div>
|
||||||
|
<p style="white-space: pre-line">@Model.Error</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<input type="hidden" id="redirect" name="redirect">
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
<h2 class="ui black image header centered">
|
||||||
|
<img src="~/logo-color.png" class="image" style="width: 128px;">
|
||||||
|
<div class="content">
|
||||||
|
@Model.Title
|
||||||
|
</div>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
@{
|
||||||
|
string username = ServerConfiguration.Instance.Mail.MailEnabled ? Model.Translate(GeneralStrings.Email) : Model.Translate(GeneralStrings.Username);
|
||||||
|
}
|
||||||
|
<label>@username</label>
|
||||||
|
<div class="ui left icon input">
|
||||||
|
<input type="text" name="username" id="text" placeholder="@username">
|
||||||
|
<i class="user icon"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label>@Model.Translate(GeneralStrings.Password)</label>
|
||||||
|
<div class="ui left icon input">
|
||||||
|
<input type="password" id="password" placeholder="@Model.Translate(GeneralStrings.Password)">
|
||||||
|
<input type="hidden" id="password-submit" name="password">
|
||||||
|
<i class="lock icon"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (ServerConfiguration.Instance.Captcha.CaptchaEnabled)
|
||||||
|
{
|
||||||
|
@await Html.PartialAsync("Partials/CaptchaPartial")
|
||||||
|
<div class="ui divider fitted hidden"></div>
|
||||||
|
}
|
||||||
|
<input type="submit" value="@Model.Translate(GeneralStrings.LogIn)" id="submit" class="ui fluid blue button" style="width: 25em; margin-right: 0">
|
||||||
|
|
||||||
|
<a href="/passwordResetRequest">
|
||||||
|
<div class="ui fluid button" style="background-color: rgba(0, 0, 0, 0)">
|
||||||
|
@Model.Translate(GeneralStrings.ForgotPassword)
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
}
|
|
||||||
</div>
|
@if (ServerConfiguration.Instance.Authentication.RegistrationEnabled)
|
||||||
<br/>
|
{
|
||||||
<a href="/passwordResetRequest">
|
<div class="ui divider"></div>
|
||||||
<div class="ui button">
|
<a href="/register" style="text-align: center;">
|
||||||
@Model.Translate(GeneralStrings.ForgotPassword)
|
<div class="ui fluid button" style="background-color: rgba(0, 0, 0, 0)">
|
||||||
|
@Model.Translate(GeneralStrings.Register)
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</form>
|
||||||
</form>
|
</div>
|
|
@ -5,6 +5,7 @@
|
||||||
@{
|
@{
|
||||||
Layout = "Layouts/BaseLayout";
|
Layout = "Layouts/BaseLayout";
|
||||||
Model.Title = "Password Reset";
|
Model.Title = "Password Reset";
|
||||||
|
Model.ShowTitleInPage = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
<script src="https://geraintluff.github.io/sha256/sha256.min.js"></script>
|
<script src="https://geraintluff.github.io/sha256/sha256.min.js"></script>
|
||||||
|
@ -33,20 +34,39 @@
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<div class="ui middle aligned center aligned grid">
|
||||||
<form onsubmit="return onSubmit(this)" method="post">
|
<form onsubmit="return onSubmit(this)" method="post">
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
|
|
||||||
<div class="ui left labeled input">
|
<div class="column">
|
||||||
<label for="password" class="ui blue label">Password: </label>
|
<h2 class="ui black image header centered">
|
||||||
<input type="password" id="password">
|
<img src="~/logo-color.png" class="image" style="width: 128px;">
|
||||||
<input type="hidden" id="password-submit" name="password">
|
<div class="content">
|
||||||
</div><br><br>
|
@Model.Title
|
||||||
|
</div>
|
||||||
|
</h2>
|
||||||
|
|
||||||
<div class="ui left labeled input">
|
<div class="ui form">
|
||||||
<label for="password" class="ui blue label">Confirm Password: </label>
|
<div class="ui field">
|
||||||
<input type="password" id="confirmPassword">
|
<label>Password</label>
|
||||||
<input type="hidden" id="confirmPassword-submit" name="confirmPassword">
|
<div class="ui left icon input">
|
||||||
</div><br><br><br>
|
<input type="password" id="password" placeholder="Password" >
|
||||||
|
<input type="hidden" name="password" id="password-submit">
|
||||||
|
<i class="lock icon"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<input type="submit" value="Reset password and continue" id="submit" class="ui green button"><br>
|
<div class="ui field">
|
||||||
</form>
|
<label>Confirm Password</label>
|
||||||
|
<div class="ui left icon input">
|
||||||
|
<input type="password" id="confirmPassword" placeholder="Confirm Password">
|
||||||
|
<input type="hidden" name="confirmPassword" id="confirm-submit">
|
||||||
|
<i class="lock icon"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="submit" value="Reset password" id="submit" class="ui fluid blue button"><br>
|
||||||
|
<div class="ui divider hidden"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
|
@ -6,6 +6,7 @@
|
||||||
Model.Title = "Password Reset Required";
|
Model.Title = "Password Reset Required";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
<p>An administrator has deemed it necessary that you reset your password. Please do so.</p>
|
<p>An administrator has deemed it necessary that you reset your password. Please do so.</p>
|
||||||
|
|
||||||
<a href="/passwordReset">
|
<a href="/passwordReset">
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Reflection;
|
|
||||||
using LBPUnion.ProjectLighthouse.Localization;
|
using LBPUnion.ProjectLighthouse.Localization;
|
||||||
using LBPUnion.ProjectLighthouse.Middlewares;
|
using LBPUnion.ProjectLighthouse.Middlewares;
|
||||||
using LBPUnion.ProjectLighthouse.Servers.Website.Middlewares;
|
using LBPUnion.ProjectLighthouse.Servers.Website.Middlewares;
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
using Microsoft.AspNetCore.Localization;
|
using Microsoft.AspNetCore.Localization;
|
||||||
using Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation;
|
|
||||||
using Microsoft.Extensions.FileProviders;
|
using Microsoft.Extensions.FileProviders;
|
||||||
|
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
|
@ -76,9 +74,10 @@ public class WebsiteStartup
|
||||||
app.UseDeveloperExceptionPage();
|
app.UseDeveloperExceptionPage();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
app.UseStatusCodePagesWithReExecute("/404");
|
||||||
|
|
||||||
app.UseForwardedHeaders();
|
app.UseForwardedHeaders();
|
||||||
|
|
||||||
app.UseMiddleware<HandlePageErrorMiddleware>();
|
|
||||||
app.UseMiddleware<RequestLogMiddleware>();
|
app.UseMiddleware<RequestLogMiddleware>();
|
||||||
app.UseMiddleware<UserRequiredRedirectMiddleware>();
|
app.UseMiddleware<UserRequiredRedirectMiddleware>();
|
||||||
app.UseMiddleware<RateLimitMiddleware>();
|
app.UseMiddleware<RateLimitMiddleware>();
|
||||||
|
|
77
ProjectLighthouse/Helpers/EmailHelper.cs
Normal file
77
ProjectLighthouse/Helpers/EmailHelper.cs
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
#nullable enable
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using LBPUnion.ProjectLighthouse.Configuration;
|
||||||
|
using LBPUnion.ProjectLighthouse.Extensions;
|
||||||
|
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
|
||||||
|
using LBPUnion.ProjectLighthouse.PlayerData.Profiles.Email;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace LBPUnion.ProjectLighthouse.Helpers;
|
||||||
|
|
||||||
|
public partial class SMTPHelper
|
||||||
|
{
|
||||||
|
// (User id, timestamp of last request + 30 seconds)
|
||||||
|
private static readonly ConcurrentDictionary<int, long> recentlySentEmail = new();
|
||||||
|
|
||||||
|
public static async Task<bool> SendVerificationEmail(Database database, User user)
|
||||||
|
{
|
||||||
|
// Remove expired entries
|
||||||
|
for (int i = recentlySentEmail.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
KeyValuePair<int, long> entry = recentlySentEmail.ElementAt(i);
|
||||||
|
bool valueExists = recentlySentEmail.TryGetValue(entry.Key, out long timestamp);
|
||||||
|
if (!valueExists)
|
||||||
|
{
|
||||||
|
recentlySentEmail.TryRemove(entry.Key, out _);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TimeHelper.TimestampMillis > timestamp) recentlySentEmail.TryRemove(entry.Key, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (recentlySentEmail.ContainsKey(user.UserId))
|
||||||
|
{
|
||||||
|
bool valueExists = recentlySentEmail.TryGetValue(user.UserId, out long timestamp);
|
||||||
|
if (!valueExists)
|
||||||
|
{
|
||||||
|
recentlySentEmail.TryRemove(user.UserId, out _);
|
||||||
|
}
|
||||||
|
else if (timestamp > TimeHelper.TimestampMillis)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string? existingToken = await database.EmailVerificationTokens.Where(v => v.UserId == user.UserId)
|
||||||
|
.Select(v => v.EmailToken)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (existingToken != null) database.EmailVerificationTokens.RemoveWhere(t => t.EmailToken == existingToken);
|
||||||
|
|
||||||
|
EmailVerificationToken verifyToken = new()
|
||||||
|
{
|
||||||
|
UserId = user.UserId,
|
||||||
|
User = user,
|
||||||
|
EmailToken = CryptoHelper.GenerateAuthToken(),
|
||||||
|
ExpiresAt = DateTime.Now.AddHours(6),
|
||||||
|
};
|
||||||
|
|
||||||
|
database.EmailVerificationTokens.Add(verifyToken);
|
||||||
|
await database.SaveChangesAsync();
|
||||||
|
|
||||||
|
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: {ServerConfiguration.Instance.ExternalUrl}/verifyEmail?token={verifyToken.EmailToken}\n\n\n" +
|
||||||
|
"If this wasn't you, feel free to ignore this email.";
|
||||||
|
|
||||||
|
bool success = await SendEmailAsync(user.EmailAddress, "Project Lighthouse Email Verification", body);
|
||||||
|
|
||||||
|
// Don't send another email for 30 seconds
|
||||||
|
recentlySentEmail.TryAdd(user.UserId, TimeHelper.TimestampMillis + 30 * 1000);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,9 +9,8 @@ using LBPUnion.ProjectLighthouse.Logging;
|
||||||
|
|
||||||
namespace LBPUnion.ProjectLighthouse.Helpers;
|
namespace LBPUnion.ProjectLighthouse.Helpers;
|
||||||
|
|
||||||
public class SMTPHelper
|
public partial class SMTPHelper
|
||||||
{
|
{
|
||||||
|
|
||||||
internal static readonly SMTPHelper Instance = new();
|
internal static readonly SMTPHelper Instance = new();
|
||||||
|
|
||||||
private readonly MailAddress fromAddress;
|
private readonly MailAddress fromAddress;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue