From 1f6825c9df148cb65cc365c5275854ab8532f363 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Fri, 22 Dec 2023 14:26:18 +0000 Subject: [PATCH] feat: add contact blacklist --- OliverBooth/Controllers/FormattedBlacklist.cs | 47 ++++++++++++ OliverBooth/Data/Web/BlacklistEntry.cs | 75 +++++++++++++++++++ .../BlacklistEntryConfiguration.cs | 21 ++++++ OliverBooth/Data/Web/IBlacklistEntry.cs | 25 +++++++ OliverBooth/Data/Web/WebContext.cs | 7 ++ OliverBooth/Pages/Contact/Blacklist.cshtml | 36 +++++++++ OliverBooth/Pages/Contact/Index.cshtml | 22 +----- OliverBooth/Program.cs | 1 + OliverBooth/Services/ContactService.cs | 26 +++++++ OliverBooth/Services/IContactService.cs | 14 ++++ 10 files changed, 256 insertions(+), 18 deletions(-) create mode 100644 OliverBooth/Controllers/FormattedBlacklist.cs create mode 100644 OliverBooth/Data/Web/BlacklistEntry.cs create mode 100644 OliverBooth/Data/Web/Configuration/BlacklistEntryConfiguration.cs create mode 100644 OliverBooth/Data/Web/IBlacklistEntry.cs create mode 100644 OliverBooth/Pages/Contact/Blacklist.cshtml create mode 100644 OliverBooth/Services/ContactService.cs create mode 100644 OliverBooth/Services/IContactService.cs diff --git a/OliverBooth/Controllers/FormattedBlacklist.cs b/OliverBooth/Controllers/FormattedBlacklist.cs new file mode 100644 index 0000000..9f29076 --- /dev/null +++ b/OliverBooth/Controllers/FormattedBlacklist.cs @@ -0,0 +1,47 @@ +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using OliverBooth.Data.Web; +using OliverBooth.Services; + +namespace OliverBooth.Controllers; + +[Controller] +[Route("contact/blacklist/formatted")] +public class FormattedBlacklistController : Controller +{ + private readonly IContactService _contactService; + + /// + /// Initializes a new instance of the class. + /// + /// The . + public FormattedBlacklistController(IContactService contactService) + { + _contactService = contactService; + } + + [HttpGet("{format}")] + public IActionResult OnGet([FromRoute] string format) + { + IReadOnlyCollection blacklist = _contactService.GetBlacklist(); + + switch (format) + { + case "json": + return Json(blacklist); + + case "csv": + var builder = new StringBuilder(); + builder.AppendLine("EmailAddress,Name,Reason"); + foreach (IBlacklistEntry entry in blacklist) + { + builder.AppendLine($"{entry.EmailAddress},{entry.Name},{entry.Reason}"); + } + + return Content(builder.ToString(), "text/csv", Encoding.UTF8); + } + + return NotFound(); + } +} \ No newline at end of file diff --git a/OliverBooth/Data/Web/BlacklistEntry.cs b/OliverBooth/Data/Web/BlacklistEntry.cs new file mode 100644 index 0000000..374d9c2 --- /dev/null +++ b/OliverBooth/Data/Web/BlacklistEntry.cs @@ -0,0 +1,75 @@ +namespace OliverBooth.Data.Web; + +/// +internal sealed class BlacklistEntry : IEquatable, IBlacklistEntry +{ + /// + public string EmailAddress { get; internal set; } = string.Empty; + + /// + public string Name { get; internal set; } = string.Empty; + + /// + public string Reason { get; internal set; } = string.Empty; + + /// + /// Returns a value indicating whether two instances of are equal. + /// + /// The first instance of to compare. + /// The second instance of to compare. + /// + /// if and are equal; otherwise, + /// . + /// + public static bool operator ==(BlacklistEntry? left, BlacklistEntry? right) => Equals(left, right); + + /// + /// Returns a value indicating whether two instances of are not equal. + /// + /// The first instance of to compare. + /// The second instance of to compare. + /// + /// if and are not equal; otherwise, + /// . + /// + public static bool operator !=(BlacklistEntry? left, BlacklistEntry? right) => !(left == right); + + /// + /// Returns a value indicating whether this instance of is equal to another + /// instance. + /// + /// An instance to compare with this instance. + /// + /// if is equal to this instance; otherwise, + /// . + /// + public bool Equals(BlacklistEntry? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return EmailAddress.Equals(other.EmailAddress); + } + + /// + /// Returns a value indicating whether this instance is equal to a specified object. + /// + /// An object to compare with this instance. + /// + /// if is an instance of and + /// equals the value of this instance; otherwise, . + /// + public override bool Equals(object? obj) + { + return ReferenceEquals(this, obj) || obj is BlacklistEntry other && Equals(other); + } + + /// + /// Gets the hash code for this instance. + /// + /// The hash code. + public override int GetHashCode() + { + // ReSharper disable once NonReadonlyMemberInGetHashCode + return EmailAddress.GetHashCode(); + } +} diff --git a/OliverBooth/Data/Web/Configuration/BlacklistEntryConfiguration.cs b/OliverBooth/Data/Web/Configuration/BlacklistEntryConfiguration.cs new file mode 100644 index 0000000..6d6f570 --- /dev/null +++ b/OliverBooth/Data/Web/Configuration/BlacklistEntryConfiguration.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace OliverBooth.Data.Web.Configuration; + +/// +/// Represents the configuration for the entity. +/// +internal sealed class BlacklistEntryConfiguration : IEntityTypeConfiguration +{ + /// + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("ContactBlacklist"); + builder.HasKey(entry => entry.EmailAddress); + + builder.Property(entry => entry.EmailAddress).IsRequired(); + builder.Property(entry => entry.Name).IsRequired(); + builder.Property(entry => entry.Reason).IsRequired(); + } +} diff --git a/OliverBooth/Data/Web/IBlacklistEntry.cs b/OliverBooth/Data/Web/IBlacklistEntry.cs new file mode 100644 index 0000000..00516fb --- /dev/null +++ b/OliverBooth/Data/Web/IBlacklistEntry.cs @@ -0,0 +1,25 @@ +namespace OliverBooth.Data.Web; + +/// +/// Represents an entry in the blacklist. +/// +public interface IBlacklistEntry +{ + /// + /// Gets the email address of the entry. + /// + /// The email address of the entry. + string EmailAddress { get; } + + /// + /// Gets the name of the entry. + /// + /// The name of the entry. + string Name { get; } + + /// + /// Gets the reason for the entry. + /// + /// The reason for the entry. + string Reason { get; } +} diff --git a/OliverBooth/Data/Web/WebContext.cs b/OliverBooth/Data/Web/WebContext.cs index ab1141e..151b24a 100644 --- a/OliverBooth/Data/Web/WebContext.cs +++ b/OliverBooth/Data/Web/WebContext.cs @@ -25,6 +25,12 @@ internal sealed class WebContext : DbContext /// The collection of books. public DbSet Books { get; private set; } = null!; + /// + /// Gets the collection of blacklist entries in the database. + /// + /// The collection of blacklist entries. + public DbSet ContactBlacklist { get; private set; } = null!; + /// /// Gets the collection of projects in the database. /// @@ -53,6 +59,7 @@ internal sealed class WebContext : DbContext protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder.ApplyConfiguration(new BlacklistEntryConfiguration()); modelBuilder.ApplyConfiguration(new BookConfiguration()); modelBuilder.ApplyConfiguration(new ProjectConfiguration()); modelBuilder.ApplyConfiguration(new TemplateConfiguration()); diff --git a/OliverBooth/Pages/Contact/Blacklist.cshtml b/OliverBooth/Pages/Contact/Blacklist.cshtml new file mode 100644 index 0000000..7b32189 --- /dev/null +++ b/OliverBooth/Pages/Contact/Blacklist.cshtml @@ -0,0 +1,36 @@ +@page +@using OliverBooth.Data.Web +@using OliverBooth.Services +@inject IContactService ContactService +@{ + ViewData["Title"] = "Blacklist"; +} + +

Contact Blacklist

+

+ Below is a list of email addresses that have been blocked from contacting me. This list is public so that others may + also block these addresses if they wish. Any email address that contains an asterisk (*) is a wildcard, meaning that + any email address that matches the pattern will be blocked. +

+ +

+ You can view this list in JSON format + here, + or in CSV format + here. +

+ + + + + + + + @foreach (IBlacklistEntry entry in ContactService.GetBlacklist()) + { + + + + + } +
Name / EmailReason
@entry.Name <@entry.EmailAddress>@entry.Reason
\ No newline at end of file diff --git a/OliverBooth/Pages/Contact/Index.cshtml b/OliverBooth/Pages/Contact/Index.cshtml index f4dabf0..9b98c6e 100644 --- a/OliverBooth/Pages/Contact/Index.cshtml +++ b/OliverBooth/Pages/Contact/Index.cshtml @@ -12,26 +12,12 @@

Spam warning

-

- In the short time that this site has been live, I have received a considerable amount of spam that I suspect is - automated. As such, I added background reCaptcha validation to this form to minimise the amount of - bot-written junk I receive. However, some spam still gets through - that means there are humans reading this. -

I am politely asking that you respect my inbox and keep your unsolicited advertising away. This is a simple - request. -

-

- If you send me any kind of spam after this, you have demonstrated that you do not have the basic human decency - to respect my wishes or my privacy, and you have lost the privilege for me to respect yours. I - will block your email address and report you to your email provider. I will also begin to - maintain a public blacklist of spammers, so that others may do the same. You will lose all credibility, and you - will be publicly shamed for it. -

-

- This does not apply to legitimate businesses who are offering something genuine - to me specifically, nor to individuals who are interested in my - services. + request. If you send me any kind of spam after this, you have demonstrated that you do not have the basic human + decency to respect my wishes or my privacy, and you have lost the privilege for me to respect yours. I + will block your email address, and I will add your name to my public blacklist of spammers, + which you can find here.

diff --git a/OliverBooth/Program.cs b/OliverBooth/Program.cs index 5dae0a1..b471977 100644 --- a/OliverBooth/Program.cs +++ b/OliverBooth/Program.cs @@ -32,6 +32,7 @@ builder.Services.AddSingleton(provider => new MarkdownPipelineBuilder() builder.Services.AddDbContextFactory(); builder.Services.AddDbContextFactory(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/OliverBooth/Services/ContactService.cs b/OliverBooth/Services/ContactService.cs new file mode 100644 index 0000000..de2b591 --- /dev/null +++ b/OliverBooth/Services/ContactService.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore; +using OliverBooth.Data.Web; + +namespace OliverBooth.Services; + +/// +internal sealed class ContactService : IContactService +{ + private readonly IDbContextFactory _dbContextFactory; + + /// + /// Initializes a new instance of the class. + /// + /// The . + public ContactService(IDbContextFactory dbContextFactory) + { + _dbContextFactory = dbContextFactory; + } + + /// + public IReadOnlyCollection GetBlacklist() + { + using WebContext context = _dbContextFactory.CreateDbContext(); + return context.ContactBlacklist.OrderBy(b => b.EmailAddress).ToArray(); + } +} diff --git a/OliverBooth/Services/IContactService.cs b/OliverBooth/Services/IContactService.cs new file mode 100644 index 0000000..800a3e7 --- /dev/null +++ b/OliverBooth/Services/IContactService.cs @@ -0,0 +1,14 @@ +using OliverBooth.Data.Web; + +namespace OliverBooth.Services; + +/// +/// Represents a service for managing contact information. +/// +public interface IContactService +{ + /// + /// Gets the blacklist. + /// + IReadOnlyCollection GetBlacklist(); +}