diff --git a/ProjectLighthouse/Database.cs b/ProjectLighthouse/Database.cs index bf8bbef8..345d0bb3 100644 --- a/ProjectLighthouse/Database.cs +++ b/ProjectLighthouse/Database.cs @@ -8,6 +8,7 @@ using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.Types.Categories; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Profiles; +using LBPUnion.ProjectLighthouse.Types.Profiles.Email; using LBPUnion.ProjectLighthouse.Types.Reports; using LBPUnion.ProjectLighthouse.Types.Reviews; using LBPUnion.ProjectLighthouse.Types.Settings; @@ -41,6 +42,8 @@ public class Database : DbContext public DbSet CustomCategories { get; set; } public DbSet Reactions { get; set; } public DbSet Reports { get; set; } + public DbSet EmailVerificationTokens { get; set; } + public DbSet EmailSetTokens { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseMySql(ServerSettings.Instance.DbConnectionString, MySqlServerVersion.LatestSupportedServerVersion); diff --git a/ProjectLighthouse/Migrations/20220301204930_AddEmailVerificationTokens.cs b/ProjectLighthouse/Migrations/20220301204930_AddEmailVerificationTokens.cs new file mode 100644 index 00000000..fb9b1541 --- /dev/null +++ b/ProjectLighthouse/Migrations/20220301204930_AddEmailVerificationTokens.cs @@ -0,0 +1,50 @@ +using LBPUnion.ProjectLighthouse; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ProjectLighthouse.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20220301204930_AddEmailVerificationTokens")] + public partial class AddEmailVerificationTokens : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "EmailVerificationToken", + columns: table => new + { + EmailVerificationTokenId = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + UserId = table.Column(type: "int", nullable: false), + EmailToken = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_EmailVerificationToken", x => x.EmailVerificationTokenId); + table.ForeignKey( + name: "FK_EmailVerificationToken_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "UserId", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_EmailVerificationToken_UserId", + table: "EmailVerificationToken", + column: "UserId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "EmailVerificationToken"); + } + } +} diff --git a/ProjectLighthouse/Migrations/20220301212120_SplitSetAndVerificationTokenTypes.cs b/ProjectLighthouse/Migrations/20220301212120_SplitSetAndVerificationTokenTypes.cs new file mode 100644 index 00000000..69619618 --- /dev/null +++ b/ProjectLighthouse/Migrations/20220301212120_SplitSetAndVerificationTokenTypes.cs @@ -0,0 +1,110 @@ +using LBPUnion.ProjectLighthouse; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ProjectLighthouse.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20220301212120_SplitSetAndVerificationTokenTypes")] + public partial class SplitSetAndVerificationTokenTypes : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "EmailVerificationToken"); + + migrationBuilder.CreateTable( + name: "EmailSetTokens", + columns: table => new + { + EmailSetTokenId = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + UserId = table.Column(type: "int", nullable: false), + EmailToken = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_EmailSetTokens", x => x.EmailSetTokenId); + table.ForeignKey( + name: "FK_EmailSetTokens_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "UserId", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "EmailVerificationTokens", + columns: table => new + { + EmailVerificationTokenId = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + UserId = table.Column(type: "int", nullable: false), + EmailToken = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_EmailVerificationTokens", x => x.EmailVerificationTokenId); + table.ForeignKey( + name: "FK_EmailVerificationTokens_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "UserId", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_EmailSetTokens_UserId", + table: "EmailSetTokens", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_EmailVerificationTokens_UserId", + table: "EmailVerificationTokens", + column: "UserId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "EmailSetTokens"); + + migrationBuilder.DropTable( + name: "EmailVerificationTokens"); + + migrationBuilder.CreateTable( + name: "EmailVerificationToken", + columns: table => new + { + EmailVerificationTokenId = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + UserId = table.Column(type: "int", nullable: false), + EmailToken = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_EmailVerificationToken", x => x.EmailVerificationTokenId); + table.ForeignKey( + name: "FK_EmailVerificationToken_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "UserId", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_EmailVerificationToken_UserId", + table: "EmailVerificationToken", + column: "UserId"); + } + } +} diff --git a/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs b/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs index 4afda237..f51b106a 100644 --- a/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs +++ b/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs @@ -452,6 +452,44 @@ namespace ProjectLighthouse.Migrations b.ToTable("Comments"); }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Profiles.Email.EmailSetToken", b => + { + b.Property("EmailSetTokenId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("EmailToken") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("EmailSetTokenId"); + + b.HasIndex("UserId"); + + b.ToTable("EmailSetTokens"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Profiles.Email.EmailVerificationToken", b => + { + b.Property("EmailVerificationTokenId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("EmailToken") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("EmailVerificationTokenId"); + + b.HasIndex("UserId"); + + b.ToTable("EmailVerificationTokens"); + }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Profiles.LastContact", b => { b.Property("UserId") @@ -923,6 +961,28 @@ namespace ProjectLighthouse.Migrations b.Navigation("Poster"); }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Profiles.Email.EmailSetToken", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Profiles.Email.EmailVerificationToken", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Reports.GriefReport", b => { b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "ReportingPlayer") diff --git a/ProjectLighthouse/Pages/LoginForm.cshtml.cs b/ProjectLighthouse/Pages/LoginForm.cshtml.cs index f2a7ff64..babb65d6 100644 --- a/ProjectLighthouse/Pages/LoginForm.cshtml.cs +++ b/ProjectLighthouse/Pages/LoginForm.cshtml.cs @@ -8,6 +8,8 @@ using LBPUnion.ProjectLighthouse.Helpers.Extensions; using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Pages.Layouts; using LBPUnion.ProjectLighthouse.Types; +using LBPUnion.ProjectLighthouse.Types.Profiles.Email; +using LBPUnion.ProjectLighthouse.Types.Settings; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -64,6 +66,23 @@ public class LoginForm : BaseLayout return this.Page(); } + if (user.EmailAddress == null && ServerSettings.Instance.SMTPEnabled) + { + Logger.Log($"User {user.Username} (id: {user.UserId}) failed to login; email not set", LoggerLevelLogin.Instance); + + EmailSetToken emailSetToken = new() + { + UserId = user.UserId, + User = user, + EmailToken = HashHelper.GenerateAuthToken(), + }; + + this.Database.EmailSetTokens.Add(emailSetToken); + await this.Database.SaveChangesAsync(); + + return this.Redirect("/login/setEmail?token=" + emailSetToken.EmailToken); + } + WebToken webToken = new() { UserId = user.UserId, diff --git a/ProjectLighthouse/Pages/RegisterForm.cshtml b/ProjectLighthouse/Pages/RegisterForm.cshtml index 025cb0f7..5573a7e7 100644 --- a/ProjectLighthouse/Pages/RegisterForm.cshtml +++ b/ProjectLighthouse/Pages/RegisterForm.cshtml @@ -43,13 +43,16 @@ -
- -
- - + @if (ServerSettings.Instance.SMTPEnabled) + { +
+ +
+ + +
-
+ }
diff --git a/ProjectLighthouse/Pages/RegisterForm.cshtml.cs b/ProjectLighthouse/Pages/RegisterForm.cshtml.cs index ab42a197..43480361 100644 --- a/ProjectLighthouse/Pages/RegisterForm.cshtml.cs +++ b/ProjectLighthouse/Pages/RegisterForm.cshtml.cs @@ -17,7 +17,6 @@ public class RegisterForm : BaseLayout {} public string Error { get; private set; } - public bool WasRegisterRequest { get; private set; } [UsedImplicitly] [SuppressMessage("ReSharper", "SpecifyStringComparison")] @@ -37,7 +36,7 @@ public class RegisterForm : BaseLayout return this.Page(); } - if (string.IsNullOrWhiteSpace(emailAddress)) + if (string.IsNullOrWhiteSpace(emailAddress) && ServerSettings.Instance.SMTPEnabled) { this.Error = "Email address field is required."; return this.Page(); diff --git a/ProjectLighthouse/Pages/SetEmailForm.cshtml b/ProjectLighthouse/Pages/SetEmailForm.cshtml new file mode 100644 index 00000000..3bac01b4 --- /dev/null +++ b/ProjectLighthouse/Pages/SetEmailForm.cshtml @@ -0,0 +1,29 @@ +@page "/login/setEmail" +@using LBPUnion.ProjectLighthouse.Types.Settings +@model LBPUnion.ProjectLighthouse.Pages.SetEmailForm + +@{ + Layout = "Layouts/BaseLayout"; + Model.Title = "Set an Email Address"; +} + +

This instance requires email verification. As your account was created before this was a requirement, you must now set an email for your account before continuing.

+ +
+ @Html.AntiForgeryToken() + + @if (ServerSettings.Instance.SMTPEnabled) + { +
+ +
+ + +
+ + +
+ } + + +
\ No newline at end of file diff --git a/ProjectLighthouse/Pages/SetEmailForm.cshtml.cs b/ProjectLighthouse/Pages/SetEmailForm.cshtml.cs new file mode 100644 index 00000000..bd46778e --- /dev/null +++ b/ProjectLighthouse/Pages/SetEmailForm.cshtml.cs @@ -0,0 +1,51 @@ +#nullable enable +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.Pages.Layouts; +using LBPUnion.ProjectLighthouse.Types.Profiles.Email; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Pages; + +public class SetEmailForm : BaseLayout +{ + public SetEmailForm(Database database) : base(database) + {} + + public EmailSetToken EmailToken; + + public async Task OnGet(string? token = null) + { + if (token == null) return this.Redirect("/login"); + + EmailSetToken? emailToken = await this.Database.EmailSetTokens.FirstOrDefaultAsync(t => t.EmailToken == token); + if (emailToken == null) return this.Redirect("/login"); + + this.EmailToken = emailToken; + + return this.Page(); + } + + public async Task OnPost(string emailAddress, string token) + { + EmailSetToken? emailToken = await this.Database.EmailSetTokens.Include(t => t.User).FirstOrDefaultAsync(t => t.EmailToken == token); + if (emailToken == null) return this.Redirect("/login"); + + emailToken.User.EmailAddress = emailAddress; + this.Database.EmailSetTokens.Remove(emailToken); + + EmailVerificationToken emailVerifyToken = new() + { + UserId = emailToken.UserId, + User = emailToken.User, + EmailToken = HashHelper.GenerateAuthToken(), + }; + + this.Database.EmailVerificationTokens.Add(emailVerifyToken); + + await this.Database.SaveChangesAsync(); + + return this.Redirect("/login/verify?token=" + emailVerifyToken.EmailToken); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/ProjectLighthouse.csproj b/ProjectLighthouse/ProjectLighthouse.csproj index 13def370..2368f804 100644 --- a/ProjectLighthouse/ProjectLighthouse.csproj +++ b/ProjectLighthouse/ProjectLighthouse.csproj @@ -51,6 +51,10 @@ + + + + diff --git a/ProjectLighthouse/Types/Profiles/Email/EmailSetToken.cs b/ProjectLighthouse/Types/Profiles/Email/EmailSetToken.cs new file mode 100644 index 00000000..0b3258af --- /dev/null +++ b/ProjectLighthouse/Types/Profiles/Email/EmailSetToken.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace LBPUnion.ProjectLighthouse.Types.Profiles.Email; + +public class EmailSetToken +{ + [Key] + public int EmailSetTokenId { get; set; } + + public int UserId { get; set; } + + [ForeignKey(nameof(UserId))] + public User User { get; set; } + + public string EmailToken { get; set; } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Profiles/Email/EmailVerificationToken.cs b/ProjectLighthouse/Types/Profiles/Email/EmailVerificationToken.cs new file mode 100644 index 00000000..0604d2e5 --- /dev/null +++ b/ProjectLighthouse/Types/Profiles/Email/EmailVerificationToken.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace LBPUnion.ProjectLighthouse.Types.Profiles.Email; + +public class EmailVerificationToken +{ + [Key] + public int EmailVerificationTokenId { get; set; } + + public int UserId { get; set; } + + [ForeignKey(nameof(UserId))] + public User User { get; set; } + + public string EmailToken { get; set; } +} \ No newline at end of file