Add project files
This commit is contained in:
parent
4dafed3553
commit
8cf01ead74
40 changed files with 3967 additions and 0 deletions
26
.dockerignore
Normal file
26
.dockerignore
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/.idea
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
/StalwartSDK/StalwartClient.cs
|
||||
40
.forgejo/workflows/package-debug.yaml
Normal file
40
.forgejo/workflows/package-debug.yaml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
pull_request:
|
||||
types: [ opened, synchronize, reopened ]
|
||||
jobs:
|
||||
publish_debug:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: ghcr.io/catthehacker/ubuntu:act-22.04
|
||||
steps:
|
||||
- name: Extract metadata (tags, labels)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
git.spgrn.com/${{ env.GITHUB_REPOSITORY }}-web
|
||||
tags: |
|
||||
type=ref,event=pr
|
||||
type=ref,event=branch
|
||||
type=sha
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: git.spgrn.com
|
||||
username: seang96
|
||||
password: ${{ secrets.TOKEN }}
|
||||
- name: Build Web Application
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./StalwartSimpleLoginMiddleware/Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
BUILD_CONFIGURATION=Debug
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
40
.forgejo/workflows/package.yaml
Normal file
40
.forgejo/workflows/package.yaml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: ghcr.io/catthehacker/ubuntu:act-22.04
|
||||
steps:
|
||||
- name: Extract metadata (tags, labels)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
git.spgrn.com/${{ env.GITHUB_REPOSITORY }}-web
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=raw,value=latest
|
||||
type=sha
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: git.spgrn.com
|
||||
username: seang96
|
||||
password: ${{ secrets.TOKEN }}
|
||||
- name: Build Web Application
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./StalwartSimpleLoginMiddleware/Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -77,3 +77,10 @@ fabric.properties
|
|||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
bin/
|
||||
obj/
|
||||
/packages/
|
||||
riderModule.iml
|
||||
/_ReSharper.Caches/
|
||||
/StalwartSDK/StalwartClient.cs
|
||||
launchSettings.json
|
||||
22
Stalwart Simple Login Middleware.sln
Normal file
22
Stalwart Simple Login Middleware.sln
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StalwartSimpleLoginMiddleware", "StalwartSimpleLoginMiddleware\StalwartSimpleLoginMiddleware.csproj", "{AFC6627E-CA72-440D-B405-384AB62F12C2}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StalwartSDK", "StalwartSDK\StalwartSDK.csproj", "{AD805E73-59A5-402C-8EE3-41113DDE86E7}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{AFC6627E-CA72-440D-B405-384AB62F12C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AFC6627E-CA72-440D-B405-384AB62F12C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AFC6627E-CA72-440D-B405-384AB62F12C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AFC6627E-CA72-440D-B405-384AB62F12C2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AD805E73-59A5-402C-8EE3-41113DDE86E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AD805E73-59A5-402C-8EE3-41113DDE86E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AD805E73-59A5-402C-8EE3-41113DDE86E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AD805E73-59A5-402C-8EE3-41113DDE86E7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
23
StalwartSDK/StalwartClientFactory.cs
Normal file
23
StalwartSDK/StalwartClientFactory.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
using System.Net.Http.Headers;
|
||||
|
||||
namespace AdOrbitSDK;
|
||||
|
||||
public partial class Client
|
||||
{
|
||||
private readonly string _apiKey;
|
||||
|
||||
public Client(HttpClient httpClient, string apiKey)
|
||||
: this(httpClient)
|
||||
{
|
||||
_apiKey = apiKey;
|
||||
}
|
||||
|
||||
partial void PrepareRequest(HttpClient client, HttpRequestMessage request, string url)
|
||||
{
|
||||
if (client.BaseAddress == null) throw new NullReferenceException("Base address cannot be null");
|
||||
|
||||
var authHeader = new AuthenticationHeaderValue("Bearer", _apiKey);
|
||||
|
||||
request.Headers.Authorization = authHeader;
|
||||
}
|
||||
}
|
||||
27
StalwartSDK/StalwartSDK.csproj
Normal file
27
StalwartSDK/StalwartSDK.csproj
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
|
||||
<PackageReference Include="NSwag.MSBuild" Version="14.2.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="NSwag" AfterTargets="CoreCompile">
|
||||
<Exec WorkingDirectory="$(ProjectDir)"
|
||||
Command="$(NSwagExe_Net80) run nswag.json"
|
||||
ContinueOnError="false"
|
||||
/>
|
||||
</Target>
|
||||
<Target Name="CleanGeneratedFiles" AfterTargets="CoreClean">
|
||||
<Delete Files="$(ProjectDir)\StalwartClient.cs"/>
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
103
StalwartSDK/nswag.json
Normal file
103
StalwartSDK/nswag.json
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
{
|
||||
"runtime": "Net80",
|
||||
"defaultVariables": null,
|
||||
"documentGenerator": {
|
||||
"fromDocument": {
|
||||
"url": "openapi.yml",
|
||||
"output": null,
|
||||
"newLineBehavior": "Auto"
|
||||
}
|
||||
},
|
||||
"codeGenerators": {
|
||||
"openApiToCSharpClient": {
|
||||
"clientBaseClass": null,
|
||||
"configurationClass": null,
|
||||
"generateClientClasses": true,
|
||||
"suppressClientClassesOutput": false,
|
||||
"generateClientInterfaces": false,
|
||||
"suppressClientInterfacesOutput": false,
|
||||
"clientBaseInterface": null,
|
||||
"injectHttpClient": true,
|
||||
"disposeHttpClient": true,
|
||||
"protectedMethods": [],
|
||||
"generateExceptionClasses": true,
|
||||
"exceptionClass": "ApiException",
|
||||
"wrapDtoExceptions": true,
|
||||
"useHttpClientCreationMethod": false,
|
||||
"httpClientType": "System.Net.Http.HttpClient",
|
||||
"useHttpRequestMessageCreationMethod": false,
|
||||
"useBaseUrl": false,
|
||||
"generateBaseUrlProperty": true,
|
||||
"generateSyncMethods": false,
|
||||
"generatePrepareRequestAndProcessResponseAsAsyncMethods": false,
|
||||
"exposeJsonSerializerSettings": false,
|
||||
"clientClassAccessModifier": "public",
|
||||
"typeAccessModifier": "public",
|
||||
"propertySetterAccessModifier": "",
|
||||
"generateNativeRecords": false,
|
||||
"generateContractsOutput": false,
|
||||
"contractsNamespace": null,
|
||||
"contractsOutputFilePath": null,
|
||||
"parameterDateTimeFormat": "yyyy-MM-dd HH:mm:ss",
|
||||
"parameterDateFormat": "yyyy-MM-dd",
|
||||
"generateUpdateJsonSerializerSettingsMethod": true,
|
||||
"useRequestAndResponseSerializationSettings": false,
|
||||
"serializeTypeInformation": false,
|
||||
"queryNullValue": "",
|
||||
"className": "{controller}Client",
|
||||
"operationGenerationMode": "MultipleClientsFromOperationId",
|
||||
"additionalNamespaceUsages": [],
|
||||
"additionalContractNamespaceUsages": [],
|
||||
"generateOptionalParameters": true,
|
||||
"generateJsonMethods": false,
|
||||
"enforceFlagEnums": false,
|
||||
"parameterArrayType": "System.Collections.Generic.IEnumerable",
|
||||
"parameterDictionaryType": "System.Collections.Generic.IDictionary",
|
||||
"responseArrayType": "System.Collections.Generic.ICollection",
|
||||
"responseDictionaryType": "System.Collections.Generic.IDictionary",
|
||||
"wrapResponses": false,
|
||||
"wrapResponseMethods": [],
|
||||
"generateResponseClasses": true,
|
||||
"responseClass": "SwaggerResponse",
|
||||
"namespace": "AdOrbitSDK",
|
||||
"requiredPropertiesMustBeDefined": true,
|
||||
"dateType": "System.DateTimeOffset",
|
||||
"jsonConverters": null,
|
||||
"anyType": "object",
|
||||
"dateTimeType": "System.DateTimeOffset",
|
||||
"timeType": "System.TimeSpan",
|
||||
"timeSpanType": "System.TimeSpan",
|
||||
"arrayType": "System.Collections.Generic.ICollection",
|
||||
"arrayInstanceType": "System.Collections.ObjectModel.Collection",
|
||||
"dictionaryType": "System.Collections.Generic.IDictionary",
|
||||
"dictionaryInstanceType": "System.Collections.Generic.Dictionary",
|
||||
"arrayBaseType": "System.Collections.ObjectModel.Collection",
|
||||
"dictionaryBaseType": "System.Collections.Generic.Dictionary",
|
||||
"classStyle": "Poco",
|
||||
"jsonLibrary": "NewtonsoftJson",
|
||||
"generateDefaultValues": true,
|
||||
"generateDataAnnotations": true,
|
||||
"excludedTypeNames": [],
|
||||
"excludedParameterNames": [
|
||||
"Accept"
|
||||
],
|
||||
"handleReferences": false,
|
||||
"generateImmutableArrayProperties": false,
|
||||
"generateImmutableDictionaryProperties": false,
|
||||
"jsonSerializerSettingsTransformationMethod": null,
|
||||
"inlineNamedArrays": false,
|
||||
"inlineNamedDictionaries": false,
|
||||
"inlineNamedTuples": true,
|
||||
"inlineNamedAny": false,
|
||||
"generateDtoTypes": true,
|
||||
"generateOptionalPropertiesAsNullable": true,
|
||||
"generateNullableReferenceTypes": false,
|
||||
"templateDirectory": null,
|
||||
"serviceHost": null,
|
||||
"serviceSchemes": null,
|
||||
"output": "StalwartClient.cs",
|
||||
"newLineBehavior": "Auto",
|
||||
"AllowAdditionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
2590
StalwartSDK/openapi.yml
Normal file
2590
StalwartSDK/openapi.yml
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,10 @@
|
|||
namespace StalwartSimpleLoginMiddleware.Constants;
|
||||
|
||||
public class EnvironmentVariable
|
||||
{
|
||||
public const string MailServerUri = "MAIL_SERVER_URI";
|
||||
public const string MailServerApiKey = "MAIL_SERVER_API_KEY";
|
||||
public const string PostgresUrl = "POSTGRES_URL";
|
||||
public const string PostgresMinPoolSize = "POSTGRES_MIN_POOL_SIZE";
|
||||
public const string PostgresMaxPoolSize = "POSTGRES_MAX_POOL_SIZE";
|
||||
}
|
||||
146
StalwartSimpleLoginMiddleware/Contexts/ApiKeyContext.cs
Normal file
146
StalwartSimpleLoginMiddleware/Contexts/ApiKeyContext.cs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Npgsql;
|
||||
using StalwartSimpleLoginMiddleware.Constants;
|
||||
using StalwartSimpleLoginMiddleware.Entities;
|
||||
using StalwartSimpleLoginMiddleware.Utilities;
|
||||
|
||||
namespace StalwartSimpleLoginMiddleware.Contexts;
|
||||
|
||||
public class ApiKeyContext : DbContext
|
||||
{
|
||||
public DbSet<ApiKey> ApiKeys { get; set; }
|
||||
public DbSet<Member> Members { get; set; }
|
||||
|
||||
public ApiKeyContext()
|
||||
{
|
||||
}
|
||||
|
||||
public ApiKeyContext(DbContextOptions<ApiKeyContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
ConfigureOptions(optionsBuilder);
|
||||
}
|
||||
|
||||
public static DbContextOptionsBuilder ConfigureOptions(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
if (optionsBuilder.IsConfigured) return optionsBuilder;
|
||||
|
||||
var connectionString = Environment.GetEnvironmentVariable(EnvironmentVariable.PostgresUrl);
|
||||
|
||||
if (string.IsNullOrEmpty(connectionString))
|
||||
{
|
||||
// Default connection
|
||||
var defaultConnectionString = "Host=localhost;Port=5432;Database=postgres;";
|
||||
optionsBuilder.UseNpgsql(defaultConnectionString,
|
||||
nsqlOptions => nsqlOptions.MigrationsAssembly("StalwartSimpleLoginMiddleware"));
|
||||
return optionsBuilder;
|
||||
}
|
||||
|
||||
var pgConnectionString = ConnectionHelper.GetPostgresConnectionString(connectionString);
|
||||
|
||||
// Configure pooling
|
||||
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(pgConnectionString)
|
||||
{
|
||||
MinPoolSize = 10
|
||||
};
|
||||
if (int.TryParse(Environment.GetEnvironmentVariable(EnvironmentVariable.PostgresMinPoolSize),
|
||||
out var minPoolSize))
|
||||
{
|
||||
connectionStringBuilder.MinPoolSize = minPoolSize;
|
||||
}
|
||||
|
||||
if (int.TryParse(Environment.GetEnvironmentVariable(EnvironmentVariable.PostgresMaxPoolSize),
|
||||
out var maxPoolSize))
|
||||
{
|
||||
connectionStringBuilder.MaxPoolSize = maxPoolSize;
|
||||
}
|
||||
|
||||
var pooledConnectionString = connectionStringBuilder.ToString();
|
||||
|
||||
optionsBuilder.UseLazyLoadingProxies()
|
||||
.UseNpgsql(pooledConnectionString,
|
||||
nsqlOptions => nsqlOptions.MigrationsAssembly("StalwartSimpleLoginMiddleware"));
|
||||
return optionsBuilder;
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<ApiKey>(entity =>
|
||||
{
|
||||
entity.HasKey(a => a.Key); // Primary key
|
||||
|
||||
// Members navigation
|
||||
entity.HasMany(a => a.Members)
|
||||
.WithOne(m => m.ApiKey)
|
||||
.HasForeignKey(m => m.ApiKeyId)
|
||||
.IsRequired()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
// Configure Member entity
|
||||
modelBuilder.Entity<Member>(entity =>
|
||||
{
|
||||
// Composite key: ApiKeyId + Email
|
||||
entity.HasKey(m => new { m.ApiKeyId, m.Email });
|
||||
|
||||
// Configure foreign key relationship with ApiKey
|
||||
entity.HasOne(m => m.ApiKey)
|
||||
.WithMany(a => a.Members)
|
||||
.HasForeignKey(m => m.ApiKeyId)
|
||||
.IsRequired()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
}
|
||||
|
||||
// Method to check and apply migrations
|
||||
public async Task EnsureDatabaseMigrated(ILogger logger)
|
||||
{
|
||||
logger.LogInformation("Checking available migrations...");
|
||||
|
||||
// Get all migrations and log them
|
||||
var migrations = Database.GetMigrations().ToList();
|
||||
logger.LogInformation($"Total migrations found: {migrations.Count}");
|
||||
|
||||
foreach (var migration in migrations)
|
||||
{
|
||||
logger.LogInformation($"Migration: {migration}");
|
||||
}
|
||||
|
||||
// Check applied migrations
|
||||
var pendingMigrations = (await Database.GetPendingMigrationsAsync()).ToArray();
|
||||
var appliedMigrations = (await Database.GetAppliedMigrationsAsync()).ToArray();
|
||||
|
||||
logger.LogInformation($"Applied migrations: {string.Join(", ", appliedMigrations)}");
|
||||
|
||||
if (pendingMigrations.Any())
|
||||
{
|
||||
logger.LogInformation($"Applying migrations: {string.Join(", ", pendingMigrations)}");
|
||||
await Database.MigrateAsync();
|
||||
logger.LogInformation("Database migrated.");
|
||||
|
||||
if (appliedMigrations.Length == 0)
|
||||
{
|
||||
// Create Admin key on first migration
|
||||
var apiKey = ApiKeyHelper.GenerateKey();
|
||||
ApiKeys.Add(new ApiKey
|
||||
{
|
||||
Key = apiKey,
|
||||
OwnerEmail = "admin@domain.tld",
|
||||
IsAdmin = true
|
||||
});
|
||||
await SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"Generated Admin API Key: {apiKey}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("No pending migrations found.");
|
||||
}
|
||||
}
|
||||
}
|
||||
109
StalwartSimpleLoginMiddleware/Controllers/AdminController.cs
Normal file
109
StalwartSimpleLoginMiddleware/Controllers/AdminController.cs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using StalwartSimpleLoginMiddleware.Contexts;
|
||||
using StalwartSimpleLoginMiddleware.Entities;
|
||||
using StalwartSimpleLoginMiddleware.Models;
|
||||
using StalwartSimpleLoginMiddleware.Utilities;
|
||||
|
||||
namespace StalwartSimpleLoginMiddleware.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize(Roles = "Admin")]
|
||||
[Route("api/[controller]/[action]")]
|
||||
public class AdminController : ControllerBase
|
||||
{
|
||||
private readonly ApiKeyContext context;
|
||||
|
||||
public AdminController(ApiKeyContext context)
|
||||
{
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ApiKey[]> ListApiKeys([FromQuery] int page = 0, [FromQuery] int limit = 100)
|
||||
{
|
||||
return await context.ApiKeys
|
||||
.Include(apiKey => apiKey.Members)
|
||||
.Skip(page * limit)
|
||||
.Take(limit).ToArrayAsync();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ApiKey> GetApiKey([FromQuery] string key)
|
||||
{
|
||||
var apiKey = await context.ApiKeys
|
||||
.Include(apiKey => apiKey.Members)
|
||||
.Select(apiKey => new ApiKey
|
||||
{
|
||||
Key = apiKey.Key,
|
||||
OwnerEmail = apiKey.OwnerEmail,
|
||||
IsAdmin = apiKey.IsAdmin,
|
||||
Members = apiKey.Members.ToArray()
|
||||
})
|
||||
.FirstOrDefaultAsync(apiKey => apiKey.Key == key);
|
||||
if (apiKey == null) throw new BadHttpRequestException("API Key is invalid.");
|
||||
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult> UpdateApiKeyOwnerEmail([FromBody] UpdateOwnerEmailInput input)
|
||||
{
|
||||
var rows = await context.ApiKeys.Where(apiKey => apiKey.Key == input.ApiKey)
|
||||
.ExecuteUpdateAsync(apiKey => apiKey.SetProperty(p => p.OwnerEmail, input.OwnerEmail));
|
||||
if (rows == 0) return NotFound();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult> CreateApiKey([FromBody] ApiKeyInput newApiKeyInput)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ApiKeyHelper.GetEmailDomain(newApiKeyInput.OwnerEmail)))
|
||||
return BadRequest("Owner Email must be a valid email address.");
|
||||
var apiKey = new ApiKey
|
||||
{
|
||||
Key = ApiKeyHelper.GenerateKey(),
|
||||
OwnerEmail = newApiKeyInput.OwnerEmail,
|
||||
IsAdmin = newApiKeyInput.IsAdmin,
|
||||
Members = newApiKeyInput.Members.Select(m => new Member { Email = m.Email, IsExternal = m.IsExternal })
|
||||
.ToArray()
|
||||
};
|
||||
context.ApiKeys.Add(apiKey);
|
||||
await context.SaveChangesAsync();
|
||||
return CreatedAtAction(nameof(GetApiKey), new { key = apiKey.Key }, apiKey);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult> CreateApiKeyMember([FromBody] AddApiKeyMemberInput input)
|
||||
{
|
||||
var member = new Member
|
||||
{
|
||||
ApiKeyId = input.ApiKey,
|
||||
Email = input.Member.Email,
|
||||
IsExternal = input.Member.IsExternal
|
||||
};
|
||||
context.Members.Add(member);
|
||||
await context.SaveChangesAsync();
|
||||
return CreatedAtAction(nameof(GetApiKey), new { key = input.ApiKey });
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
public async Task<ActionResult> DeleteApiKey([FromQuery] string key)
|
||||
{
|
||||
var rows = await context.ApiKeys.Where(apiKey => apiKey.Key == key)
|
||||
.ExecuteDeleteAsync();
|
||||
if (rows == 0) return NotFound();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
public async Task<ActionResult> DeleteApiKeyMemberEmail([FromQuery] string key, [FromQuery] string email)
|
||||
{
|
||||
var rows = await context.Members.Where(member => member.ApiKeyId == key)
|
||||
.Where(member => member.Email == email)
|
||||
.ExecuteDeleteAsync();
|
||||
if (rows == 0) return NotFound();
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
56
StalwartSimpleLoginMiddleware/Controllers/AliasController.cs
Normal file
56
StalwartSimpleLoginMiddleware/Controllers/AliasController.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
using AdOrbitSDK;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StalwartSimpleLoginMiddleware.Models;
|
||||
using StalwartSimpleLoginMiddleware.Services;
|
||||
|
||||
namespace StalwartSimpleLoginMiddleware.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/[controller]")]
|
||||
public class AliasController(StalwartClient stalwartClient) : ControllerBase
|
||||
{
|
||||
[Route("random/new")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> NewRandomAlias([FromQuery] string? hostname,
|
||||
[FromQuery] string? mode,
|
||||
[FromBody] NewRandomAliasInput body)
|
||||
{
|
||||
var apiKeyAccessor = HttpContext.RequestServices.GetRequiredService<IApiKeyAccessor>();
|
||||
var randomAlias = Get8CharacterRandomString();
|
||||
var client = await stalwartClient.GetClient();
|
||||
var requestBody = new Body2
|
||||
{
|
||||
Type = "list",
|
||||
Name = randomAlias,
|
||||
Description = body.Note,
|
||||
Emails = new List<object> { $"{randomAlias}@{apiKeyAccessor.Metadata.Domain}" },
|
||||
Members = apiKeyAccessor.Metadata.Members as ICollection<object>,
|
||||
ExternalMembers = apiKeyAccessor.Metadata.ExternalMembers as ICollection<object>
|
||||
};
|
||||
await client.PrincipalPOSTAsync(requestBody);
|
||||
|
||||
return Created(null as string, new NewRandomAliasOutput
|
||||
{
|
||||
CreationDate = DateTime.Today.Date,
|
||||
CreationTimestamp = DateTime.Now.Ticks,
|
||||
Email = requestBody.Emails?.OfType<string>().FirstOrDefault() ?? string.Empty,
|
||||
Alias = requestBody.Emails?.OfType<string>().FirstOrDefault() ?? string.Empty,
|
||||
Name = requestBody.Name ?? string.Empty,
|
||||
Enabled = true,
|
||||
Id = new Random().Next(),
|
||||
Mailbox = new Mailbox { Id = new Random().Next(), Email = apiKeyAccessor.Metadata.Members.First() },
|
||||
Mailboxes = apiKeyAccessor.Metadata.Members.Concat(apiKeyAccessor.Metadata.ExternalMembers)
|
||||
.Select(email => new Mailbox { Id = new Random().Next(), Email = email.ToString() }),
|
||||
Note = body.Note ?? string.Empty,
|
||||
});
|
||||
}
|
||||
|
||||
private static string Get8CharacterRandomString()
|
||||
{
|
||||
var path = Path.GetRandomFileName();
|
||||
path = path.Replace(".", ""); // Remove period
|
||||
return path.Substring(0, 8); // Return 8 character string
|
||||
}
|
||||
}
|
||||
29
StalwartSimpleLoginMiddleware/Dockerfile
Normal file
29
StalwartSimpleLoginMiddleware/Dockerfile
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-jammy-chiseled-extra AS base
|
||||
USER $APP_UID
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0-jammy AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
ENV DOTNET_TOOLS_PATH=/build/.dotnet-tools
|
||||
ENV PATH="$DOTNET_TOOLS_PATH:$PATH"
|
||||
|
||||
RUN dotnet tool install NSwag.ConsoleCore --tool-path $DOTNET_TOOLS_PATH --version 14.2.0.0
|
||||
|
||||
WORKDIR /src
|
||||
COPY StalwartSimpleLoginMiddleware/StalwartSimpleLoginMiddleware.csproj StalwartSimpleLoginMiddleware/
|
||||
COPY StalwartSDK/StalwartSDK.csproj StalwartSDK/
|
||||
|
||||
RUN dotnet restore "StalwartSimpleLoginMiddleware/StalwartSimpleLoginMiddleware.csproj"
|
||||
|
||||
COPY . .
|
||||
# Run NSwag for StalwartSDK
|
||||
RUN cd /src/StalwartSDK && \
|
||||
nswag run nswag.json
|
||||
|
||||
RUN dotnet publish StalwartSimpleLoginMiddleware/StalwartSimpleLoginMiddleware.csproj -c $BUILD_CONFIGURATION --no-restore -o /app/publish
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
ENTRYPOINT ["dotnet", "StalwartSimpleLoginMiddleware.dll"]
|
||||
18
StalwartSimpleLoginMiddleware/Entities/ApiKey.cs
Normal file
18
StalwartSimpleLoginMiddleware/Entities/ApiKey.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace StalwartSimpleLoginMiddleware.Entities;
|
||||
|
||||
public class ApiKey
|
||||
{
|
||||
[Key]
|
||||
[StringLength(88)]
|
||||
[Unicode(false)]
|
||||
public string Key { get; set; }
|
||||
|
||||
[Required] [StringLength(254)] public string OwnerEmail { get; set; }
|
||||
|
||||
public bool IsAdmin { get; set; }
|
||||
|
||||
public virtual ICollection<Member> Members { get; set; }
|
||||
}
|
||||
14
StalwartSimpleLoginMiddleware/Entities/Member.cs
Normal file
14
StalwartSimpleLoginMiddleware/Entities/Member.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace StalwartSimpleLoginMiddleware.Entities;
|
||||
|
||||
public class Member
|
||||
{
|
||||
[Required] [StringLength(88)] public string ApiKeyId { get; set; }
|
||||
|
||||
[Required] [StringLength(254)] public string Email { get; set; }
|
||||
|
||||
public bool IsExternal { get; set; }
|
||||
|
||||
public virtual ApiKey ApiKey { get; set; }
|
||||
}
|
||||
83
StalwartSimpleLoginMiddleware/Migrations/20250510085312_Initial.Designer.cs
generated
Normal file
83
StalwartSimpleLoginMiddleware/Migrations/20250510085312_Initial.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using StalwartSimpleLoginMiddleware.Contexts;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace StalwartSimpleLoginMiddleware.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApiKeyContext))]
|
||||
[Migration("20250510085312_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.11")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("StalwartSimpleLoginMiddleware.Entities.ApiKey", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(88)
|
||||
.IsUnicode(false)
|
||||
.HasColumnType("character varying(88)");
|
||||
|
||||
b.Property<bool>("IsAdmin")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("OwnerEmail")
|
||||
.IsRequired()
|
||||
.HasMaxLength(254)
|
||||
.HasColumnType("character varying(254)");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("ApiKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("StalwartSimpleLoginMiddleware.Entities.Member", b =>
|
||||
{
|
||||
b.Property<string>("ApiKeyId")
|
||||
.HasMaxLength(88)
|
||||
.HasColumnType("character varying(88)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(254)
|
||||
.HasColumnType("character varying(254)");
|
||||
|
||||
b.Property<bool>("IsExternal")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("ApiKeyId", "Email");
|
||||
|
||||
b.ToTable("Members");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("StalwartSimpleLoginMiddleware.Entities.Member", b =>
|
||||
{
|
||||
b.HasOne("StalwartSimpleLoginMiddleware.Entities.ApiKey", "ApiKey")
|
||||
.WithMany("Members")
|
||||
.HasForeignKey("ApiKeyId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ApiKey");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("StalwartSimpleLoginMiddleware.Entities.ApiKey", b =>
|
||||
{
|
||||
b.Navigation("Members");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace StalwartSimpleLoginMiddleware.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Initial : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ApiKeys",
|
||||
columns: table => new
|
||||
{
|
||||
Key = table.Column<string>(type: "character varying(88)", unicode: false, maxLength: 88, nullable: false),
|
||||
OwnerEmail = table.Column<string>(type: "character varying(254)", maxLength: 254, nullable: false),
|
||||
IsAdmin = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ApiKeys", x => x.Key);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Members",
|
||||
columns: table => new
|
||||
{
|
||||
ApiKeyId = table.Column<string>(type: "character varying(88)", maxLength: 88, nullable: false),
|
||||
Email = table.Column<string>(type: "character varying(254)", maxLength: 254, nullable: false),
|
||||
IsExternal = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Members", x => new { x.ApiKeyId, x.Email });
|
||||
table.ForeignKey(
|
||||
name: "FK_Members_ApiKeys_ApiKeyId",
|
||||
column: x => x.ApiKeyId,
|
||||
principalTable: "ApiKeys",
|
||||
principalColumn: "Key",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Members");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApiKeys");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using StalwartSimpleLoginMiddleware.Contexts;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace StalwartSimpleLoginMiddleware.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApiKeyContext))]
|
||||
partial class ApiKeyContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.11")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("StalwartSimpleLoginMiddleware.Entities.ApiKey", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(88)
|
||||
.IsUnicode(false)
|
||||
.HasColumnType("character varying(88)");
|
||||
|
||||
b.Property<bool>("IsAdmin")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("OwnerEmail")
|
||||
.IsRequired()
|
||||
.HasMaxLength(254)
|
||||
.HasColumnType("character varying(254)");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("ApiKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("StalwartSimpleLoginMiddleware.Entities.Member", b =>
|
||||
{
|
||||
b.Property<string>("ApiKeyId")
|
||||
.HasMaxLength(88)
|
||||
.HasColumnType("character varying(88)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(254)
|
||||
.HasColumnType("character varying(254)");
|
||||
|
||||
b.Property<bool>("IsExternal")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("ApiKeyId", "Email");
|
||||
|
||||
b.ToTable("Members");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("StalwartSimpleLoginMiddleware.Entities.Member", b =>
|
||||
{
|
||||
b.HasOne("StalwartSimpleLoginMiddleware.Entities.ApiKey", "ApiKey")
|
||||
.WithMany("Members")
|
||||
.HasForeignKey("ApiKeyId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ApiKey");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("StalwartSimpleLoginMiddleware.Entities.ApiKey", b =>
|
||||
{
|
||||
b.Navigation("Members");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
namespace StalwartSimpleLoginMiddleware.Models;
|
||||
|
||||
public class AddApiKeyMemberInput
|
||||
{
|
||||
public string ApiKey { get; set; }
|
||||
public MemberInput Member { get; set; }
|
||||
}
|
||||
9
StalwartSimpleLoginMiddleware/Models/ApiKeyAccessor.cs
Normal file
9
StalwartSimpleLoginMiddleware/Models/ApiKeyAccessor.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
using AspNetCore.Authentication.ApiKey;
|
||||
|
||||
namespace StalwartSimpleLoginMiddleware.Models;
|
||||
|
||||
public class ApiKeyAccessor : IApiKeyAccessor
|
||||
{
|
||||
public IApiKey ApiKey { get; set; }
|
||||
public KeyMetadata Metadata { get; set; }
|
||||
}
|
||||
8
StalwartSimpleLoginMiddleware/Models/ApiKeyInput.cs
Normal file
8
StalwartSimpleLoginMiddleware/Models/ApiKeyInput.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
namespace StalwartSimpleLoginMiddleware.Models;
|
||||
|
||||
public class ApiKeyInput
|
||||
{
|
||||
public string OwnerEmail { get; set; }
|
||||
public bool IsAdmin { get; set; }
|
||||
public ICollection<MemberInput> Members { get; set; }
|
||||
}
|
||||
11
StalwartSimpleLoginMiddleware/Models/DbApiKey.cs
Normal file
11
StalwartSimpleLoginMiddleware/Models/DbApiKey.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using System.Security.Claims;
|
||||
using AspNetCore.Authentication.ApiKey;
|
||||
|
||||
namespace StalwartSimpleLoginMiddleware.Models;
|
||||
|
||||
public class DbApiKey : IApiKey
|
||||
{
|
||||
public string Key { get; set; }
|
||||
public string OwnerName { get; set; }
|
||||
public IReadOnlyCollection<Claim> Claims { get; set; }
|
||||
}
|
||||
9
StalwartSimpleLoginMiddleware/Models/IApiKeyAccessor.cs
Normal file
9
StalwartSimpleLoginMiddleware/Models/IApiKeyAccessor.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
using AspNetCore.Authentication.ApiKey;
|
||||
|
||||
namespace StalwartSimpleLoginMiddleware.Models;
|
||||
|
||||
public interface IApiKeyAccessor
|
||||
{
|
||||
IApiKey ApiKey { get; set; }
|
||||
KeyMetadata Metadata { get; set; }
|
||||
}
|
||||
8
StalwartSimpleLoginMiddleware/Models/KeyMetadata.cs
Normal file
8
StalwartSimpleLoginMiddleware/Models/KeyMetadata.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
namespace StalwartSimpleLoginMiddleware.Models;
|
||||
|
||||
public class KeyMetadata
|
||||
{
|
||||
public string Domain { get; set; }
|
||||
public ICollection<string> Members { get; set; }
|
||||
public ICollection<string> ExternalMembers { get; set; }
|
||||
}
|
||||
7
StalwartSimpleLoginMiddleware/Models/MemberInput.cs
Normal file
7
StalwartSimpleLoginMiddleware/Models/MemberInput.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace StalwartSimpleLoginMiddleware.Models;
|
||||
|
||||
public class MemberInput
|
||||
{
|
||||
public string Email { get; set; }
|
||||
public bool IsExternal { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
namespace StalwartSimpleLoginMiddleware.Models;
|
||||
|
||||
public class NewRandomAliasInput
|
||||
{
|
||||
public string? Note { get; set; }
|
||||
}
|
||||
21
StalwartSimpleLoginMiddleware/Models/NewRandomAliasOutput.cs
Normal file
21
StalwartSimpleLoginMiddleware/Models/NewRandomAliasOutput.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
namespace StalwartSimpleLoginMiddleware.Models;
|
||||
|
||||
public class NewRandomAliasOutput
|
||||
{
|
||||
public DateTime CreationDate { get; set; }
|
||||
public long CreationTimestamp { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string Alias { get; set; }
|
||||
public string Name { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public int Id { get; set; }
|
||||
public Mailbox Mailbox { get; set; }
|
||||
public IEnumerable<Mailbox> Mailboxes { get; set; }
|
||||
public string Note { get; set; }
|
||||
}
|
||||
|
||||
public class Mailbox
|
||||
{
|
||||
public string Email { get; set; }
|
||||
public int Id { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
namespace StalwartSimpleLoginMiddleware.Models;
|
||||
|
||||
public class UpdateOwnerEmailInput
|
||||
{
|
||||
public string ApiKey { get; set; }
|
||||
public string OwnerEmail { get; set; }
|
||||
}
|
||||
116
StalwartSimpleLoginMiddleware/Program.cs
Normal file
116
StalwartSimpleLoginMiddleware/Program.cs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using AspNetCore.Authentication.ApiKey;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Npgsql;
|
||||
using StalwartSimpleLoginMiddleware.Contexts;
|
||||
using StalwartSimpleLoginMiddleware.Models;
|
||||
using StalwartSimpleLoginMiddleware.Repositories;
|
||||
using StalwartSimpleLoginMiddleware.Services;
|
||||
|
||||
var logger = LoggerFactory.Create(logging => logging.AddConsole()).CreateLogger("Startup");
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
|
||||
builder.Services.AddDbContextPool<ApiKeyContext>((_, options) => ApiKeyContext.ConfigureOptions(options));
|
||||
|
||||
builder.Services.AddHealthChecks()
|
||||
.AddCheck("WebService", () => HealthCheckResult.Healthy("The web service is running."))
|
||||
.AddDbContextCheck<ApiKeyContext>();
|
||||
|
||||
builder.Services.AddScoped<IApiKeyAccessor, ApiKeyAccessor>()
|
||||
.AddScoped<IApiKeyRepository, ApiKeyContextRepository>();
|
||||
|
||||
builder.Services.AddAuthentication(ApiKeyDefaults.AuthenticationScheme)
|
||||
.AddApiKeyInHeader(options =>
|
||||
{
|
||||
options.Realm = "StalwartSimpleLoginMiddleware";
|
||||
options.KeyName = "Authentication";
|
||||
options.Events = new ApiKeyProvider();
|
||||
});
|
||||
|
||||
builder.Services.AddAuthorization(options =>
|
||||
{
|
||||
options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
|
||||
});
|
||||
|
||||
builder.Services.AddControllers()
|
||||
.AddJsonOptions(options => { options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; });
|
||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(c =>
|
||||
{
|
||||
// Add the API Key Security Definition
|
||||
c.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme
|
||||
{
|
||||
Description =
|
||||
"API Key needed to access endpoints. Add it to the request headers as 'Authentication: <API_KEY>'",
|
||||
Type = SecuritySchemeType.ApiKey,
|
||||
Name = "Authentication", // Header name for the API Key
|
||||
In = ParameterLocation.Header
|
||||
});
|
||||
|
||||
// Add security requirements to ensure the header is required
|
||||
c.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = "ApiKey"
|
||||
}
|
||||
},
|
||||
new string[] { } // No specific scopes
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>()
|
||||
.AddSingleton<StalwartClient>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Ensure the database is migrated at startup.
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
try
|
||||
{
|
||||
var services = scope.ServiceProvider;
|
||||
var context = services.GetRequiredService<ApiKeyContext>();
|
||||
await context.EnsureDatabaseMigrated(logger);
|
||||
}
|
||||
catch (NpgsqlException ex)
|
||||
{
|
||||
if (ex.SqlState is PostgresErrorCodes.ConnectionFailure or PostgresErrorCodes.ConnectionException ||
|
||||
ex.Message.StartsWith("Failed to connect"))
|
||||
logger.LogCritical($"Database connection failed: {ex.Message}");
|
||||
else if (ex.SqlState is PostgresErrorCodes.InvalidPassword
|
||||
or PostgresErrorCodes.InvalidAuthorizationSpecification)
|
||||
logger.LogCritical("Failed to connect. Invalid password.");
|
||||
else
|
||||
logger.LogCritical($"An unknown error occurred:{Environment.NewLine}{ex.StackTrace}");
|
||||
}
|
||||
}
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.MapHealthChecks("/health");
|
||||
app.Run();
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
using AspNetCore.Authentication.ApiKey;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using StalwartSimpleLoginMiddleware.Contexts;
|
||||
using StalwartSimpleLoginMiddleware.Models;
|
||||
using StalwartSimpleLoginMiddleware.Utilities;
|
||||
|
||||
namespace StalwartSimpleLoginMiddleware.Repositories;
|
||||
|
||||
public class ApiKeyContextRepository(ApiKeyContext context) : IApiKeyRepository
|
||||
{
|
||||
public async Task<IApiKey?> GetApiKeyAsync(string key)
|
||||
{
|
||||
var dbKey = await context.ApiKeys.AsNoTracking()
|
||||
.FirstOrDefaultAsync(api => api.Key == key);
|
||||
if (dbKey == null) return null;
|
||||
|
||||
return ApiKeyHelper.CreateDbApiKey(dbKey);
|
||||
}
|
||||
|
||||
public async Task<KeyMetadata> GetMetadataAsync(string key)
|
||||
{
|
||||
var dbKey = await context.ApiKeys.AsNoTracking()
|
||||
.Include(api => api.Members)
|
||||
.FirstAsync(api => api.Key == key);
|
||||
|
||||
return ApiKeyHelper.CreateKeyMetadata(dbKey);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
using AspNetCore.Authentication.ApiKey;
|
||||
using StalwartSimpleLoginMiddleware.Models;
|
||||
|
||||
namespace StalwartSimpleLoginMiddleware.Repositories;
|
||||
|
||||
public interface IApiKeyRepository
|
||||
{
|
||||
Task<IApiKey?> GetApiKeyAsync(string key);
|
||||
Task<KeyMetadata> GetMetadataAsync(string key);
|
||||
}
|
||||
40
StalwartSimpleLoginMiddleware/Services/ApiKeyProvider.cs
Normal file
40
StalwartSimpleLoginMiddleware/Services/ApiKeyProvider.cs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
using AspNetCore.Authentication.ApiKey;
|
||||
using StalwartSimpleLoginMiddleware.Models;
|
||||
using StalwartSimpleLoginMiddleware.Repositories;
|
||||
|
||||
namespace StalwartSimpleLoginMiddleware.Services;
|
||||
|
||||
public class ApiKeyProvider : ApiKeyEvents
|
||||
{
|
||||
public ApiKeyProvider()
|
||||
{
|
||||
OnValidateKey = OnValidateKeyAsync;
|
||||
}
|
||||
|
||||
private static async Task OnValidateKeyAsync(ApiKeyValidateKeyContext context)
|
||||
{
|
||||
var apiKeyRepository = context.HttpContext.RequestServices.GetRequiredService<IApiKeyRepository>();
|
||||
var apiKey = await apiKeyRepository.GetApiKeyAsync(context.ApiKey);
|
||||
|
||||
if (apiKey == null || !apiKey.Key.Equals(context.ApiKey, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
context.ValidationFailed();
|
||||
return;
|
||||
}
|
||||
|
||||
context.ValidationSucceeded(apiKey.OwnerName, apiKey.Claims);
|
||||
|
||||
var apiKeyAccessor = context.HttpContext.RequestServices.GetRequiredService<IApiKeyAccessor>();
|
||||
apiKeyAccessor.ApiKey = apiKey;
|
||||
apiKeyAccessor.Metadata = await apiKeyRepository.GetMetadataAsync(context.ApiKey);
|
||||
}
|
||||
|
||||
public override async Task HandleChallengeAsync(ApiKeyHandleChallengeContext context)
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
|
||||
await context.Response.WriteAsync("{\"Unauthorized\": 401}");
|
||||
|
||||
context.Handled();
|
||||
}
|
||||
}
|
||||
41
StalwartSimpleLoginMiddleware/Services/StalwartClient.cs
Normal file
41
StalwartSimpleLoginMiddleware/Services/StalwartClient.cs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
using AdOrbitSDK;
|
||||
using StalwartSimpleLoginMiddleware.Constants;
|
||||
|
||||
namespace StalwartSimpleLoginMiddleware.Services;
|
||||
|
||||
public class StalwartClient() : IDisposable
|
||||
{
|
||||
private Client _userClient;
|
||||
|
||||
private static HttpClient HttpClient => new()
|
||||
{
|
||||
BaseAddress = new Uri(Environment.GetEnvironmentVariable(EnvironmentVariable.MailServerUri) + "/api/")
|
||||
};
|
||||
|
||||
public async Task<Client> GetClient()
|
||||
{
|
||||
if (_userClient != null) return _userClient;
|
||||
|
||||
return GetClient(Environment.GetEnvironmentVariable(EnvironmentVariable.MailServerApiKey) ?? string.Empty);
|
||||
}
|
||||
|
||||
private Client GetClient(string apiKey)
|
||||
{
|
||||
if (_userClient != null) return _userClient;
|
||||
|
||||
if (string.IsNullOrEmpty(HttpClient.BaseAddress?.AbsoluteUri))
|
||||
throw new NullReferenceException($"{EnvironmentVariable.MailServerUri} is missing in environment.");
|
||||
|
||||
if (string.IsNullOrEmpty(apiKey))
|
||||
throw new NullReferenceException($"{EnvironmentVariable.MailServerApiKey} is missing in environment.");
|
||||
|
||||
_userClient = new Client(HttpClient, apiKey);
|
||||
return _userClient;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
HttpClient.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AspNetCore.Authentication.ApiKey" Version="8.0.1"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="8.0.11"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.11"/>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11"/>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\.dockerignore">
|
||||
<Link>.dockerignore</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StalwartSDK\StalwartSDK.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Migrations\"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
66
StalwartSimpleLoginMiddleware/Utilities/ApiKeyHelper.cs
Normal file
66
StalwartSimpleLoginMiddleware/Utilities/ApiKeyHelper.cs
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
using System.Net.Mail;
|
||||
using System.Security.Cryptography;
|
||||
using StalwartSimpleLoginMiddleware.Entities;
|
||||
using StalwartSimpleLoginMiddleware.Models;
|
||||
|
||||
namespace StalwartSimpleLoginMiddleware.Utilities;
|
||||
|
||||
public class ApiKeyHelper
|
||||
{
|
||||
public static string GenerateKey()
|
||||
{
|
||||
var key = new byte[64];
|
||||
using (var generator = RandomNumberGenerator.Create())
|
||||
generator.GetBytes(key);
|
||||
return Convert.ToBase64String(key);
|
||||
}
|
||||
|
||||
public static DbApiKey CreateDbApiKey(ApiKey dbKey)
|
||||
{
|
||||
return new DbApiKey
|
||||
{
|
||||
Key = dbKey.Key,
|
||||
OwnerName = dbKey.OwnerEmail,
|
||||
Claims = ClaimsHelper.BuildClaims(dbKey)
|
||||
};
|
||||
}
|
||||
|
||||
public static KeyMetadata CreateKeyMetadata(ApiKey dbKey)
|
||||
{
|
||||
return new KeyMetadata
|
||||
{
|
||||
Domain = GetEmailDomain(dbKey.OwnerEmail),
|
||||
Members = GetMembers(dbKey, false),
|
||||
ExternalMembers = GetMembers(dbKey, true)
|
||||
};
|
||||
}
|
||||
|
||||
public static ICollection<string> GetMembers(ApiKey dbKey, bool isExternal)
|
||||
{
|
||||
var members = dbKey.Members.Where(m => m.IsExternal == isExternal)
|
||||
.Select(m => m.Email);
|
||||
|
||||
if (!isExternal)
|
||||
{
|
||||
return members.Append(dbKey.OwnerEmail)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
return members.ToArray();
|
||||
}
|
||||
|
||||
public static string GetEmailDomain(string email)
|
||||
{
|
||||
if (string.IsNullOrEmpty(email))
|
||||
{
|
||||
throw new ArgumentException("Email cannot be null or empty.", nameof(email));
|
||||
}
|
||||
|
||||
if (MailAddress.TryCreate(email, out var address))
|
||||
{
|
||||
return address.Host;
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Invalid email format: {email}", nameof(email));
|
||||
}
|
||||
}
|
||||
19
StalwartSimpleLoginMiddleware/Utilities/ClaimsHelper.cs
Normal file
19
StalwartSimpleLoginMiddleware/Utilities/ClaimsHelper.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
using System.Security.Claims;
|
||||
using StalwartSimpleLoginMiddleware.Entities;
|
||||
|
||||
namespace StalwartSimpleLoginMiddleware.Utilities;
|
||||
|
||||
public static class ClaimsHelper
|
||||
{
|
||||
public static List<Claim> BuildClaims(ApiKey apiKey)
|
||||
{
|
||||
var claims = new List<Claim>();
|
||||
|
||||
if (apiKey.IsAdmin)
|
||||
{
|
||||
claims.Add(new Claim(ClaimTypes.Role, "Admin"));
|
||||
}
|
||||
|
||||
return claims;
|
||||
}
|
||||
}
|
||||
22
StalwartSimpleLoginMiddleware/Utilities/ConnectionHelper.cs
Normal file
22
StalwartSimpleLoginMiddleware/Utilities/ConnectionHelper.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
using Npgsql;
|
||||
|
||||
namespace StalwartSimpleLoginMiddleware.Utilities;
|
||||
|
||||
public static class ConnectionHelper
|
||||
{
|
||||
public static string GetPostgresConnectionString(string url)
|
||||
{
|
||||
var uri = new Uri(url);
|
||||
var userInfo = uri.UserInfo.Split(':');
|
||||
var builder = new NpgsqlConnectionStringBuilder
|
||||
{
|
||||
Host = uri.Host,
|
||||
Port = uri.Port,
|
||||
Database = uri.AbsolutePath.Trim('/'),
|
||||
Username = userInfo[0],
|
||||
Password = userInfo[1],
|
||||
SslMode = SslMode.Prefer
|
||||
};
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
StalwartSimpleLoginMiddleware/appsettings.json
Normal file
9
StalwartSimpleLoginMiddleware/appsettings.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue