Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions EmailManager.sln
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmailManager.Shared", "src\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmailManager.BusinessLayer", "src\EmailManager.BusinessLayer\EmailManager.BusinessLayer.csproj", "{D7DF698B-95A0-4515-A88F-65DA674E97F3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmailManager.DataAccessLayer", "src\EmailManager.DataAccessLayer\EmailManager.DataAccessLayer.csproj", "{EEA8A36B-9EA6-42B5-A371-0E406A48E3FE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -34,6 +36,10 @@ Global
{D7DF698B-95A0-4515-A88F-65DA674E97F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D7DF698B-95A0-4515-A88F-65DA674E97F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D7DF698B-95A0-4515-A88F-65DA674E97F3}.Release|Any CPU.Build.0 = Release|Any CPU
{EEA8A36B-9EA6-42B5-A371-0E406A48E3FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EEA8A36B-9EA6-42B5-A371-0E406A48E3FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EEA8A36B-9EA6-42B5-A371-0E406A48E3FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EEA8A36B-9EA6-42B5-A371-0E406A48E3FE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Security.Claims;
using EmailManager.DataAccessLayer;
using Microsoft.EntityFrameworkCore;
using SimpleAuthentication.ApiKey;

namespace EmailManager.BusinessLayer.Authentication;

public class SubscriptionValidator(ApplicationDbContext dbContext, TimeProvider timeProvider) : IApiKeyValidator
{
public async Task<ApiKeyValidationResult> ValidateAsync(string apiKey)
{
var subscription = await dbContext.Subscriptions.FirstOrDefaultAsync(s => s.ApiKey == apiKey);
if (subscription is null)
{
return ApiKeyValidationResult.Fail("API key is invalid");
}

var now = timeProvider.GetUtcNow();
if (subscription.ValidFrom > now || subscription.ValidTo < now)
{
return ApiKeyValidationResult.Fail("API key is expired");
}

var claims = new List<Claim>
{
new("requests_per_window", subscription.RequestsPerWindow.ToString()),
new("window_minutes", subscription.WindowMinutes.ToString())
};

return ApiKeyValidationResult.Success(subscription.UserName, claims);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
<ItemGroup>
<PackageReference Include="FluentValidation" Version="12.0.0" />
<PackageReference Include="MailKit" Version="4.13.0" />
<PackageReference Include="SimpleAuthenticationTools.Abstractions" Version="3.0.11" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\EmailManager.DataAccessLayer\EmailManager.DataAccessLayer.csproj" />
<ProjectReference Include="..\EmailManager.Shared\EmailManager.Shared.csproj" />
</ItemGroup>

Expand Down
57 changes: 57 additions & 0 deletions src/EmailManager.DataAccessLayer/ApplicationDbContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System.Reflection;
using EmailManager.DataAccessLayer.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace EmailManager.DataAccessLayer;

public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IConfiguration configuration) : DbContext(options)
{
public DbSet<Subscription> Subscriptions { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);

var administratorUserName = configuration.GetValue<string>("AppSettings:AdministratorUserName")!;
var administratorApiKey = configuration.GetValue<string>("AppSettings:AdministratorApiKey")!;

optionsBuilder.UseSeeding((context, _) =>
{
var subscription = context.Set<Subscription>().FirstOrDefault(s => s.UserName == administratorUserName);
CheckSubscription(context, subscription, administratorUserName, administratorApiKey);

context.SaveChanges();
});

optionsBuilder.UseAsyncSeeding(async (context, _, cancellationToken) =>
{
var subscription = await context.Set<Subscription>().FirstOrDefaultAsync(s => s.UserName == administratorUserName, cancellationToken: cancellationToken);
CheckSubscription(context, subscription, administratorUserName, administratorApiKey);

await context.SaveChangesAsync(cancellationToken);
});

static void CheckSubscription(DbContext context, Subscription? subscription, string administratorUserName, string administratorApiKey)
{
if (subscription is null)
{
context.Add(new Subscription
{
UserName = administratorUserName,
ApiKey = administratorApiKey,
RequestsPerWindow = 10,
WindowMinutes = 1,
ValidFrom = DateTimeOffset.MinValue,
ValidTo = DateTimeOffset.MaxValue
});
}
}
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
base.OnModelCreating(modelBuilder);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using EmailManager.DataAccessLayer.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace EmailManager.DataAccessLayer.Configurations;

internal class SubscriptionConfiguration : IEntityTypeConfiguration<Subscription>
{
public void Configure(EntityTypeBuilder<Subscription> builder)
{
builder.ToTable("Subscriptions");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).HasDefaultValueSql("(newsequentialid())");

builder.HasIndex(e => e.ApiKey, "IX_Subscriptions_ApiKey").IsUnique();
builder.HasIndex(e => e.UserName, "IX_Subscriptions_UserName").IsUnique();

builder.Property(e => e.ApiKey).HasMaxLength(512).IsUnicode(false);
builder.Property(e => e.UserName).HasMaxLength(255);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.8" />
</ItemGroup>



</Project>
18 changes: 18 additions & 0 deletions src/EmailManager.DataAccessLayer/Entities/Subscription.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace EmailManager.DataAccessLayer.Entities;

public class Subscription
{
public Guid Id { get; set; }

public string UserName { get; set; } = null!;

public string ApiKey { get; set; } = null!;

public DateTimeOffset ValidFrom { get; set; }

public DateTimeOffset ValidTo { get; set; }

public int RequestsPerWindow { get; set; }

public int WindowMinutes { get; set; }
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace EmailManager.DataAccessLayer.Migrations
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Subscriptions",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false, defaultValueSql: "(newsequentialid())"),
UserName = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
ApiKey = table.Column<string>(type: "varchar(512)", unicode: false, maxLength: 512, nullable: false),
ValidFrom = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
ValidTo = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
RequestsPerWindow = table.Column<int>(type: "int", nullable: false),
WindowMinutes = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Subscriptions", x => x.Id);
});

migrationBuilder.CreateIndex(
name: "IX_Subscriptions_ApiKey",
table: "Subscriptions",
column: "ApiKey",
unique: true);

migrationBuilder.CreateIndex(
name: "IX_Subscriptions_UserName",
table: "Subscriptions",
column: "UserName",
unique: true);
}

/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Subscriptions");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// <auto-generated />
using System;
using EmailManager.DataAccessLayer;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;

#nullable disable

namespace EmailManager.DataAccessLayer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 128);

SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);

modelBuilder.Entity("EmailManager.DataAccessLayer.Entities.Subscription", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier")
.HasDefaultValueSql("(newsequentialid())");

b.Property<string>("ApiKey")
.IsRequired()
.HasMaxLength(512)
.IsUnicode(false)
.HasColumnType("varchar(512)");

b.Property<int>("RequestsPerWindow")
.HasColumnType("int");

b.Property<string>("UserName")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");

b.Property<DateTimeOffset>("ValidFrom")
.HasColumnType("datetimeoffset");

b.Property<DateTimeOffset>("ValidTo")
.HasColumnType("datetimeoffset");

b.Property<int>("WindowMinutes")
.HasColumnType("int");

b.HasKey("Id");

b.HasIndex(new[] { "ApiKey" }, "IX_Subscriptions_ApiKey")
.IsUnique();

b.HasIndex(new[] { "UserName" }, "IX_Subscriptions_UserName")
.IsUnique();

b.ToTable("Subscriptions", (string)null);
});
#pragma warning restore 612, 618
}
}
}
Loading