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
|
# Android studio 3.1+ serialized cache file
|
||||||
.idea/caches/build_file_checksums.ser
|
.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