Fix account and backup admin settings flows
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
using JobTrackerApi.Data;
|
||||
using JobTrackerApi.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobTrackerApi.Services;
|
||||
|
||||
public sealed record EmailSettingsSnapshot(
|
||||
bool Enabled,
|
||||
string Host,
|
||||
int Port,
|
||||
string User,
|
||||
string Password,
|
||||
string From,
|
||||
string FromName,
|
||||
bool EnableSsl,
|
||||
int TimeoutMs,
|
||||
bool UsesOverrides,
|
||||
bool HasPassword);
|
||||
|
||||
public sealed record EmailSettingsAdminDto(
|
||||
bool Enabled,
|
||||
string Host,
|
||||
int Port,
|
||||
string User,
|
||||
string From,
|
||||
string FromName,
|
||||
bool EnableSsl,
|
||||
int TimeoutMs,
|
||||
bool UsesOverrides,
|
||||
bool HasPassword);
|
||||
|
||||
public sealed record UpdateEmailSettingsRequest(
|
||||
bool Enabled,
|
||||
string? Host,
|
||||
int Port,
|
||||
string? User,
|
||||
string? Password,
|
||||
bool ClearPassword,
|
||||
string? From,
|
||||
string? FromName,
|
||||
bool EnableSsl,
|
||||
int TimeoutMs);
|
||||
|
||||
public interface IEmailSettingsResolver
|
||||
{
|
||||
Task<EmailSettingsSnapshot> GetSnapshotAsync(CancellationToken cancellationToken = default);
|
||||
Task<EmailSettingsAdminDto> GetAdminDtoAsync(CancellationToken cancellationToken = default);
|
||||
Task<EmailSettingsAdminDto> UpdateAsync(UpdateEmailSettingsRequest request, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed class EmailSettingsResolver : IEmailSettingsResolver
|
||||
{
|
||||
private readonly IConfiguration _cfg;
|
||||
private readonly JobTrackerContext _db;
|
||||
|
||||
public EmailSettingsResolver(IConfiguration cfg, JobTrackerContext db)
|
||||
{
|
||||
_cfg = cfg;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<EmailSettingsSnapshot> GetSnapshotAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var overrides = await _db.SystemEmailSettings.AsNoTracking().FirstOrDefaultAsync(x => x.Id == 1, cancellationToken);
|
||||
return Merge(overrides);
|
||||
}
|
||||
|
||||
public async Task<EmailSettingsAdminDto> GetAdminDtoAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var snapshot = await GetSnapshotAsync(cancellationToken);
|
||||
return new EmailSettingsAdminDto(
|
||||
Enabled: snapshot.Enabled,
|
||||
Host: snapshot.Host,
|
||||
Port: snapshot.Port,
|
||||
User: snapshot.User,
|
||||
From: snapshot.From,
|
||||
FromName: snapshot.FromName,
|
||||
EnableSsl: snapshot.EnableSsl,
|
||||
TimeoutMs: snapshot.TimeoutMs,
|
||||
UsesOverrides: snapshot.UsesOverrides,
|
||||
HasPassword: snapshot.HasPassword);
|
||||
}
|
||||
|
||||
public async Task<EmailSettingsAdminDto> UpdateAsync(UpdateEmailSettingsRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var settings = await _db.SystemEmailSettings.FirstOrDefaultAsync(x => x.Id == 1, cancellationToken);
|
||||
if (settings is null)
|
||||
{
|
||||
settings = new SystemEmailSettings { Id = 1 };
|
||||
_db.SystemEmailSettings.Add(settings);
|
||||
}
|
||||
|
||||
settings.Enabled = request.Enabled;
|
||||
settings.SmtpHost = TrimOrNull(request.Host);
|
||||
settings.SmtpPort = request.Port <= 0 ? 587 : request.Port;
|
||||
settings.SmtpUser = TrimOrNull(request.User);
|
||||
settings.From = TrimOrNull(request.From);
|
||||
settings.FromName = TrimOrNull(request.FromName);
|
||||
settings.SmtpEnableSsl = request.EnableSsl;
|
||||
settings.SmtpTimeoutMs = request.TimeoutMs <= 0 ? 15000 : request.TimeoutMs;
|
||||
|
||||
if (request.ClearPassword)
|
||||
{
|
||||
settings.SmtpPassword = null;
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(request.Password))
|
||||
{
|
||||
settings.SmtpPassword = request.Password.Trim();
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
return await GetAdminDtoAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private EmailSettingsSnapshot Merge(SystemEmailSettings? overrides)
|
||||
{
|
||||
var host = overrides?.SmtpHost ?? (_cfg["Email:SmtpHost"] ?? string.Empty).Trim();
|
||||
var user = overrides?.SmtpUser ?? (_cfg["Email:SmtpUser"] ?? string.Empty).Trim();
|
||||
var password = overrides?.SmtpPassword ?? (_cfg["Email:SmtpPassword"] ?? string.Empty).Trim();
|
||||
var from = overrides?.From ?? (_cfg["Email:From"] ?? user).Trim();
|
||||
var fromName = overrides?.FromName ?? (_cfg["Email:FromName"] ?? "Jobbjakt").Trim();
|
||||
var port = overrides?.SmtpPort ?? _cfg.GetValue("Email:SmtpPort", 587);
|
||||
if (port <= 0) port = 587;
|
||||
var enableSsl = overrides?.SmtpEnableSsl ?? _cfg.GetValue("Email:SmtpEnableSsl", true);
|
||||
var timeoutMs = overrides?.SmtpTimeoutMs ?? _cfg.GetValue("Email:SmtpTimeoutMs", 15000);
|
||||
if (timeoutMs <= 0) timeoutMs = 15000;
|
||||
var enabled = overrides?.Enabled ?? _cfg.GetValue("Email:Enabled", false);
|
||||
var usesOverrides = overrides is not null;
|
||||
|
||||
return new EmailSettingsSnapshot(
|
||||
Enabled: enabled,
|
||||
Host: host,
|
||||
Port: port,
|
||||
User: user,
|
||||
Password: password,
|
||||
From: from,
|
||||
FromName: fromName,
|
||||
EnableSsl: enableSsl,
|
||||
TimeoutMs: timeoutMs,
|
||||
UsesOverrides: usesOverrides,
|
||||
HasPassword: !string.IsNullOrWhiteSpace(password));
|
||||
}
|
||||
|
||||
private static string? TrimOrNull(string? value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.IdentityModel.Protocols;
|
||||
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
@@ -25,6 +26,12 @@ public sealed class GoogleTokenValidator : IGoogleTokenValidator
|
||||
new OpenIdConnectConfigurationRetriever());
|
||||
}
|
||||
|
||||
public GoogleTokenValidator(IConfiguration cfg, IConfigurationManager<OpenIdConnectConfiguration> configManager)
|
||||
{
|
||||
_cfg = cfg;
|
||||
_configManager = configManager;
|
||||
}
|
||||
|
||||
public async Task<GoogleTokenPrincipal> ValidateAsync(string idToken, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var audience = (_cfg["Auth:GoogleClientId"] ?? "").Trim();
|
||||
@@ -47,7 +54,9 @@ public sealed class GoogleTokenValidator : IGoogleTokenValidator
|
||||
ClockSkew = TimeSpan.FromMinutes(2),
|
||||
}, out _);
|
||||
|
||||
var subject = principal.FindFirst("sub")?.Value?.Trim();
|
||||
var subject = principal.FindFirst("sub")?.Value?.Trim()
|
||||
?? principal.FindFirst(JwtRegisteredClaimNames.Sub)?.Value?.Trim()
|
||||
?? principal.FindFirst(ClaimTypes.NameIdentifier)?.Value?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(subject))
|
||||
{
|
||||
throw new InvalidOperationException("Google token is missing a subject.");
|
||||
@@ -55,11 +64,11 @@ public sealed class GoogleTokenValidator : IGoogleTokenValidator
|
||||
|
||||
return new GoogleTokenPrincipal(
|
||||
Subject: subject,
|
||||
Email: principal.FindFirst("email")?.Value?.Trim(),
|
||||
Email: principal.FindFirst("email")?.Value?.Trim() ?? principal.FindFirst(ClaimTypes.Email)?.Value?.Trim(),
|
||||
EmailVerified: IsEmailVerified(principal),
|
||||
GivenName: principal.FindFirst("given_name")?.Value?.Trim(),
|
||||
FamilyName: principal.FindFirst("family_name")?.Value?.Trim(),
|
||||
Name: principal.FindFirst("name")?.Value?.Trim()
|
||||
GivenName: principal.FindFirst("given_name")?.Value?.Trim() ?? principal.FindFirst(ClaimTypes.GivenName)?.Value?.Trim(),
|
||||
FamilyName: principal.FindFirst("family_name")?.Value?.Trim() ?? principal.FindFirst(ClaimTypes.Surname)?.Value?.Trim(),
|
||||
Name: principal.FindFirst("name")?.Value?.Trim() ?? principal.Identity?.Name?.Trim()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,34 +10,31 @@ public interface IAppEmailSender
|
||||
|
||||
public sealed class SmtpEmailSender : IAppEmailSender
|
||||
{
|
||||
private readonly IConfiguration _cfg;
|
||||
private readonly IEmailSettingsResolver _settings;
|
||||
private readonly ILogger<SmtpEmailSender> _logger;
|
||||
|
||||
public SmtpEmailSender(IConfiguration cfg, ILogger<SmtpEmailSender> logger)
|
||||
public SmtpEmailSender(IEmailSettingsResolver settings, ILogger<SmtpEmailSender> logger)
|
||||
{
|
||||
_cfg = cfg;
|
||||
_settings = settings;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task SendAsync(string toEmail, string subject, string bodyText, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var host = (_cfg["Email:SmtpHost"] ?? "").Trim();
|
||||
var user = (_cfg["Email:SmtpUser"] ?? "").Trim();
|
||||
var pass = (_cfg["Email:SmtpPassword"] ?? "").Trim();
|
||||
var from = (_cfg["Email:From"] ?? user).Trim();
|
||||
var fromName = (_cfg["Email:FromName"] ?? "Jobbjakt").Trim();
|
||||
var snapshot = await _settings.GetSnapshotAsync(cancellationToken);
|
||||
var host = snapshot.Host;
|
||||
var user = snapshot.User;
|
||||
var pass = snapshot.Password;
|
||||
var from = snapshot.From;
|
||||
var fromName = snapshot.FromName;
|
||||
var port = snapshot.Port;
|
||||
var enableSsl = snapshot.EnableSsl;
|
||||
var timeoutMs = snapshot.TimeoutMs;
|
||||
|
||||
var port = _cfg.GetValue("Email:SmtpPort", 587);
|
||||
if (port <= 0) port = 587;
|
||||
|
||||
var enableSsl = _cfg.GetValue("Email:SmtpEnableSsl", true);
|
||||
var timeoutMs = _cfg.GetValue("Email:SmtpTimeoutMs", 15000);
|
||||
if (timeoutMs <= 0) timeoutMs = 15000;
|
||||
|
||||
var enabled = _cfg.GetValue("Email:Enabled", false);
|
||||
var enabled = snapshot.Enabled;
|
||||
if (!enabled)
|
||||
{
|
||||
_logger.LogWarning("Email sending is disabled (Email:Enabled=false). Suppressed email to {To} subject={Subject}", toEmail, subject);
|
||||
_logger.LogWarning("Email sending is disabled. Suppressed email to {To} subject={Subject}", toEmail, subject);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -63,7 +60,6 @@ public sealed class SmtpEmailSender : IAppEmailSender
|
||||
smtp.Credentials = new NetworkCredential(user, pass);
|
||||
}
|
||||
|
||||
// SmtpClient has no CancellationToken support; run on thread pool.
|
||||
await Task.Run(() => smtp.Send(msg), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user