♻️ Mix things up
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,7 @@
|
||||
bin/
|
||||
obj/
|
||||
/packages/
|
||||
/Certificates/
|
||||
riderModule.iml
|
||||
/_ReSharper.Caches/
|
||||
.idea
|
||||
|
@ -2,6 +2,7 @@ using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using DysonNetwork.Pass.Account;
|
||||
using DysonNetwork.Pass.Auth;
|
||||
using DysonNetwork.Pass.Developer;
|
||||
using DysonNetwork.Pass.Permission;
|
||||
using DysonNetwork.Pass.Wallet;
|
||||
using DysonNetwork.Shared.Data;
|
||||
@ -47,6 +48,9 @@ public class AppDatabase(
|
||||
public DbSet<Subscription> WalletSubscriptions { get; set; }
|
||||
public DbSet<Coupon> WalletCoupons { get; set; }
|
||||
|
||||
public DbSet<CustomApp> CustomApps { get; set; }
|
||||
public DbSet<CustomAppSecret> CustomAppSecrets { get; set; }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
optionsBuilder.UseNpgsql(
|
||||
|
@ -5,6 +5,7 @@ using System.Text;
|
||||
using DysonNetwork.Pass.Auth.OidcProvider.Models;
|
||||
using DysonNetwork.Pass.Auth.OidcProvider.Options;
|
||||
using DysonNetwork.Pass.Auth.OidcProvider.Responses;
|
||||
using DysonNetwork.Pass.Developer;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Pass;
|
||||
using DysonNetwork.Pass.Developer;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using NodaTime;
|
||||
using NodaTime.Serialization.Protobuf;
|
||||
@ -21,7 +22,7 @@ public class AuthSession : ModelBase
|
||||
public Guid ChallengeId { get; set; }
|
||||
public AuthChallenge Challenge { get; set; } = null!;
|
||||
public Guid? AppId { get; set; }
|
||||
// public CustomApp? App { get; set; }
|
||||
public CustomApp? App { get; set; }
|
||||
|
||||
public Shared.Proto.AuthSession ToProtoValue() => new()
|
||||
{
|
||||
|
68
DysonNetwork.Pass/Developer/CustomApp.cs
Normal file
68
DysonNetwork.Pass/Developer/CustomApp.cs
Normal file
@ -0,0 +1,68 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Pass.Account;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Pass.Developer;
|
||||
|
||||
public enum CustomAppStatus
|
||||
{
|
||||
Developing,
|
||||
Staging,
|
||||
Production,
|
||||
Suspended
|
||||
}
|
||||
|
||||
public class CustomApp : ModelBase, IIdentifiedResource
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[MaxLength(1024)] public string Slug { get; set; } = null!;
|
||||
[MaxLength(1024)] public string Name { get; set; } = null!;
|
||||
[MaxLength(4096)] public string? Description { get; set; }
|
||||
public CustomAppStatus Status { get; set; } = CustomAppStatus.Developing;
|
||||
|
||||
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; }
|
||||
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; }
|
||||
|
||||
[Column(TypeName = "jsonb")] public VerificationMark? Verification { get; set; }
|
||||
[Column(TypeName = "jsonb")] public CustomAppOauthConfig? OauthConfig { get; set; }
|
||||
[Column(TypeName = "jsonb")] public CustomAppLinks? Links { get; set; }
|
||||
|
||||
[JsonIgnore] public ICollection<CustomAppSecret> Secrets { get; set; } = new List<CustomAppSecret>();
|
||||
|
||||
// TODO: Publisher
|
||||
|
||||
[NotMapped] public string ResourceIdentifier => "custom-app/" + Id;
|
||||
}
|
||||
|
||||
public class CustomAppLinks
|
||||
{
|
||||
[MaxLength(8192)] public string? HomePage { get; set; }
|
||||
[MaxLength(8192)] public string? PrivacyPolicy { get; set; }
|
||||
[MaxLength(8192)] public string? TermsOfService { get; set; }
|
||||
}
|
||||
|
||||
public class CustomAppOauthConfig
|
||||
{
|
||||
[MaxLength(1024)] public string? ClientUri { get; set; }
|
||||
[MaxLength(4096)] public string[] RedirectUris { get; set; } = [];
|
||||
[MaxLength(4096)] public string[]? PostLogoutRedirectUris { get; set; }
|
||||
[MaxLength(256)] public string[]? AllowedScopes { get; set; } = ["openid", "profile", "email"];
|
||||
[MaxLength(256)] public string[] AllowedGrantTypes { get; set; } = ["authorization_code", "refresh_token"];
|
||||
public bool RequirePkce { get; set; } = true;
|
||||
public bool AllowOfflineAccess { get; set; } = false;
|
||||
}
|
||||
|
||||
public class CustomAppSecret : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[MaxLength(1024)] public string Secret { get; set; } = null!;
|
||||
[MaxLength(4096)] public string? Description { get; set; } = null!;
|
||||
public Instant? ExpiredAt { get; set; }
|
||||
public bool IsOidc { get; set; } = false; // Indicates if this secret is for OIDC/OAuth
|
||||
|
||||
public Guid AppId { get; set; }
|
||||
public CustomApp App { get; set; } = null!;
|
||||
}
|
@ -1,90 +1,54 @@
|
||||
using MailKit.Net.Smtp;
|
||||
using dotnet_etcd;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MimeKit;
|
||||
|
||||
namespace DysonNetwork.Pass.Email;
|
||||
|
||||
public class EmailServiceConfiguration
|
||||
{
|
||||
public string Server { get; set; } = null!;
|
||||
public int Port { get; set; }
|
||||
public bool UseSsl { get; set; }
|
||||
public string Username { get; set; } = null!;
|
||||
public string Password { get; set; } = null!;
|
||||
public string FromAddress { get; set; } = null!;
|
||||
public string FromName { get; set; } = null!;
|
||||
public string SubjectPrefix { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class EmailService
|
||||
{
|
||||
private readonly EmailServiceConfiguration _configuration;
|
||||
private readonly PusherService.PusherServiceClient _client;
|
||||
private readonly RazorViewRenderer _viewRenderer;
|
||||
private readonly ILogger<EmailService> _logger;
|
||||
|
||||
public EmailService(IConfiguration configuration, RazorViewRenderer viewRenderer, ILogger<EmailService> logger)
|
||||
public EmailService(
|
||||
EtcdClient etcd,
|
||||
RazorViewRenderer viewRenderer,
|
||||
IConfiguration configuration,
|
||||
ILogger<EmailService> logger,
|
||||
PusherService.PusherServiceClient client
|
||||
)
|
||||
{
|
||||
var cfg = configuration.GetSection("Email").Get<EmailServiceConfiguration>();
|
||||
_configuration = cfg ?? throw new ArgumentException("Email service was not configured.");
|
||||
_client = GrpcClientHelper.CreatePusherServiceClient(
|
||||
etcd,
|
||||
configuration["Service:CertPath"]!,
|
||||
configuration["Service:KeyPath"]!
|
||||
).GetAwaiter().GetResult();
|
||||
_viewRenderer = viewRenderer;
|
||||
_logger = logger;
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public async Task SendEmailAsync(string? recipientName, string recipientEmail, string subject, string textBody)
|
||||
public async Task SendEmailAsync(
|
||||
string? recipientName,
|
||||
string recipientEmail,
|
||||
string subject,
|
||||
string htmlBody
|
||||
)
|
||||
{
|
||||
await SendEmailAsync(recipientName, recipientEmail, subject, textBody, null);
|
||||
}
|
||||
subject = $"[Solarpass] {subject}";
|
||||
|
||||
public async Task SendEmailAsync(string? recipientName, string recipientEmail, string subject, string textBody,
|
||||
string? htmlBody)
|
||||
{
|
||||
subject = $"[{_configuration.SubjectPrefix}] {subject}";
|
||||
|
||||
var emailMessage = new MimeMessage();
|
||||
emailMessage.From.Add(new MailboxAddress(_configuration.FromName, _configuration.FromAddress));
|
||||
emailMessage.To.Add(new MailboxAddress(recipientName, recipientEmail));
|
||||
emailMessage.Subject = subject;
|
||||
|
||||
var bodyBuilder = new BodyBuilder
|
||||
{
|
||||
TextBody = textBody
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(htmlBody))
|
||||
bodyBuilder.HtmlBody = htmlBody;
|
||||
|
||||
emailMessage.Body = bodyBuilder.ToMessageBody();
|
||||
|
||||
using var client = new SmtpClient();
|
||||
await client.ConnectAsync(_configuration.Server, _configuration.Port, _configuration.UseSsl);
|
||||
await client.AuthenticateAsync(_configuration.Username, _configuration.Password);
|
||||
await client.SendAsync(emailMessage);
|
||||
await client.DisconnectAsync(true);
|
||||
}
|
||||
|
||||
private static string _ConvertHtmlToPlainText(string html)
|
||||
{
|
||||
// Remove style tags and their contents
|
||||
html = System.Text.RegularExpressions.Regex.Replace(html, "<style[^>]*>.*?</style>", "",
|
||||
System.Text.RegularExpressions.RegexOptions.Singleline);
|
||||
|
||||
// Replace header tags with text + newlines
|
||||
html = System.Text.RegularExpressions.Regex.Replace(html, "<h[1-6][^>]*>(.*?)</h[1-6]>", "$1\n\n",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||
|
||||
// Replace line breaks
|
||||
html = html.Replace("<br>", "\n").Replace("<br/>", "\n").Replace("<br />", "\n");
|
||||
|
||||
// Remove all remaining HTML tags
|
||||
html = System.Text.RegularExpressions.Regex.Replace(html, "<[^>]+>", "");
|
||||
|
||||
// Decode HTML entities
|
||||
html = System.Net.WebUtility.HtmlDecode(html);
|
||||
|
||||
// Remove excess whitespace
|
||||
html = System.Text.RegularExpressions.Regex.Replace(html, @"\s+", " ").Trim();
|
||||
|
||||
return html;
|
||||
await _client.SendEmailAsync(
|
||||
new SendEmailRequest()
|
||||
{
|
||||
Email = new EmailMessage()
|
||||
{
|
||||
ToName = recipientName,
|
||||
ToAddress = recipientEmail,
|
||||
Subject = subject,
|
||||
Body = htmlBody
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public async Task SendTemplatedEmailAsync<TComponent, TModel>(string? recipientName, string recipientEmail,
|
||||
@ -94,8 +58,7 @@ public class EmailService
|
||||
try
|
||||
{
|
||||
var htmlBody = await _viewRenderer.RenderComponentToStringAsync<TComponent, TModel>(model);
|
||||
var fallbackTextBody = _ConvertHtmlToPlainText(htmlBody);
|
||||
await SendEmailAsync(recipientName, recipientEmail, subject, fallbackTextBody, htmlBody);
|
||||
await SendEmailAsync(recipientName, recipientEmail, subject, htmlBody);
|
||||
}
|
||||
catch (Exception err)
|
||||
{
|
||||
|
@ -1,6 +1,7 @@
|
||||
using DysonNetwork.Pass;
|
||||
using DysonNetwork.Pass.Account;
|
||||
using DysonNetwork.Pass.Startup;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@ -12,6 +13,7 @@ builder.ConfigureAppKestrel();
|
||||
builder.Services.AddAppMetrics();
|
||||
|
||||
// Add application services
|
||||
builder.Services.AddEtcdService(builder.Configuration);
|
||||
builder.Services.AddAppServices(builder.Configuration);
|
||||
builder.Services.AddAppRateLimiting();
|
||||
builder.Services.AddAppAuthentication();
|
||||
@ -26,6 +28,8 @@ builder.Services.AddAppBusinessServices(builder.Configuration);
|
||||
// Add scheduled jobs
|
||||
builder.Services.AddAppScheduledJobs();
|
||||
|
||||
builder.Services.AddHostedService<ServiceRegistrationHostedService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Run database migrations
|
||||
|
@ -0,0 +1,56 @@
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DysonNetwork.Pass.Startup;
|
||||
|
||||
public class ServiceRegistrationHostedService : IHostedService
|
||||
{
|
||||
private readonly ServiceRegistry _serviceRegistry;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<ServiceRegistrationHostedService> _logger;
|
||||
|
||||
public ServiceRegistrationHostedService(
|
||||
ServiceRegistry serviceRegistry,
|
||||
IConfiguration configuration,
|
||||
ILogger<ServiceRegistrationHostedService> logger)
|
||||
{
|
||||
_serviceRegistry = serviceRegistry;
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var serviceName = "DysonNetwork.Pass"; // Preset service name
|
||||
var serviceUrl = _configuration["Service:Url"];
|
||||
|
||||
if (string.IsNullOrEmpty(serviceName) || string.IsNullOrEmpty(serviceUrl))
|
||||
{
|
||||
_logger.LogWarning("Service name or URL not configured. Skipping Etcd registration.");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Registering service {ServiceName} at {ServiceUrl} with Etcd.", serviceName, serviceUrl);
|
||||
try
|
||||
{
|
||||
await _serviceRegistry.RegisterService(serviceName, serviceUrl);
|
||||
_logger.LogInformation("Service {ServiceName} registered successfully.", serviceName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to register service {ServiceName} with Etcd.", serviceName);
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// The lease will expire automatically if the service stops.
|
||||
// For explicit unregistration, you would implement it here.
|
||||
_logger.LogInformation("Service registration hosted service is stopping.");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
@ -23,6 +23,8 @@ builder.Services.AddAppBusinessServices();
|
||||
// Add scheduled jobs
|
||||
builder.Services.AddAppScheduledJobs();
|
||||
|
||||
builder.Services.AddHostedService<ServiceRegistrationHostedService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Run database migrations
|
||||
@ -35,6 +37,8 @@ using (var scope = app.Services.CreateScope())
|
||||
// Configure application middleware pipeline
|
||||
app.ConfigureAppMiddleware(builder.Configuration);
|
||||
|
||||
app.UseMiddleware<DysonNetwork.Shared.Middleware.AuthMiddleware>();
|
||||
|
||||
// Configure gRPC
|
||||
app.ConfigureGrpcServices();
|
||||
|
||||
|
@ -1,9 +1,11 @@
|
||||
using System.Text.Json;
|
||||
using System.Threading.RateLimiting;
|
||||
using dotnet_etcd.interfaces;
|
||||
using DysonNetwork.Pusher.Email;
|
||||
using DysonNetwork.Pusher.Notification;
|
||||
using DysonNetwork.Pusher.Services;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using NodaTime;
|
||||
@ -42,6 +44,18 @@ public static class ServiceCollectionExtensions
|
||||
// Register gRPC services
|
||||
services.AddScoped<PusherServiceGrpc>();
|
||||
|
||||
// Register AuthService.AuthServiceClient for AuthMiddleware
|
||||
services.AddSingleton(sp =>
|
||||
{
|
||||
var etcdClient = sp.GetRequiredService<IEtcdClient>();
|
||||
var configuration = sp.GetRequiredService<IConfiguration>();
|
||||
var clientCertPath = configuration["ClientCert:Path"];
|
||||
var clientKeyPath = configuration["ClientKey:Path"];
|
||||
var clientCertPassword = configuration["ClientCert:Password"];
|
||||
|
||||
return GrpcClientHelper.CreateAuthServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword);
|
||||
});
|
||||
|
||||
// Register OIDC services
|
||||
services.AddControllers().AddJsonOptions(options =>
|
||||
{
|
||||
|
@ -0,0 +1,56 @@
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DysonNetwork.Pusher.Startup;
|
||||
|
||||
public class ServiceRegistrationHostedService : IHostedService
|
||||
{
|
||||
private readonly ServiceRegistry _serviceRegistry;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<ServiceRegistrationHostedService> _logger;
|
||||
|
||||
public ServiceRegistrationHostedService(
|
||||
ServiceRegistry serviceRegistry,
|
||||
IConfiguration configuration,
|
||||
ILogger<ServiceRegistrationHostedService> logger)
|
||||
{
|
||||
_serviceRegistry = serviceRegistry;
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var serviceName = "DysonNetwork.Pusher"; // Preset service name
|
||||
var serviceUrl = _configuration["Service:Url"];
|
||||
|
||||
if (string.IsNullOrEmpty(serviceUrl))
|
||||
{
|
||||
_logger.LogWarning("Service URL not configured. Skipping Etcd registration.");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Registering service {ServiceName} at {ServiceUrl} with Etcd.", serviceName, serviceUrl);
|
||||
try
|
||||
{
|
||||
await _serviceRegistry.RegisterService(serviceName, serviceUrl);
|
||||
_logger.LogInformation("Service {ServiceName} registered successfully.", serviceName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to register service {ServiceName} with Etcd.", serviceName);
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// The lease will expire automatically if the service stops.
|
||||
// For explicit unregistration, you would implement it here.
|
||||
_logger.LogInformation("Service registration hosted service is stopping.");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
22
DysonNetwork.Shared/Auth/AuthConstants.cs
Normal file
22
DysonNetwork.Shared/Auth/AuthConstants.cs
Normal file
@ -0,0 +1,22 @@
|
||||
namespace DysonNetwork.Shared.Auth;
|
||||
|
||||
public static class AuthConstants
|
||||
{
|
||||
public const string SchemeName = "DysonToken";
|
||||
public const string TokenQueryParamName = "tk";
|
||||
public const string CookieTokenName = "AuthToken";
|
||||
}
|
||||
|
||||
public enum TokenType
|
||||
{
|
||||
AuthKey,
|
||||
ApiKey,
|
||||
OidcKey,
|
||||
Unknown
|
||||
}
|
||||
|
||||
public class TokenInfo
|
||||
{
|
||||
public string Token { get; set; } = string.Empty;
|
||||
public TokenType Type { get; set; } = TokenType.Unknown;
|
||||
}
|
@ -7,6 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="dotnet-etcd" Version="8.0.1" />
|
||||
<PackageReference Include="Google.Api.CommonProtos" Version="2.17.0"/>
|
||||
<PackageReference Include="Google.Protobuf" Version="3.31.1" />
|
||||
<PackageReference Include="Google.Protobuf.Tools" Version="3.31.1" />
|
||||
@ -17,6 +18,8 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.7"/>
|
||||
<PackageReference Include="NetTopologySuite" Version="2.6.0"/>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
|
||||
@ -24,6 +27,7 @@
|
||||
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0"/>
|
||||
<PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2"/>
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.41"/>
|
||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
107
DysonNetwork.Shared/Middleware/AuthMiddleware.cs
Normal file
107
DysonNetwork.Shared/Middleware/AuthMiddleware.cs
Normal file
@ -0,0 +1,107 @@
|
||||
using Grpc.Core;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using System.Threading.Tasks;
|
||||
using DysonNetwork.Shared.Auth;
|
||||
|
||||
namespace DysonNetwork.Shared.Middleware;
|
||||
|
||||
public class AuthMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<AuthMiddleware> _logger;
|
||||
|
||||
public AuthMiddleware(RequestDelegate next, ILogger<AuthMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context, AuthService.AuthServiceClient authServiceClient)
|
||||
{
|
||||
var tokenInfo = _ExtractToken(context.Request);
|
||||
|
||||
if (tokenInfo == null || string.IsNullOrEmpty(tokenInfo.Token))
|
||||
{
|
||||
await _next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var authSession = await authServiceClient.AuthenticateAsync(new AuthenticateRequest { Token = tokenInfo.Token });
|
||||
context.Items["AuthSession"] = authSession;
|
||||
context.Items["CurrentTokenType"] = tokenInfo.Type.ToString();
|
||||
// Assuming AuthSession contains Account information or can be retrieved
|
||||
// context.Items["CurrentUser"] = authSession.Account; // You might need to fetch Account separately if not embedded
|
||||
}
|
||||
catch (RpcException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Authentication failed for token: {Token}", tokenInfo.Token);
|
||||
// Optionally, you can return an unauthorized response here
|
||||
// context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
// return;
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
|
||||
private TokenInfo? _ExtractToken(HttpRequest request)
|
||||
{
|
||||
// Check for token in query parameters
|
||||
if (request.Query.TryGetValue(AuthConstants.TokenQueryParamName, out var queryToken))
|
||||
{
|
||||
return new TokenInfo
|
||||
{
|
||||
Token = queryToken.ToString(),
|
||||
Type = TokenType.AuthKey
|
||||
};
|
||||
}
|
||||
|
||||
// Check for token in Authorization header
|
||||
var authHeader = request.Headers["Authorization"].ToString();
|
||||
if (!string.IsNullOrEmpty(authHeader))
|
||||
{
|
||||
if (authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var token = authHeader["Bearer ".Length..].Trim();
|
||||
var parts = token.Split('.');
|
||||
|
||||
return new TokenInfo
|
||||
{
|
||||
Token = token,
|
||||
Type = parts.Length == 3 ? TokenType.OidcKey : TokenType.AuthKey
|
||||
};
|
||||
}
|
||||
else if (authHeader.StartsWith("AtField ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new TokenInfo
|
||||
{
|
||||
Token = authHeader["AtField ".Length..].Trim(),
|
||||
Type = TokenType.AuthKey
|
||||
};
|
||||
}
|
||||
else if (authHeader.StartsWith("AkField ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new TokenInfo
|
||||
{
|
||||
Token = authHeader["AkField ".Length..].Trim(),
|
||||
Type = TokenType.ApiKey
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check for token in cookies
|
||||
if (request.Cookies.TryGetValue(AuthConstants.CookieTokenName, out var cookieToken))
|
||||
{
|
||||
return new TokenInfo
|
||||
{
|
||||
Token = cookieToken,
|
||||
Type = cookieToken.Count(c => c == '.') == 2 ? TokenType.OidcKey : TokenType.AuthKey
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
104
DysonNetwork.Shared/Proto/GrpcClientHelper.cs
Normal file
104
DysonNetwork.Shared/Proto/GrpcClientHelper.cs
Normal file
@ -0,0 +1,104 @@
|
||||
using Grpc.Net.Client;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Grpc.Core;
|
||||
using dotnet_etcd.interfaces;
|
||||
|
||||
namespace DysonNetwork.Shared.Proto;
|
||||
|
||||
public static class GrpcClientHelper
|
||||
{
|
||||
private static CallInvoker CreateCallInvoker(
|
||||
string url,
|
||||
string clientCertPath,
|
||||
string clientKeyPath,
|
||||
string? clientCertPassword = null
|
||||
)
|
||||
{
|
||||
var handler = new HttpClientHandler();
|
||||
handler.ClientCertificates.Add(
|
||||
clientCertPassword is null ?
|
||||
X509Certificate2.CreateFromPemFile(clientCertPath, clientKeyPath) :
|
||||
X509Certificate2.CreateFromEncryptedPemFile(clientCertPath, clientCertPassword, clientKeyPath)
|
||||
);
|
||||
return GrpcChannel.ForAddress(url, new GrpcChannelOptions { HttpHandler = handler }).CreateCallInvoker();
|
||||
}
|
||||
|
||||
private static async Task<string> GetServiceUrlFromEtcd(IEtcdClient etcdClient, string serviceName)
|
||||
{
|
||||
var response = await etcdClient.GetAsync($"/services/{serviceName}");
|
||||
if (response.Kvs.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Service '{serviceName}' not found in Etcd.");
|
||||
}
|
||||
return response.Kvs[0].Value.ToStringUtf8();
|
||||
}
|
||||
|
||||
public static AccountService.AccountServiceClient CreateAccountServiceClient(
|
||||
string url,
|
||||
string clientCertPath,
|
||||
string clientKeyPath,
|
||||
string? clientCertPassword = null
|
||||
)
|
||||
{
|
||||
return new AccountService.AccountServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
||||
clientCertPassword));
|
||||
}
|
||||
|
||||
public static async Task<AccountService.AccountServiceClient> CreateAccountServiceClient(
|
||||
IEtcdClient etcdClient,
|
||||
string clientCertPath,
|
||||
string clientKeyPath,
|
||||
string? clientCertPassword = null
|
||||
)
|
||||
{
|
||||
var url = await GetServiceUrlFromEtcd(etcdClient, "AccountService");
|
||||
return new AccountService.AccountServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
||||
clientCertPassword));
|
||||
}
|
||||
|
||||
public static AuthService.AuthServiceClient CreateAuthServiceClient(
|
||||
string url,
|
||||
string clientCertPath,
|
||||
string clientKeyPath,
|
||||
string? clientCertPassword = null
|
||||
)
|
||||
{
|
||||
return new AuthService.AuthServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
||||
clientCertPassword));
|
||||
}
|
||||
|
||||
public static async Task<AuthService.AuthServiceClient> CreateAuthServiceClient(
|
||||
IEtcdClient etcdClient,
|
||||
string clientCertPath,
|
||||
string clientKeyPath,
|
||||
string? clientCertPassword = null
|
||||
)
|
||||
{
|
||||
var url = await GetServiceUrlFromEtcd(etcdClient, "AuthService");
|
||||
return new AuthService.AuthServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
||||
clientCertPassword));
|
||||
}
|
||||
|
||||
public static PusherService.PusherServiceClient CreatePusherServiceClient(
|
||||
string url,
|
||||
string clientCertPath,
|
||||
string clientKeyPath,
|
||||
string? clientCertPassword = null
|
||||
)
|
||||
{
|
||||
return new PusherService.PusherServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
||||
clientCertPassword));
|
||||
}
|
||||
|
||||
public static async Task<PusherService.PusherServiceClient> CreatePusherServiceClient(
|
||||
IEtcdClient etcdClient,
|
||||
string clientCertPath,
|
||||
string clientKeyPath,
|
||||
string? clientCertPassword = null
|
||||
)
|
||||
{
|
||||
var url = await GetServiceUrlFromEtcd(etcdClient, "PusherService");
|
||||
return new PusherService.PusherServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
||||
clientCertPassword));
|
||||
}
|
||||
}
|
28
DysonNetwork.Shared/Registry/ServiceRegistry.cs
Normal file
28
DysonNetwork.Shared/Registry/ServiceRegistry.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using System.Text;
|
||||
using dotnet_etcd.interfaces;
|
||||
using Etcdserverpb;
|
||||
using Google.Protobuf;
|
||||
|
||||
namespace DysonNetwork.Shared.Registry;
|
||||
|
||||
public class ServiceRegistry(IEtcdClient etcd)
|
||||
{
|
||||
public async Task RegisterService(string serviceName, string serviceUrl, long leaseTtlSeconds = 60)
|
||||
{
|
||||
var key = $"/services/{serviceName}";
|
||||
var leaseResponse = await etcd.LeaseGrantAsync(new LeaseGrantRequest { TTL = leaseTtlSeconds });
|
||||
await etcd.PutAsync(new PutRequest
|
||||
{
|
||||
Key = ByteString.CopyFrom(key, Encoding.UTF8),
|
||||
Value = ByteString.CopyFrom(serviceUrl, Encoding.UTF8),
|
||||
Lease = leaseResponse.ID
|
||||
});
|
||||
await etcd.LeaseKeepAlive(leaseResponse.ID, CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task UnregisterService(string serviceName)
|
||||
{
|
||||
var key = $"/services/{serviceName}";
|
||||
await etcd.DeleteAsync(key);
|
||||
}
|
||||
}
|
23
DysonNetwork.Shared/Registry/Startup.cs
Normal file
23
DysonNetwork.Shared/Registry/Startup.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using dotnet_etcd.DependencyInjection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace DysonNetwork.Shared.Registry;
|
||||
|
||||
public static class EtcdStartup
|
||||
{
|
||||
public static IServiceCollection AddEtcdService(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration
|
||||
)
|
||||
{
|
||||
services.AddEtcdClient(options =>
|
||||
{
|
||||
options.ConnectionString = configuration.GetConnectionString("Etcd");
|
||||
options.UseInsecureChannel = configuration.GetValue<bool>("Etcd:Insecure");
|
||||
});
|
||||
services.AddSingleton<ServiceRegistry>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
@ -10,7 +10,8 @@
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"App": "Host=localhost;Port=5432;Database=dyson_network;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60",
|
||||
"FastRetrieve": "localhost:6379"
|
||||
"FastRetrieve": "localhost:6379",
|
||||
"Etcd": "localhost:2379"
|
||||
},
|
||||
"Authentication": {
|
||||
"Schemes": {
|
||||
@ -125,5 +126,8 @@
|
||||
"KnownProxies": [
|
||||
"127.0.0.1",
|
||||
"::1"
|
||||
]
|
||||
],
|
||||
"Etcd": {
|
||||
"Insecure": true
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,8 @@
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ABodyBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fc5c8aba04a29d49c65d772c9ffcd93ac7eb38ccbb49a5f506518a0b9bdcaa75_003FBodyBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ABucketArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd515fb889657fcdcace3fed90735057b458ff9e0bb60bded7c8fe8b3a4673c_003FBucketArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ABucketArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fd515fb889657fcdcace3fed90735057b458ff9e0bb60bded7c8fe8b3a4673c_003FBucketArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AByteString_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F331aca3f6f414013b09964063341351379060_003F2e_003F1935b2a7_003FByteString_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACallInvoker_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F85d5cf7bc00d9afe6109255f942125d252e7d3bf3d33f44c445162ab59e52b_003FCallInvoker_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AChapterData_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffef366b36a224d469ff150d30f9a866d23c00_003Fe6_003F64a6c0f7_003FChapterData_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AClaim_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa7fdc52b6e574ae7b9822133be91162a15800_003Ff7_003Feebffd8d_003FClaim_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConnectionMultiplexer_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F2ed0e2f073b1d77b98dadb822da09ee8a9dfb91bf29bf2bbaecb8750d7e74cc9_003FConnectionMultiplexer_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
@ -39,8 +41,10 @@
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExifTag_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fef3339e864a448e2b1ec6fa7bbf4c6661fee00_003F5c_003F8ed75f18_003FExifTag_002Ecs_002Fz_003A2_002D1/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFileResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0b5acdd962e549369896cece0026e556214600_003F8c_003F9f6e3f4f_003FFileResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AForwardedHeaders_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fcfe5737f9bb84738979cbfedd11822a8ea00_003F50_003F9a335f87_003FForwardedHeaders_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpRequestHeaders_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb904f9896c4049fabd596decf1be9c381dc400_003F32_003F906beb77_003FHttpRequestHeaders_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpUtility_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F95cd5fa21c574d4087dec626d8227d77be00_003F08_003Fdd41228e_003FHttpUtility_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIConfiguration_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbb55221b2bd14b31a20b0d8bdcc7ff457328_003F19_003F707d23be_003FIConfiguration_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIEtcdClient_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F866376757aa64634b820c41d3553727886400_003Fbb_003F0fd3f8d7_003FIEtcdClient_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIHtmlString_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F95cd5fa21c574d4087dec626d8227d77be00_003Ff1_003F3a8957fa_003FIHtmlString_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AImageFile_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa932cb9090ed48088111ae919dcdd9021ba00_003F71_003F0a804432_003FImageFile_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AImage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fdaa8d9c408cd4b4286bbef7e35f1a42e31c00_003F9f_003Fc5bde8be_003FImage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
|
Reference in New Issue
Block a user