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>
|
||||
</data>
|
||||
<data name="register" xml:space="preserve">
|
||||
<value>Register</value>
|
||||
<value>Create an account</value>
|
||||
</data>
|
||||
<data name="forgot_password" xml:space="preserve">
|
||||
<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
|
||||
using LBPUnion.ProjectLighthouse.Configuration;
|
||||
using LBPUnion.ProjectLighthouse.Helpers;
|
||||
using LBPUnion.ProjectLighthouse.PlayerData;
|
||||
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
|
||||
using LBPUnion.ProjectLighthouse.PlayerData.Profiles.Email;
|
||||
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
|
||||
|
@ -19,9 +21,6 @@ public class CompleteEmailVerificationPage : BaseLayout
|
|||
{
|
||||
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);
|
||||
if (emailVerifyToken == null)
|
||||
{
|
||||
|
@ -29,6 +28,8 @@ public class CompleteEmailVerificationPage : BaseLayout
|
|||
return this.Page();
|
||||
}
|
||||
|
||||
User user = await this.Database.Users.FirstAsync(u => u.UserId == emailVerifyToken.UserId);
|
||||
|
||||
if (DateTime.Now > emailVerifyToken.ExpiresAt)
|
||||
{
|
||||
this.Error = "This token has expired";
|
||||
|
@ -44,9 +45,27 @@ public class CompleteEmailVerificationPage : BaseLayout
|
|||
this.Database.EmailVerificationTokens.Remove(emailVerifyToken);
|
||||
|
||||
user.EmailAddressVerified = true;
|
||||
|
||||
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
|
||||
using System.Collections.Concurrent;
|
||||
using LBPUnion.ProjectLighthouse.Configuration;
|
||||
using LBPUnion.ProjectLighthouse.Extensions;
|
||||
using LBPUnion.ProjectLighthouse.Helpers;
|
||||
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
|
||||
using LBPUnion.ProjectLighthouse.PlayerData.Profiles.Email;
|
||||
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Email;
|
||||
|
||||
|
@ -16,9 +12,6 @@ public class SendVerificationEmailPage : BaseLayout
|
|||
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 async Task<IActionResult> OnGet()
|
||||
|
@ -28,70 +21,9 @@ public class SendVerificationEmailPage : BaseLayout
|
|||
User? user = this.Database.UserFromWebRequest(this.Request);
|
||||
if (user == null) return this.Redirect("/login");
|
||||
|
||||
// `using` weirdness here. I tried to fix it, but I couldn't.
|
||||
// The user should never see this page once they've been verified, so assert here.
|
||||
System.Diagnostics.Debug.Assert(!user.EmailAddressVerified);
|
||||
if (user.EmailAddressVerified) return this.Redirect("/");
|
||||
|
||||
// Othewise, on a release build, just silently redirect them to the landing page.
|
||||
#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);
|
||||
this.Success = await SMTPHelper.SendVerificationEmail(this.Database, user);
|
||||
|
||||
return this.Page();
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Errors;
|
||||
|
||||
|
@ -8,8 +8,5 @@ public class NotFoundPage : BaseLayout
|
|||
public NotFoundPage(Database database) : base(database)
|
||||
{}
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
|
||||
}
|
||||
public IActionResult OnGet() => this.Page();
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
@{
|
||||
Layout = "Layouts/BaseLayout";
|
||||
Model.Title = Model.Translate(GeneralStrings.LogIn);
|
||||
Model.ShowTitleInPage = false;
|
||||
}
|
||||
|
||||
<script src="https://geraintluff.github.io/sha256/sha256.min.js"></script>
|
||||
|
@ -26,61 +27,70 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Error))
|
||||
{
|
||||
<div class="ui negative message">
|
||||
<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)
|
||||
<div class="ui middle aligned center aligned grid">
|
||||
<form class="ui form" onsubmit="return onSubmit(this)" method="post">
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Error))
|
||||
{
|
||||
<a href="/register">
|
||||
<div class="ui button">
|
||||
<i class="user alternate add icon"></i>
|
||||
@Model.Translate(GeneralStrings.Register)
|
||||
<div class="ui negative message">
|
||||
<div class="header">
|
||||
@Model.Translate(GeneralStrings.Error)
|
||||
</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>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
<br/>
|
||||
<a href="/passwordResetRequest">
|
||||
<div class="ui button">
|
||||
@Model.Translate(GeneralStrings.ForgotPassword)
|
||||
|
||||
@if (ServerConfiguration.Instance.Authentication.RegistrationEnabled)
|
||||
{
|
||||
<div class="ui divider"></div>
|
||||
<a href="/register" style="text-align: center;">
|
||||
<div class="ui fluid button" style="background-color: rgba(0, 0, 0, 0)">
|
||||
@Model.Translate(GeneralStrings.Register)
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
</form>
|
||||
</form>
|
||||
</div>
|
|
@ -5,6 +5,7 @@
|
|||
@{
|
||||
Layout = "Layouts/BaseLayout";
|
||||
Model.Title = "Password Reset";
|
||||
Model.ShowTitleInPage = false;
|
||||
}
|
||||
|
||||
<script src="https://geraintluff.github.io/sha256/sha256.min.js"></script>
|
||||
|
@ -33,20 +34,39 @@
|
|||
</div>
|
||||
}
|
||||
|
||||
<div class="ui middle aligned center aligned grid">
|
||||
<form onsubmit="return onSubmit(this)" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
|
||||
<div class="ui left labeled input">
|
||||
<label for="password" class="ui blue label">Password: </label>
|
||||
<input type="password" id="password">
|
||||
<input type="hidden" id="password-submit" name="password">
|
||||
</div><br><br>
|
||||
<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="ui left labeled input">
|
||||
<label for="password" class="ui blue label">Confirm Password: </label>
|
||||
<input type="password" id="confirmPassword">
|
||||
<input type="hidden" id="confirmPassword-submit" name="confirmPassword">
|
||||
</div><br><br><br>
|
||||
<div class="ui form">
|
||||
<div class="ui field">
|
||||
<label>Password</label>
|
||||
<div class="ui left icon input">
|
||||
<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">
|
||||
<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";
|
||||
}
|
||||
|
||||
|
||||
<p>An administrator has deemed it necessary that you reset your password. Please do so.</p>
|
||||
|
||||
<a href="/passwordReset">
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using LBPUnion.ProjectLighthouse.Localization;
|
||||
using LBPUnion.ProjectLighthouse.Middlewares;
|
||||
using LBPUnion.ProjectLighthouse.Servers.Website.Middlewares;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.Localization;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
|
||||
#if !DEBUG
|
||||
|
@ -76,9 +74,10 @@ public class WebsiteStartup
|
|||
app.UseDeveloperExceptionPage();
|
||||
#endif
|
||||
|
||||
app.UseStatusCodePagesWithReExecute("/404");
|
||||
|
||||
app.UseForwardedHeaders();
|
||||
|
||||
app.UseMiddleware<HandlePageErrorMiddleware>();
|
||||
app.UseMiddleware<RequestLogMiddleware>();
|
||||
app.UseMiddleware<UserRequiredRedirectMiddleware>();
|
||||
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;
|
||||
|
||||
public class SMTPHelper
|
||||
public partial class SMTPHelper
|
||||
{
|
||||
|
||||
internal static readonly SMTPHelper Instance = new();
|
||||
|
||||
private readonly MailAddress fromAddress;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue