feat: add contact blacklist
This commit is contained in:
parent
25822c6577
commit
1f6825c9df
47
OliverBooth/Controllers/FormattedBlacklist.cs
Normal file
47
OliverBooth/Controllers/FormattedBlacklist.cs
Normal file
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FormattedBlacklistController" /> class.
|
||||
/// </summary>
|
||||
/// <param name="contactService">The <see cref="IContactService" />.</param>
|
||||
public FormattedBlacklistController(IContactService contactService)
|
||||
{
|
||||
_contactService = contactService;
|
||||
}
|
||||
|
||||
[HttpGet("{format}")]
|
||||
public IActionResult OnGet([FromRoute] string format)
|
||||
{
|
||||
IReadOnlyCollection<IBlacklistEntry> 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();
|
||||
}
|
||||
}
|
75
OliverBooth/Data/Web/BlacklistEntry.cs
Normal file
75
OliverBooth/Data/Web/BlacklistEntry.cs
Normal file
@ -0,0 +1,75 @@
|
||||
namespace OliverBooth.Data.Web;
|
||||
|
||||
/// <inheritdoc cref="IBlacklistEntry"/>
|
||||
internal sealed class BlacklistEntry : IEquatable<BlacklistEntry>, IBlacklistEntry
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string EmailAddress { get; internal set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name { get; internal set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Reason { get; internal set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether two instances of <see cref="BlacklistEntry" /> are equal.
|
||||
/// </summary>
|
||||
/// <param name="left">The first instance of <see cref="BlacklistEntry" /> to compare.</param>
|
||||
/// <param name="right">The second instance of <see cref="BlacklistEntry" /> to compare.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true" /> if <paramref name="left" /> and <paramref name="right" /> are equal; otherwise,
|
||||
/// <see langword="false" />.
|
||||
/// </returns>
|
||||
public static bool operator ==(BlacklistEntry? left, BlacklistEntry? right) => Equals(left, right);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether two instances of <see cref="BlacklistEntry" /> are not equal.
|
||||
/// </summary>
|
||||
/// <param name="left">The first instance of <see cref="BlacklistEntry" /> to compare.</param>
|
||||
/// <param name="right">The second instance of <see cref="BlacklistEntry" /> to compare.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true" /> if <paramref name="left" /> and <paramref name="right" /> are not equal; otherwise,
|
||||
/// <see langword="false" />.
|
||||
/// </returns>
|
||||
public static bool operator !=(BlacklistEntry? left, BlacklistEntry? right) => !(left == right);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether this instance of <see cref="BlacklistEntry" /> is equal to another
|
||||
/// instance.
|
||||
/// </summary>
|
||||
/// <param name="other">An instance to compare with this instance.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true" /> if <paramref name="other" /> is equal to this instance; otherwise,
|
||||
/// <see langword="false" />.
|
||||
/// </returns>
|
||||
public bool Equals(BlacklistEntry? other)
|
||||
{
|
||||
if (ReferenceEquals(null, other)) return false;
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
return EmailAddress.Equals(other.EmailAddress);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether this instance is equal to a specified object.
|
||||
/// </summary>
|
||||
/// <param name="obj">An object to compare with this instance.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true" /> if <paramref name="obj" /> is an instance of <see cref="BlacklistEntry" /> and
|
||||
/// equals the value of this instance; otherwise, <see langword="false" />.
|
||||
/// </returns>
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return ReferenceEquals(this, obj) || obj is BlacklistEntry other && Equals(other);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the hash code for this instance.
|
||||
/// </summary>
|
||||
/// <returns>The hash code.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
// ReSharper disable once NonReadonlyMemberInGetHashCode
|
||||
return EmailAddress.GetHashCode();
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace OliverBooth.Data.Web.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the configuration for the <see cref="BlacklistEntry" /> entity.
|
||||
/// </summary>
|
||||
internal sealed class BlacklistEntryConfiguration : IEntityTypeConfiguration<BlacklistEntry>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Configure(EntityTypeBuilder<BlacklistEntry> 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();
|
||||
}
|
||||
}
|
25
OliverBooth/Data/Web/IBlacklistEntry.cs
Normal file
25
OliverBooth/Data/Web/IBlacklistEntry.cs
Normal file
@ -0,0 +1,25 @@
|
||||
namespace OliverBooth.Data.Web;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an entry in the blacklist.
|
||||
/// </summary>
|
||||
public interface IBlacklistEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the email address of the entry.
|
||||
/// </summary>
|
||||
/// <value>The email address of the entry.</value>
|
||||
string EmailAddress { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the entry.
|
||||
/// </summary>
|
||||
/// <value>The name of the entry.</value>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reason for the entry.
|
||||
/// </summary>
|
||||
/// <value>The reason for the entry.</value>
|
||||
string Reason { get; }
|
||||
}
|
@ -25,6 +25,12 @@ internal sealed class WebContext : DbContext
|
||||
/// <value>The collection of books.</value>
|
||||
public DbSet<Book> Books { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of blacklist entries in the database.
|
||||
/// </summary>
|
||||
/// <value>The collection of blacklist entries.</value>
|
||||
public DbSet<BlacklistEntry> ContactBlacklist { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of projects in the database.
|
||||
/// </summary>
|
||||
@ -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());
|
||||
|
36
OliverBooth/Pages/Contact/Blacklist.cshtml
Normal file
36
OliverBooth/Pages/Contact/Blacklist.cshtml
Normal file
@ -0,0 +1,36 @@
|
||||
@page
|
||||
@using OliverBooth.Data.Web
|
||||
@using OliverBooth.Services
|
||||
@inject IContactService ContactService
|
||||
@{
|
||||
ViewData["Title"] = "Blacklist";
|
||||
}
|
||||
|
||||
<h1 class="display-4">Contact Blacklist</h1>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You can view this list in JSON format
|
||||
<a asp-controller="FormattedBlacklist" asp-action="OnGet" asp-route-format="json">here</a>,
|
||||
or in CSV format
|
||||
<a asp-controller="FormattedBlacklist" asp-action="OnGet" asp-route-format="csv">here</a>.
|
||||
</p>
|
||||
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Name / Email</th>
|
||||
<th>Reason</th>
|
||||
</tr>
|
||||
|
||||
@foreach (IBlacklistEntry entry in ContactService.GetBlacklist())
|
||||
{
|
||||
<tr>
|
||||
<td>@entry.Name <@entry.EmailAddress></td>
|
||||
<td>@entry.Reason</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
@ -12,26 +12,12 @@
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<p class="lead"><i class="fa-solid fa-triangle-exclamation"></i> Spam warning</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
I am politely asking that you respect my inbox and keep your unsolicited advertising away. This is a simple
|
||||
request.
|
||||
</p>
|
||||
<p>
|
||||
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
|
||||
<strong>will</strong> 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
|
||||
<strong>will</strong> be publicly shamed for it.
|
||||
</p>
|
||||
<p>
|
||||
This <strong>does not apply</strong> to legitimate businesses who are offering something genuine
|
||||
<span style="text-decoration: underline;">to me specifically</span>, 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
|
||||
<strong>will</strong> block your email address, and I will add your name to my public blacklist of spammers,
|
||||
which you can find <a asp-page="/Contact/Blacklist">here</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
@ -32,6 +32,7 @@ builder.Services.AddSingleton(provider => new MarkdownPipelineBuilder()
|
||||
|
||||
builder.Services.AddDbContextFactory<BlogContext>();
|
||||
builder.Services.AddDbContextFactory<WebContext>();
|
||||
builder.Services.AddSingleton<IContactService, ContactService>();
|
||||
builder.Services.AddSingleton<ITemplateService, TemplateService>();
|
||||
builder.Services.AddSingleton<IBlogPostService, BlogPostService>();
|
||||
builder.Services.AddSingleton<IBlogUserService, BlogUserService>();
|
||||
|
26
OliverBooth/Services/ContactService.cs
Normal file
26
OliverBooth/Services/ContactService.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OliverBooth.Data.Web;
|
||||
|
||||
namespace OliverBooth.Services;
|
||||
|
||||
/// <inheritdoc cref="IContactService" />
|
||||
internal sealed class ContactService : IContactService
|
||||
{
|
||||
private readonly IDbContextFactory<WebContext> _dbContextFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ContactService" /> class.
|
||||
/// </summary>
|
||||
/// <param name="dbContextFactory">The <see cref="IDbContextFactory{TContext}" />.</param>
|
||||
public ContactService(IDbContextFactory<WebContext> dbContextFactory)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyCollection<IBlacklistEntry> GetBlacklist()
|
||||
{
|
||||
using WebContext context = _dbContextFactory.CreateDbContext();
|
||||
return context.ContactBlacklist.OrderBy(b => b.EmailAddress).ToArray();
|
||||
}
|
||||
}
|
14
OliverBooth/Services/IContactService.cs
Normal file
14
OliverBooth/Services/IContactService.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using OliverBooth.Data.Web;
|
||||
|
||||
namespace OliverBooth.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a service for managing contact information.
|
||||
/// </summary>
|
||||
public interface IContactService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the blacklist.
|
||||
/// </summary>
|
||||
IReadOnlyCollection<IBlacklistEntry> GetBlacklist();
|
||||
}
|
Loading…
Reference in New Issue
Block a user